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, ",,,,,,,\n"); + } + + return measurement; +} + +FileStats updateStats(FileStats stats, TestMeasurement measurement, int referenceParseTime, int recoverySuccessLimit) { + stats.totalParses += 1; + + int ratio = measurement.duration/referenceParseTime; + int parseTimeRatio = ratio == 0 ? 0 : round(log2(ratio)); + + switch (measurement) { + case successfulParse(): { + print("."); + stats.successfulParses += 1; + } + case recovered(errorCount=errorCount, errorSize=errorSize): { + stats.parseTimeRatios = increment(stats.parseTimeRatios, parseTimeRatio); + stats.errorCounts = increment(stats.errorCounts, errorCount); + stats.errorSizes = increment(stats.errorSizes, errorSize); + if (errorSize <= recoverySuccessLimit) { + print("+"); + stats.successfulRecoveries += 1; + } else { + print("-"); + stats.failedRecoveries += 1; + } + } + case successfulDisambiguation(): { + stats.parseTimeRatios = increment(stats.parseTimeRatios, parseTimeRatio); + print("&"); + stats.successfulDisambiguations += 1; + } + case parseError(): { + stats.parseTimeRatios = increment(stats.parseTimeRatios, parseTimeRatio); + print("?"); + stats.parseErrors += 1; + } + } + + if (measurement.duration > referenceParseTime*10) { + print("!"); + stats.slowParses += 1; + } + + return stats; +} + +FileStats mergeFileStats(FileStats stats1, FileStats stats2) { + return fileStats( + totalParses = stats1.totalParses + stats2.totalParses, + successfulParses = stats1.successfulParses + stats2.successfulParses, + successfulRecoveries = stats1.successfulRecoveries + stats2.successfulRecoveries, + successfulDisambiguations = stats1.successfulDisambiguations + stats2.successfulDisambiguations, + failedRecoveries = stats1.failedRecoveries + stats2.failedRecoveries, + parseErrors = stats1.parseErrors + stats2.parseErrors, + slowParses = stats1.slowParses + stats2.slowParses, + parseTimeRatios = mergeFrequencyTables(stats1.parseTimeRatios, stats2.parseTimeRatios), + errorCounts = mergeFrequencyTables(stats1.errorCounts, stats2.errorCounts), + errorSizes = mergeFrequencyTables(stats1.errorSizes, stats2.errorSizes) + ); +} + +FrequencyTable increment(FrequencyTable frequencyTable, int val) { + if (val in frequencyTable) { + frequencyTable[val] += 1; + } else { + frequencyTable[val] = 1; + } + + return frequencyTable; +} + +TestStats consolidateStats(TestStats cumulativeStats, FileStats fileStats) { + int totalFailed = fileStats.totalParses - fileStats.successfulParses; + + cumulativeStats.successfulParses = increment(cumulativeStats.successfulParses, percentage(fileStats.successfulParses, fileStats.totalParses)); + cumulativeStats.successfulRecoveries = increment(cumulativeStats.successfulRecoveries, percentage(fileStats.successfulRecoveries, totalFailed)); + cumulativeStats.successfulDisambiguations = increment(cumulativeStats.successfulDisambiguations, percentage(fileStats.successfulDisambiguations, totalFailed)); + cumulativeStats.failedRecoveries = increment(cumulativeStats.failedRecoveries, percentage(fileStats.failedRecoveries, totalFailed)); + cumulativeStats.parseErrors = increment(cumulativeStats.parseErrors, percentage(fileStats.parseErrors, totalFailed)); + cumulativeStats.slowParses = increment(cumulativeStats.slowParses, percentage(fileStats.slowParses, totalFailed)); + cumulativeStats.parseTimeRatios = mergeFrequencyTables(cumulativeStats.parseTimeRatios, fileStats.parseTimeRatios); + cumulativeStats.errorCounts = mergeFrequencyTables(cumulativeStats.errorCounts, fileStats.errorCounts); + cumulativeStats.errorSizes = mergeFrequencyTables(cumulativeStats.errorSizes, fileStats.errorSizes); + + cumulativeStats.filesTested += 1; + cumulativeStats.testCount += fileStats.totalParses; + + return cumulativeStats; +} + +map[int,int] mergeFrequencyTables(map[int,int] hist1, map[int,int] hist2) { + for (int pct <- hist2) { + if (pct in hist1) { + hist1[pct] += hist2[pct]; + } else { + hist1[pct] = hist2[pct]; + } + } + + return hist1; +} + +TestStats mergeStats(TestStats stats, TestStats stats2) { + stats.filesTested += stats2.filesTested; + stats.testCount += stats2.testCount; + stats.successfulParses = mergeFrequencyTables(stats.successfulParses, stats2.successfulParses); + stats.successfulRecoveries = mergeFrequencyTables(stats.successfulRecoveries, stats2.successfulRecoveries); + stats.successfulDisambiguations = mergeFrequencyTables(stats.successfulDisambiguations, stats2.successfulDisambiguations); + stats.failedRecoveries = mergeFrequencyTables(stats.failedRecoveries, stats2.failedRecoveries); + stats.parseErrors = mergeFrequencyTables(stats.parseErrors, stats2.parseErrors); + stats.slowParses = mergeFrequencyTables(stats.slowParses, stats2.slowParses); + stats.parseTimeRatios = mergeFrequencyTables(stats.parseTimeRatios, stats2.parseTimeRatios); + stats.errorCounts = mergeFrequencyTables(stats.errorCounts, stats2.errorCounts); + stats.errorSizes = mergeFrequencyTables(stats.errorSizes, stats2.errorSizes); + + return stats; +} + +FileStats testSingleCharDeletions(&T (value input, loc origin) standardParser, &T (value input, loc origin) recoveryParser, loc source, str input, int referenceParseTime, int recoverySuccessLimit, int begin=0, int end=-1, loc statFile=|unknown:///|) { + FileStats stats = fileStats(); + int len = size(input); + int i = begin; + + while (i < len && (end == -1 || i<=end)) { + str modifiedInput = substring(input, 0, i) + substring(input, i+1); + source.query = "deletedChar="; + TestMeasurement measurement = testRecovery(standardParser, recoveryParser, modifiedInput, source, statFile, referenceParseTime); + stats = updateStats(stats, measurement, referenceParseTime, recoverySuccessLimit); + if (i < len && substring(input, i, i+1) == "\n") { + println(); + } + i = i+1; + } + + return stats; +} + +FileStats testDeleteUntilEol(&T (value input, loc origin) standardParser, &T (value input, loc origin) recoveryParser, loc source, str input, int referenceParseTime, int recoverySuccessLimit, int begin=0, int end=-1, loc statFile=|unknown:///|) { + FileStats stats = fileStats(); + int lineStart = begin; + list[int] lineEndings = findAll(input, "\n"); + + int line = 1; + for (int lineEnd <- lineEndings) { + lineLength = lineEnd - lineStart; + for (int pos <- [lineStart..lineEnd]) { + // Check boundaries (only used for quick bug testing) + if (end != -1 && end < pos) { + return stats; + } + if (pos < begin) { + continue; + } + modifiedInput = substring(input, 0, pos) + substring(input, lineEnd); + source.query = "deletedUntilEol=::"; + TestMeasurement measurement = testRecovery(standardParser, recoveryParser, modifiedInput, source, statFile, referenceParseTime); + stats = updateStats(stats, measurement, referenceParseTime, recoverySuccessLimit); + } + lineStart = lineEnd+1; + println(); + line = line+1; + } + + return stats; +} + +private int percentage(int number, int total) { + return total == 0 ? 0 : (100*number)/total; +} + +int statLabelWidth = 40; +int statFieldWidth = 10; + + +void printFileStats(FileStats fileStats) { + void printStat(str label, int stat, int total, bool prints=true) { + int pct = total == 0 ? 0 : stat*100/total; + print(left(label + ":", statLabelWidth)); + str pctStr = prints ? " (%)" : ""; + println(left("", statFieldWidth)); + } + + println(); + printStat("Total parses", fileStats.totalParses, fileStats.totalParses); + printStat("Successful parses", fileStats.successfulParses, fileStats.totalParses); + int failedParses = fileStats.totalParses - fileStats.successfulParses; + printStat("Successful recoveries", fileStats.successfulRecoveries, failedParses); + printStat("Successful disambiguations", fileStats.successfulDisambiguations, failedParses); + printStat("Failed recoveries", fileStats.failedRecoveries, failedParses); + printStat("Parse errors", fileStats.parseErrors, failedParses); + printStat("Slow parses", fileStats.slowParses, failedParses); + printFrequencyTableHeader(); + printFrequencyTableStats("Parse time ratios", fileStats.parseTimeRatios, unit = "log2(ratio)", printTotal=false); + printFrequencyTableStats("Parse error count", fileStats.errorCounts, unit="errors"); + printFrequencyTableStats("Error size", fileStats.errorSizes, unit="chars"); +} + +void printFrequencyTableHeader() { + print(left("", statLabelWidth+1)); + print(right("mean", statFieldWidth)); + print(right("median", statFieldWidth)); + print(right("95 %", statFieldWidth)); + print(right("min", statFieldWidth)); + print(right("max", statFieldWidth)); + println(right("total", statFieldWidth)); + } + +void printFrequencyTableStats(str label, FrequencyTable frequencyTable, str unit = "%", bool printTotal=true, bool ignoreZero=false) { + print(left(label + " ():", statLabelWidth)); + + int totalCount = (0 | it+frequencyTable[val] | val <- frequencyTable); + + int total = 0; + int median = 0; + int medianCount = 0; + int cumulativeCount = 0; + int ninetyFivePercentileLimit = round(totalCount * 0.95); + int ninetyFivePercentile = -1; + int minVal = 1000000000; + int maxVal = -1000000000; + + for (val <- sort(toList(frequencyTable.val))) { + minVal = min(minVal, val); + maxVal = max(maxVal, val); + + int count = frequencyTable[val]; + cumulativeCount += count; + + if (ninetyFivePercentile == -1 && cumulativeCount >= ninetyFivePercentileLimit) { + ninetyFivePercentile = val; + } + + total += val*count; + + if (!(val == 0 && ignoreZero) && count > medianCount) { + medianCount = count; + median = val; + } + } + + if (totalCount == 0) { + print("-"); + } else { + num mean = round(toReal(total)/totalCount, 0.01); + print(right("", statFieldWidth)); + print(right("", statFieldWidth)); + print(right("", statFieldWidth)); + print(right("", statFieldWidth)); + print(right("", statFieldWidth)); + println(right("", statFieldWidth)); + } +} + +void printStats(TestStats stats) { + if (stats.filesTested != 1) { + println("Files tested: "); + } + println("Total parses: "); + printFrequencyTableHeader(); + printFrequencyTableStats("Succesful parses", stats.successfulParses); + printFrequencyTableStats("Succesful recoveries", stats.successfulRecoveries); + printFrequencyTableStats("Succesful disambiguations", stats.successfulDisambiguations); + printFrequencyTableStats("Failed recoveries", stats.failedRecoveries); + printFrequencyTableStats("Parse errors", stats.parseErrors); + printFrequencyTableStats("Slow parses", stats.slowParses); + printFrequencyTableStats("Parse time ratios", stats.parseTimeRatios, unit = "log2/%", printTotal=false); + printFrequencyTableStats("Parse error counts", stats.errorCounts, unit = "errors", ignoreZero=true); + printFrequencyTableStats("Parse error sizes", stats.errorSizes, unit = "chars", ignoreZero=true); + + println(); +} + +private str syntaxLocToModuleName(loc syntaxFile) { + str path = replaceLast(substring(syntaxFile.path, 1), ".rsc", ""); + return replaceAll(path, "/", "::"); +} + +loc zippedFile(str zip, str path) { + loc res = getResource(zip); + loc zipFile = res[scheme="jar+"][path=res.path + "!/"]; + return zipFile + path; +} + +FileStats testErrorRecovery(loc syntaxFile, str topSort, loc testInput) = testErrorRecovery(syntaxFile, topSort, testInput, readFile(testInput)); + +FileStats testErrorRecovery(loc syntaxFile, str topSort, loc testInput, str input, loc statFile=|unknown:///|) { + Module \module = parse(#start[Module], syntaxFile).top; + str modName = syntaxLocToModuleName(syntaxFile); + Grammar gram = modules2grammar(modName, {\module}); + + if (sym:\start(\sort(topSort)) <- gram.starts) { + type[value] begin = type(sym, gram.rules); + standardParser = parser(begin, allowAmbiguity=true, allowRecovery=false); + recoveryParser = parser(begin, allowAmbiguity=true, allowRecovery=true); + + // Initialization run + standardParser(input, testInput); + + // Timed run + int startTime = realTime(); + standardParser(input, testInput); + int referenceParseTime = max(1, realTime() - startTime); + + recoverySuccessLimit = size(input)/4; + + println("Error recovery of () on , reference parse time: ms."); + + println(); + println("Single char deletions:"); + FileStats singleCharDeletionStats = testSingleCharDeletions(standardParser, recoveryParser, testInput, input, referenceParseTime, recoverySuccessLimit, statFile=statFile); + printFileStats(singleCharDeletionStats); + + println(); + println("Deletes until end-of-line:"); + FileStats deleteUntilEolStats = testDeleteUntilEol(standardParser, recoveryParser, testInput, input, referenceParseTime, recoverySuccessLimit, statFile=statFile); + printFileStats(deleteUntilEolStats); + + FileStats stats = mergeFileStats(singleCharDeletionStats, deleteUntilEolStats); + println(); + println("-----------------------------------------------------------"); + println("Total test stats for :"); + printFileStats(stats); + return stats; + } else { + throw "Cannot find top sort in "; + } +} + +private int fileNr = 0; +private int fromFile = 0; + +TestStats batchRecoveryTest(loc syntaxFile, str topSort, loc dir, str ext, int maxFiles, int minFileSize, int maxFileSize, int from, loc statFile) { + fileNr = 0; + fromFile = from; + + if (statFile != |unknown:///|) { + writeFile(statFile, "source,size,result,duration,ratio,disambiguationDuration,errorCount,errorSize\n"); + } + + return runBatchRecoveryTest(syntaxFile, topSort, dir, ext, maxFiles, minFileSize, maxFileSize, statFile, testStats()); +} + +TestStats runBatchRecoveryTest(loc syntaxFile, str topSort, loc dir, str ext, int maxFiles, int minFileSize, int maxFileSize, loc statFile, TestStats cumulativeStats) { + println("Batch testing in directory (maxFiles=, maxFileSize=, fromFile=)"); + for (entry <- listEntries(dir)) { + loc file = dir + entry; + if (isFile(file)) { + if (endsWith(file.path, ext)) { + str content = readFile(file); + int contentSize = size(content); + if (contentSize >= minFileSize && contentSize < maxFileSize) { + fileNr += 1; + if (fileNr < fromFile) { + println("Skipping file #: , (\< )"); + continue; + } + println("========================================================================"); + println("Testing file # ( of left)"); + FileStats fileStats = testErrorRecovery(syntaxFile, topSort, file, content, statFile=statFile); + cumulativeStats = consolidateStats(cumulativeStats, fileStats); + println(); + println("------------------------------------------------------------------------"); + println("Cumulative stats after testing :"); + printStats(cumulativeStats); + } + } + } else if (isDirectory(file)) { + cumulativeStats = runBatchRecoveryTest(syntaxFile, topSort, file, ext, maxFiles, minFileSize, maxFileSize, statFile, cumulativeStats); + } + + if (cumulativeStats.filesTested >= maxFiles) { + break; + } + } + + return cumulativeStats; +} + +bool checkRecovery(type[&T<:Tree] begin, str input, list[str] expectedErrors, bool visualize=false) { + Tree t = parser(begin, allowRecovery=true, allowAmbiguity=true)(input, |unknown:///?visualize=<"">|); + return checkErrors(t, expectedErrors); +} + +// Print a list of errors +void printErrors(list[Tree] errors) { + for (Tree error <- errors) { + println("\'\'"); + } +} + +// Check a tree contains exactly the expected error +bool checkError(Tree t, str expectedError) = checkErrors(t, [expectedError]); + +// Check if a tree contains exactly the expected errors +bool checkErrors(Tree t, list[str] expectedErrors) { + list[Tree] errors = findBestErrors(t); + if (size(errors) != size(expectedErrors)) { + println("Expected errors, found "); + printErrors(errors); + return false; + } + + for (error <- errors) { + str errorText = getErrorText(error); + if (errorText notin expectedErrors) { + println("Unexpected error: \'\'"); + println("All errors found:"); + printErrors(errors); + return false; + } + } + + return true; +} \ No newline at end of file diff --git a/src/org/rascalmpl/library/lang/rascal/tests/concrete/recovery/ToyRascal.rsc b/src/org/rascalmpl/library/lang/rascal/tests/concrete/recovery/ToyRascal.rsc new file mode 100644 index 00000000000..4a60dfb88e4 --- /dev/null +++ b/src/org/rascalmpl/library/lang/rascal/tests/concrete/recovery/ToyRascal.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::ToyRascal + +start syntax FunctionDeclaration = Signature FunctionBody; + +syntax Signature = Name Parameters; + +syntax Name = "f" | "g"; + +syntax Parameters = "(" ")"; + +syntax FunctionBody = "{" Statement* statements "}" ; + +syntax Statement + = "{" Statement+ statements "}" + | "if" "(" { Expression "," }+ ")" Statement + | "s" ";"; + +syntax Expression = "1"; + +syntax Label + = "l" ":" + | ; + +layout Layout = [ \n\r\t]*; diff --git a/src/org/rascalmpl/library/lang/rascal/tests/concrete/recovery/ToyRascalRecoveryTests.rsc b/src/org/rascalmpl/library/lang/rascal/tests/concrete/recovery/ToyRascalRecoveryTests.rsc new file mode 100644 index 00000000000..fe213811137 --- /dev/null +++ b/src/org/rascalmpl/library/lang/rascal/tests/concrete/recovery/ToyRascalRecoveryTests.rsc @@ -0,0 +1,59 @@ +/** + * 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::ToyRascalRecoveryTests + +import lang::rascal::tests::concrete::recovery::ToyRascal; + +import ParseTree; +import util::ErrorRecovery; +import IO; +import util::Maybe; + +import lang::rascal::tests::concrete::recovery::RecoveryTestSupport; + +Tree parseToyRascal(str input, bool visualize=false) { + Tree result = parser(#start[FunctionDeclaration], allowRecovery=true, allowAmbiguity=true)(input, |unknown:///?visualize=<"">|); + list[Tree] errors = findAllErrors(result); + if (errors != []) { + println("Tree has errors"); + for (error <- errors) { + println("- "); + } + + println("Best error: "); + } + + return result; +} + +test bool toyRascalOk() { + Tree t = parseToyRascal("f(){s;}"); + return !hasErrors(t); +} + +test bool toyRascalMissingOpenParen() { + Tree t = parseToyRascal("f){}", visualize=false); + return hasErrors(t) && getErrorText(findBestErrors(t)[0]) == ")"; +} + +test bool toyRascalMissingCloseParen() { + Tree t = parseToyRascal("f({}", visualize=false); + return hasErrors(t) && getErrorText(findBestErrors(t)[0]) == "("; +} + +test bool toyRascalMissingIfBody() { + Tree t = parseToyRascal("f(){if(1){}}", visualize=false); + return hasErrors(t) && getErrorText(findBestErrors(t)[0]) == "}"; +} \ No newline at end of file diff --git a/src/org/rascalmpl/library/lang/rascal/tests/concrete/recovery/bugs/DisambiguationPerformanceTest.rsc b/src/org/rascalmpl/library/lang/rascal/tests/concrete/recovery/bugs/DisambiguationPerformanceTest.rsc new file mode 100644 index 00000000000..e2fbf4a32c1 --- /dev/null +++ b/src/org/rascalmpl/library/lang/rascal/tests/concrete/recovery/bugs/DisambiguationPerformanceTest.rsc @@ -0,0 +1,28 @@ +/** +* 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::bugs::DisambiguationPerformanceTest + + +import lang::rascal::tests::concrete::recovery::RecoveryTestSupport; +import lang::rascal::\syntax::Rascal; +import ParseTree; +import IO; + +void testPerformance() { + standardParser = parser(#start[Module], allowRecovery=false, allowAmbiguity=true); + recoveryParser = parser(#start[Module], allowRecovery=true, allowAmbiguity=true); + loc source = |std:///lang/box/util/Box2Text.rsc|; + input = readFile(source); + FileStats stats = testDeleteUntilEol(standardParser, recoveryParser, source, input, 200, 100, begin=17496, end=17496); + println(""); +} diff --git a/src/org/rascalmpl/library/lang/rascal/tests/concrete/recovery/bugs/InfiniteLoopBug.rsc b/src/org/rascalmpl/library/lang/rascal/tests/concrete/recovery/bugs/InfiniteLoopBug.rsc new file mode 100644 index 00000000000..d9750305a01 --- /dev/null +++ b/src/org/rascalmpl/library/lang/rascal/tests/concrete/recovery/bugs/InfiniteLoopBug.rsc @@ -0,0 +1,32 @@ +/** +* 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::bugs::InfiniteLoopBug + +import lang::rascal::tests::concrete::recovery::RecoveryTestSupport; +import lang::rascal::\syntax::Rascal; +import ParseTree; +import IO; + +void testInfiniteLoop1() { + str input = readFile(|std:///lang/rascal/tests/concrete/recovery/bugs/InfiniteLoopInput.txt|); + p = parser(#start[Module], allowRecovery=true, allowAmbiguity=true); + println("starting parse"); + p(input, |unknown:///?visualize=false|); +} + +void testInfiniteLoop2() { + standardParser = parser(#start[Module], allowRecovery=false, allowAmbiguity=true); + recoveryParser = parser(#start[Module], allowRecovery=true, allowAmbiguity=true); + input = readFile(|std:///analysis/m3/FlowGraph.rsc|); + testDeleteUntilEol(standardParser, recoveryParser, input, 200, 100); +} diff --git a/src/org/rascalmpl/library/lang/rascal/tests/concrete/recovery/bugs/InfiniteLoopInput.txt b/src/org/rascalmpl/library/lang/rascal/tests/concrete/recovery/bugs/InfiniteLoopInput.txt new file mode 100644 index 00000000000..26359b3e558 --- /dev/null +++ b/src/org/rascalmpl/library/lang/rascal/tests/concrete/recovery/bugs/InfiniteLoopInput.txt @@ -0,0 +1,3 @@ +module M +alias G = rel[loc, set[E +data P = t() | f(); diff --git a/src/org/rascalmpl/library/lang/rascal/tests/concrete/recovery/bugs/InfiniteLoopMultiError.rsc b/src/org/rascalmpl/library/lang/rascal/tests/concrete/recovery/bugs/InfiniteLoopMultiError.rsc new file mode 100644 index 00000000000..2ed3223f925 --- /dev/null +++ b/src/org/rascalmpl/library/lang/rascal/tests/concrete/recovery/bugs/InfiniteLoopMultiError.rsc @@ -0,0 +1,27 @@ +/** +* 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::bugs::InfiniteLoopMultiError + + +import lang::rascal::tests::concrete::recovery::RecoveryTestSupport; +import lang::rascal::\syntax::Rascal; +import ParseTree; +import IO; + +void testInfiniteLoopMultiError() { + standardParser = parser(#start[Module], allowRecovery=false, allowAmbiguity=true); + recoveryParser = parser(#start[Module], allowRecovery=true, allowAmbiguity=true); + source = |std:///Exception.rsc|; + input = readFile(source); + testDeleteUntilEol(standardParser, recoveryParser, source, input, 200, 100, begin=662, end=664); +} diff --git a/src/org/rascalmpl/library/lang/rascal/tests/concrete/recovery/bugs/LostSkipBug.rsc b/src/org/rascalmpl/library/lang/rascal/tests/concrete/recovery/bugs/LostSkipBug.rsc new file mode 100644 index 00000000000..a192eab758a --- /dev/null +++ b/src/org/rascalmpl/library/lang/rascal/tests/concrete/recovery/bugs/LostSkipBug.rsc @@ -0,0 +1,26 @@ +/** +* 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::bugs::LostSkipBug + + +import lang::rascal::tests::concrete::recovery::RecoveryTestSupport; +import lang::rascal::\syntax::Rascal; +import ParseTree; +import IO; + +void testBug() { + standardParser = parser(#start[Module], allowRecovery=false, allowAmbiguity=true); + recoveryParser = parser(#start[Module], allowRecovery=true, allowAmbiguity=true); + input = readFile(|std:///analysis/diff/edits/ExecuteTextEdits.rsc|); + testSingleCharDeletions(standardParser, recoveryParser, input, 200, 100, begin=235, end=235); +} diff --git a/src/org/rascalmpl/library/lang/rascal/tests/concrete/recovery/bugs/MinimalMultiError.rsc b/src/org/rascalmpl/library/lang/rascal/tests/concrete/recovery/bugs/MinimalMultiError.rsc new file mode 100644 index 00000000000..ad3ed90ca4e --- /dev/null +++ b/src/org/rascalmpl/library/lang/rascal/tests/concrete/recovery/bugs/MinimalMultiError.rsc @@ -0,0 +1,29 @@ +/** +* 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::bugs::MinimalMultiError + +import lang::rascal::tests::concrete::recovery::RecoveryTestSupport; + +layout Layout = [\ ]* !>> [\ ]; + +syntax S = T; + +syntax T = AB AB AB End; +syntax AB = 'a' 'b'; +syntax End = "$"; + +test bool multiOk() = checkRecovery(#S, "ababab$", []); + +test bool multiOneError() = checkRecovery(#S, "acabab$", ["c"]); + +test bool multiTwoError() = checkRecovery(#S, "acacab$", ["c","c"]); diff --git a/src/org/rascalmpl/library/lang/rascal/tests/concrete/recovery/bugs/MultiErrorBug.rsc b/src/org/rascalmpl/library/lang/rascal/tests/concrete/recovery/bugs/MultiErrorBug.rsc new file mode 100644 index 00000000000..ff3b37c160c --- /dev/null +++ b/src/org/rascalmpl/library/lang/rascal/tests/concrete/recovery/bugs/MultiErrorBug.rsc @@ -0,0 +1,63 @@ +/** +* 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::bugs::MultiErrorBug + +import ParseTree; +import IO; +import util::ErrorRecovery; +import List; +import vis::Text; + +start syntax Module = SyntaxDefinition* ; + +syntax SyntaxDefinition = Prod ";" ; + +lexical Name = [A-Z]; + +syntax Sym + = Sym NonterminalLabel + | StringConstant + ; + +lexical StringConstant = "\"" StringCharacter* "\"" ; + +lexical StringCharacter = ![\"] ; + +lexical NonterminalLabel = [a-z] ; + +syntax Prod = Sym* ; + +// This is an open issue: instead of two small errors, one big error tree is returned. +bool multiErrorBug() { + str input = "#\"a\";#\"b\";"; + Tree t = parser(#start[Module], allowRecovery=true, allowAmbiguity=true)(input, |unknown:///?visualize=false|); + + println(prettyTree(t)); + + list[Tree] errors = findAllErrors(t); + println(" Errors"); + for (Tree error <- errors) { + Tree skipped = getSkipped(error); + println(" : "); + } + Tree disambiguated = disambiguateErrors(t); + list[Tree] disambiguatedErrors = findAllErrors(disambiguated); + println("After disambiguating:"); + println(" Errors"); + for (Tree error <- disambiguatedErrors) { + Tree skipped = getSkipped(error); + println(" : "); + } + return true; +} + diff --git a/src/org/rascalmpl/library/lang/rascal/tests/concrete/recovery/bugs/MultiErrorPico.rsc b/src/org/rascalmpl/library/lang/rascal/tests/concrete/recovery/bugs/MultiErrorPico.rsc new file mode 100644 index 00000000000..be1c171279c --- /dev/null +++ b/src/org/rascalmpl/library/lang/rascal/tests/concrete/recovery/bugs/MultiErrorPico.rsc @@ -0,0 +1,25 @@ +/** +* 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::bugs::MultiErrorPico + +import lang::pico::\syntax::Main; +import lang::rascal::tests::concrete::recovery::RecoveryTestSupport; + +bool multiErrorPico() { + return checkRecovery(#start[Program], "begin + declare; + i := #1; + j := #2; + k := 3 +end" , ["#1", "#2"]); +} diff --git a/src/org/rascalmpl/library/lang/rascal/tests/concrete/recovery/bugs/MultiErrorPicoInput.pico b/src/org/rascalmpl/library/lang/rascal/tests/concrete/recovery/bugs/MultiErrorPicoInput.pico new file mode 100644 index 00000000000..a7393e9f705 --- /dev/null +++ b/src/org/rascalmpl/library/lang/rascal/tests/concrete/recovery/bugs/MultiErrorPicoInput.pico @@ -0,0 +1,6 @@ +begin + declare; + i := #1; + j := #2; + k := 3 +end \ No newline at end of file diff --git a/src/org/rascalmpl/library/lang/rascal/tests/concrete/recovery/bugs/MultiProdRecovery.rsc b/src/org/rascalmpl/library/lang/rascal/tests/concrete/recovery/bugs/MultiProdRecovery.rsc new file mode 100644 index 00000000000..6b02902e6a4 --- /dev/null +++ b/src/org/rascalmpl/library/lang/rascal/tests/concrete/recovery/bugs/MultiProdRecovery.rsc @@ -0,0 +1,32 @@ +/** +* 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::bugs::MultiProdRecovery + +import lang::rascal::tests::concrete::recovery::RecoveryTestSupport; + +layout Layout = [\ ]* !>> [\ ]; + +syntax Prog = Stat*; + +syntax Stat + = Expr ";" + | "{" Stat* "}"; + +syntax Expr + = Expr "+" Expr + | Expr "-" Expr + | "e"; + +test bool multiProdOk() = checkRecovery(#Prog, "{ e + e; }", []); + +test bool multiProdOperatorError() = checkRecovery(#Prog, "{ e * e; }", ["* "]); diff --git a/src/org/rascalmpl/library/lang/rascal/tests/concrete/recovery/bugs/NoErrorsAfterDisambBug.rsc b/src/org/rascalmpl/library/lang/rascal/tests/concrete/recovery/bugs/NoErrorsAfterDisambBug.rsc new file mode 100644 index 00000000000..3743e9a9e34 --- /dev/null +++ b/src/org/rascalmpl/library/lang/rascal/tests/concrete/recovery/bugs/NoErrorsAfterDisambBug.rsc @@ -0,0 +1,26 @@ +/** +* 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::bugs::NoErrorsAfterDisambBug + + +import lang::rascal::tests::concrete::recovery::RecoveryTestSupport; +import lang::rascal::\syntax::Rascal; +import ParseTree; +import IO; + +void testBug() { + standardParser = parser(#start[Module], allowRecovery=false, allowAmbiguity=true); + recoveryParser = parser(#start[Module], allowRecovery=true, allowAmbiguity=true); + input = readFile(|std:///lang/rascal/tests/basic/ListRelations.rsc|); + testSingleCharDeletions(standardParser, recoveryParser, input, 200, 100, begin=1916, end=1916); +} diff --git a/src/org/rascalmpl/library/lang/rascal/tests/concrete/recovery/bugs/OutOfMemoryBug.rsc b/src/org/rascalmpl/library/lang/rascal/tests/concrete/recovery/bugs/OutOfMemoryBug.rsc new file mode 100644 index 00000000000..cfe7d9228ef --- /dev/null +++ b/src/org/rascalmpl/library/lang/rascal/tests/concrete/recovery/bugs/OutOfMemoryBug.rsc @@ -0,0 +1,27 @@ +/** +* 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::bugs::OutOfMemoryBug + + +import lang::rascal::tests::concrete::recovery::RecoveryTestSupport; +import lang::rascal::\syntax::Rascal; +import ParseTree; +import IO; + +void testBug() { + standardParser = parser(#start[Module], allowRecovery=false, allowAmbiguity=true); + recoveryParser = parser(#start[Module], allowRecovery=true, allowAmbiguity=true); + loc source = |std:///lang/rascal/tests/functionality/PatternSet3.rsc|; + input = readFile(source); + testDeleteUntilEol(standardParser, recoveryParser, source, input, 200, 150, begin=581, end=581); +} diff --git a/src/org/rascalmpl/library/lang/rascal/tests/concrete/recovery/bugs/OvertakeBug.rsc b/src/org/rascalmpl/library/lang/rascal/tests/concrete/recovery/bugs/OvertakeBug.rsc new file mode 100644 index 00000000000..a68b46ea99c --- /dev/null +++ b/src/org/rascalmpl/library/lang/rascal/tests/concrete/recovery/bugs/OvertakeBug.rsc @@ -0,0 +1,23 @@ +/** +* 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::bugs::OvertakeBug + +import lang::rascal::\syntax::Rascal; +import ParseTree; +import IO; + +bool testOvertakeBug() { + str input = readFile(|std:///lang/rascal/tests/concrete/recovery/bugs/OvertakeBugInput.txt|); + parser(#Module, allowRecovery=true, allowAmbiguity=true)(input, |unknown:///?visualize=false|); + return true; +} \ No newline at end of file diff --git a/src/org/rascalmpl/library/lang/rascal/tests/concrete/recovery/bugs/OvertakeBugInput.txt b/src/org/rascalmpl/library/lang/rascal/tests/concrete/recovery/bugs/OvertakeBugInput.txt new file mode 100644 index 00000000000..a6821039bc7 --- /dev/null +++ b/src/org/rascalmpl/library/lang/rascal/tests/concrete/recovery/bugs/OvertakeBugInput.txt @@ -0,0 +1,39 @@ +module analysis::diff::edits::ExecuteTextEdits + +extend analysis::diff::edits::TextEdits; +import IO; +import String; +import List; + +void executeDocumentEdits(list[DocumentEdit] edits) { + for (e <- edits) { + executeDocumentEdit(e); + } +} + +void executeDocumentEdit(removed(loc f)) { + remove(f.top; +} + +void executeDocumentEdit(created(loc f)) { + writeFile(f, ""); +} + +void executeDocumentEdit(renamed(loc from, loc to)) { + move(from.top, to.top, overwrite=true); +} + +void executeDocumentEdit(changed(loc file, 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 = ""; + } + + writeFile(file.top, content); +} \ No newline at end of file diff --git a/src/org/rascalmpl/library/lang/rascal/tests/concrete/recovery/bugs/OvertakenNullableBug.rsc b/src/org/rascalmpl/library/lang/rascal/tests/concrete/recovery/bugs/OvertakenNullableBug.rsc new file mode 100644 index 00000000000..c25a791ccf8 --- /dev/null +++ b/src/org/rascalmpl/library/lang/rascal/tests/concrete/recovery/bugs/OvertakenNullableBug.rsc @@ -0,0 +1,26 @@ +/** +* 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::bugs::OvertakenNullableBug + + +import lang::rascal::tests::concrete::recovery::RecoveryTestSupport; +import lang::rascal::\syntax::Rascal; +import ParseTree; +import IO; + +void testBug() { + standardParser = parser(#start[Module], allowRecovery=false, allowAmbiguity=true); + recoveryParser = parser(#start[Module], allowRecovery=true, allowAmbiguity=true); + input = readFile(|std:///lang/rascal/tests/library/analysis/statistics/DescriptiveTests.rsc|); + testDeleteUntilEol(standardParser, recoveryParser, input, 200, 100, begin=561, end=561); +} diff --git a/src/org/rascalmpl/library/lang/rascal/tests/concrete/recovery/bugs/SlowDisambiguationBug.rsc b/src/org/rascalmpl/library/lang/rascal/tests/concrete/recovery/bugs/SlowDisambiguationBug.rsc new file mode 100644 index 00000000000..dd3dc6673fa --- /dev/null +++ b/src/org/rascalmpl/library/lang/rascal/tests/concrete/recovery/bugs/SlowDisambiguationBug.rsc @@ -0,0 +1,26 @@ +/** +* 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::bugs::SlowDisambiguationBug + +import lang::rascal::tests::concrete::recovery::RecoveryTestSupport; +import lang::rascal::\syntax::Rascal; +import ParseTree; +import IO; + +void testBug() { + standardParser = parser(#start[Module], allowRecovery=false, allowAmbiguity=true); + recoveryParser = parser(#start[Module], allowRecovery=true, allowAmbiguity=true); + input = readFile(|std:///lang/rascal/grammar/tests/TestGrammars.rsc|); + testDeleteUntilEol(standardParser, recoveryParser, input, 200, 100, begin=278, end=278); +} + diff --git a/src/org/rascalmpl/library/lang/rascal/tests/concrete/recovery/recovery-stats.R b/src/org/rascalmpl/library/lang/rascal/tests/concrete/recovery/recovery-stats.R new file mode 100644 index 00000000000..142e01608c1 --- /dev/null +++ b/src/org/rascalmpl/library/lang/rascal/tests/concrete/recovery/recovery-stats.R @@ -0,0 +1,42 @@ +# nolint start: line_length_linter. + +options("width" = 60) + +library("fs") + +input <- path_expand("~/stats/benchmark-stats-2024-11-16-0-5120.txt") +raw_data <- read.csv(input, header = TRUE) + +# Select interesting data subsets +recovery_data <- raw_data[raw_data$result == "recovery",] +error_data <- raw_data[raw_data$result == "error", ] +success_data <- raw_data[raw_data$result == "success", ] + +drop <- c("source", "result") + +recovery_fail_data <- recovery_data[recovery_data$errorSize >= recovery_data$size / 4, ] +recovery_ok_data <- recovery_data[recovery_data$errorSize < recovery_data$size / 4, ] + +# Drop uninteresting columns +recovery <- recovery_data[, !(names(recovery_data) %in% drop)] +error <- error_data[, !(names(error_data) %in% drop)] +success <- success_data[, !(names(success_data) %in% drop)] +recovery_fail <- recovery_fail_data[, !(names(recovery_fail_data) %in% drop)] +recovery_ok <- recovery_ok_data[, !(names(recovery_ok_data) %in% drop)] + +print("Total recovery stats") +summary(recovery) + +print("Successful recovery stats (error size < 25% of file size)") +summary(recovery_ok) + +print("Failed recovery stats (error size >= 25% of file size)") +summary(recovery_fail) + +print("Parse error stats") +summary(error) + +print("Successful parse stats") +summary(success) + +# nolint end diff --git a/src/org/rascalmpl/library/lang/rascal/vis/ImportGraph.rsc b/src/org/rascalmpl/library/lang/rascal/vis/ImportGraph.rsc index a5271a6f0e8..2e26e0a4854 100644 --- a/src/org/rascalmpl/library/lang/rascal/vis/ImportGraph.rsc +++ b/src/org/rascalmpl/library/lang/rascal/vis/ImportGraph.rsc @@ -105,4 +105,4 @@ ProjectModel getProjectModel(loc file) { } catch ParseError(_) : return projectModel(); -} \ No newline at end of file +} diff --git a/src/org/rascalmpl/library/util/ErrorRecovery.java b/src/org/rascalmpl/library/util/ErrorRecovery.java new file mode 100644 index 00000000000..3012d1a2a25 --- /dev/null +++ b/src/org/rascalmpl/library/util/ErrorRecovery.java @@ -0,0 +1,246 @@ +/** + * 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. + **/ +package org.rascalmpl.library.util; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Set; +import java.util.Map; + +import org.rascalmpl.interpreter.asserts.Ambiguous; +import org.rascalmpl.values.IRascalValueFactory; +import org.rascalmpl.values.RascalValueFactory; +import org.rascalmpl.values.parsetrees.ITree; +import org.rascalmpl.values.parsetrees.ProductionAdapter; +import org.rascalmpl.values.parsetrees.TreeAdapter; + +import io.usethesource.vallang.IBool; +import io.usethesource.vallang.IConstructor; +import io.usethesource.vallang.IList; +import io.usethesource.vallang.IListWriter; +import io.usethesource.vallang.ISet; +import io.usethesource.vallang.ISetWriter; +import io.usethesource.vallang.IValue; +import io.usethesource.vallang.type.Type; + +public class ErrorRecovery { + private final IRascalValueFactory rascalValues; + + public ErrorRecovery(IRascalValueFactory rascalValues) { + this.rascalValues = rascalValues; + } + + private static class ScoredTree { + public final IConstructor tree; + public final int score; + public final boolean hasErrors; + + public ScoredTree(IConstructor tree, int score, boolean hasErrors) { + this.tree = tree; + this.score = score; + this.hasErrors = hasErrors; + } + } + + /** + * Disambiguate error trees. Error recovery often produces ambiguous trees where errors can be recovered in multiple ways. + * This filter removes error trees until no ambiguities caused by error recovery are left. + * Regular ambiguous trees remain in the parse forest unless `allowAmbiguity` is set to false in which case an error is thrown. + * It can happen that the original tree has errors but the result does not: + * when a tree has an ambiguity where one branch has an error and the other has not the error branch is pruned leaving an error-free tree + * possibly without ambiguities. + */ + + public IConstructor disambiguateErrors(IConstructor arg, IBool allowAmbiguity) { + return disambiguate(arg, allowAmbiguity.getValue(), true, new HashMap<>()).tree; + } + + private ScoredTree disambiguate(IConstructor tree, boolean allowAmbiguity, boolean buildTree, Map processedTrees) { + Type type = tree.getConstructorType(); + ScoredTree result; + + if (type == RascalValueFactory.Tree_Appl) { + result = disambiguateAppl((ITree) tree, allowAmbiguity, buildTree, processedTrees); + } else if (type == RascalValueFactory.Tree_Amb) { + result = disambiguateAmb((ITree) tree, allowAmbiguity, buildTree, processedTrees); + } else { + // Other trees (cycle, char) do not have subtrees so they have a score of 0 and no errors + result = new ScoredTree(tree, 0, false); + } + + return result; + } + + private ScoredTree disambiguateAppl(ITree appl, boolean allowAmbiguity, boolean buildTree, Map processedTrees) { + ScoredTree result = processedTrees.get(appl); + if (result != null) { + return result; + } + + if (ProductionAdapter.isSkipped(appl.getProduction())) { + result = new ScoredTree(appl, ((IList) appl.get(1)).length(), true); + } else { + IList args = TreeAdapter.getArgs(appl); + int totalScore = 0; + boolean hasErrors = false; + IListWriter disambiguatedArgs = null; + + // Disambiguate and score all children + for (int i=0; i processedTrees) { + ScoredTree result = processedTrees.get(amb); + if (result != null) { + return result; + } + + ISet originalAlts = (ISet) amb.get(0); + + ISetWriter alternativesWithoutErrors = null; + + ScoredTree errorAltWithBestScore = null; + for (IValue alt : originalAlts) { + ScoredTree disambiguatedAlt = disambiguate((IConstructor) alt, allowAmbiguity, buildTree, processedTrees); + if (disambiguatedAlt.hasErrors) { + // Only keep the best of the error trees + if (errorAltWithBestScore == null || errorAltWithBestScore.score > disambiguatedAlt.score) { + errorAltWithBestScore = disambiguatedAlt; + } + } else { + // Non-error tree + if (alternativesWithoutErrors == null) { + alternativesWithoutErrors = rascalValues.setWriter(); + } + alternativesWithoutErrors.insert(disambiguatedAlt.tree); + } + } + + if (alternativesWithoutErrors == null) { + assert errorAltWithBestScore != null : "No trees with and no trees without errors?"; + processedTrees.put(amb, errorAltWithBestScore); + return errorAltWithBestScore; + } + + ISet remainingAlts = alternativesWithoutErrors.done(); + + ITree resultTree = null; + + if (remainingAlts.size() > 1 && !allowAmbiguity) { + // We have an ambiguity between non-error trees + resultTree = rascalValues.amb(remainingAlts); + throw new Ambiguous(resultTree); + } + + if (buildTree) { + if (remainingAlts.size() == originalAlts.size()) { + // All children are without errors, return the original tree + resultTree = amb; + } else if (remainingAlts.size() == 1) { + // One child without errors remains, dissolve the amb tree + resultTree = (ITree) remainingAlts.iterator().next(); + } else { + // Create a new amb tree with the remaining non-error trees + resultTree = rascalValues.amb(remainingAlts); + } + } + + result = new ScoredTree(resultTree, 0, false); + processedTrees.put(amb, result); + + return result; + } + + public IList findAllErrors(IConstructor tree) { + IListWriter errors = rascalValues.listWriter(); + collectErrors((ITree) tree, errors, new HashSet<>()); + return errors.done(); + } + + public boolean hasErrors(IConstructor tree) { + return !findAllErrors(tree).isEmpty(); + } + + private void collectErrors(ITree tree, IListWriter errors, Set processedTrees) { + Type type = tree.getConstructorType(); + + if (type == RascalValueFactory.Tree_Appl) { + collectApplErrors(tree, errors, processedTrees); + } else if (type == RascalValueFactory.Tree_Amb) { + collectAmbErrors(tree, errors, processedTrees); + } + } + + private void collectApplErrors(ITree appl, IListWriter errors, Set processedTrees) { + if (!processedTrees.add(appl)) { + return; + } + + if (ProductionAdapter.isError(appl.getProduction())) { + errors.append(appl); + } + + IList args = TreeAdapter.getArgs(appl); + for (int i=0; i processedTrees) { + if (!processedTrees.add(amb)) { + return; + } + + for (IValue alt : TreeAdapter.getAlternatives(amb)) { + collectErrors((ITree) alt, errors, processedTrees); + } + } + + public void checkForRegularAmbiguities(IConstructor parseForest) { + disambiguate(parseForest, false, false, new HashMap<>()); + } +} diff --git a/src/org/rascalmpl/library/util/ErrorRecovery.rsc b/src/org/rascalmpl/library/util/ErrorRecovery.rsc new file mode 100644 index 00000000000..b547b893008 --- /dev/null +++ b/src/org/rascalmpl/library/util/ErrorRecovery.rsc @@ -0,0 +1,58 @@ +/** + * 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 util::ErrorRecovery + +import ParseTree; +import String; + +@synopsis{Check if a parse tree contains any error nodes, the result of error recovery.} +bool hasErrors(Tree tree) = /appl(error(_, _, _), _) := tree; + +@javaClass{org.rascalmpl.library.util.ErrorRecovery} +@synopsis{Find all error productions in a parse tree. The list is created by an outermost visit of the parse tree so if an error tree contains other errors the outermost tree is returned first.} +java list[Tree] findAllErrors(Tree tree); + +@synopsis{Disambiguate the error ambiguities in a tree and return the list of remaining errors. +The list is created by an outermost visit of the parse tree so if an error tree contains other errors the outermost tree is returned first.} +list[Tree] findBestErrors(Tree tree) = findAllErrors(disambiguateErrors(tree)); + +@synopsis{Get the symbol (sort) of the failing production} +Symbol getErrorSymbol(appl(error(Symbol sym, _, _), _)) = sym; + +@synopsis{Get the production that failed} +Production getErrorProduction(appl(error(_, Production prod, _), _)) = prod; + +@synopsis{Get the dot (position in the production) of the failing element in a production} +int getErrorDot(appl(error(_, _, int dot), _)) = dot; + +@synopsis{Get the skipped tree} +Tree getSkipped(appl(error(_, _, _), [*_, skip:appl(skipped(_), _)])) = skip; + +@synopsis{Get the text that failed to parse. This is only the text of the part that has been skipped to be able to continue parsing. +If you want the text of the whole error tree, you can just use string interpolation: "". +} +str getErrorText(appl(error(_, _, _), [*_, appl(skipped(_), chars)])) = stringChars([c | char(c) <- chars]); + +@javaClass{org.rascalmpl.library.util.ErrorRecovery} +@synopsis{Error recovery often produces ambiguous trees where errors can be recovered in multiple ways. +This filter removes error trees until no ambiguities caused by error recovery are left. +Note that regular ambiguous trees remain in the parse forest unless `allowAmbiguity` is set to false in which case an error is thrown. +This method uses simple and somewhat arbitrary heuristics, so its usefulness is limited. +} +java Tree disambiguateErrors(Tree t, bool allowAmbiguity=true); + +@synopsis{Create a parse filter based on `disambiguateErrors` with or without `allowAmbiguity`.} +Tree(Tree) createErrorFilter(bool allowAmbiguity) = + Tree(Tree t) { return disambiguateErrors(t, allowAmbiguity=allowAmbiguity); }; diff --git a/src/org/rascalmpl/library/vis/Text.rsc b/src/org/rascalmpl/library/vis/Text.rsc index 566a229c92d..5c8c6846925 100644 --- a/src/org/rascalmpl/library/vis/Text.rsc +++ b/src/org/rascalmpl/library/vis/Text.rsc @@ -41,6 +41,8 @@ str prettyTree(Tree t, bool src=false, bool characters=true, bool \layout=false, str nodeLabel(appl(prod(label(str l, Symbol nt), _, _), _)) = " = : "; str nodeLabel(appl(prod(Symbol nt, as, _), _)) = " = <}>"; + str nodeLabel(appl(error(Symbol nt, Production p, int dot), _)) = "!error dot=: "; + str nodeLabel(appl(skipped(Symbol s), chars)) = "skipped"; str nodeLabel(appl(regular(Symbol nt), _)) = ""; str nodeLabel(char(32)) = "⎵"; str nodeLabel(char(10)) = "\\r"; diff --git a/src/org/rascalmpl/parser/gtd/ExpectsProvider.java b/src/org/rascalmpl/parser/gtd/ExpectsProvider.java new file mode 100644 index 00000000000..fbbd7970c4e --- /dev/null +++ b/src/org/rascalmpl/parser/gtd/ExpectsProvider.java @@ -0,0 +1,21 @@ +/** + * 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. + **/ + +package org.rascalmpl.parser.gtd; + +import org.rascalmpl.parser.gtd.stack.AbstractStackNode; + +public interface ExpectsProvider

{ + AbstractStackNode

[] getExpects(String nonTerminal); +} diff --git a/src/org/rascalmpl/parser/gtd/IGTD.java b/src/org/rascalmpl/parser/gtd/IGTD.java index bb4e9b5c656..0e61d06b213 100644 --- a/src/org/rascalmpl/parser/gtd/IGTD.java +++ b/src/org/rascalmpl/parser/gtd/IGTD.java @@ -22,7 +22,7 @@ /** * Parser interface. */ -public interface IGTD{ +public interface IGTD extends ExpectsProvider

{ /** * Parse the input string, using the given non-terminal as start node. If * the parse process successfully completes a result will be constructed diff --git a/src/org/rascalmpl/parser/gtd/SGTDBF.java b/src/org/rascalmpl/parser/gtd/SGTDBF.java index b0232bacf3e..9d38723078f 100755 --- a/src/org/rascalmpl/parser/gtd/SGTDBF.java +++ b/src/org/rascalmpl/parser/gtd/SGTDBF.java @@ -1,18 +1,16 @@ /******************************************************************************* - * Copyright (c) 2009-2013 CWI - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the Eclipse Public License v1.0 - * which accompanies this distribution, and is available at - * http://www.eclipse.org/legal/epl-v10.html + * Copyright (c) 2009-2013 CWI All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License v1.0 which accompanies this + * distribution, and is available at http://www.eclipse.org/legal/epl-v10.html * - * Contributors: - * * Arnold Lankamp - Arnold.Lankamp@cwi.nl + * Contributors: * Arnold Lankamp - Arnold.Lankamp@cwi.nl *******************************************************************************/ package org.rascalmpl.parser.gtd; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.net.URI; +import java.util.Map; import org.rascalmpl.parser.gtd.debug.IDebugListener; import org.rascalmpl.parser.gtd.exception.ParseError; @@ -34,6 +32,7 @@ import org.rascalmpl.parser.gtd.stack.AbstractStackNode; import org.rascalmpl.parser.gtd.stack.EpsilonStackNode; import org.rascalmpl.parser.gtd.stack.NonTerminalStackNode; +import org.rascalmpl.parser.gtd.stack.SkippingStackNode; import org.rascalmpl.parser.gtd.stack.edge.EdgesSet; import org.rascalmpl.parser.gtd.stack.filter.ICompletionFilter; import org.rascalmpl.parser.gtd.stack.filter.IEnterFilter; @@ -45,33 +44,84 @@ import org.rascalmpl.parser.gtd.util.IntegerList; import org.rascalmpl.parser.gtd.util.IntegerObjectList; import org.rascalmpl.parser.gtd.util.Stack; +import org.rascalmpl.parser.uptr.debug.NopDebugListener; +import org.rascalmpl.parser.util.DebugUtil; +import org.rascalmpl.parser.util.ParseStateVisualizer; +import org.rascalmpl.util.visualize.dot.NodeId; +import org.rascalmpl.values.RascalValueFactory; +import org.rascalmpl.values.parsetrees.ITree; +import org.rascalmpl.values.parsetrees.TreeAdapter; + +import io.usethesource.vallang.IConstructor; +import io.usethesource.vallang.IList; +import io.usethesource.vallang.ISet; +import io.usethesource.vallang.IValue; +import io.usethesource.vallang.type.Type; /** * This is the core of the parser; it drives the parse process. */ -public abstract class SGTDBF implements IGTD{ +public abstract class SGTDBF implements IGTD { private final static int DEFAULT_TODOLIST_CAPACITY = 16; private URI inputURI; private int[] input; - + private int location; + protected int lookAheadChar; + + // A mapping between character location and line/column. private final PositionStore positionStore; + // Terminals that matched. Circular buffer indexed by length of the terminal. Each entry contains + // the node to reduce and the result node + // This is a circular buffer where queueIndex determines the start of the buffer. + // At each position, a stack is maintained of all terminals to reduce of a certain length. + // So at queueIndex+3, all terminals of length 3 that need reducing are stored. private DoubleStack, AbstractNode>[] todoLists; private int queueIndex; + // Stack of non-terminal nodes to expand + // - Nodes are removed in expand, which pops and expands all stack nodes on this stack + // - Nodes are added in: + // - parse: the root node is pushed + // - updateNextNode: next node of the production is pushed + // - updateAlternativeNextNode: next node of a prefix-shared production is pushed + // - handleExpects: non-matchable first element of each alternative is pushed + // - expandStack: when an expandable stack is expanded, all non-matchable children are pushed private final Stack> stacksToExpand; + + // The current stack of non-terminals to reduce. Each stack has a container node to accumulate + // results. + // - Nodes are removed in `reduceNonTerminals` where all productions are advanced one dot past the + // non-terminal node + // - Nodes are added in: + // - handleEdgeList: result container node is created and all edges are pushed with the same result + // container node + // - handleEdgeListWithRestrictions: result container node is created and all edges are pushed with + // the same result container node + // - expandStack: non-matchable, non-expandable nodes (and their existing result container node) are + // added if their name can be found in `cachedEdgesForExpect`. + // - expandStack: expandable nodes that are nullable? Might be a cycle thing private final DoubleStack, AbstractContainerNode

> stacksWithNonTerminalsToReduce; + + // The current stack of non-terminals to reduce: it contains the matchable node with the smallest + // length from todoLists. + // - Nodes are removed in `reduceTerminals` where all productions are advanced one dot past the + // matchable node + // - Variable is assigned in: + // - findFirstStacksToReduce: the first non-empty `todoList` is assigned to this variable + // - findStacksToReduce: again the first non-empty `todoList` is assigned to this variable + // - parse: variable is used in main reduce/expand loop to determine when it is time to look for + // more `stacksToReduce`. private DoubleStack, AbstractNode> stacksWithTerminalsToReduce; private final HashMap> cachedEdgesForExpect; private final IntegerKeyedDoubleValueHashMap, DoubleArrayList, AbstractNode>> sharedNextNodes; - private int location; - - protected int lookAheadChar; + // Reflection is used to get the expects for each non-terminal. + // This cache is used so the reflection call is only needed once per non-terminal. private final HashMap[]> expectCache; private final IntegerObjectList> sharedLastExpects; @@ -81,26 +131,31 @@ public abstract class SGTDBF implements IGTD{ // Error reporting private final Stack> unexpandableNodes; - private final Stack> unmatchableLeafNodes; + private final Stack> unmatchableLeafNodes; // Leaf nodes (for instance literals) that failed to + // match private final DoubleStack, AbstractNode>, AbstractStackNode

> unmatchableMidProductionNodes; private final DoubleStack, AbstractNode> filteredNodes; // Error reporting guards - private boolean parseErrorOccured; + private boolean parseErrorEncountered; + private boolean parseErrorRecovered; // Error recovery private IRecoverer

recoverer; + private Map processedTrees = new java.util.HashMap<>(); // Used to preserve sharing during error node introduction // Debugging private IDebugListener

debugListener; + private ParseStateVisualizer visualizer; // Temporary instrumentation for accurate profiling private long timestamp; private boolean printTimes = false; + public SGTDBF(){ super(); - + positionStore = new PositionStore(); stacksToExpand = new Stack>(); @@ -108,7 +163,8 @@ public SGTDBF(){ cachedEdgesForExpect = new HashMap>(); - sharedNextNodes = new IntegerKeyedDoubleValueHashMap, DoubleArrayList, AbstractNode>>(); + sharedNextNodes = + new IntegerKeyedDoubleValueHashMap, DoubleArrayList, AbstractNode>>(); location = 0; @@ -118,55 +174,65 @@ public SGTDBF(){ unexpandableNodes = new Stack>(); unmatchableLeafNodes = new Stack>(); - unmatchableMidProductionNodes = new DoubleStack, AbstractNode>, AbstractStackNode

>(); + unmatchableMidProductionNodes = + new DoubleStack, AbstractNode>, AbstractStackNode

>(); filteredNodes = new DoubleStack, AbstractNode>(); } /** - * Return a stack node id that is guaranteed not to be in use. - * The parser generator generates an override for this method as it knows which ids have been dispensed. - * Tests that need this should override this method, probably using a common base class. + * Return a stack node id that is guaranteed not to be in use. The parser generator generates an + * override for this method as it knows which ids have been dispensed. Tests that need this should + * override this method, probably using a common base class. */ protected int getFreeStackNodeId() { throw new UnsupportedOperationException(); } - /** - * Triggers the gathering of alternatives for the given non-terminal. - */ @SuppressWarnings("unchecked") - protected AbstractStackNode

[] invokeExpects(AbstractStackNode

nonTerminal){ - String name = nonTerminal.getName(); - AbstractStackNode

[] expects = expectCache.get(name); - if(expects == null){ + public AbstractStackNode

[] getExpects(String nonTerminal) { + AbstractStackNode

[] expects = expectCache.get(nonTerminal); + if (expects == null) { try{ - Method method = getClass().getMethod(name); - try{ + Method method = getClass().getMethod(nonTerminal); + try { method.setAccessible(true); // Try to bypass the 'isAccessible' check to save time. - }catch(SecurityException sex){ + } + catch (SecurityException sex) { // Ignore this if it happens. } expects = (AbstractStackNode

[]) method.invoke(this); - }catch(NoSuchMethodException nsmex){ - throw new UndeclaredNonTerminalException(name, getClass()); - }catch(IllegalAccessException iaex){ + } + catch (NoSuchMethodException nsmex) { + throw new UndeclaredNonTerminalException(nonTerminal, getClass()); + } + catch (IllegalAccessException iaex) { throw new RuntimeException(iaex); - }catch(InvocationTargetException itex){ + } + catch (InvocationTargetException itex) { throw new RuntimeException(itex.getTargetException()); } - expectCache.putUnsafe(name, expects); + expectCache.putUnsafe(nonTerminal, expects); } - - return expects; + + return expects; + } + + /** + * Triggers the gathering of alternatives for the given non-terminal. + */ + protected AbstractStackNode

[] invokeExpects(AbstractStackNode

nonTerminal){ + return getExpects(nonTerminal.getName()); } /** * Moves to the next symbol in the production. */ - private AbstractStackNode

updateNextNode(AbstractStackNode

next, AbstractStackNode

node, AbstractNode result){ - IntegerKeyedDoubleValueHashMap.Entry, DoubleArrayList, AbstractNode>> alternativeEntry = sharedNextNodes.get(next.getId()); + private AbstractStackNode

updateNextNode(AbstractStackNode

next, AbstractStackNode

node, + AbstractNode result) { + IntegerKeyedDoubleValueHashMap.Entry, DoubleArrayList, AbstractNode>> alternativeEntry = + sharedNextNodes.get(next.getId()); if(alternativeEntry != null){ // Sharing check. DoubleArrayList, AbstractNode> predecessors = alternativeEntry.value2; if(predecessors != null){ @@ -181,20 +247,27 @@ private AbstractStackNode

updateNextNode(AbstractStackNode

next, AbstractS // Encountered a possible stack 'overtake'. if(node.getStartLocation() != location){ propagateEdgesAndPrefixes(node, result, alternative, alternative.getResult()); - }else{ - propagateEdgesAndPrefixesForNullable(node, result, alternative, alternative.getResult(), node.getEdges().size()); + } + else { + propagateEdgesAndPrefixesForNullable(node, result, alternative, alternative.getResult(), + node.getEdges().size()); } return alternative; } - }else{ + } + else { EdgesSet

alternativeEdgesSet = alternative.getIncomingEdges(); int resultStoreId = getResultStoreId(alternative.getId()); - if(alternativeEdgesSet != null && alternativeEdgesSet.getLastVisitedLevel(resultStoreId) == location){ + if (alternativeEdgesSet != null + && alternativeEdgesSet.getLastVisitedLevel(resultStoreId) == location) { // Encountered a possible stack 'overtake'. if(node.getStartLocation() != location){ - propagateEdgesAndPrefixes(node, result, alternative, alternativeEdgesSet.getLastResult(resultStoreId)); - }else{ - propagateEdgesAndPrefixesForNullable(node, result, alternative, alternativeEdgesSet.getLastResult(resultStoreId), node.getEdges().size()); + propagateEdgesAndPrefixes(node, result, alternative, + alternativeEdgesSet.getLastResult(resultStoreId)); + } + else { + propagateEdgesAndPrefixesForNullable(node, result, alternative, + alternativeEdgesSet.getLastResult(resultStoreId), node.getEdges().size()); } return alternative; } @@ -203,42 +276,47 @@ private AbstractStackNode

updateNextNode(AbstractStackNode

next, AbstractS alternative.updateNode(node, result); - if(debugListener != null) debugListener.progressed(node, result, alternative); + debugListener.progressed(node, result, alternative); return alternative; } if(next.isMatchable()){ // Eager matching optimization. - if((location + next.getLength()) > input.length) return null; + if ((location + next.getLength()) > input.length) + return null; AbstractNode nextResult = next.match(input, location); if(nextResult == null){ - // Push the node including it's predecessor to the appropriate error tracking collection (and take care of merging when necessary). - DoubleArrayList, AbstractNode> predecessors = new DoubleArrayList, AbstractNode>(); + // Push the node including it's predecessor to the appropriate error tracking collection (and take + // care of merging when necessary). + DoubleArrayList, AbstractNode> predecessors = + new DoubleArrayList, AbstractNode>(); predecessors.add(node, result); unmatchableMidProductionNodes.push(predecessors, next); sharedNextNodes.putUnsafe(next.getId(), next, predecessors); - if(debugListener != null) debugListener.failedToMatch(next); + debugListener.failedToMatch(next); return null; } - if(debugListener != null) debugListener.matched(next, nextResult); + debugListener.matched(next, nextResult); next = next.getCleanCopyWithResult(location, nextResult); - }else{ + } + else { next = next.getCleanCopy(location); } if(!node.isMatchable() || result.isEmpty()){ next.updateNode(node, result); - }else{ // Non-nullable terminal specific edge set sharing optimization. + } + else { // Non-nullable terminal specific edge set sharing optimization. next.updateNodeAfterNonEmptyMatchable(node, result); } - if(debugListener != null) debugListener.progressed(node, result, next); + debugListener.progressed(node, result, next); sharedNextNodes.putUnsafe(next.getId(), next, null); stacksToExpand.push(next); @@ -249,9 +327,11 @@ private AbstractStackNode

updateNextNode(AbstractStackNode

next, AbstractS /** * Moves to the next symbol in an alternative continuation of a prefix-shared production. */ - private boolean updateAlternativeNextNode(AbstractStackNode

next, AbstractStackNode

node, AbstractNode result, IntegerObjectList> edgesMap, ArrayList[] prefixesMap){ + private boolean updateAlternativeNextNode(AbstractStackNode

next, AbstractStackNode

node, AbstractNode result, + IntegerObjectList> edgesMap, ArrayList[] prefixesMap) { int id = next.getId(); - IntegerKeyedDoubleValueHashMap.Entry, DoubleArrayList, AbstractNode>> alternativeEntry = sharedNextNodes.get(id); + IntegerKeyedDoubleValueHashMap.Entry, DoubleArrayList, AbstractNode>> alternativeEntry = + sharedNextNodes.get(id); if(alternativeEntry != null){ // Sharing check. DoubleArrayList, AbstractNode> predecessors = alternativeEntry.value2; if(predecessors != null){ @@ -264,55 +344,64 @@ private boolean updateAlternativeNextNode(AbstractStackNode

next, AbstractSta if(alternative.isMatchable()){ if(alternative.isEmptyLeafNode()){ // Encountered a possible stack 'overtake'. - propagateAlternativeEdgesAndPrefixes(node, result, alternative, alternative.getResult(), node.getEdges().size(), edgesMap, prefixesMap); + propagateAlternativeEdgesAndPrefixes(node, result, alternative, alternative.getResult(), + node.getEdges().size(), edgesMap, prefixesMap); return true; } - }else{ + } + else { EdgesSet

alternativeEdgesSet = alternative.getIncomingEdges(); int resultStoreId = getResultStoreId(alternative.getId()); - if(alternativeEdgesSet != null && alternativeEdgesSet.getLastVisitedLevel(resultStoreId) == location){ + if (alternativeEdgesSet != null + && alternativeEdgesSet.getLastVisitedLevel(resultStoreId) == location) { AbstractContainerNode

nextResult = alternativeEdgesSet.getLastResult(resultStoreId); // Encountered a possible stack 'overtake'. - propagateAlternativeEdgesAndPrefixes(node, result, alternative, nextResult, node.getEdges().size(), edgesMap, prefixesMap); + propagateAlternativeEdgesAndPrefixes(node, result, alternative, nextResult, + node.getEdges().size(), edgesMap, prefixesMap); return true; } } } - alternative.updatePrefixSharedNode(edgesMap, prefixesMap); // Prevent unnecessary overhead; share whenever possible. + alternative.updatePrefixSharedNode(edgesMap, prefixesMap); // Prevent unnecessary overhead; share whenever + // possible. - if(debugListener != null) debugListener.progressed(node, result, alternative); + debugListener.progressed(node, result, alternative); return true; } if(next.isMatchable()){ // Eager matching optimization. - if((location + next.getLength()) > input.length) return false; + if ((location + next.getLength()) > input.length) + return false; AbstractNode nextResult = next.match(input, location); if(nextResult == null){ - // Push the node including it's predecessor to the appropriate error tracking collection (and take care of merging when necessary). - DoubleArrayList, AbstractNode> predecessors = new DoubleArrayList, AbstractNode>(); + // Push the node including it's predecessor to the appropriate error tracking collection (and take + // care of merging when necessary). + DoubleArrayList, AbstractNode> predecessors = + new DoubleArrayList, AbstractNode>(); predecessors.add(node, result); unmatchableMidProductionNodes.push(predecessors, next); sharedNextNodes.putUnsafe(id, next, predecessors); - if(debugListener != null) debugListener.failedToMatch(next); + debugListener.failedToMatch(next); return false; } - if(debugListener != null) debugListener.matched(next, nextResult); + debugListener.matched(next, nextResult); next = next.getCleanCopyWithResult(location, nextResult); - }else{ + } + else { next = next.getCleanCopy(location); } next.updatePrefixSharedNode(edgesMap, prefixesMap); // Prevent unnecessary overhead; share whenever possible. - if(debugListener != null) debugListener.progressed(node, result, next); + debugListener.progressed(node, result, next); sharedNextNodes.putUnsafe(id, next, null); stacksToExpand.push(next); @@ -321,10 +410,10 @@ private boolean updateAlternativeNextNode(AbstractStackNode

next, AbstractSta } /** - * Part of the hidden-right-recursion fix. - * Executes absent reductions. + * Part of the hidden-right-recursion fix. Executes absent reductions. */ - private void propagateReductions(AbstractStackNode

node, AbstractNode nodeResultStore, AbstractStackNode

next, AbstractNode nextResultStore, int potentialNewEdges){ + private void propagateReductions(AbstractStackNode

node, AbstractNode nodeResultStore, AbstractStackNode

next, + AbstractNode nextResultStore, int potentialNewEdges) { IntegerList propagatedReductions = next.getPropagatedReductions(); IntegerObjectList> edgesMap = node.getEdges(); @@ -354,26 +443,27 @@ private void propagateReductions(AbstractStackNode

node, AbstractNode nodeRes EdgesSet

edgeSet = edgesMap.getValue(i); - if(debugListener != null) debugListener.reducing(node, resultLink, edgeSet); + debugListener.reducing(node, resultLink, edgeSet); if(!hasNestingRestrictions){ handleEdgeList(edgeSet, name, production, resultLink, startLocation); - }else{ + } + else { handleEdgeListWithRestrictions(edgeSet, name, production, resultLink, startLocation, filteredParents); } } } /** - * Part of the hidden-right-recursion fix. - * Propagates absent prefixes. + * Part of the hidden-right-recursion fix. Propagates absent prefixes. */ private void propagatePrefixes(AbstractStackNode

next, AbstractNode nextResult, int nrOfAddedEdges){ // Proceed with the tail of the production. int nextDot = next.getDot() + 1; AbstractStackNode

[] prod = next.getProduction(); AbstractStackNode

nextNext = prod[nextDot]; - IntegerKeyedDoubleValueHashMap.Entry, DoubleArrayList, AbstractNode>> nextNextAlternativeEntry = sharedNextNodes.get(nextNext.getId()); + IntegerKeyedDoubleValueHashMap.Entry, DoubleArrayList, AbstractNode>> nextNextAlternativeEntry = + sharedNextNodes.get(nextNext.getId()); AbstractStackNode

nextNextAlternative = null; if(nextNextAlternativeEntry != null){ // Valid continuation. DoubleArrayList, AbstractNode> predecessors = nextNextAlternativeEntry.value2; @@ -381,24 +471,31 @@ private void propagatePrefixes(AbstractStackNode

next, AbstractNode nextResul nextNextAlternative = nextNextAlternativeEntry.value1; if(nextNextAlternative.isMatchable()){ if(nextNextAlternative.isEmptyLeafNode()){ - propagateEdgesAndPrefixesForNullable(next, nextResult, nextNextAlternative, nextNextAlternative.getResult(), nrOfAddedEdges); - }else{ + propagateEdgesAndPrefixesForNullable(next, nextResult, nextNextAlternative, + nextNextAlternative.getResult(), nrOfAddedEdges); + } + else { nextNextAlternative.updateNode(next, nextResult); - if(debugListener != null) debugListener.propagated(next, nextResult, nextNextAlternative); + debugListener.propagated(next, nextResult, nextNextAlternative); + } } - }else{ + else { EdgesSet

nextNextAlternativeEdgesSet = nextNextAlternative.getIncomingEdges(); int resultStoreId = getResultStoreId(nextNextAlternative.getId()); - if(nextNextAlternativeEdgesSet != null && nextNextAlternativeEdgesSet.getLastVisitedLevel(resultStoreId) == location){ - propagateEdgesAndPrefixesForNullable(next, nextResult, nextNextAlternative, nextNextAlternativeEdgesSet.getLastResult(resultStoreId), nrOfAddedEdges); - }else{ + if (nextNextAlternativeEdgesSet != null + && nextNextAlternativeEdgesSet.getLastVisitedLevel(resultStoreId) == location) { + propagateEdgesAndPrefixesForNullable(next, nextResult, nextNextAlternative, + nextNextAlternativeEdgesSet.getLastResult(resultStoreId), nrOfAddedEdges); + } + else { nextNextAlternative.updateNode(next, nextResult); - if(debugListener != null) debugListener.propagated(next, nextResult, nextNextAlternative); + debugListener.propagated(next, nextResult, nextNextAlternative); + } } } - }else{ + else { predecessors.add(next, nextResult); } } @@ -406,12 +503,15 @@ private void propagatePrefixes(AbstractStackNode

next, AbstractNode nextResul // Handle alternative continuations (related to prefix sharing). AbstractStackNode

[][] alternateProds = next.getAlternateProductions(); if(alternateProds != null){ - if(nextNextAlternative == null){ // If the first continuation has not been initialized yet (it may be a matchable that didn't match), create a dummy version to construct the necessary edges and prefixes. - if(!nextNext.isMatchable()) return; // Matchable, abort. + if (nextNextAlternative == null) { // If the first continuation has not been initialized yet (it may be a + // matchable that didn't match), create a dummy version to construct the + // necessary edges and prefixes. + if (!nextNext.isMatchable()) + return; // Matchable, abort. nextNextAlternative = nextNext.getCleanCopy(location); nextNextAlternative.updateNode(next, nextResult); - if(debugListener != null) debugListener.propagated(next, nextResult, nextNextAlternative); + debugListener.propagated(next, nextResult, nextNextAlternative); } IntegerObjectList> nextNextEdgesMap = nextNextAlternative.getEdges(); @@ -419,35 +519,47 @@ private void propagatePrefixes(AbstractStackNode

next, AbstractNode nextResul for(int i = alternateProds.length - 1; i >= 0; --i){ prod = alternateProds[i]; - if(nextDot == prod.length) continue; + if (nextDot == prod.length) + continue; AbstractStackNode

alternativeNextNext = prod[nextDot]; - IntegerKeyedDoubleValueHashMap.Entry, DoubleArrayList, AbstractNode>> nextNextAltAlternativeEntry = sharedNextNodes.get(alternativeNextNext.getId()); + IntegerKeyedDoubleValueHashMap.Entry, DoubleArrayList, AbstractNode>> nextNextAltAlternativeEntry = + sharedNextNodes.get(alternativeNextNext.getId()); AbstractStackNode

nextNextAltAlternative = null; if(nextNextAltAlternativeEntry != null){ - DoubleArrayList, AbstractNode> predecessors = nextNextAltAlternativeEntry.value2; + DoubleArrayList, AbstractNode> predecessors = + nextNextAltAlternativeEntry.value2; if(predecessors == null){ nextNextAltAlternative = nextNextAltAlternativeEntry.value1; if(nextNextAltAlternative.isMatchable()){ if(nextNextAltAlternative.isEmptyLeafNode()){ - propagateAlternativeEdgesAndPrefixes(next, nextResult, nextNextAltAlternative, nextNextAltAlternative.getResult(), nrOfAddedEdges, nextNextEdgesMap, nextNextPrefixesMap); - }else{ + propagateAlternativeEdgesAndPrefixes(next, nextResult, nextNextAltAlternative, + nextNextAltAlternative.getResult(), nrOfAddedEdges, nextNextEdgesMap, + nextNextPrefixesMap); + } + else { nextNextAltAlternative.updatePrefixSharedNode(nextNextEdgesMap, nextNextPrefixesMap); - if(debugListener != null) debugListener.propagated(next, nextResult, nextNextAltAlternative); + debugListener.propagated(next, nextResult, nextNextAltAlternative); } - }else{ + } + else { EdgesSet

nextAlternativeEdgesSet = nextNextAltAlternative.getIncomingEdges(); int resultStoreId = getResultStoreId(nextNextAltAlternative.getId()); - if(nextAlternativeEdgesSet != null && nextAlternativeEdgesSet.getLastVisitedLevel(resultStoreId) == location){ - propagateAlternativeEdgesAndPrefixes(next, nextResult, nextNextAltAlternative, nextAlternativeEdgesSet.getLastResult(resultStoreId), nrOfAddedEdges, nextNextEdgesMap, nextNextPrefixesMap); - }else{ + if (nextAlternativeEdgesSet != null + && nextAlternativeEdgesSet.getLastVisitedLevel(resultStoreId) == location) { + propagateAlternativeEdgesAndPrefixes(next, nextResult, nextNextAltAlternative, + nextAlternativeEdgesSet.getLastResult(resultStoreId), nrOfAddedEdges, + nextNextEdgesMap, nextNextPrefixesMap); + } + else { nextNextAltAlternative.updatePrefixSharedNode(nextNextEdgesMap, nextNextPrefixesMap); - if(debugListener != null) debugListener.propagated(next, nextResult, nextNextAltAlternative); + debugListener.propagated(next, nextResult, nextNextAltAlternative); } } - }else{ + } + else { predecessors.add(next, nextResult); } } @@ -456,15 +568,17 @@ private void propagatePrefixes(AbstractStackNode

next, AbstractNode nextResul } /** - * Part of the hidden-right-recursion fix. - * Inserts missing prefixes and triggers reductions where necessary. + * Part of the hidden-right-recursion fix. Inserts missing prefixes and triggers reductions where + * necessary. */ - private void propagateEdgesAndPrefixes(AbstractStackNode

node, AbstractNode nodeResult, AbstractStackNode

next, AbstractNode nextResult){ + private void propagateEdgesAndPrefixes(AbstractStackNode

node, AbstractNode nodeResult, + AbstractStackNode

next, AbstractNode nextResult) { int nrOfAddedEdges = next.updateOvertakenNode(node, nodeResult); - if(debugListener != null) debugListener.propagated(node, nodeResult, next); + debugListener.propagated(node, nodeResult, next); - if(nrOfAddedEdges == 0) return; + if (nrOfAddedEdges == 0) + return; if(next.isEndNode()){ propagateReductions(node, nodeResult, next, nextResult, nrOfAddedEdges); @@ -476,15 +590,17 @@ private void propagateEdgesAndPrefixes(AbstractStackNode

node, AbstractNode n } /** - * Part of the hidden-right-recursion fix. - * Inserts missing prefixes and triggers reductions where necessary (specific for nullable nodes). + * Part of the hidden-right-recursion fix. Inserts missing prefixes and triggers reductions where + * necessary (specific for nullable nodes). */ - private void propagateEdgesAndPrefixesForNullable(AbstractStackNode

node, AbstractNode nodeResult, AbstractStackNode

next, AbstractNode nextResult, int potentialNewEdges){ + private void propagateEdgesAndPrefixesForNullable(AbstractStackNode

node, AbstractNode nodeResult, + AbstractStackNode

next, AbstractNode nextResult, int potentialNewEdges) { int nrOfAddedEdges = next.updateOvertakenNullableNode(node, nodeResult, potentialNewEdges); - if(debugListener != null) debugListener.propagated(node, nodeResult, next); + debugListener.propagated(node, nodeResult, next); - if(nrOfAddedEdges == 0) return; + if (nrOfAddedEdges == 0) + return; if(next.isEndNode()){ propagateReductions(node, nodeResult, next, nextResult, nrOfAddedEdges); @@ -496,15 +612,18 @@ private void propagateEdgesAndPrefixesForNullable(AbstractStackNode

node, Abs } /** - * Part of the hidden-right-recursion fix. - * Inserts missing prefixes and triggers reductions where necessary (specifically for alternative continuations of prefix-shared productions). + * Part of the hidden-right-recursion fix. Inserts missing prefixes and triggers reductions where + * necessary (specifically for alternative continuations of prefix-shared productions). */ - private void propagateAlternativeEdgesAndPrefixes(AbstractStackNode

node, AbstractNode nodeResult, AbstractStackNode

next, AbstractNode nextResult, int potentialNewEdges, IntegerObjectList> edgesMap, ArrayList[] prefixesMap){ + private void propagateAlternativeEdgesAndPrefixes(AbstractStackNode

node, AbstractNode nodeResult, + AbstractStackNode

next, AbstractNode nextResult, int potentialNewEdges, + IntegerObjectList> edgesMap, ArrayList[] prefixesMap) { next.updatePrefixSharedNode(edgesMap, prefixesMap); - if(debugListener != null) debugListener.propagated(node, nodeResult, next); + debugListener.propagated(node, nodeResult, next); - if(potentialNewEdges == 0) return; + if (potentialNewEdges == 0) + return; if(next.isEndNode()){ propagateReductions(node, nodeResult, next, nextResult, potentialNewEdges); @@ -537,12 +656,15 @@ private void updateEdges(AbstractStackNode

node, AbstractNode result){ EdgesSet

edgeSet = edgesMap.getValue(i); - if(debugListener != null) debugListener.reducing(node, resultLink, edgeSet); + debugListener.reducing(node, resultLink, edgeSet); - if(!hasNestingRestrictions){ // Select the optimized path for handling edge sets that don't have nesting restrictions associated with them. + if (!hasNestingRestrictions) { // Select the optimized path for handling edge sets that don't have nesting + // restrictions associated with them. handleEdgeList(edgeSet, name, production, resultLink, edgesMap.getKey(i)); - }else{ - handleEdgeListWithRestrictions(edgeSet, name, production, resultLink, edgesMap.getKey(i), filteredParents); + } + else { + handleEdgeListWithRestrictions(edgeSet, name, production, resultLink, edgesMap.getKey(i), + filteredParents); } } } @@ -571,18 +693,21 @@ private void updateNullableEdges(AbstractStackNode

node, AbstractNode result) for(int i = edgesMap.size() - 1; i >= 0; --i){ int startLocation = edgesMap.getKey(i); - if(propagatedReductions.containsBefore(startLocation, initialSize)) continue; // Prevent duplicate reductions (artifact of the hidden-right-recursion fix). + if (propagatedReductions.containsBefore(startLocation, initialSize)) + continue; // Prevent duplicate reductions (artifact of the hidden-right-recursion fix). propagatedReductions.add(startLocation); Link resultLink = new Link((prefixesMap != null) ? prefixesMap[i] : null, result); EdgesSet

edgeSet = edgesMap.getValue(i); - if(debugListener != null) debugListener.reducing(node, resultLink, edgeSet); + debugListener.reducing(node, resultLink, edgeSet); - if(!hasNestingRestrictions){ // Select the optimized path for handling edge sets that don't have nesting restrictions associated with them. + if (!hasNestingRestrictions) { // Select the optimized path for handling edge sets that don't have nesting + // restrictions associated with them. handleEdgeList(edgeSet, name, production, resultLink, startLocation); - }else{ + } + else { handleEdgeListWithRestrictions(edgeSet, name, production, resultLink, startLocation, filteredParents); } } @@ -599,26 +724,31 @@ private void handleEdgeList(EdgesSet

edgeSet, String name, P production, Link if(edge.isRecovered()){ resultStore = new RecoveredNode

(inputURI, startLocation, location); - }else if(edge.isExpandable()){ - resultStore = new ExpandableContainerNode

(inputURI, startLocation, location, startLocation == location, edge.isSeparator(), edge.isLayout()); - }else{ - resultStore = new SortContainerNode

(inputURI, startLocation, location, startLocation == location, edge.isSeparator(), edge.isLayout()); + } + else if (edge.isExpandable()) { + resultStore = new ExpandableContainerNode

(inputURI, startLocation, location, + startLocation == location, edge.isSeparator(), edge.isLayout()); + } + else { + resultStore = new SortContainerNode

(inputURI, startLocation, location, startLocation == location, + edge.isSeparator(), edge.isLayout()); } stacksWithNonTerminalsToReduce.push(edge, resultStore); - if(debugListener != null) debugListener.reduced(edge); + debugListener.reduced(edge); for(int j = edgeSet.size() - 1; j >= 1; --j){ edge = edgeSet.get(j); stacksWithNonTerminalsToReduce.push(edge, resultStore); - if(debugListener != null) debugListener.reduced(edge); + debugListener.reduced(edge); } edgeSet.setLastVisitedLevel(location, resultStoreId); edgeSet.setLastResult(resultStore, resultStoreId); - }else{ + } + else { resultStore = edgeSet.getLastResult(resultStoreId); } @@ -632,9 +762,11 @@ private void handleEdgeList(EdgesSet

edgeSet, String name, P production, Link /** * Handles reductions which may be associated with nesting restrictions. */ - private void handleEdgeListWithRestrictions(EdgesSet

edgeSet, String name, P production, Link resultLink, int startLocation, IntegerList filteredParents){ + private void handleEdgeListWithRestrictions(EdgesSet

edgeSet, String name, P production, Link resultLink, + int startLocation, IntegerList filteredParents) { // Only add the result to each resultstore once. - // Make sure each edge only gets added to the non-terminal reduction list once per level, by keeping track of them. + // Make sure each edge only gets added to the non-terminal reduction list once per level, by keeping + // track of them. firstTimeRegistration.clear(); firstTimeReductions.clear(); for(int j = edgeSet.size() - 1; j >= 0; --j){ @@ -643,7 +775,7 @@ private void handleEdgeListWithRestrictions(EdgesSet

edgeSet, String name, P if(!firstTimeReductions.contains(resultStoreId)){ if(firstTimeRegistration.contains(resultStoreId)){ - if(debugListener != null) debugListener.filteredByNestingRestriction(edge); + debugListener.filteredByNestingRestriction(edge); continue; } @@ -658,10 +790,14 @@ private void handleEdgeListWithRestrictions(EdgesSet

edgeSet, String name, P if(resultStore == null){ if (edge.isRecovered()) { resultStore = new RecoveredNode

(inputURI, startLocation, location); - }else if (edge.isExpandable()) { - resultStore = new ExpandableContainerNode

(inputURI, startLocation, location, startLocation == location, edge.isSeparator(), edge.isLayout()); - }else { - resultStore = new SortContainerNode

(inputURI, startLocation, location, startLocation == location, edge.isSeparator(), edge.isLayout()); + } + else if (edge.isExpandable()) { + resultStore = new ExpandableContainerNode

(inputURI, startLocation, location, + startLocation == location, edge.isSeparator(), edge.isLayout()); + } + else { + resultStore = new SortContainerNode

(inputURI, startLocation, location, + startLocation == location, edge.isSeparator(), edge.isLayout()); } edgeSet.setLastVisitedLevel(location, resultStoreId); @@ -673,11 +809,13 @@ private void handleEdgeListWithRestrictions(EdgesSet

edgeSet, String name, P resultStore.addAlternative(production, resultLink); - if(debugListener != null) debugListener.reduced(edge); - }else{ - if(debugListener != null) debugListener.filteredByNestingRestriction(edge); + debugListener.reduced(edge); + } + else { + debugListener.filteredByNestingRestriction(edge); } - }else{ + } + else { AbstractContainerNode

resultStore = edgeSet.getLastResult(resultStoreId); stacksWithNonTerminalsToReduce.push(edge, resultStore); } @@ -706,12 +844,14 @@ private void moveToNext(AbstractStackNode

node, AbstractNode result){ for(int i = alternateProds.length - 1; i >= 0; --i){ prod = alternateProds[i]; - if(nextDot == prod.length) continue; + if (nextDot == prod.length) + continue; AbstractStackNode

newAlternativeNext = prod[nextDot]; if(edgesMap != null){ updateAlternativeNextNode(newAlternativeNext, node, result, edgesMap, prefixesMap); - }else{ + } + else { AbstractStackNode

alternativeNext = updateNextNode(newAlternativeNext, node, result); if(alternativeNext != null){ @@ -724,12 +864,12 @@ private void moveToNext(AbstractStackNode

node, AbstractNode result){ } /** - * Progress to the next 'states' associated with the given node. - * I.e. move to the next symbol(s) in the production (if available) and executed reductions if necessary. + * Progress to the next 'states' associated with the given node. I.e. move to the next symbol(s) in + * the production (if available) and executed reductions if necessary. */ private void move(AbstractStackNode

node, AbstractNode result){ - if(debugListener != null) debugListener.moving(node, result); - + debugListener.moving(node, result); + // Handle filtering. ICompletionFilter[] completionFilters = node.getCompletionFilters(); if(completionFilters != null){ @@ -738,7 +878,7 @@ private void move(AbstractStackNode

node, AbstractNode result){ if(completionFilters[i].isFiltered(input, startLocation, location, positionStore)){ filteredNodes.push(node, result); - if(debugListener != null) debugListener.filteredByCompletionFilter(node, result); + debugListener.filteredByCompletionFilter(node, result); return; } @@ -746,9 +886,11 @@ private void move(AbstractStackNode

node, AbstractNode result){ } if(node.isEndNode()){ - if(!result.isEmpty() || node.getId() == AbstractExpandableStackNode.DEFAULT_LIST_EPSILON_ID){ // Only go into the nullable fix path for nullables (special list epsilons can be ignored as well). + // Only go into the nullable fix path for nullables (special list epsilons can be ignored as well). + if (!result.isEmpty() || node.getId() == AbstractExpandableStackNode.DEFAULT_LIST_EPSILON_ID) { updateEdges(node, result); - }else{ + } + else { updateNullableEdges(node, result); } } @@ -761,20 +903,25 @@ private void move(AbstractStackNode

node, AbstractNode result){ /** * Initiate the handling of stacks. */ - private void reduce(){ + private void reduceTerminals() { // Reduce terminals + visualize("Reducing terminals", ParseStateVisualizer.TERMINALS_TO_REDUCE_ID); while(!stacksWithTerminalsToReduce.isEmpty()){ move(stacksWithTerminalsToReduce.peekFirst(), stacksWithTerminalsToReduce.popSecond()); } - + } + + private void reduceNonTerminals() { // Reduce non-terminals + visualize("Reducing non-terminals", ParseStateVisualizer.NON_TERMINALS_TO_REDUCE_ID); while(!stacksWithNonTerminalsToReduce.isEmpty()){ move(stacksWithNonTerminalsToReduce.peekFirst(), stacksWithNonTerminalsToReduce.popSecond()); } } /** - * Locates the initial set of stacks that is queued for handling, for which the least amount of characters needs to be shifted. + * Locates the initial set of stacks that is queued for handling, for which the least amount of + * characters needs to be shifted. */ private boolean findFirstStacksToReduce(){ for(int i = 0; i < todoLists.length; ++i){ @@ -790,32 +937,26 @@ private boolean findFirstStacksToReduce(){ } } - if (recoverer != null) { - DoubleArrayList, AbstractNode> recoveredNodes = recoverer.reviveStacks(input, location, unexpandableNodes, unmatchableLeafNodes, unmatchableMidProductionNodes, filteredNodes); - if (recoveredNodes.size() > 0) { // TODO Do something with the revived node. Is this the right location to do this? - for (int i = 0; i < recoveredNodes.size(); i++) { - AbstractStackNode

recovered = recoveredNodes.getFirst(i); - addTodo(recovered, recovered.getLength(), recoveredNodes.getSecond(i)); - } - return findStacksToReduce(); - } - - parseErrorOccured = true; - } - - return false; + return attemptRecovery(); } /** - * Locates the set of stacks that is queued for handling, for which the least amount of characters needs to be shifted. + * Locates the set of stacks that is queued for handling, for which the least amount of characters + * needs to be shifted. */ private boolean findStacksToReduce(){ + visualize("Finding stacks to reduce", ParseStateVisualizer.TODO_LISTS_ID); int queueDepth = todoLists.length; for(int i = 1; i < queueDepth; ++i){ queueIndex = (queueIndex + 1) % queueDepth; DoubleStack, AbstractNode> terminalsTodo = todoLists[queueIndex]; if(!(terminalsTodo == null || terminalsTodo.isEmpty())){ + if (ParseStateVisualizer.VISUALIZATION_ENABLED) { + NodeId reduceNodeId = new NodeId("todo-" + i); + visualize("Found stack to reduce", reduceNodeId); + } + stacksWithTerminalsToReduce = terminalsTodo; location += i; @@ -824,35 +965,56 @@ private boolean findStacksToReduce(){ } } + return false; + } + + private boolean attemptRecovery() { if (recoverer != null) { - DoubleArrayList, AbstractNode> recoveredNodes = recoverer.reviveStacks(input, location, unexpandableNodes, unmatchableLeafNodes, unmatchableMidProductionNodes, filteredNodes); + debugListener.reviving(input, location, unexpandableNodes, unmatchableLeafNodes, + unmatchableMidProductionNodes, filteredNodes); + visualize("Recovering", ParseStateVisualizer.ERROR_TRACKING_ID); + DoubleArrayList, AbstractNode> recoveredNodes = recoverer.reviveStacks(input, location, + unexpandableNodes, unmatchableLeafNodes, unmatchableMidProductionNodes, filteredNodes); + debugListener.revived(recoveredNodes); + if (ParseStateVisualizer.VISUALIZATION_ENABLED && visualizer != null) { + // Visualize state and include recovered nodes + visualizer.createGraph(this, "Reviving"); + visualizer.addRecoveredNodes(recoveredNodes); + visualizer.writeGraph(); + DebugUtil.opportunityToBreak(); + } + if (recoveredNodes.size() > 0) { for (int i = 0; i < recoveredNodes.size(); i++) { AbstractStackNode

recovered = recoveredNodes.getFirst(i); - - int levelsFromHere = recovered.getLength() - (location - recovered.getStartLocation()); - - addTodo(recovered, levelsFromHere, recoveredNodes.getSecond(i)); + debugListener.reviving(input, location, unexpandableNodes, unmatchableLeafNodes, + unmatchableMidProductionNodes, filteredNodes); + visualize("Queue recovery node", ParseStateVisualizer.getNodeId(recovered)); + queueRecoveryNode(recovered, recovered.getStartLocation(), recovered.getLength(), + recoveredNodes.getSecond(i)); } + parseErrorRecovered = true; return findStacksToReduce(); } - parseErrorOccured = true; + parseErrorEncountered = true; } return false; } - + + public boolean parseErrorHasOccurred(){ - return parseErrorOccured; + return parseErrorEncountered; } /** - * Inserts a stack bottom into the todo-list. + * Inserts a stack bottom into the todoList. */ @SuppressWarnings("unchecked") - private void addTodo(AbstractStackNode

node, int length, AbstractNode result){ - if(result == null) throw new RuntimeException(); + private void queueMatchableNode(AbstractStackNode

node, int length, AbstractNode result){ + assert result != null; + int queueDepth = todoLists.length; if(length >= queueDepth){ DoubleStack, AbstractNode>[] oldTodoLists = todoLists; @@ -866,16 +1028,67 @@ private void addTodo(AbstractStackNode

node, int length, AbstractNode result) int insertLocation = (queueIndex + length) % queueDepth; DoubleStack, AbstractNode> terminalsTodo = todoLists[insertLocation]; if(terminalsTodo == null){ - terminalsTodo = new DoubleStack, AbstractNode>(); + terminalsTodo = new DoubleStack<>(); todoLists[insertLocation] = terminalsTodo; } terminalsTodo.push(node, result); } + /** + * Inserts a recovery node into the todoList, and possibly rewinds the parser to an earlier location + * in the input + */ + @SuppressWarnings("unchecked") + private void queueRecoveryNode(AbstractStackNode

node, int startPosition, int length, AbstractNode result){ + assert result != null; + + int queueDepth = todoLists.length; + + if (startPosition < location) { + // Have to reset the parser to an earlier location to at least + // be able to process the new node. Cannot throw away the queue, + // because there are possibly already other recovery tokens in the queue. + // However, we may assume that the queue before the current index is + // done, based on the way we cycle the queue now. The queue is + // looking forward to the future and we never re-use past entries. + + int negativeOffset = location - startPosition; + + DoubleStack, AbstractNode>[] oldTodoLists = todoLists; + todoLists = new DoubleStack[negativeOffset + Math.max(queueDepth, length) + 1]; + System.arraycopy(oldTodoLists, queueIndex, todoLists, negativeOffset, queueDepth - queueIndex); + System.arraycopy(oldTodoLists, 0, todoLists, queueDepth - queueIndex + negativeOffset , queueIndex); + + // reset the parser! + queueIndex = 0; + location = startPosition; + + DoubleStack, AbstractNode> terminalsTodo = todoLists[length]; + if (terminalsTodo == null) { + terminalsTodo = new DoubleStack<>(); + todoLists[length] = terminalsTodo; + } + + terminalsTodo.push(node, result); + } + else if (startPosition == location) { + // this is the normal case where new matchable nodes are discovered + // for the current parsing location, so reuse the code for queuing + queueMatchableNode(node, length, result); + } + else { + // This would mean we have discovered a recovery node for a location + // we have not been yet. That would be odd because then there would + // not have been a parse error and we wouldn't need recovery... + throw new RuntimeException("discovered a future recovery? " + node); + } + } + /** * Handles the retrieved alternatives for the given stack. */ - private boolean handleExpects(AbstractStackNode

[] expects, EdgesSet

cachedEdges, AbstractStackNode

stackBeingWorkedOn){ + private boolean handleExpects(AbstractStackNode

[] expects, EdgesSet

cachedEdges, + AbstractStackNode

stackBeingWorkedOn) { boolean hasValidAlternatives = false; sharedLastExpects.dirtyClear(); @@ -886,25 +1099,26 @@ private boolean handleExpects(AbstractStackNode

[] expects, EdgesSet

cached if(first.isMatchable()){ // Eager matching optimization. int length = first.getLength(); int endLocation = location + length; - if(endLocation > input.length) continue; + if (endLocation > input.length) + continue; AbstractNode result = first.match(input, location); if(result == null){ unmatchableLeafNodes.push(first); - if(debugListener != null) debugListener.failedToMatch(first); + debugListener.failedToMatch(first); continue; } - if(debugListener != null) debugListener.matched(first, result); + debugListener.matched(first, result); // Handle filtering. IEnterFilter[] enterFilters = first.getEnterFilters(); if(enterFilters != null){ for(int j = enterFilters.length - 1; j >= 0; --j){ if(enterFilters[j].isFiltered(input, location, positionStore)){ - if(debugListener != null) debugListener.filteredByEnterFilter(first); + debugListener.filteredByEnterFilter(first); continue EXPECTS; } @@ -913,8 +1127,9 @@ private boolean handleExpects(AbstractStackNode

[] expects, EdgesSet

cached first = first.getCleanCopyWithResult(location, result); - addTodo(first, length, result); - }else{ + queueMatchableNode(first, length, result); + } + else { first = first.getCleanCopy(location); stacksToExpand.push(first); } @@ -926,7 +1141,7 @@ private boolean handleExpects(AbstractStackNode

[] expects, EdgesSet

cached hasValidAlternatives = true; - if(debugListener != null) debugListener.expanded(stackBeingWorkedOn, first); + debugListener.expanded(stackBeingWorkedOn, first); } return hasValidAlternatives; @@ -957,7 +1172,7 @@ protected int getResultStoreId(int id){ * Expands the given stack node. */ private void expandStack(AbstractStackNode

stack){ - if(debugListener != null) debugListener.expanding(stack); + debugListener.expanding(stack); // Handle filtering. IEnterFilter[] enterFilters = stack.getEnterFilters(); @@ -966,7 +1181,7 @@ private void expandStack(AbstractStackNode

stack){ if(enterFilters[i].isFiltered(input, location, positionStore)){ unexpandableNodes.push(stack); - if(debugListener != null) debugListener.filteredByEnterFilter(stack); + debugListener.filteredByEnterFilter(stack); return; } @@ -974,8 +1189,9 @@ private void expandStack(AbstractStackNode

stack){ } if(stack.isMatchable()){ // Eager matching optimization related. - addTodo(stack, stack.getLength(), stack.getResult()); - }else if(!stack.isExpandable()){ // A 'normal' non-terminal. + queueMatchableNode(stack, stack.getLength(), stack.getResult()); + } + else if (!stack.isExpandable()) { // A 'normal' non-terminal. EdgesSet

cachedEdges = cachedEdgesForExpect.get(stack.getName()); if(cachedEdges == null){ cachedEdges = new EdgesSet

(1); @@ -991,19 +1207,21 @@ private void expandStack(AbstractStackNode

stack){ unexpandableNodes.push(stack); return; } - }else{ + } + else { int resultStoreId = getResultStoreId(stack.getId()); if(cachedEdges.getLastVisitedLevel(resultStoreId) == location){ // Is nullable, add the known results. stacksWithNonTerminalsToReduce.push(stack, cachedEdges.getLastResult(resultStoreId)); - if(debugListener != null) debugListener.foundIterationCachedNullableResult(stack); + debugListener.foundIterationCachedNullableResult(stack); } } cachedEdges.add(stack); stack.setIncomingEdges(cachedEdges); - }else{ // Expandable + } + else { // Expandable EdgesSet

cachedEdges = cachedEdgesForExpect.get(stack.getName()); if(cachedEdges == null){ boolean expanded = false; @@ -1017,33 +1235,36 @@ private void expandStack(AbstractStackNode

stack){ AbstractStackNode

child = listChildren[i]; int childId = child.getId(); - IntegerKeyedDoubleValueHashMap.Entry, DoubleArrayList, AbstractNode>> sharedChildEntry = sharedNextNodes.get(childId); + IntegerKeyedDoubleValueHashMap.Entry, DoubleArrayList, AbstractNode>> sharedChildEntry = + sharedNextNodes.get(childId); if(sharedChildEntry != null && sharedChildEntry.value2 == null){ AbstractStackNode

sharedChild = sharedChildEntry.value1; sharedChild.setEdgesSetWithPrefix(cachedEdges, null, location); - }else{ + } + else { if(child.isMatchable()){ int length = child.getLength(); int endLocation = location + length; - if(endLocation > input.length) continue; + if (endLocation > input.length) + continue; AbstractNode result = child.match(input, location); if(result == null){ unmatchableLeafNodes.push(child); - if(debugListener != null) debugListener.failedToMatch(child); + debugListener.failedToMatch(child); continue; } - if(debugListener != null) debugListener.matched(child, result); + debugListener.matched(child, result); // Handle filtering IEnterFilter[] childEnterFilters = child.getEnterFilters(); if(childEnterFilters != null){ for(int j = childEnterFilters.length - 1; j >= 0; --j){ if(childEnterFilters[j].isFiltered(input, location, positionStore)) { - if(debugListener != null) debugListener.filteredByEnterFilter(child); + debugListener.filteredByEnterFilter(child); continue CHILDREN; } @@ -1051,8 +1272,9 @@ private void expandStack(AbstractStackNode

stack){ } child = child.getCleanCopyWithResult(location, result); - addTodo(child, length, result); - }else{ + queueMatchableNode(child, length, result); + } + else { child = child.getCleanCopy(location); stacksToExpand.push(child); } @@ -1062,20 +1284,21 @@ private void expandStack(AbstractStackNode

stack){ sharedNextNodes.putUnsafe(childId, child, null); - if(debugListener != null) debugListener.expanded(stack, child); + debugListener.expanded(stack, child); } expanded = true; } if(stack.canBeEmpty()){ // Star list or optional. - AbstractStackNode

empty = stack.getEmptyChild().getCleanCopyWithResult(location, EpsilonStackNode.EPSILON_RESULT); + AbstractStackNode

empty = + stack.getEmptyChild().getCleanCopyWithResult(location, EpsilonStackNode.EPSILON_RESULT); empty.initEdges(); empty.addEdges(cachedEdges, location); stacksToExpand.push(empty); - if(debugListener != null) debugListener.expanded(stack, empty); + debugListener.expanded(stack, empty); expanded = true; } @@ -1089,7 +1312,7 @@ private void expandStack(AbstractStackNode

stack){ if(cachedEdges.getLastVisitedLevel(resultStoreId) == location){ // Is nullable, add the known results. stacksWithNonTerminalsToReduce.push(stack, cachedEdges.getLastResult(resultStoreId)); - if(debugListener != null) debugListener.foundIterationCachedNullableResult(stack); + debugListener.foundIterationCachedNullableResult(stack); } cachedEdges.add(stack); @@ -1102,6 +1325,7 @@ private void expandStack(AbstractStackNode

stack){ * Initiate stack expansion for all queued stacks. */ private void expand(){ + visualize("Expanding", ParseStateVisualizer.STACKS_TO_EXPAND_ID); while(!stacksToExpand.isEmpty()){ expandStack(stacksToExpand.pop()); } @@ -1115,105 +1339,153 @@ protected AbstractNode parse(AbstractStackNode

startNode, URI inputURI, int[] * Initiates parsing. */ @SuppressWarnings("unchecked") - protected AbstractNode parse(AbstractStackNode

startNode, URI inputURI, int[] input, IRecoverer

recoverer, IDebugListener

debugListener){ - initTime(); + protected AbstractNode parse(AbstractStackNode

startNode, URI inputURI, int[] input, + IRecoverer

recoverer, IDebugListener

debugListener) { + if (debugListener == null) { + debugListener = new NopDebugListener<>(); + } - try { + initTime(); - if(invoked){ - throw new RuntimeException("Can only invoke 'parse' once."); - } + try { + if(invoked){ + throw new RuntimeException("Can only invoke 'parse' once."); + } + + invoked = true; + // Initialize. + this.inputURI = inputURI; + this.input = input; - invoked = true; + this.recoverer = recoverer; + this.debugListener = debugListener; - // Initialize. - this.inputURI = inputURI; - this.input = input; + visualizer = ParseStateVisualizer.shouldVisualizeUri(inputURI) ? new ParseStateVisualizer("Parser") : null; - this.recoverer = recoverer; - this.debugListener = debugListener; + // Initialzed the position store. + positionStore.index(input); - // Initialzed the position store. - positionStore.index(input); + todoLists = new DoubleStack[DEFAULT_TODOLIST_CAPACITY]; - todoLists = new DoubleStack[DEFAULT_TODOLIST_CAPACITY]; + // Handle the initial expansion of the root node. + AbstractStackNode

rootNode = startNode; + rootNode.initEdges(); + stacksToExpand.push(rootNode); + lookAheadChar = (input.length > 0) ? input[0] : 0; - // Handle the initial expansion of the root node. - AbstractStackNode

rootNode = startNode; - rootNode.initEdges(); - stacksToExpand.push(rootNode); - lookAheadChar = (input.length > 0) ? input[0] : 0; + debugListener.shifting(location, input, positionStore); - if(debugListener != null) debugListener.shifting(location, input, positionStore); + expand(); - expand(); + AbstractContainerNode

result = null; + if(findFirstStacksToReduce()) { + boolean shiftedLevel = (location != 0); + while (true) { + lookAheadChar = (location < input.length) ? input[location] : 0; + if(shiftedLevel){ // Nullable fix for the first level. + sharedNextNodes.clear(); + cachedEdgesForExpect.clear(); - if(findFirstStacksToReduce()){ - boolean shiftedLevel = (location != 0); + unexpandableNodes.dirtyClear(); + unmatchableLeafNodes.dirtyClear(); + unmatchableMidProductionNodes.dirtyClear(); + filteredNodes.dirtyClear(); - do{ - lookAheadChar = (location < input.length) ? input[location] : 0; - if(shiftedLevel){ // Nullable fix for the first level. - sharedNextNodes.clear(); - cachedEdgesForExpect.clear(); + debugListener.shifting(location, input, positionStore); + } - unexpandableNodes.dirtyClear(); - unmatchableLeafNodes.dirtyClear(); - unmatchableMidProductionNodes.dirtyClear(); - filteredNodes.dirtyClear(); + // Reduce-expand loop. + do { + debugListener.iterating(); + reduceTerminals(); + reduceNonTerminals(); + expand(); + } + while (!stacksWithNonTerminalsToReduce.isEmpty() || !stacksWithTerminalsToReduce.isEmpty()); - if(debugListener != null) debugListener.shifting(location, input, positionStore); - } + shiftedLevel = true; - // Reduce-expand loop. - do{ - if(debugListener != null) debugListener.iterating(); + if (onlyRecoveredStacksLeft() && attemptRecovery()) { + continue; + } - reduce(); + if (!findStacksToReduce()) { + if(location == input.length) { + EdgesSet

startNodeEdgesSet = startNode.getIncomingEdges(); + int resultStoreId = getResultStoreId(startNode.getId()); + if(startNodeEdgesSet != null && startNodeEdgesSet.getLastVisitedLevel(resultStoreId) == input.length) { + result = startNodeEdgesSet.getLastResult(resultStoreId); // Success. + break; + } + } + if (!attemptRecovery()) { + // Unsuccessful parse + break; + } + } + } + } - expand(); - }while(!stacksWithNonTerminalsToReduce.isEmpty() || !stacksWithTerminalsToReduce.isEmpty()); + visualize("Done", ParseStateVisualizer.PARSER_ID); - shiftedLevel = true; - }while(findStacksToReduce()); - } + if (result != null) { + return result; + } + } finally { + checkTime("Parsing"); + } - // Check if we were successful. - if(location == input.length){ - EdgesSet

startNodeEdgesSet = startNode.getIncomingEdges(); - int resultStoreId = getResultStoreId(startNode.getId()); - if(startNodeEdgesSet != null && startNodeEdgesSet.getLastVisitedLevel(resultStoreId) == input.length){ - // Parsing succeeded. - return startNodeEdgesSet.getLastResult(resultStoreId); // Success. - } - } - } - finally { - checkTime("Parsing"); - } + try { + // A parse error occured, and recovery failed as well + parseErrorEncountered = true; - try { - // A parse error occured, and recovery failed as well - parseErrorOccured = true; - - int errorLocation = (location == Integer.MAX_VALUE ? 0 : location); - int line = positionStore.findLine(errorLocation); - int column = positionStore.getColumn(errorLocation, line); - if (location == input.length) { - throw new ParseError("Parse error", inputURI, errorLocation, 0, line + 1, line + 1, column, column, (Stack>) (Stack) unexpandableNodes, (Stack>) (Stack) unmatchableLeafNodes, (DoubleStack>, AbstractStackNode>) (DoubleStack) unmatchableMidProductionNodes, (DoubleStack, AbstractNode>) (DoubleStack) filteredNodes); - } - throw new ParseError("Parse error", inputURI, errorLocation, 1, line + 1, line + 1, column, column + 1, (Stack>) (Stack) unexpandableNodes, (Stack>) (Stack) unmatchableLeafNodes, (DoubleStack>, AbstractStackNode>) (DoubleStack) unmatchableMidProductionNodes, (DoubleStack, AbstractNode>) (DoubleStack) filteredNodes); - } - finally { - checkTime("Error handling"); - } + int errorLocation = (location == Integer.MAX_VALUE ? 0 : location); + int line = positionStore.findLine(errorLocation); + int column = positionStore.getColumn(errorLocation, line); + if (location == input.length) { + throw new ParseError("Parse error", inputURI, errorLocation, 0, line + 1, line + 1, column, column, + (Stack>) (Stack) unexpandableNodes, + (Stack>) (Stack) unmatchableLeafNodes, + (DoubleStack>, AbstractStackNode>) (DoubleStack) unmatchableMidProductionNodes, + (DoubleStack, AbstractNode>) (DoubleStack) filteredNodes); + } + throw new ParseError("Parse error", inputURI, errorLocation, 1, line + 1, line + 1, column, column + 1, + (Stack>) (Stack) unexpandableNodes, + (Stack>) (Stack) unmatchableLeafNodes, + (DoubleStack>, AbstractStackNode>) (DoubleStack) unmatchableMidProductionNodes, + (DoubleStack, AbstractNode>) (DoubleStack) filteredNodes); + } finally { + checkTime("Error handling"); + } } private void initTime() { - timestamp = System.nanoTime(); + timestamp = System.nanoTime(); } + private boolean onlyRecoveredStacksLeft() { + int recoveredStacksFound = 0; + + for (int i=0; i, AbstractNode> todoList = todoLists[i]; + if (todoList != null) { + int size = todoList.getSize(); + for (int j=0; j 50) { + return false; + } + } + } + } + + return recoveredStacksFound > 0; + } + private void checkTime(String msg) { long newStamp = System.nanoTime(); long duration = newStamp - timestamp; @@ -1240,52 +1512,73 @@ private static int[] charsToInts(char[] input){ /** * Parses with post parse filtering. */ - private T parse(String nonterminal, URI inputURI, int[] input, IActionExecutor actionExecutor, INodeFlattener converter, INodeConstructorFactory nodeConstructorFactory, IRecoverer

recoverer, IDebugListener

debugListener){ - AbstractNode result = parse(new NonTerminalStackNode

(AbstractStackNode.START_SYMBOL_ID, 0, nonterminal), inputURI, input, recoverer, debugListener); + private T parse(String nonterminal, URI inputURI, int[] input, IActionExecutor actionExecutor, + INodeFlattener converter, INodeConstructorFactory nodeConstructorFactory, IRecoverer

recoverer, + IDebugListener

debugListener) { + AbstractNode result = parse(new NonTerminalStackNode

(AbstractStackNode.START_SYMBOL_ID, 0, nonterminal), + inputURI, input, recoverer, debugListener); return buildResult(result, converter, nodeConstructorFactory, actionExecutor); } - public T parse(String nonterminal, URI inputURI, char[] input, IActionExecutor actionExecutor, INodeFlattener converter, INodeConstructorFactory nodeConstructorFactory, IRecoverer

recoverer, IDebugListener

debugListener){ - return parse(nonterminal, inputURI, charsToInts(input), actionExecutor, converter, nodeConstructorFactory, recoverer, debugListener); + public T parse(String nonterminal, URI inputURI, char[] input, IActionExecutor actionExecutor, + INodeFlattener converter, INodeConstructorFactory nodeConstructorFactory, IRecoverer

recoverer, + IDebugListener

debugListener) { + return parse(nonterminal, inputURI, charsToInts(input), actionExecutor, converter, nodeConstructorFactory, + recoverer, debugListener); } - public T parse(String nonterminal, URI inputURI, char[] input, IActionExecutor actionExecutor, INodeFlattener converter, INodeConstructorFactory nodeConstructorFactory, IRecoverer

recoverer){ + public T parse(String nonterminal, URI inputURI, char[] input, IActionExecutor actionExecutor, + INodeFlattener converter, INodeConstructorFactory nodeConstructorFactory, IRecoverer

recoverer) { return parse(nonterminal, inputURI, input, actionExecutor, converter, nodeConstructorFactory, recoverer, null); } - public T parse(String nonterminal, URI inputURI, char[] input, IActionExecutor actionExecutor, INodeFlattener converter, INodeConstructorFactory nodeConstructorFactory, IDebugListener

debugListener){ - return parse(nonterminal, inputURI, input, actionExecutor, converter, nodeConstructorFactory, null, debugListener); + public T parse(String nonterminal, URI inputURI, char[] input, IActionExecutor actionExecutor, + INodeFlattener converter, INodeConstructorFactory nodeConstructorFactory, + IDebugListener

debugListener) { + return parse(nonterminal, inputURI, input, actionExecutor, converter, nodeConstructorFactory, null, + debugListener); } - public T parse(String nonterminal, URI inputURI, char[] input, IActionExecutor actionExecutor, INodeFlattener converter, INodeConstructorFactory nodeConstructorFactory){ + public T parse(String nonterminal, URI inputURI, char[] input, IActionExecutor actionExecutor, + INodeFlattener converter, INodeConstructorFactory nodeConstructorFactory) { return parse(nonterminal, inputURI, input, actionExecutor, converter, nodeConstructorFactory, null, null); } /** * Parses without post parse filtering. */ - private T parse(String nonterminal, URI inputURI, int[] input, INodeFlattener converter, INodeConstructorFactory nodeConstructorFactory, IRecoverer

recoverer, IDebugListener

debugListener){ - AbstractNode result = parse(new NonTerminalStackNode

(AbstractStackNode.START_SYMBOL_ID, 0, nonterminal), inputURI, input, recoverer, debugListener); + private T parse(String nonterminal, URI inputURI, int[] input, INodeFlattener converter, + INodeConstructorFactory nodeConstructorFactory, IRecoverer

recoverer, + IDebugListener

debugListener) { + AbstractNode result = parse(new NonTerminalStackNode

(AbstractStackNode.START_SYMBOL_ID, 0, nonterminal), + inputURI, input, recoverer, debugListener); return buildResult(result, converter, nodeConstructorFactory, new VoidActionExecutor()); } - public T parse(String nonterminal, URI inputURI, char[] input, INodeFlattener converter, INodeConstructorFactory nodeConstructorFactory, IRecoverer

recoverer, IDebugListener

debugListener){ - return parse(nonterminal, inputURI, charsToInts(input), converter, nodeConstructorFactory, recoverer, debugListener); + public T parse(String nonterminal, URI inputURI, char[] input, INodeFlattener converter, + INodeConstructorFactory nodeConstructorFactory, IRecoverer

recoverer, + IDebugListener

debugListener) { + return parse(nonterminal, inputURI, charsToInts(input), converter, nodeConstructorFactory, recoverer, + debugListener); } - public T parse(String nonterminal, URI inputURI, char[] input, INodeFlattener converter, INodeConstructorFactory nodeConstructorFactory, IRecoverer

recoverer){ + public T parse(String nonterminal, URI inputURI, char[] input, INodeFlattener converter, + INodeConstructorFactory nodeConstructorFactory, IRecoverer

recoverer) { return parse(nonterminal, inputURI, input, converter, nodeConstructorFactory, recoverer, null); } - public T parse(String nonterminal, URI inputURI, char[] input, INodeFlattener converter, INodeConstructorFactory nodeConstructorFactory, IDebugListener

debugListener){ + public T parse(String nonterminal, URI inputURI, char[] input, INodeFlattener converter, + INodeConstructorFactory nodeConstructorFactory, IDebugListener

debugListener) { return parse(nonterminal, inputURI, input, converter, nodeConstructorFactory, null, debugListener); } - public T parse(String nonterminal, URI inputURI, char[] input, INodeFlattener converter, INodeConstructorFactory nodeConstructorFactory) { + public T parse(String nonterminal, URI inputURI, char[] input, INodeFlattener converter, + INodeConstructorFactory nodeConstructorFactory) { return parse(nonterminal, inputURI, charsToInts(input), converter, nodeConstructorFactory, null, null); } - protected T parse(AbstractStackNode

startNode, URI inputURI, char[] input, INodeFlattener converter, INodeConstructorFactory nodeConstructorFactory) { + protected T parse(AbstractStackNode

startNode, URI inputURI, char[] input, INodeFlattener converter, + INodeConstructorFactory nodeConstructorFactory) { AbstractNode result = parse(startNode, inputURI, charsToInts(input), null, null); @@ -1295,7 +1588,8 @@ protected T parse(AbstractStackNode

startNode, URI inputURI, char[] input, IN /** * Constructed the final parse result using the given converter. */ - protected T buildResult(AbstractNode result, INodeFlattener converter, INodeConstructorFactory nodeConstructorFactory, IActionExecutor actionExecutor){ + protected T buildResult(AbstractNode result, INodeFlattener converter, + INodeConstructorFactory nodeConstructorFactory, IActionExecutor actionExecutor) { initTime(); try { FilteringTracker filteringTracker = new FilteringTracker(); @@ -1303,13 +1597,17 @@ protected T buildResult(AbstractNode result, INodeFlattener converter, INo Object rootEnvironment = actionExecutor != null ? actionExecutor.createRootEnvironment() : null; T parseResult = null; try { - parseResult = converter.convert(nodeConstructorFactory, result, positionStore, filteringTracker, actionExecutor, rootEnvironment); + parseResult = converter.convert(nodeConstructorFactory, result, positionStore, filteringTracker, + actionExecutor, rootEnvironment); } finally { actionExecutor.completed(rootEnvironment, (parseResult == null)); } - if(parseResult != null){ - return parseResult; // Success. + if(parseResult != null) { + if (recoverer != null && parseErrorRecovered) { + parseResult = introduceErrorNodes(parseResult, nodeConstructorFactory); + } + return parseResult; // Success. } int offset = filteringTracker.getOffset(); @@ -1319,10 +1617,198 @@ protected T buildResult(AbstractNode result, INodeFlattener converter, INo int beginColumn = positionStore.getColumn(offset, beginLine); int endLine = positionStore.findLine(endOffset); int endColumn = positionStore.getColumn(endOffset, endLine); - throw new ParseError("All results were filtered", inputURI, offset, length, beginLine + 1, endLine + 1, beginColumn, endColumn); + throw new ParseError("All results were filtered", inputURI, offset, length, beginLine + 1, endLine + 1, + beginColumn, endColumn); } finally { checkTime("Unbinarizing, post-parse filtering, and mapping to UPTR"); } } + + /** + * After parsing, parse trees will only contain `skipped` nodes. This post-processing step + * transforms the original tree into a more useful form. In essence, subtrees containing errors look + * like this after parsing: `appl(prod(S,[]), + * [,,...,appl(skipped([]))])` This method transforms these trees into: + * `appl(error(S,prod(S,[]),), [,,...,appl(skipped([]))])` + * This means productions that failed to parse can be recognized at the top level. Note that this + * can only be done when we know the actual type of T is IConstructor. + */ + @SuppressWarnings("unchecked") + private T introduceErrorNodes(T tree, INodeConstructorFactory nodeConstructorFactory) { + if (!(tree instanceof IConstructor)) { + return tree; + } + + return (T) introduceErrorNodes((IConstructor) tree, + (INodeConstructorFactory) nodeConstructorFactory); + } + + private IConstructor introduceErrorNodes(IConstructor tree, + INodeConstructorFactory nodeConstructorFactory) { + IConstructor result = processedTrees.get(tree); + if (result != null) { + return result; + } + + Type type = tree.getConstructorType(); + if (type == RascalValueFactory.Tree_Appl) { + result = fixErrorAppl((ITree) tree, nodeConstructorFactory); + } + else if (type == RascalValueFactory.Tree_Char) { + result = tree; + } + else if (type == RascalValueFactory.Tree_Amb) { + result = fixErrorAmb((ITree) tree, nodeConstructorFactory); + } + else if (type == RascalValueFactory.Tree_Cycle) { + result = tree; + } + else { + throw new RuntimeException("Unrecognized tree type: " + type); + } + + if (result != tree && tree.asWithKeywordParameters().hasParameter(RascalValueFactory.Location)) { + IValue loc = tree.asWithKeywordParameters().getParameter(RascalValueFactory.Location); + result = result.asWithKeywordParameters().setParameter(RascalValueFactory.Location, loc); + } + + processedTrees.put(tree, result); + + return result; + } + + private IConstructor fixErrorAppl(ITree tree, + INodeConstructorFactory nodeConstructorFactory) { + + IValue prod = TreeAdapter.getProduction(tree); + IList childList = TreeAdapter.getArgs(tree); + + ArrayList newChildren = null; + boolean errorTree = false; + int childCount = childList.length(); + for (int i=0; i(childCount); + for (int j=0; j nodeConstructorFactory) { + ISet alternativeSet = TreeAdapter.getAlternatives(tree); + ArrayList alternatives = new ArrayList<>(alternativeSet.size()); + boolean anyChanges = false; + for (IValue alt : alternativeSet) { + IConstructor newAlt = introduceErrorNodes((IConstructor) alt, nodeConstructorFactory); + if (newAlt != alt) { + anyChanges = true; + } + alternatives.add(newAlt); + } + + if (anyChanges) { + return nodeConstructorFactory.createAmbiguityNode(alternatives); + } + + return tree; + } + + /** + * Datastructure visualization for debugging purposes + */ + + private void visualize(String step, NodeId highlight) { + // Only visualize when debugging + if (ParseStateVisualizer.VISUALIZATION_ENABLED && visualizer != null) { + visualizer.createGraph(this, step); + if (highlight != null) { + visualizer.highlight(highlight); + } + visualizer.writeGraph(); + + DebugUtil.opportunityToBreak(); + } + } + + /** + * Getters used for graph generation for debugging (see DebugVisualizer) + */ + + public int[] getInput() { + return input; + } + + public int getLocation() { + return location; + } + + public int getLookAheadChar() { + return lookAheadChar; + } + + public DoubleStack, AbstractNode>[] getTodoLists() { + return todoLists; + } + + public int getQueueIndex() { + return queueIndex; + } + + public Stack> getStacksToExpand() { + return stacksToExpand; + } + + public DoubleStack, AbstractContainerNode

> getStacksWithNonTerminalsToReduce() { + return stacksWithNonTerminalsToReduce; + } + + public DoubleStack, AbstractNode> getStacksWithTerminalsToReduce() { + return stacksWithTerminalsToReduce; + } + + public Stack> getUnexpandableNodes() { + return unexpandableNodes; + } + + public Stack> getUnmatchableLeafNodes() { + return unmatchableLeafNodes; + } + + public DoubleStack, AbstractNode>, AbstractStackNode

> getUnmatchableMidProductionNodes() { + return unmatchableMidProductionNodes; + } + + public DoubleStack, AbstractNode> getFilteredNodes() { + return filteredNodes; + } } diff --git a/src/org/rascalmpl/parser/gtd/debug/IDebugListener.java b/src/org/rascalmpl/parser/gtd/debug/IDebugListener.java index ae412bde870..3a8f18a07c7 100644 --- a/src/org/rascalmpl/parser/gtd/debug/IDebugListener.java +++ b/src/org/rascalmpl/parser/gtd/debug/IDebugListener.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2012-2013 CWI + * Copyright (c) 2012-2024 CWI * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -16,6 +16,9 @@ import org.rascalmpl.parser.gtd.result.struct.Link; import org.rascalmpl.parser.gtd.stack.AbstractStackNode; import org.rascalmpl.parser.gtd.stack.edge.EdgesSet; +import org.rascalmpl.parser.gtd.util.DoubleArrayList; +import org.rascalmpl.parser.gtd.util.DoubleStack; +import org.rascalmpl.parser.gtd.util.Stack; public interface IDebugListener

{ void shifting(int offset, int[] input, PositionStore positionStore); @@ -47,4 +50,14 @@ public interface IDebugListener

{ void filteredByEnterFilter(AbstractStackNode

node); void filteredByCompletionFilter(AbstractStackNode

node, AbstractNode result); + + void reviving(int[] input, + int location, + Stack> unexpandableNodes, + Stack> unmatchableLeafNodes, + DoubleStack, AbstractNode>, + AbstractStackNode

> unmatchableMidProductionNodes, + DoubleStack, AbstractNode> filteredNodes); + + void revived(DoubleArrayList, AbstractNode> recoveredNodes); } diff --git a/src/org/rascalmpl/parser/gtd/recovery/IRecoverer.java b/src/org/rascalmpl/parser/gtd/recovery/IRecoverer.java index f7db2b352ab..40dc4d52257 100644 --- a/src/org/rascalmpl/parser/gtd/recovery/IRecoverer.java +++ b/src/org/rascalmpl/parser/gtd/recovery/IRecoverer.java @@ -17,7 +17,22 @@ import org.rascalmpl.parser.gtd.util.DoubleStack; import org.rascalmpl.parser.gtd.util.Stack; -public interface IRecoverer

{ +public interface IRecoverer

{ + /** + * reviveStacks is called when the parser is unable to make more progress and the end-of-input + * has not been reached. The parameters provide insight into the current state of the parser + * and some of its history. With this information new stack nodes may be generated for the parser + * to continue with. It is up to the reviveStacks method to make sure the parser still generates + * derivation trees that cover the entire input. + * + * @param input the 24-bit unicode input character array + * @param location the current character offset in the input that the parser got stuck on + * @param unexpandableNodes these are non-terminals that were predicted at this location but did not fly + * @param unmatchableLeafNodes these are the terminals that were predicted but did not fly + * @param unmatchableMidProductionNodes these are quasi-non-terminals due to prefix sharing that did not fly + * @param filteredNodes these are non-terminals nodes that did not fly due to a disambiguation filter + * @return a list of new predictions for the parser to continue with + */ DoubleArrayList, AbstractNode> reviveStacks(int[] input, int location, Stack> unexpandableNodes, diff --git a/src/org/rascalmpl/parser/gtd/result/AbstractContainerNode.java b/src/org/rascalmpl/parser/gtd/result/AbstractContainerNode.java index 1a9d8373f59..f57f543ec94 100644 --- a/src/org/rascalmpl/parser/gtd/result/AbstractContainerNode.java +++ b/src/org/rascalmpl/parser/gtd/result/AbstractContainerNode.java @@ -15,6 +15,9 @@ import org.rascalmpl.parser.gtd.result.struct.Link; import org.rascalmpl.parser.gtd.util.ArrayList; +import org.rascalmpl.parser.util.DebugUtil; + +import io.usethesource.vallang.IConstructor; /** * All nodes in the resulting tree that can contain other nodes are a subtype @@ -147,4 +150,37 @@ public ArrayList

getAdditionalProductions(){ public ArrayList getAdditionalAlternatives(){ return alternatives; } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("offset=" + offset); + builder.append(",endOffset=" + endOffset); + if (isNullable) { + builder.append(",nullable"); + } + if (isSeparator) { + builder.append(",separator"); + } + if (isLayout) { + builder.append(",layout"); + } + if (firstAlternative != null) { + builder.append(",alternatives=["); + builder.append(firstAlternative); + builder.append(":"); + builder.append(DebugUtil.prodToString((IConstructor) firstProduction)); + + if (alternatives != null) { + for (int i=0; i implements INodeFlattener{ private final CharNodeFlattener charNodeConverter; private final LiteralNodeFlattener literalNodeConverter; private final SortContainerNodeFlattener sortContainerNodeConverter; private final ListContainerNodeFlattener listContainerNodeConverter; - private final RecoveryNodeFlattener recoveryNodeConverter; + private final SkippedNodeFlattener skippedNodeConverter; public DefaultNodeFlattener(){ super(); @@ -40,7 +39,7 @@ public DefaultNodeFlattener(){ literalNodeConverter = new LiteralNodeFlattener(); sortContainerNodeConverter = new SortContainerNodeFlattener(); listContainerNodeConverter = new ListContainerNodeFlattener(); - recoveryNodeConverter = new RecoveryNodeFlattener(); + skippedNodeConverter = new SkippedNodeFlattener(); } /** @@ -53,6 +52,7 @@ protected static class IsInError{ /** * Convert the given node. */ + @SuppressWarnings("unchecked") public T convert(INodeConstructorFactory nodeConstructorFactory, AbstractNode node, IndexedStack stack, int depth, CycleMark cycleMark, PositionStore positionStore, FilteringTracker filteringTracker, IActionExecutor actionExecutor, Object environment){ switch(node.getTypeIdentifier()){ case CharNode.ID: @@ -66,7 +66,7 @@ public T convert(INodeConstructorFactory nodeConstructorFactory, AbstractN case RecoveredNode.ID: return convert(nodeConstructorFactory, ((SortContainerNode) node).getFirstAlternative().getNode(), stack, depth, cycleMark, positionStore, filteringTracker, actionExecutor, environment); case SkippedNode.ID: - return recoveryNodeConverter.convertToUPTR(nodeConstructorFactory, (SkippedNode) node); + return skippedNodeConverter.convertToUPTR(nodeConstructorFactory, (SkippedNode) node, positionStore); default: throw new RuntimeException("Incorrect result node id: "+node.getTypeIdentifier()); } @@ -76,6 +76,6 @@ public T convert(INodeConstructorFactory nodeConstructorFactory, AbstractN * Converts the given parse tree to a tree in UPTR format. */ public T convert(INodeConstructorFactory nodeConstructorFactory, AbstractNode parseTree, PositionStore positionStore, FilteringTracker filteringTracker, IActionExecutor actionExecutor, Object rootEnvironment){ - return convert(nodeConstructorFactory, parseTree, new IndexedStack(), 0, new CycleMark(), positionStore, filteringTracker, actionExecutor, rootEnvironment); + return convert(nodeConstructorFactory, parseTree, new IndexedStack<>(), 0, new CycleMark(), positionStore, filteringTracker, actionExecutor, rootEnvironment); } } diff --git a/src/org/rascalmpl/parser/gtd/result/out/INodeConstructorFactory.java b/src/org/rascalmpl/parser/gtd/result/out/INodeConstructorFactory.java index e79ae43757b..1de4b441a58 100644 --- a/src/org/rascalmpl/parser/gtd/result/out/INodeConstructorFactory.java +++ b/src/org/rascalmpl/parser/gtd/result/out/INodeConstructorFactory.java @@ -38,8 +38,10 @@ public interface INodeConstructorFactory { T createListAmbiguityNode(ArrayList alternatives); - T createRecoveryNode(int[] characters); - + T createSkippedNode(int[] unrecognizedCharacters); + + T createErrorNode(ArrayList children, Object production); + ArrayList getChildren(T node); P createPositionInformation(URI input, int offset, int endOffset, PositionStore positionStore); diff --git a/src/org/rascalmpl/parser/gtd/result/out/RecoveryNodeFlattener.java b/src/org/rascalmpl/parser/gtd/result/out/SkippedNodeFlattener.java similarity index 52% rename from src/org/rascalmpl/parser/gtd/result/out/RecoveryNodeFlattener.java rename to src/org/rascalmpl/parser/gtd/result/out/SkippedNodeFlattener.java index 833da06dcd4..32191551dd2 100644 --- a/src/org/rascalmpl/parser/gtd/result/out/RecoveryNodeFlattener.java +++ b/src/org/rascalmpl/parser/gtd/result/out/SkippedNodeFlattener.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2009-2013 CWI + * Copyright (c) 2009-2024 CWI * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -11,18 +11,28 @@ *******************************************************************************/ package org.rascalmpl.parser.gtd.result.out; +import org.rascalmpl.parser.gtd.location.PositionStore; import org.rascalmpl.parser.gtd.result.SkippedNode; /** * A converter for result nodes that contain skipped characters for error recovery */ -public class RecoveryNodeFlattener{ - - public RecoveryNodeFlattener(){ +public class SkippedNodeFlattener{ + public SkippedNodeFlattener(){ super(); } - public T convertToUPTR(INodeConstructorFactory nodeConstructorFactory, SkippedNode node){ - return nodeConstructorFactory.createRecoveryNode(node.getSkippedChars()); + public T convertToUPTR(INodeConstructorFactory nodeConstructorFactory, SkippedNode node, PositionStore positionStore){ + T result = nodeConstructorFactory.createSkippedNode(node.getSkippedChars()); + + // Add source location + if (node.getInputUri() != null) { + int startOffset = node.getOffset(); + int endOffset = startOffset + node.getLength(); + P sourceLocation = nodeConstructorFactory.createPositionInformation(node.getInputUri(), startOffset, endOffset, positionStore); + result = nodeConstructorFactory.addPositionInformation(result, sourceLocation); + } + + return result; } } diff --git a/src/org/rascalmpl/parser/gtd/result/struct/Link.java b/src/org/rascalmpl/parser/gtd/result/struct/Link.java index 105da47b9e0..22e805f91da 100644 --- a/src/org/rascalmpl/parser/gtd/result/struct/Link.java +++ b/src/org/rascalmpl/parser/gtd/result/struct/Link.java @@ -35,4 +35,8 @@ public ArrayList getPrefixes(){ public AbstractNode getNode(){ return node; } + + public String toString() { + return "Link[node=" + node + ", prefixes=" + (prefixes == null ? 0 : prefixes.size()) + "]"; + } } diff --git a/src/org/rascalmpl/parser/gtd/stack/AbstractMatchableStackNode.java b/src/org/rascalmpl/parser/gtd/stack/AbstractMatchableStackNode.java index 41951569714..fddba15db90 100644 --- a/src/org/rascalmpl/parser/gtd/stack/AbstractMatchableStackNode.java +++ b/src/org/rascalmpl/parser/gtd/stack/AbstractMatchableStackNode.java @@ -25,7 +25,11 @@ public abstract class AbstractMatchableStackNode

extends AbstractStackNode

protected AbstractMatchableStackNode(int id, int dot){ super(id, dot); } - + + protected AbstractMatchableStackNode(int id, int dot, int startLocation){ + super(id, dot, startLocation); + } + protected AbstractMatchableStackNode(int id, int dot, IEnterFilter[] enterFilters, ICompletionFilter[] completionFilters){ super(id, dot, enterFilters, completionFilters); } @@ -62,8 +66,9 @@ public AbstractStackNode

getEmptyChild(){ throw new UnsupportedOperationException(); } + @Override public final boolean isMatchable(){ return true; } - + } diff --git a/src/org/rascalmpl/parser/gtd/stack/AbstractStackNode.java b/src/org/rascalmpl/parser/gtd/stack/AbstractStackNode.java index 562a2c1303c..99087f718cd 100644 --- a/src/org/rascalmpl/parser/gtd/stack/AbstractStackNode.java +++ b/src/org/rascalmpl/parser/gtd/stack/AbstractStackNode.java @@ -11,6 +11,8 @@ *******************************************************************************/ package org.rascalmpl.parser.gtd.stack; +import java.util.Arrays; + import org.rascalmpl.parser.gtd.result.AbstractNode; import org.rascalmpl.parser.gtd.result.struct.Link; import org.rascalmpl.parser.gtd.stack.edge.EdgesSet; @@ -20,6 +22,9 @@ import org.rascalmpl.parser.gtd.util.BitSet; import org.rascalmpl.parser.gtd.util.IntegerList; import org.rascalmpl.parser.gtd.util.IntegerObjectList; +import org.rascalmpl.parser.util.DebugUtil; + +import io.usethesource.vallang.IConstructor; @SuppressWarnings({"unchecked", "cast"}) public abstract class AbstractStackNode

{ @@ -28,8 +33,10 @@ public abstract class AbstractStackNode

{ protected AbstractStackNode

[] production; protected AbstractStackNode

[][] alternateProductions; - - protected IntegerObjectList> edgesMap; + + // Our edges + protected IntegerObjectList> edgesMap; // key=startLocation, value=EdgesSet at that location + // Edges of our children protected ArrayList[] prefixesMap; protected EdgesSet

incomingEdges; @@ -56,17 +63,21 @@ public abstract class AbstractStackNode

{ private IntegerList propagatedReductions; protected AbstractStackNode(int id, int dot){ + this(id, dot, DEFAULT_START_LOCATION); + } + + protected AbstractStackNode(int id, int dot, int startLocation) { super(); this.id = id; this.dot = dot; - this.startLocation = DEFAULT_START_LOCATION; + this.startLocation = startLocation; this.enterFilters = null; this.completionFilters = null; } - + protected AbstractStackNode(int id, int dot, IEnterFilter[] enterFilters, ICompletionFilter[] completionFilters){ super(); @@ -180,7 +191,7 @@ public boolean isExpandable(){ * Returns the name associated with the symbol in this node (optional operation). */ public abstract String getName(); - + /** * Check whether of this this node is equal to the given node. */ @@ -579,7 +590,10 @@ public int updateOvertakenNode(AbstractStackNode

predecessor, AbstractNode re // Initialize the prefixes map. int edgesMapSize = edgesMap.size(); - int possibleMaxSize = edgesMapSize + edgesMapSize; + // Before error recovery: int possibleMaxSize = edgesMapSize + edgesMapSize; + // It is unclear why error recovery can cause more edges to be added than previously accounted for, + // although this might just have been a bug. + int possibleMaxSize = edgesMapSize + edgesMapToAdd.size(); if(prefixesMap == null){ prefixesMap = new ArrayList[possibleMaxSize]; }else{ @@ -640,7 +654,9 @@ public int updateOvertakenNullableNode(AbstractStackNode

predecessor, Abstrac // Initialize the prefixes map. int edgesMapSize = edgesMap.size(); - int possibleMaxSize = edgesMapSize + potentialNewEdges; + // Before error recovery: int possibleMaxSize = edgesMapSize + potentialNewEdges; + // It is unclear why error recovery can cause more edges to be added than previously accounted for. + int possibleMaxSize = edgesMapSize + edgesMapToAdd.size(); if(prefixesMap == null){ prefixesMap = new ArrayList[possibleMaxSize]; @@ -653,7 +669,7 @@ public int updateOvertakenNullableNode(AbstractStackNode

predecessor, Abstrac } if(propagatedPrefixes == null){ - propagatedPrefixes = new BitSet(edgesMapSize); + propagatedPrefixes = new BitSet(possibleMaxSize); }else{ propagatedPrefixes.enlargeTo(possibleMaxSize); } @@ -736,7 +752,64 @@ public IntegerList getPropagatedReductions(){ return propagatedReductions; } - + + /** + * Return a short string representation of this object for debugging purposes + */ + public abstract String toShortString(); + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(id + "." + dot + "@" + startLocation); + if (production != null) { + builder.append(",prod=["); + boolean first = true; + for (AbstractStackNode

prodElem : production) { + if (first) { + first = false; + } else { + builder.append(","); + } + builder.append(prodElem.toShortString()); + } + builder.append("]"); + } + if (isEndNode) { + builder.append(",endNode"); + } + if (isSeparator) { + builder.append(",separator"); + } + if (isLayout) { + builder.append(",layout"); + } + + if (alternateProductions != null && alternateProductions.length != 0) { + builder.append(",alternateProductions=" + Arrays.toString(alternateProductions)); + } + if (prefixesMap != null && prefixesMap.length != 0) { + builder.append(",prefixes=" + Arrays.toString(prefixesMap)); + } + if (incomingEdges != null && incomingEdges.size() != 0) { + builder.append(",incomingEdges=" + incomingEdges); + } + if (alternativeProduction != null) { + builder.append(",alternativeProduction=" + DebugUtil.prodToString((IConstructor)alternativeProduction)); + } + if (propagatedPrefixes != null) { + builder.append(",propagatedPrefixes=" + propagatedPrefixes); + } + if (propagatedReductions != null) { + builder.append(",propagatedReductions=" + propagatedReductions); + } + + // Do not print filters for now. + + return builder.toString(); + } + + public abstract R accept(StackNodeVisitor visitor); + // Matchables. /** * Matches the symbol associated with this node to the input at the specified location. diff --git a/src/org/rascalmpl/parser/gtd/stack/AlternativeStackNode.java b/src/org/rascalmpl/parser/gtd/stack/AlternativeStackNode.java index 041e9273ce9..928040e92ea 100644 --- a/src/org/rascalmpl/parser/gtd/stack/AlternativeStackNode.java +++ b/src/org/rascalmpl/parser/gtd/stack/AlternativeStackNode.java @@ -47,7 +47,7 @@ private AlternativeStackNode(AlternativeStackNode

original, int startLocation children = original.children; } - + /** * Generates and initializes the alternatives for this alternative. */ @@ -88,6 +88,12 @@ public AbstractStackNode

getEmptyChild(){ throw new UnsupportedOperationException(); } + @Override + public String toShortString() { + return name; + } + + @Override public String toString(){ StringBuilder sb = new StringBuilder(); sb.append("alt"); @@ -99,10 +105,16 @@ public String toString(){ return sb.toString(); } + @Override public int hashCode(){ return production.hashCode(); } + @Override + public boolean equals(Object peer) { + return super.equals(peer); + } + public boolean isEqual(AbstractStackNode

stackNode){ if(!(stackNode instanceof AlternativeStackNode)) return false; @@ -112,4 +124,10 @@ public boolean isEqual(AbstractStackNode

stackNode){ return hasEqualFilters(stackNode); } + + @Override + public R accept(StackNodeVisitor visitor) { + return visitor.visit(this); + } + } diff --git a/src/org/rascalmpl/parser/gtd/stack/CaseInsensitiveLiteralStackNode.java b/src/org/rascalmpl/parser/gtd/stack/CaseInsensitiveLiteralStackNode.java index 9bf625a33fb..bf04c2cc309 100644 --- a/src/org/rascalmpl/parser/gtd/stack/CaseInsensitiveLiteralStackNode.java +++ b/src/org/rascalmpl/parser/gtd/stack/CaseInsensitiveLiteralStackNode.java @@ -68,10 +68,9 @@ private static int[][] fill(int[] ciLiteral){ int[][] ciLiteralResult = new int[nrOfCharacters][]; for(int i = nrOfCharacters - 1; i >= 0; --i){ int character = ciLiteral[i]; - int type = Character.getType(character); - if(type == Character.LOWERCASE_LETTER){ + if (Character.isLowerCase(character)) { ciLiteralResult[i] = new int[]{character, Character.toUpperCase(character)}; - }else if(type == Character.UPPERCASE_LETTER){ + } else if(Character.isUpperCase(character)) { ciLiteralResult[i] = new int[]{character, Character.toLowerCase(character)}; }else{ ciLiteralResult[i] = new int[]{character}; @@ -103,11 +102,11 @@ public AbstractNode match(int[] input, int location){ } public AbstractStackNode

getCleanCopy(int startLocation){ - return new CaseInsensitiveLiteralStackNode

(this, startLocation); + return new CaseInsensitiveLiteralStackNode<>(this, startLocation); } public AbstractStackNode

getCleanCopyWithResult(int startLocation, AbstractNode result){ - return new CaseInsensitiveLiteralStackNode

(this, startLocation, result); + return new CaseInsensitiveLiteralStackNode<>(this, startLocation, result); } public int getLength(){ @@ -117,7 +116,21 @@ public int getLength(){ public AbstractNode getResult(){ return result; } + + public int[][] getLiteral() { + return ciLiteral; + } + @Override + public String toShortString() { + int[] codePoints = new int[ciLiteral.length]; + for (int i=0; i stackNode){ if(!(stackNode instanceof CaseInsensitiveLiteralStackNode)) return false; @@ -144,4 +164,10 @@ public boolean isEqual(AbstractStackNode

stackNode){ return hasEqualFilters(stackNode); } + + @Override + public R accept(StackNodeVisitor visitor) { + return visitor.visit(this); + } + } diff --git a/src/org/rascalmpl/parser/gtd/stack/CharStackNode.java b/src/org/rascalmpl/parser/gtd/stack/CharStackNode.java index ff2d8b2e326..de4a8b25198 100644 --- a/src/org/rascalmpl/parser/gtd/stack/CharStackNode.java +++ b/src/org/rascalmpl/parser/gtd/stack/CharStackNode.java @@ -71,11 +71,11 @@ public AbstractNode match(int[] input, int location){ } public AbstractStackNode

getCleanCopy(int startLocation){ - return new CharStackNode

(this, startLocation); + return new CharStackNode<>(this, startLocation); } public AbstractStackNode

getCleanCopyWithResult(int startLocation, AbstractNode result){ - return new CharStackNode

(this, startLocation, result); + return new CharStackNode<>(this, startLocation, result); } public int getLength(){ @@ -86,28 +86,53 @@ public AbstractNode getResult(){ return result; } + @Override + public String toShortString() { + StringBuilder sb = new StringBuilder(); + for (int i=0; i 0) { + sb.append(','); + } + + int[] range = ranges[i]; + sb.append(codePointToString(range[0])); + if (range[0] != range[1]) { + sb.append('-'); + sb.append(codePointToString(range[1])); + } + } + + return sb.toString(); + } + + private String codePointToString(int codePoint) { + if (Character.isLetterOrDigit(codePoint)) { + return new String(Character.toChars(codePoint)); + } + + return String.valueOf(codePoint); + } + + @Override public String toString(){ StringBuilder sb = new StringBuilder(); - sb.append('['); + sb.append("CharStackNode[class="); int[] range = ranges[0]; sb.append(range[0]); sb.append('-'); sb.append(range[1]); - for(int i = ranges.length - 2; i >= 0; --i){ + for(int i = 1; i stackNode){ if(!(stackNode instanceof CharStackNode)) return false; @@ -143,4 +173,10 @@ public boolean isEqual(AbstractStackNode

stackNode){ return hasEqualFilters(stackNode); } + + @Override + public R accept(StackNodeVisitor visitor) { + return visitor.visit(this); + } + } diff --git a/src/org/rascalmpl/parser/gtd/stack/EmptyStackNode.java b/src/org/rascalmpl/parser/gtd/stack/EmptyStackNode.java index 79297837d49..6d0032b5309 100644 --- a/src/org/rascalmpl/parser/gtd/stack/EmptyStackNode.java +++ b/src/org/rascalmpl/parser/gtd/stack/EmptyStackNode.java @@ -80,6 +80,12 @@ public AbstractStackNode

getEmptyChild(){ return emptyChild; } + @Override + public String toShortString() { + return name; + } + + @Override public String toString(){ StringBuilder sb = new StringBuilder(); sb.append(name); @@ -90,13 +96,26 @@ public String toString(){ return sb.toString(); } + @Override public int hashCode(){ return 1; } + @Override + public boolean equals(Object peer) { + return super.equals(peer); + } + + @Override public boolean isEqual(AbstractStackNode

stackNode){ if(!(stackNode instanceof EmptyStackNode)) return false; return hasEqualFilters(stackNode); } + + @Override + public R accept(StackNodeVisitor visitor) { + return visitor.visit(this); + } + } diff --git a/src/org/rascalmpl/parser/gtd/stack/EpsilonStackNode.java b/src/org/rascalmpl/parser/gtd/stack/EpsilonStackNode.java index dfa16c05fed..369326ac0db 100644 --- a/src/org/rascalmpl/parser/gtd/stack/EpsilonStackNode.java +++ b/src/org/rascalmpl/parser/gtd/stack/EpsilonStackNode.java @@ -54,11 +54,11 @@ public AbstractNode match(int[] input, int location){ } public AbstractStackNode

getCleanCopy(int startLocation){ - return new EpsilonStackNode

(this, startLocation); + return new EpsilonStackNode<>(this, startLocation); } public AbstractStackNode

getCleanCopyWithResult(int startLocation, AbstractNode result){ - return new EpsilonStackNode

(this, startLocation, result); + return new EpsilonStackNode<>(this, startLocation, result); } public int getLength(){ @@ -69,6 +69,12 @@ public AbstractNode getResult(){ return result; } + @Override + public String toShortString() { + return "\u03B5"; + } + + @Override public String toString(){ StringBuilder sb = new StringBuilder(); sb.append('('); @@ -78,13 +84,25 @@ public String toString(){ return sb.toString(); } + @Override public int hashCode(){ return 0; } + @Override + public boolean equals(Object peer) { + return super.equals(peer); + } + public boolean isEqual(AbstractStackNode

stackNode){ if(!(stackNode instanceof EpsilonStackNode)) return false; return hasEqualFilters(stackNode); } + + @Override + public R accept(StackNodeVisitor visitor) { + return visitor.visit(this); + } + } diff --git a/src/org/rascalmpl/parser/gtd/stack/ListStackNode.java b/src/org/rascalmpl/parser/gtd/stack/ListStackNode.java index 7f5734b2a10..5856d526699 100644 --- a/src/org/rascalmpl/parser/gtd/stack/ListStackNode.java +++ b/src/org/rascalmpl/parser/gtd/stack/ListStackNode.java @@ -78,7 +78,7 @@ public String getName(){ } public AbstractStackNode

getCleanCopy(int startLocation){ - return new ListStackNode

(this, startLocation); + return new ListStackNode<>(this, startLocation); } public AbstractStackNode

[] getChildren(){ @@ -93,6 +93,12 @@ public AbstractStackNode

getEmptyChild(){ return emptyChild; } + @Override + public String toShortString() { + return name; + } + + @Override public String toString(){ StringBuilder sb = new StringBuilder(); sb.append(name); @@ -103,10 +109,16 @@ public String toString(){ return sb.toString(); } + @Override public int hashCode(){ return production.hashCode(); } + @Override + public boolean equals(Object peer) { + return super.equals(peer); + } + public boolean isEqual(AbstractStackNode

stackNode){ if(!(stackNode instanceof ListStackNode)) return false; @@ -116,4 +128,10 @@ public boolean isEqual(AbstractStackNode

stackNode){ return hasEqualFilters(stackNode); } + + @Override + public R accept(StackNodeVisitor visitor) { + return visitor.visit(this); + } + } diff --git a/src/org/rascalmpl/parser/gtd/stack/LiteralStackNode.java b/src/org/rascalmpl/parser/gtd/stack/LiteralStackNode.java index 0dafc11f539..bec131eff73 100644 --- a/src/org/rascalmpl/parser/gtd/stack/LiteralStackNode.java +++ b/src/org/rascalmpl/parser/gtd/stack/LiteralStackNode.java @@ -39,6 +39,10 @@ public LiteralStackNode(int id, int dot, P production, int[] literal, IEnterFilt result = new LiteralNode(production, literal); } + + public int[] getLiteral() { + return literal; + } private LiteralStackNode(LiteralStackNode

original, int startLocation){ super(original, startLocation); @@ -76,24 +80,34 @@ public int getLength(){ public AbstractNode getResult(){ return result; } + + @Override + public String toShortString() { + return "'" + new String(literal, 0, literal.length) + "'"; + } + @Override public String toString(){ - StringBuilder sb = new StringBuilder(); - for (int i : literal) { - sb.appendCodePoint(i); - } - sb.append(getId()); - sb.append('('); - sb.append(startLocation); - sb.append(')'); + StringBuilder sb = new StringBuilder("lit['"); + sb.append(new String(literal, 0, literal.length)); + sb.append("',"); + sb.append(super.toString()); + sb.append(']'); + return sb.toString(); } + @Override public int hashCode(){ return production.hashCode(); } + @Override + public boolean equals(Object peer) { + return super.equals(peer); + } + public boolean isEqual(AbstractStackNode

stackNode){ if(!(stackNode instanceof LiteralStackNode)) return false; @@ -103,4 +117,10 @@ public boolean isEqual(AbstractStackNode

stackNode){ return hasEqualFilters(stackNode); } + + @Override + public R accept(StackNodeVisitor visitor) { + return visitor.visit(this); + } + } diff --git a/src/org/rascalmpl/parser/gtd/stack/MultiCharacterStackNode.java b/src/org/rascalmpl/parser/gtd/stack/MultiCharacterStackNode.java index 6543b14f9e1..6f1c2900d91 100644 --- a/src/org/rascalmpl/parser/gtd/stack/MultiCharacterStackNode.java +++ b/src/org/rascalmpl/parser/gtd/stack/MultiCharacterStackNode.java @@ -104,9 +104,9 @@ public AbstractNode getResult(){ return result; } - public String toString(){ + @Override + public String toShortString() { StringBuilder sb = new StringBuilder(); - sb.append('['); for(int i = characters.length - 1; i >= 0; --i){ int[] range = characters[i]; @@ -115,21 +115,21 @@ public String toString(){ sb.append(range[1]); } sb.append(']'); + return toString(); + } - sb.append(getId()); - sb.append('('); - sb.append(startLocation); - sb.append(')'); - - return sb.toString(); + @Override + public String toString(){ + return toShortString(); } + @Override public int hashCode(){ int hash = 0; for(int i = characters.length - 1; i >= 0; --i){ int[] chars = characters[i]; - for(int j = chars.length - 1; j <= 0; --j){ + for(int j = chars.length - 1; j >= 0; --j){ hash = hash << 3 + hash >> 5; hash ^= chars[0] + (chars[1] << 2); } @@ -138,6 +138,11 @@ public int hashCode(){ return hash; } + @Override + public boolean equals(Object peer) { + return super.equals(peer); + } + public boolean isEqual(AbstractStackNode

stackNode){ if(!(stackNode instanceof MultiCharacterStackNode)) return false; @@ -163,4 +168,10 @@ public boolean isEqual(AbstractStackNode

stackNode){ return hasEqualFilters(stackNode); } + + @Override + public R accept(StackNodeVisitor visitor) { + return visitor.visit(this); + } + } diff --git a/src/org/rascalmpl/parser/gtd/stack/NonTerminalStackNode.java b/src/org/rascalmpl/parser/gtd/stack/NonTerminalStackNode.java index 7f00fe8397b..3d971f8112f 100644 --- a/src/org/rascalmpl/parser/gtd/stack/NonTerminalStackNode.java +++ b/src/org/rascalmpl/parser/gtd/stack/NonTerminalStackNode.java @@ -49,7 +49,7 @@ public AbstractNode match(int[] input, int location){ } public AbstractStackNode

getCleanCopy(int startLocation){ - return new NonTerminalStackNode

(this, startLocation); + return new NonTerminalStackNode<>(this, startLocation); } public AbstractStackNode

getCleanCopyWithResult(int startLocation, AbstractNode result){ @@ -76,21 +76,32 @@ public AbstractNode getResult(){ throw new UnsupportedOperationException(); } + @Override + public String toShortString() { + return expectIdentifier; + } + + @Override public String toString(){ - StringBuilder sb = new StringBuilder(); + StringBuilder sb = new StringBuilder("NonTerminal["); sb.append(expectIdentifier); - sb.append(getId()); - sb.append('('); - sb.append(startLocation); - sb.append(')'); + sb.append(","); + sb.append(super.toString()); + sb.append("]"); return sb.toString(); } + @Override public int hashCode(){ return expectIdentifier.hashCode(); } + @Override + public boolean equals(Object peer) { + return super.equals(peer); + } + public boolean isEqual(AbstractStackNode

stackNode){ if(!(stackNode instanceof NonTerminalStackNode)) return false; @@ -100,4 +111,10 @@ public boolean isEqual(AbstractStackNode

stackNode){ return hasEqualFilters(stackNode); } + + @Override + public R accept(StackNodeVisitor visitor) { + return visitor.visit(this); + } + } diff --git a/src/org/rascalmpl/parser/gtd/stack/OptionalStackNode.java b/src/org/rascalmpl/parser/gtd/stack/OptionalStackNode.java index 4b916f2a3be..5e258e92a8e 100644 --- a/src/org/rascalmpl/parser/gtd/stack/OptionalStackNode.java +++ b/src/org/rascalmpl/parser/gtd/stack/OptionalStackNode.java @@ -92,6 +92,12 @@ public AbstractStackNode

getEmptyChild(){ return emptyChild; } + @Override + public String toShortString() { + return name; + } + + @Override public String toString(){ StringBuilder sb = new StringBuilder(); sb.append(name); @@ -102,10 +108,16 @@ public String toString(){ return sb.toString(); } + @Override public int hashCode(){ return production.hashCode(); } + @Override + public boolean equals(Object peer) { + return super.equals(peer); + } + public boolean isEqual(AbstractStackNode

stackNode){ if(!(stackNode instanceof OptionalStackNode)) return false; @@ -115,4 +127,10 @@ public boolean isEqual(AbstractStackNode

stackNode){ return hasEqualFilters(stackNode); } + + @Override + public R accept(StackNodeVisitor visitor) { + return visitor.visit(this); + } + } diff --git a/src/org/rascalmpl/parser/gtd/stack/RecoveryPointStackNode.java b/src/org/rascalmpl/parser/gtd/stack/RecoveryPointStackNode.java index 60daa28bc93..a1f1358d393 100644 --- a/src/org/rascalmpl/parser/gtd/stack/RecoveryPointStackNode.java +++ b/src/org/rascalmpl/parser/gtd/stack/RecoveryPointStackNode.java @@ -58,7 +58,7 @@ public boolean isEndNode() { @Override public String getName(){ - return "***robust:" + name + "***"; + return name; } public AbstractNode match(int[] input, int location){ @@ -93,6 +93,12 @@ public AbstractNode getResult(){ throw new UnsupportedOperationException(); } + @Override + public String toShortString() { + return name; + } + + @Override public String toString(){ StringBuilder sb = new StringBuilder(); sb.append(getName()); @@ -105,10 +111,16 @@ public String toString(){ return sb.toString(); } + @Override public int hashCode(){ return getName().hashCode(); } + @Override + public boolean equals(Object peer) { + return super.equals(peer); + } + public boolean isEqual(AbstractStackNode

stackNode){ if(!(stackNode instanceof RecoveryPointStackNode)) return false; @@ -116,4 +128,10 @@ public boolean isEqual(AbstractStackNode

stackNode){ return otherNode.name.equals(name) && otherNode.startLocation == startLocation; } + + @Override + public R accept(StackNodeVisitor visitor) { + return visitor.visit(this); + } + } diff --git a/src/org/rascalmpl/parser/gtd/stack/SeparatedListStackNode.java b/src/org/rascalmpl/parser/gtd/stack/SeparatedListStackNode.java index 38adc9cf619..8cf6bbaaf23 100644 --- a/src/org/rascalmpl/parser/gtd/stack/SeparatedListStackNode.java +++ b/src/org/rascalmpl/parser/gtd/stack/SeparatedListStackNode.java @@ -31,8 +31,11 @@ public SeparatedListStackNode(int id, int dot, P production, AbstractStackNode

child, AbstractStackNode

[] separators, boolean isPlusList, IEnterFilter[] enterFilters, ICompletionFilter[] completionFilters){ super(id, dot, enterFilters, completionFilters); @@ -107,6 +110,12 @@ public AbstractStackNode

getEmptyChild(){ return emptyChild; } + @Override + public String toShortString() { + return name; + } + + @Override public String toString(){ StringBuilder sb = new StringBuilder(); sb.append(name); @@ -117,10 +126,16 @@ public String toString(){ return sb.toString(); } + @Override public int hashCode(){ return production.hashCode(); } + @Override + public boolean equals(Object peer) { + return super.equals(peer); + } + public boolean isEqual(AbstractStackNode

stackNode){ if(!(stackNode instanceof SeparatedListStackNode)) return false; @@ -130,4 +145,10 @@ public boolean isEqual(AbstractStackNode

stackNode){ return hasEqualFilters(stackNode); } + + @Override + public R accept(StackNodeVisitor visitor) { + return visitor.visit(this); + } + } diff --git a/src/org/rascalmpl/parser/gtd/stack/SequenceStackNode.java b/src/org/rascalmpl/parser/gtd/stack/SequenceStackNode.java index 638d978d00b..b7e90c362c7 100644 --- a/src/org/rascalmpl/parser/gtd/stack/SequenceStackNode.java +++ b/src/org/rascalmpl/parser/gtd/stack/SequenceStackNode.java @@ -71,7 +71,7 @@ public String getName(){ } public AbstractStackNode

getCleanCopy(int startLocation){ - return new SequenceStackNode

(this, startLocation); + return new SequenceStackNode<>(this, startLocation); } public AbstractStackNode

[] getChildren(){ @@ -86,6 +86,12 @@ public AbstractStackNode

getEmptyChild(){ throw new UnsupportedOperationException(); } + @Override + public String toShortString() { + return name; + } + + @Override public String toString(){ StringBuilder sb = new StringBuilder(); sb.append("seq"); @@ -97,10 +103,16 @@ public String toString(){ return sb.toString(); } + @Override public int hashCode(){ return production.hashCode(); } + @Override + public boolean equals(Object peer) { + return super.equals(peer); + } + public boolean isEqual(AbstractStackNode

stackNode){ if(!(stackNode instanceof SequenceStackNode)) return false; @@ -110,4 +122,10 @@ public boolean isEqual(AbstractStackNode

stackNode){ return hasEqualFilters(stackNode); } + + @Override + public R accept(StackNodeVisitor visitor) { + return visitor.visit(this); + } + } diff --git a/src/org/rascalmpl/parser/gtd/stack/SkippingStackNode.java b/src/org/rascalmpl/parser/gtd/stack/SkippingStackNode.java index eb243b3b6c0..06450f62c4d 100644 --- a/src/org/rascalmpl/parser/gtd/stack/SkippingStackNode.java +++ b/src/org/rascalmpl/parser/gtd/stack/SkippingStackNode.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2009-2013 CWI + * Copyright (c) 2009-2024 CWI * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -8,22 +8,60 @@ * Contributors: * * Arnold Lankamp - Arnold.Lankamp@cwi.nl + * * Pieter Olivier - Pieter.Olivier@swat.engineering *******************************************************************************/ package org.rascalmpl.parser.gtd.stack; +import java.net.URI; + import org.rascalmpl.parser.gtd.result.AbstractNode; import org.rascalmpl.parser.gtd.result.SkippedNode; public final class SkippingStackNode

extends AbstractMatchableStackNode

{ private final SkippedNode result; - public SkippingStackNode(int id, int[] until, int[] input, int startLocation, P parentProduction){ + public static SkippedNode createResultUntilCharClass(URI uri, int[] until, int[] input, int startLocation) { + for (int to = startLocation ; to < input.length; ++to) { + for (int i = 0; i < until.length; ++i) { + if (input[to] == until[i]) { + int length = to - startLocation; + return new SkippedNode(uri, createSkippedToken(input, startLocation, length), startLocation); + } + } + } + + return new SkippedNode(uri, new int[0], startLocation); + } + + public static SkippedNode createResultUntilEndOfInput(URI uri, int[] input, int startLocation) { + int length = input.length - startLocation; + return new SkippedNode(uri, createSkippedToken(input, startLocation, length), startLocation); + } + + public static SkippedNode createResultUntilChar(URI uri, int[] input, int startLocation, int endLocation) { + return new SkippedNode(uri, createSkippedToken(input, startLocation, endLocation - startLocation), startLocation); + } + + private static int[] createSkippedToken(int[] input, int startLocation, int length) { + int[] token = new int[length]; + System.arraycopy(input, startLocation, token, 0, length); + return token; + } + + public SkippingStackNode(int id, P parentProduction, SkippedNode result) { super(id, 0); - this.result = buildResult(input, until, startLocation); + this.result = result; + setAlternativeProduction(parentProduction); + } + + public SkippingStackNode(int id, P parentProduction, SkippedNode result, int startLocation) { + super(id, 0, startLocation); + + this.result = result; setAlternativeProduction(parentProduction); } - + private SkippingStackNode(SkippingStackNode

original, int startLocation){ super(original, startLocation); @@ -35,23 +73,7 @@ private SkippingStackNode(SkippingStackNode

original, SkippedNode result, int this.result = result; } - - private static SkippedNode buildResult(int[] input, int[] until, int startLocation){ - for (int to = startLocation ; to < input.length; ++to) { - for (int i = 0; i < until.length; ++i) { - if (input[to] == until[i]) { - int length = to - startLocation; - int[] chars = new int[length]; - System.arraycopy(input, startLocation, chars, 0, length); - - return new SkippedNode(chars, startLocation); - } - } - } - return new SkippedNode(new int[0], startLocation); - } - public boolean isEmptyLeafNode(){ return result.isEmpty(); } @@ -61,11 +83,11 @@ public AbstractNode match(int[] input, int location){ } public AbstractStackNode

getCleanCopy(int startLocation){ - return new SkippingStackNode

(this, startLocation); + return new SkippingStackNode<>(this, startLocation); } public AbstractStackNode

getCleanCopyWithResult(int startLocation, AbstractNode result){ - return new SkippingStackNode

(this, (SkippedNode) result, startLocation); + return new SkippingStackNode<>(this, (SkippedNode) result, startLocation); } public int getLength(){ @@ -76,25 +98,38 @@ public AbstractNode getResult(){ return result; } - public String toString(){ - StringBuilder sb = new StringBuilder(); - sb.append(getId()); - sb.append('('); - sb.append(startLocation); - sb.append(')'); - - return sb.toString(); + @Override + public String toShortString() { + return "skip(" + result.toString() + ")"; } - + + @Override + public String toString() { + return "SkippingStackNode[result=" + result + "," + super.toString() + "]"; + } + + @Override public int hashCode(){ return getParentProduction().hashCode(); } - + + @Override + public boolean equals(Object rhs) { + return super.equals(rhs); + } + public boolean isEqual(AbstractStackNode

stackNode){ - if(!(stackNode instanceof SkippingStackNode)) return false; + if ( !(stackNode instanceof SkippingStackNode)) { + return false; + } SkippingStackNode

otherNode = (SkippingStackNode

) stackNode; return otherNode.id == id; } + + @Override + public R accept(StackNodeVisitor visitor) { + return visitor.visit(this); + } } diff --git a/src/org/rascalmpl/parser/gtd/stack/StackNodeVisitor.java b/src/org/rascalmpl/parser/gtd/stack/StackNodeVisitor.java new file mode 100644 index 00000000000..fbe9b7939d8 --- /dev/null +++ b/src/org/rascalmpl/parser/gtd/stack/StackNodeVisitor.java @@ -0,0 +1,32 @@ +/** + * 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. + **/ + +package org.rascalmpl.parser.gtd.stack; + +public interface StackNodeVisitor { + R visit(AlternativeStackNode

node); + R visit(CaseInsensitiveLiteralStackNode

node); + R visit(CharStackNode

node); + R visit(EmptyStackNode

node); + R visit(EpsilonStackNode

node); + R visit(ListStackNode

node); + R visit(LiteralStackNode

node); + R visit(MultiCharacterStackNode

node); + R visit(NonTerminalStackNode

node); + R visit(OptionalStackNode

node); + R visit(RecoveryPointStackNode

node); + R visit(SeparatedListStackNode

node); + R visit(SequenceStackNode

node); + R visit(SkippingStackNode

node); +} diff --git a/src/org/rascalmpl/parser/gtd/stack/StackNodeVisitorAdapter.java b/src/org/rascalmpl/parser/gtd/stack/StackNodeVisitorAdapter.java new file mode 100644 index 00000000000..68094cc199a --- /dev/null +++ b/src/org/rascalmpl/parser/gtd/stack/StackNodeVisitorAdapter.java @@ -0,0 +1,102 @@ +/** + * 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. + **/ + +package org.rascalmpl.parser.gtd.stack; + +public class StackNodeVisitorAdapter implements StackNodeVisitor { + + @Override + public R visit(AlternativeStackNode

node) { + // Do nothing fallback + return null; + } + + @Override + public R visit(CaseInsensitiveLiteralStackNode

node) { + // Do nothing fallback + return null; + } + + @Override + public R visit(CharStackNode

node) { + // Do nothing fallback + return null; + } + + @Override + public R visit(EmptyStackNode

node) { + // Do nothing fallback + return null; + } + + @Override + public R visit(EpsilonStackNode

node) { + // Do nothing fallback + return null; + } + + @Override + public R visit(ListStackNode

node) { + // Do nothing fallback + return null; + } + + @Override + public R visit(LiteralStackNode

node) { + // Do nothing fallback + return null; + } + + @Override + public R visit(MultiCharacterStackNode

node) { + // Do nothing fallback + return null; + } + + @Override + public R visit(NonTerminalStackNode

node) { + // Do nothing fallback + return null; + } + + @Override + public R visit(OptionalStackNode

node) { + // Do nothing fallback + return null; + } + + @Override + public R visit(RecoveryPointStackNode

node) { + // Do nothing fallback + return null; + } + + @Override + public R visit(SeparatedListStackNode

node) { + // Do nothing fallback + return null; + } + + @Override + public R visit(SequenceStackNode

node) { + // Do nothing fallback + return null; + } + + @Override + public R visit(SkippingStackNode

node) { + // Do nothing fallback + return null; + } +} diff --git a/src/org/rascalmpl/parser/gtd/stack/edge/EdgesSet.java b/src/org/rascalmpl/parser/gtd/stack/edge/EdgesSet.java index b778f6677a3..351cd048f7b 100644 --- a/src/org/rascalmpl/parser/gtd/stack/edge/EdgesSet.java +++ b/src/org/rascalmpl/parser/gtd/stack/edge/EdgesSet.java @@ -22,17 +22,19 @@ @SuppressWarnings({"unchecked", "cast"}) public class EdgesSet

{ - public final static int DEFAULT_RESULT_STORE_ID = -1; + public static final int DEFAULT_RESULT_STORE_ID = -1; - private final static int DEFAULT_SIZE = 8; + private static final int DEFAULT_SIZE = 8; private AbstractStackNode

[] edges; private int size; - + private int lastVisitedLevel = -1; private IntegerMap lastVisitedFilteredLevel; private AbstractContainerNode

lastResults; + + // Indexed by `resultStoreId`. private IntegerObjectList> lastFilteredResults; public EdgesSet(){ @@ -140,4 +142,33 @@ public int size(){ public void clear(){ size = 0; } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder("EdgesSet[{"); + + for (int i=0; i 0) { + builder.append(","); + } + builder.append(edges[i].getName() + "=" + edges[i].getId() + "." + edges[i].getDot() + "@" + edges[i].getStartLocation()); + } + + builder.append("}"); + + if (lastVisitedLevel >= 0) { + builder.append(",lastVisitedLevel=" + lastVisitedLevel); + } + if (lastResults != null) { + builder.append(",lastResults=" + lastResults); + } + + // Skip 'filtered' fields for now + + builder.append("]"); + + return builder.toString(); + } + + } diff --git a/src/org/rascalmpl/parser/gtd/util/ArrayList.java b/src/org/rascalmpl/parser/gtd/util/ArrayList.java index d93c1740981..899fcdbb0d4 100644 --- a/src/org/rascalmpl/parser/gtd/util/ArrayList.java +++ b/src/org/rascalmpl/parser/gtd/util/ArrayList.java @@ -104,4 +104,17 @@ public int size(){ public Object[] getBackingArray(){ return data; } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder("["); + for (int i=0; i 0) { + builder.append(","); + } + builder.append(data[i]); + } + builder.append("]"); + return builder.toString(); + } } diff --git a/src/org/rascalmpl/parser/gtd/util/DoubleArrayList.java b/src/org/rascalmpl/parser/gtd/util/DoubleArrayList.java index e1e6d4ea9ea..37db8124f39 100644 --- a/src/org/rascalmpl/parser/gtd/util/DoubleArrayList.java +++ b/src/org/rascalmpl/parser/gtd/util/DoubleArrayList.java @@ -11,6 +11,10 @@ *******************************************************************************/ package org.rascalmpl.parser.gtd.util; +import java.util.Comparator; + +import org.apache.commons.lang3.tuple.Pair; + @SuppressWarnings("unchecked") public class DoubleArrayList{ private final static int DEFAULT_SIZE = 8; @@ -102,4 +106,40 @@ public void ditryClear(){ public int size(){ return size; } + + private java.util.List> zip() { + java.util.List> entries = new java.util.ArrayList<>(size); + for (int i=0; i> comparator) { + java.util.List> entries = zip(); + + entries.sort(comparator); + + for (int i=0; i elem = entries.get(i); + first[i] = elem.getLeft(); + second[i] = elem.getRight(); + } + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder("list["); + for (int i=0; i"); + } + builder.append("]\n"); + return builder.toString(); + } + } diff --git a/src/org/rascalmpl/parser/gtd/util/DoubleStack.java b/src/org/rascalmpl/parser/gtd/util/DoubleStack.java index 9251e89a519..3c2c6f3a6d7 100644 --- a/src/org/rascalmpl/parser/gtd/util/DoubleStack.java +++ b/src/org/rascalmpl/parser/gtd/util/DoubleStack.java @@ -171,4 +171,19 @@ public void clear(){ public void dirtyClear(){ size = 0; } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder("stack["); + for (int i=0; i"); + } + builder.append("]\n"); + return builder.toString(); + } } diff --git a/src/org/rascalmpl/parser/gtd/util/IdDispenser.java b/src/org/rascalmpl/parser/gtd/util/IdDispenser.java new file mode 100644 index 00000000000..f83fe0c4bf6 --- /dev/null +++ b/src/org/rascalmpl/parser/gtd/util/IdDispenser.java @@ -0,0 +1,19 @@ +/** + * 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. + **/ + +package org.rascalmpl.parser.gtd.util; + +public interface IdDispenser { + int dispenseId(); +} diff --git a/src/org/rascalmpl/parser/gtd/util/IntegerObjectList.java b/src/org/rascalmpl/parser/gtd/util/IntegerObjectList.java index 92d1009b02a..30b63fd46db 100644 --- a/src/org/rascalmpl/parser/gtd/util/IntegerObjectList.java +++ b/src/org/rascalmpl/parser/gtd/util/IntegerObjectList.java @@ -125,4 +125,20 @@ public void clear(){ public void dirtyClear(){ size = 0; } + + public String toString() { + StringBuilder builder = new StringBuilder("["); + + for (int i=0; i 0) { + builder.append(","); + } + builder.append(keys[i]); + builder.append("="); + builder.append(values[i]); + } + + builder.append("]"); + return builder.toString(); + } } diff --git a/src/org/rascalmpl/parser/gtd/util/StackNodeIdDispenser.java b/src/org/rascalmpl/parser/gtd/util/StackNodeIdDispenser.java new file mode 100644 index 00000000000..944da1d99a4 --- /dev/null +++ b/src/org/rascalmpl/parser/gtd/util/StackNodeIdDispenser.java @@ -0,0 +1,67 @@ +/** + * 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. + **/ + +package org.rascalmpl.parser.gtd.util; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +import org.rascalmpl.parser.gtd.IGTD; +import org.rascalmpl.values.parsetrees.ITree; + +import io.usethesource.vallang.IConstructor; +import io.usethesource.vallang.ISourceLocation; + +/** + * To offer backwards compatibility for generated parsers that do not yet have the "getFreeStackNodeId" method yet, + * this class uses reflection to find that method and otherwise improvises by just using a ridiculously high starting number. + */ +public class StackNodeIdDispenser implements IdDispenser { + private IGTD parser; + private Method dispenseMethod; + private int nextNodeIdBackup = (Integer.MAX_VALUE/4)*3; + + public StackNodeIdDispenser(IGTD parser) { + try { + dispenseMethod = parser.getClass().getMethod("getFreeStackNodeId"); + } catch (NoSuchMethodException e) { + // Custom IGTB implementation without "getFreeStackNodeId" method. No biggy, we just use nextNodeIdBackup. + } + } + + @Override + public int dispenseId() { + if (dispenseMethod != null) { + try { + return (Integer)dispenseMethod.invoke(parser); + } + catch (InvocationTargetException e) { + if (e.getTargetException() instanceof UnsupportedOperationException) { + // We are dealing with a parser class that has no generated "getFreeStackNodeId" method (yet), + // for backwards compatibility we fall back on "nextNodeIdBackup". + dispenseMethod = null; // No reason to try again. + } else { + throw new RuntimeException(e); + } + } + catch (IllegalAccessException | IllegalArgumentException e) { + throw new RuntimeException(e); + } + } + + return nextNodeIdBackup++; + } + +} + diff --git a/src/org/rascalmpl/parser/uptr/UPTRNodeFactory.java b/src/org/rascalmpl/parser/uptr/UPTRNodeFactory.java index 8a6199fa2a0..006187c7f9a 100644 --- 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.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 Object getProductionFromNode(ITree node){ } @Override - public ITree createRecoveryNode(int[] characters) { - throw new UnsupportedOperationException(); - } + public ITree createSkippedNode(int[] characters) { + return createLiteralNode(characters, SKIPPED); + } + + public ITree createErrorNode(ArrayList 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/parser/uptr/debug/DebugLogger.java b/src/org/rascalmpl/parser/uptr/debug/DebugLogger.java index 89ba535a3bc..fb05ba5d95d 100644 --- a/src/org/rascalmpl/parser/uptr/debug/DebugLogger.java +++ b/src/org/rascalmpl/parser/uptr/debug/DebugLogger.java @@ -9,6 +9,9 @@ import org.rascalmpl.parser.gtd.stack.AbstractStackNode; import org.rascalmpl.parser.gtd.stack.edge.EdgesSet; import org.rascalmpl.parser.gtd.util.ArrayList; +import org.rascalmpl.parser.gtd.util.DoubleArrayList; +import org.rascalmpl.parser.gtd.util.DoubleStack; +import org.rascalmpl.parser.gtd.util.Stack; import org.rascalmpl.values.parsetrees.ProductionAdapter; import io.usethesource.vallang.IConstructor; @@ -153,4 +156,57 @@ public void filteredByEnterFilter(AbstractStackNode node){ public void filteredByCompletionFilter(AbstractStackNode node, AbstractNode result){ out.println(String.format("Filtered by completion filter: %s", node)); } + + @Override + public void reviving(int[] input, int location, Stack> unexpandableNodes, + Stack> unmatchableLeafNodes, + DoubleStack, AbstractNode>, AbstractStackNode> unmatchableMidProductionNodes, + DoubleStack, AbstractNode> filteredNodes) { + out.print("Reviving at "); + out.print(location); + out.print(": input='"); + for (int i=0; i<8 && location+i < input.length; i++) { + out.print((char) input[location+i]); + } + out.print("', unexpandable="); + + boolean first = true; + for (int i=0; i 0) { + out.print(", unmatchableLeafNodes="); + out.print(unmatchableLeafNodes.getSize()); + } + + if (unmatchableMidProductionNodes.getSize() > 0) { + out.print(", unmatchableMidProductionNodes="); + out.print(unmatchableMidProductionNodes.toString()); + } + + if (filteredNodes.getSize() > 0) { + out.print(", filteredNodes="); + out.print(filteredNodes.getSize()); + } + + out.println(); + } + + @Override + public void revived(DoubleArrayList, AbstractNode> recoveredNodes) { + out.println("Revived nodes:"); + for (int i=0; i implements IDebugListener

{ + + @Override + public void shifting(int offset, int[] input, PositionStore positionStore) { + // Do nothing + } + + @Override + public void iterating() { + // Do nothing + } + + @Override + public void matched(AbstractStackNode

node, AbstractNode result) { + // Do nothing + } + + @Override + public void failedToMatch(AbstractStackNode

node) { + // Do nothing + } + + @Override + public void expanding(AbstractStackNode

node) { + // Do nothing + } + + @Override + public void expanded(AbstractStackNode

node, AbstractStackNode

child) { + // Do nothing + } + + @Override + public void foundIterationCachedNullableResult(AbstractStackNode

node) { + // Do nothing + } + + @Override + public void moving(AbstractStackNode

node, AbstractNode result) { + // Do nothing + } + + @Override + public void progressed(AbstractStackNode

node, AbstractNode result, AbstractStackNode

next) { + // Do nothing + } + + @Override + public void propagated(AbstractStackNode

node, AbstractNode nodeResult, AbstractStackNode

next) { + // Do nothing + } + + @Override + public void reducing(AbstractStackNode

node, Link resultLink, EdgesSet

edges) { + // Do nothing + } + + @Override + public void reduced(AbstractStackNode

parent) { + // Do nothing + } + + @Override + public void filteredByNestingRestriction(AbstractStackNode

parent) { + // Do nothing + } + + @Override + public void filteredByEnterFilter(AbstractStackNode

node) { + // Do nothing + } + + @Override + public void filteredByCompletionFilter(AbstractStackNode

node, AbstractNode result) { + // Do nothing + } + + @Override + public void reviving(int[] input, int location, Stack> unexpandableNodes, + Stack> unmatchableLeafNodes, + DoubleStack, AbstractNode>, AbstractStackNode

> unmatchableMidProductionNodes, + DoubleStack, AbstractNode> filteredNodes) { + // Do nothing + } + + @Override + public void revived(DoubleArrayList, AbstractNode> recoveredNodes) { + // Do nothing + } + +} diff --git a/src/org/rascalmpl/parser/uptr/recovery/CaseInsensitiveLiteralMatcher.java b/src/org/rascalmpl/parser/uptr/recovery/CaseInsensitiveLiteralMatcher.java new file mode 100644 index 00000000000..6c1fab53339 --- /dev/null +++ b/src/org/rascalmpl/parser/uptr/recovery/CaseInsensitiveLiteralMatcher.java @@ -0,0 +1,75 @@ +/** + * 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. + **/ + +package org.rascalmpl.parser.uptr.recovery; + +public class CaseInsensitiveLiteralMatcher implements InputMatcher { + private final int[] chars; + private final int[] altChars; + + public CaseInsensitiveLiteralMatcher(String literal) { + int length = literal.length(); + chars = new int[length]; + altChars = new int[length]; + for(int i = 0; i InputMatcher createMatcher(AbstractStackNode

stackNode) { + return stackNode.accept(new StackNodeVisitorAdapter() { + @Override + public InputMatcher visit(LiteralStackNode

literal) { + return new LiteralMatcher(literal.getLiteral()); + } + + @Override + public InputMatcher visit(CaseInsensitiveLiteralStackNode

literal) { + return new CaseInsensitiveLiteralMatcher(literal.getLiteral()); + } + }); + } +} diff --git a/src/org/rascalmpl/parser/uptr/recovery/LiteralMatcher.java b/src/org/rascalmpl/parser/uptr/recovery/LiteralMatcher.java new file mode 100644 index 00000000000..c6afd99fdd3 --- /dev/null +++ b/src/org/rascalmpl/parser/uptr/recovery/LiteralMatcher.java @@ -0,0 +1,56 @@ +/** + * Copyright (c) 2022, 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. + **/ + +package org.rascalmpl.parser.uptr.recovery; + +public class LiteralMatcher implements InputMatcher { + private int[] chars; + + public LiteralMatcher(String literal) { + chars = new int[literal.length()]; + for (int i=0; i { + private static final boolean VISUALIZE_RECOVERY_NODES = false; + + private URI uri; + private IdDispenser stackNodeIdDispenser; + private ExpectsProvider expectsProvider; + + private Set processedNodes = new HashSet<>(); + + public ToTokenRecoverer(URI uri, ExpectsProvider expectsProvider, IdDispenser stackNodeIdDispenser) { + this.uri = uri; + this.expectsProvider = expectsProvider; + this.stackNodeIdDispenser = stackNodeIdDispenser; + } + + @Override + public DoubleArrayList, AbstractNode> reviveStacks(int[] input, int location, + Stack> unexpandableNodes, + Stack> unmatchableLeafNodes, + DoubleStack, AbstractNode>, AbstractStackNode> unmatchableMidProductionNodes, + DoubleStack, AbstractNode> filteredNodes) { + + // For now we ignore unmatchable leaf nodes and filtered nodes. At some point we might use those to + // improve error recovery. + + ArrayList> failedNodes = new ArrayList<>(); + collectUnexpandableNodes(unexpandableNodes, failedNodes); + collectUnmatchableMidProductionNodes(location, unmatchableMidProductionNodes, failedNodes); + + return reviveFailedNodes(input, location, failedNodes); + } + + private DoubleArrayList, AbstractNode> reviveNodes(int[] input, int location, + DoubleArrayList, ArrayList> recoveryNodes) { + DoubleArrayList, AbstractNode> recoveredNodes = new DoubleArrayList<>(); + + Set> skippedIds = new HashSet<>(); + + // Sort nodes by start location + recoveryNodes.sort((e1, e2) -> Integer.compare(e2.getLeft().getStartLocation(), e1.getLeft().getStartLocation())); + + if (VISUALIZE_RECOVERY_NODES) { + ParseStateVisualizer visualizer = new ParseStateVisualizer("Recovery"); + visualizer.visualizeRecoveryNodes(recoveryNodes); + } + + for (int i = 0; i recoveryNode = recoveryNodes.getFirst(i); + ArrayList prods = recoveryNodes.getSecond(i); + + int startLocation = recoveryNode.getStartLocation(); + + // Handle every possible continuation associated with the recovery node (there can be more then one + // because of prefix-sharing). + for (int j = prods.size() - 1; j >= 0; --j) { + IConstructor prod = prods.get(j); + + IConstructor type = ProductionAdapter.getType(prod); + + List> skippingNodes = + findSkippingNodes(input, location, recoveryNode, prod, startLocation); + for (SkippingStackNode skippingNode : skippingNodes) { + int skipLength = skippingNode.getLength(); + + if (!skippedIds.add(Triple.ofNonNull(startLocation, type, skipLength))) { + // Do not add this skipped node if a node with the same startLocation, type, and skipLength has been added already + continue; + } + + AbstractStackNode continuer = new RecoveryPointStackNode<>(stackNodeIdDispenser.dispenseId(), prod, recoveryNode); + + EdgesSet edges = new EdgesSet<>(1); + edges.add(continuer); + + continuer.setIncomingEdges(edges); + + skippingNode.initEdges(); + skippingNode.addEdges(edges, startLocation); + recoveredNodes.add(skippingNode, skippingNode.getResult()); + } + } + } + + return recoveredNodes; + } + + private List> findSkippingNodes(int[] input, int location, + AbstractStackNode recoveryNode, IConstructor prod, int startLocation) { + List> nodes = new java.util.ArrayList<>(); + + SkippedNode result; + + // If we are at the end of the input, skip nothing + if (location >= input.length) { + result = SkippingStackNode.createResultUntilEndOfInput(uri, input, startLocation); + nodes.add(new SkippingStackNode<>(stackNodeIdDispenser.dispenseId(), prod, result, startLocation)); + return nodes; // No other nodes would be useful + } + + // If we are the top-level node, just skip the rest of the input + if (!recoveryNode.isEndNode() && isTopLevelProduction(recoveryNode)) { + result = SkippingStackNode.createResultUntilEndOfInput(uri, input, startLocation); + nodes.add(new SkippingStackNode<>(stackNodeIdDispenser.dispenseId(), prod, result, startLocation)); + return nodes; // No other nodes would be useful + } + + // Find the last token of this production and skip until after that + List endMatchers = findEndMatchers(recoveryNode); + for (InputMatcher endMatcher : endMatchers) { + // For now take a very large (basically unlimited) "max match length", experiment with smaller limit later + MatchResult endMatch = endMatcher.findMatch(input, startLocation, Integer.MAX_VALUE/2); + if (endMatch != null) { + result = SkippingStackNode.createResultUntilChar(uri, input, startLocation, endMatch.getEnd()); + nodes.add(new SkippingStackNode<>(stackNodeIdDispenser.dispenseId(), prod, result, startLocation)); + } + } + + // Find the first token of the next production and skip until before that + List nextMatchers = findNextMatchers(recoveryNode); + for (InputMatcher nextMatcher : nextMatchers) { + // For now take a very large (basically unlimited) "max match length", experiment with smaller limit later + MatchResult nextMatch = nextMatcher.findMatch(input, startLocation+1, Integer.MAX_VALUE/2); + if (nextMatch != null) { + result = SkippingStackNode.createResultUntilChar(uri, input, startLocation, nextMatch.getStart()); + nodes.add(new SkippingStackNode<>(stackNodeIdDispenser.dispenseId(), prod, result, startLocation)); + } + } + + return nodes; + } + + // Find matchers for the last token of the current stack node + private List findEndMatchers(AbstractStackNode stackNode) { + final List matchers = new java.util.ArrayList<>(); + + AbstractStackNode[] prod = stackNode.getProduction(); + addEndMatchers(prod, prod.length-1, matchers, new HashSet<>()); + + IConstructor parentProduction = stackNode.getParentProduction(); + if (parentProduction != null && ProductionAdapter.isContextFree(parentProduction)) { + matchers.add(new LiteralMatcher("\n")); + } + + return matchers; + } + + private void addEndMatchers(AbstractStackNode[] prod, int dot, List matchers, + Set visitedNodes) { + if (prod == null || dot < 0 || dot >= prod.length) { + return; + } + + AbstractStackNode last = prod[dot]; + if (visitedNodes.contains(last.getId())) { + return; + } + visitedNodes.add(last.getId()); + + if (isNullable(last)) { + addEndMatchers(prod, dot-1, matchers, visitedNodes); + } + + last.accept(new StackNodeVisitorAdapter() { + @Override + public Void visit(LiteralStackNode literal) { + matchers.add(new LiteralMatcher(literal.getLiteral())); + return null; + } + + @Override + public Void visit(CaseInsensitiveLiteralStackNode literal) { + matchers.add(new CaseInsensitiveLiteralMatcher(literal.getLiteral())); + return null; + } + + @Override + public Void visit(NonTerminalStackNode nonTerminal) { + String name = nonTerminal.getName(); + AbstractStackNode[] alternatives = expectsProvider.getExpects(name); + for (AbstractStackNode alternative : alternatives) { + addEndMatchers(alternative.getProduction(), 0, matchers, visitedNodes); + } + return null; + } + }); + } + + private AbstractStackNode getSingleParentStack(AbstractStackNode stackNode) { + if (stackNode == null) { + return null; + } + + IntegerObjectList> edges = stackNode.getEdges(); + if (edges != null) { + EdgesSet edgesList = edges.getValue(0); + if (edgesList != null) { + return edgesList.get(0); + } + } + + return null; + } + + // Find matchers for the first token after the current stack node + private List findNextMatchers(AbstractStackNode stackNode) { + final List matchers = new java.util.ArrayList<>(); + + // Future improvement: use all parents instead of just one + AbstractStackNode parent = getSingleParentStack(stackNode); + if (parent == null) { + return matchers; + } + + addNextMatchers(parent.getProduction(), parent.getDot()+1, matchers, new HashSet<>()); + + return matchers; + } + + private void addNextMatchers(AbstractStackNode[] prod, int dot, List matchers, Set visitedNodes) { + if (prod == null || dot < 0 || dot >= prod.length) { + return; + } + + AbstractStackNode next = prod[dot]; + if (visitedNodes.contains(next.getId())) { + return; + } + visitedNodes.add(next.getId()); + + if (isNullable(next)) { + // In the future, when a node can be empty, we should also consider all prefix-shared alternatives. + addNextMatchers(prod, dot+1, matchers, visitedNodes); + } + + next.accept(new StackNodeVisitorAdapter() { + @Override + public Void visit(LiteralStackNode literal) { + matchers.add(new LiteralMatcher(literal.getLiteral())); + return null; + } + + @Override + public Void visit(CaseInsensitiveLiteralStackNode literal) { + matchers.add(new CaseInsensitiveLiteralMatcher(literal.getLiteral())); + return null; + } + + @Override + public Void visit(NonTerminalStackNode nonTerminal) { + String name = nonTerminal.getName(); + AbstractStackNode[] alternatives = expectsProvider.getExpects(name); + for (AbstractStackNode alternative : alternatives) { + addNextMatchers(alternative.getProduction(), 0, matchers, visitedNodes); + } + + return null; + } + }); + } + + private boolean isNullable(AbstractStackNode stackNode) { + if (stackNode instanceof NonTerminalStackNode && stackNode.getName().startsWith("layouts_")) { + return true; + } + + if (stackNode instanceof EpsilonStackNode || stackNode instanceof EmptyStackNode) { + return true; + } + + if (stackNode instanceof AbstractExpandableStackNode) { + return stackNode.canBeEmpty(); + } + + return false; + } + + // Check if a node is a top-level production (i.e., its parent production node has no parents and + // starts at position -1) + // As this is experimental code, this method is extremely conservative. + // Any sharing will result in returning 'false'. + // We will need to change this strategy in the future to improve error recovery. + private boolean isTopLevelProduction(AbstractStackNode node) { + + while (node != null && node.getDot() != 0) { + node = getSinglePredecessor(node); + } + + if (node != null) { + node = getSinglePredecessor(node); + return node != null && node.getStartLocation() == -1; + } + + return false; + } + + private AbstractStackNode getSinglePredecessor(AbstractStackNode node) { + IntegerObjectList> edgeMap = node.getEdges(); + if (edgeMap.size() == 1) { + EdgesSet edges = edgeMap.getValue(0); + if (edges.size() == 1) { + return edges.get(0); + } + } + + return null; + } + + + private DoubleArrayList, AbstractNode> reviveFailedNodes( + int[] input, + int location, + ArrayList> failedNodes) { + DoubleArrayList, ArrayList> recoveryNodes = new DoubleArrayList<>(); + + for (int i = failedNodes.size() - 1; i >= 0; --i) { + AbstractStackNode failedNode = failedNodes.get(i); + + // Protect against endless loop + long id = (long) failedNode.getId() << 32 | failedNode.getStartLocation(); + if (!processedNodes.add(id)) { + continue; + } + + findRecoveryNodes(failedNodes.get(i), recoveryNodes); + } + + return reviveNodes(input, location, recoveryNodes); + } + + private static void collectUnexpandableNodes(Stack> unexpandableNodes, + ArrayList> failedNodes) { + for (int i = unexpandableNodes.getSize() - 1; i >= 0; --i) { + failedNodes.add(unexpandableNodes.get(i)); + } + } + + /** + * Make a fresh copy of each unmatchable mid-production node and link in the predecessors of the + * original node. The new copies are added to `failedNodes` + * + * @param location the location where the failure occurs + * @param unmatchableMidProductionNodes each pair consists of a list of predecessors and a node that + * failed to match + * @param failedNodes the list to which failed nodes must be added + */ + private static void collectUnmatchableMidProductionNodes(int location, + DoubleStack, AbstractNode>, AbstractStackNode> unmatchableMidProductionNodes, + ArrayList> failedNodes) { + for (int i = unmatchableMidProductionNodes.getSize() - 1; i >= 0; --i) { + DoubleArrayList, AbstractNode> failedNodePredecessors = unmatchableMidProductionNodes.getFirst(i); + AbstractStackNode failedNode = + unmatchableMidProductionNodes.getSecond(i).getCleanCopy(location); // Clone it to prevent by-reference updates of the static version + + // Merge the information on the predecessors into the failed node. + for(int j = failedNodePredecessors.size() - 1; j >= 0; --j) { + AbstractStackNode predecessor = failedNodePredecessors.getFirst(j); + AbstractNode predecessorResult = failedNodePredecessors.getSecond(j); + failedNode.updateNode(predecessor, predecessorResult); + } + + failedNodes.add(failedNode); + } + } + + /** + * Travels up the parse graph in an attempt to find the closest recoverable parent nodes. + */ + private void findRecoveryNodes(AbstractStackNode failer, + DoubleArrayList, ArrayList> recoveryNodes) { + ObjectKeyedIntegerMap> visited = new ObjectKeyedIntegerMap<>(); + Stack> todo = new Stack<>(); + + todo.push(failer); + + while (!todo.isEmpty()) { + AbstractStackNode node = todo.pop(); + + if (visited.contains(node)) { + continue; // Don't follow cycles + } + + visited.put(node, 0); + + ArrayList recoveryProductions = new ArrayList<>(); + collectProductions(node, recoveryProductions); + if (recoveryProductions.size() > 0) { + addRecoveryNode(node, recoveryProductions, recoveryNodes); + } + + IntegerObjectList> edges = node.getEdges(); + + for (int i = edges.size() - 1; i >= 0; --i) { // Rewind + EdgesSet edgesList = edges.getValue(i); + + if (edgesList != null) { + for (int j = edgesList.size() - 1; j >= 0; --j) { + AbstractStackNode parent = edgesList.get(j); + todo.push(parent); + } + } + } + } + } + + // Only add recovery nodes that are not already present. + private void addRecoveryNode(AbstractStackNode node, ArrayList productions, + DoubleArrayList, ArrayList> recoveryNodes) { + for (int i=0; i prods1, ArrayList prods2) { + if (prods1.size() != prods2.size()) { + return false; + } + + for (int j = 0; j < prods1.size(); j++) { + if (prods1.get(j) != prods2.get(j)) { + return false; + } + } + + return true; + } + + // Gathers all productions that are marked for recovery (the given node can be part of a prefix + // shared production) + private void collectProductions(AbstractStackNode node, ArrayList productions) { + AbstractStackNode[] production = node.getProduction(); + if (production == null) { + return; // The root node does not have a production, so ignore it. + } + + if (node.isEndNode()) { + IConstructor parentProduction = node.getParentProduction(); + if (ProductionAdapter.isContextFree(parentProduction)){ + productions.add(parentProduction); + + if (ProductionAdapter.isList(parentProduction)) { + return; // Don't follow productions in lists productions, since they are 'cyclic'. + } + } + } + + int dot = node.getDot(); + for (int i = dot + 1; i < production.length; ++i) { + AbstractStackNode currentNode = production[i]; + if (currentNode.isEndNode()) { + IConstructor parentProduction = currentNode.getParentProduction(); + if (ProductionAdapter.isContextFree(parentProduction)) { + productions.add(parentProduction); + } + } + + AbstractStackNode[][] alternateProductions = currentNode.getAlternateProductions(); + if (alternateProductions != null) { + for (int j = alternateProductions.length - 1; j >= 0; --j) { + collectProductions(alternateProductions[j][i], productions); + } + } + } + } +} diff --git a/src/org/rascalmpl/parser/util/DebugUtil.java b/src/org/rascalmpl/parser/util/DebugUtil.java new file mode 100644 index 00000000000..bd28756b8a7 --- /dev/null +++ b/src/org/rascalmpl/parser/util/DebugUtil.java @@ -0,0 +1,72 @@ +/** + * 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. + **/ + +package org.rascalmpl.parser.util; + +import org.rascalmpl.values.parsetrees.ProductionAdapter; + +import io.usethesource.vallang.IConstructor; +import io.usethesource.vallang.IList; +import io.usethesource.vallang.IValue; + +public class DebugUtil { + /** + * Turn a production IConstructor into a string of the form "S -> E1 E2 ..." + */ + + private DebugUtil() { + } + + public static String prodToString(IConstructor prod) { + StringBuilder builder = new StringBuilder("'"); + + builder.append(quotedStringToPlain(ProductionAdapter.getSortName(prod))); + + builder.append(" ->"); + + if (prod.getName().equals("prod")) { + ProductionAdapter.getConstructorName(prod); + IList children = (IList) prod.get(1); + for (IValue child : children) { + builder.append(" "); + IConstructor conChild = (IConstructor) child; + builder.append(quotedStringToPlain(String.valueOf((conChild).get(0)))); + } + } else { + builder.append(" "); + builder.append(prod.toString()); + } + + builder.append("'"); + + return builder.toString(); + } + + private static String quotedStringToPlain(String s) { + if (s.isEmpty()) { + return s; + } + + if (s.charAt(0) == '"' && s.charAt(s.length()-1) == '"') { + return s.substring(1, s.length()-1).replace("\\", ""); + } + + return s; + } + + public static void opportunityToBreak() { + // Nop method that allows breakpoints to be set at the call site even if originally there is no code to break on + } + +} diff --git a/src/org/rascalmpl/parser/util/ParseStateVisualizer.java b/src/org/rascalmpl/parser/util/ParseStateVisualizer.java new file mode 100644 index 00000000000..c99537b0f91 --- /dev/null +++ b/src/org/rascalmpl/parser/util/ParseStateVisualizer.java @@ -0,0 +1,709 @@ +/** + * 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. + **/ + +package org.rascalmpl.parser.util; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.URI; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.function.Consumer; + +import org.apache.commons.io.FileUtils; +import org.rascalmpl.parser.gtd.SGTDBF; +import org.rascalmpl.parser.gtd.result.AbstractContainerNode; +import org.rascalmpl.parser.gtd.result.AbstractNode; +import org.rascalmpl.parser.gtd.result.CharNode; +import org.rascalmpl.parser.gtd.result.EpsilonNode; +import org.rascalmpl.parser.gtd.result.LiteralNode; +import org.rascalmpl.parser.gtd.result.RecoveredNode; +import org.rascalmpl.parser.gtd.result.SkippedNode; +import org.rascalmpl.parser.gtd.result.SortContainerNode; +import org.rascalmpl.parser.gtd.result.struct.Link; +import org.rascalmpl.parser.gtd.stack.AbstractStackNode; +import org.rascalmpl.parser.gtd.stack.edge.EdgesSet; +import org.rascalmpl.parser.gtd.util.ArrayList; +import org.rascalmpl.parser.gtd.util.DoubleArrayList; +import org.rascalmpl.parser.gtd.util.DoubleStack; +import org.rascalmpl.parser.gtd.util.IntegerObjectList; +import org.rascalmpl.parser.gtd.util.Stack; +import org.rascalmpl.util.visualize.dot.CompassPoint; +import org.rascalmpl.util.visualize.dot.DotAttribute; +import org.rascalmpl.util.visualize.dot.DotEdge; +import org.rascalmpl.util.visualize.dot.DotField; +import org.rascalmpl.util.visualize.dot.DotGraph; +import org.rascalmpl.util.visualize.dot.DotNode; +import org.rascalmpl.util.visualize.dot.DotRecord; +import org.rascalmpl.util.visualize.dot.NodeId; + +import io.usethesource.vallang.IConstructor; + +/** + * The parser uses quite complex datastructures. + * In order to understand what is going on when parsing, this class can generate graphs (as dot files) + * representing the internal datastructurs of the parser. + * + * These graphs are written to files that are relative to a directory specified in the environment + * variable PARSER_VISUALIZATION_PATH. + * + * The parser can generate a large number of snapshots of the parser state during a single parse. + * The file 'replay.html' contains an simple example of a html file to navigate through these snapshots. + */ +public class ParseStateVisualizer { + public static final boolean VISUALIZATION_ENABLED = true; + private static final String VISUALIZATION_URI_PATTERN_ENV = "PARSER_VISUALIZATION_URI_PATTERN"; + private static final String PARSER_VISUALIZATION_PATH_ENV = "PARSER_VISUALIZATION_PATH"; + private static final boolean INCLUDE_PRODUCTIONS = false; + + public static final NodeId PARSER_ID = new NodeId("Parser"); + public static final NodeId TODO_LISTS_ID= new NodeId("todoLists"); + public static final NodeId STACKS_TO_EXPAND_ID = new NodeId("stacksToExpand"); + public static final NodeId TERMINALS_TO_REDUCE_ID = new NodeId("terminalsToReduce"); + public static final NodeId NON_TERMINALS_TO_REDUCE_ID = new NodeId("nonTerminalsToReduce"); + + public static final NodeId ERROR_TRACKING_ID = new NodeId("error"); + public static final NodeId UNEXPANDABLE_NODES_ID = new NodeId("unexpandableNodes"); + public static final NodeId UNMATCHABLE_LEAF_NODES_ID = new NodeId("unmatchableLeafNodes"); + public static final NodeId UNMATCHABLE_MID_PRODUCTION_NODES_ID = new NodeId("unmatchableMidProductionNodes"); + public static final NodeId FILTERED_NODES_ID = new NodeId("filteredNodes"); + + private static final NodeId RECOVERED_NODES_ID = new NodeId("recoveredNodes"); + + public static boolean shouldVisualizeUri(URI inputUri) { + if (!VISUALIZATION_ENABLED) { + return false; + } + + String pattern = System.getenv(VISUALIZATION_URI_PATTERN_ENV); + if (pattern == null) { + return false; + } + + return inputUri.toString().matches(pattern); + } + + private static class StreamGobbler implements Runnable { + private InputStream inputStream; + private Consumer consumer; + + public StreamGobbler(InputStream inputStream, Consumer consumer) { + this.inputStream = inputStream; + this.consumer = consumer; + } + + @Override + public void run() { + new BufferedReader(new InputStreamReader(inputStream)).lines() + .forEach(consumer); + } + } + + private final String name; + private final File basePath; + private final File frameDir; + private final Map stackNodeNodes; + private DotGraph graph; + private int frame; + + + public ParseStateVisualizer(String name) { + // In the future we might want to offer some way to control the path from within Rascal. + String path = System.getenv(PARSER_VISUALIZATION_PATH_ENV); + if (path == null) { + throw new RuntimeException("The environment variable '" + PARSER_VISUALIZATION_PATH_ENV + "' is not set."); + } + basePath = new File(System.getenv(PARSER_VISUALIZATION_PATH_ENV)); + + this.name = name; + stackNodeNodes = new HashMap<>(); + + frameDir = new File(new File(basePath, "frames"), name); + if (frameDir.exists()) { + try { + FileUtils.deleteDirectory(frameDir); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + frameDir.mkdirs(); + } + + public void visualize(AbstractStackNode node) { + if (VISUALIZATION_ENABLED) { + writeGraph(createGraph(node)); + } + } + + public void visualizeRecoveryNodes(DoubleArrayList, ArrayList> recoveryNodes) { + writeGraph(createGraph(recoveryNodes)); + } + + public void visualizeProductionTrees(AbstractStackNode[] nodes) { + writeGraph(createProductionGraph(nodes)); + } + + public void visualizeNode(AbstractNode node) { + writeGraph(createGraph(node)); + } + + public int getFrame() { + return frame; + } + + private void reset() { + stackNodeNodes.clear(); + graph = null; + frame++; + } + + public void highlight(NodeId id) { + if (graph != null) { + graph.highlight(id); + } + } + + public void highlightStack(AbstractStackNode stack) { + if (graph != null) { + DotNode dotNode = stackNodeNodes.get(stack.getId()); + highlight(dotNode.getId()); + } + } + + private synchronized DotGraph createGraph(AbstractStackNode stackNode) { + reset(); + graph = new DotGraph(name, true); + addStack(graph, stackNode); + return graph; + } + + private synchronized DotGraph createGraph(AbstractNode parserNode) { + reset(); + graph = new DotGraph(name, true); + addParserNodes(graph, parserNode); + return graph; + } + + private DotGraph createGraph(DoubleArrayList, ArrayList> recoveryNodes) { + reset(); + graph = new DotGraph(name, true); + final NodeId recoveryNodesId = new NodeId("recovery-nodes"); + + DotNode arrayNode = DotNode.createArrayNode(recoveryNodesId, recoveryNodes.size()); + graph.addNode(arrayNode); + + for (int i=0; i[] stackNodes) { + reset(); + graph = new DotGraph(name, true); + for (AbstractStackNode stackNode : stackNodes) { + addProductionNodes(graph, stackNode); + } + return graph; + } + + private

NodeId addProductionNodes(DotGraph graph, AbstractStackNode

stackNode) { + DotNode node = createDotNode(stackNode); + graph.addNode(node); + + AbstractStackNode

[] prods = stackNode.getProduction(); + if (prods != null) { + NodeId prodArrayId = new NodeId(node.getId() + "-prod"); + graph.addArrayNode(prodArrayId, prods.length); + for (int i=0; i child = prods[i]; + DotNode childNode = createDotNode(child); + graph.addNode(childNode); + graph.addEdge(new NodeId(prodArrayId, String.valueOf(i)), childNode.getId()); + } + + graph.addEdge(node.getId(), prodArrayId, "Production"); + } + + return node.getId(); + } + + private void addProductionArray(DotGraph graph, NodeId nodeId, ArrayList productions) { + DotNode arrayNode = DotNode.createArrayNode(nodeId, productions.size()); + graph.addNode(arrayNode); + for (int i=0; i void addRecoveredNodes(DoubleArrayList, AbstractNode> recoveredNodes) { + addStackAndNodeDoubleList(graph, RECOVERED_NODES_ID, recoveredNodes); + graph.addEdge(ERROR_TRACKING_ID, RECOVERED_NODES_ID, "Nodes to revive"); + highlight(RECOVERED_NODES_ID); + } + + private

DotNode addStack(DotGraph graph, AbstractStackNode

stackNode) { + DotNode node = stackNodeNodes.get(stackNode.getId()); + if (node != null) { + return node; + } + + node = createDotNode(stackNode); + + stackNodeNodes.put(stackNode.getId(), node); + + graph.addNode(node); + + if (INCLUDE_PRODUCTIONS) { + addProductionNodes(graph, stackNode); + } + + IntegerObjectList> edges = stackNode.getEdges(); + if (edges != null) { + for (int i = edges.size() - 1; i >= 0; --i) { + EdgesSet

edgesList = edges.getValue(i); + if (edgesList != null) { + for (int j = edgesList.size() - 1; j >= 0; --j) { + AbstractStackNode

parentStackNode = edgesList.get(j); + DotNode parentDotNode = addStack(graph, parentStackNode); + graph.addEdge(node.getId(), parentDotNode.getId()); + } + } + } + } + + return node; + } + + private

DotNode createDotNode(AbstractStackNode

stackNode) { + String type = stackNode.getClass().getSimpleName(); + if (type.endsWith("StackNode")) { + type = type.substring(0, type.length() - "StackNode".length()); + } + + String nodeName; + + try { + nodeName = stackNode.getName(); + } catch (UnsupportedOperationException e) { + nodeName = ""; + } + + if (nodeName.startsWith("layouts_")) { + nodeName = nodeName.substring("layouts_".length()); + } + + int dot = stackNode.getDot(); + + String extraInfo = ""; + if (stackNode.isMatchable()) { + extraInfo += ",matchable"; + } + if (stackNode.isSeparator()) { + extraInfo += ",sep"; + } + if (stackNode.isExpandable()) { + extraInfo += ",expandable"; + } + if (stackNode.isLayout()) { + extraInfo += ",layout"; + } + if (stackNode.isEndNode()) { + extraInfo += ",end"; + } + + DotNode node = new DotNode(getNodeId(stackNode)); + String label = String.format("%s: %s\n.%d@%d %s", + type, nodeName, dot, stackNode.getStartLocation(), extraInfo); + + String shortString = stackNode.toShortString(); + if (shortString != null) { + label += "\n" + shortString; + } + + P parentProduction = stackNode.getParentProduction(); + if (parentProduction instanceof IConstructor) { + label += "\nin: " + DebugUtil.prodToString((IConstructor) parentProduction); + } else { + if (stackNode.getProduction() != null) { + label += "\nin:"; + for (AbstractStackNode

n : stackNode.getProduction()) { + String s = n.toShortString(); + if (!s.startsWith("layouts_")) { + label += " " + n.toShortString(); + } + } + } + } + node.addAttribute(DotAttribute.ATTR_LABEL, label); + + return node; + } + + private NodeId addParserNodes(DotGraph graph, AbstractNode parserNode) { + NodeId id = addParserNode(graph, parserNode); + if (parserNode instanceof AbstractContainerNode) { + @SuppressWarnings("unchecked") + AbstractContainerNode container = (AbstractContainerNode) parserNode; + Link link = container.getFirstAlternative(); + if (link != null) { + NodeId firstPrefix = addPrefixes(graph, link); + graph.addEdge(id, firstPrefix); + } + } + return id; + } + + private NodeId addPrefixes(DotGraph graph, Link link) { + NodeId id = addParserNodes(graph, link.getNode()); + ArrayList prefixes = link.getPrefixes(); + if (prefixes != null) { + for (int i=0; i) parserNode); + break; + case SkippedNode.ID: + enrichSkippedNode(dotNode, (SkippedNode) parserNode); + break; + default: + enrichUnknownParserNode(dotNode, parserNode); + break; + } + + graph.addNode(dotNode); + + return id; + } + + private void enrichCharNode(DotNode dotNode, CharNode charNode) { + int c = charNode.getCharacter(); + String label = dotNode.getAttributeValue(DotAttribute.ATTR_LABEL) + "\nchar=" + c + "('" + (char) c + "')"; + dotNode.setAttribute(DotAttribute.ATTR_LABEL, label); + } + + private void enrichLiteralNode(DotNode dotNode, LiteralNode literalNode) { + int[] content = literalNode.getContent(); + String label = dotNode.getAttributeValue(DotAttribute.ATTR_LABEL) + " \"" + new String(content, 0, content.length) + "\""; + dotNode.setAttribute(DotAttribute.ATTR_LABEL, label); + } + + private void enrichSkippedNode(DotNode dotNode, SkippedNode skippedNode) { + String label = dotNode.getAttributeValue(DotAttribute.ATTR_LABEL); + int[] skipped = skippedNode.getSkippedChars(); + label += "\n@" + skippedNode.getOffset() + ": " + " \"" + new String(skipped, 0, skipped.length) + "\""; + + dotNode.setAttribute(DotAttribute.ATTR_LABEL, label); + } + + private void enrichSortContainerNode(DotNode dotNode, SortContainerNode sortNode) { + String label = dotNode.getAttributeValue(DotAttribute.ATTR_LABEL); + label += " " + sortNode.getOffset() + "-" + sortNode.getEndOffset(); + label += "\n" + DebugUtil.prodToString(sortNode.getFirstProduction()); + dotNode.setAttribute(DotAttribute.ATTR_LABEL, label); + } + + private void enrichUnknownParserNode(DotNode dotNode, AbstractNode parserNode) { + String label = dotNode.getAttributeValue(DotAttribute.ATTR_LABEL); + label += "\ntype=" + parserNode.getTypeIdentifier(); + dotNode.setAttribute(DotAttribute.ATTR_LABEL, label); + } + + public static

NodeId getNodeId(AbstractStackNode

stackNode) { + return new NodeId(String.valueOf(stackNode.getId())); + } + + private static NodeId getNodeId(Object node) { + return new NodeId(String.valueOf(System.identityHashCode(node))); + } + + public void writeGraph() { + if (graph != null) { + writeGraph(graph); + } + } + + public void createGraph(SGTDBF parser, String step) { + if (!VISUALIZATION_ENABLED) { + return; + } + reset(); + + graph = new DotGraph(name, true); + + int location = parser.getLocation(); + + DotNode parserNode = new DotNode(PARSER_ID); + + int[] inputChars = parser.getInput(); + String input = new String(inputChars, 0, inputChars.length); + + char lookahead = (char) parser.getLookAheadChar(); + if (lookahead == '\0') { + lookahead = '$'; + } + + String label = String.format("Parser\nInput: \"%s\"\nLocation: %d ('%c')\nStep %d: %s", + input, location, lookahead, frame, step); + parserNode.setAttribute(DotAttribute.ATTR_LABEL, label); + graph.addNode(parserNode); + + addTodoLists(parser, graph); + addStacksToExpand(parser, graph); + addTerminalsToReduce(parser, graph); + addNonTerminalsToReduce(parser, graph); + + addErrorNodes(parser, graph); + } + + private void addTodoLists(SGTDBF parser, DotGraph graph) { + DoubleStack, AbstractNode>[] todoLists = parser.getTodoLists(); + int start = parser.getQueueIndex(); + + int todos = Math.min(todoLists.length, 50); + + DotNode todoListsNode = DotNode.createArrayNode(TODO_LISTS_ID, todos); + + for (int tokenLength=1; tokenLength<=todos+1; tokenLength++) { + int index = (start + tokenLength - 1) % todoLists.length; + DoubleStack, AbstractNode> todoList = todoLists[index]; + if (todoList != null && !todoList.isEmpty()) { + NodeId todoListId = new NodeId("todo-" + tokenLength); + addStackAndNodeDoubleStack(graph, todoListId, todoList); + graph.addEdge(DotEdge.createArrayEdge(TODO_LISTS_ID, tokenLength, todoListId)); + } + } + + graph.addNode(todoListsNode); + graph.addEdge(PARSER_ID, TODO_LISTS_ID, "todo lists"); + } + + private void addStacksToExpand(SGTDBF parser, DotGraph graph) { + Stack> stacksToExpand = parser.getStacksToExpand(); + addStackNodeStack(graph, STACKS_TO_EXPAND_ID, stacksToExpand); + graph.addEdge(PARSER_ID, STACKS_TO_EXPAND_ID, "stacks to expand"); + } + + private void addTerminalsToReduce(SGTDBF parser, DotGraph graph) { + addStackAndNodeDoubleStack(graph, TERMINALS_TO_REDUCE_ID, parser.getStacksWithTerminalsToReduce()); + graph.addEdge(PARSER_ID, TERMINALS_TO_REDUCE_ID, "terminals to reduce"); + } + + private void addNonTerminalsToReduce(SGTDBF parser, DotGraph graph) { + addStackAndNodeDoubleStack(graph, NON_TERMINALS_TO_REDUCE_ID, parser.getStacksWithNonTerminalsToReduce()); + graph.addEdge(PARSER_ID, NON_TERMINALS_TO_REDUCE_ID, "non-terminals to reduce"); + } + + private void addErrorNodes(SGTDBF parser, DotGraph graph) { + addUnexpandableNodes(parser, graph); + addUnmatchableLeafNodes(parser, graph); + addUnmatchableMidProductionNodes(parser, graph); + addFilteredNodes(parser, graph); + + graph.addNode(ERROR_TRACKING_ID, "Errors"); + + graph.addEdge(PARSER_ID, ERROR_TRACKING_ID, "error tracking"); + graph.addEdge(ERROR_TRACKING_ID, UNEXPANDABLE_NODES_ID, "unexpandable"); + graph.addEdge(ERROR_TRACKING_ID, UNMATCHABLE_LEAF_NODES_ID, "unmatchable leafs"); + graph.addEdge(ERROR_TRACKING_ID, UNMATCHABLE_MID_PRODUCTION_NODES_ID, "unmatchable mid-prod"); + graph.addEdge(ERROR_TRACKING_ID, FILTERED_NODES_ID, "filtered"); + } + + private void addUnexpandableNodes(SGTDBF parser, DotGraph graph) { + addStackNodeStack(graph, UNEXPANDABLE_NODES_ID, parser.getUnexpandableNodes()); + } + + private void addUnmatchableLeafNodes(SGTDBF parser, DotGraph graph) { + addStackNodeStack(graph, UNMATCHABLE_LEAF_NODES_ID, parser.getUnmatchableLeafNodes()); + } + + private void addUnmatchableMidProductionNodes(SGTDBF parser, DotGraph graph) { + DoubleStack, AbstractNode>, AbstractStackNode

> unmatchableMidProductionNodes = parser.getUnmatchableMidProductionNodes(); + + graph.addArrayNode(UNMATCHABLE_MID_PRODUCTION_NODES_ID, unmatchableMidProductionNodes.getSize()); + for (int i = unmatchableMidProductionNodes.getSize() - 1; i >= 0; --i) { + NodeId failureId = new NodeId("unmatchable-mid-production-" + i); + DotRecord failureRecord = new DotRecord(); + failureRecord.addEntry(new DotField("Failed Node", "failedNode")); + failureRecord.addEntry(new DotField("Predecessors", "predecessors")); + graph.addRecordNode(failureId, failureRecord); + graph.addEdge(new NodeId(UNMATCHABLE_MID_PRODUCTION_NODES_ID, String.valueOf(i)), failureId); + + DoubleArrayList, AbstractNode> failedNodePredecessors = unmatchableMidProductionNodes.getFirst(i); + AbstractStackNode

failedNode = unmatchableMidProductionNodes.getSecond(i); + + DotNode node = addStack(graph, failedNode); + NodeId predecessorsId = new NodeId("unmatchable-mid-production-predecessors-" + i); + addStackAndNodeDoubleList(graph, predecessorsId, failedNodePredecessors); + + graph.addEdge(new NodeId(failureId, "failedNode"), node.getId()); + graph.addEdge(new NodeId(failureId, "predecessors"), predecessorsId); + } + } + + private void addFilteredNodes(SGTDBF parser, DotGraph graph) { + addStackAndNodeDoubleStack(graph, FILTERED_NODES_ID, parser.getFilteredNodes()); + } + + private void addStackAndNodeDoubleStack(DotGraph graph, NodeId nodeId, DoubleStack, N> doubleStack) { + DotNode arrayNode = DotNode.createArrayNode(nodeId, doubleStack == null ? 0 : doubleStack.getSize()); + graph.addNode(arrayNode); + + if (doubleStack == null) { + return; + } + + for (int j=0; j stack = doubleStack.getFirst(j); + DotNode stackDotNode = addStack(graph, stack); + graph.addEdge(new NodeId(nodeId, String.valueOf(j), CompassPoint.SW), stackDotNode.getId(), "Stack"); + + AbstractNode node = doubleStack.getSecond(j); + addParserNode(graph, node); + graph.addEdge(new NodeId(nodeId, String.valueOf(j), CompassPoint.SE), getNodeId(node), "Node"); + } + } + + private void addStackAndNodeDoubleList(DotGraph graph, NodeId nodeId, DoubleArrayList, N> doubleList) { + DotNode arrayNode = DotNode.createArrayNode(nodeId, doubleList.size()); + graph.addNode(arrayNode); + + for (int i=0; i stack = doubleList.getFirst(i); + DotNode stackDotNode = addStack(graph, stack); + graph.addEdge(new NodeId(entryId, "stack", CompassPoint.SW), stackDotNode.getId(), "Stack"); + + AbstractNode node = doubleList.getSecond(i); + addParserNode(graph, node); + graph.addEdge(new NodeId(entryId, "node", CompassPoint.SE), getNodeId(node), "Node"); + + + graph.addEdge(new NodeId(nodeId, String.valueOf(i)), entryId); + } + } + + private

void addStackNodeStack(DotGraph graph, NodeId nodeId, Stack> stack) { + if (stack == null) { + return; + } + + DotNode arrayNode = DotNode.createArrayNode(nodeId, stack.getSize()); + + for (int j=0; j stackNode = stack.get(j); + addStack(graph, stackNode); + + graph.addEdge(DotEdge.createArrayEdge(nodeId, j, getNodeId(stackNode))); + } + + graph.addNode(arrayNode); + } + + private void writeGraph(DotGraph graph) { + try { + File dotFile = new File(basePath, name + ".dot"); + File svgFile = new File(basePath, name + ".svg"); + //File frameDir = new File(basePath, BASE_DIR + "/frames/" + name + "/"; + File frameSvgFile = new File(frameDir, String.format("%04d", frame) + ".svg"); + File frameDotFile = new File(frameDir, String.format("%04d", frame) + ".dot"); + FileWriter writer = new FileWriter(dotFile); + writer.write(graph.toString()); + writer.close(); + + String cmd = String.format("dot -Tsvg %s -o %s", dotFile, svgFile); + Process process = Runtime.getRuntime().exec(cmd); + StreamGobbler streamGobbler = new StreamGobbler(process.getInputStream(), System.out::println); + ExecutorService executorService = Executors.newSingleThreadExecutor(); + Future future = executorService.submit(streamGobbler); + + process.waitFor(); + future.get(10, TimeUnit.SECONDS); + + Files.copy(svgFile.toPath(), frameSvgFile.toPath(), StandardCopyOption.REPLACE_EXISTING); + Files.copy(dotFile.toPath(), frameDotFile.toPath(), StandardCopyOption.REPLACE_EXISTING); + } catch (IOException | InterruptedException | ExecutionException | TimeoutException e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/org/rascalmpl/parser/util/replay.html b/src/org/rascalmpl/parser/util/replay.html new file mode 100644 index 00000000000..23e9f700ed0 --- /dev/null +++ b/src/org/rascalmpl/parser/util/replay.html @@ -0,0 +1,48 @@ + + + + + + diff --git a/src/org/rascalmpl/semantics/dynamic/Expression.java b/src/org/rascalmpl/semantics/dynamic/Expression.java index a7f7b5e6d12..5a6902ab438 100644 --- a/src/org/rascalmpl/semantics/dynamic/Expression.java +++ b/src/org/rascalmpl/semantics/dynamic/Expression.java @@ -1073,7 +1073,7 @@ private boolean isBootstrapped(IEvaluatorContext eval) { return false; } - private ITree parseObject(IEvaluatorContext eval, IConstructor grammar, ISet filters, ISourceLocation location, char[] input, boolean allowAmbiguity, boolean hasSideEffects) { + private ITree parseObject(IEvaluatorContext eval, IConstructor grammar, ISet filters, ISourceLocation location, char[] input, boolean allowAmbiguity, boolean allowRecovery, boolean hasSideEffects) { RascalFunctionValueFactory vf = eval.getFunctionValueFactory(); IString str = vf.string(new String(input)); @@ -1081,19 +1081,19 @@ private ITree parseObject(IEvaluatorContext eval, IConstructor grammar, ISet fil return (ITree) vf.bootstrapParsers().call(grammar, str, location); } else { - IFunction parser = vf.parser(grammar, vf.bool(allowAmbiguity), vf.bool(hasSideEffects), vf.bool(false), filters); + IFunction parser = vf.parser(grammar, vf.bool(allowAmbiguity), vf.bool(allowRecovery), vf.bool(hasSideEffects), vf.bool(false), filters); return (ITree) parser.call(vf.string(new String(input)), location); } } - private ITree parseObject(IEvaluatorContext eval, IConstructor grammar, ISet filters, ISourceLocation location, boolean allowAmbiguity, boolean hasSideEffects) { + private ITree parseObject(IEvaluatorContext eval, IConstructor grammar, ISet filters, ISourceLocation location, boolean allowAmbiguity, boolean allowRecovery, boolean hasSideEffects) { RascalFunctionValueFactory vf = eval.getFunctionValueFactory(); if (isBootstrapped(eval)) { return (ITree) vf.bootstrapParsers().call(grammar, location, location); } else { - IFunction parser = vf.parser(grammar, vf.bool(allowAmbiguity), vf.bool(hasSideEffects), vf.bool(false), filters); + IFunction parser = vf.parser(grammar, vf.bool(allowAmbiguity), vf.bool(allowRecovery), vf.bool(hasSideEffects), vf.bool(false), filters); return (ITree) parser.call(location, location); } } @@ -1129,10 +1129,10 @@ public Result interpret(IEvaluator> __eval) { if (result.getStaticType().isString()) { tree = parseObject(__eval, value, VF.set(), this.getLocation(), - ((IString) result.getValue()).getValue().toCharArray(), true, false); + ((IString) result.getValue()).getValue().toCharArray(), true, false, false); } else if (result.getStaticType().isSourceLocation()) { - tree = parseObject(__eval, value, VF.set(), (ISourceLocation) result.getValue(), true, false); + tree = parseObject(__eval, value, VF.set(), (ISourceLocation) result.getValue(), true, false, false); } assert tree != null; // because we checked earlier diff --git a/src/org/rascalmpl/semantics/dynamic/Import.java b/src/org/rascalmpl/semantics/dynamic/Import.java index ebb4656494a..35088776167 100644 --- a/src/org/rascalmpl/semantics/dynamic/Import.java +++ b/src/org/rascalmpl/semantics/dynamic/Import.java @@ -487,7 +487,7 @@ public static ITree parseModuleAndFragments(char[] data, ISourceLocation locatio } else if (reg.exists(parserCacheFile)) { // if we cached a ModuleFile.parsers file, we will use the parser from that (typically after deployment time) - parsers = vf.loadParsers(parserCacheFile, vf.bool(false),vf.bool(false),vf.bool(false), vf.set()); + parsers = vf.loadParsers(parserCacheFile, vf.bool(false), vf.bool(false), vf.bool(false), vf.bool(false), vf.set()); } else { // otherwise we have to generate a fresh parser for this module now @@ -495,7 +495,7 @@ else if (reg.exists(parserCacheFile)) { IMap syntaxDefinition = env.getSyntaxDefinition(); IMap grammar = (IMap) eval.getParserGenerator().getGrammarFromModules(eval.getMonitor(),env.getName(), syntaxDefinition).get("rules"); IConstructor reifiedType = vf.reifiedType(dummy, grammar); - parsers = vf.parsers(reifiedType, vf.bool(false), vf.bool(false), vf.bool(false), vf.set()); + parsers = vf.parsers(reifiedType, vf.bool(false), vf.bool(false), vf.bool(false), vf.bool(false), vf.set()); } try { diff --git a/src/org/rascalmpl/util/visualize/dot/AttributeStatement.java b/src/org/rascalmpl/util/visualize/dot/AttributeStatement.java new file mode 100644 index 00000000000..34cd55e08ab --- /dev/null +++ b/src/org/rascalmpl/util/visualize/dot/AttributeStatement.java @@ -0,0 +1,41 @@ +/** + * 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. + **/ +package org.rascalmpl.util.visualize.dot; + +import java.io.PrintWriter; + +public class AttributeStatement { + enum Scope { GRAPH, NODE, EDGE } + + private Scope scope; + private DotAttribute attribute; + + void writeSource(PrintWriter writer) { + if (scope != null) { + switch (scope) { + case GRAPH: + writer.write("graph "); + break; + case NODE: + writer.write("node "); + break; + case EDGE: + writer.write("edge "); + break; + } + } + + attribute.writeSource(writer); + } +} diff --git a/src/org/rascalmpl/util/visualize/dot/CompassPoint.java b/src/org/rascalmpl/util/visualize/dot/CompassPoint.java new file mode 100644 index 00000000000..5f4c475c119 --- /dev/null +++ b/src/org/rascalmpl/util/visualize/dot/CompassPoint.java @@ -0,0 +1,21 @@ +/** + * 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. + **/ + +package org.rascalmpl.util.visualize.dot; + + + // Note: C means Center, X means unspecified. +public enum CompassPoint { + N, NE, E, SE, S, SW, W, NW, C, X; +} diff --git a/src/org/rascalmpl/util/visualize/dot/DotAttribute.java b/src/org/rascalmpl/util/visualize/dot/DotAttribute.java new file mode 100644 index 00000000000..b9420fa598b --- /dev/null +++ b/src/org/rascalmpl/util/visualize/dot/DotAttribute.java @@ -0,0 +1,118 @@ +/** + * 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. + **/ + +package org.rascalmpl.util.visualize.dot; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.List; + +public class DotAttribute { + public static final String ATTR_LABEL = "label"; + public static final String ATTR_COLOR = "color"; + public static final String ATTR_NODE_SHAPE = "shape"; + public static final String NODE_SHAPE_RECORD = "record"; + + String property; + String value; + + public DotAttribute(String property, String value) { + this.property = property; + this.value = value; + } + + public String getProperty() { + return property; + } + + public String getValue() { + return value; + } + + void setValue(String value) { + this.value = value; + } + + void writeSource(PrintWriter writer) { + NodeId.writeId(writer, property); + writer.write("="); + NodeId.writeId(writer, value); + } + + static void writeAttributes(PrintWriter writer, List attributes) { + if (!attributes.isEmpty()) { + writer.write("["); + boolean first = true; + for (DotAttribute attribute : attributes) { + if (first) { + first = false; + } else { + writer.write(", "); + } + attribute.writeSource(writer); + } + writer.write("]"); + } + } + + public static DotAttribute createRecordLabel(List elements) { + StringBuilder value = new StringBuilder(); + + boolean first = true; + for (String element : elements) { + if (first) { + first = false; + } else { + value.append("| "); + } + + value.append('<'); + value.append(element); + value.append('>'); + value.append(' '); + value.append(element); + } + + return new DotAttribute(ATTR_LABEL, value.toString()); + } + + public static DotAttribute createArrayLabel(int size) { + StringBuilder value = new StringBuilder(); + + boolean first = true; + for (int i=0; i'); + value.append(' '); + + value.append(i); + } + + return new DotAttribute(ATTR_LABEL, value.toString()); + } + + public static DotAttribute createRecordLabel(DotRecord rec) { + StringWriter writer = new StringWriter(); + rec.writeSource(new PrintWriter(writer, true), true); + return new DotAttribute(ATTR_LABEL, writer.toString()); + } + +} diff --git a/src/org/rascalmpl/util/visualize/dot/DotEdge.java b/src/org/rascalmpl/util/visualize/dot/DotEdge.java new file mode 100644 index 00000000000..d8fb169e29b --- /dev/null +++ b/src/org/rascalmpl/util/visualize/dot/DotEdge.java @@ -0,0 +1,80 @@ +/** + * 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. + **/ + +package org.rascalmpl.util.visualize.dot; + +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.List; + +public class DotEdge implements DotStatement { + private NodeId from; + private List to; + private boolean directed; + private List attributes; + + public DotEdge(NodeId from) { + this(from, true); + } + + public DotEdge(NodeId from, boolean directed) { + this.from = from; + this.directed = directed; + to = new ArrayList<>(); + attributes = new ArrayList<>(); + } + + public DotEdge(NodeId from, NodeId... to) { + this(from, true, to); + } + + public DotEdge(NodeId from, boolean directed, NodeId... to) { + this(from, directed); + for (NodeId node : to) { + addTo(node); + } + } + + public void addTo(NodeId node) { + to.add(node); + } + + public void addAttribute(String property, String value) { + attributes.add(new DotAttribute(property, value)); + } + + @Override + public void writeSource(PrintWriter writer) { + from.writeSource(writer); + for (NodeId node : to) { + if (directed) { + writer.write(" -> "); + } else { + writer.write(" -- "); + } + + node.writeSource(writer); + } + DotAttribute.writeAttributes(writer, attributes); + } + + public static DotEdge createArrayEdge(NodeId array, int index, NodeId element) { + return new DotEdge(new NodeId(array, String.valueOf(index)), true, element); + } + + public static DotEdge createArrayEdge(NodeId array, int index, CompassPoint direction, NodeId element) { + return new DotEdge(new NodeId(array.getId(), String.valueOf(index), direction), true, element); + } +} + diff --git a/src/org/rascalmpl/util/visualize/dot/DotField.java b/src/org/rascalmpl/util/visualize/dot/DotField.java new file mode 100644 index 00000000000..c9c947a5503 --- /dev/null +++ b/src/org/rascalmpl/util/visualize/dot/DotField.java @@ -0,0 +1,37 @@ +/** + * 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. + **/ + +package org.rascalmpl.util.visualize.dot; + +import java.io.PrintWriter; + +public class DotField implements DotRecordEntry { + private String label; + private String portId; + + public DotField(String label, String portId) { + this.label = label; + this.portId = portId; + } + + @Override + public void writeSource(PrintWriter writer, boolean topLevel) { + if (portId != null) { + writer.write('<'); + writer.write(portId); + writer.write("> "); + } + NodeId.writeString(writer, label); + } +} diff --git a/src/org/rascalmpl/util/visualize/dot/DotGraph.java b/src/org/rascalmpl/util/visualize/dot/DotGraph.java new file mode 100644 index 00000000000..1553491c368 --- /dev/null +++ b/src/org/rascalmpl/util/visualize/dot/DotGraph.java @@ -0,0 +1,123 @@ +/** + * 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. + **/ + +package org.rascalmpl.util.visualize.dot; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/* +Loosely follow (a subset of) the DotGraph syntax definition: +https://graphviz.org/doc/info/lang.html +*/ + +public class DotGraph { + private static final String COLOR_HIGHLIGHT = "red"; + + private String id; + private boolean digraph; + private List statements; + private Map nodes; + + public DotGraph(String id, boolean digraph) { + this.id = id; + this.digraph = digraph; + statements = new ArrayList<>(); + nodes = new HashMap<>(); + } + + private void addStatement(DotStatement statement) { + statements.add(statement); + } + + public void addNode(DotNode node) { + if (!nodes.containsKey(node.getId())) { + addStatement(node); + nodes.put(node.getId(), node); + } + } + + public void addNode(String id, String label) { + addNode(new NodeId(id), label); + } + + public void addNode(NodeId id, String label) { + DotNode node = new DotNode(id); + node.addAttribute(DotAttribute.ATTR_LABEL, label); + addNode(node); + } + + public void addArrayNode(NodeId id, int size) { + DotNode node = new DotNode(id); + node.addAttribute(DotAttribute.ATTR_NODE_SHAPE, DotAttribute.NODE_SHAPE_RECORD); + node.addAttribute(DotAttribute.createArrayLabel(size)); + addNode(node); + } + + public void addRecordNode(NodeId id, DotRecord dotRecord) { + DotNode node = new DotNode(id); + node.addAttribute(DotAttribute.ATTR_NODE_SHAPE, DotAttribute.NODE_SHAPE_RECORD); + node.addAttribute(DotAttribute.createRecordLabel(dotRecord)); + addNode(node); + } + + public void highlight(NodeId id) { + DotNode node = nodes.get(id); + if (node != null) { + node.addAttribute(DotAttribute.ATTR_COLOR, COLOR_HIGHLIGHT); + } + } + + public void addEdge(String from, String to) { + addEdge(new NodeId(from), new NodeId(to)); + } + + public void addEdge(NodeId from, NodeId to) { + addEdge(new DotEdge(from, true, to)); + } + + public void addEdge(NodeId from, NodeId to, String label) { + DotEdge edge = new DotEdge(from, true, to); + edge.addAttribute(DotAttribute.ATTR_LABEL, label); + addEdge(edge); + } + + public void addEdge(DotEdge edge) { + addStatement(edge); + } + + public void writeSource(PrintWriter writer) { + writer.write(digraph ? "digraph" : "graph"); + if (id != null) { + writer.write(" "); + writer.write(id); + } + writer.println(" {"); + for (DotStatement statement : statements) { + statement.writeSource(writer); + writer.println(";"); + } + writer.println("}"); + } + + public String toString() { + StringWriter writer = new StringWriter(); + writeSource(new PrintWriter(writer)); + return writer.toString(); + } +} diff --git a/src/org/rascalmpl/util/visualize/dot/DotNode.java b/src/org/rascalmpl/util/visualize/dot/DotNode.java new file mode 100644 index 00000000000..027c404e4a6 --- /dev/null +++ b/src/org/rascalmpl/util/visualize/dot/DotNode.java @@ -0,0 +1,83 @@ +/** + * 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. + **/ + +package org.rascalmpl.util.visualize.dot; + +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.List; + +public class DotNode implements DotStatement { + public static DotNode createArrayNode(NodeId id, int size) { + DotNode node = new DotNode(id); + node.addAttribute(DotAttribute.createArrayLabel(size)); + node.addAttribute(DotAttribute.ATTR_NODE_SHAPE, DotAttribute.NODE_SHAPE_RECORD); + return node; + } + + private NodeId id; + private List attributes; + + public DotNode(String id) { + this(new NodeId(id)); + } + + public DotNode(NodeId id) { + this.id = id; + attributes = new ArrayList<>(); + } + + public NodeId getId() { + return id; + } + + public void addAttribute(String property, String value) { + addAttribute(new DotAttribute(property, value)); + } + + public void addAttribute(DotAttribute attribute) { + attributes.add(attribute); + } + + public void setAttribute(String property, String value) { + for (DotAttribute attribute : attributes) { + if (attribute.getProperty().equals(property)) { + attribute.setValue(value); + return; + } + } + + addAttribute(property, value); + } + + public String getAttributeValue(String property) { + for (DotAttribute attribute : attributes) { + if (attribute.getProperty().equals(property)) { + return attribute.getValue(); + } + } + + return null; + } + + public void setLabel(String label) { + setAttribute(DotAttribute.ATTR_LABEL, label); + } + + @Override + public void writeSource(PrintWriter writer) { + id.writeSource(writer); + DotAttribute.writeAttributes(writer, attributes); + } +} diff --git a/src/org/rascalmpl/util/visualize/dot/DotRecord.java b/src/org/rascalmpl/util/visualize/dot/DotRecord.java new file mode 100644 index 00000000000..f4ef5045c6b --- /dev/null +++ b/src/org/rascalmpl/util/visualize/dot/DotRecord.java @@ -0,0 +1,51 @@ +/** + * 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. + **/ + +package org.rascalmpl.util.visualize.dot; + +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.List; + +public class DotRecord implements DotRecordEntry { + private List entries; + + public DotRecord() { + entries = new ArrayList<>(); + } + + public void addEntry(DotRecordEntry entry) { + entries.add(entry); + } + + @Override + public void writeSource(PrintWriter writer, boolean topLevel) { + if (!topLevel) { + writer.write("{ "); + } + boolean first = true; + for (DotRecordEntry entry : entries) { + if (first) { + first = false; + } else { + writer.write(" | "); + } + + entry.writeSource(writer, false); + } + if (!topLevel) { + writer.write("} "); + } + } +} diff --git a/src/org/rascalmpl/util/visualize/dot/DotRecordEntry.java b/src/org/rascalmpl/util/visualize/dot/DotRecordEntry.java new file mode 100644 index 00000000000..318485024a6 --- /dev/null +++ b/src/org/rascalmpl/util/visualize/dot/DotRecordEntry.java @@ -0,0 +1,21 @@ +/** + * 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. + **/ + +package org.rascalmpl.util.visualize.dot; + +import java.io.PrintWriter; + +public interface DotRecordEntry { + void writeSource(PrintWriter writer, boolean topLevel); +} diff --git a/src/org/rascalmpl/util/visualize/dot/DotStatement.java b/src/org/rascalmpl/util/visualize/dot/DotStatement.java new file mode 100644 index 00000000000..a27780d2aba --- /dev/null +++ b/src/org/rascalmpl/util/visualize/dot/DotStatement.java @@ -0,0 +1,21 @@ +/** + * 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. + **/ + +package org.rascalmpl.util.visualize.dot; + +import java.io.PrintWriter; + +public interface DotStatement { + void writeSource(PrintWriter writer); +} diff --git a/src/org/rascalmpl/util/visualize/dot/DotSubgraph.java b/src/org/rascalmpl/util/visualize/dot/DotSubgraph.java new file mode 100644 index 00000000000..fa09078eb9d --- /dev/null +++ b/src/org/rascalmpl/util/visualize/dot/DotSubgraph.java @@ -0,0 +1,54 @@ +/** + * 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. + **/ + +package org.rascalmpl.util.visualize.dot; + +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.List; + +public class DotSubgraph implements DotStatement { + private String id; + private List statements; + + public DotSubgraph(String id) { + this.id = id; + statements = new ArrayList<>(); + } + + public void addStatement(DotStatement statement) { + statements.add(statement); + } + + public String getId() { + return id; + } + + @Override + public void writeSource(PrintWriter writer) { + writer.write("subgraph"); + if (id != null) { + NodeId.writeId(writer, id); + writer.write(" "); + } + writer.println(" {"); + for (DotStatement statement : statements) { + writer.write(" "); + statement.writeSource(writer); + writer.println(";"); + } + + writer.println(" }"); + } +} diff --git a/src/org/rascalmpl/util/visualize/dot/NodeId.java b/src/org/rascalmpl/util/visualize/dot/NodeId.java new file mode 100644 index 00000000000..ff1c35811e8 --- /dev/null +++ b/src/org/rascalmpl/util/visualize/dot/NodeId.java @@ -0,0 +1,130 @@ +/** + * 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. + **/ + +package org.rascalmpl.util.visualize.dot; + +import java.io.PrintWriter; +import java.util.Objects; + +public class NodeId { + public static void writeId(PrintWriter stream, String id) { + stream.write("\""); + writeString(stream, id); + stream.write("\""); + } + + public static void writeString(PrintWriter stream, String s) { + for (int i=0; i getParserGenerator(), this, caller, parser, allowAmbiguity, hasSideEffects, firstAmbiguity, filters)); + return function(functionType, new ParametrizedParseFunction(() -> getParserGenerator(), this, caller, parser, allowAmbiguity, allowRecovery, hasSideEffects, firstAmbiguity, filters)); } @Override @@ -292,7 +297,7 @@ public void storeParsers(IValue reifiedGrammar, ISourceLocation saveLocation) th } @Override - public IFunction loadParsers(ISourceLocation saveLocation, IBool allowAmbiguity, IBool hasSideEffects, IBool firstAmbiguity, ISet filters) throws IOException, ClassNotFoundException { + public IFunction loadParsers(ISourceLocation saveLocation, IBool allowAmbiguity, IBool allowRecovery, IBool hasSideEffects, IBool firstAmbiguity, ISet filters) throws IOException, ClassNotFoundException { RascalTypeFactory rtf = RascalTypeFactory.getInstance(); TypeFactory tf = TypeFactory.getInstance(); @@ -318,12 +323,12 @@ public IFunction loadParsers(ISourceLocation saveLocation, IBool allowAmbiguity, this, caller, parser, - allowAmbiguity, hasSideEffects, firstAmbiguity, filters) + allowAmbiguity, allowRecovery, hasSideEffects, firstAmbiguity, filters) ); } @Override - public IFunction loadParser(IValue reifiedGrammar, ISourceLocation saveLocation, IBool allowAmbiguity, IBool hasSideEffects, IBool firstAmbiguity, ISet filters) throws IOException, ClassNotFoundException { + public IFunction loadParser(IValue reifiedGrammar, ISourceLocation saveLocation, IBool allowAmbiguity, IBool allowRecovery, IBool hasSideEffects, IBool firstAmbiguity, ISet filters) throws IOException, ClassNotFoundException { TypeFactory tf = TypeFactory.getInstance(); Type functionType = tf.functionType(reifiedGrammar.getType().getTypeParameters().getFieldType(0), @@ -347,7 +352,7 @@ public IFunction loadParser(IValue reifiedGrammar, ISourceLocation saveLocation, name = generator.getParserMethodName(startSort); } - return function(functionType, new ParseFunction(ctx.getValueFactory(), caller, parser, name, allowAmbiguity, hasSideEffects, firstAmbiguity, filters)); + return function(functionType, new ParseFunction(ctx.getValueFactory(), caller, parser, name, allowAmbiguity, allowRecovery, hasSideEffects, firstAmbiguity, filters)); } /** @@ -372,7 +377,7 @@ public IFunction bootstrapParsers() { AbstractAST current = ctx.getCurrentAST(); ISourceLocation caller = current != null ? current.getLocation() : URIUtil.rootLocation("unknown"); - return function(functionType, new ParametrizedParseFunction(() -> getParserGenerator(), this, caller, parser, vf.bool(false), vf.bool(false), vf.bool(false), ctx.getValueFactory().set())); + return function(functionType, new ParametrizedParseFunction(() -> getParserGenerator(), this, caller, parser, vf.bool(false), vf.bool(false), vf.bool(false), vf.bool(false), ctx.getValueFactory().set())); } public IString createHole(ITree part, IInteger index) { @@ -417,19 +422,21 @@ static private class ParseFunction implements BiFunction> parser; protected final String methodName; protected final ISourceLocation caller; - public ParseFunction(IValueFactory vf, ISourceLocation caller, Class> parser, String methodName, IBool allowAmbiguity, IBool hasSideEffects, IBool firstAmbiguity, ISet filters) { + public ParseFunction(IValueFactory vf, ISourceLocation caller, Class> parser, String methodName, IBool allowAmbiguity, IBool allowRecovery, IBool hasSideEffects, IBool firstAmbiguity, ISet filters) { this.vf = vf; this.caller = caller; this.parser = parser; this.methodName = methodName; this.filters = filters; this.allowAmbiguity = allowAmbiguity.getValue() || firstAmbiguity.getValue(); + this.allowRecovery = allowRecovery.getValue(); this.hasSideEffects = hasSideEffects.getValue(); this.firstAmbiguity = firstAmbiguity.getValue(); } @@ -454,10 +461,10 @@ else if (parameters[0].getType().isSourceLocation()) { } if (parameters[0].getType().isString()) { - return parse(methodName, filters, (IString) parameters[0], (ISourceLocation) parameters[1], allowAmbiguity, hasSideEffects); + return parse(methodName, filters, (IString) parameters[0], (ISourceLocation) parameters[1], allowAmbiguity, allowRecovery, hasSideEffects); } else if (parameters[0].getType().isSourceLocation()) { - return parse(methodName, filters, (ISourceLocation) parameters[0], (ISourceLocation) parameters[1], allowAmbiguity, hasSideEffects); + return parse(methodName, filters, (ISourceLocation) parameters[0], (ISourceLocation) parameters[1], allowAmbiguity, allowRecovery, hasSideEffects); } } @@ -477,13 +484,13 @@ private IGTD getParser() { } } - protected IValue parse(String methodName, ISet filters, IString input, ISourceLocation origin, boolean allowAmbiguity, boolean hasSideEffects) { + protected IValue parse(String methodName, ISet filters, IString input, ISourceLocation origin, boolean allowAmbiguity, boolean allowRecovery, boolean hasSideEffects) { try { if (origin == null) { origin = URIUtil.rootLocation("unknown"); } - return parseObject(methodName, origin, input.getValue().toCharArray(), allowAmbiguity, hasSideEffects, filters); + return parseObject(methodName, origin, input.getValue().toCharArray(), allowAmbiguity, allowRecovery, hasSideEffects, filters); } catch (ParseError pe) { ISourceLocation errorLoc = pe.getLocation(); @@ -500,7 +507,7 @@ protected IValue parse(String methodName, ISet filters, IString input, ISourceL protected IValue firstAmbiguity(String methodName, IString input) { try { - return parseObject(methodName, URIUtil.invalidLocation(), input.getValue().toCharArray(), false, false, vf.set()); + return parseObject(methodName, URIUtil.invalidLocation(), input.getValue().toCharArray(), false, false, false, vf.set()); } catch (ParseError pe) { ISourceLocation errorLoc = pe.getLocation(); @@ -516,7 +523,7 @@ protected IValue firstAmbiguity(String methodName, IString input) { protected IValue firstAmbiguity(String methodName, ISourceLocation input) { try { - return parseObject(methodName, input, readAll(input), false, false, vf.set()); + return parseObject(methodName, input, readAll(input), false, false, false, vf.set()); } catch (ParseError pe) { ISourceLocation errorLoc = pe.getLocation(); @@ -543,13 +550,13 @@ private IString printSymbol(IConstructor symbol) { return vf.string(SymbolAdapter.toString(symbol, false)); } - protected IValue parse(String methodName, ISet filters, ISourceLocation input, ISourceLocation origin, boolean allowAmbiguity, boolean hasSideEffects) { + protected IValue parse(String methodName, ISet filters, ISourceLocation input, ISourceLocation origin, boolean allowAmbiguity, boolean allowRecovery, boolean hasSideEffects) { if (origin != null && !origin.equals(input)) { throw new IllegalArgumentException("input and origin should be equal: != "); } try { - return parseObject(methodName, input, readAll(input), allowAmbiguity, hasSideEffects, filters); + return parseObject(methodName, input, readAll(input), allowAmbiguity, allowRecovery, hasSideEffects, filters); } catch (ParseError pe) { ISourceLocation errorLoc = pe.getLocation(); @@ -567,10 +574,24 @@ protected IValue parse(String methodName, ISet filters, ISourceLocation input, I } } - private ITree parseObject(String methodName, ISourceLocation location, char[] input, boolean allowAmbiguity, boolean hasSideEffects, ISet filters) { + private ITree parseObject(String methodName, ISourceLocation location, char[] input, boolean allowAmbiguity, boolean allowRecovery, boolean hasSideEffects, ISet filters) { IActionExecutor exec = filters.isEmpty() ? new NoActionExecutor() : new RascalFunctionActionExecutor(filters, !hasSideEffects); + IGTD parserInstance = getParser(); + IRecoverer recoverer = null; + IDebugListener debugListener = null; + URI uri = location.getURI(); + if (allowRecovery) { + recoverer = new ToTokenRecoverer(uri, parserInstance, new StackNodeIdDispenser(parserInstance)); + } + ITree parseForest = (ITree) parserInstance.parse(methodName, uri, input, exec, new DefaultNodeFlattener<>(), new UPTRNodeFactory(allowRecovery || allowAmbiguity), recoverer, debugListener); + + if (!allowAmbiguity && allowRecovery) { + // Check for 'regular' (non-error) ambiguities + RascalValueFactory valueFactory = (RascalValueFactory) ValueFactoryFactory.getValueFactory(); + new ErrorRecovery(valueFactory).checkForRegularAmbiguities(parseForest); + } - return (ITree) getParser().parse(methodName, location.getURI(), input, exec, new DefaultNodeFlattener(), new UPTRNodeFactory(allowAmbiguity), (IRecoverer) null); + return parseForest; } } @@ -585,8 +606,8 @@ private ITree parseObject(String methodName, ISourceLocation location, char[] in static private class ParametrizedParseFunction extends ParseFunction { private Supplier generator; - public ParametrizedParseFunction(Supplier generator, IValueFactory vf, ISourceLocation caller, Class> parser, IBool allowAmbiguity, IBool hasSideEffects, IBool firstAmbiguity, ISet filters) { - super(vf, caller, parser, null, allowAmbiguity, hasSideEffects, firstAmbiguity, filters); + public ParametrizedParseFunction(Supplier generator, IValueFactory vf, ISourceLocation caller, Class> parser, IBool allowAmbiguity, IBool allowRecovery, IBool hasSideEffects, IBool firstAmbiguity, ISet filters) { + super(vf, caller, parser, null, allowAmbiguity, allowRecovery, hasSideEffects, firstAmbiguity, filters); this.generator = generator; } @@ -621,10 +642,10 @@ else if (parameters[1].getType().isSourceLocation()) { } if (parameters[1].getType().isString()) { - return parse(name, filters, (IString) parameters[1], (ISourceLocation) parameters[2], allowAmbiguity, hasSideEffects); + return parse(name, filters, (IString) parameters[1], (ISourceLocation) parameters[2], allowAmbiguity, allowRecovery, hasSideEffects); } else if (parameters[1].getType().isSourceLocation()) { - return parse(name, filters, (ISourceLocation) parameters[1], (ISourceLocation) parameters[2], allowAmbiguity, hasSideEffects); + return parse(name, filters, (ISourceLocation) parameters[1], (ISourceLocation) parameters[2], allowAmbiguity, allowRecovery, hasSideEffects); } } diff --git a/src/org/rascalmpl/values/RascalValueFactory.java b/src/org/rascalmpl/values/RascalValueFactory.java index 8ec9fc76a0a..3c4a7218b5d 100644 --- a/src/org/rascalmpl/values/RascalValueFactory.java +++ b/src/org/rascalmpl/values/RascalValueFactory.java @@ -152,6 +152,8 @@ public class RascalValueFactory extends AbstractValueFactoryAdapter implements I public static final Type Production_Priority = tf.constructor(uptr, Production, "priority", Symbol, "def", tf.listType(Production), "choices"); public static final Type Production_Composition = tf.constructor(uptr, Production, "composition", Production, "lhs", Production, "rhs"); public static final Type Production_Associativity = tf.constructor(uptr, Production, "associativity", Symbol, "def", Associativity, "assoc", tf.setType(Production), "alternatives"); + public static final Type Production_Error = tf.constructor(uptr, Production, "error", Symbol, "def", Production, "prod", tf.integerType(), "dot"); + public static final Type Production_Skipped = tf.constructor(uptr, Production, "skipped", Symbol, "def"); /* Constructors for Attr */ public static final Type Attr_Assoc = tf.constructor(uptr, Attr, "assoc", Associativity, "assoc"); diff --git a/src/org/rascalmpl/values/parsetrees/ProductionAdapter.java b/src/org/rascalmpl/values/parsetrees/ProductionAdapter.java index 287e32002fb..286ed092154 100644 --- a/src/org/rascalmpl/values/parsetrees/ProductionAdapter.java +++ b/src/org/rascalmpl/values/parsetrees/ProductionAdapter.java @@ -139,6 +139,14 @@ public static boolean isDefault(IConstructor tree) { public static boolean isRegular(IConstructor tree) { return tree.getConstructorType() == RascalValueFactory.Production_Regular; } + + public static boolean isSkipped(IConstructor tree) { + return tree.getConstructorType() == RascalValueFactory.Production_Skipped; + } + + public static boolean isError(IConstructor tree) { + return tree.getConstructorType() == RascalValueFactory.Production_Error; + } public static boolean isSeparatedList(IConstructor tree) { IConstructor rhs = getType(tree); diff --git a/test/org/rascalmpl/test/AllSuite.java b/test/org/rascalmpl/test/AllSuite.java index 2de69a1d039..f41adf2bb10 100644 --- a/test/org/rascalmpl/test/AllSuite.java +++ b/test/org/rascalmpl/test/AllSuite.java @@ -16,6 +16,6 @@ import org.rascalmpl.test.infrastructure.RecursiveTestSuite; @RunWith(RecursiveTestSuite.class) -@RecursiveTest({"basic", "concrete", "extend", "functionality", "imports", "library", "parser", "syntax", "demo", "benchmark"}) +@RecursiveTest({"basic", "concrete", "extend", "functionality", "imports", "library", "parser", "recovery", "syntax", "demo", "benchmark"}) public class AllSuite { } diff --git a/test/org/rascalmpl/test/AllSuiteParallel.java b/test/org/rascalmpl/test/AllSuiteParallel.java index 7fcce545df5..7afc9db367a 100644 --- a/test/org/rascalmpl/test/AllSuiteParallel.java +++ b/test/org/rascalmpl/test/AllSuiteParallel.java @@ -7,6 +7,6 @@ @RunWith(RecursiveTestSuite.class) @RecursiveTest({"parallel"}) -@RecursiveJavaOnlyTest({"basic", "concrete", "functionality", "library", "parser", "demo", "benchmark"}) +@RecursiveJavaOnlyTest({"basic", "concrete", "functionality", "library", "parser", "recovery", "demo", "benchmark"}) public class AllSuiteParallel { } diff --git a/test/org/rascalmpl/test/parser/IParserTest.java b/test/org/rascalmpl/test/parser/IParserTest.java index 9cf98df7cb2..f41262a589e 100644 --- a/test/org/rascalmpl/test/parser/IParserTest.java +++ b/test/org/rascalmpl/test/parser/IParserTest.java @@ -15,12 +15,13 @@ import io.usethesource.vallang.IValue; import io.usethesource.vallang.IValueFactory; + import org.rascalmpl.values.ValueFactoryFactory; import org.rascalmpl.values.parsetrees.ITree; public interface IParserTest{ public final static IValueFactory VF = ValueFactoryFactory.getValueFactory(); - + ITree executeParser(); IValue getExpectedResult() throws IOException; diff --git a/test/org/rascalmpl/test/parser/StackNodeTest.java b/test/org/rascalmpl/test/parser/StackNodeTest.java index 6b849ca5fa7..08cca000a5a 100644 --- a/test/org/rascalmpl/test/parser/StackNodeTest.java +++ b/test/org/rascalmpl/test/parser/StackNodeTest.java @@ -1,3 +1,17 @@ +/** + * 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. + **/ + package org.rascalmpl.test.parser; import org.junit.Assert; diff --git a/test/org/rascalmpl/test/recovery/RunRascalTestModules.java b/test/org/rascalmpl/test/recovery/RunRascalTestModules.java new file mode 100644 index 00000000000..4c3acd8a692 --- /dev/null +++ b/test/org/rascalmpl/test/recovery/RunRascalTestModules.java @@ -0,0 +1,25 @@ +/** + * Copyright (c) 2022, 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. + **/ + +package org.rascalmpl.test.recovery; + +import org.junit.runner.RunWith; +import org.rascalmpl.test.infrastructure.RascalJUnitTestPrefix; +import org.rascalmpl.test.infrastructure.RascalJUnitTestRunner; + +@RunWith(RascalJUnitTestRunner.class) +@RascalJUnitTestPrefix("lang::rascal::tests::recovery") +public class RunRascalTestModules { + +}