Files
sdk/pkg/front_end/tool/fuzz/gReader.dart
T
Jens Johansen c95568aede [CFE] Fuzzer using Dart.G
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>
2025-01-09 23:13:33 -08:00

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);
}