blob: 49894bd1daef5e8bbd7ad5709007f7609d8c26a6 [file] [log] [blame]
// 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 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;
GraphIsomorphizer(this.names, this.maxBit,
{this.outDirectory: '.', this.skipCopyright: 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();
// 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.
Set<String> uniques = {};
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");
// 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');
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) {
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 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);
}
void main(List<String> args) {
var graphIsomorphizer = createGraphIsomorphizer(args);
graphIsomorphizer.run();
}