blob: 308be95f6d10634c5f73da2b5590b40f8381b79e [file] [log] [blame] [edit]
// 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);
}