diff --git a/pkg/compiler/lib/src/common_elements.dart b/pkg/compiler/lib/src/common_elements.dart index 7b7387ccded..8d84fe789f3 100644 --- a/pkg/compiler/lib/src/common_elements.dart +++ b/pkg/compiler/lib/src/common_elements.dart @@ -595,6 +595,13 @@ abstract class JCommonElements implements CommonElements { /// compilation. bool isNamedListConstructor(String name, ConstructorEntity element); + /// Returns `true` if [element] is the named constructor of `JSArray`, + /// e.g. `JSArray.fixed`. + /// + /// This will not resolve the constructor if it hasn't been seen yet during + /// compilation. + bool isNamedJSArrayConstructor(String name, ConstructorEntity element); + bool isDefaultEqualityImplementation(MemberEntity element); /// Returns `true` if [selector] applies to `JSIndexable.length`. @@ -885,6 +892,14 @@ class CommonElementsImpl bool isNamedListConstructor(String name, ConstructorEntity element) => element.name == name && element.enclosingClass == listClass; + /// Returns `true` if [element] is the [name]d constructor of `JSArray`. + /// + /// This will not resolve the constructor if it hasn't been seen yet during + /// compilation. + @override + bool isNamedJSArrayConstructor(String name, ConstructorEntity element) => + element.name == name && element.enclosingClass == jsArrayClass; + @override DynamicType get dynamicType => _env.dynamicType; diff --git a/pkg/compiler/lib/src/inferrer/builder_kernel.dart b/pkg/compiler/lib/src/inferrer/builder_kernel.dart index afc24a325c7..55d7ab6d24b 100644 --- a/pkg/compiler/lib/src/inferrer/builder_kernel.dart +++ b/pkg/compiler/lib/src/inferrer/builder_kernel.dart @@ -1296,10 +1296,11 @@ class KernelTypeGraphBuilder extends ir.Visitor { // We have something like `List.empty(growable: true)`. TypeInformation baseType = _listBaseType(arguments, defaultGrowable: false); + TypeInformation elementType = _types.nonNullEmpty(); // No elements! return _inferrer.concreteTypes.putIfAbsent( node, () => _types.allocateList( - baseType, node, _analyzedMember, _types.nonNullEmpty(), 0)); + baseType, node, _analyzedMember, elementType, 0)); } if (commonElements.isNamedListConstructor('of', constructor) || commonElements.isNamedListConstructor('from', constructor)) { @@ -1314,6 +1315,44 @@ class KernelTypeGraphBuilder extends ir.Visitor { () => _types.allocateList( baseType, node, _analyzedMember, elementType)); } + + // `JSArray.fixed` corresponds to `new Array(length)`, which is a list + // filled with `null`. + if (commonElements.isNamedJSArrayConstructor('fixed', constructor)) { + int length = _findLength(arguments); + TypeInformation elementType = _types.nullType; + return _inferrer.concreteTypes.putIfAbsent( + node, + () => _types.allocateList(_types.fixedListType, node, _analyzedMember, + elementType, length)); + } + + // `JSArray.allocateFixed` creates an array with 'no elements'. The contract + // is that the caller will assign a value to each member before any element + // is accessed. We can start tracking the element type as 'bottom'. + if (commonElements.isNamedJSArrayConstructor( + 'allocateFixed', constructor)) { + int length = _findLength(arguments); + TypeInformation elementType = _types.nonNullEmpty(); + return _inferrer.concreteTypes.putIfAbsent( + node, + () => _types.allocateList(_types.fixedListType, node, _analyzedMember, + elementType, length)); + } + + // `JSArray.allocateGrowable` creates an array with 'no elements'. The + // contract is that the caller will assign a value to each member before any + // element is accessed. We can start tracking the element type as 'bottom'. + if (commonElements.isNamedJSArrayConstructor( + 'allocateGrowable', constructor)) { + int length = _findLength(arguments); + TypeInformation elementType = _types.nonNullEmpty(); + return _inferrer.concreteTypes.putIfAbsent( + node, + () => _types.allocateList(_types.growableListType, node, + _analyzedMember, elementType, length)); + } + if (_isConstructorOfTypedArraySubclass(constructor)) { // We have something like `Uint32List(len)`. int length = _findLength(arguments); diff --git a/pkg/compiler/lib/src/kernel/dart2js_target.dart b/pkg/compiler/lib/src/kernel/dart2js_target.dart index 07cb160360e..671bfac041f 100644 --- a/pkg/compiler/lib/src/kernel/dart2js_target.dart +++ b/pkg/compiler/lib/src/kernel/dart2js_target.dart @@ -17,6 +17,7 @@ import 'package:kernel/target/changed_structure_notifier.dart'; import 'package:kernel/target/targets.dart'; import 'invocation_mirror_constants.dart'; +import 'transformations/lowering.dart' as lowering show transformLibraries; const Iterable _allowedDartSchemePaths = const [ 'async', @@ -82,6 +83,12 @@ class Dart2jsTarget extends Target { @override List get extraRequiredLibraries => _requiredLibraries[name]; + @override + List get extraIndexedLibraries => const [ + 'dart:_interceptors', + 'dart:_js_helper', + ]; + @override bool mayDefineRestrictedType(Uri uri) => uri.scheme == 'dart' && @@ -118,6 +125,9 @@ class Dart2jsTarget extends Target { diagnosticReporter as DiagnosticReporter) .visitLibrary(library); } + lowering.transformLibraries( + libraries, coreTypes, hierarchy, flags.enableNullSafety); + logger?.call("Lowering transformations performed"); } @override diff --git a/pkg/compiler/lib/src/kernel/transformations/factory_specializer.dart b/pkg/compiler/lib/src/kernel/transformations/factory_specializer.dart new file mode 100644 index 00000000000..ebc00aec60b --- /dev/null +++ b/pkg/compiler/lib/src/kernel/transformations/factory_specializer.dart @@ -0,0 +1,39 @@ +// Copyright (c) 2020, 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:kernel/kernel.dart'; +import 'package:kernel/class_hierarchy.dart' show ClassHierarchy; +import 'package:kernel/core_types.dart'; +import 'list_factory_specializer.dart'; + +typedef SpecializerTransformer = TreeNode Function( + StaticInvocation node, Member contextMember); + +abstract class BaseSpecializer { + // Populated in constructors of subclasses. + final Map transformers = {}; +} + +class FactorySpecializer extends BaseSpecializer { + final ListFactorySpecializer _listFactorySpecializer; + + FactorySpecializer(CoreTypes coreTypes, ClassHierarchy hierarchy) + : _listFactorySpecializer = ListFactorySpecializer(coreTypes, hierarchy) { + transformers.addAll(_listFactorySpecializer.transformers); + } + + TreeNode transformStaticInvocation( + StaticInvocation invocation, Member contextMember) { + final target = invocation.target; + if (target == null) { + return invocation; + } + + final transformer = transformers[target]; + if (transformer != null) { + return transformer(invocation, contextMember); + } + return invocation; + } +} diff --git a/pkg/compiler/lib/src/kernel/transformations/list_factory_specializer.dart b/pkg/compiler/lib/src/kernel/transformations/list_factory_specializer.dart new file mode 100644 index 00000000000..e61d1ddc054 --- /dev/null +++ b/pkg/compiler/lib/src/kernel/transformations/list_factory_specializer.dart @@ -0,0 +1,343 @@ +// Copyright (c) 2020, 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:kernel/ast.dart'; +import 'package:kernel/class_hierarchy.dart' show ClassHierarchy; +import 'package:kernel/clone.dart' show CloneVisitorNotMembers; +import 'package:kernel/core_types.dart' show CoreTypes; +import 'factory_specializer.dart'; + +/// Replaces invocation of List factory constructors. +/// +/// Expands `List.generate` to a loop when the function argument is a function +/// expression (immediate closure). +/// +class ListFactorySpecializer extends BaseSpecializer { + final CoreTypes coreTypes; + final ClassHierarchy hierarchy; + + final Class _intClass; + final Class _jsArrayClass; + final Procedure _listGenerateFactory; + final Procedure _arrayAllocateFixedFactory; + final Procedure _arrayAllocateGrowableFactory; + + ListFactorySpecializer(this.coreTypes, this.hierarchy) + : _listGenerateFactory = + coreTypes.index.getMember('dart:core', 'List', 'generate'), + _arrayAllocateFixedFactory = coreTypes.index + .getMember('dart:_interceptors', 'JSArray', 'allocateFixed'), + _arrayAllocateGrowableFactory = coreTypes.index + .getMember('dart:_interceptors', 'JSArray', 'allocateGrowable'), + _jsArrayClass = + coreTypes.index.getClass('dart:_interceptors', 'JSArray'), + _intClass = coreTypes.index.getClass('dart:core', 'int') { + assert(_listGenerateFactory.isFactory); + assert(_arrayAllocateGrowableFactory.isFactory); + assert(_arrayAllocateFixedFactory.isFactory); + transformers.addAll({ + _listGenerateFactory: transformListGenerateFactory, + }); + } + + Member _intPlus; + Member get intPlus => + _intPlus ??= hierarchy.getInterfaceMember(_intClass, Name('+')); + + Member _intLess; + Member get intLess => + _intLess ??= hierarchy.getInterfaceMember(_intClass, Name('<')); + + Member _jsArrayIndexSet; + Member get jsArrayIndexSet => _jsArrayIndexSet ??= + hierarchy.getInterfaceMember(_jsArrayClass, Name('[]=')); + + /// Replace calls to `List.generate(length, (i) => e)` with an expansion + /// + /// BlockExpression + /// Block + /// var _length = ; + /// var _list = List.allocate(_length); + /// for (var _i = 0; _i < _length; _i++) { + /// _list[_i] = e; + /// } + /// => _list + /// + /// Declines to expand if: + /// - the function argument is not a simple closure, + /// - the `growable:` argument cannot be determined. + TreeNode transformListGenerateFactory( + StaticInvocation node, Member contextMember) { + final args = node.arguments; + assert(args.positional.length == 2); + final length = args.positional[0]; + final generator = args.positional[1]; + final bool growable = + _getConstantNamedOptionalArgument(args, 'growable', true); + if (growable == null) return node; + + if (generator is! FunctionExpression) return node; + + if (!ListGenerateLoopBodyInliner.suitableFunctionExpression(generator)) { + return node; + } + + final intType = contextMember.isNonNullableByDefault + ? coreTypes.intLegacyRawType + : coreTypes.intNonNullableRawType; + + // If the length is a constant, use the constant directly so that the + // inferrer can see the constant length. + int /*?*/ lengthConstant = _getLengthArgument(args); + VariableDeclaration lengthVariable; + + Expression getLength() { + if (lengthConstant != null) return IntLiteral(lengthConstant); + lengthVariable ??= VariableDeclaration('_length', + initializer: length, isFinal: true, type: intType) + ..fileOffset = node.fileOffset; + return VariableGet(lengthVariable)..fileOffset = node.fileOffset; + } + + TreeNode allocation = StaticInvocation( + growable ? _arrayAllocateGrowableFactory : _arrayAllocateFixedFactory, + Arguments( + [getLength()], + types: args.types, + )) + ..fileOffset = node.fileOffset; + + final listVariable = VariableDeclaration( + _listNameFromContext(node), + initializer: allocation, + isFinal: true, + type: InterfaceType( + _jsArrayClass, Nullability.nonNullable, [...args.types]), + )..fileOffset = node.fileOffset; + + final indexVariable = VariableDeclaration( + _indexNameFromContext(generator), + initializer: IntLiteral(0), + type: intType, + )..fileOffset = node.fileOffset; + indexVariable.fileOffset = (generator as FunctionExpression) + .function + .positionalParameters + .first + .fileOffset; + + final loop = ForStatement( + // initializers: _i = 0 + [indexVariable], + // condition: _i < _length + MethodInvocation( + VariableGet(indexVariable)..fileOffset = node.fileOffset, + Name('<'), + Arguments([getLength()]), + )..interfaceTarget = intLess, + // updates: _i++ + [ + VariableSet( + indexVariable, + MethodInvocation( + VariableGet(indexVariable)..fileOffset = node.fileOffset, + Name('+'), + Arguments([IntLiteral(1)]), + )..interfaceTarget = intPlus, + )..fileOffset = node.fileOffset, + ], + // body, e.g. _list[_i] = expression; + _loopBody(node.fileOffset, listVariable, indexVariable, generator), + )..fileOffset = node.fileOffset; + + return BlockExpression( + Block([ + if (lengthVariable != null) lengthVariable, + listVariable, + loop, + ]), + VariableGet(listVariable)..fileOffset = node.fileOffset, + ); + } + + Statement _loopBody( + int constructorFileOffset, + VariableDeclaration listVariable, + VariableDeclaration indexVariable, + FunctionExpression generator) { + final inliner = ListGenerateLoopBodyInliner( + this, constructorFileOffset, listVariable, generator.function); + inliner.bind(indexVariable); + return inliner.run(); + } + + /// Returns constant value of the first argument in [args], or null if it is + /// not a constant. + int /*?*/ _getLengthArgument(Arguments args) { + if (args.positional.length < 1) return null; + final value = args.positional.first; + if (value is IntLiteral) { + return value.value; + } else if (value is ConstantExpression) { + final constant = value.constant; + if (constant is IntConstant) { + return constant.value; + } + } + return null; + } + + /// Returns constant value of the only named optional argument in [args], or + /// null if it is not a bool constant. Returns [defaultValue] if optional + /// argument is not passed. Argument is asserted to have the given [name]. + bool /*?*/ _getConstantNamedOptionalArgument( + Arguments args, String name, bool defaultValue) { + if (args.named.isEmpty) { + return defaultValue; + } + final namedArg = args.named.single; + assert(namedArg.name == name); + final value = namedArg.value; + if (value is BoolLiteral) { + return value.value; + } else if (value is ConstantExpression) { + final constant = value.constant; + if (constant is BoolConstant) { + return constant.value; + } + } + return null; + } + + /// Choose a name for the `_list` temporary. If the `List.generate` expression + /// is an initializer for a variable, use that name so that dart2js can try to + /// use one JavaScript variable with the source name for 'both' variables. + String _listNameFromContext(Expression node) { + TreeNode parent = node.parent; + if (parent is VariableDeclaration) return parent.name; + return '_list'; + } + + String _indexNameFromContext(FunctionExpression generator) { + final function = generator.function; + String /*?*/ candidate = function.positionalParameters.first.name; + if (candidate == null || candidate == '' || candidate == '_') return '_i'; + return candidate; + } +} + +/// Inliner for function expressions of `List.generate` calls. +class ListGenerateLoopBodyInliner extends CloneVisitorNotMembers { + final ListFactorySpecializer listFactorySpecializer; + + /// Offset for the constructor call, used for all nodes that carry the value of the list. + final int constructorFileOffset; + final VariableDeclaration listVariable; + final FunctionNode function; + VariableDeclaration argument; + VariableDeclaration parameter; + int functionNestingLevel = 0; + + ListGenerateLoopBodyInliner(this.listFactorySpecializer, + this.constructorFileOffset, this.listVariable, this.function); + + static bool suitableFunctionExpression(FunctionExpression node) { + final function = node.function; + // These conditions should be satisfied by language rules. + if (function.typeParameters.isNotEmpty) return false; + if (function.requiredParameterCount != 1) return false; + if (function.positionalParameters.length != 1) return false; + if (function.namedParameters.isNotEmpty) return false; + + final body = function.body; + // For now, only arrow functions. + if (body is ReturnStatement) return true; + + return false; + } + + void bind(VariableDeclaration argument) { + // The [argument] is the loop index variable. In the general case this needs + // to be copied to a variable for the closure parameter as that is a + // separate location that may be mutated. In the usual case the closure + // parameter is not modified. We use the same name for the parameter and + // argument to help dart2js allocate both locations to the same JavaScript + // variable. The argument is usually named after the closure parameter. + final closureParameter = function.positionalParameters.single; + parameter = VariableDeclaration(argument.name, + initializer: VariableGet(argument)..fileOffset = argument.fileOffset, + type: closureParameter.type) + ..fileOffset = closureParameter.fileOffset; + this.argument = argument; + variables[closureParameter] = parameter; + } + + Statement run() { + final body = cloneInContext(function.body); + return Block([parameter, body]); + } + + @override + visitReturnStatement(ReturnStatement node) { + // Do the default for return statements in nested functions. + if (functionNestingLevel > 0) return super.visitReturnStatement(node); + + // We don't use a variable for the returned value. In the simple case it is + // not necessary, and it is not clear that the rules for definite assignment + // are not a perfect match for the locations of return statements. Instead + // we expand + // + // return expression; + // + // to + // + // list[index] = expression; + // + // TODO(sra): Currently this inliner accepts only arrow functions (a single + // return). If a wider variety is accepted, we might need to break after the + // assignment to 'exit' the inlined code. + + final expression = node.expression; + final value = expression == null ? NullLiteral() : clone(expression); + // TODO(sra): Indicate that this indexed setter is safe. + return ExpressionStatement( + MethodInvocation( + VariableGet(listVariable)..fileOffset = constructorFileOffset, + Name('[]='), + Arguments([ + VariableGet(argument)..fileOffset = node.fileOffset, + value, + ]), + ) + ..interfaceTarget = listFactorySpecializer.jsArrayIndexSet + ..isInvariant = true + ..isBoundsSafe = true + ..fileOffset = constructorFileOffset, + ); + } + + /// Nested functions. + @override + visitFunctionNode(FunctionNode node) { + functionNestingLevel++; + final cloned = super.visitFunctionNode(node); + functionNestingLevel--; + return cloned; + } + + @override + visitVariableGet(VariableGet node) { + // Unmapped variables are from an outer scope. + var mapped = variables[node.variable] ?? node.variable; + return VariableGet(mapped, visitOptionalType(node.promotedType)) + ..fileOffset = node.fileOffset; + } + + @override + visitVariableSet(VariableSet node) { + // Unmapped variables are from an outer scope. + var mapped = variables[node.variable] ?? node.variable; + return VariableSet(mapped, clone(node.value))..fileOffset = node.fileOffset; + } +} diff --git a/pkg/compiler/lib/src/kernel/transformations/lowering.dart b/pkg/compiler/lib/src/kernel/transformations/lowering.dart new file mode 100644 index 00000000000..72c4f153349 --- /dev/null +++ b/pkg/compiler/lib/src/kernel/transformations/lowering.dart @@ -0,0 +1,56 @@ +// Copyright (c) 2020, 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:kernel/ast.dart'; +import 'package:kernel/class_hierarchy.dart' show ClassHierarchy; +import 'package:kernel/core_types.dart' show CoreTypes; +import 'package:kernel/type_environment.dart' + show StaticTypeContext, TypeEnvironment; +import 'factory_specializer.dart'; + +/// dart2js-specific lowering transformations and optimizations combined into a +/// single transformation pass. +/// +/// Each transformation is applied locally to AST nodes of certain types after +/// transforming children nodes. +void transformLibraries(List libraries, CoreTypes coreTypes, + ClassHierarchy hierarchy, bool nullSafety) { + final transformer = _Lowering(coreTypes, hierarchy, nullSafety); + libraries.forEach(transformer.visitLibrary); +} + +class _Lowering extends Transformer { + final TypeEnvironment env; + final bool nullSafety; + final FactorySpecializer factorySpecializer; + + Member _currentMember; + StaticTypeContext _cachedStaticTypeContext; + + _Lowering(CoreTypes coreTypes, ClassHierarchy hierarchy, this.nullSafety) + : env = TypeEnvironment(coreTypes, hierarchy), + factorySpecializer = FactorySpecializer(coreTypes, hierarchy); + + // ignore: unused_element + StaticTypeContext get _staticTypeContext => + _cachedStaticTypeContext ??= StaticTypeContext(_currentMember, env); + + @override + defaultMember(Member node) { + _currentMember = node; + _cachedStaticTypeContext = null; + + final result = super.defaultMember(node); + + _currentMember = null; + _cachedStaticTypeContext = null; + return result; + } + + @override + visitStaticInvocation(StaticInvocation node) { + node.transformChildren(this); + return factorySpecializer.transformStaticInvocation(node, _currentMember); + } +} diff --git a/pkg/compiler/lib/src/ssa/builder_kernel.dart b/pkg/compiler/lib/src/ssa/builder_kernel.dart index cb44da27bc4..1032865c4b3 100644 --- a/pkg/compiler/lib/src/ssa/builder_kernel.dart +++ b/pkg/compiler/lib/src/ssa/builder_kernel.dart @@ -3882,7 +3882,10 @@ class KernelSsaGraphBuilder extends ir.Visitor { if (_abstractValueDomain.isFixedArray(resultType).isDefinitelyTrue) { // These constructors all take a length as the first argument. if (_commonElements.isNamedListConstructor('filled', function) || - _commonElements.isNamedListConstructor('generate', function)) { + _commonElements.isNamedListConstructor('generate', function) || + _commonElements.isNamedJSArrayConstructor('fixed', function) || + _commonElements.isNamedJSArrayConstructor( + 'allocateFixed', function)) { isFixedList = true; } } diff --git a/pkg/compiler/lib/src/ssa/invoke_dynamic_specializers.dart b/pkg/compiler/lib/src/ssa/invoke_dynamic_specializers.dart index 49e534a090c..92745f66dc6 100644 --- a/pkg/compiler/lib/src/ssa/invoke_dynamic_specializers.dart +++ b/pkg/compiler/lib/src/ssa/invoke_dynamic_specializers.dart @@ -192,6 +192,9 @@ class IndexAssignSpecializer extends InvokeDynamicSpecializer { HInstruction value, JCommonElements commonElements, JClosedWorld closedWorld) { + if (instruction.isInvariant) { + return true; + } // Handle typed arrays by recognizing the exact implementation of `[]=` and // checking if [value] has the appropriate type. if (instruction.element != null) { diff --git a/pkg/compiler/pubspec.yaml b/pkg/compiler/pubspec.yaml index 58607bb5a2b..6a7ec657c66 100644 --- a/pkg/compiler/pubspec.yaml +++ b/pkg/compiler/pubspec.yaml @@ -34,6 +34,7 @@ dev_dependencies: package_config: any path: any source_maps: any + test: any # Unpublished packages that can be used via path dependency async_helper: path: ../async_helper diff --git a/pkg/compiler/test/inference/data/list2.dart b/pkg/compiler/test/inference/data/list2.dart index 16b21d88f70..81d7d8f4cc3 100644 --- a/pkg/compiler/test/inference/data/list2.dart +++ b/pkg/compiler/test/inference/data/list2.dart @@ -23,6 +23,7 @@ main() { listGenerateGrowable, listGenerateFixed, listGenerateEither, + listGenerateBigClosure, listOfDefault, listOfGrowable, listOfFixed, @@ -72,24 +73,37 @@ get listFilledEither => List.filled(5, 'e', growable: boolFlag); // -------- List.generate -------- /*member: listGenerateDefault:Container([exact=JSExtendableArray], element: [exact=JSString], length: 8)*/ -get listGenerateDefault => List.generate( - 8, /*[exact=JSString]*/ (/*[subclass=JSPositiveInt]*/ i) => 'x$i'); +get listGenerateDefault => List + . /*update: Container([exact=JSExtendableArray], element: [exact=JSString], length: 8)*/ generate( + 8, (i) => 'x$i'); /*member: listGenerateGrowable:Container([exact=JSExtendableArray], element: [exact=JSString], length: 8)*/ -get listGenerateGrowable => List.generate( - 8, /*[exact=JSString]*/ (/*[subclass=JSPositiveInt]*/ i) => 'g$i', - growable: true); +get listGenerateGrowable => List + . /*update: Container([exact=JSExtendableArray], element: [exact=JSString], length: 8)*/ generate( + 8, (i) => 'g$i', + growable: true); /*member: listGenerateFixed:Container([exact=JSFixedArray], element: [exact=JSString], length: 8)*/ -get listGenerateFixed => List.generate( - 8, /*[exact=JSString]*/ (/*[subclass=JSPositiveInt]*/ i) => 'f$i', - growable: false); +get listGenerateFixed => List + . /*update: Container([exact=JSFixedArray], element: [exact=JSString], length: 8)*/ generate( + 8, (i) => 'f$i', + growable: false); /*member: listGenerateEither:Container([subclass=JSMutableArray], element: [exact=JSString], length: 8)*/ get listGenerateEither => List.generate( 8, /*[exact=JSString]*/ (/*[subclass=JSPositiveInt]*/ i) => 'e$i', growable: boolFlag); +/*member: listGenerateBigClosure:Container([exact=JSExtendableArray], element: [exact=JSString], length: 8)*/ +get listGenerateBigClosure => List.generate( + 8, + /*[exact=JSString]*/ (/*[subclass=JSPositiveInt]*/ i) { + if (i /*invoke: [subclass=JSPositiveInt]*/ == 1) return 'one'; + if (i /*invoke: [subclass=JSPositiveInt]*/ == 2) return 'two'; + return '$i'; + }, + ); + // -------- List.of -------- /*member: listOfDefault:Container([exact=JSExtendableArray], element: [null|subclass=Object], length: null)*/ diff --git a/pkg/compiler/test/kernel/common_test_utils.dart b/pkg/compiler/test/kernel/common_test_utils.dart new file mode 100644 index 00000000000..ed7c9e52109 --- /dev/null +++ b/pkg/compiler/test/kernel/common_test_utils.dart @@ -0,0 +1,135 @@ +// Copyright (c) 2020, 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 'dart:async'; +import 'dart:io'; + +import 'package:front_end/src/api_unstable/dart2js.dart' + show + CompilerOptions, + DiagnosticMessage, + computePlatformBinariesLocation, + kernelForProgram, + parseExperimentalArguments, + parseExperimentalFlags; +import 'package:kernel/ast.dart'; +import 'package:kernel/text/ast_to_text.dart' show Printer; +import 'package:kernel/target/targets.dart'; +import 'package:test/test.dart'; + +import 'package:compiler/src/kernel/dart2js_target.dart' show Dart2jsTarget; + +/// Environment define to update expectation files on failures. +const kUpdateExpectations = 'updateExpectations'; + +/// Environment define to dump actual results alongside expectations. +const kDumpActualResult = 'dump.actual.result'; + +class TestingDart2jsTarget extends Dart2jsTarget { + TestingDart2jsTarget(TargetFlags flags) : super('dart2js', flags); +} + +Future compileTestCaseToKernelProgram(Uri sourceUri, + {Target target, + bool enableSuperMixins = false, + List experimentalFlags, + Map environmentDefines}) async { + final platformKernel = + computePlatformBinariesLocation().resolve('dart2js_platform.dill'); + target ??= TestingDart2jsTarget(TargetFlags()); + environmentDefines ??= {}; + final options = CompilerOptions() + ..target = target + ..additionalDills = [platformKernel] + ..environmentDefines = environmentDefines + ..explicitExperimentalFlags = + parseExperimentalFlags(parseExperimentalArguments(experimentalFlags), + onError: (String message) { + throw message; + }) + ..onDiagnostic = (DiagnosticMessage message) { + fail("Compilation error: ${message.plainTextFormatted.join('\n')}"); + }; + + final Component component = + (await kernelForProgram(sourceUri, options)).component; + + // Make sure the library name is the same and does not depend on the order + // of test cases. + component.mainMethod.enclosingLibrary.name = '#lib'; + + return component; +} + +String kernelLibraryToString(Library library) { + final buffer = StringBuffer(); + Printer(buffer, showMetadata: true).writeLibraryFile(library); + return buffer + .toString() + .replaceAll(library.importUri.toString(), library.name); +} + +String kernelComponentToString(Component component) { + final buffer = StringBuffer(); + Printer(buffer, showMetadata: true).writeComponentFile(component); + final mainLibrary = component.mainMethod.enclosingLibrary; + return buffer + .toString() + .replaceAll(mainLibrary.importUri.toString(), mainLibrary.name); +} + +class Difference { + final int line; + final String actual; + final String expected; + + Difference(this.line, this.actual, this.expected); +} + +Difference findFirstDifference(String actual, String expected) { + final actualLines = actual.split('\n'); + final expectedLines = expected.split('\n'); + int i = 0; + for (; i < actualLines.length && i < expectedLines.length; ++i) { + if (actualLines[i] != expectedLines[i]) { + return Difference(i + 1, actualLines[i], expectedLines[i]); + } + } + return Difference(i + 1, i < actualLines.length ? actualLines[i] : '', + i < expectedLines.length ? expectedLines[i] : ''); +} + +void compareResultWithExpectationsFile(Uri source, String actual) { + final expectFile = File(source.toFilePath() + '.expect'); + final expected = expectFile.existsSync() ? expectFile.readAsStringSync() : ''; + + if (actual != expected) { + if (bool.fromEnvironment(kUpdateExpectations)) { + expectFile.writeAsStringSync(actual); + print(" Updated $expectFile"); + } else { + if (bool.fromEnvironment(kDumpActualResult)) { + File(source.toFilePath() + '.actual').writeAsStringSync(actual); + } + Difference diff = findFirstDifference(actual, expected); + fail(""" + +Result is different for the test case $source + +The first difference is at line ${diff.line}. +Actual: ${diff.actual} +Expected: ${diff.expected} + +This failure can be caused by changes in the front-end if it starts generating +different kernel AST for the same Dart programs. + +In order to re-generate expectations run tests with -D$kUpdateExpectations=true VM option: + + sdk/bin/dart -D$kUpdateExpectations=true pkg/compiler/test/kernel/goldens_test.dart + +In order to dump actual results into .actual files run tests with -D$kDumpActualResult=true VM option. +"""); + } + } +} diff --git a/pkg/compiler/test/kernel/data/list_generate_1.dart b/pkg/compiler/test/kernel/data/list_generate_1.dart new file mode 100644 index 00000000000..3365cf5d1cf --- /dev/null +++ b/pkg/compiler/test/kernel/data/list_generate_1.dart @@ -0,0 +1,15 @@ +// Copyright (c) 2020, 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. + +var list1 = List.generate(10, (i) => i); +var list2 = List.generate(10, (i) => i, growable: true); +var list3 = List.generate(10, (i) => i, growable: false); +var list4 = List.generate(10, (i) => i, growable: someGrowable); + +bool someGrowable = true; + +void main() { + someGrowable = !someGrowable; + print([list1, list2, list3, list4]); +} diff --git a/pkg/compiler/test/kernel/data/list_generate_1.dart.expect b/pkg/compiler/test/kernel/data/list_generate_1.dart.expect new file mode 100644 index 00000000000..be2484b35f0 --- /dev/null +++ b/pkg/compiler/test/kernel/data/list_generate_1.dart.expect @@ -0,0 +1,32 @@ +library #lib; +import self as self; +import "dart:core" as core; +import "dart:_interceptors" as _in; + +static field core::List* list1 = block { + final _in::JSArray _list = _in::JSArray::allocateGrowable(10); + for (core::int i = 0; i.{core::num::<}(10); i = i.{core::num::+}(1)) { + core::int* i = i; + _list.{_in::JSArray::[]=}{Invariant,BoundsSafe}(i, i); + } +} =>_list; +static field core::List* list2 = block { + final _in::JSArray _list = _in::JSArray::allocateGrowable(10); + for (core::int i = 0; i.{core::num::<}(10); i = i.{core::num::+}(1)) { + core::int* i = i; + _list.{_in::JSArray::[]=}{Invariant,BoundsSafe}(i, i); + } +} =>_list; +static field core::List* list3 = block { + final _in::JSArray _list = _in::JSArray::allocateFixed(10); + for (core::int i = 0; i.{core::num::<}(10); i = i.{core::num::+}(1)) { + core::int* i = i; + _list.{_in::JSArray::[]=}{Invariant,BoundsSafe}(i, i); + } +} =>_list; +static field core::List* list4 = core::List::generate(10, (core::int* i) → core::int* => i, growable: self::someGrowable); +static field core::bool* someGrowable = true; +static method main() → void { + self::someGrowable = !self::someGrowable; + core::print(*>[self::list1, self::list2, self::list3, self::list4]); +} diff --git a/pkg/compiler/test/kernel/data/list_generate_2.dart b/pkg/compiler/test/kernel/data/list_generate_2.dart new file mode 100644 index 00000000000..af9196dc7d7 --- /dev/null +++ b/pkg/compiler/test/kernel/data/list_generate_2.dart @@ -0,0 +1,8 @@ +// Copyright (c) 2020, 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. + +void main() { + // 'nested' List.generate calls. + print(List.generate(10, (i) => List.generate(i, (i) => i + 1))); +} diff --git a/pkg/compiler/test/kernel/data/list_generate_2.dart.expect b/pkg/compiler/test/kernel/data/list_generate_2.dart.expect new file mode 100644 index 00000000000..e2a2196cfff --- /dev/null +++ b/pkg/compiler/test/kernel/data/list_generate_2.dart.expect @@ -0,0 +1,21 @@ +library #lib; +import self as self; +import "dart:core" as core; +import "dart:_interceptors" as _in; + +static method main() → void { + core::print( block { + final _in::JSArray*> _list = _in::JSArray::allocateGrowable*>(10); + for (core::int i = 0; i.{core::num::<}(10); i = i.{core::num::+}(1)) { + core::int* i = i; + _list.{_in::JSArray::[]=}{Invariant,BoundsSafe}(i, block { + final core::int _length = i; + final _in::JSArray _list = _in::JSArray::allocateGrowable(_length); + for (core::int i = 0; i.{core::num::<}(_length); i = i.{core::num::+}(1)) { + core::int* i = i; + _list.{_in::JSArray::[]=}{Invariant,BoundsSafe}(i, i.{core::num::+}(1)); + } + } =>_list); + } + } =>_list); +} diff --git a/pkg/compiler/test/kernel/goldens_test.dart b/pkg/compiler/test/kernel/goldens_test.dart new file mode 100644 index 00000000000..a9457291024 --- /dev/null +++ b/pkg/compiler/test/kernel/goldens_test.dart @@ -0,0 +1,46 @@ +// Copyright (c) 2020, 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 'dart:io'; + +import 'package:kernel/target/targets.dart'; +import 'package:kernel/ast.dart'; +import 'package:kernel/kernel.dart'; +import 'package:test/test.dart'; + +import 'common_test_utils.dart'; + +final String testRootDir = Platform.script.resolve('.').toFilePath(); + +runTestCase( + Uri source, List experimentalFlags, bool enableNullSafety) async { + final target = + TestingDart2jsTarget(TargetFlags(enableNullSafety: enableNullSafety)); + Component component = await compileTestCaseToKernelProgram(source, + target: target, experimentalFlags: experimentalFlags); + + String actual = kernelLibraryToString(component.mainMethod.enclosingLibrary); + + compareResultWithExpectationsFile(source, actual); +} + +main() { + group('goldens', () { + final testCasesDir = new Directory(testRootDir + '/data'); + + for (var entry + in testCasesDir.listSync(recursive: true, followLinks: false)) { + final path = entry.path; + if (path.endsWith('.dart')) { + final bool enableNullSafety = path.endsWith('_nnbd_strong.dart'); + final bool enableNNBD = enableNullSafety || path.endsWith('_nnbd.dart'); + final List experimentalFlags = [ + if (enableNNBD) 'non-nullable', + ]; + test(path, + () => runTestCase(entry.uri, experimentalFlags, enableNullSafety)); + } + } + }, timeout: Timeout.none); +} diff --git a/pkg/dev_compiler/tool/dart2js_nnbd_sdk_error_golden.txt b/pkg/dev_compiler/tool/dart2js_nnbd_sdk_error_golden.txt index bf806189d34..d85029af0b0 100644 --- a/pkg/dev_compiler/tool/dart2js_nnbd_sdk_error_golden.txt +++ b/pkg/dev_compiler/tool/dart2js_nnbd_sdk_error_golden.txt @@ -1,12 +1,12 @@ -ERROR|COMPILE_TIME_ERROR|INCONSISTENT_INHERITANCE|lib/_internal/js_runtime/lib/interceptors.dart|1659|7|5|Superinterfaces don't have a valid override for '&': JSNumber.& (num Function(num)), int.& (int Function(int)). -ERROR|COMPILE_TIME_ERROR|INCONSISTENT_INHERITANCE|lib/_internal/js_runtime/lib/interceptors.dart|1659|7|5|Superinterfaces don't have a valid override for '<<': JSNumber.<< (num Function(num)), int.<< (int Function(int)). -ERROR|COMPILE_TIME_ERROR|INCONSISTENT_INHERITANCE|lib/_internal/js_runtime/lib/interceptors.dart|1659|7|5|Superinterfaces don't have a valid override for '>>': JSNumber.>> (num Function(num)), int.>> (int Function(int)). -ERROR|COMPILE_TIME_ERROR|INCONSISTENT_INHERITANCE|lib/_internal/js_runtime/lib/interceptors.dart|1659|7|5|Superinterfaces don't have a valid override for '\|': JSNumber.\| (num Function(num)), int.\| (int Function(int)). -ERROR|COMPILE_TIME_ERROR|INCONSISTENT_INHERITANCE|lib/_internal/js_runtime/lib/interceptors.dart|1659|7|5|Superinterfaces don't have a valid override for '^': JSNumber.^ (num Function(num)), int.^ (int Function(int)). -ERROR|COMPILE_TIME_ERROR|RETURN_OF_INVALID_TYPE|lib/_internal/js_runtime/lib/interceptors.dart|1514|14|45|A value of type 'double' can't be returned from method '%' because it has a return type of 'JSNumber'. -ERROR|COMPILE_TIME_ERROR|RETURN_OF_INVALID_TYPE|lib/_internal/js_runtime/lib/interceptors.dart|1516|14|45|A value of type 'double' can't be returned from method '%' because it has a return type of 'JSNumber'. -ERROR|COMPILE_TIME_ERROR|UNDEFINED_OPERATOR|lib/_internal/js_runtime/lib/interceptors.dart|1676|28|1|The operator '&' isn't defined for the type 'JSInt'. -ERROR|COMPILE_TIME_ERROR|UNDEFINED_OPERATOR|lib/_internal/js_runtime/lib/interceptors.dart|1678|27|1|The operator '&' isn't defined for the type 'JSInt'. -ERROR|COMPILE_TIME_ERROR|UNDEFINED_OPERATOR|lib/_internal/js_runtime/lib/interceptors.dart|1681|17|1|The operator '&' isn't defined for the type 'JSInt'. -ERROR|COMPILE_TIME_ERROR|UNDEFINED_OPERATOR|lib/_internal/js_runtime/lib/interceptors.dart|1686|18|1|The operator '&' isn't defined for the type 'JSInt'. -ERROR|COMPILE_TIME_ERROR|UNDEFINED_OPERATOR|lib/_internal/js_runtime/lib/interceptors.dart|1686|44|1|The operator '&' isn't defined for the type 'JSInt'. +ERROR|COMPILE_TIME_ERROR|INCONSISTENT_INHERITANCE|lib/_internal/js_runtime/lib/interceptors.dart|1706|7|5|Superinterfaces don't have a valid override for '&': JSNumber.& (num Function(num)), int.& (int Function(int)). +ERROR|COMPILE_TIME_ERROR|INCONSISTENT_INHERITANCE|lib/_internal/js_runtime/lib/interceptors.dart|1706|7|5|Superinterfaces don't have a valid override for '<<': JSNumber.<< (num Function(num)), int.<< (int Function(int)). +ERROR|COMPILE_TIME_ERROR|INCONSISTENT_INHERITANCE|lib/_internal/js_runtime/lib/interceptors.dart|1706|7|5|Superinterfaces don't have a valid override for '>>': JSNumber.>> (num Function(num)), int.>> (int Function(int)). +ERROR|COMPILE_TIME_ERROR|INCONSISTENT_INHERITANCE|lib/_internal/js_runtime/lib/interceptors.dart|1706|7|5|Superinterfaces don't have a valid override for '\|': JSNumber.\| (num Function(num)), int.\| (int Function(int)). +ERROR|COMPILE_TIME_ERROR|INCONSISTENT_INHERITANCE|lib/_internal/js_runtime/lib/interceptors.dart|1706|7|5|Superinterfaces don't have a valid override for '^': JSNumber.^ (num Function(num)), int.^ (int Function(int)). +ERROR|COMPILE_TIME_ERROR|RETURN_OF_INVALID_TYPE|lib/_internal/js_runtime/lib/interceptors.dart|1561|14|45|A value of type 'double' can't be returned from method '%' because it has a return type of 'JSNumber'. +ERROR|COMPILE_TIME_ERROR|RETURN_OF_INVALID_TYPE|lib/_internal/js_runtime/lib/interceptors.dart|1563|14|45|A value of type 'double' can't be returned from method '%' because it has a return type of 'JSNumber'. +ERROR|COMPILE_TIME_ERROR|UNDEFINED_OPERATOR|lib/_internal/js_runtime/lib/interceptors.dart|1723|28|1|The operator '&' isn't defined for the type 'JSInt'. +ERROR|COMPILE_TIME_ERROR|UNDEFINED_OPERATOR|lib/_internal/js_runtime/lib/interceptors.dart|1725|27|1|The operator '&' isn't defined for the type 'JSInt'. +ERROR|COMPILE_TIME_ERROR|UNDEFINED_OPERATOR|lib/_internal/js_runtime/lib/interceptors.dart|1728|17|1|The operator '&' isn't defined for the type 'JSInt'. +ERROR|COMPILE_TIME_ERROR|UNDEFINED_OPERATOR|lib/_internal/js_runtime/lib/interceptors.dart|1733|18|1|The operator '&' isn't defined for the type 'JSInt'. +ERROR|COMPILE_TIME_ERROR|UNDEFINED_OPERATOR|lib/_internal/js_runtime/lib/interceptors.dart|1733|44|1|The operator '&' isn't defined for the type 'JSInt'. diff --git a/pkg/kernel/lib/clone.dart b/pkg/kernel/lib/clone.dart index 950d05230c9..ed6e438681d 100644 --- a/pkg/kernel/lib/clone.dart +++ b/pkg/kernel/lib/clone.dart @@ -103,6 +103,24 @@ class CloneVisitorNotMembers implements TreeVisitor { return result; } + /// Root entry point for cloning a subtree within the same context where the + /// file offsets are valid. + T cloneInContext(T node) { + assert(_activeFileUri == null); + _activeFileUri = _activeFileUriFromContext(node); + final TreeNode result = clone(node); + _activeFileUri = null; + return result; + } + + Uri _activeFileUriFromContext(TreeNode node) { + while (node != null) { + if (node is FileUriNode && node.fileUri != null) return node.fileUri; + node = node.parent; + } + return null; + } + DartType visitType(DartType type) { return substitute(type, typeSubstitution); } diff --git a/sdk/lib/_internal/js_runtime/lib/js_array.dart b/sdk/lib/_internal/js_runtime/lib/js_array.dart index e3a6b03353d..93bd80c2e3a 100644 --- a/sdk/lib/_internal/js_runtime/lib/js_array.dart +++ b/sdk/lib/_internal/js_runtime/lib/js_array.dart @@ -26,7 +26,9 @@ class JSArray extends Interceptor implements List, JSIndexable { return new JSArray.fixed(length); } - /// Returns a fresh JavaScript Array, marked as fixed-length. + /// Returns a fresh JavaScript Array, marked as fixed-length. The holes in the + /// array yield `undefined`, making the Dart List appear to be filled with + /// `null` values. /// /// [length] must be a non-negative integer. factory JSArray.fixed(int length) { @@ -36,8 +38,34 @@ class JSArray extends Interceptor implements List, JSIndexable { if (length is! int) { throw new ArgumentError.value(length, 'length', 'is not an integer'); } - // The JavaScript Array constructor with one argument throws if - // the value is not a UInt32. Give a better error message. + // The JavaScript Array constructor with one argument throws if the value is + // not a UInt32 but the error message does not contain the bad value. Give a + // better error message. + int maxJSArrayLength = 0xFFFFFFFF; + if (length < 0 || length > maxJSArrayLength) { + throw new RangeError.range(length, 0, maxJSArrayLength, 'length'); + } + return new JSArray.markFixed(JS('', 'new Array(#)', length)); + } + + /// Returns a fresh JavaScript Array, marked as fixed-length. The Array is + /// allocated but no elements are assigned. + /// + /// All elements of the array must be assigned before the array is valid. This + /// is essentially the same as `JSArray.fixed` except that global type + /// inference starts with bottom for the element type. + /// + /// [length] must be a non-negative integer. + factory JSArray.allocateFixed(int length) { + // Explicit type test is necessary to guard against JavaScript conversions + // in unchecked mode, and against `new Array(null)` which creates a single + // element Array containing `null`. + if (length is! int) { + throw new ArgumentError.value(length, 'length', 'is not an integer'); + } + // The JavaScript Array constructor with one argument throws if the value is + // not a UInt32 but the error message does not contain the bad value. Give a + // better error message. int maxJSArrayLength = 0xFFFFFFFF; if (length < 0 || length > maxJSArrayLength) { throw new RangeError.range(length, 0, maxJSArrayLength, 'length'); @@ -48,9 +76,11 @@ class JSArray extends Interceptor implements List, JSIndexable { /// Returns a fresh growable JavaScript Array of zero length length. factory JSArray.emptyGrowable() => new JSArray.markGrowable(JS('', '[]')); - /// Returns a fresh growable JavaScript Array with initial length. + /// Returns a fresh growable JavaScript Array with initial length. The holes + /// in the array yield `undefined`, making the Dart List appear to be filled + /// with `null` values. /// - /// [validatedLength] must be a non-negative integer. + /// [length] must be a non-negative integer. factory JSArray.growable(int length) { // Explicit type test is necessary to guard against JavaScript conversions // in unchecked mode. @@ -60,6 +90,23 @@ class JSArray extends Interceptor implements List, JSIndexable { return new JSArray.markGrowable(JS('', 'new Array(#)', length)); } + /// Returns a fresh growable JavaScript Array with initial length. The Array + /// is allocated but no elements are assigned. + /// + /// All elements of the array must be assigned before the array is valid. This + /// is essentially the same as `JSArray.growable` except that global type + /// inference starts with bottom for the element type. + /// + /// [length] must be a non-negative integer. + factory JSArray.allocateGrowable(int length) { + // Explicit type test is necessary to guard against JavaScript conversions + // in unchecked mode. + if ((length is! int) || (length < 0)) { + throw new ArgumentError('Length must be a non-negative integer: $length'); + } + return new JSArray.markGrowable(JS('', 'new Array(#)', length)); + } + /// Constructor for adding type parameters to an existing JavaScript Array. /// The compiler specially recognizes this constructor. /// diff --git a/tests/dart2js/list_generate_1_test.dart b/tests/dart2js/list_generate_1_test.dart new file mode 100644 index 00000000000..1b2fb4d036e --- /dev/null +++ b/tests/dart2js/list_generate_1_test.dart @@ -0,0 +1,69 @@ +// Copyright (c) 2020, 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:expect/expect.dart'; + +dynamic function = 'function'; +dynamic growable = 'growable'; +dynamic length = 'length'; + +// Use top level dynamic variables so this call is not lowered. This function +// and the callers (test1, test2) are not inlined to prevent store-forwarding of +// the top-level variables. +@pragma('dart2js:noInline') +List general() => List.generate(length, function, growable: growable); + +void main() { + function = (int i) => i; + growable = true; + length = 5; + + test1(); + + int k = 0; + function = (int u) => seq3(u += 10, k += 100, () => u += k + 100000); + growable = false; + length = 5; + + test2(); +} + +@pragma('dart2js:noInline') +void test1() { + // Simple test. + final r1 = List.generate(5, (i) => i, growable: true); + final r2 = general(); + + Expect.equals('[0, 1, 2, 3, 4]', '$r1'); + Expect.equals('[0, 1, 2, 3, 4]', '$r2'); +} + +// A sequence of two operations in expression form, returning the last value. +T seq2(dynamic a, T b) => b; +T seq3(dynamic a, dynamic b, T c) => c; + +@pragma('dart2js:noInline') +void test2() { + // Test with a complex environment. + int c = 0; + final r1 = List.generate( + 5, + (i) => seq3(i += 10, c += 100, () => i += c + 100000), + growable: false, + ); + final r2 = general(); + + final e12 = r1[2]; + final e22 = r2[2]; + + final s123 = [e12(), e12(), e12()]; + final s223 = [e22(), e22(), e22()]; + + // 'i' is bound to the loop variable (2 for element at [2]). + // 'i' is incremented by 10, so the low digits are '12' + // 'c' is shared by all closures, and has been incremented to 500 at end of + // construction, so each call increments 'i' by 100500. + Expect.equals('[100512, 201012, 301512]', '$s123'); + Expect.equals('[100512, 201012, 301512]', '$s123'); +} diff --git a/tests/dart2js/list_generate_2_test.dart b/tests/dart2js/list_generate_2_test.dart new file mode 100644 index 00000000000..83487e51063 --- /dev/null +++ b/tests/dart2js/list_generate_2_test.dart @@ -0,0 +1,21 @@ +// Copyright (c) 2020, 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:expect/expect.dart'; + +void main() { + test(0, '[]'); + test(1, '[[[1]]]'); + test(2, '[[[1]], [[2], [3, 4]]]'); + test(3, '[[[1]], [[2], [3, 4]], [[5], [6, 7], [8, 9, 10]]]'); +} + +void test(int i, String expected) { + // Many nested closures with shadowing variables. + int c = 0; + final r = List.generate( + i, (i) => List.generate(i + 1, (i) => List.generate(i + 1, (i) => ++c))); + + Expect.equals(expected, '$r'); +}