Reland "[dart2js] Add support for bundling fragments."

This is a reland of d0f55d0e42

Original change's description:
> [dart2js] Add support for bundling fragments.
>
> Also changes the default to bundling rather than interleaving fragments.
>
> Change-Id: Id79d03a8a8b5be7465b8535f6c9c47dfad120c9c
> Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/191484
> Commit-Queue: Joshua Litt <joshualitt@google.com>
> Reviewed-by: Stephen Adams <sra@google.com>
> Reviewed-by: Sigmund Cherem <sigmund@google.com>

Change-Id: I0347ddb6dd93eb57f0abc259fc477ec3a9d7231b
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/194323
Reviewed-by: Sigmund Cherem <sigmund@google.com>
Commit-Queue: Joshua Litt <joshualitt@google.com>
This commit is contained in:
Joshua Litt 2021-04-08 17:46:50 +00:00 committed by commit-bot@chromium.org
parent 9d5846b86e
commit 28aa79ddd7
13 changed files with 154 additions and 136 deletions

View file

@ -143,6 +143,10 @@ abstract class CodeOutput implements SourceLocationsProvider {
}
abstract class AbstractCodeOutput extends CodeOutput {
final List<CodeOutputListener> _listeners;
AbstractCodeOutput([this._listeners]);
Map<String, _SourceLocationsImpl> sourceLocationsMap =
<String, _SourceLocationsImpl>{};
@override
@ -150,12 +154,17 @@ abstract class AbstractCodeOutput extends CodeOutput {
void _addInternal(String text);
void _add(String text) {
_addInternal(text);
_listeners?.forEach((listener) => listener.onText(text));
}
@override
void add(String text) {
if (isClosed) {
throw new StateError("Code output is closed. Trying to write '$text'.");
}
_addInternal(text);
_add(text);
}
@override
@ -166,7 +175,7 @@ abstract class AbstractCodeOutput extends CodeOutput {
if (!other.isClosed) {
other.close();
}
_addInternal(other.getText());
_add(other.getText());
}
@override
@ -175,6 +184,7 @@ abstract class AbstractCodeOutput extends CodeOutput {
throw new StateError("Code output is already closed.");
}
isClosed = true;
_listeners?.forEach((listener) => listener.onDone(length));
}
@override
@ -194,6 +204,8 @@ abstract class BufferedCodeOutput {
class CodeBuffer extends AbstractCodeOutput implements BufferedCodeOutput {
StringBuffer buffer = new StringBuffer();
CodeBuffer([List<CodeOutputListener> listeners]) : super(listeners);
@override
void _addInternal(String text) {
buffer.write(text);
@ -218,25 +230,19 @@ class StreamCodeOutput extends AbstractCodeOutput {
@override
int length = 0;
final OutputSink output;
final List<CodeOutputListener> _listeners;
StreamCodeOutput(this.output, [this._listeners]);
StreamCodeOutput(this.output, [List<CodeOutputListener> listeners])
: super(listeners);
@override
void _addInternal(String text) {
output.add(text);
length += text.length;
if (_listeners != null) {
_listeners.forEach((listener) => listener.onText(text));
}
}
@override
void close() {
output.close();
super.close();
if (_listeners != null) {
_listeners.forEach((listener) => listener.onDone(length));
}
}
}

View file

@ -9,7 +9,7 @@ import 'package:js_ast/js_ast.dart';
import '../common.dart';
import '../options.dart';
import '../dump_info.dart' show DumpInfoTask;
import '../io/code_output.dart' show CodeBuffer;
import '../io/code_output.dart' show CodeBuffer, CodeOutputListener;
import 'js_source_mapping.dart';
export 'package:js_ast/js_ast.dart';
@ -38,12 +38,13 @@ CodeBuffer createCodeBuffer(Node node, CompilerOptions compilerOptions,
JavaScriptSourceInformationStrategy sourceInformationStrategy,
{DumpInfoTask monitor,
bool allowVariableMinification: true,
Renamer renamerForNames: JavaScriptPrintingOptions.identityRenamer}) {
Renamer renamerForNames: JavaScriptPrintingOptions.identityRenamer,
List<CodeOutputListener> listeners: const []}) {
JavaScriptPrintingOptions options = new JavaScriptPrintingOptions(
shouldCompressOutput: compilerOptions.enableMinification,
minifyLocalVariables: allowVariableMinification,
renamerForNames: renamerForNames);
CodeBuffer outBuffer = new CodeBuffer();
CodeBuffer outBuffer = new CodeBuffer(listeners);
SourceInformationProcessor sourceInformationProcessor =
sourceInformationStrategy.createProcessor(
new SourceMapperProviderImpl(outBuffer),

View file

@ -1894,11 +1894,16 @@ class FragmentEmitter {
void finalizeDeferredLoadingData(
Map<String, List<CodeFragment>> codeFragmentsToLoad,
Map<CodeFragment, FinalizedFragment> codeFragmentMap,
Map<FinalizedFragment, String> deferredLoadHashes,
Map<CodeFragment, String> deferredLoadHashes,
DeferredLoadingState deferredLoadingState) {
if (codeFragmentsToLoad.isEmpty) return;
Map<FinalizedFragment, int> fragmentIndexes = {};
// We store a map of indices to uris and hashes. Because multiple
// [CodeFragments] can map to a single file, a uri may appear multiple times
// in [fragmentUris] once per [CodeFragment] reference in that file.
// TODO(joshualitt): Use a string table to avoid duplicating part file
// names.
Map<CodeFragment, int> fragmentIndexes = {};
List<String> fragmentUris = [];
List<String> fragmentHashes = [];
@ -1909,14 +1914,14 @@ class FragmentEmitter {
List<js.Expression> indexes = [];
for (var codeFragment in codeFragments) {
var fragment = codeFragmentMap[codeFragment];
String fragmentHash = deferredLoadHashes[fragment];
if (fragmentHash == null) continue;
int index = fragmentIndexes[fragment];
String codeFragmentHash = deferredLoadHashes[codeFragment];
if (codeFragmentHash == null) continue;
int index = fragmentIndexes[codeFragment];
if (index == null) {
index = fragmentIndexes[fragment] = fragmentIndexes.length;
index = fragmentIndexes[codeFragment] = fragmentIndexes.length;
fragmentUris.add(
"${fragment.outputFileName}.${ModelEmitter.deferredExtension}");
fragmentHashes.add(fragmentHash);
fragmentHashes.add(codeFragmentHash);
}
indexes.add(js.number(index));
}

View file

@ -165,7 +165,8 @@ class PreFragment {
final Set<PreFragment> predecessors = {};
FinalizedFragment finalizedFragment;
int size = 0;
bool shouldInterleave = true;
// TODO(joshualitt): interleave dynamically when it makes sense.
bool shouldInterleave = false;
PreFragment(
this.outputFileName, EmittedOutputUnit emittedOutputUnit, this.size) {

View file

@ -270,22 +270,20 @@ class ModelEmitter {
// Finalize and emit fragments.
Map<OutputUnit, CodeFragment> outputUnitMap = {};
Map<CodeFragment, FinalizedFragment> codeFragmentMap = {};
Map<FinalizedFragment, EmittedCodeFragment> deferredFragmentsCode = {};
Map<FinalizedFragment, List<EmittedCodeFragment>> deferredFragmentsCode =
{};
for (var preDeferredFragment in preDeferredFragments) {
var finalizedFragment =
preDeferredFragment.finalize(program, outputUnitMap, codeFragmentMap);
// TODO(joshualitt): Support bundling.
assert(finalizedFragment.codeFragments.length == 1);
var codeFragment = finalizedFragment.codeFragments.single;
js.Expression fragmentCode =
fragmentEmitter.emitCodeFragment(codeFragment, program.holders);
if (fragmentCode != null) {
deferredFragmentsCode[finalizedFragment] =
EmittedCodeFragment(codeFragment, fragmentCode);
} else {
finalizedFragment.codeFragments.forEach((codeFragment) {
for (var codeFragment in finalizedFragment.codeFragments) {
js.Expression fragmentCode =
fragmentEmitter.emitCodeFragment(codeFragment, program.holders);
if (fragmentCode != null) {
(deferredFragmentsCode[finalizedFragment] ??= [])
.add(EmittedCodeFragment(codeFragment, fragmentCode));
} else {
omittedOutputUnits.addAll(codeFragment.outputUnits);
});
}
}
}
@ -306,9 +304,11 @@ class ModelEmitter {
// Count tokens and run finalizers.
js.TokenCounter counter = new js.TokenCounter();
deferredFragmentsCode.values.forEach((emittedCodeFragment) {
counter.countTokens(emittedCodeFragment.code);
});
for (var emittedFragments in deferredFragmentsCode.values) {
for (var emittedFragment in emittedFragments) {
counter.countTokens(emittedFragment.code);
}
}
counter.countTokens(mainCode);
program.finalizers.forEach((js.TokenFinalizer f) => f.finalizeTokens());
@ -317,15 +317,15 @@ class ModelEmitter {
// deferred ASTs inside the parts) have any contents. We should wait until
// this point to decide if a part is empty.
Map<FinalizedFragment, String> hunkHashes =
Map<CodeFragment, String> codeFragmentHashes =
_task.measureSubtask('write fragments', () {
return writeDeferredFragments(deferredFragmentsCode);
return writeFinalizedFragments(deferredFragmentsCode);
});
// Now that we have written the deferred hunks, we can create the deferred
// loading data.
fragmentEmitter.finalizeDeferredLoadingData(
codeFragmentsToLoad, codeFragmentMap, hunkHashes, deferredLoadingState);
fragmentEmitter.finalizeDeferredLoadingData(codeFragmentsToLoad,
codeFragmentMap, codeFragmentHashes, deferredLoadingState);
_task.measureSubtask('write fragments', () {
writeMainFragment(mainFragment, mainCode,
@ -362,25 +362,6 @@ class ModelEmitter {
return new js.Comment(generatedBy(_options, flavor: '$flavor'));
}
/// Writes all deferred fragment's code into files.
///
/// Returns a map from fragment to its hashcode (as used for the deferred
/// library code).
///
/// Updates the shared [outputBuffers] field with the output.
Map<FinalizedFragment, String> writeDeferredFragments(
Map<FinalizedFragment, EmittedCodeFragment> fragmentsCode) {
Map<FinalizedFragment, String> hunkHashes = {};
fragmentsCode.forEach(
(FinalizedFragment fragment, EmittedCodeFragment emittedCodeFragment) {
hunkHashes[fragment] =
writeDeferredFragment(fragment, emittedCodeFragment.code);
});
return hunkHashes;
}
js.Statement buildDeferredInitializerGlobal() {
return js.js.statement(
'self.#deferredInitializers = '
@ -445,16 +426,24 @@ class ModelEmitter {
}
}
// Writes the given [fragment]'s [code] into a file.
//
// Returns the deferred fragment's hash.
//
// Updates the shared [outputBuffers] field with the output.
String writeDeferredFragment(FinalizedFragment fragment, js.Expression code) {
List<CodeOutputListener> outputListeners = [];
Hasher hasher = new Hasher();
outputListeners.add(hasher);
/// Writes all [FinalizedFragments] to files, returning a map of
/// [CodeFragment] to their initialization hashes.
Map<CodeFragment, String> writeFinalizedFragments(
Map<FinalizedFragment, List<EmittedCodeFragment>> fragmentsCode) {
Map<CodeFragment, String> fragmentHashes = {};
fragmentsCode.forEach((fragment, code) {
writeFinalizedFragment(fragment, code, fragmentHashes);
});
return fragmentHashes;
}
/// Writes a single [FinalizedFragment] and all of its [CodeFragments] to
/// file, updating the [fragmentHashes] map as necessary.
void writeFinalizedFragment(
FinalizedFragment fragment,
List<EmittedCodeFragment> fragmentCode,
Map<CodeFragment, String> fragmentHashes) {
List<CodeOutputListener> outputListeners = [];
LocationCollector locationCollector;
if (_shouldGenerateSourceMap) {
_task.measureSubtask('source-maps', () {
@ -463,55 +452,21 @@ class ModelEmitter {
});
}
String hunkPrefix = fragment.outputFileName;
CodeOutput output = new StreamCodeOutput(
String outputFileName = fragment.outputFileName;
CodeOutput output = StreamCodeOutput(
_outputProvider.createOutputSink(
hunkPrefix, deferredExtension, OutputType.jsPart),
outputFileName, deferredExtension, OutputType.jsPart),
outputListeners);
// TODO(joshualitt): This breaks dump_info when we merge, but fixing it will
// require updating the schema.
emittedOutputBuffers[fragment.canonicalOutputUnit] = output;
// The [code] contains the function that must be invoked when the deferred
// hunk is loaded.
// That function must be in a map from its hashcode to the function. Since
// we don't know the hash before we actually emit the code we store the
// function in a temporary field first:
//
// deferredInitializer.current = <pretty-printed code>;
// deferredInitializer[<hash>] = deferredInitializer.current;
js.Program program = new js.Program([
buildGeneratedBy(),
buildDeferredInitializerGlobal(),
js.js.statement('$deferredInitializersGlobal.current = #', code)
]);
CodeBuffer buffer = js.createCodeBuffer(
program, _options, _sourceInformationStrategy,
monitor: _dumpInfoTask);
_task.measureSubtask('emit buffers', () {
output.addBuffer(buffer);
});
// Make a unique hash of the code (before the sourcemaps are added)
// This will be used to retrieve the initializing function from the global
// variable.
String hash = hasher.getHash();
// Now we copy the deferredInitializer.current into its correct hash.
output.add('\n${deferredInitializersGlobal}["$hash"] = '
'${deferredInitializersGlobal}.current');
writeCodeFragments(fragmentCode, fragmentHashes, output);
if (_shouldGenerateSourceMap) {
_task.measureSubtask('source-maps', () {
Uri mapUri, partUri;
Uri sourceMapUri = _options.sourceMapUri;
Uri outputUri = _options.outputUri;
String partName = "$hunkPrefix.$partExtension";
String hunkFileName = "$hunkPrefix.$deferredExtension";
String partName = "$outputFileName.$partExtension";
String hunkFileName = "$outputFileName.$deferredExtension";
if (sourceMapUri != null) {
String mapFileName = hunkFileName + ".map";
@ -534,7 +489,61 @@ class ModelEmitter {
} else {
output.close();
}
}
/// Writes a list of [CodeFragments] to [CodeOutput].
void writeCodeFragments(List<EmittedCodeFragment> fragmentCode,
Map<CodeFragment, String> fragmentHashes, CodeOutput output) {
bool isFirst = true;
for (var emittedCodeFragment in fragmentCode) {
var codeFragment = emittedCodeFragment.codeFragment;
var code = emittedCodeFragment.code;
for (var outputUnit in codeFragment.outputUnits) {
emittedOutputBuffers[outputUnit] = output;
}
fragmentHashes[codeFragment] = writeCodeFragment(output, code, isFirst);
isFirst = false;
}
}
// Writes the given [fragment]'s [code] into a file.
//
// Returns the deferred fragment's hash.
//
// Updates the shared [outputBuffers] field with the output.
String writeCodeFragment(
CodeOutput output, js.Expression code, bool isFirst) {
// The [code] contains the function that must be invoked when the deferred
// hunk is loaded.
// That function must be in a map from its hashcode to the function. Since
// we don't know the hash before we actually emit the code we store the
// function in a temporary field first:
//
// deferredInitializer.current = <pretty-printed code>;
// deferredInitializer[<hash>] = deferredInitializer.current;
js.Program program = new js.Program([
if (isFirst) buildGeneratedBy(),
if (isFirst) buildDeferredInitializerGlobal(),
js.js.statement('$deferredInitializersGlobal.current = #', code)
]);
Hasher hasher = new Hasher();
CodeBuffer buffer = js.createCodeBuffer(
program, _options, _sourceInformationStrategy,
monitor: _dumpInfoTask, listeners: [hasher]);
_task.measureSubtask('emit buffers', () {
output.addBuffer(buffer);
});
// Make a unique hash of the code (before the sourcemaps are added)
// This will be used to retrieve the initializing function from the global
// variable.
String hash = hasher.getHash();
// Now we copy the deferredInitializer.current into its correct hash.
output.add('\n${deferredInitializersGlobal}["$hash"] = '
'${deferredInitializersGlobal}.current\n');
return hash;
}

View file

@ -32,8 +32,8 @@
p3: {units: [3{libB, libC, libD, libE}, 6{libE}], usedBy: [], needs: [p2]}],
b_finalized_fragments=[
f1: [1{libA}],
f2: [5{libD}+4{libC}+2{libB}],
f3: [3{libB, libC, libD, libE}+6{libE}]],
f2: [5{libD}, 4{libC}, 2{libB}],
f3: [3{libB, libC, libD, libE}, 6{libE}]],
c_steps=[
libA=(f1),
libB=(f3, f2),
@ -50,8 +50,8 @@
p4: {units: [3{libB, libC, libD, libE}], usedBy: [], needs: [p2, p3]}],
b_finalized_fragments=[
f1: [1{libA}],
f2: [4{libC}+2{libB}],
f3: [6{libE}+5{libD}],
f2: [4{libC}, 2{libB}],
f3: [6{libE}, 5{libD}],
f4: [3{libB, libC, libD, libE}]],
c_steps=[
libA=(f1),

View file

@ -36,7 +36,7 @@
p2: {units: [2{lib1, lib2}, 3{lib2}], usedBy: [], needs: [p1]}],
b_finalized_fragments=[
f1: [1{lib1}],
f2: [2{lib1, lib2}+3{lib2}]],
f2: [2{lib1, lib2}, 3{lib2}]],
c_steps=[
lib1=(f2, f1),
lib2=(f2)]

View file

@ -14,8 +14,6 @@
f1: [6{libA}],
f2: [1{libB}],
f3: [2{libC}],
f4: [],
f5: [],
f6: [3{libA, libB, libC}]],
c_steps=[
libA=(f6, f1),
@ -29,12 +27,12 @@
p2: {units: [5{libB, libC}, 4{libA, libC}, 2{libC}], usedBy: [p3], needs: [p1]},
p3: {units: [3{libA, libB, libC}], usedBy: [], needs: [p2]}],
b_finalized_fragments=[
f1: [1{libB}+6{libA}],
f2: [5{libB, libC}+4{libA, libC}+2{libC}],
f1: [1{libB}, 6{libA}],
f2: [2{libC}],
f3: [3{libA, libB, libC}]],
c_steps=[
libA=(f3, f2, f1),
libB=(f3, f2, f1),
libA=(f3, f1),
libB=(f3, f1),
libC=(f3, f2)]
*/
@ -48,7 +46,7 @@
f1: [6{libA}],
f2: [1{libB}],
f3: [2{libC}],
f4: [3{libA, libB, libC}+5{libB, libC}+4{libA, libC}]],
f4: [3{libA, libB, libC}]],
c_steps=[
libA=(f4, f1),
libB=(f4, f2),

View file

@ -82,9 +82,9 @@
p3: {units: [24{b2, b3, b4, b5}, 16{b1, b3, b4, b5}, 15{b1, b2, b4, b5}, 13{b1, b2, b3, b5}], usedBy: [p4], needs: [p2]},
p4: {units: [1{b1, b2, b3, b4, b5}], usedBy: [], needs: [p2, p3]}],
b_finalized_fragments=[
f1: [26{b3, b4}+21{b2, b5}+19{b2, b4}+18{b2, b3}+10{b1, b5}+6{b1, b4}+4{b1, b3}+3{b1, b2}+31{b5}+29{b4}+25{b3}+17{b2}+2{b1}],
f2: [9{b1, b2, b3, b4}+28{b3, b4, b5}+23{b2, b4, b5}+22{b2, b3, b5}+20{b2, b3, b4}+14{b1, b4, b5}+12{b1, b3, b5}+8{b1, b3, b4}+11{b1, b2, b5}+7{b1, b2, b4}+5{b1, b2, b3}+30{b4, b5}+27{b3, b5}],
f3: [24{b2, b3, b4, b5}+16{b1, b3, b4, b5}+15{b1, b2, b4, b5}+13{b1, b2, b3, b5}],
f1: [26{b3, b4}, 21{b2, b5}, 19{b2, b4}, 18{b2, b3}, 10{b1, b5}, 6{b1, b4}, 4{b1, b3}, 3{b1, b2}, 31{b5}, 29{b4}, 25{b3}, 17{b2}, 2{b1}],
f2: [9{b1, b2, b3, b4}, 28{b3, b4, b5}, 23{b2, b4, b5}, 22{b2, b3, b5}, 20{b2, b3, b4}, 14{b1, b4, b5}, 12{b1, b3, b5}, 8{b1, b3, b4}, 11{b1, b2, b5}, 7{b1, b2, b4}, 5{b1, b2, b3}, 30{b4, b5}, 27{b3, b5}],
f3: [24{b2, b3, b4, b5}, 16{b1, b3, b4, b5}, 15{b1, b2, b4, b5}, 13{b1, b2, b3, b5}],
f4: [1{b1, b2, b3, b4, b5}]],
c_steps=[
b1=(f4, f3, f2, f1),
@ -100,8 +100,8 @@
p2: {units: [24{b2, b3, b4, b5}, 16{b1, b3, b4, b5}, 15{b1, b2, b4, b5}, 13{b1, b2, b3, b5}, 9{b1, b2, b3, b4}, 28{b3, b4, b5}, 23{b2, b4, b5}, 22{b2, b3, b5}, 20{b2, b3, b4}, 14{b1, b4, b5}, 12{b1, b3, b5}], usedBy: [p3], needs: [p1]},
p3: {units: [1{b1, b2, b3, b4, b5}], usedBy: [], needs: [p2]}],
b_finalized_fragments=[
f1: [8{b1, b3, b4}+11{b1, b2, b5}+7{b1, b2, b4}+5{b1, b2, b3}+30{b4, b5}+27{b3, b5}+26{b3, b4}+21{b2, b5}+19{b2, b4}+18{b2, b3}+10{b1, b5}+6{b1, b4}+4{b1, b3}+3{b1, b2}+31{b5}+29{b4}+25{b3}+17{b2}+2{b1}],
f2: [24{b2, b3, b4, b5}+16{b1, b3, b4, b5}+15{b1, b2, b4, b5}+13{b1, b2, b3, b5}+9{b1, b2, b3, b4}+28{b3, b4, b5}+23{b2, b4, b5}+22{b2, b3, b5}+20{b2, b3, b4}+14{b1, b4, b5}+12{b1, b3, b5}],
f1: [8{b1, b3, b4}, 11{b1, b2, b5}, 7{b1, b2, b4}, 5{b1, b2, b3}, 30{b4, b5}, 27{b3, b5}, 26{b3, b4}, 21{b2, b5}, 19{b2, b4}, 18{b2, b3}, 10{b1, b5}, 6{b1, b4}, 4{b1, b3}, 3{b1, b2}, 31{b5}, 29{b4}, 25{b3}, 17{b2}, 2{b1}],
f2: [24{b2, b3, b4, b5}, 16{b1, b3, b4, b5}, 15{b1, b2, b4, b5}, 13{b1, b2, b3, b5}, 9{b1, b2, b3, b4}, 28{b3, b4, b5}, 23{b2, b4, b5}, 22{b2, b3, b5}, 20{b2, b3, b4}, 14{b1, b4, b5}, 12{b1, b3, b5}],
f3: [1{b1, b2, b3, b4, b5}]],
c_steps=[
b1=(f3, f2, f1),

View file

@ -9,8 +9,7 @@
p3: {units: [2{libb, liba}], usedBy: [], needs: []}],
b_finalized_fragments=[
f1: [3{liba}],
f2: [1{libb}],
f3: []],
f2: [1{libb}]],
c_steps=[
liba=(f1),
libb=(f2)]
@ -23,8 +22,7 @@
p3: {units: [2{libb, liba}], usedBy: [], needs: [p1, p2]}],
b_finalized_fragments=[
f1: [3{liba}],
f2: [1{libb}],
f3: []],
f2: [1{libb}]],
c_steps=[
liba=(f1),
libb=(f2)]

View file

@ -9,8 +9,7 @@
p3: {units: [3{lib1, lib3}], usedBy: [], needs: []}],
b_finalized_fragments=[
f1: [1{lib1}],
f2: [2{lib3}],
f3: []],
f2: [2{lib3}]],
c_steps=[
lib1=(f1),
lib3=(f2)]
@ -23,8 +22,7 @@
p3: {units: [3{lib1, lib3}], usedBy: [], needs: [p1, p2]}],
b_finalized_fragments=[
f1: [1{lib1}],
f2: [2{lib3}],
f3: []],
f2: [2{lib3}]],
c_steps=[
lib1=(f1),
lib3=(f2)]

View file

@ -245,7 +245,9 @@ class PreFragmentsIrComputer extends IrDataExtractor<Features> {
outputUnitStrings.add(outputUnitString(outputUnit));
}
}
supplied.add(outputUnitStrings.join('+'));
if (outputUnitStrings.isNotEmpty) {
supplied.add(outputUnitStrings.join('+'));
}
}
if (supplied.isNotEmpty) {

View file

@ -10,9 +10,9 @@ import "deferred_overlapping_lib2.dart" deferred as lib2;
// will fail because the base class does not exist.
void main() {
lib1.loadLibrary().then((_) {
var a = new lib1.C1();
print(new lib1.C1());
lib2.loadLibrary().then((_) {
var b = new lib2.C2();
print(new lib2.C2());
});
});
}