mirror of
https://github.com/dart-lang/sdk
synced 2024-09-15 21:20:36 +00:00
5c2ca380a4
Change-Id: Ie9c7548dc81b58db0c1149124db79e4cf1430cc1 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/239723 Reviewed-by: Nate Biggs <natebiggs@google.com> Reviewed-by: Joshua Litt <joshualitt@google.com> Commit-Queue: Stephen Adams <sra@google.com>
519 lines
17 KiB
Dart
519 lines
17 KiB
Dart
// Copyright (c) 2021, 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.
|
|
|
|
// @dart = 2.10
|
|
|
|
/// This tool builds a program with a deferred graph isomorphic to the provided
|
|
/// graph, or generates permutations of bits and the associated files to
|
|
/// generate complex deferred graphs.
|
|
|
|
/// For example, if 5 bits are permuted, we end up with files like:
|
|
/// lib1.dart
|
|
/// lib2.dart
|
|
/// lib3.dart
|
|
/// lib4.dart
|
|
/// lib5.dart
|
|
/// libB.dart
|
|
/// lib_000_01.dart
|
|
/// lib_000_10.dart
|
|
/// lib_001_00.dart
|
|
/// lib_010_00.dart
|
|
/// lib_100_00.dart
|
|
/// main.dart
|
|
///
|
|
/// Where
|
|
/// main.dart contains main().
|
|
/// libX.dart contains the actual deferred import of the file with the bit at
|
|
/// the X position starting from the left, ie lib1 imports lib_100_00, lib2
|
|
/// imports lib_010_00, etc.
|
|
/// libImport.dart is the 'top of the diamond' which contains all of the code.
|
|
/// lib_XXX_XX.dart invokes all of the functions in libImport which have a
|
|
/// 1 bit at that position, ie lib_100_00 invokes all code in libImport with
|
|
/// a first bit of 1, f100_00, f110_00, etc.
|
|
///
|
|
/// Note: There are restrictions to what we can generate. Specifically, certain
|
|
/// OutputUnits can never be empty, namely we will always generate one file for
|
|
/// each entryLib, and because of our dependency on expect, we will always have
|
|
/// a file representing the intersection of all import entities. So, for example
|
|
/// with three bits, each of 100, 010, 001 and 111 must be present in the graph
|
|
/// file, but 110, 101, and 011 are optional.
|
|
|
|
// TODO(joshualitt): This is a good start for a fuzzer. There is still work to
|
|
// do:
|
|
// * Emit some classes as const and some not
|
|
// * Randomize what we emit as we walk the graph so it is more sparse.
|
|
|
|
import 'dart:io';
|
|
import 'dart:math';
|
|
import 'package:dart_style/dart_style.dart' show DartFormatter;
|
|
|
|
typedef NameFunc = String Function(List<int>, int);
|
|
|
|
/// A simple constant pass through function for bit strings that don't need
|
|
/// special printing, ie stops.
|
|
String passThroughNameFunc(List<int> l, int i) => l[i].toString();
|
|
|
|
/// Generates all permutations of bits recursively.
|
|
void generatePermutedNames(
|
|
Map<int, List<List<int>>> names, int maxBit, List<int> bits, int bit) {
|
|
if (bit == maxBit) {
|
|
for (int i = 0; i < bits.length; i++) {
|
|
if (bits[i] == 1) {
|
|
names.putIfAbsent(i, () => []);
|
|
names[i].add(List.from(bits));
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
generatePermutedNames(names, maxBit, bits, bit + 1);
|
|
bits[bit] = 1;
|
|
generatePermutedNames(names, maxBit, bits, bit + 1);
|
|
bits[bit] = 0;
|
|
}
|
|
|
|
/// A helper function to generate names from lists of strings of bits.
|
|
int namesFromGraphFileLines(
|
|
List<String> lines, Map<int, List<List<int>>> names) {
|
|
int maxBit = 0;
|
|
for (var line in lines) {
|
|
List<int> name = [];
|
|
// Each line should have the same length.
|
|
assert(maxBit == 0 || maxBit == line.length);
|
|
maxBit = max(maxBit, line.length);
|
|
for (int i = 0; i < line.length; i++) {
|
|
var bit = line[i];
|
|
if (bit == '1') {
|
|
name.add(1);
|
|
(names[i] ??= []).add(name);
|
|
} else {
|
|
name.add(0);
|
|
}
|
|
}
|
|
}
|
|
return maxBit;
|
|
}
|
|
|
|
/// Parses names from a graph file dumped from dart2js and returns the max bit.
|
|
int namesFromGraphFile(String graphFile, Map<int, List<List<int>>> names) {
|
|
var lines = File(graphFile).readAsLinesSync();
|
|
return namesFromGraphFileLines(lines, names);
|
|
}
|
|
|
|
class ImportData {
|
|
String import;
|
|
String entryPoint;
|
|
|
|
ImportData(this.import, this.entryPoint);
|
|
}
|
|
|
|
class GraphIsomorphizer {
|
|
/// The output directory, only relevant if files are written out.
|
|
final String outDirectory;
|
|
|
|
/// Various buffers for the files the GraphIsomorphizer generates.
|
|
StringBuffer rootImportBuffer = StringBuffer();
|
|
StringBuffer mainBuffer = StringBuffer();
|
|
Map<String, StringBuffer> mixerLibBuffers = {};
|
|
Map<String, StringBuffer> entryLibBuffers = {};
|
|
|
|
/// A map of bit positions to lists of bit lists.
|
|
final Map<int, List<List<int>>> names;
|
|
|
|
/// A map of bit positions to lists of class names.
|
|
final Map<int, List<String>> classNames = {};
|
|
final Map<int, List<String>> mixerClassNames = {};
|
|
|
|
/// A map of bit positions to lists of mixin names.
|
|
final Map<int, List<String>> mixinNames = {};
|
|
|
|
/// A map of bit positions to lists of class names used only as types.
|
|
final Map<int, List<String>> typeNames = {};
|
|
final Map<int, List<String>> mixerTypeNames = {};
|
|
final Map<int, List<String>> closureNames = {};
|
|
|
|
/// We will permute bits up until the maximum bit.
|
|
int maxBit = 0;
|
|
|
|
/// The 'top of the diamond' import file containing all code.
|
|
final String rootImportFilename = 'libImport.dart';
|
|
|
|
/// The main filename.
|
|
final String mainFilename = 'main.dart';
|
|
|
|
/// A bool to omit the comment block.
|
|
final bool skipCopyright;
|
|
|
|
// A bool to generate simple code within test files.
|
|
final bool simple;
|
|
|
|
GraphIsomorphizer(this.names, this.maxBit,
|
|
{this.outDirectory: '.', this.skipCopyright: false, this.simple: false});
|
|
|
|
void noInlineDecorator(StringBuffer out) {
|
|
out.write("@pragma('dart2js:noInline')\n");
|
|
}
|
|
|
|
void importExpect(StringBuffer out) {
|
|
out.write('import "package:expect/expect.dart";\n\n');
|
|
}
|
|
|
|
void newline(StringBuffer out) {
|
|
out.write('\n');
|
|
}
|
|
|
|
/// Generates the header for a file.
|
|
void generateHeader(StringBuffer out) {
|
|
if (!skipCopyright) {
|
|
out.write("""
|
|
// Copyright (c) 2021, 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.
|
|
|
|
// This file was autogenerated by the pkg/compiler/tool/graph_isomorphizer.dart.
|
|
""");
|
|
}
|
|
}
|
|
|
|
/// Generates the root import, where classes, types, mixins, and closures
|
|
/// live.
|
|
void generateRootImport(StringBuffer out) {
|
|
generateHeader(out);
|
|
importExpect(out);
|
|
|
|
// We verify that each function in libImport is invoked only once from each
|
|
// mixerLib and that only the correct functions are called, ie for lib_001,
|
|
// only functions with XX1 are invoked.
|
|
out.write('void v(Set<String> u, String name, int bit) {\n' +
|
|
' Expect.isTrue(u.add(name));\n' +
|
|
" Expect.equals(name[bit], '1');\n" +
|
|
'}\n\n');
|
|
|
|
// Sort the names to ensure they are in a canonical order.
|
|
var nameKeys = names.keys.toList();
|
|
nameKeys.sort();
|
|
|
|
Set<String> uniques = {};
|
|
if (!simple) {
|
|
// Generate the 'base' classes, mixins, and types which will be combined to
|
|
// generate hierarchies. Also generate a const instance per class and a closure
|
|
// to invoke.
|
|
for (var bitPosition in nameKeys) {
|
|
var bitsList = names[bitPosition];
|
|
for (var bits in bitsList) {
|
|
var name = generateBitString(bits);
|
|
if (!uniques.add(name)) continue;
|
|
String className = 'C$name';
|
|
String mixinName = 'M$name';
|
|
String typeName = 'T$name';
|
|
(classNames[bitPosition] ??= []).add(className);
|
|
(mixinNames[bitPosition] ??= []).add(mixinName);
|
|
(typeNames[bitPosition] ??= []).add(typeName);
|
|
(mixerClassNames[bitPosition] ??= []).add(className);
|
|
(mixerTypeNames[bitPosition] ??= []).add(typeName);
|
|
out.write('class $className { const $className(); }\n');
|
|
out.write('class $mixinName {}\n');
|
|
out.write('class $typeName {}\n');
|
|
out.write('const $className i$className = const $className();\n');
|
|
out.write('closure$className(foo) => ($className unused) ');
|
|
out.write('=> i$className.toString() == foo.toString();\n');
|
|
}
|
|
}
|
|
|
|
// Generate combined classes and types, as well as const instances and
|
|
// closures.
|
|
newline(out);
|
|
uniques = {};
|
|
for (var bitPosition in nameKeys) {
|
|
var bitsList = names[bitPosition];
|
|
for (var bits in bitsList) {
|
|
var name = generateBitString(bits);
|
|
var bitCount = bits.reduce((a, b) => a + b);
|
|
var baseName = 'C$name';
|
|
if (!uniques.add(baseName)) continue;
|
|
if (bitCount > 1) {
|
|
List<String> classes = [];
|
|
List<String> mixins = [];
|
|
List<String> types = [];
|
|
for (int i = 0; i < bits.length; i++) {
|
|
if (bits[i] == 1) {
|
|
classes.addAll(classNames[i]);
|
|
mixins.addAll(mixinNames[i]);
|
|
types.addAll(typeNames[i]);
|
|
}
|
|
}
|
|
String mixinString = mixins.join(', ');
|
|
int count = 1;
|
|
assert(classes.length == types.length);
|
|
for (int i = 0; i < classes.length; i++) {
|
|
var cls = classes[i];
|
|
var type = types[i];
|
|
List<String> classImpls = [];
|
|
List<String> typeImpls = [];
|
|
if (i > 0) {
|
|
classImpls.addAll(classes.sublist(0, i));
|
|
typeImpls.addAll(types.sublist(0, i));
|
|
}
|
|
if (i < classes.length - 1) {
|
|
classImpls.addAll(classes.sublist(i + 1));
|
|
typeImpls.addAll(types.sublist(i + 1));
|
|
}
|
|
var classImplementsString = classImpls.join(', ');
|
|
String className = '${baseName}_class_${count}';
|
|
out.write('class $className extends $cls with $mixinString ');
|
|
out.write(
|
|
'implements $classImplementsString { const $className(); }\n');
|
|
out.write('const $className i$className = const $className();\n');
|
|
out.write('closure$className(foo) => ($className unused) ');
|
|
out.write('=> i$className.toString() == foo.toString();\n');
|
|
|
|
var typeImplementsString = typeImpls.join(', ');
|
|
String typeName = 'T${name}_type__${count}';
|
|
out.write('class $typeName extends $type with $mixinString ');
|
|
out.write('implements $typeImplementsString {}\n');
|
|
for (int i = 0; i < bits.length; i++) {
|
|
if (bits[i] == 1) {
|
|
mixerClassNames[i].add(className);
|
|
mixerTypeNames[i].add(typeName);
|
|
}
|
|
}
|
|
count++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Generate functions.
|
|
newline(out);
|
|
uniques = {};
|
|
for (var name in nameKeys) {
|
|
var bitsList = names[name];
|
|
for (var bits in bitsList) {
|
|
var name = generateBitString(bits);
|
|
if (uniques.add(name)) {
|
|
noInlineDecorator(out);
|
|
var stringBits = generateBitString(bits, withStops: false);
|
|
out.write(
|
|
"f$name(Set<String> u, int b) => v(u, '$stringBits', b);\n");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Generates a mixerLib which will be loaded as a deferred library from an entryLib.
|
|
void generateMixerLib(
|
|
String name, StringBuffer out, String import, List<int> bits, int bit) {
|
|
generateHeader(out);
|
|
importExpect(out);
|
|
out.write("import '$import';\n\n");
|
|
|
|
if (!simple) {
|
|
// create type test.
|
|
noInlineDecorator(out);
|
|
out.write('typeTest(dynamic t) {\n');
|
|
for (var type in mixerTypeNames[bit]) {
|
|
out.write(' if (t is $type) { return true; }\n');
|
|
}
|
|
out.write(' return false;\n');
|
|
out.write('}\n\n');
|
|
}
|
|
|
|
noInlineDecorator(out);
|
|
out.write('g$name() {\n');
|
|
|
|
if (!simple) {
|
|
out.write(' // C${generateCommentName(bits, bit)};\n');
|
|
|
|
// Construct new instances of each class and pass them to the typeTest
|
|
for (var cls in mixerClassNames[bit]) {
|
|
out.write(' Expect.isFalse(typeTest($cls()));\n');
|
|
}
|
|
newline(out);
|
|
|
|
// Invoke the test closure for each class.
|
|
for (var cls in mixerClassNames[bit]) {
|
|
out.write(' Expect.isTrue(closure$cls($cls())($cls()));\n');
|
|
}
|
|
newline(out);
|
|
|
|
// Verify the runtimeTypes of the closures haven't been mangled.
|
|
for (var cls in mixerClassNames[bit]) {
|
|
out.write(
|
|
' Expect.equals(closure$cls($cls()).runtimeType.toString(), ');
|
|
out.write("'($cls) => bool');\n");
|
|
}
|
|
}
|
|
newline(out);
|
|
|
|
// Collect the names so we can sort them and put them in a canonical order.
|
|
int count = 0;
|
|
List<String> namesBits = [];
|
|
names[bit].forEach((nameBits) {
|
|
var nameString = generateBitString(nameBits);
|
|
namesBits.add(nameString);
|
|
count++;
|
|
});
|
|
|
|
out.write(' Set<String> uniques = {};\n\n'
|
|
' // f${generateCommentName(bits, bit)};\n');
|
|
|
|
namesBits.sort();
|
|
for (var name in namesBits) {
|
|
out.write(' f$name(uniques, $bit);\n');
|
|
}
|
|
|
|
// We expect 'count' unique strings added to be added to 'uniques'.
|
|
out.write(" Expect.equals($count, uniques.length);\n"
|
|
'}\n');
|
|
}
|
|
|
|
/// Generates a string of bits, with optional parameters to control how the
|
|
/// bits print.
|
|
String generateBitString(List<int> bits,
|
|
{NameFunc f: passThroughNameFunc, bool withStops: true}) {
|
|
int stop = 0;
|
|
StringBuffer sb = StringBuffer();
|
|
for (int i = 0; i < bits.length; i++) {
|
|
if (stop++ % 3 == 0 && withStops) {
|
|
sb.write('_');
|
|
}
|
|
sb.write(f(bits, i));
|
|
}
|
|
return sb.toString();
|
|
}
|
|
|
|
/// Generates a pretty bit string for use in comments.
|
|
String generateCommentName(List<int> bits, int fixBit) {
|
|
return generateBitString(bits,
|
|
f: (List<int> bits, int bit) => bit == fixBit ? '1' : '*');
|
|
}
|
|
|
|
/// Generates an entryLib file.
|
|
void generateEntryLib(StringBuffer out, String mainName, String funcName,
|
|
String import, int bit) {
|
|
generateHeader(out);
|
|
var name = 'b$bit';
|
|
out.write("import '$import' deferred as $name;\n\n"
|
|
'$mainName async {\n'
|
|
' await $name.loadLibrary();\n'
|
|
' $name.g$funcName();\n'
|
|
'}\n');
|
|
}
|
|
|
|
/// Generates entry and mixer libs for the supplied names.
|
|
List<ImportData> generateEntryAndMixerLibs() {
|
|
// Generates each lib_XXX.dart and the associated entryLib file.
|
|
List<ImportData> importData = [];
|
|
for (int i = 1; i <= maxBit; i++) {
|
|
// Generate the bit list representing this library. ie a list of all
|
|
// 0s with a single 1 bit flipped.
|
|
int oneBit = i - 1;
|
|
List<int> bits = [];
|
|
for (int j = 0; j < maxBit; j++) bits.add(j == oneBit ? 1 : 0);
|
|
|
|
// Generate the mixerLib for this entryLib.
|
|
var name = generateBitString(bits);
|
|
var mixerLibBuffer = StringBuffer();
|
|
var mixerLibName = "lib$name.dart";
|
|
generateMixerLib(name, mixerLibBuffer, rootImportFilename, bits, oneBit);
|
|
mixerLibBuffers[mixerLibName] = mixerLibBuffer;
|
|
|
|
// Generate the entryLib for this mixerLib.
|
|
var entryLibName = 'lib$i.dart';
|
|
var entryFuncName = 'entryLib$i()';
|
|
var entryLibBuffer = StringBuffer();
|
|
generateEntryLib(entryLibBuffer, entryFuncName, name, mixerLibName, i);
|
|
entryLibBuffers[entryLibName] = entryLibBuffer;
|
|
|
|
// Stash the entry point in entryLib for later reference in the main file.
|
|
importData.add(ImportData(entryLibName, entryFuncName));
|
|
}
|
|
return importData;
|
|
}
|
|
|
|
/// Generates the main file.
|
|
void generateMain(StringBuffer out, List<ImportData> importData) {
|
|
generateHeader(mainBuffer);
|
|
for (var data in importData) {
|
|
out.write("import '${data.import}';\n");
|
|
}
|
|
out.write('\n'
|
|
'main() {\n');
|
|
for (var data in importData) {
|
|
out.write(' ${data.entryPoint};\n');
|
|
}
|
|
out.write('}\n');
|
|
}
|
|
|
|
/// Generates all files into buffers.
|
|
void generateFiles() {
|
|
generateRootImport(rootImportBuffer);
|
|
var importData = generateEntryAndMixerLibs();
|
|
generateMain(mainBuffer, importData);
|
|
}
|
|
|
|
/// Helper to dump contents to file.
|
|
void writeToFile(String filename, StringBuffer contents) {
|
|
var file = File(this.outDirectory + '/' + filename);
|
|
file.createSync(recursive: true);
|
|
var sink = file.openWrite();
|
|
sink.write(DartFormatter().format(contents.toString()));
|
|
sink.close();
|
|
}
|
|
|
|
/// Writes all buffers to files.
|
|
void writeFiles() {
|
|
mixerLibBuffers.forEach(writeToFile);
|
|
entryLibBuffers.forEach(writeToFile);
|
|
writeToFile(rootImportFilename, rootImportBuffer);
|
|
writeToFile(mainFilename, mainBuffer);
|
|
}
|
|
|
|
/// Generate and write files.
|
|
void run() {
|
|
generateFiles();
|
|
writeFiles();
|
|
}
|
|
}
|
|
|
|
/// Creates a GraphIsomorphizer based on the provided args.
|
|
GraphIsomorphizer createGraphIsomorphizer(List<String> args) {
|
|
bool simple = true;
|
|
int maxBit = 0;
|
|
String graphFile = '';
|
|
String outDirectory = '.';
|
|
|
|
for (var arg in args) {
|
|
if (arg.startsWith('--max-bit')) {
|
|
maxBit = int.parse(arg.substring('--max-bit='.length));
|
|
}
|
|
if (arg.startsWith('--graph-file')) {
|
|
graphFile = arg.substring('--graph-file='.length);
|
|
}
|
|
if (arg.startsWith('--out-dir')) {
|
|
outDirectory = arg.substring('--out-dir='.length);
|
|
}
|
|
if (arg == '--simple') {
|
|
simple = true;
|
|
}
|
|
}
|
|
|
|
// If we don't have a graphFile, then we generate all permutations of bits up
|
|
// to maxBit.
|
|
Map<int, List<List<int>>> names = {};
|
|
if (graphFile.isEmpty) {
|
|
List<int> bits = List.filled(maxBit, 0);
|
|
generatePermutedNames(names, maxBit, bits, 0);
|
|
} else {
|
|
maxBit = namesFromGraphFile(graphFile, names);
|
|
}
|
|
return GraphIsomorphizer(names, maxBit,
|
|
outDirectory: outDirectory, simple: simple);
|
|
}
|
|
|
|
void main(List<String> args) {
|
|
var graphIsomorphizer = createGraphIsomorphizer(args);
|
|
graphIsomorphizer.run();
|
|
}
|