blob: a116e6fd69106084b537d5a91972ac777e736b7e [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.
import 'package:front_end/src/api_unstable/dart2js.dart' as fe;
import '../common.dart';
import '../constants/values.dart'
show ConstantValue, DeferredGlobalConstantValue;
import '../elements/entities.dart';
import '../serialization/serialization.dart';
import '../options.dart';
/// A "hunk" of the program that will be loaded whenever one of its [imports]
/// are loaded.
///
/// Elements that are only used in one deferred import, is in an OutputUnit with
/// the deferred import as single element in the [imports] set.
///
/// Whenever a deferred Element is shared between several deferred imports it is
/// in an output unit with those imports in the [imports] Set.
///
/// We never create two OutputUnits sharing the same set of [imports].
class OutputUnit implements Comparable<OutputUnit> {
/// `true` if this output unit is for the main output file.
final bool isMainOutput;
/// A unique name representing this [OutputUnit].
final String name;
/// The deferred imports that use the elements in this output unit.
final Set<ImportEntity> imports;
OutputUnit(this.isMainOutput, this.name, this.imports);
@override
int compareTo(OutputUnit other) {
if (identical(this, other)) return 0;
if (isMainOutput && !other.isMainOutput) return -1;
if (!isMainOutput && other.isMainOutput) return 1;
var size = imports.length;
var otherSize = other.imports.length;
if (size != otherSize) return size.compareTo(otherSize);
var thisImports = imports.toList();
var otherImports = other.imports.toList();
for (var i = 0; i < size; i++) {
var cmp = compareImportEntities(thisImports[i], otherImports[i]);
if (cmp != 0) return cmp;
}
// TODO(sigmund): make compare stable. If we hit this point, all imported
// libraries are the same, however [this] and [other] use different deferred
// imports in the program. We can make this stable if we sort based on the
// deferred imports themselves (e.g. their declaration location).
return name.compareTo(other.name);
}
@override
String toString() => "OutputUnit($name, $imports)";
}
int compareImportEntities(ImportEntity a, ImportEntity b) {
if (a == b) {
return 0;
} else {
return a.uri.path.compareTo(b.uri.path);
}
}
/// Interface for updating an [OutputUnitData] object with data for late
/// members, that is, members created on demand during code generation.
class LateOutputUnitDataBuilder {
final OutputUnitData _outputUnitData;
LateOutputUnitDataBuilder(this._outputUnitData);
/// Registers [newEntity] to be emitted in the same output unit as
/// [existingEntity];
void registerColocatedMembers(
MemberEntity existingEntity, MemberEntity newEntity) {
assert(_outputUnitData._memberToUnit[newEntity] == null);
_outputUnitData._memberToUnit[newEntity] =
_outputUnitData.outputUnitForMember(existingEntity);
}
}
/// Results of the deferred loading algorithm.
///
/// Provides information about the output unit associated with entities and
/// constants, as well as other helper methods.
// TODO(sigmund): consider moving here every piece of data used as a result of
// deferred loading (including hunksToLoad, etc).
class OutputUnitData {
/// Tag used for identifying serialized [OutputUnitData] objects in a
/// debugging data stream.
static const String tag = 'output-unit-data';
final bool isProgramSplit;
final OutputUnit mainOutputUnit;
final Map<ClassEntity, OutputUnit> _classToUnit;
final Map<ClassEntity, OutputUnit> _classTypeToUnit;
final Map<MemberEntity, OutputUnit> _memberToUnit;
final Map<Local, OutputUnit> _localFunctionToUnit;
final Map<ConstantValue, OutputUnit> _constantToUnit;
final List<OutputUnit> outputUnits;
final Map<ImportEntity, String> importDeferName;
/// Because the token-stream is forgotten later in the program, we cache a
/// description of each deferred import.
final Map<ImportEntity, ImportDescription> deferredImportDescriptions;
OutputUnitData(
this.isProgramSplit,
this.mainOutputUnit,
this._classToUnit,
this._classTypeToUnit,
this._memberToUnit,
this._localFunctionToUnit,
this._constantToUnit,
this.outputUnits,
this.importDeferName,
this.deferredImportDescriptions);
// Creates J-world data from the K-world data.
factory OutputUnitData.from(
OutputUnitData other,
LibraryEntity convertLibrary(LibraryEntity library),
Map<ClassEntity, OutputUnit> Function(
Map<ClassEntity, OutputUnit>, Map<Local, OutputUnit>)
convertClassMap,
Map<MemberEntity, OutputUnit> Function(
Map<MemberEntity, OutputUnit>, Map<Local, OutputUnit>)
convertMemberMap,
Map<ConstantValue, OutputUnit> Function(Map<ConstantValue, OutputUnit>)
convertConstantMap) {
Map<ClassEntity, OutputUnit> classToUnit =
convertClassMap(other._classToUnit, other._localFunctionToUnit);
Map<ClassEntity, OutputUnit> classTypeToUnit =
convertClassMap(other._classTypeToUnit, other._localFunctionToUnit);
Map<MemberEntity, OutputUnit> memberToUnit =
convertMemberMap(other._memberToUnit, other._localFunctionToUnit);
Map<ConstantValue, OutputUnit> constantToUnit =
convertConstantMap(other._constantToUnit);
Map<ImportEntity, ImportDescription> deferredImportDescriptions = {};
other.deferredImportDescriptions
.forEach((ImportEntity import, ImportDescription description) {
deferredImportDescriptions[import] = ImportDescription.internal(
description.importingUri,
description.prefix,
convertLibrary(description.importingLibrary));
});
return OutputUnitData(
other.isProgramSplit,
other.mainOutputUnit,
classToUnit,
classTypeToUnit,
memberToUnit,
// Local functions only make sense in the K-world model.
const {},
constantToUnit,
other.outputUnits,
other.importDeferName,
deferredImportDescriptions);
}
/// Deserializes an [OutputUnitData] object from [source].
factory OutputUnitData.readFromDataSource(DataSourceReader source) {
source.begin(tag);
bool isProgramSplit = source.readBool();
List<OutputUnit> outputUnits = source.readList(() {
bool isMainOutput = source.readBool();
String name = source.readString();
Set<ImportEntity> importSet = source.readImports().toSet();
return OutputUnit(isMainOutput, name, importSet);
});
OutputUnit mainOutputUnit = outputUnits[source.readInt()];
Map<ClassEntity, OutputUnit> classToUnit = source.readClassMap(() {
return outputUnits[source.readInt()];
});
Map<ClassEntity, OutputUnit> classTypeToUnit = source.readClassMap(() {
return outputUnits[source.readInt()];
});
Map<MemberEntity, OutputUnit> memberToUnit =
source.readMemberMap((MemberEntity member) {
return outputUnits[source.readInt()];
});
Map<ConstantValue, OutputUnit> constantToUnit = source.readConstantMap(() {
return outputUnits[source.readInt()];
});
Map<ImportEntity, String> importDeferName =
source.readImportMap(source.readString);
Map<ImportEntity, ImportDescription> deferredImportDescriptions =
source.readImportMap(() {
String importingUri = source.readString();
String prefix = source.readString();
LibraryEntity importingLibrary = source.readLibrary();
return ImportDescription.internal(importingUri, prefix, importingLibrary);
});
source.end(tag);
return OutputUnitData(
isProgramSplit,
mainOutputUnit,
classToUnit,
classTypeToUnit,
memberToUnit,
// Local functions only make sense in the K-world model.
const {},
constantToUnit,
outputUnits,
importDeferName,
deferredImportDescriptions);
}
/// Serializes this [OutputUnitData] to [sink].
void writeToDataSink(DataSinkWriter sink) {
sink.begin(tag);
sink.writeBool(isProgramSplit);
Map<OutputUnit, int> outputUnitIndices = {};
sink.writeList(outputUnits, (OutputUnit outputUnit) {
outputUnitIndices[outputUnit] = outputUnitIndices.length;
sink.writeBool(outputUnit.isMainOutput);
sink.writeString(outputUnit.name);
sink.writeImports(outputUnit.imports);
});
sink.writeInt(outputUnitIndices[mainOutputUnit]!);
sink.writeClassMap(_classToUnit, (OutputUnit outputUnit) {
sink.writeInt(outputUnitIndices[outputUnit]!);
});
sink.writeClassMap(_classTypeToUnit, (OutputUnit outputUnit) {
sink.writeInt(outputUnitIndices[outputUnit]!);
});
sink.writeMemberMap(_memberToUnit,
(MemberEntity member, OutputUnit outputUnit) {
sink.writeInt(outputUnitIndices[outputUnit]!);
});
sink.writeConstantMap(_constantToUnit, (OutputUnit outputUnit) {
sink.writeInt(outputUnitIndices[outputUnit]!);
});
sink.writeImportMap(importDeferName, sink.writeString);
sink.writeImportMap(deferredImportDescriptions,
(ImportDescription importDescription) {
sink.writeString(importDescription.importingUri);
sink.writeString(importDescription.prefix);
sink.writeLibrary(importDescription.importingLibrary);
});
sink.end(tag);
}
/// Returns the [OutputUnit] where [cls] belongs.
// TODO(johnniwinther): Remove the need for [allowNull]. Dump-info currently
// needs it.
OutputUnit outputUnitForClass(ClassEntity cls, {bool allowNull = false}) {
if (!isProgramSplit) return mainOutputUnit;
OutputUnit? unit = _classToUnit[cls];
assert(allowNull || unit != null, 'No output unit for class $cls');
return unit ?? mainOutputUnit;
}
OutputUnit? outputUnitForClassForTesting(ClassEntity cls) =>
_classToUnit[cls];
/// Returns the [OutputUnit] where [cls]'s type belongs.
// TODO(joshualitt): see above TODO regarding allowNull.
OutputUnit outputUnitForClassType(ClassEntity cls, {bool allowNull = false}) {
if (!isProgramSplit) return mainOutputUnit;
OutputUnit? unit = _classTypeToUnit[cls];
assert(allowNull || unit != null, 'No output unit for type $cls');
return unit ?? mainOutputUnit;
}
OutputUnit? outputUnitForClassTypeForTesting(ClassEntity cls) =>
_classTypeToUnit[cls];
/// Returns the [OutputUnit] where [member] belongs.
OutputUnit outputUnitForMember(MemberEntity member) {
if (!isProgramSplit) return mainOutputUnit;
OutputUnit? unit = _memberToUnit[member];
assert(unit != null, 'No output unit for member $member');
return unit ?? mainOutputUnit;
}
OutputUnit? outputUnitForMemberForTesting(MemberEntity member) =>
_memberToUnit[member];
/// Direct access to the output-unit to constants map used for testing.
Iterable<ConstantValue> get constantsForTesting => _constantToUnit.keys;
/// Returns the [OutputUnit] where [constant] belongs.
OutputUnit outputUnitForConstant(ConstantValue constant) {
if (!isProgramSplit) return mainOutputUnit;
OutputUnit? unit = _constantToUnit[constant];
// TODO(sigmund): enforce unit is not null: it is sometimes null on some
// corner cases on internal apps.
return unit ?? mainOutputUnit;
}
OutputUnit? outputUnitForConstantForTesting(ConstantValue constant) =>
_constantToUnit[constant];
/// Indicates whether [element] is deferred.
bool isDeferredClass(ClassEntity element) {
return outputUnitForClass(element) != mainOutputUnit;
}
/// Returns `true` if element [to] is reachable from element [from] without
/// crossing a deferred import.
///
/// For example, if we have two deferred libraries `A` and `B` that both
/// import a library `C`, then even though elements from `A` and `C` end up in
/// different output units, there is a non-deferred path between `A` and `C`.
bool hasOnlyNonDeferredImportPaths(MemberEntity from, MemberEntity to) {
OutputUnit outputUnitFrom = outputUnitForMember(from);
OutputUnit outputUnitTo = outputUnitForMember(to);
if (outputUnitTo == mainOutputUnit) return true;
if (outputUnitFrom == mainOutputUnit) return false;
return outputUnitTo.imports.containsAll(outputUnitFrom.imports);
}
/// Returns `true` if constant [to] is reachable from element [from] without
/// crossing a deferred import.
///
/// For example, if we have two deferred libraries `A` and `B` that both
/// import a library `C`, then even though elements from `A` and `C` end up in
/// different output units, there is a non-deferred path between `A` and `C`.
bool hasOnlyNonDeferredImportPathsToConstant(
MemberEntity from, ConstantValue to) {
OutputUnit outputUnitFrom = outputUnitForMember(from);
OutputUnit outputUnitTo = outputUnitForConstant(to);
if (outputUnitTo == mainOutputUnit) return true;
if (outputUnitFrom == mainOutputUnit) return false;
return outputUnitTo.imports.containsAll(outputUnitFrom.imports);
}
/// Returns `true` if class [to] is reachable from element [from] without
/// crossing a deferred import.
///
/// For example, if we have two deferred libraries `A` and `B` that both
/// import a library `C`, then even though elements from `A` and `C` end up in
/// different output units, there is a non-deferred path between `A` and `C`.
bool hasOnlyNonDeferredImportPathsToClass(MemberEntity from, ClassEntity to) {
OutputUnit outputUnitFrom = outputUnitForMember(from);
OutputUnit outputUnitTo = outputUnitForClass(to);
if (outputUnitTo == mainOutputUnit) return true;
if (outputUnitFrom == mainOutputUnit) return false;
return outputUnitTo.imports.containsAll(outputUnitFrom.imports);
}
/// Registers that a constant is used in the same deferred output unit as
/// [field].
void registerConstantDeferredUse(DeferredGlobalConstantValue constant) {
if (!isProgramSplit) return;
OutputUnit unit = constant.unit;
assert(
_constantToUnit[constant] == null || _constantToUnit[constant] == unit);
_constantToUnit[constant] = unit;
}
/// Returns the unique name for the given deferred [import].
String getImportDeferName(Spannable node, ImportEntity import) {
String? name = importDeferName[import];
if (name == null) {
throw SpannableAssertionFailure(node, "No deferred name for $import.");
}
return name;
}
/// Returns the names associated with each deferred import in [unit].
Iterable<String> getImportNames(OutputUnit unit) {
return unit.imports.map((i) => importDeferName[i]!);
}
}
class ImportDescription {
/// Relative uri to the importing library.
final String importingUri;
/// The prefix this import is imported as.
final String prefix;
final LibraryEntity importingLibrary;
ImportDescription.internal(
this.importingUri, this.prefix, this.importingLibrary);
ImportDescription(
ImportEntity import, LibraryEntity importingLibrary, Uri mainLibraryUri)
: this.internal(
fe.relativizeUri(
mainLibraryUri, importingLibrary.canonicalUri, false),
import.name!,
importingLibrary);
}
/// Returns the filename for the output-unit named [name].
///
/// The filename is of the form "<main output file>_<name>.part.js".
/// If [addExtension] is false, the ".part.js" suffix is left out.
String deferredPartFileName(CompilerOptions options, String name,
{bool addExtension = true}) {
assert(name != "");
String outPath = options.outputUri != null ? options.outputUri!.path : "out";
String outName = outPath.substring(outPath.lastIndexOf('/') + 1);
String extension = addExtension ? ".part.js" : "";
return "${outName}_$name$extension";
}