diff --git a/pkg/dart2wasm/analysis_options.yaml b/pkg/dart2wasm/analysis_options.yaml index c54ed7392f1..bb01371e023 100644 --- a/pkg/dart2wasm/analysis_options.yaml +++ b/pkg/dart2wasm/analysis_options.yaml @@ -10,6 +10,7 @@ analyzer: exclude: - test/ir_tests/*.dart + - test/deferred_loading/partition_tests/*.dart linter: rules: diff --git a/pkg/dart2wasm/lib/deferred_load/partition.dart b/pkg/dart2wasm/lib/deferred_load/partition.dart index ee79fa53231..181d17accd5 100644 --- a/pkg/dart2wasm/lib/deferred_load/partition.dart +++ b/pkg/dart2wasm/lib/deferred_load/partition.dart @@ -255,17 +255,22 @@ class _Algorithm { final namedNodes = ProgramSplitBuilder(); final orderNodes = []; + final filteredUserConstraints = _filterUserConstraints( + userConstraints, + allDeferredImportsIncludingRoot, + ); + // If user provided constraints, initialize from them. final existingNames = {}; - if (userConstraints != null) { - for (final named in userConstraints!.named) { + if (filteredUserConstraints != null) { + for (final named in filteredUserConstraints.named) { if (named is ReferenceNode) { final import = UriAndPrefix(named.uri, named.prefix).toString(); existingNames[import] = named; } namedNodes.namedNodes[named.name] = named; } - for (final ordered in userConstraints!.ordered) { + for (final ordered in filteredUserConstraints.ordered) { orderNodes.add(ordered); } } @@ -298,6 +303,94 @@ class _Algorithm { ).build(allDeferredImportsIncludingRoot); } + ConstraintData? _filterUserConstraints( + ConstraintData? userConstraints, + Set allDeferredImportsIncludingRoot, + ) { + if (userConstraints == null) return null; + + /// The set of all prefixes (even unreachable ones). + final allPrefixNames = { + for (final library in component.libraries) + for (final import in library.dependencies) + if (import.isDeferred) import.uriPrefix, + }; + + /// The set of reachable prefixes. + final validImportPrefixes = allDeferredImportsIncludingRoot + .map((d) => d.uriPrefix) + .toSet(); + + final newNamedNodes = {}; + + // Find [ReferenceNode]s that exist. + for (final NamedNode node in userConstraints.named) { + if (node is ReferenceNode) { + final importPrefix = UriAndPrefix(node.uri, node.prefix).toString(); + if (validImportPrefixes.contains(importPrefix)) { + newNamedNodes[node] = node; + continue; + } + if (!allPrefixNames.contains(importPrefix)) { + throw StateError('The library $importPrefix is not known.'); + } + // The [importPrefix] is in the Kernel AST but unreachable. This can + // happen due to RTA+TFA leaving unreachable code behind. We therefore + // prune this node and simplify or remove depending constraint nodes + // below. + } + } + + // Prune or remove [CombinerNode]s. + for (final NamedNode node in userConstraints.named) { + if (node is CombinerNode) { + final Set remaining = node.nodes + .where(newNamedNodes.containsKey) + .toSet(); + if (remaining.isEmpty) continue; + if (node.type == CombinerType.and && + remaining.length < node.nodes.length) { + continue; + } + if (remaining.length == 1) { + newNamedNodes[node] = remaining.first; + } else { + newNamedNodes[node] = CombinerNode(node.name, node.type, remaining); + } + } + } + + // Filter/prune [OrderNode]s. + final newOrderNodes = []; + for (final node in userConstraints.ordered) { + if (node is RelativeOrderNode) { + final predecessor = newNamedNodes[node.predecessor]; + final successor = newNamedNodes[node.successor]; + if (predecessor != null && successor != null) { + newOrderNodes.add( + RelativeOrderNode(predecessor: predecessor, successor: successor), + ); + } + continue; + } + if (node is FuseNode) { + final newNodes = {}; + for (final node in node.nodes) { + final newNode = newNamedNodes[node]; + if (newNode == null) continue; + newNodes.add(newNode); + } + if (newNodes.length >= 2) { + newOrderNodes.add(FuseNode(newNodes)); + } + continue; + } + throw StateError('Unknown order node $node.'); + } + + return ConstraintData(newNamedNodes.values.toList(), newOrderNodes); + } + void collectDependencies(Set roots) { for (final reference in roots) { ensureReferenceDependencies(reference); diff --git a/pkg/dart2wasm/test/deferred_loading/partition_test.dart b/pkg/dart2wasm/test/deferred_loading/partition_test.dart index 7080f8f879f..89fa1bc202c 100644 --- a/pkg/dart2wasm/test/deferred_loading/partition_test.dart +++ b/pkg/dart2wasm/test/deferred_loading/partition_test.dart @@ -38,15 +38,36 @@ Future runDart2wasmTests() async { .toList(); for (final mainFile in tests) { final mainFilePath = mainFile.path; - final defaultSplitExpectationFile = - '${mainFilePath.substring(0, mainFilePath.length - '.dart'.length)}.default.txt'; + final mainFileBasePath = mainFilePath.substring( + 0, + mainFilePath.length - '.dart'.length, + ); - // Test without constraints. + // Generate a partitioning without constraints under + // `.default.txt`. await testPartitionExpectation( mainFilePath, null, - defaultSplitExpectationFile, + '$mainFileBasePath.default.txt', ); + + // If the `.constraints.json` exists generate a partitioining with + // constraints under `.constraints.txt`. + final jsonConstraintsFile = File('$mainFileBasePath.constraints.json'); + if (jsonConstraintsFile.existsSync()) { + final constraintsJsonString = jsonConstraintsFile + .readAsStringSync() + .replaceAll( + '\${SDK_ROOT}', + Directory.current.absolute.uri.toString(), + ); + final constraints = Parser().read(constraintsJsonString); + await testPartitionExpectation( + mainFilePath, + constraints, + '$mainFileBasePath.constraints.txt', + ); + } } } diff --git a/pkg/dart2wasm/test/deferred_loading/partition_tests/dead_constraint.constraints.json b/pkg/dart2wasm/test/deferred_loading/partition_tests/dead_constraint.constraints.json new file mode 100644 index 00000000000..826d81067eb --- /dev/null +++ b/pkg/dart2wasm/test/deferred_loading/partition_tests/dead_constraint.constraints.json @@ -0,0 +1,90 @@ +[ + { + "type": "reference", + "name": "${SDK_ROOT}pkg/dart2wasm/test/deferred_loading/partition_tests/dead_constraint.dart#OrAlive", + "import": "${SDK_ROOT}pkg/dart2wasm/test/deferred_loading/partition_tests/dead_constraint.dart#OrAlive" + }, + { + "type": "reference", + "name": "${SDK_ROOT}pkg/dart2wasm/test/deferred_loading/partition_tests/dead_constraint.dart#OrAlive_1", + "import": "${SDK_ROOT}pkg/dart2wasm/test/deferred_loading/partition_tests/dead_constraint.dart#OrAlive_1" + }, + { + "type": "reference", + "name": "${SDK_ROOT}pkg/dart2wasm/test/deferred_loading/partition_tests/dead_constraint.dart#OrAlive_2", + "import": "${SDK_ROOT}pkg/dart2wasm/test/deferred_loading/partition_tests/dead_constraint.dart#OrAlive_2" + }, + { + "type": "reference", + "name": "${SDK_ROOT}pkg/dart2wasm/test/deferred_loading/partition_tests/dead_constraint.dart#OrDead", + "import": "${SDK_ROOT}pkg/dart2wasm/test/deferred_loading/partition_tests/dead_constraint.dart#OrDead" + }, + { + "type": "reference", + "name": "${SDK_ROOT}pkg/dart2wasm/test/deferred_loading/partition_tests/dead_constraint.dart#AndAlive", + "import": "${SDK_ROOT}pkg/dart2wasm/test/deferred_loading/partition_tests/dead_constraint.dart#AndAlive" + }, + { + "type": "reference", + "name": "${SDK_ROOT}pkg/dart2wasm/test/deferred_loading/partition_tests/dead_constraint.dart#AndAlive_1", + "import": "${SDK_ROOT}pkg/dart2wasm/test/deferred_loading/partition_tests/dead_constraint.dart#AndAlive_1" + }, + { + "type": "reference", + "name": "${SDK_ROOT}pkg/dart2wasm/test/deferred_loading/partition_tests/dead_constraint.dart#AndAlive_2", + "import": "${SDK_ROOT}pkg/dart2wasm/test/deferred_loading/partition_tests/dead_constraint.dart#AndAlive_2" + }, + { + "type": "reference", + "name": "${SDK_ROOT}pkg/dart2wasm/test/deferred_loading/partition_tests/dead_constraint.dart#AndDead", + "import": "${SDK_ROOT}pkg/dart2wasm/test/deferred_loading/partition_tests/dead_constraint.dart#AndDead" + }, + { + "type": "reference", + "name": "${SDK_ROOT}pkg/dart2wasm/test/deferred_loading/partition_tests/dead_constraint.dart#FuseAlive", + "import": "${SDK_ROOT}pkg/dart2wasm/test/deferred_loading/partition_tests/dead_constraint.dart#FuseAlive" + }, + { + "type": "reference", + "name": "${SDK_ROOT}pkg/dart2wasm/test/deferred_loading/partition_tests/dead_constraint.dart#FuseAlive_1", + "import": "${SDK_ROOT}pkg/dart2wasm/test/deferred_loading/partition_tests/dead_constraint.dart#FuseAlive_1" + }, + { + "type": "reference", + "name": "${SDK_ROOT}pkg/dart2wasm/test/deferred_loading/partition_tests/dead_constraint.dart#FuseAlive_2", + "import": "${SDK_ROOT}pkg/dart2wasm/test/deferred_loading/partition_tests/dead_constraint.dart#FuseAlive_2" + }, + { + "type": "reference", + "name": "${SDK_ROOT}pkg/dart2wasm/test/deferred_loading/partition_tests/dead_constraint.dart#FuseDead", + "import": "${SDK_ROOT}pkg/dart2wasm/test/deferred_loading/partition_tests/dead_constraint.dart#FuseDead" + }, + { + "type": "or", + "name": "or_with_dead", + "nodes": [ + "${SDK_ROOT}pkg/dart2wasm/test/deferred_loading/partition_tests/dead_constraint.dart#OrAlive_1", + "${SDK_ROOT}pkg/dart2wasm/test/deferred_loading/partition_tests/dead_constraint.dart#OrAlive_2", + "${SDK_ROOT}pkg/dart2wasm/test/deferred_loading/partition_tests/dead_constraint.dart#OrDead" + ] + }, + { + "type": "and", + "name": "and_with_dead", + "nodes": [ + "${SDK_ROOT}pkg/dart2wasm/test/deferred_loading/partition_tests/dead_constraint.dart#AndAlive_1", + "${SDK_ROOT}pkg/dart2wasm/test/deferred_loading/partition_tests/dead_constraint.dart#AndAlive_2", + "${SDK_ROOT}pkg/dart2wasm/test/deferred_loading/partition_tests/dead_constraint.dart#AndDead" + ] + }, + { + "type": "fuse", + "nodes": [ + "or_with_dead", + "and_with_dead", + "${SDK_ROOT}pkg/dart2wasm/test/deferred_loading/partition_tests/dead_constraint.dart#FuseAlive_1", + "${SDK_ROOT}pkg/dart2wasm/test/deferred_loading/partition_tests/dead_constraint.dart#FuseAlive_2", + "${SDK_ROOT}pkg/dart2wasm/test/deferred_loading/partition_tests/dead_constraint.dart#FuseDead" + ] + } +] diff --git a/pkg/dart2wasm/test/deferred_loading/partition_tests/dead_constraint.constraints.txt b/pkg/dart2wasm/test/deferred_loading/partition_tests/dead_constraint.constraints.txt new file mode 100644 index 00000000000..805d7277b5e --- /dev/null +++ b/pkg/dart2wasm/test/deferred_loading/partition_tests/dead_constraint.constraints.txt @@ -0,0 +1,75 @@ +Part 0 + ImportSet + - pkg/dart2wasm/test/deferred_loading/partition_tests/dead_constraint.dart prefix: FuseAlive_1 + - pkg/dart2wasm/test/deferred_loading/partition_tests/dead_constraint.dart prefix: FuseAlive_2 + - pkg/dart2wasm/test/deferred_loading/partition_tests/dead_constraint.dart prefix: OrAlive + - pkg/dart2wasm/test/deferred_loading/partition_tests/dead_constraint.dart prefix: OrAlive_1 + - pkg/dart2wasm/test/deferred_loading/partition_tests/dead_constraint.dart prefix: OrAlive_2 + References + - pkg/dart2wasm/test/deferred_loading/partition_tests/dead_constraint.dart::@methods::testOr + Constants + +Part 1 + ImportSet + - pkg/dart2wasm/test/deferred_loading/partition_tests/dead_constraint.dart prefix: AndAlive + - pkg/dart2wasm/test/deferred_loading/partition_tests/dead_constraint.dart prefix: AndAlive_1 + - pkg/dart2wasm/test/deferred_loading/partition_tests/dead_constraint.dart prefix: AndAlive_2 + References + - pkg/dart2wasm/test/deferred_loading/partition_tests/dead_constraint.dart::@methods::testAnd + Constants + +Part 2 + ImportSet + - pkg/dart2wasm/test/deferred_loading/partition_tests/dead_constraint.dart prefix: FuseAlive + - pkg/dart2wasm/test/deferred_loading/partition_tests/dead_constraint.dart prefix: FuseAlive_1 + - pkg/dart2wasm/test/deferred_loading/partition_tests/dead_constraint.dart prefix: FuseAlive_2 + - pkg/dart2wasm/test/deferred_loading/partition_tests/dead_constraint.dart prefix: OrAlive_1 + - pkg/dart2wasm/test/deferred_loading/partition_tests/dead_constraint.dart prefix: OrAlive_2 + References + - pkg/dart2wasm/test/deferred_loading/partition_tests/dead_constraint.dart::@methods::testFuse + Constants + +Part 3 + ImportSet + - pkg/dart2wasm/test/deferred_loading/partition_tests/dead_constraint.dart prefix: OrAlive_1 + References + - pkg/dart2wasm/test/deferred_loading/partition_tests/dead_constraint.dart::@methods::orTest_alive_1 + Constants + - StringConstant("orTest_alive_1") + +Part 4 + ImportSet + - pkg/dart2wasm/test/deferred_loading/partition_tests/dead_constraint.dart prefix: OrAlive_2 + References + - pkg/dart2wasm/test/deferred_loading/partition_tests/dead_constraint.dart::@methods::orTest_alive_2 + Constants + - StringConstant("orTest_alive_2") + +Part 5 + ImportSet + - pkg/dart2wasm/test/deferred_loading/partition_tests/dead_constraint.dart prefix: AndAlive_1 + References + - pkg/dart2wasm/test/deferred_loading/partition_tests/dead_constraint.dart::@methods::andTest_alive_1 + Constants + - StringConstant("andTest_alive_1") + +Part 6 + ImportSet + - pkg/dart2wasm/test/deferred_loading/partition_tests/dead_constraint.dart prefix: AndAlive_2 + References + - pkg/dart2wasm/test/deferred_loading/partition_tests/dead_constraint.dart::@methods::andTest_alive_2 + Constants + - StringConstant("andTest_alive_2") + +Part 7 + ImportSet + - pkg/dart2wasm/test/deferred_loading/partition_tests/dead_constraint.dart prefix: FuseAlive_1 + - pkg/dart2wasm/test/deferred_loading/partition_tests/dead_constraint.dart prefix: FuseAlive_2 + - pkg/dart2wasm/test/deferred_loading/partition_tests/dead_constraint.dart prefix: OrAlive_1 + - pkg/dart2wasm/test/deferred_loading/partition_tests/dead_constraint.dart prefix: OrAlive_2 + References + - pkg/dart2wasm/test/deferred_loading/partition_tests/dead_constraint.dart::@methods::fuseTest_alive_1 + - pkg/dart2wasm/test/deferred_loading/partition_tests/dead_constraint.dart::@methods::fuseTest_alive_2 + Constants + - StringConstant("fuseTest_alive_1") + - StringConstant("fuseTest_alive_2") diff --git a/pkg/dart2wasm/test/deferred_loading/partition_tests/dead_constraint.dart b/pkg/dart2wasm/test/deferred_loading/partition_tests/dead_constraint.dart new file mode 100644 index 00000000000..5948d9f68c4 --- /dev/null +++ b/pkg/dart2wasm/test/deferred_loading/partition_tests/dead_constraint.dart @@ -0,0 +1,102 @@ +// Copyright (c) 2026, 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. + +// Or node +import '' deferred as OrAlive; +import '' deferred as OrAlive_1; +import '' deferred as OrAlive_2; +import '' deferred as OrDead; + +// And node +import '' deferred as AndAlive; +import '' deferred as AndAlive_1; +import '' deferred as AndAlive_2; +import '' deferred as AndDead; + +// Fuse node +import '' deferred as FuseAlive; +import '' deferred as FuseAlive_1; +import '' deferred as FuseAlive_2; +import '' deferred as FuseDead; + +void main() async { + print('main'); + switch (opaqueInt) { + case 1: + await OrAlive.loadLibrary(); + await OrAlive.testOr(); + break; + case 2: + await AndAlive.loadLibrary(); + await AndAlive.testAnd(); + break; + case 3: + await FuseAlive.loadLibrary(); + await FuseAlive.testFuse(); + break; + } +} + +Future testOr() async { + switch (opaqueInt) { + case 1: + await OrAlive_1.loadLibrary(); + OrAlive_1.orTest_alive_1(); + break; + case 2: + await OrAlive_2.loadLibrary(); + OrAlive_2.orTest_alive_2(); + break; + default: + if (alwaysFalse) { + await OrDead.loadLibrary(); + OrDead.orTest_dead(); + } + break; + } +} + +Future testAnd() async { + for (int i = 0; i < 3; ++i) { + if (i == 0) { + await AndAlive_1.loadLibrary(); + AndAlive_1.andTest_alive_1(); + } else if (i == 1) { + await AndAlive_2.loadLibrary(); + AndAlive_2.andTest_alive_2(); + } else { + if (!alwaysTrue) { + await AndDead.loadLibrary(); + AndDead.andTest_dead(); + } + } + } +} + +Future testFuse() async { + await FuseAlive_1.loadLibrary(); + FuseAlive_1.fuseTest_alive_1(); + await FuseAlive_2.loadLibrary(); + FuseAlive_2.fuseTest_alive_2(); + if (alwaysFalse) { + await FuseDead.loadLibrary(); + FuseDead.fuseTest_dead(); + } +} + +void orTest_alive_1() => print('orTest_alive_1'); +void orTest_alive_2() => print('orTest_alive_2'); +void orTest_dead() => print('orTest_dead'); + +void andTest_alive_1() => print('andTest_alive_1'); +void andTest_alive_2() => print('andTest_alive_2'); +void andTest_dead() => print('andTest_dead'); + +void fuseTest_alive_1() => print('fuseTest_alive_1'); +void fuseTest_alive_2() => print('fuseTest_alive_2'); +void fuseTest_dead() => print('fuseTest_dead'); + +int get opaqueInt => int.parse('1'); +bool get alwaysTrue => true; +bool get alwaysFalse => false; diff --git a/pkg/dart2wasm/test/deferred_loading/partition_tests/dead_constraint.default.txt b/pkg/dart2wasm/test/deferred_loading/partition_tests/dead_constraint.default.txt new file mode 100644 index 00000000000..0fc04876eaa --- /dev/null +++ b/pkg/dart2wasm/test/deferred_loading/partition_tests/dead_constraint.default.txt @@ -0,0 +1,74 @@ +Part 0 + ImportSet + - pkg/dart2wasm/test/deferred_loading/partition_tests/dead_constraint.dart prefix: OrAlive + - pkg/dart2wasm/test/deferred_loading/partition_tests/dead_constraint.dart prefix: OrAlive_1 + - pkg/dart2wasm/test/deferred_loading/partition_tests/dead_constraint.dart prefix: OrAlive_2 + References + - pkg/dart2wasm/test/deferred_loading/partition_tests/dead_constraint.dart::@methods::testOr + Constants + +Part 1 + ImportSet + - pkg/dart2wasm/test/deferred_loading/partition_tests/dead_constraint.dart prefix: AndAlive + - pkg/dart2wasm/test/deferred_loading/partition_tests/dead_constraint.dart prefix: AndAlive_1 + - pkg/dart2wasm/test/deferred_loading/partition_tests/dead_constraint.dart prefix: AndAlive_2 + References + - pkg/dart2wasm/test/deferred_loading/partition_tests/dead_constraint.dart::@methods::testAnd + Constants + +Part 2 + ImportSet + - pkg/dart2wasm/test/deferred_loading/partition_tests/dead_constraint.dart prefix: FuseAlive + - pkg/dart2wasm/test/deferred_loading/partition_tests/dead_constraint.dart prefix: FuseAlive_1 + - pkg/dart2wasm/test/deferred_loading/partition_tests/dead_constraint.dart prefix: FuseAlive_2 + References + - pkg/dart2wasm/test/deferred_loading/partition_tests/dead_constraint.dart::@methods::testFuse + Constants + +Part 3 + ImportSet + - pkg/dart2wasm/test/deferred_loading/partition_tests/dead_constraint.dart prefix: OrAlive_1 + References + - pkg/dart2wasm/test/deferred_loading/partition_tests/dead_constraint.dart::@methods::orTest_alive_1 + Constants + - StringConstant("orTest_alive_1") + +Part 4 + ImportSet + - pkg/dart2wasm/test/deferred_loading/partition_tests/dead_constraint.dart prefix: OrAlive_2 + References + - pkg/dart2wasm/test/deferred_loading/partition_tests/dead_constraint.dart::@methods::orTest_alive_2 + Constants + - StringConstant("orTest_alive_2") + +Part 5 + ImportSet + - pkg/dart2wasm/test/deferred_loading/partition_tests/dead_constraint.dart prefix: AndAlive_1 + References + - pkg/dart2wasm/test/deferred_loading/partition_tests/dead_constraint.dart::@methods::andTest_alive_1 + Constants + - StringConstant("andTest_alive_1") + +Part 6 + ImportSet + - pkg/dart2wasm/test/deferred_loading/partition_tests/dead_constraint.dart prefix: AndAlive_2 + References + - pkg/dart2wasm/test/deferred_loading/partition_tests/dead_constraint.dart::@methods::andTest_alive_2 + Constants + - StringConstant("andTest_alive_2") + +Part 7 + ImportSet + - pkg/dart2wasm/test/deferred_loading/partition_tests/dead_constraint.dart prefix: FuseAlive_1 + References + - pkg/dart2wasm/test/deferred_loading/partition_tests/dead_constraint.dart::@methods::fuseTest_alive_1 + Constants + - StringConstant("fuseTest_alive_1") + +Part 8 + ImportSet + - pkg/dart2wasm/test/deferred_loading/partition_tests/dead_constraint.dart prefix: FuseAlive_2 + References + - pkg/dart2wasm/test/deferred_loading/partition_tests/dead_constraint.dart::@methods::fuseTest_alive_2 + Constants + - StringConstant("fuseTest_alive_2")