c95568aede
This is the first commit of this effort. It's not polished and mostly generates nonsense output that, though, should not crash the compiler. Further work might add options to create less-nonsense output). Change-Id: Ic7f140ea6f248bfa9a0e62ca37326def1d0a5086 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/403601 Reviewed-by: Johnni Winther <johnniwinther@google.com> Commit-Queue: Jens Johansen <jensj@google.com>
1141 lines
27 KiB
Dart
1141 lines
27 KiB
Dart
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
|
|
// for details. All rights reserved. Use of this source code is governed by a
|
|
// BSD-style license that can be found in the LICENSE file.
|
|
|
|
import 'dart:convert' show utf8;
|
|
import 'dart:io' show File, Platform, stderr;
|
|
import 'dart:math' show Random;
|
|
|
|
import 'package:_fe_analyzer_shared/src/scanner/characters.dart';
|
|
|
|
List<Node> stack = [];
|
|
Map<String, Node> data = {};
|
|
|
|
void main() {
|
|
initialize(Platform.script.resolve("Dart.g"));
|
|
print(createRandomProgram(includeWhy: true));
|
|
}
|
|
|
|
void initialize(Uri dartG) {
|
|
parseDartG(dartG);
|
|
postParse();
|
|
}
|
|
|
|
String createRandomProgram({required bool includeWhy}) {
|
|
ProgramCreator programCreator = new ProgramCreator(createWhy: includeWhy);
|
|
programCreator.path.add("startSymbol");
|
|
data["startSymbol"]!.accept(programCreator);
|
|
if (includeWhy) {
|
|
return "${programCreator.sb.toString()}"
|
|
"\n\n\n\n"
|
|
"${programCreator.sb.toStringWhy()}";
|
|
} else {
|
|
return programCreator.sb.toString();
|
|
}
|
|
}
|
|
|
|
void postParse() {
|
|
EntryChecker entryChecker = new EntryChecker();
|
|
for (MapEntry<String, Node> entry in data.entries) {
|
|
entryChecker.parent = entry.key;
|
|
entry.value.accept(entryChecker);
|
|
}
|
|
DepthMarker depthMarker = new DepthMarker();
|
|
for (int i = 0; i < 2; i++) {
|
|
for (MapEntry<String, Node> entry in data.entries) {
|
|
entry.value.accept(depthMarker);
|
|
}
|
|
depthMarker.recurse = true;
|
|
}
|
|
SanityChecker sanityChecker = new SanityChecker();
|
|
for (MapEntry<String, Node> entry in data.entries) {
|
|
entry.value.accept(sanityChecker);
|
|
}
|
|
}
|
|
|
|
void parseDartG(Uri dartG) {
|
|
File f = new File.fromUri(dartG);
|
|
List<int> input = f.readAsBytesSync();
|
|
List<Token> tokens = tokenize(input);
|
|
|
|
Token? currentToken = tokens.first;
|
|
while (currentToken != null) {
|
|
currentToken = parseDef(currentToken);
|
|
}
|
|
// Attempt to have less exotic identifiers and types etc.
|
|
data["identifier"] = new Literal("foo");
|
|
data["typeIdentifier"] = new Literal("foo");
|
|
data["typeNotVoid"] = new Literal("foo");
|
|
data["singleLineString"] = new Literal("'ssls'"); // small single line string
|
|
data["argument"] = new Literal("42");
|
|
}
|
|
|
|
class StringBufferWrapper {
|
|
final bool createWhy;
|
|
final StringBuffer sb = new StringBuffer();
|
|
final StringBuffer sbWhy = new StringBuffer();
|
|
int get length => sb.length;
|
|
final ProgramCreator parent;
|
|
int currentNewlines = 1;
|
|
String prev = "";
|
|
|
|
StringBufferWrapper(this.createWhy, this.parent);
|
|
|
|
void addNewline() {
|
|
if (parent.singleLineStringDepth > 0) return;
|
|
if (parent.numericLiteralDepth > 0) return;
|
|
if (currentNewlines >= 2) return;
|
|
sb.writeln();
|
|
if (createWhy) {
|
|
sbWhy.writeln();
|
|
}
|
|
currentNewlines++;
|
|
}
|
|
|
|
void write(String s, String why) {
|
|
if (currentNewlines > 0 || parent.numericLiteralDepth > 0) {
|
|
// No space
|
|
} else if (prev == "" || prev == "@" || prev == "." || prev == "(") {
|
|
// No space.
|
|
} else if (s == "." || s == "," || s == "(" || s == ")" || s == ";") {
|
|
// No space.
|
|
} else {
|
|
sb.write(" ");
|
|
}
|
|
currentNewlines = 0;
|
|
prev = s;
|
|
sb.write(s);
|
|
if (createWhy) {
|
|
sbWhy.writeln("$s: ${parent.path.join("/")}");
|
|
}
|
|
}
|
|
|
|
void writeCharCode(int charCode, String why) {
|
|
write(new String.fromCharCode(charCode), why);
|
|
}
|
|
|
|
@override
|
|
String toString() => sb.toString();
|
|
String toStringWhy() => sbWhy.toString();
|
|
}
|
|
|
|
class WrappedRandom {
|
|
final Random random = new Random.secure();
|
|
|
|
int nextInt(int max) {
|
|
int result = random.nextInt(max);
|
|
return result;
|
|
}
|
|
|
|
bool nextBool() {
|
|
bool result = random.nextBool();
|
|
return result;
|
|
}
|
|
}
|
|
|
|
class ProgramCreator implements Visitor {
|
|
final WrappedRandom random = new WrappedRandom();
|
|
late final StringBufferWrapper sb;
|
|
int depth = 0;
|
|
int? currentExpressionStart;
|
|
int? currentTopLevelStart;
|
|
int singleLineStringDepth = 0;
|
|
int numericLiteralDepth = 0;
|
|
final List<String> path = [];
|
|
|
|
bool isDepthHigh() => depth > 1000;
|
|
bool isOutputLarge() =>
|
|
sb.length > 14000 ||
|
|
(currentTopLevelStart != null &&
|
|
(sb.length - currentTopLevelStart!) > 700);
|
|
bool isBigExpression() =>
|
|
currentExpressionStart != null &&
|
|
(sb.length - currentExpressionStart!) >= 20;
|
|
|
|
ProgramCreator({required bool createWhy}) {
|
|
sb = new StringBufferWrapper(createWhy, this);
|
|
}
|
|
|
|
@override
|
|
void defaultNode(Node node) {
|
|
throw "Should never get to defaultNode";
|
|
}
|
|
|
|
@override
|
|
void visitAnyCharNode(AnyCharNode node) {
|
|
int from = $SPACE;
|
|
int to = $z;
|
|
sb.writeCharCode(random.nextInt(to - from + 1) + from, "visitAnyCharNode");
|
|
}
|
|
|
|
@override
|
|
void visitDummyNode(DummyNode node) {
|
|
throw "Should never get to visitDummyNode";
|
|
}
|
|
|
|
@override
|
|
void visitEmpty(Empty node) {
|
|
// do nothing
|
|
}
|
|
|
|
@override
|
|
void visitEndOfFileNode(EndOfFileNode node) {
|
|
// do nothing
|
|
}
|
|
|
|
@override
|
|
void visitLeaf(Leaf node) {
|
|
depth++;
|
|
final String what = node.token.value;
|
|
path.add(what);
|
|
if (what == "singleLineString") {
|
|
singleLineStringDepth++;
|
|
} else if (what == "numericLiteral") {
|
|
numericLiteralDepth++;
|
|
}
|
|
int? oldExpressionStart = currentExpressionStart;
|
|
if (what == "expression" && oldExpressionStart == null) {
|
|
// Only write if it's not already set.
|
|
currentExpressionStart = sb.length;
|
|
}
|
|
int? oldTopLevelStart = currentTopLevelStart;
|
|
if (what == "topLevelDefinition" && oldTopLevelStart == null) {
|
|
// Only write if it's not already set.
|
|
currentTopLevelStart = sb.length;
|
|
}
|
|
|
|
const newLinesAround = {
|
|
"topLevelDefinition",
|
|
"libraryName",
|
|
"partDirective"
|
|
};
|
|
const newLinesAfter = {"metadatum"};
|
|
bool addNewlinesAround = newLinesAround.contains(what);
|
|
bool addNewlinesAfter = newLinesAfter.contains(what);
|
|
|
|
if (addNewlinesAround) {
|
|
sb.addNewline();
|
|
}
|
|
|
|
Node linked = data[what]!;
|
|
linked.accept(this);
|
|
|
|
if (addNewlinesAround || addNewlinesAfter) {
|
|
sb.addNewline();
|
|
}
|
|
|
|
currentExpressionStart = oldExpressionStart;
|
|
currentTopLevelStart = oldTopLevelStart;
|
|
|
|
if (what == "singleLineString") {
|
|
singleLineStringDepth--;
|
|
} else if (what == "numericLiteral") {
|
|
numericLiteralDepth--;
|
|
}
|
|
|
|
if (path.removeLast() != what) throw "Bad remove!";
|
|
depth--;
|
|
}
|
|
|
|
@override
|
|
void visitLiteral(Literal node) {
|
|
sb.write(node.content, "visitLiteral");
|
|
}
|
|
|
|
@override
|
|
void visitOneOrMore(OneOrMore node) {
|
|
depth++;
|
|
int count = random.nextInt(3) + 1;
|
|
if (isDepthHigh() || isOutputLarge() || isBigExpression()) count = 1;
|
|
for (int i = 0; i < count; i++) {
|
|
node.visitChildren(this);
|
|
}
|
|
depth--;
|
|
}
|
|
|
|
@override
|
|
void visitOptional(Optional node) {
|
|
if (isDepthHigh() || isOutputLarge() || isBigExpression()) return;
|
|
|
|
depth++;
|
|
if (random.nextInt(10) < 2) {
|
|
node.visitChildren(this);
|
|
}
|
|
depth--;
|
|
}
|
|
|
|
@override
|
|
void visitOrNode(OrNode node) {
|
|
depth++;
|
|
List<Node> nodes = node.nodesFiltered;
|
|
if (nodes.length == 1) {
|
|
nodes[0].accept(this);
|
|
} else {
|
|
// In practise some should be more likely than others.
|
|
if (nodes.length == 2 &&
|
|
nodes[0] is Leaf &&
|
|
(nodes[0] as Leaf).token.value == "libraryDeclaration" &&
|
|
nodes[1] is Leaf &&
|
|
(nodes[1] as Leaf).token.value == "partDeclaration") {
|
|
// libraryDeclaration should be more likely.
|
|
if (random.nextInt(100) < 95) {
|
|
nodes[0].accept(this);
|
|
} else {
|
|
nodes[1].accept(this);
|
|
}
|
|
} else {
|
|
Node? chosen;
|
|
bool pickSmallestDepth = false;
|
|
if (isBigExpression() || isDepthHigh() || isOutputLarge()) {
|
|
// Choose the one with the smallest depth.
|
|
pickSmallestDepth = true;
|
|
} else if (random.nextBool() &&
|
|
1 + 1 == 3 /* i.e. currently disabled */) {
|
|
// If depth and length is not too big yet, and if several nodes has
|
|
// depth <= 10, choose randomly among those.
|
|
List<Node> smallDepthNodes = [];
|
|
for (Node childNode in nodes) {
|
|
if (childNode.depth! > 0 && (childNode.depth! <= 10)) {
|
|
smallDepthNodes.add(childNode);
|
|
}
|
|
}
|
|
if (smallDepthNodes.isNotEmpty) {
|
|
chosen = smallDepthNodes[random.nextInt(smallDepthNodes.length)];
|
|
}
|
|
}
|
|
|
|
if (pickSmallestDepth) {
|
|
chosen = nodes[0];
|
|
for (Node childNode in nodes) {
|
|
if (childNode.depth! > 0 &&
|
|
(childNode.depth! < chosen!.depth! || chosen.depth! < 0)) {
|
|
chosen = childNode;
|
|
}
|
|
}
|
|
if (chosen!.depth! < 0) {
|
|
throw "Not good";
|
|
}
|
|
}
|
|
if (chosen == null) {
|
|
chosen = nodes[random.nextInt(nodes.length)];
|
|
}
|
|
|
|
chosen.accept(this);
|
|
}
|
|
}
|
|
depth--;
|
|
}
|
|
|
|
@override
|
|
void visitRangeNode(RangeNode node) {
|
|
String a = node.a.content;
|
|
String b = node.b.content;
|
|
if (a.length != 1 || b.length != 1) {
|
|
throw "Unexpected range: $node ('$a' '$b')";
|
|
}
|
|
int from = a.codeUnitAt(0);
|
|
int to = b.codeUnitAt(0);
|
|
sb.writeCharCode(random.nextInt(to - from + 1) + from, "visitRangeNode");
|
|
}
|
|
|
|
@override
|
|
void visitSequence(Sequence node) {
|
|
depth++;
|
|
node.visitChildren(this);
|
|
if (node.nodes.isNotEmpty) {
|
|
Node last = node.nodes.last;
|
|
if (last is Literal && last.content == ";") {
|
|
sb.addNewline();
|
|
}
|
|
}
|
|
depth--;
|
|
}
|
|
|
|
@override
|
|
void visitTildeNode(TildeNode node) {
|
|
Set<int> no = {};
|
|
List<Node> getNodes(Node node) {
|
|
List<Node> result = [];
|
|
if (node is Sequence) {
|
|
for (Node childNode in node.nodes) {
|
|
result.addAll(getNodes(childNode));
|
|
}
|
|
} else if (node is OrNode) {
|
|
for (Node childNode in node.nodes) {
|
|
result.addAll(getNodes(childNode));
|
|
}
|
|
} else if (node is Literal) {
|
|
result.add(node);
|
|
} else {
|
|
throw "Can't get nodes of ${node} (${node.runtimeType})";
|
|
}
|
|
return result;
|
|
}
|
|
|
|
List<Node> nodes = getNodes(node.a);
|
|
for (Node childNode in nodes) {
|
|
if (childNode is! Literal) {
|
|
throw "Not a literal...: ${childNode.runtimeType}: $node";
|
|
}
|
|
Literal literal = childNode;
|
|
String s = literal.content;
|
|
for (int i = 0; i < s.length; i++) {
|
|
no.add(s.codeUnitAt(i));
|
|
}
|
|
}
|
|
|
|
int from = $SPACE;
|
|
int to = $z;
|
|
int charCode = random.nextInt(to - from + 1) + from;
|
|
while (no.contains(charCode)) {
|
|
charCode = random.nextInt(to - from + 1) + from;
|
|
}
|
|
sb.writeCharCode(charCode, "visitTildeNode");
|
|
}
|
|
|
|
@override
|
|
void visitZeroOrMore(ZeroOrMore node) {
|
|
depth++;
|
|
int count = random.nextInt(3);
|
|
if (node.a is Sequence &&
|
|
(node.a as Sequence)
|
|
.nodes
|
|
.where(
|
|
(Node e) => e is Leaf && e.token.value == "topLevelDefinition")
|
|
.isNotEmpty) {
|
|
// We likely want more top level definitions.
|
|
count = random.nextInt(20);
|
|
} else if (node.a is Sequence &&
|
|
(node.a as Sequence)
|
|
.nodes
|
|
.where((Node e) => e is Leaf && e.token.value == "metadatum")
|
|
.isNotEmpty) {
|
|
// We likely don't want too many metadata entries...
|
|
if (random.nextInt(100) < 90) {
|
|
count = 0;
|
|
}
|
|
}
|
|
if (isDepthHigh() || isOutputLarge() || isBigExpression()) {
|
|
count = 0;
|
|
}
|
|
for (int i = 0; i < count; i++) {
|
|
node.visitChildren(this);
|
|
|
|
if (isBigExpression()) {
|
|
break;
|
|
}
|
|
}
|
|
depth--;
|
|
}
|
|
}
|
|
|
|
class SanityChecker extends Visitor {
|
|
ProgramCreator programCreator = new ProgramCreator(createWhy: false);
|
|
|
|
@override
|
|
void defaultNode(Node node) {
|
|
if (node.depth == null) throw "$node (${node.runtimeType}) has null depth";
|
|
if (node is TildeNode) {
|
|
programCreator.visitTildeNode(node);
|
|
}
|
|
super.defaultNode(node);
|
|
}
|
|
}
|
|
|
|
class DepthMarker implements Visitor {
|
|
bool recurse = false;
|
|
|
|
@override
|
|
void defaultNode(Node node) {
|
|
throw "Should never get here.";
|
|
}
|
|
|
|
@override
|
|
void visitAnyCharNode(AnyCharNode node) {
|
|
node.depth = 1;
|
|
if (recurse) node.visitChildren(this);
|
|
}
|
|
|
|
@override
|
|
void visitDummyNode(DummyNode node) {
|
|
node.depth = 1;
|
|
if (recurse) node.visitChildren(this);
|
|
}
|
|
|
|
@override
|
|
void visitEmpty(Empty node) {
|
|
node.depth = 1;
|
|
if (recurse) node.visitChildren(this);
|
|
}
|
|
|
|
@override
|
|
void visitEndOfFileNode(EndOfFileNode node) {
|
|
node.depth = 1;
|
|
if (recurse) node.visitChildren(this);
|
|
}
|
|
|
|
@override
|
|
void visitLeaf(Leaf node) {
|
|
Node linked = data[node.token.value]!;
|
|
if (linked.depth != null && linked.depth! > 0) {
|
|
node.depth = linked.depth! + 1;
|
|
} else if (node.depth == null) {
|
|
node.depth = -1;
|
|
linked.accept(this);
|
|
if (linked.depth! > 0) {
|
|
node.depth = linked.depth! + 1;
|
|
} else {
|
|
// Recursive... Keep the -1 depth and see that as "infinite".
|
|
}
|
|
} else {
|
|
// Recursive... Keep the -1 depth and see that as "infinite".
|
|
}
|
|
}
|
|
|
|
@override
|
|
void visitLiteral(Literal node) {
|
|
node.depth = 1;
|
|
if (recurse) node.visitChildren(this);
|
|
}
|
|
|
|
@override
|
|
void visitOneOrMore(OneOrMore node) {
|
|
node.visitChildren(this);
|
|
node.depth = node.a.depth! + 1;
|
|
}
|
|
|
|
@override
|
|
void visitOptional(Optional node) {
|
|
node.depth = 1;
|
|
if (recurse) node.visitChildren(this);
|
|
}
|
|
|
|
@override
|
|
void visitOrNode(OrNode node) {
|
|
node.visitChildren(this);
|
|
int min = node.nodes.first.depth!;
|
|
for (Node node in node.nodes) {
|
|
if (node.depth! > 0 && (node.depth! < min || min < 0)) min = node.depth!;
|
|
}
|
|
node.depth = min + 1;
|
|
}
|
|
|
|
@override
|
|
void visitRangeNode(RangeNode node) {
|
|
node.depth = 1;
|
|
if (recurse) node.visitChildren(this);
|
|
}
|
|
|
|
@override
|
|
void visitSequence(Sequence node) {
|
|
node.visitChildren(this);
|
|
int max = node.nodes.first.depth!;
|
|
for (Node childNode in node.nodes) {
|
|
if (childNode.depth! > max) max = childNode.depth!;
|
|
if (childNode.depth == -1) max = -1;
|
|
if (max == -1) {
|
|
node.depth = -1;
|
|
return;
|
|
}
|
|
}
|
|
node.depth = max + 1;
|
|
}
|
|
|
|
@override
|
|
void visitTildeNode(TildeNode node) {
|
|
// Naively assume that this is true.
|
|
node.depth = 1;
|
|
if (recurse) node.visitChildren(this);
|
|
}
|
|
|
|
@override
|
|
void visitZeroOrMore(ZeroOrMore node) {
|
|
node.depth = 1;
|
|
if (recurse) node.visitChildren(this);
|
|
}
|
|
}
|
|
|
|
class EntryChecker extends Visitor {
|
|
String? parent;
|
|
|
|
@override
|
|
void visitLeaf(Leaf node) {
|
|
if (!data.containsKey(node.token.value)) {
|
|
throw "Reference to unknown entry '${node.token.value}' from '$parent'";
|
|
}
|
|
}
|
|
}
|
|
|
|
Token? parseDef(Token token) {
|
|
if (token.value == "grammar" &&
|
|
token.next?.next?.value == ";" &&
|
|
token.next?.next?.next != null) {
|
|
// Skip something like 'grammar Dart;'.
|
|
token = token.next!.next!.next!;
|
|
}
|
|
if (token.value == "fragment") {
|
|
// Skip 'fragment'.
|
|
token = token.next!;
|
|
}
|
|
while (token.value == "@") {
|
|
// Skip injected java code.
|
|
while (token.value != "{") {
|
|
token = token.next!;
|
|
}
|
|
int count = 1;
|
|
token = token.next!;
|
|
while (count > 0) {
|
|
if (token.value == "{") {
|
|
count++;
|
|
} else if (token.value == "}") {
|
|
count--;
|
|
}
|
|
token = token.next!;
|
|
if (count == 0) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
Token name = token;
|
|
token = token.next!;
|
|
if (token.value != ":") throw "Expected ':' at around ${token.offset}";
|
|
token = token.next!;
|
|
token = parseContent(token, depth: 1);
|
|
if (token.value != ";") throw "Expected ';' at around offset ${token.offset}";
|
|
|
|
data[name.value] = stack.removeLast();
|
|
if (stack.isNotEmpty) throw "Expected stack to be empty";
|
|
|
|
return token.next;
|
|
}
|
|
|
|
void join() {
|
|
if (stack.length > 1) {
|
|
List<Node> sequenceList = [];
|
|
for (Node node in stack) {
|
|
if (node is! DummyNode) sequenceList.add(node);
|
|
}
|
|
if (sequenceList.length > 1) {
|
|
// If something like "'a' .. 'z'" make that into a RangeNode.
|
|
if (sequenceList.length == 3 &&
|
|
sequenceList[0] is Literal &&
|
|
sequenceList[1] is Leaf &&
|
|
(sequenceList[1] as Leaf).token.value == ".." &&
|
|
sequenceList[2] is Literal) {
|
|
RangeNode range = new RangeNode(
|
|
sequenceList[0] as Literal, sequenceList[2] as Literal);
|
|
stack = [range];
|
|
} else {
|
|
Sequence sequence = new Sequence(sequenceList);
|
|
stack = [sequence];
|
|
}
|
|
} else {
|
|
stack = sequenceList;
|
|
}
|
|
}
|
|
}
|
|
|
|
Token parseContent(Token token, {required int depth}) {
|
|
Token originalToken = token;
|
|
try {
|
|
while (token.value != ";" && token.value != ")") {
|
|
if (token.value == "|" && stack.isEmpty) {
|
|
stderr.writeln("Warning: Seemingly stray '|' at ${token.offset}");
|
|
token = token.next!;
|
|
}
|
|
if (token.value == "|") {
|
|
if (token != originalToken && depth > 1) break;
|
|
join();
|
|
Node a = stack.removeLast();
|
|
token = parseContent(token.next!, depth: depth + 1);
|
|
Node b = stack.removeLast();
|
|
OrNode orNode = new OrNode();
|
|
if (a is OrNode) {
|
|
orNode.nodes.addAll(a.nodes);
|
|
} else {
|
|
orNode.nodes.add(a);
|
|
}
|
|
if (b is OrNode) {
|
|
orNode.nodes.addAll(b.nodes);
|
|
} else {
|
|
orNode.nodes.add(b);
|
|
}
|
|
stack.add(orNode);
|
|
} else if (token.value == "(") {
|
|
List<Node> savedStack = stack;
|
|
stack = [];
|
|
token = parseContent(token.next!, depth: 1);
|
|
if (token.value != ")") throw "Expected ')'";
|
|
if (stack.isEmpty) {
|
|
// ()
|
|
stack.add(new Empty());
|
|
}
|
|
if (stack.length != 1) throw "Expected stack to have length 1.";
|
|
savedStack.addAll(stack);
|
|
stack = savedStack;
|
|
token = token.next!;
|
|
} else if (token.value == "{") {
|
|
token = parseBrackets(token.next!);
|
|
} else if (token.value == "'") {
|
|
token = parseLiteral(token.next!);
|
|
} else if (token.value == "?") {
|
|
Node a = stack.removeLast();
|
|
if (a is DummyNode) {
|
|
stack.add(a);
|
|
} else {
|
|
stack.add(new Optional(a));
|
|
}
|
|
token = token.next!;
|
|
} else if (token.value == "+") {
|
|
Node a = stack.removeLast();
|
|
stack.add(new OneOrMore(a));
|
|
token = token.next!;
|
|
} else if (token.value == "*") {
|
|
Node a = stack.removeLast();
|
|
stack.add(new ZeroOrMore(a));
|
|
token = token.next!;
|
|
} else if (token.value == "~") {
|
|
token = parseContent(token.next!, depth: depth + 1);
|
|
Node a = stack.removeLast();
|
|
stack.add(new TildeNode(a));
|
|
} else if (token.value == ".") {
|
|
stack.add(new AnyCharNode(token));
|
|
token = token.next!;
|
|
} else if (token.value == "EOF") {
|
|
stack.add(new EndOfFileNode(token));
|
|
token = token.next!;
|
|
} else {
|
|
stack.add(new Leaf(token));
|
|
token = token.next!;
|
|
}
|
|
}
|
|
join();
|
|
} catch (e, st) {
|
|
print("Got error at around ${token.offset}: $st");
|
|
rethrow;
|
|
}
|
|
return token;
|
|
}
|
|
|
|
Token parseBrackets(Token token) {
|
|
while (token.value != "}") {
|
|
token = token.next!;
|
|
}
|
|
stack.add(new DummyNode());
|
|
return token.next!;
|
|
}
|
|
|
|
Token parseLiteral(Token token) {
|
|
List<String> content = [];
|
|
while (token.value != "'") {
|
|
if (token.value == r"\" && token.next != null) {
|
|
// Escape stuff...
|
|
Token next = token.next!;
|
|
if (next.value == '\'') {
|
|
content.add(next.value);
|
|
token = next.next!;
|
|
} else if (next.value == '\\') {
|
|
content.add("\\");
|
|
token = next.next!;
|
|
} else if (next.value == 'r') {
|
|
content.add("\r");
|
|
token = next.next!;
|
|
} else if (next.value == 'n') {
|
|
content.add("\n");
|
|
token = next.next!;
|
|
} else if (next.value == 'uFEFF') {
|
|
content.add("\uFEFF");
|
|
token = next.next!;
|
|
} else if (next.value == 't') {
|
|
content.add("\t");
|
|
token = next.next!;
|
|
} else {
|
|
print(token.next!.value);
|
|
}
|
|
} else {
|
|
content.add(token.value);
|
|
token = token.next!;
|
|
}
|
|
}
|
|
stack.add(new Literal(content.join()));
|
|
return token.next!;
|
|
}
|
|
|
|
List<Token> tokenize(List<int> bytes) {
|
|
List<int> tmp = [];
|
|
int index = 0;
|
|
bool inComment = false;
|
|
bool inQuote = false;
|
|
bool inEscaped = false;
|
|
List<Token> tokens = [];
|
|
|
|
void pushToken() {
|
|
if (inComment) {
|
|
tmp.clear();
|
|
} else if (tmp.isNotEmpty) {
|
|
String s = utf8.decode(tmp);
|
|
if (s == "." && tokens.isNotEmpty && tokens.last.value == ".") {
|
|
// Hack: Join them.
|
|
Token last = tokens.removeLast();
|
|
Token nextToken = new Token("..", last.offset);
|
|
if (tokens.isNotEmpty) tokens.last.next = nextToken;
|
|
tokens.add(nextToken);
|
|
} else {
|
|
Token nextToken = new Token(s, index);
|
|
if (tokens.isNotEmpty) tokens.last.next = nextToken;
|
|
tokens.add(nextToken);
|
|
}
|
|
tmp.clear();
|
|
}
|
|
}
|
|
|
|
while (index < bytes.length) {
|
|
int b = bytes[index++];
|
|
if (b == $LF || b == $CR) {
|
|
pushToken();
|
|
inComment = false;
|
|
inEscaped = false;
|
|
} else if (inComment) {
|
|
// ignore comments.
|
|
} else if (!inQuote &&
|
|
b == $SLASH &&
|
|
index < bytes.length &&
|
|
bytes[index] == $SLASH) {
|
|
inComment = true;
|
|
} else if (!inEscaped &&
|
|
b == $BACKSLASH &&
|
|
index < bytes.length &&
|
|
((bytes[index] == $SQ) || (bytes[index] == $BACKSLASH))) {
|
|
// Escaped ' or escaped \.
|
|
inEscaped = true;
|
|
pushToken();
|
|
tmp.add(b);
|
|
pushToken();
|
|
} else if (b == $SQ) {
|
|
if (!inEscaped) {
|
|
inQuote = !inQuote;
|
|
}
|
|
inEscaped = false;
|
|
pushToken();
|
|
tmp.add(b);
|
|
pushToken();
|
|
} else if (b == $SPACE && !inQuote) {
|
|
pushToken();
|
|
inEscaped = false;
|
|
} else if ((b >= $0 && b <= $9) ||
|
|
(b >= $a && b <= $z) ||
|
|
(b >= $A && b <= $Z) ||
|
|
b == $_) {
|
|
tmp.add(b);
|
|
inEscaped = false;
|
|
} else {
|
|
pushToken();
|
|
tmp.add(b);
|
|
pushToken();
|
|
inEscaped = false;
|
|
}
|
|
}
|
|
pushToken();
|
|
|
|
return tokens;
|
|
}
|
|
|
|
class Token {
|
|
final String value;
|
|
final int offset;
|
|
Token? next;
|
|
|
|
Token(this.value, this.offset);
|
|
|
|
@override
|
|
String toString() => "$value";
|
|
}
|
|
|
|
abstract class Node {
|
|
/// Not really depth,
|
|
/// but minimum number of steps to the possibility of ending.
|
|
int? depth;
|
|
|
|
void accept(Visitor visitor);
|
|
void visitChildren(Visitor visitor);
|
|
}
|
|
|
|
class DummyNode extends Node {
|
|
@override
|
|
void accept(Visitor visitor) {
|
|
visitor.visitDummyNode(this);
|
|
}
|
|
|
|
@override
|
|
void visitChildren(Visitor visitor) {}
|
|
}
|
|
|
|
class Empty extends Node {
|
|
@override
|
|
void accept(Visitor visitor) {
|
|
visitor.visitEmpty(this);
|
|
}
|
|
|
|
@override
|
|
void visitChildren(Visitor visitor) {}
|
|
}
|
|
|
|
class AnyCharNode extends Node {
|
|
final Token token;
|
|
|
|
AnyCharNode(this.token);
|
|
|
|
@override
|
|
String toString() => "$token";
|
|
|
|
@override
|
|
void accept(Visitor visitor) {
|
|
visitor.visitAnyCharNode(this);
|
|
}
|
|
|
|
@override
|
|
void visitChildren(Visitor visitor) {}
|
|
}
|
|
|
|
class EndOfFileNode extends Node {
|
|
final Token token;
|
|
|
|
EndOfFileNode(this.token);
|
|
|
|
@override
|
|
String toString() => "$token";
|
|
|
|
@override
|
|
void accept(Visitor visitor) {
|
|
visitor.visitEndOfFileNode(this);
|
|
}
|
|
|
|
@override
|
|
void visitChildren(Visitor visitor) {}
|
|
}
|
|
|
|
class Leaf extends Node {
|
|
final Token token;
|
|
|
|
Leaf(this.token);
|
|
|
|
@override
|
|
String toString() => "$token";
|
|
|
|
@override
|
|
void accept(Visitor visitor) {
|
|
visitor.visitLeaf(this);
|
|
}
|
|
|
|
@override
|
|
void visitChildren(Visitor visitor) {}
|
|
}
|
|
|
|
class Sequence extends Node {
|
|
final List<Node> nodes;
|
|
|
|
Sequence(this.nodes);
|
|
|
|
@override
|
|
String toString() => "$nodes";
|
|
|
|
@override
|
|
void accept(Visitor visitor) {
|
|
visitor.visitSequence(this);
|
|
}
|
|
|
|
@override
|
|
void visitChildren(Visitor visitor) {
|
|
for (Node node in nodes) {
|
|
node.accept(visitor);
|
|
}
|
|
}
|
|
}
|
|
|
|
class OrNode extends Node {
|
|
List<Node> nodes = [];
|
|
List<Node>? _nodesFiltered;
|
|
|
|
List<Node> get nodesFiltered {
|
|
return _nodesFiltered ??= _filterNodes();
|
|
}
|
|
|
|
List<Node> _filterNodes() {
|
|
bool skipLeaf(Leaf leaf) {
|
|
const Set<String> skip = {
|
|
"functionType",
|
|
"multiLineString",
|
|
"SUPER",
|
|
"awaitExpression",
|
|
"functionExpression",
|
|
};
|
|
return skip.contains(leaf.token.value);
|
|
}
|
|
|
|
List<Node> result = [];
|
|
for (Node node in nodes) {
|
|
if (node is Sequence &&
|
|
node.nodes
|
|
.where((child) => child is Leaf && skipLeaf(child))
|
|
.isNotEmpty) {
|
|
// Skip
|
|
} else if (node is Leaf && skipLeaf(node)) {
|
|
// Skip
|
|
} else {
|
|
result.add(node);
|
|
}
|
|
}
|
|
if (result.isEmpty) return nodes;
|
|
return result;
|
|
}
|
|
|
|
@override
|
|
String toString() => "${nodes.join(" | ")}";
|
|
|
|
@override
|
|
void accept(Visitor visitor) {
|
|
visitor.visitOrNode(this);
|
|
}
|
|
|
|
@override
|
|
void visitChildren(Visitor visitor) {
|
|
for (Node node in nodes) {
|
|
node.accept(visitor);
|
|
}
|
|
}
|
|
}
|
|
|
|
class RangeNode extends Node {
|
|
final Literal a;
|
|
final Literal b;
|
|
|
|
RangeNode(this.a, this.b);
|
|
|
|
@override
|
|
String toString() => "$a .. $b";
|
|
|
|
@override
|
|
void accept(Visitor visitor) {
|
|
visitor.visitRangeNode(this);
|
|
}
|
|
|
|
@override
|
|
void visitChildren(Visitor visitor) {
|
|
a.accept(visitor);
|
|
b.accept(visitor);
|
|
}
|
|
}
|
|
|
|
class Optional extends Node {
|
|
final Node a;
|
|
|
|
Optional(this.a);
|
|
|
|
@override
|
|
String toString() => "($a)?";
|
|
|
|
@override
|
|
void accept(Visitor visitor) {
|
|
visitor.visitOptional(this);
|
|
}
|
|
|
|
@override
|
|
void visitChildren(Visitor visitor) {
|
|
a.accept(visitor);
|
|
}
|
|
}
|
|
|
|
class OneOrMore extends Node {
|
|
final Node a;
|
|
|
|
OneOrMore(this.a);
|
|
|
|
@override
|
|
String toString() => "($a)+";
|
|
|
|
@override
|
|
void accept(Visitor visitor) {
|
|
visitor.visitOneOrMore(this);
|
|
}
|
|
|
|
@override
|
|
void visitChildren(Visitor visitor) {
|
|
a.accept(visitor);
|
|
}
|
|
}
|
|
|
|
class ZeroOrMore extends Node {
|
|
final Node a;
|
|
|
|
ZeroOrMore(this.a);
|
|
|
|
@override
|
|
String toString() => "($a)*";
|
|
|
|
@override
|
|
void accept(Visitor visitor) {
|
|
visitor.visitZeroOrMore(this);
|
|
}
|
|
|
|
@override
|
|
void visitChildren(Visitor visitor) {
|
|
a.accept(visitor);
|
|
}
|
|
}
|
|
|
|
class TildeNode extends Node {
|
|
final Node a;
|
|
|
|
TildeNode(this.a);
|
|
|
|
@override
|
|
String toString() => "~($a)";
|
|
|
|
@override
|
|
void accept(Visitor visitor) {
|
|
visitor.visitTildeNode(this);
|
|
}
|
|
|
|
@override
|
|
void visitChildren(Visitor visitor) {
|
|
a.accept(visitor);
|
|
}
|
|
}
|
|
|
|
class Literal extends Node {
|
|
String content;
|
|
|
|
Literal(this.content);
|
|
|
|
@override
|
|
String toString() => "'${content}'";
|
|
|
|
@override
|
|
void accept(Visitor visitor) {
|
|
visitor.visitLiteral(this);
|
|
}
|
|
|
|
@override
|
|
void visitChildren(Visitor visitor) {}
|
|
}
|
|
|
|
abstract class Visitor {
|
|
void defaultNode(Node node) {
|
|
node.visitChildren(this);
|
|
}
|
|
|
|
void visitDummyNode(DummyNode node) => defaultNode(node);
|
|
void visitEmpty(Empty node) => defaultNode(node);
|
|
void visitLeaf(Leaf node) => defaultNode(node);
|
|
void visitAnyCharNode(AnyCharNode node) => defaultNode(node);
|
|
void visitEndOfFileNode(EndOfFileNode node) => defaultNode(node);
|
|
void visitSequence(Sequence node) => defaultNode(node);
|
|
void visitOrNode(OrNode node) => defaultNode(node);
|
|
void visitRangeNode(RangeNode node) => defaultNode(node);
|
|
void visitOptional(Optional node) => defaultNode(node);
|
|
void visitOneOrMore(OneOrMore node) => defaultNode(node);
|
|
void visitZeroOrMore(ZeroOrMore node) => defaultNode(node);
|
|
void visitTildeNode(TildeNode node) => defaultNode(node);
|
|
void visitLiteral(Literal node) => defaultNode(node);
|
|
}
|