From e66a0cbde8cea182d3a04bb319f1df4c3175af53 Mon Sep 17 00:00:00 2001 From: Stephen Adams Date: Wed, 10 Jul 2024 01:39:58 +0000 Subject: [PATCH] [dart2js] Unmodifiable Array and typed data using flags - Add SSA HArrayFlags{Check,Get,Set} instructions to check for fixed-length and unmodifiable JSArray and JavaScript typed data instances. - Add optimizations to remove redundant checks. - Added HOutputConstrained interface for instructions that must have the input and output in the same JavaScript variable. This generalizes some code generation logic already applied to HCheck. Bug: #53785 Change-Id: I61750dd03aa3a964eed3bc76e1656c5f60f77109 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/372002 Commit-Queue: Stephen Adams Reviewed-by: Mayank Patke --- pkg/compiler/lib/src/common/elements.dart | 3 + pkg/compiler/lib/src/js_backend/namer.dart | 2 + .../interceptor_stub_generator.dart | 9 +- .../startup_emitter/fragment_emitter.dart | 7 +- .../startup_emitter/model_emitter.dart | 3 + .../lib/src/kernel/dart2js_target.dart | 2 + pkg/compiler/lib/src/ssa/builder.dart | 73 ++ pkg/compiler/lib/src/ssa/codegen.dart | 86 ++- pkg/compiler/lib/src/ssa/codegen_helpers.dart | 14 + .../src/ssa/invoke_dynamic_specializers.dart | 39 +- pkg/compiler/lib/src/ssa/nodes.dart | 144 +++- pkg/compiler/lib/src/ssa/optimize.dart | 122 +++- pkg/compiler/lib/src/ssa/tracer.dart | 18 + .../lib/src/ssa/types_propagation.dart | 8 + .../lib/src/ssa/variable_allocator.dart | 45 +- .../codegen/data/unmodifiable_bytedata.dart | 117 +++ .../test/codegen/data/unmodifiable_list.dart | 50 ++ .../codegen/data/unmodifiable_typed_list.dart | 196 +++++ .../test/optimization/data/index_assign.dart | 3 +- .../rti/data/generic_methods_dynamic_05.dart | 6 +- pkg/compiler/test/rti/data/list_literal.dart | 4 +- pkg/compiler/test/rti/data/list_to_set.dart | 4 +- .../rti/data/local_function_list_literal.dart | 2 +- pkg/js_runtime/lib/synced/array_flags.dart | 63 ++ .../js_runtime/lib/foreign_helper.dart | 57 ++ .../js_runtime/lib/interceptors.dart | 10 +- .../_internal/js_runtime/lib/js_array.dart | 46 +- .../_internal/js_runtime/lib/js_helper.dart | 43 ++ .../js_runtime/lib/native_typed_data.dart | 684 ++++++++---------- .../js_runtime/lib/synced/array_flags.dart | 63 ++ sdk/lib/libraries.json | 3 + sdk/lib/libraries.yaml | 3 + ...nt_unmodifiable_typed_data_error_test.dart | 217 ++++++ ...nt_unmodifiable_typed_list_error_test.dart | 154 ++++ 34 files changed, 1824 insertions(+), 476 deletions(-) create mode 100644 pkg/compiler/test/codegen/data/unmodifiable_bytedata.dart create mode 100644 pkg/compiler/test/codegen/data/unmodifiable_list.dart create mode 100644 pkg/compiler/test/codegen/data/unmodifiable_typed_list.dart create mode 100644 pkg/js_runtime/lib/synced/array_flags.dart create mode 100644 sdk/lib/_internal/js_runtime/lib/synced/array_flags.dart create mode 100644 tests/web/consistent_unmodifiable_typed_data_error_test.dart create mode 100644 tests/web/consistent_unmodifiable_typed_list_error_test.dart diff --git a/pkg/compiler/lib/src/common/elements.dart b/pkg/compiler/lib/src/common/elements.dart index 04e82de9a0b..39764000a7b 100644 --- a/pkg/compiler/lib/src/common/elements.dart +++ b/pkg/compiler/lib/src/common/elements.dart @@ -820,6 +820,9 @@ abstract class CommonElements { FunctionEntity get cyclicThrowHelper => _findHelperFunction("throwCyclicInit"); + FunctionEntity get throwUnsupportedOperation => + _findHelperFunction('throwUnsupportedOperation'); + FunctionEntity get defineProperty => _findHelperFunction('defineProperty'); FunctionEntity get throwLateFieldNI => diff --git a/pkg/compiler/lib/src/js_backend/namer.dart b/pkg/compiler/lib/src/js_backend/namer.dart index f80e8d601a1..a4da9b54d30 100644 --- a/pkg/compiler/lib/src/js_backend/namer.dart +++ b/pkg/compiler/lib/src/js_backend/namer.dart @@ -2192,6 +2192,8 @@ class FixedNames { String get recordShapeRecipe => r'$recipe'; String get recordShapeTag => r'$shape'; + + String get arrayFlagsPropertyName => r'$flags'; } /// Minified version of the fixed names usage by the namer. diff --git a/pkg/compiler/lib/src/js_emitter/interceptor_stub_generator.dart b/pkg/compiler/lib/src/js_emitter/interceptor_stub_generator.dart index 5b9d6087eaa..5c6ab76d729 100644 --- a/pkg/compiler/lib/src/js_emitter/interceptor_stub_generator.dart +++ b/pkg/compiler/lib/src/js_emitter/interceptor_stub_generator.dart @@ -4,6 +4,7 @@ library dart2js.js_emitter.interceptor_stub_generator; +import 'package:js_runtime/synced/array_flags.dart'; import 'package:js_runtime/synced/embedded_names.dart' as embeddedNames; import '../common/elements.dart'; @@ -409,10 +410,14 @@ class InterceptorStubGenerator { return js.statement(r''' if (typeof a0 === "number") - if (# && !receiver.immutable$list && + if (# && !(receiver.# & #) && (a0 >>> 0) === a0 && a0 < receiver.length) return receiver[a0] = a1; - ''', typeCheck); + ''', [ + typeCheck, + _namer.fixedNames.arrayFlagsPropertyName, + js.number(ArrayFlags.unmodifiableCheck) + ]); } } else if (selector.isCall) { if (selector.name == 'abs' && selector.argumentCount == 0) { diff --git a/pkg/compiler/lib/src/js_emitter/startup_emitter/fragment_emitter.dart b/pkg/compiler/lib/src/js_emitter/startup_emitter/fragment_emitter.dart index 9749e044e60..1f5ed26873b 100644 --- a/pkg/compiler/lib/src/js_emitter/startup_emitter/fragment_emitter.dart +++ b/pkg/compiler/lib/src/js_emitter/startup_emitter/fragment_emitter.dart @@ -171,11 +171,7 @@ function lazyFinal(holder, name, getterName, initializer) { // // The runtime ensures that const-lists cannot be modified. function makeConstList(list) { - // By assigning a function to the properties they become part of the - // hidden class. The actual values of the fields don't matter, since we - // only check if they exist. - list.immutable\$list = Array; - list.fixed\$length = Array; + list.#arrayFlagsProperty = ${ArrayFlags.constant}; return list; } @@ -675,6 +671,7 @@ class FragmentEmitter { 'directAccessTestExpression': js.js(_directAccessTestExpression), 'throwLateFieldADI': _emitter .staticFunctionAccess(_closedWorld.commonElements.throwLateFieldADI), + 'arrayFlagsProperty': js.string(_namer.fixedNames.arrayFlagsPropertyName), 'operatorIsPrefix': js.string(_namer.fixedNames.operatorIsPrefix), 'tearOffCode': js.Block( buildTearOffCode(_options, _emitter, _closedWorld.commonElements)), diff --git a/pkg/compiler/lib/src/js_emitter/startup_emitter/model_emitter.dart b/pkg/compiler/lib/src/js_emitter/startup_emitter/model_emitter.dart index 7870c0d83cd..74ac4ee401d 100644 --- a/pkg/compiler/lib/src/js_emitter/startup_emitter/model_emitter.dart +++ b/pkg/compiler/lib/src/js_emitter/startup_emitter/model_emitter.dart @@ -6,6 +6,8 @@ library dart2js.js_emitter.startup_emitter.model_emitter; import 'dart:convert' show JsonEncoder; +import 'package:js_runtime/synced/array_flags.dart' show ArrayFlags; + import 'package:js_runtime/synced/embedded_names.dart' show DEFERRED_INITIALIZED, @@ -35,6 +37,7 @@ import 'package:js_shared/synced/embedded_names.dart' RTI_UNIVERSE, RtiUniverseFieldNames, TYPES; + import 'package:js_shared/variance.dart'; import 'package:js_ast/src/precedence.dart' as js_precedence; diff --git a/pkg/compiler/lib/src/kernel/dart2js_target.dart b/pkg/compiler/lib/src/kernel/dart2js_target.dart index dea373b12b2..444b9b816ab 100644 --- a/pkg/compiler/lib/src/kernel/dart2js_target.dart +++ b/pkg/compiler/lib/src/kernel/dart2js_target.dart @@ -255,6 +255,7 @@ const implicitlyUsedLibraries = [ // compile-platform should just specify which libraries to compile instead. const requiredLibraries = >{ 'dart2js': [ + 'dart:_array_flags', 'dart:_async_status_codes', 'dart:_dart2js_only', 'dart:_dart2js_runtime_metrics', @@ -297,6 +298,7 @@ const requiredLibraries = >{ 'dart:web_gl', ], 'dart2js_server': [ + 'dart:_array_flags', 'dart:_async_status_codes', 'dart:_dart2js_only', 'dart:_dart2js_runtime_metrics', diff --git a/pkg/compiler/lib/src/ssa/builder.dart b/pkg/compiler/lib/src/ssa/builder.dart index 5d10b1eb484..63e4850bbcb 100644 --- a/pkg/compiler/lib/src/ssa/builder.dart +++ b/pkg/compiler/lib/src/ssa/builder.dart @@ -4631,6 +4631,12 @@ class KernelSsaGraphBuilder extends ir.VisitorDefault _handleLateInitializeOnceCheck(invocation, sourceInformation); } else if (name == 'HCharCodeAt') { _handleCharCodeAt(invocation, sourceInformation); + } else if (name == 'HArrayFlagsGet') { + _handleArrayFlagsGet(invocation, sourceInformation); + } else if (name == 'HArrayFlagsSet') { + _handleArrayFlagsSet(invocation, sourceInformation); + } else if (name == 'HArrayFlagsCheck') { + _handleArrayFlagsCheck(invocation, sourceInformation); } else { reporter.internalError( _elementMap.getSpannable(targetElement, invocation), @@ -5372,6 +5378,73 @@ class KernelSsaGraphBuilder extends ir.VisitorDefault ..sourceInformation = sourceInformation); } + void _handleArrayFlagsGet( + ir.StaticInvocation invocation, SourceInformation? sourceInformation) { + if (_unexpectedForeignArguments(invocation, + minPositional: 1, maxPositional: 1)) { + // Result expected on stack. + stack.add(graph.addConstantNull(closedWorld)); + return; + } + List inputs = _visitPositionalArguments(invocation.arguments); + final array = inputs.single; + + push(HArrayFlagsGet(array, _abstractValueDomain.uint31Type) + ..sourceInformation = sourceInformation); + } + + void _handleArrayFlagsSet( + ir.StaticInvocation invocation, SourceInformation? sourceInformation) { + if (_unexpectedForeignArguments(invocation, + minPositional: 2, maxPositional: 2, typeArgumentCount: 1)) { + // Result expected on stack. + stack.add(graph.addConstantNull(closedWorld)); + return; + } + + List inputs = _visitPositionalArguments(invocation.arguments); + final array = inputs[0]; + final flags = inputs[1]; + + // TODO(sra): Use the flags to improve in the AbstractValue, which may + // contain powerset domain bits outside of the conventional type + // system. Perhaps do this in types_propagation. + DartType type = _getDartTypeIfValid(invocation.arguments.types.single); + AbstractValue? instructionType = _typeBuilder.trustTypeMask(type); + // TODO(sra): Better type + instructionType ??= _abstractValueDomain.dynamicType; + + push(HArrayFlagsSet(array, flags, instructionType) + ..sourceInformation = sourceInformation); + } + + void _handleArrayFlagsCheck( + ir.StaticInvocation invocation, SourceInformation? sourceInformation) { + if (_unexpectedForeignArguments(invocation, + minPositional: 4, maxPositional: 4, typeArgumentCount: 1)) { + // Result expected on stack. + stack.add(graph.addConstantNull(closedWorld)); + return; + } + List inputs = _visitPositionalArguments(invocation.arguments); + final array = inputs[0]; + final arrayFlags = inputs[1]; + final checkFlags = inputs[2]; + final operation = inputs[3]; + + // TODO(sra): Use the flags to improve in the AbstractValue, which may + // contain powerset domain bits outside of the conventional type + // system. Perhaps do this in types_propagation. + DartType type = _getDartTypeIfValid(invocation.arguments.types.single); + AbstractValue? instructionType = _typeBuilder.trustTypeMask(type); + // TODO(sra): Better type + instructionType ??= _abstractValueDomain.dynamicType; + + push(HArrayFlagsCheck( + array, arrayFlags, checkFlags, operation, instructionType) + ..sourceInformation = sourceInformation); + } + void _handleForeignTypeRef( ir.StaticInvocation invocation, SourceInformation? sourceInformation) { if (_unexpectedForeignArguments(invocation, diff --git a/pkg/compiler/lib/src/ssa/codegen.dart b/pkg/compiler/lib/src/ssa/codegen.dart index 5785b8deaad..4fa574d8f25 100644 --- a/pkg/compiler/lib/src/ssa/codegen.dart +++ b/pkg/compiler/lib/src/ssa/codegen.dart @@ -682,14 +682,15 @@ class SsaCodeGenerator implements HVisitor, HBlockInformationVisitor { // emit an assignment, because the intTypeCheck just returns its // argument. bool needsAssignment = true; - if (instruction is HCheck) { + if (instruction is HOutputConstrainedToAnInput) { if (instruction is HPrimitiveCheck || instruction is HAsCheck || instruction is HAsCheckSimple || instruction is HBoolConversion || instruction is HNullCheck || - instruction is HLateReadCheck) { - String? inputName = variableNames.getName(instruction.checkedInput); + instruction is HLateReadCheck || + instruction is HArrayFlagsSet) { + String? inputName = variableNames.getName(instruction.constrainedInput); if (variableNames.getName(instruction) == inputName) { needsAssignment = false; } @@ -715,9 +716,9 @@ class SsaCodeGenerator implements HVisitor, HBlockInformationVisitor { } } - HInstruction skipGenerateAtUseCheckInputs(HCheck check) { - HInstruction input = check.checkedInput; - if (input is HCheck && isGenerateAtUseSite(input)) { + HInstruction skipGenerateAtUseCheckInputs(HOutputConstrainedToAnInput check) { + HInstruction input = check.constrainedInput; + if (input is HOutputConstrainedToAnInput && isGenerateAtUseSite(input)) { return skipGenerateAtUseCheckInputs(input); } return input; @@ -726,7 +727,8 @@ class SsaCodeGenerator implements HVisitor, HBlockInformationVisitor { void use(HInstruction argument) { if (isGenerateAtUseSite(argument)) { visitExpression(argument); - } else if (argument is HCheck && !variableNames.hasName(argument)) { + } else if (argument is HOutputConstrainedToAnInput && + !variableNames.hasName(argument)) { // We have a check that is not generate-at-use and has no name, yet is a // subexpression (we are in 'use'). This happens when we have a chain of // checks on an available unnamed value (e.g. a constant). The checks are @@ -737,11 +739,10 @@ class SsaCodeGenerator implements HVisitor, HBlockInformationVisitor { // instruction has a name or is generate-at-use". This would require // naming the input or output of the chain-of-checks. - HCheck check = argument; // This can only happen if the checked node also does not have a name. - assert(!variableNames.hasName(check.checkedInput)); + assert(!variableNames.hasName(argument.constrainedInput)); - use(skipGenerateAtUseCheckInputs(check)); + use(skipGenerateAtUseCheckInputs(argument)); } else { assert(variableNames.hasName(argument)); push(js.VariableUse(variableNames.getName(argument)!)); @@ -3441,4 +3442,69 @@ class SsaCodeGenerator implements HVisitor, HBlockInformationVisitor { _metrics.countHIsLateSentinel++; _emitIsLateSentinel(node.inputs.single, node.sourceInformation); } + + @override + void visitArrayFlagsGet(HArrayFlagsGet node) { + use(node.inputs.single); + js.Expression array = pop(); + js.Expression flags = + js.js(r'#.#', [array, _namer.fixedNames.arrayFlagsPropertyName]); + if (isGenerateAtUseSite(node) && node.usedBy.single is HArrayFlagsCheck) { + // The enclosing expression will be an immediate `& mask`. + push(flags); + } else { + // The flags are reused, possibly hoisted, so force an `undefined` to be a + // small integer once rather than at each check. + push(js.js(r'# | 0', flags)); + } + } + + @override + void visitArrayFlagsSet(HArrayFlagsSet node) { + use(node.inputs[0]); + js.Expression array = pop(); + use(node.inputs[1]); + js.Expression arrayFlags = pop(); + pushStatement(js.js.statement(r'#.# = #;', [ + array, + _namer.fixedNames.arrayFlagsPropertyName, + arrayFlags + ]).withSourceInformation(node.sourceInformation)); + } + + @override + void visitArrayFlagsCheck(HArrayFlagsCheck node) { + use(node.array); + js.Expression array = pop(); + + js.Expression? test; + if (!node.alwaysThrows()) { + use(node.arrayFlags); + js.Expression arrayFlags = pop(); + use(node.checkFlags); + js.Expression checkFlags = pop(); + test = js.js('# & #', [arrayFlags, checkFlags]); + } + + final operation = node.operation; + // Most common operation is "[]=", so 'pass' that by leaving it out. + if (operation + case HConstant(constant: StringConstantValue(stringValue: '[]='))) { + _pushCallStatic(_commonElements.throwUnsupportedOperation, [array], + node.sourceInformation); + } else { + use(operation); + _pushCallStatic(_commonElements.throwUnsupportedOperation, [array, pop()], + node.sourceInformation); + } + + js.Statement check; + if (test == null) { + check = js.js.statement('#;', pop()); + } else { + check = js.js.statement('# && #;', [test, pop()]); + } + + pushStatement(check.withSourceInformation(node.sourceInformation)); + } } diff --git a/pkg/compiler/lib/src/ssa/codegen_helpers.dart b/pkg/compiler/lib/src/ssa/codegen_helpers.dart index dcee553a703..9f990d265e2 100644 --- a/pkg/compiler/lib/src/ssa/codegen_helpers.dart +++ b/pkg/compiler/lib/src/ssa/codegen_helpers.dart @@ -1041,6 +1041,20 @@ class SsaInstructionMerger extends HBaseVisitor implements CodegenPhase { } } + @override + void visitArrayFlagsSet(HArrayFlagsSet instruction) { + // Cannot generate-at-use the array input, it is an alias for the value of + // this instruction and need to be allocated to a variable. + analyzeInputs(instruction, 1); + } + + @override + void visitArrayFlagsCheck(HArrayFlagsCheck instruction) { + // Cannot generate-at-use the array input, it is an alias for the value of + // this instruction and need to be allocated to a variable. + analyzeInputs(instruction, 1); + } + void tryGenerateAtUseSite(HInstruction instruction) { if (instruction.isControlFlow()) return; markAsGenerateAtUseSite(instruction); diff --git a/pkg/compiler/lib/src/ssa/invoke_dynamic_specializers.dart b/pkg/compiler/lib/src/ssa/invoke_dynamic_specializers.dart index 05fb3d4e358..0e6fc59eecc 100644 --- a/pkg/compiler/lib/src/ssa/invoke_dynamic_specializers.dart +++ b/pkg/compiler/lib/src/ssa/invoke_dynamic_specializers.dart @@ -2,6 +2,7 @@ // 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:js_runtime/synced/array_flags.dart' show ArrayFlags; import '../common/elements.dart' show JCommonElements; import '../constants/constant_system.dart' as constant_system; import '../constants/values.dart'; @@ -203,13 +204,23 @@ class IndexAssignSpecializer extends InvokeDynamicSpecializer { OptimizationTestLog? log) { HInstruction receiver = instruction.inputs[1]; HInstruction index = instruction.inputs[2]; - if (receiver - .isMutableIndexable(closedWorld.abstractValueDomain) - .isPotentiallyFalse) { - return null; + final abstractValueDomain = closedWorld.abstractValueDomain; + + bool needsMutableCheck = false; + if (abstractValueDomain + .isTypedArray(receiver.instructionType) + .isDefinitelyTrue) { + needsMutableCheck = true; + } else if (receiver.isArray(abstractValueDomain).isDefinitelyTrue) { + needsMutableCheck = + receiver.isMutableArray(abstractValueDomain).isPotentiallyFalse; + } else { + if (receiver.isMutableIndexable(abstractValueDomain).isPotentiallyFalse) { + return null; + } } // TODO(johnniwinther): Merge this and the following if statement. - if (index.isInteger(closedWorld.abstractValueDomain).isPotentiallyFalse && + if (index.isInteger(abstractValueDomain).isPotentiallyFalse && // TODO(johnniwinther): Support annotations on the possible targets // and used their parameter check policy here. closedWorld.annotationsData.getParameterCheckPolicy(null).isEmitted) { @@ -227,6 +238,24 @@ class IndexAssignSpecializer extends InvokeDynamicSpecializer { } } + if (needsMutableCheck) { + HInstruction getFlags = + HArrayFlagsGet(receiver, abstractValueDomain.uint31Type) + ..sourceInformation = instruction.sourceInformation; + instruction.block!.addBefore(instruction, getFlags); + HInstruction mask = + graph.addConstantInt(ArrayFlags.unmodifiableCheck, closedWorld); + HInstruction name = graph.addConstantString('[]=', closedWorld); + final instructionType = receiver.instructionType; + final checkFlags = + HArrayFlagsCheck(receiver, getFlags, mask, name, instructionType) + ..sourceInformation = instruction.sourceInformation; + instruction.block!.addBefore(instruction, checkFlags); + checkFlags.instructionType = checkFlags.computeInstructionType( + instructionType, abstractValueDomain); + receiver = checkFlags; + } + HInstruction checkedIndex = index; if (requiresBoundsCheck(instruction, closedWorld)) { checkedIndex = diff --git a/pkg/compiler/lib/src/ssa/nodes.dart b/pkg/compiler/lib/src/ssa/nodes.dart index d8ab7bb8059..e55719b13bd 100644 --- a/pkg/compiler/lib/src/ssa/nodes.dart +++ b/pkg/compiler/lib/src/ssa/nodes.dart @@ -130,6 +130,10 @@ abstract class HVisitor { R visitInstanceEnvironment(HInstanceEnvironment node); R visitTypeEval(HTypeEval node); R visitTypeBind(HTypeBind node); + + R visitArrayFlagsCheck(HArrayFlagsCheck node); + R visitArrayFlagsGet(HArrayFlagsGet node); + R visitArrayFlagsSet(HArrayFlagsSet node); } abstract class HGraphVisitor { @@ -644,6 +648,13 @@ class HBaseVisitor extends HGraphVisitor implements HVisitor { R visitTypeEval(HTypeEval node) => visitInstruction(node); @override R visitTypeBind(HTypeBind node) => visitInstruction(node); + + @override + R visitArrayFlagsCheck(HArrayFlagsCheck node) => visitCheck(node); + @override + R visitArrayFlagsGet(HArrayFlagsGet node) => visitInstruction(node); + @override + R visitArrayFlagsSet(HArrayFlagsSet node) => visitInstruction(node); } class SubGraph { @@ -1108,6 +1119,8 @@ enum _GvnType { lateWriteOnceCheck, lateInitializeOnceCheck, charCodeAt, + arrayFlagsGet, + arrayFlagsCheck, } abstract class HInstruction implements SpannableWithEntity { @@ -1607,11 +1620,21 @@ class HRef extends HInstruction { /// generating JavaScript. abstract interface class HLateInstruction {} +/// Interface for instructions where the output is constrained to be one of the +/// inputs. Used for checks, where the SSA value of the check represents the +/// same value as the input, but restricted in some way, e.g., being of a +/// refined type or in a checked range. +abstract interface class HOutputConstrainedToAnInput implements HInstruction { + /// The input which is the 'same' as the output. + HInstruction get constrainedInput; +} + /// A [HCheck] instruction is an instruction that might do a dynamic check at /// runtime on an input instruction. To have proper instruction dependencies in /// the graph, instructions that depend on the check being done reference the /// [HCheck] instruction instead of the input instruction. -abstract class HCheck extends HInstruction { +abstract class HCheck extends HInstruction + implements HOutputConstrainedToAnInput { HCheck(super.inputs, super.type) { setUseGvn(); } @@ -1624,6 +1647,9 @@ abstract class HCheck extends HInstruction { HInstruction get checkedInput => inputs[0]; + @override + HInstruction get constrainedInput => checkedInput; + @override bool isJsStatement() => true; @@ -4674,6 +4700,122 @@ class HTypeBind extends HInstruction implements HRtiInstruction { String toString() => 'HTypeBind()'; } +/// Check Array or TypedData for permission to modify or grow. +/// +/// Typical use to check modifiability for `a[i] = 0`. The array flags are +/// checked to see if there is a bit that prohibits modification. +/// +/// a = ... +/// f = HArrayFlagsGet(a); +/// a2 = HArrayFlagsCheck(a, f, ArrayFlags.unmodifiableCheck, "[]=") +/// a2[i] = 0 +/// +/// HArrayFlagsGet is a separate instruction so that 'loading' the flags from +/// the Array can by hoisted. +class HArrayFlagsCheck extends HCheck { + HArrayFlagsCheck(HInstruction array, HInstruction arrayFlags, + HInstruction checkFlags, HInstruction operation, AbstractValue type) + : super([array, arrayFlags, checkFlags, operation], type); + + HInstruction get array => inputs[0]; + HInstruction get arrayFlags => inputs[1]; + HInstruction get checkFlags => inputs[2]; + HInstruction get operation => inputs[3]; + + // The checked type is the input type, refined to match the flags. + AbstractValue computeInstructionType( + AbstractValue inputType, AbstractValueDomain domain) { + // TODO(sra): Depening on the checked flags, the output is fixed-length or + // unmodifiable. Refine the type to the degree an AbstractValue can express + // that. + return inputType; + } + + bool alwaysThrows() { + if ((arrayFlags, checkFlags) + case ( + HConstant(constant: IntConstantValue(intValue: final arrayBits)), + HConstant(constant: IntConstantValue(intValue: final checkBits)) + ) when arrayBits & checkBits != BigInt.zero) { + return true; + } + return false; + } + + @override + R accept(HVisitor visitor) => visitor.visitArrayFlagsCheck(this); + + @override + bool isControlFlow() => true; + @override + bool isJsStatement() => true; + + @override + _GvnType get _gvnType => _GvnType.arrayFlagsCheck; + + @override + bool typeEquals(HInstruction other) => other is HArrayFlagsCheck; + + @override + bool dataEquals(HArrayFlagsCheck other) => true; +} + +class HArrayFlagsGet extends HInstruction { + HArrayFlagsGet(HInstruction array, AbstractValue type) + : super([array], type) { + sideEffects.clearAllSideEffects(); + sideEffects.clearAllDependencies(); + // Dependency on HArrayFlagsSet. + sideEffects.setDependsOnInstancePropertyStore(); + setUseGvn(); + } + + @override + R accept(HVisitor visitor) => visitor.visitArrayFlagsGet(this); + + @override + _GvnType get _gvnType => _GvnType.arrayFlagsGet; + + @override + bool typeEquals(HInstruction other) => other is HArrayFlagsGet; + + @override + bool dataEquals(HArrayFlagsGet other) => true; +} + +/// Tag an Array or TypedData object to mark it as unmodifiable or fixed-length. +/// +/// The HArrayFlagsSet instruction represents the tagged Array or TypedData +/// object. The instruction type can be different to the `array` input. +/// HArrayFlagsSet is used in a 'linear' style - there are no accesses to the +/// input after this operation. +/// +/// To ensure that HArrayFlagsGet (possibly from inlined code) does not float +/// past HArrayFlagsSet, we use the 'instance property' effect. +class HArrayFlagsSet extends HInstruction + implements HOutputConstrainedToAnInput { + HArrayFlagsSet(HInstruction array, HInstruction flags, AbstractValue type) + : super([array, flags], type) { + // For correct ordering with respect to HArrayFlagsGet: + sideEffects.setChangesInstanceProperty(); + // Be conservative and make HArrayFlagsSet be a memory fence: + sideEffects.setAllSideEffects(); + sideEffects.setDependsOnSomething(); + } + + HInstruction get array => inputs[0]; + HInstruction get flags => inputs[1]; + + @override + HInstruction get constrainedInput => array; + + @override + R accept(HVisitor visitor) => visitor.visitArrayFlagsSet(this); + + @override + bool isJsStatement() => true; +} + class HIsLateSentinel extends HInstruction { HIsLateSentinel(super.value, super.type) : super._1() { setUseGvn(); diff --git a/pkg/compiler/lib/src/ssa/optimize.dart b/pkg/compiler/lib/src/ssa/optimize.dart index c2132f5aee2..624c66bbdfb 100644 --- a/pkg/compiler/lib/src/ssa/optimize.dart +++ b/pkg/compiler/lib/src/ssa/optimize.dart @@ -2,6 +2,8 @@ // 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:js_runtime/synced/array_flags.dart' show ArrayFlags; + import '../common.dart'; import '../common/codegen.dart' show CodegenRegistry; import '../common/elements.dart' show JCommonElements; @@ -255,6 +257,7 @@ class SsaInstructionSimplifier extends HBaseVisitor void visitGraph(HGraph visitee) { _graph = visitee; visitDominatorTree(visitee); + finalizeArrayFlagEffects(); } @override @@ -1618,11 +1621,21 @@ class SsaInstructionSimplifier extends HBaseVisitor // Can we find the length as an input to an allocation? HInstruction potentialAllocation = receiver; - if (receiver is HInvokeStatic && - receiver.element == commonElements.setArrayType) { - // Look through `setArrayType(new Array(), ...)` - potentialAllocation = receiver.inputs.first; + + SCAN: + while (!_graph.allocatedFixedLists.contains(potentialAllocation)) { + switch (potentialAllocation) { + case HInvokeStatic(:final element) + when element == commonElements.setArrayType: + // Look through `setArrayType(new Array(), ...)` + potentialAllocation = potentialAllocation.inputs.first; + case HArrayFlagsCheck(:final array) || HArrayFlagsSet(:final array): + potentialAllocation = array; + default: + break SCAN; + } } + if (_graph.allocatedFixedLists.contains(potentialAllocation)) { // TODO(sra): How do we keep this working if we lower/inline the receiver // in an optimization? @@ -2350,7 +2363,7 @@ class SsaInstructionSimplifier extends HBaseVisitor @override HInstruction visitInstanceEnvironment(HInstanceEnvironment node) { - HInstruction instance = node.inputs.single; + HInstruction instance = node.inputs.single.nonCheck(); // Store-forward instance types of created instances and constant instances. // @@ -2626,6 +2639,105 @@ class SsaInstructionSimplifier extends HBaseVisitor } return node; } + + @override + HInstruction visitArrayFlagsCheck(HArrayFlagsCheck node) { + // TODO(sra): Implement removal on basis of type, an 'isRedundant' check. + + final array = node.array; + final arrayFlags = node.arrayFlags; + final checkFlags = node.checkFlags; + + if (arrayFlags is HConstant && arrayFlags.constant.isZero) return array; + + if (array is HArrayFlagsCheck) { + // Dependent check. Checks become dependent during types_propagation. + if (arrayFlags == array.arrayFlags && checkFlags == array.checkFlags) { + // Check is redundant, even if the `node.operation` is different + // (different operations are not picked up by GVN). + // + // TODO(sra): If a stronger check dominates a weaker check (e.g. check + // for immutable before check for fixed length), we can match that with + // different flags. + return array; + } + } + + return node; + } + + /// All HArrayFlagsGet instructions that depend on something. Used to promote + /// `HArrayFlagsGet` instructions to side-effect insensitive. See + /// [finalizeArrayFlagEffects] for details. + List? _arrayFlagsGets; + bool _arrayFlagsEffect = false; + + @override + HInstruction visitArrayFlagsSet(HArrayFlagsSet node) { + _arrayFlagsEffect = true; + return node; + } + + @override + HInstruction visitArrayFlagsGet(HArrayFlagsGet node) { + if (node.sideEffects.dependsOnSomething()) { + (_arrayFlagsGets ??= []).add(node); + } else { + // If the HArrayFlagsGet is pure and the source is visible, then there is + // no HArrayFlagsSet instruction that changes the flags, so the flags are + // `0`. This can remove checks on allocations in the same method. To do + // this for typed arrays, we need to recognize the allocation. + + final array = node.inputs.single; + + if (array is HForeignCode) { + final behavior = array.nativeBehavior; + if (behavior != null && behavior.isAllocation) { + return _graph.addConstantInt(ArrayFlags.none, _closedWorld); + } + } + } + + // The following store-forwarding of the flags is valid only because all + // code in the SDK has a 'linear' pattern where the original value is never + // accessed after it is 'tagged' with the flags. + HInstruction array = node.inputs.single; + while (array is HArrayFlagsCheck) { + array = array.array; + } + if (array case HArrayFlagsSet(:final flags)) return flags; + + return node; + } + + void finalizeArrayFlagEffects() { + // HArrayFlagsGet operations must not be moved past HArrayFlagsSet + // operations on the same Array or typed data view. Initially we prevent + // this by making HArrayFlagsSet have a changes-property side effect, and + // making HArrayFlagsGet depend on that effect. + // + // This turns out to be rather restrictive and a general 'depends on + // property' dependency inhibits important optimizations like hoisting + // HArrayFlagsGet out of loops. We could try an add a new effect, but since + // the effect analysis is not aware of (non)aliasing, the new effect would + // largely have the same problem. + // + // Instead we notice that HArrayFlagsSet is rare: it is used to implement + // constructors that initialize the data, and then mark it as unmodifiable + // or fixed-length. If we invoke a callee that does a HArrayFlagsSet + // operation, the target of that operation is not visible to the caller. + // + // Therefore we assume that if we can't see any HArrayFlagsSet operations in + // the current method, they cannot change the value observed by + // HArrayFlagsGet, and we can pretent the HArrayFlagsGets are pure. + + if (_arrayFlagsGets == null || _arrayFlagsEffect) return; + + for (final instruction in _arrayFlagsGets!) { + // Instruction may have been removed from the CFG, but that is harmless. + instruction.sideEffects.clearAllDependencies(); + } + } } class SsaDeadCodeEliminator extends HGraphVisitor implements OptimizationPhase { diff --git a/pkg/compiler/lib/src/ssa/tracer.dart b/pkg/compiler/lib/src/ssa/tracer.dart index 397f2d5c008..e0d09f9acbc 100644 --- a/pkg/compiler/lib/src/ssa/tracer.dart +++ b/pkg/compiler/lib/src/ssa/tracer.dart @@ -781,4 +781,22 @@ class HInstructionStringifier implements HVisitor { var inputs = node.inputs.map(temporaryId).join(', '); return "TypeBind: $inputs"; } + + @override + String visitArrayFlagsCheck(HArrayFlagsCheck node) { + var inputs = node.inputs.map(temporaryId).join(', '); + return "ArrayFlagsCheck: $inputs"; + } + + @override + String visitArrayFlagsGet(HArrayFlagsGet node) { + var inputs = node.inputs.map(temporaryId).join(', '); + return "ArrayFlagsGet: $inputs"; + } + + @override + String visitArrayFlagsSet(HArrayFlagsSet node) { + var inputs = node.inputs.map(temporaryId).join(', '); + return "ArrayFlagsSet: $inputs"; + } } diff --git a/pkg/compiler/lib/src/ssa/types_propagation.dart b/pkg/compiler/lib/src/ssa/types_propagation.dart index 11569df45c3..082036c060e 100644 --- a/pkg/compiler/lib/src/ssa/types_propagation.dart +++ b/pkg/compiler/lib/src/ssa/types_propagation.dart @@ -503,4 +503,12 @@ class SsaTypePropagator extends HBaseVisitor return abstractValueDomain.intersection( abstractValueDomain.boolType, instruction.checkedInput.instructionType); } + + @override + AbstractValue visitArrayFlagsCheck(HArrayFlagsCheck instruction) { + instruction.array + .replaceAllUsersDominatedBy(instruction.next!, instruction); + AbstractValue inputType = instruction.array.instructionType; + return instruction.computeInstructionType(inputType, abstractValueDomain); + } } diff --git a/pkg/compiler/lib/src/ssa/variable_allocator.dart b/pkg/compiler/lib/src/ssa/variable_allocator.dart index 895e6289334..d1457e83d22 100644 --- a/pkg/compiler/lib/src/ssa/variable_allocator.dart +++ b/pkg/compiler/lib/src/ssa/variable_allocator.dart @@ -225,14 +225,15 @@ class SsaLiveIntervalBuilder extends HBaseVisitor with CodegenPhase { // not on the checked instruction t1. // When looking for the checkedInstructionOrNonGenerateAtUseSite of t3 we must // return t2. - HInstruction checkedInstructionOrNonGenerateAtUseSite(HCheck check) { - HInstruction checked = check.checkedInput; - while (checked is HCheck) { - HInstruction next = checked.checkedInput; + HInstruction checkedInstructionOrNonGenerateAtUseSite( + HOutputConstrainedToAnInput check) { + HInstruction constraint = check.constrainedInput; + while (constraint is HOutputConstrainedToAnInput) { + HInstruction next = constraint.constrainedInput; if (generateAtUseSite.contains(next)) break; - checked = next; + constraint = next; } - return checked; + return constraint; } void markAsLiveInEnvironment( @@ -244,11 +245,11 @@ class SsaLiveIntervalBuilder extends HBaseVisitor with CodegenPhase { // Special case the HCheck instruction to mark the actual // checked instruction live. The checked instruction and the // [HCheck] will share the same live ranges. - if (instruction is HCheck) { - HCheck check = instruction; - HInstruction checked = checkedInstructionOrNonGenerateAtUseSite(check); - if (!generateAtUseSite.contains(checked)) { - environment.add(checked, instructionId); + if (instruction is HOutputConstrainedToAnInput) { + HInstruction constraint = + checkedInstructionOrNonGenerateAtUseSite(instruction); + if (!generateAtUseSite.contains(constraint)) { + environment.add(constraint, instructionId); } } } @@ -259,15 +260,15 @@ class SsaLiveIntervalBuilder extends HBaseVisitor with CodegenPhase { environment.remove(instruction, instructionId); // Special case the HCheck instruction to have the same live // interval as the instruction it is checking. - if (instruction is HCheck) { - HCheck check = instruction; - HInstruction checked = checkedInstructionOrNonGenerateAtUseSite(check); - if (!generateAtUseSite.contains(checked)) { - liveIntervals.putIfAbsent(checked, () => LiveInterval()); + if (instruction is HOutputConstrainedToAnInput) { + HInstruction constraint = + checkedInstructionOrNonGenerateAtUseSite(instruction); + if (!generateAtUseSite.contains(constraint)) { + liveIntervals.putIfAbsent(constraint, () => LiveInterval()); // Unconditionally force the live ranges of the HCheck to // be the live ranges of the instruction it is checking. liveIntervals[instruction] = - LiveInterval.forCheck(instructionId, liveIntervals[checked]!); + LiveInterval.forCheck(instructionId, liveIntervals[constraint]!); } } } @@ -493,14 +494,14 @@ class VariableNamer { String allocateName(HInstruction instruction) { String? name; - if (instruction is HCheck) { - // Special case this instruction to use the name of its - // input if it has one. + if (instruction is HOutputConstrainedToAnInput) { + // Special case this instruction to use the name of its input if it has + // one. HInstruction temp = instruction; do { - temp = (temp as HCheck).checkedInput; + temp = (temp as HOutputConstrainedToAnInput).constrainedInput; name = names.ownName[temp]; - } while (name == null && temp is HCheck); + } while (name == null && temp is HOutputConstrainedToAnInput); if (name != null) return addAllocatedName(instruction, name); } diff --git a/pkg/compiler/test/codegen/data/unmodifiable_bytedata.dart b/pkg/compiler/test/codegen/data/unmodifiable_bytedata.dart new file mode 100644 index 00000000000..84b30543c51 --- /dev/null +++ b/pkg/compiler/test/codegen/data/unmodifiable_bytedata.dart @@ -0,0 +1,117 @@ +// Copyright (c) 2024, 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:_foreign_helper' show ArrayFlags, HArrayFlagsSet; + +import 'dart:typed_data' show ByteData, Endian; + +@pragma('dart2js:never-inline') +/*member: returnUnmodifiable:function() { + var a = new DataView(new ArrayBuffer(10)); + a.$flags = 3; + return a; +}*/ +returnUnmodifiable() { + final a = ByteData(10); + ByteData b = HArrayFlagsSet(a, ArrayFlags.unmodifiable); + return b; +} + +// Two writes, neither checked. +@pragma('dart2js:never-inline') +/*member: returnModifiable1:function() { + var data = new DataView(new ArrayBuffer(10)); + data.setInt16(0, 200, false); + data.setInt32(4, 200, false); + return data; +}*/ +returnModifiable1() { + final data = ByteData(10); + data.setInt16(0, 200); + data.setInt32(4, 200); + return data; +} + +// Two writes, neither checked. +@pragma('dart2js:never-inline') +/*member: returnModifiable2:function() { + var data = new DataView(new ArrayBuffer(10)); + data.setInt16(0, 200, false); + data.setInt32(4, 200, false); + return data; +}*/ +returnModifiable2() { + final data = ByteData(10); + data.setInt16(0, 200); + data.setInt32(4, 200); + return data; +} + +@pragma('dart2js:never-inline') +/*member: guaranteedFail:function() { + var a = new DataView(new ArrayBuffer(10)); + a.$flags = 3; + A.throwUnsupportedOperation(a, "setInt32"); + a.setInt32(0, 100, false); + a.setUint32(4, 2000, false); + return a; +}*/ +guaranteedFail() { + final a = ByteData(10); + ByteData b = HArrayFlagsSet(a, ArrayFlags.unmodifiable); + b.setInt32(0, 100); + b.setUint32(4, 2000); + return b; +} + +@pragma('dart2js:never-inline') +/*member: multipleWrites:function(data) { + data.$flags & 2 && A.throwUnsupportedOperation(data, "setFloat64"); + data.setFloat64(0, 1.23, false); + data.setFloat32(8, 1.23, false); + return data; +}*/ +multipleWrites(ByteData data) { + // there should only be one write check. + data.setFloat64(0, 1.23); + data.setFloat32(8, 1.23); + return data; +} + +@pragma('dart2js:never-inline') +/*member: hoistedLoad:function(data) { + var t1, i; + for (t1 = data.$flags | 0, i = 0; i < data.byteLength; i += 2) { + t1 & 2 && A.throwUnsupportedOperation(data, "setUint16"); + data.setUint16(i, 100, true); + } + return data; +}*/ +ByteData hoistedLoad(ByteData data) { + // The load of the flags is hoisted, but the check is not. + for (int i = 0; i < data.lengthInBytes; i += 2) { + data.setUint16(i, 100, Endian.little); + } + return data; +} + +@pragma('dart2js:never-inline') +/*member: maybeUnmodifiable:ignore*/ +ByteData maybeUnmodifiable() { + var data = ByteData(100); + if (DateTime.now().millisecondsSinceEpoch == 42) { + data = data.asUnmodifiableView(); + } + return data; +} + +/*member: main:ignore*/ +main() { + print(returnUnmodifiable()); + print(returnModifiable1()); + print(returnModifiable2()); + print(guaranteedFail); + print(multipleWrites(maybeUnmodifiable())); + print(hoistedLoad(maybeUnmodifiable())); +} diff --git a/pkg/compiler/test/codegen/data/unmodifiable_list.dart b/pkg/compiler/test/codegen/data/unmodifiable_list.dart new file mode 100644 index 00000000000..caca8faa22f --- /dev/null +++ b/pkg/compiler/test/codegen/data/unmodifiable_list.dart @@ -0,0 +1,50 @@ +// Copyright (c) 2024, 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. + +@pragma('dart2js:never-inline') +/*spec|canary.member: test1:function(a) { + B.JSArray_methods.$indexSet(a, 5, 1); + B.JSArray_methods.$indexSet(a, 0, 2); + return a; +}*/ +/*prod.member: test1:function(a) { + a.$flags & 2 && A.throwUnsupportedOperation(a); + if (5 >= a.length) + return A.ioore(a, 5); + a[5] = 1; + a[0] = 2; + return a; +}*/ +List test1(List a) { + a[5] = 1; + a[0] = 2; + return a; +} + +@pragma('dart2js:never-inline') +/*member: test2:function(a) { + B.JSArray_methods.add$1(a, 100); + return a; +}*/ +List test2(List a) { + a.add(100); + return a; +} + +@pragma('dart2js:never-inline') +bool isEven(int i) => i.isEven; + +@pragma('dart2js:never-inline') +/*member: maybeUnmodifiable:ignore*/ +List maybeUnmodifiable() { + List d = List.filled(10, 0); + if (DateTime.now().millisecondsSinceEpoch == 42) d = List.unmodifiable(d); + return d; +} + +/*member: main:ignore*/ +main() { + print(test1(maybeUnmodifiable())); + print(test2(maybeUnmodifiable())); +} diff --git a/pkg/compiler/test/codegen/data/unmodifiable_typed_list.dart b/pkg/compiler/test/codegen/data/unmodifiable_typed_list.dart new file mode 100644 index 00000000000..61a9ed82065 --- /dev/null +++ b/pkg/compiler/test/codegen/data/unmodifiable_typed_list.dart @@ -0,0 +1,196 @@ +// Copyright (c) 2024, 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:_foreign_helper' show ArrayFlags, HArrayFlagsSet; + +import 'dart:typed_data'; + +@pragma('dart2js:never-inline') +/*member: returnUnmodifiable:function() { + var a = new Int8Array(10); + a.$flags = 3; + return a; +}*/ +returnUnmodifiable() { + final a = Int8List(10); + Int8List b = HArrayFlagsSet(a, ArrayFlags.unmodifiable); + return b; +} + +// Two writes, neither checked. +@pragma('dart2js:never-inline') +/*member: return200:function() { + var a = new Uint8Array(10); + a[0] = 200; + a[1] = 201; + return a; +}*/ +return200() { + final a = Uint8List(10); + a[0] = 200; + a[1] = 201; + return a; +} + +@pragma('dart2js:never-inline') +/*member: guaranteedFail:function() { + var a = new Uint8Array(10); + a.$flags = 3; + A.throwUnsupportedOperation(a); + a[0] = 200; + a[1] = 201; + return a; +}*/ +guaranteedFail() { + final a = Uint8List(10); + Uint8List b = HArrayFlagsSet(a, ArrayFlags.unmodifiable); + b[0] = 200; + b[1] = 201; + return b; +} + +@pragma('dart2js:never-inline') +/*member: multipleWrites:function() { + var a = A.maybeUnmodifiable(); + a.$flags & 2 && A.throwUnsupportedOperation(a); + if (5 >= a.length) + return A.ioore(a, 5); + a[5] = 100; + a[1] = 200; + return a; +}*/ +multipleWrites() { + final a = maybeUnmodifiable(); + a[5] = 100; + a[1] = 200; // there should only be one write check. + return a; +} + +@pragma('dart2js:never-inline') +/*member: hoistedLoad:function(a) { + var t1, t2, i; + for (t1 = a.length, t2 = a.$flags | 0, i = 0; i < t1; ++i) { + t2 & 2 && A.throwUnsupportedOperation(a); + a[i] = 100; + } + return a; +}*/ +Uint8List hoistedLoad(Uint8List a) { + // The load of the flags is hoisted, but the check is not. + for (int i = 0; i < a.length; i++) { + a[i] = 100; + } + return a; +} + +@pragma('dart2js:never-inline') +/*member: hoistedCheck2:function(a) { + var t2, i, + t1 = a.length; + if (t1 > 0) + for (t2 = a.$flags | 0, i = 0; i < t1; ++i) { + t2 & 2 && A.throwUnsupportedOperation(a); + a[i] = 100; + } + return a; +}*/ +Uint8List hoistedCheck2(Uint8List a) { + if (a.length > 0) { + // We should be able to do better here - the loop has a non-zero minimum + // trip count. + for (int i = 0; i < a.length; i++) { + a[i] = 100; + } + } + return a; +} + +@pragma('dart2js:never-inline') +/*spec|canary.member: hoistedCheck3:function(a) { + var t2, i, + t1 = a.length; + if (t1 > 0) { + a.$flags & 2 && A.throwUnsupportedOperation(a); + a[0] = 100; + for (t2 = a.$flags | 0, i = 1; i < t1; ++i) { + t2 & 2 && A.throwUnsupportedOperation(a); + a[i] = 100; + } + } + return a; +}*/ +/*prod.member: hoistedCheck3:function(a) { + var i, + t1 = a.length; + if (t1 > 0) { + a.$flags & 2 && A.throwUnsupportedOperation(a); + a[0] = 100; + for (i = 1; i < t1; ++i) + a[i] = 100; + } + return a; +}*/ +Uint8List hoistedCheck3(Uint8List a) { + if (a.length > 0) { + a[0] = 100; + // Checks in the loop are removed via simple dominance. + for (int i = 1; i < a.length; i++) { + a[i] = 100; + } + } + return a; +} + +@pragma('dart2js:never-inline') +/*spec|canary.member: list1:function(a) { + B.JSArray_methods.$indexSet(a, 1, 100); + B.JSArray_methods.$indexSet(a, 2, 200); + return a; +}*/ +/*prod.member: list1:function(a) { + var t1; + a.$flags & 2 && A.throwUnsupportedOperation(a); + t1 = a.length; + if (1 >= t1) + return A.ioore(a, 1); + a[1] = 100; + if (2 >= t1) + return A.ioore(a, 2); + a[2] = 200; + return a; +}*/ +List list1(List a) { + a[1] = 100; + a[2] = 200; + return a; +} + +@pragma('dart2js:never-inline') +/*member: maybeUnmodifiable:ignore*/ +Uint8List maybeUnmodifiable() { + var d = Uint8List(10); + if (DateTime.now().millisecondsSinceEpoch == 42) d = d.asUnmodifiableView(); + return d; +} + +@pragma('dart2js:never-inline') +/*member: maybeUnmodifiableList:ignore*/ +List maybeUnmodifiableList() { + var d = List.filled(10, 0); + if (DateTime.now().millisecondsSinceEpoch == 42) d = List.unmodifiable(d); + return d; +} + +/*member: main:ignore*/ +main() { + print(returnUnmodifiable()); + print(return200()); + print(guaranteedFail); + print(multipleWrites()); + print(hoistedLoad(maybeUnmodifiable())); + print(hoistedCheck2(maybeUnmodifiable())); + print(hoistedCheck3(maybeUnmodifiable())); + + print(list1(maybeUnmodifiableList())); +} diff --git a/pkg/compiler/test/optimization/data/index_assign.dart b/pkg/compiler/test/optimization/data/index_assign.dart index 28a23bb51b8..3b7678821d4 100644 --- a/pkg/compiler/test/optimization/data/index_assign.dart +++ b/pkg/compiler/test/optimization/data/index_assign.dart @@ -47,7 +47,8 @@ mutableListDynamicValueIndexAssign(dynamic value) { list[0] = value; } -/*member: immutableListIndexAssign:Specializer=[!IndexAssign]*/ +/*spec.member: immutableListIndexAssign:Specializer=[!IndexAssign]*/ +/*prod.member: immutableListIndexAssign:Specializer=[IndexAssign]*/ @pragma('dart2js:noInline') immutableListIndexAssign() { var list = const [0]; diff --git a/pkg/compiler/test/rti/data/generic_methods_dynamic_05.dart b/pkg/compiler/test/rti/data/generic_methods_dynamic_05.dart index 416d81b9170..2fe2fdbb24f 100644 --- a/pkg/compiler/test/rti/data/generic_methods_dynamic_05.dart +++ b/pkg/compiler/test/rti/data/generic_methods_dynamic_05.dart @@ -6,11 +6,11 @@ import 'package:compiler/src/util/testing.dart'; // Test derived from language/generic_methods_dynamic_test/05 -/*spec.class: global#JSArray:deps=[ArrayIterator,List],explicit=[JSArray,JSArray.E,JSArray],implicit=[JSArray.E],needsArgs,test*/ +/*spec.class: global#JSArray:deps=[ArrayIterator,List],explicit=[JSArray.E,JSArray],implicit=[JSArray.E],needsArgs,test*/ /*prod.class: global#JSArray:deps=[List],implicit=[JSArray.E],needsArgs,test*/ -/*spec.class: global#List:deps=[C.bar,JSArray.markFixedList],explicit=[List,List,List,List?,List],needsArgs,test*/ -/*prod.class: global#List:deps=[C.bar],explicit=[List],needsArgs,test*/ +/*spec.class: global#List:deps=[C.bar,JSArray.markFixedList],explicit=[List,List,List,List,List?,List],needsArgs,test*/ +/*prod.class: global#List:deps=[C.bar],explicit=[List,List],needsArgs,test*/ class A {} diff --git a/pkg/compiler/test/rti/data/list_literal.dart b/pkg/compiler/test/rti/data/list_literal.dart index a7d1ed60d1f..58e938f4774 100644 --- a/pkg/compiler/test/rti/data/list_literal.dart +++ b/pkg/compiler/test/rti/data/list_literal.dart @@ -3,9 +3,9 @@ // BSD-style license that can be found in the LICENSE file. /*spec.class: global#List:deps=[Class.m,JSArray.markFixedList],explicit=[List,List,List,List?,List],needsArgs,test*/ -/*prod.class: global#List:deps=[Class.m],needsArgs,test*/ +/*prod.class: global#List:deps=[Class.m],explicit=[List],needsArgs,test*/ -/*spec.class: global#JSArray:deps=[ArrayIterator,List],explicit=[JSArray,JSArray.E,JSArray],implicit=[JSArray.E],needsArgs,test*/ +/*spec.class: global#JSArray:deps=[ArrayIterator,List],explicit=[JSArray.E,JSArray],implicit=[JSArray.E],needsArgs,test*/ /*prod.class: global#JSArray:deps=[List],implicit=[JSArray.E],needsArgs,test*/ main() { diff --git a/pkg/compiler/test/rti/data/list_to_set.dart b/pkg/compiler/test/rti/data/list_to_set.dart index 1628fdde4c4..646d0f38819 100644 --- a/pkg/compiler/test/rti/data/list_to_set.dart +++ b/pkg/compiler/test/rti/data/list_to_set.dart @@ -3,9 +3,9 @@ // BSD-style license that can be found in the LICENSE file. /*spec.class: global#List:deps=[Class,JSArray.markFixedList],explicit=[List,List,List,List?,List],needsArgs,test*/ -/*prod.class: global#List:deps=[Class],needsArgs,test*/ +/*prod.class: global#List:deps=[Class],explicit=[List],needsArgs,test*/ -/*spec.class: global#JSArray:deps=[ArrayIterator,List],explicit=[JSArray,JSArray.E,JSArray],implicit=[JSArray.E],needsArgs,test*/ +/*spec.class: global#JSArray:deps=[ArrayIterator,List],explicit=[JSArray.E,JSArray],implicit=[JSArray.E],needsArgs,test*/ /*prod.class: global#JSArray:deps=[List],implicit=[JSArray.E],needsArgs,test*/ main() { diff --git a/pkg/compiler/test/rti/data/local_function_list_literal.dart b/pkg/compiler/test/rti/data/local_function_list_literal.dart index 9b3c422c192..0dd686949d3 100644 --- a/pkg/compiler/test/rti/data/local_function_list_literal.dart +++ b/pkg/compiler/test/rti/data/local_function_list_literal.dart @@ -4,7 +4,7 @@ import 'package:compiler/src/util/testing.dart'; -/*spec.class: global#JSArray:deps=[ArrayIterator,List],explicit=[JSArray,JSArray.E,JSArray],implicit=[JSArray.E],needsArgs,test*/ +/*spec.class: global#JSArray:deps=[ArrayIterator,List],explicit=[JSArray.E,JSArray],implicit=[JSArray.E],needsArgs,test*/ /*prod.class: global#JSArray:deps=[List],implicit=[JSArray.E],needsArgs,test*/ @pragma('dart2js:noInline') diff --git a/pkg/js_runtime/lib/synced/array_flags.dart b/pkg/js_runtime/lib/synced/array_flags.dart new file mode 100644 index 00000000000..d8ca41ea66f --- /dev/null +++ b/pkg/js_runtime/lib/synced/array_flags.dart @@ -0,0 +1,63 @@ +// Copyright (c) 2024, 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. + +/// Values for the 'array flags' property that is attached to native JSArray and +/// typed data objects to encode restrictions. +/// +/// We encode the restrictions as two bits - one for potentially growable +/// objects that are restricted to be fixed length, and a second for objects +/// with potentially modifiable contents that are restricted to have +/// unmodifiable contents. We only use three combinations (00 = growable, 01 = +/// fixed-length, 11 = unmodifiable), since the core List class does not have a +/// variant that is an 'append-only' list (would be 10). +/// +/// We use the same scheme for typed data so that the same check works for +/// modifiability checks for `a[i] = v` when `a` is a JSarray or Uint8Array. +/// +/// `const` JSArray values are tagged with an additional bit for future use in +/// avoiding copying. We could also use the `const` bit for unmodifiable typed +/// data for which there in no modifable view of the underlying buffer. +/// +/// Given the absense of append-only lists, we could have used a more +/// numerically compact scheme (0 = growable, 1 = fixed-length, 2 = +/// unmodifiable, 3 = const). This compact scheme requires comparison +/// +/// if (x.flags > 0) +/// +/// rather than mask-testing +/// +/// if (x.flags & 1) +/// +/// The unrestricted flags (0) are usually encoded as a missing property, i.e., +/// `undefined`, which is converted to NaN for '>', but converted to the more +/// comfortable '0' for '&'. We hope that there is less to go potentially go +/// wrong with JavaScript engine slow paths with the bitmask. +class ArrayFlags { + /// Value of array flags that marks a JSArray as being fixed-length. This is + /// not used on typed data since that is always fixed-length. + static const int fixedLength = fixedLengthCheck; + + /// Value of array flags that marks a JSArray or typed-data object as + /// unmodifiable. Includes the 'fixed-length' bit, since all unmodifiable + /// lists are also fixed length. + static const int unmodifiable = fixedLengthCheck | unmodifiableCheck; + + /// Value of array flags that marks a JSArray as a constant (transitively + /// unmodifiable). + static const int constant = + fixedLengthCheck | unmodifiableCheck | constantCheck; + + /// Default value of array flags when there is no flags property. This value + /// is not stored on the flags property. + static const int none = 0; + + /// Bit to check for fixed-length JSArray. + static const int fixedLengthCheck = 1; + + /// Bit to check for unmodifiable JSArray and typed data. + static const int unmodifiableCheck = 2; + + /// Bit to check for constant JSArray. + static const int constantCheck = 4; +} diff --git a/sdk/lib/_internal/js_runtime/lib/foreign_helper.dart b/sdk/lib/_internal/js_runtime/lib/foreign_helper.dart index 75772bd758e..6d01aabc2ba 100644 --- a/sdk/lib/_internal/js_runtime/lib/foreign_helper.dart +++ b/sdk/lib/_internal/js_runtime/lib/foreign_helper.dart @@ -6,6 +6,9 @@ library _foreign_helper; import 'dart:_js_shared_embedded_names' show JsGetName, JsBuiltin; import 'dart:_rti' show Rti; +import 'dart:_js_helper' show throwUnsupportedOperation; + +export 'dart:_array_flags'; /// Emits a JavaScript code fragment parametrized by arguments. /// @@ -300,3 +303,57 @@ external T createJsSentinel(); /// Returns `true` if [value] is the sentinel JavaScript value created through /// [createJsSentinel]. external bool isJsSentinel(dynamic value); + +/// Reads the permission flags from a JSArray, typed data array or typed data +/// ByteData. +/// +/// Corresponds to the SSA instruction with the same name. +external int HArrayFlagsGet(Object array); + +/// Checks the 'array flags' of [array] for permission to modify or grow the +/// array or typed data. Throws if the [arrayFlags] do not permit the +/// operation(s) specified by [checkFlags]. Returns `array`, possibly with a +/// different type that reflects the checked flags. +/// +/// Corresponds to the SSA instruction with the same name. +/// +/// The following is a typical use to check modifiability for `a[i] = 0`. The +/// array flags are checked to see if they prohibit modification. +/// +/// a = ... +/// f = HArrayFlagsGet(a); +/// a2 = HArrayFlagsCheck(a, f, ArrayFlags.unmodifiableCheck, "[]=") +/// a2[i] = 0; +/// +/// It is critical that the modifying operation uses the value returned by +/// HArrayFlagsCheck and not the argument. This data dependency is how we +/// prevent the optimizer from moving the modifying operation before the check. +/// +/// Code should be written with a HArrayFlagsGet / HArrayFlagsCheck pair +/// immediately before each modifying operation. The optimizer will remove +/// unnecessary instructions, but to do so correctly, it requires the Get/Check +/// to be initially in a position where there can be no way for there to be an +/// intervening HArrayFlagsSet that might invalidate the check. +@pragma('dart2js:as:trust') +T HArrayFlagsCheck( + Object array, int arrayFlags, int checkFlags, String operation) { + // This body is unused but serves as a model for global for impacts and + // analysis. + if (arrayFlags & checkFlags != 0) { + throwUnsupportedOperation(array, operation); + } + return array as T; +} + +/// Sets the permission flags on a JSArray or typed data object. Returns +/// `array`, possibly with a different type that reflects the changed flags. +/// +/// Corresponds to the SSA instruction with the same name. +/// +/// Care must be taken when combining HArrayFlagsSet with HArrayFlagsGet and +/// HArrayFlagsCheck. In order to ensure that the most recent version of the +/// flags is used, code must be written in a 'linear' style where the input to +/// HArrayFlagsSet is not used afterwards, only the result. One coding style +/// that ensures a linear pattern is to return the HArrayFlagsSet of something +/// that is allocated within the function and not otherwise stored. +external T HArrayFlagsSet(Object array, int flags); diff --git a/sdk/lib/_internal/js_runtime/lib/interceptors.dart b/sdk/lib/_internal/js_runtime/lib/interceptors.dart index f430efab1f5..71fa1516025 100644 --- a/sdk/lib/_internal/js_runtime/lib/interceptors.dart +++ b/sdk/lib/_internal/js_runtime/lib/interceptors.dart @@ -8,7 +8,15 @@ import 'dart:_js_embedded_names' show DISPATCH_PROPERTY_NAME, TYPE_TO_INTERCEPTOR_MAP; import 'dart:collection' hide LinkedList, LinkedListEntry; -import 'dart:_foreign_helper' show JS_FALSE, JS_GET_FLAG, TYPE_REF; +import 'dart:_foreign_helper' + show + JS_FALSE, + JS_GET_FLAG, + TYPE_REF, + ArrayFlags, + HArrayFlagsGet, + HArrayFlagsSet, + HArrayFlagsCheck; import 'dart:_internal' hide Symbol; import "dart:_internal" as _symbol_dev show Symbol; import 'dart:_js_helper' diff --git a/sdk/lib/_internal/js_runtime/lib/js_array.dart b/sdk/lib/_internal/js_runtime/lib/js_array.dart index 00d4fe09be5..03ccb142f0f 100644 --- a/sdk/lib/_internal/js_runtime/lib/js_array.dart +++ b/sdk/lib/_internal/js_runtime/lib/js_array.dart @@ -36,7 +36,7 @@ class JSArray extends JavaScriptObject implements List, JSIndexable { if (length < 0 || length > maxJSArrayLength) { throw RangeError.range(length, 0, maxJSArrayLength, 'length'); } - return JSArray.markFixed(JS('', 'new Array(#)', length)); + return JSArray.markFixed(JS('JSArray', 'new Array(#)', length)); } /// Returns a fresh JavaScript Array, marked as fixed-length. The Array is @@ -61,7 +61,7 @@ class JSArray extends JavaScriptObject implements List, JSIndexable { if (length < 0 || length > maxJSArrayLength) { throw RangeError.range(length, 0, maxJSArrayLength, 'length'); } - return JSArray.markFixed(JS('', 'new Array(#)', length)); + return JSArray.markFixed(JS('JSArray', 'new Array(#)', length)); } /// Returns a fresh growable JavaScript Array of zero length length. @@ -78,7 +78,7 @@ class JSArray extends JavaScriptObject implements List, JSIndexable { if ((length is! int) || (length < 0)) { throw ArgumentError('Length must be a non-negative integer: $length'); } - return JSArray.markGrowable(JS('', 'new Array(#)', length)); + return JSArray.markGrowable(JS('JSArray', 'new Array(#)', length)); } /// Returns a fresh growable JavaScript Array with initial length. The Array @@ -95,7 +95,7 @@ class JSArray extends JavaScriptObject implements List, JSIndexable { if ((length is! int) || (length < 0)) { throw ArgumentError('Length must be a non-negative integer: $length'); } - return JSArray.markGrowable(JS('', 'new Array(#)', length)); + return JSArray.markGrowable(JS('JSArray', 'new Array(#)', length)); } /// Constructor for adding type parameters to an existing JavaScript Array. @@ -118,29 +118,24 @@ class JSArray extends JavaScriptObject implements List, JSIndexable { factory JSArray.markGrowable(allocation) => JS('JSExtendableArray', '#', JSArray.typed(allocation)); + @pragma('dart2js:prefer-inline') static List markFixedList(List list) { - // Functions are stored in the hidden class and not as properties in - // the object. We never actually look at the value, but only want - // to know if the property exists. - JS('void', r'#.fixed$length = Array', list); - return JS('JSFixedArray', '#', list); + return JS( + 'JSFixedArray', '#', HArrayFlagsSet(list, ArrayFlags.fixedLength)); } + @pragma('dart2js:prefer-inline') static List markUnmodifiableList(List list) { - // Functions are stored in the hidden class and not as properties in - // the object. We never actually look at the value, but only want - // to know if the property exists. - JS('void', r'#.fixed$length = Array', list); - JS('void', r'#.immutable$list = Array', list); - return JS('JSUnmodifiableArray', '#', list); + return JS('JSUnmodifiableArray', '#', + HArrayFlagsSet(list, ArrayFlags.unmodifiable)); } static bool isFixedLength(JSArray a) { - return !JS('bool', r'!#.fixed$length', a); + return HArrayFlagsGet(a) & ArrayFlags.fixedLengthCheck != 0; } static bool isUnmodifiable(JSArray a) { - return !JS('bool', r'!#.immutable$list', a); + return HArrayFlagsGet(a) & ArrayFlags.unmodifiableCheck != 0; } static bool isGrowable(JSArray a) { @@ -152,15 +147,13 @@ class JSArray extends JavaScriptObject implements List, JSIndexable { } checkMutable(String reason) { - if (!isMutable(this)) { - throw UnsupportedError(reason); - } + final int flags = HArrayFlagsGet(this); + HArrayFlagsCheck(this, flags, ArrayFlags.unmodifiableCheck, reason); } checkGrowable(String reason) { - if (!isGrowable(this)) { - throw UnsupportedError(reason); - } + final int flags = HArrayFlagsGet(this); + HArrayFlagsCheck(this, flags, ArrayFlags.fixedLengthCheck, reason); } List cast() => List.castFrom(this); @@ -790,11 +783,14 @@ class JSArray extends JavaScriptObject implements List, JSIndexable { } void operator []=(int index, E value) { - checkMutable('indexed set'); + final int flags = HArrayFlagsGet(this); + final checked = + HArrayFlagsCheck(this, flags, ArrayFlags.unmodifiableCheck, '[]='); + if (index is! int) throw diagnoseIndexError(this, index); // This form of the range test correctly rejects NaN. if (!(index >= 0 && index < length)) throw diagnoseIndexError(this, index); - JS('void', r'#[#] = #', this, index, value); + JS('void', r'#[#] = #', checked, index, value); } Map asMap() { diff --git a/sdk/lib/_internal/js_runtime/lib/js_helper.dart b/sdk/lib/_internal/js_runtime/lib/js_helper.dart index 6f15c9e6128..13d35a88a8a 100644 --- a/sdk/lib/_internal/js_runtime/lib/js_helper.dart +++ b/sdk/lib/_internal/js_runtime/lib/js_helper.dart @@ -36,6 +36,8 @@ import 'dart:_foreign_helper' show DART_CLOSURE_TO_JS, getInterceptor, + ArrayFlags, + HArrayFlagsGet, JS, JS_BUILTIN, JS_CONST, @@ -1249,6 +1251,47 @@ throwUnsupportedError(message) { throw UnsupportedError(message); } +/// Called from code generated for the HArrayFlagsCheck instruction. +/// +/// A missing `operation` argument defaults to '[]='. +Never throwUnsupportedOperation(Object o, [String? operation]) { + // Missing argument is defaulted manually. The calling convention for + // top-level methods is that the call site provides the default values. Since + // the generated code omits the second argument, `undefined` is passed, which + // presents as Dart `null`. + operation ??= '[]='; + final wrapper = JS('', 'Error()'); + throwExpressionWithWrapper( + _diagnoseUnsupportedOperation(o, operation), wrapper); +} + +Error _diagnoseUnsupportedOperation(Object o, String operation) { + String? verb; + String adjective = ''; + String article = 'a'; + String object; + if (o is List) { + object = 'list'; + // TODO(sra): Should we do more of this? + if (operation == 'add' || operation == 'addAll') verb ??= 'add to'; + } else { + object = 'ByteData'; + verb ??= "'$operation' on"; + } + verb ??= 'modify'; + final flags = HArrayFlagsGet(o); + if (flags & ArrayFlags.unmodifiableCheck != 0) { + article = 'an '; + adjective = 'unmodifiable '; + } else { + // No need to test for fixed-length, otherwise we would not be diagnosing + // the error. + article = 'a '; + adjective = 'fixed-length '; + } + return UnsupportedError('Cannot $verb $article$adjective$object'); +} + // This is used in open coded for-in loops on arrays. // // checkConcurrentModificationError(a.length == startLength, a) diff --git a/sdk/lib/_internal/js_runtime/lib/native_typed_data.dart b/sdk/lib/_internal/js_runtime/lib/native_typed_data.dart index 1fb2edb04fb..dd3f0160641 100644 --- a/sdk/lib/_internal/js_runtime/lib/native_typed_data.dart +++ b/sdk/lib/_internal/js_runtime/lib/native_typed_data.dart @@ -8,7 +8,7 @@ library dart.typed_data.implementation; import 'dart:collection' show ListMixin; import 'dart:_internal' show FixedLengthListMixin hide Symbol; -import "dart:_internal" show UnmodifiableListBase; +import "dart:_internal" show UnmodifiableListMixin; import 'dart:_interceptors' show JavaScriptObject, JSIndexable, JSUInt32, JSUInt31; import 'dart:_js_helper' @@ -22,7 +22,9 @@ import 'dart:_js_helper' diagnoseIndexError, diagnoseRangeError, TrustedGetRuntimeType; -import 'dart:_foreign_helper' show JS; + +import 'dart:_foreign_helper' + show JS, ArrayFlags, HArrayFlagsCheck, HArrayFlagsGet, HArrayFlagsSet; import 'dart:math' as Math; @@ -36,31 +38,32 @@ final class NativeByteBuffer extends JavaScriptObject Type get runtimeType => ByteBuffer; - Uint8List asUint8List([int offsetInBytes = 0, int? length]) { + NativeUint8List asUint8List([int offsetInBytes = 0, int? length]) { return NativeUint8List.view(this, offsetInBytes, length); } - Int8List asInt8List([int offsetInBytes = 0, int? length]) { + NativeInt8List asInt8List([int offsetInBytes = 0, int? length]) { return NativeInt8List.view(this, offsetInBytes, length); } - Uint8ClampedList asUint8ClampedList([int offsetInBytes = 0, int? length]) { + NativeUint8ClampedList asUint8ClampedList( + [int offsetInBytes = 0, int? length]) { return NativeUint8ClampedList.view(this, offsetInBytes, length); } - Uint16List asUint16List([int offsetInBytes = 0, int? length]) { + NativeUint16List asUint16List([int offsetInBytes = 0, int? length]) { return NativeUint16List.view(this, offsetInBytes, length); } - Int16List asInt16List([int offsetInBytes = 0, int? length]) { + NativeInt16List asInt16List([int offsetInBytes = 0, int? length]) { return NativeInt16List.view(this, offsetInBytes, length); } - Uint32List asUint32List([int offsetInBytes = 0, int? length]) { + NativeUint32List asUint32List([int offsetInBytes = 0, int? length]) { return NativeUint32List.view(this, offsetInBytes, length); } - Int32List asInt32List([int offsetInBytes = 0, int? length]) { + NativeInt32List asInt32List([int offsetInBytes = 0, int? length]) { return NativeInt32List.view(this, offsetInBytes, length); } @@ -72,33 +75,33 @@ final class NativeByteBuffer extends JavaScriptObject throw UnsupportedError('Int64List not supported by dart2js.'); } - Int32x4List asInt32x4List([int offsetInBytes = 0, int? length]) { + NativeInt32x4List asInt32x4List([int offsetInBytes = 0, int? length]) { length ??= (lengthInBytes - offsetInBytes) ~/ Int32x4List.bytesPerElement; var storage = this.asInt32List(offsetInBytes, length * 4); return NativeInt32x4List._externalStorage(storage); } - Float32List asFloat32List([int offsetInBytes = 0, int? length]) { + NativeFloat32List asFloat32List([int offsetInBytes = 0, int? length]) { return NativeFloat32List.view(this, offsetInBytes, length); } - Float64List asFloat64List([int offsetInBytes = 0, int? length]) { + NativeFloat64List asFloat64List([int offsetInBytes = 0, int? length]) { return NativeFloat64List.view(this, offsetInBytes, length); } - Float32x4List asFloat32x4List([int offsetInBytes = 0, int? length]) { + NativeFloat32x4List asFloat32x4List([int offsetInBytes = 0, int? length]) { length ??= (lengthInBytes - offsetInBytes) ~/ Float32x4List.bytesPerElement; var storage = this.asFloat32List(offsetInBytes, length * 4); return NativeFloat32x4List._externalStorage(storage); } - Float64x2List asFloat64x2List([int offsetInBytes = 0, int? length]) { + NativeFloat64x2List asFloat64x2List([int offsetInBytes = 0, int? length]) { length ??= (lengthInBytes - offsetInBytes) ~/ Float64x2List.bytesPerElement; var storage = this.asFloat64List(offsetInBytes, length * 2); return NativeFloat64x2List._externalStorage(storage); } - ByteData asByteData([int offsetInBytes = 0, int? length]) { + NativeByteData asByteData([int offsetInBytes = 0, int? length]) { return NativeByteData.view(this, offsetInBytes, length); } } @@ -109,7 +112,7 @@ final class NativeByteBuffer extends JavaScriptObject final class NativeFloat32x4List extends Object with ListMixin, FixedLengthListMixin implements Float32x4List, TrustedGetRuntimeType { - final Float32List _storage; + final NativeFloat32List _storage; /// Creates a [Float32x4List] of the specified length (in elements), /// all of whose elements are initially zero. @@ -168,7 +171,8 @@ final class NativeFloat32x4List extends Object _storage[(index * 4) + 3] = value.w; } - Float32x4List asUnmodifiableView() => _UnmodifiableFloat32x4ListView(this); + Float32x4List asUnmodifiableView() => + _UnmodifiableFloat32x4ListView(this._storage); Float32x4List sublist(int start, [int? end]) { var stop = _checkValidRange(start, end, this.length); @@ -177,19 +181,30 @@ final class NativeFloat32x4List extends Object } } +/// View of a [Float32x4List] that disallows modification. +final class _UnmodifiableFloat32x4ListView extends NativeFloat32x4List + with UnmodifiableListMixin { + _UnmodifiableFloat32x4ListView(super._storage) : super._externalStorage(); + + ByteBuffer get buffer => + _UnmodifiableNativeByteBufferView(_storage._nativeBuffer); + + Float32x4List asUnmodifiableView() => this; +} + /// A fixed-length list of Int32x4 numbers that is viewable as a /// [TypedData]. For long lists, this implementation will be considerably more /// space- and time-efficient than the default [List] implementation. final class NativeInt32x4List extends Object with ListMixin, FixedLengthListMixin implements Int32x4List, TrustedGetRuntimeType { - final Int32List _storage; + final NativeInt32List _storage; /// Creates a [Int32x4List] of the specified length (in elements), /// all of whose elements are initially zero. NativeInt32x4List(int length) : _storage = NativeInt32List(length * 4); - NativeInt32x4List._externalStorage(Int32List storage) : _storage = storage; + NativeInt32x4List._externalStorage(this._storage); NativeInt32x4List._slowFromList(List list) : _storage = NativeInt32List(list.length * 4) { @@ -242,7 +257,7 @@ final class NativeInt32x4List extends Object _storage[(index * 4) + 3] = value.w; } - Int32x4List asUnmodifiableView() => _UnmodifiableInt32x4ListView(this); + Int32x4List asUnmodifiableView() => _UnmodifiableInt32x4ListView(_storage); Int32x4List sublist(int start, [int? end]) { var stop = _checkValidRange(start, end, this.length); @@ -251,13 +266,24 @@ final class NativeInt32x4List extends Object } } +/// View of a [Int32x4List] that disallows modification. +final class _UnmodifiableInt32x4ListView extends NativeInt32x4List + with UnmodifiableListMixin { + _UnmodifiableInt32x4ListView(super._storage) : super._externalStorage(); + + ByteBuffer get buffer => + _UnmodifiableNativeByteBufferView(_storage._nativeBuffer); + + Int32x4List asUnmodifiableView() => this; +} + /// A fixed-length list of Float64x2 numbers that is viewable as a /// [TypedData]. For long lists, this implementation will be considerably more /// space- and time-efficient than the default [List] implementation. final class NativeFloat64x2List extends Object with ListMixin, FixedLengthListMixin implements Float64x2List, TrustedGetRuntimeType { - final Float64List _storage; + final NativeFloat64List _storage; /// Creates a [Float64x2List] of the specified length (in elements), /// all of whose elements are initially zero. @@ -310,7 +336,8 @@ final class NativeFloat64x2List extends Object _storage[(index * 2) + 1] = value.y; } - Float64x2List asUnmodifiableView() => _UnmodifiableFloat64x2ListView(this); + Float64x2List asUnmodifiableView() => + _UnmodifiableFloat64x2ListView(this._storage); Float64x2List sublist(int start, [int? end]) { var stop = _checkValidRange(start, end, this.length); @@ -319,12 +346,35 @@ final class NativeFloat64x2List extends Object } } +/// View of a [Float64x2List] that disallows modification. +final class _UnmodifiableFloat64x2ListView extends NativeFloat64x2List + with UnmodifiableListMixin { + _UnmodifiableFloat64x2ListView(super._storage) : super._externalStorage(); + + ByteBuffer get buffer => + _UnmodifiableNativeByteBufferView(_storage._nativeBuffer); + + Float64x2List asUnmodifiableView() => this; +} + @Native('ArrayBufferView') final class NativeTypedData extends JavaScriptObject implements TypedData { /// Returns the byte buffer associated with this object. + ByteBuffer get buffer { + if (_isUnmodifiable()) { + return _UnmodifiableNativeByteBufferView(_nativeBuffer); + } else { + // TODO(sra): Consider always wrapping the ArrayBuffer - this would make + // user-code accesses monomorphic in the ByteBuffer implementation, but + // might cause interop problems, e.g., with `postMessage`. + return _nativeBuffer; + } + } + @Creates('NativeByteBuffer') @Returns('NativeByteBuffer') - ByteBuffer get buffer native; + @JSName('buffer') + NativeByteBuffer get _nativeBuffer native; /// Returns the length of this view, in bytes. @JSName('byteLength') @@ -339,6 +389,10 @@ final class NativeTypedData extends JavaScriptObject implements TypedData { @JSName('BYTES_PER_ELEMENT') int get elementSizeInBytes native; + bool _isUnmodifiable() { + return HArrayFlagsGet(this) & ArrayFlags.unmodifiableCheck != 0; + } + void _invalidPosition(int position, int length, String name) { if (position is! int) { throw ArgumentError.value(position, name, 'Invalid list position'); @@ -354,6 +408,95 @@ final class NativeTypedData extends JavaScriptObject implements TypedData { _invalidPosition(position, length, name); } } + + @pragma('dart2js:prefer-inline') + void _checkMutable(String operation) { + HArrayFlagsCheck( + this, HArrayFlagsGet(this), ArrayFlags.unmodifiableCheck, operation); + } +} + +/// A view of a ByteBuffer (ArrayBuffer) that yields only unmodifiable views. +/// +/// The underlying ByteBuffer may have both modifiable and unmodifiable views. +final class _UnmodifiableNativeByteBufferView implements ByteBuffer { + final NativeByteBuffer _data; + + _UnmodifiableNativeByteBufferView(this._data); + + int get lengthInBytes => _data.lengthInBytes; + + NativeUint8List asUint8List([int offsetInBytes = 0, int? length]) { + final result = _data.asUint8List(offsetInBytes, length); + return HArrayFlagsSet(result, ArrayFlags.unmodifiable); + } + + Int8List asInt8List([int offsetInBytes = 0, int? length]) { + final result = _data.asInt8List(offsetInBytes, length); + return HArrayFlagsSet(result, ArrayFlags.unmodifiable); + } + + Uint8ClampedList asUint8ClampedList([int offsetInBytes = 0, int? length]) { + final result = _data.asUint8ClampedList(offsetInBytes, length); + return HArrayFlagsSet(result, ArrayFlags.unmodifiable); + } + + Uint16List asUint16List([int offsetInBytes = 0, int? length]) { + final result = _data.asUint16List(offsetInBytes, length); + return HArrayFlagsSet(result, ArrayFlags.unmodifiable); + } + + Int16List asInt16List([int offsetInBytes = 0, int? length]) { + final result = _data.asInt16List(offsetInBytes, length); + return HArrayFlagsSet(result, ArrayFlags.unmodifiable); + } + + Uint32List asUint32List([int offsetInBytes = 0, int? length]) { + final result = _data.asUint32List(offsetInBytes, length); + return HArrayFlagsSet(result, ArrayFlags.unmodifiable); + } + + Int32List asInt32List([int offsetInBytes = 0, int? length]) { + final result = _data.asInt32List(offsetInBytes, length); + return HArrayFlagsSet(result, ArrayFlags.unmodifiable); + } + + Uint64List asUint64List([int offsetInBytes = 0, int? length]) { + final result = _data.asUint64List(offsetInBytes, length); + return HArrayFlagsSet(result, ArrayFlags.unmodifiable); + } + + Int64List asInt64List([int offsetInBytes = 0, int? length]) { + final result = _data.asInt64List(offsetInBytes, length); + return HArrayFlagsSet(result, ArrayFlags.unmodifiable); + } + + Int32x4List asInt32x4List([int offsetInBytes = 0, int? length]) { + return _data.asInt32x4List(offsetInBytes, length).asUnmodifiableView(); + } + + Float32List asFloat32List([int offsetInBytes = 0, int? length]) { + final result = _data.asFloat32List(offsetInBytes, length); + return HArrayFlagsSet(result, ArrayFlags.unmodifiable); + } + + Float64List asFloat64List([int offsetInBytes = 0, int? length]) { + final result = _data.asFloat64List(offsetInBytes, length); + return HArrayFlagsSet(result, ArrayFlags.unmodifiable); + } + + Float32x4List asFloat32x4List([int offsetInBytes = 0, int? length]) { + return _data.asFloat32x4List(offsetInBytes, length).asUnmodifiableView(); + } + + Float64x2List asFloat64x2List([int offsetInBytes = 0, int? length]) { + return _data.asFloat64x2List(offsetInBytes, length).asUnmodifiableView(); + } + + ByteData asByteData([int offsetInBytes = 0, int? length]) { + final result = _data.asByteData(offsetInBytes, length); + return HArrayFlagsSet(result, ArrayFlags.unmodifiable); + } } // Validates the unnamed constructor length argument. Checking is necessary @@ -420,7 +563,11 @@ final class NativeByteData extends NativeTypedData int get elementSizeInBytes => 1; - ByteData asUnmodifiableView() => _UnmodifiableByteDataView(this); + ByteData asUnmodifiableView() { + if (_isUnmodifiable()) return this; + return HArrayFlagsSet(_create3(_nativeBuffer, offsetInBytes, lengthInBytes), + ArrayFlags.unmodifiable); + } /// Returns the floating point number represented by the four bytes at /// the specified [byteOffset] in this object, in IEEE 754 @@ -433,6 +580,7 @@ final class NativeByteData extends NativeTypedData @JSName('getFloat32') @Returns('double') + @pragma('dart2js:parameter:trust') // TODO(https://dartbug.com/56062): remove. double _getFloat32(int byteOffset, [bool? littleEndian]) native; /// Returns the floating point number represented by the eight bytes at @@ -446,6 +594,7 @@ final class NativeByteData extends NativeTypedData @JSName('getFloat64') @Returns('double') + @pragma('dart2js:parameter:trust') // TODO(https://dartbug.com/56062): remove. double _getFloat64(int byteOffset, [bool? littleEndian]) native; /// Returns the (possibly negative) integer represented by the two bytes at @@ -461,6 +610,7 @@ final class NativeByteData extends NativeTypedData @JSName('getInt16') @Returns('int') + @pragma('dart2js:parameter:trust') // TODO(https://dartbug.com/56062): remove. int _getInt16(int byteOffset, [bool? littleEndian]) native; /// Returns the (possibly negative) integer represented by the four bytes at @@ -476,6 +626,7 @@ final class NativeByteData extends NativeTypedData @JSName('getInt32') @Returns('int') + @pragma('dart2js:parameter:trust') // TODO(https://dartbug.com/56062): remove. int _getInt32(int byteOffset, [bool? littleEndian]) native; /// Returns the (possibly negative) integer represented by the eight bytes at @@ -510,6 +661,7 @@ final class NativeByteData extends NativeTypedData @JSName('getUint16') @Returns('JSUInt31') + @pragma('dart2js:parameter:trust') // TODO(https://dartbug.com/56062): remove. int _getUint16(int byteOffset, [bool? littleEndian]) native; /// Returns the positive integer represented by the four bytes starting @@ -524,6 +676,7 @@ final class NativeByteData extends NativeTypedData @JSName('getUint32') @Returns('JSUInt32') + @pragma('dart2js:parameter:trust') // TODO(https://dartbug.com/56062): remove. int _getUint32(int byteOffset, [bool? littleEndian]) native; /// Returns the positive integer represented by the eight bytes starting @@ -560,10 +713,14 @@ final class NativeByteData extends NativeTypedData /// /// The [byteOffset] must be non-negative, and /// `byteOffset + 4` must be less than or equal to the length of this object. - void setFloat32(int byteOffset, num value, [Endian endian = Endian.big]) => - _setFloat32(byteOffset, value, Endian.little == endian); + @pragma('dart2js:prefer-inline') + void setFloat32(int byteOffset, num value, [Endian endian = Endian.big]) { + _checkMutable('setFloat32'); + _setFloat32(byteOffset, value, Endian.little == endian); + } @JSName('setFloat32') + @pragma('dart2js:parameter:trust') // TODO(https://dartbug.com/56062): remove. void _setFloat32(int byteOffset, num value, [bool? littleEndian]) native; /// Sets the eight bytes starting at the specified [byteOffset] in this @@ -572,10 +729,14 @@ final class NativeByteData extends NativeTypedData /// /// The [byteOffset] must be non-negative, and /// `byteOffset + 8` must be less than or equal to the length of this object. - void setFloat64(int byteOffset, num value, [Endian endian = Endian.big]) => - _setFloat64(byteOffset, value, Endian.little == endian); + @pragma('dart2js:prefer-inline') + void setFloat64(int byteOffset, num value, [Endian endian = Endian.big]) { + _checkMutable('setFloat64'); + _setFloat64(byteOffset, value, Endian.little == endian); + } @JSName('setFloat64') + @pragma('dart2js:parameter:trust') // TODO(https://dartbug.com/56062): remove. void _setFloat64(int byteOffset, num value, [bool? littleEndian]) native; /// Sets the two bytes starting at the specified [byteOffset] in this @@ -585,10 +746,14 @@ final class NativeByteData extends NativeTypedData /// /// The [byteOffset] must be non-negative, and /// `byteOffset + 2` must be less than or equal to the length of this object. - void setInt16(int byteOffset, int value, [Endian endian = Endian.big]) => - _setInt16(byteOffset, value, Endian.little == endian); + @pragma('dart2js:prefer-inline') + void setInt16(int byteOffset, int value, [Endian endian = Endian.big]) { + _checkMutable('setInt16'); + _setInt16(byteOffset, value, Endian.little == endian); + } @JSName('setInt16') + @pragma('dart2js:parameter:trust') // TODO(https://dartbug.com/56062): remove. void _setInt16(int byteOffset, int value, [bool? littleEndian]) native; /// Sets the four bytes starting at the specified [byteOffset] in this @@ -598,10 +763,14 @@ final class NativeByteData extends NativeTypedData /// /// The [byteOffset] must be non-negative, and /// `byteOffset + 4` must be less than or equal to the length of this object. - void setInt32(int byteOffset, int value, [Endian endian = Endian.big]) => - _setInt32(byteOffset, value, Endian.little == endian); + @pragma('dart2js:prefer-inline') + void setInt32(int byteOffset, int value, [Endian endian = Endian.big]) { + _checkMutable('setInt32'); + _setInt32(byteOffset, value, Endian.little == endian); + } @JSName('setInt32') + @pragma('dart2js:parameter:trust') // TODO(https://dartbug.com/56062): remove. void _setInt32(int byteOffset, int value, [bool? littleEndian]) native; /// Sets the eight bytes starting at the specified [byteOffset] in this @@ -622,7 +791,15 @@ final class NativeByteData extends NativeTypedData /// /// The [byteOffset] must be non-negative, and /// less than the length of this object. - void setInt8(int byteOffset, int value) native; + @pragma('dart2js:prefer-inline') + void setInt8(int byteOffset, int value) { + _checkMutable('setInt8'); + _setInt8(byteOffset, value); + } + + @JSName('setInt8') + @pragma('dart2js:parameter:trust') // TODO(https://dartbug.com/56062): remove. + void _setInt8(int byteOffset, int value) native; /// Sets the two bytes starting at the specified [byteOffset] in this object /// to the unsigned binary representation of the specified [value], @@ -631,10 +808,14 @@ final class NativeByteData extends NativeTypedData /// /// The [byteOffset] must be non-negative, and /// `byteOffset + 2` must be less than or equal to the length of this object. - void setUint16(int byteOffset, int value, [Endian endian = Endian.big]) => - _setUint16(byteOffset, value, Endian.little == endian); + @pragma('dart2js:prefer-inline') + void setUint16(int byteOffset, int value, [Endian endian = Endian.big]) { + _checkMutable('setUint16'); + _setUint16(byteOffset, value, Endian.little == endian); + } @JSName('setUint16') + @pragma('dart2js:parameter:trust') // TODO(https://dartbug.com/56062): remove. void _setUint16(int byteOffset, int value, [bool? littleEndian]) native; /// Sets the four bytes starting at the specified [byteOffset] in this object @@ -644,10 +825,14 @@ final class NativeByteData extends NativeTypedData /// /// The [byteOffset] must be non-negative, and /// `byteOffset + 4` must be less than or equal to the length of this object. - void setUint32(int byteOffset, int value, [Endian endian = Endian.big]) => - _setUint32(byteOffset, value, Endian.little == endian); + @pragma('dart2js:prefer-inline') + void setUint32(int byteOffset, int value, [Endian endian = Endian.big]) { + _checkMutable('setUint32'); + _setUint32(byteOffset, value, Endian.little == endian); + } @JSName('setUint32') + @pragma('dart2js:parameter:trust') // TODO(https://dartbug.com/56062): remove. void _setUint32(int byteOffset, int value, [bool? littleEndian]) native; /// Sets the eight bytes starting at the specified [byteOffset] in this object @@ -668,10 +853,20 @@ final class NativeByteData extends NativeTypedData /// /// The [byteOffset] must be non-negative, and /// less than the length of this object. - void setUint8(int byteOffset, int value) native; + @pragma('dart2js:prefer-inline') + void setUint8(int byteOffset, int value) { + _checkMutable('setUint8'); + _setUint8(byteOffset, value); + } - static NativeByteData _create1(arg) => - JS('NativeByteData', 'new DataView(new ArrayBuffer(#))', arg); + @JSName('setUint8') + @pragma('dart2js:parameter:trust') // TODO(https://dartbug.com/56062): remove. + void _setUint8(int byteOffset, int value) native; + + static NativeByteData _create1(arg) => JS( + 'returns:NativeByteData;effects:none;depends:none;new:true', + 'new DataView(new ArrayBuffer(#))', + arg); static NativeByteData _create2(arg1, arg2) => JS('NativeByteData', 'new DataView(#, #)', arg1, arg2); @@ -715,12 +910,14 @@ abstract final class NativeTypedArrayOfDouble extends NativeTypedArray } void operator []=(int index, double value) { + _checkMutable('[]='); _checkValidIndex(index, this, this.length); JS('void', '#[#] = #', this, index, value); } void setRange(int start, int end, Iterable iterable, [int skipCount = 0]) { + _checkMutable('setRange'); if (iterable is NativeTypedArrayOfDouble) { _setRangeFast(start, end, iterable, skipCount); return; @@ -736,12 +933,14 @@ abstract final class NativeTypedArrayOfInt extends NativeTypedArray // types void operator []=(int index, int value) { + _checkMutable('[]='); _checkValidIndex(index, this, this.length); JS('void', '#[#] = #', this, index, value); } void setRange(int start, int end, Iterable iterable, [int skipCount = 0]) { + _checkMutable('setRange'); if (iterable is NativeTypedArrayOfInt) { _setRangeFast(start, end, iterable, skipCount); return; @@ -768,9 +967,13 @@ final class NativeFloat32List extends NativeTypedArrayOfDouble Type get runtimeType => Float32List; - Float32List asUnmodifiableView() => _UnmodifiableFloat32ListView(this); + Float32List asUnmodifiableView() { + if (_isUnmodifiable()) return this; + return HArrayFlagsSet(_create3(_nativeBuffer, offsetInBytes, length), + ArrayFlags.unmodifiable); + } - Float32List sublist(int start, [int? end]) { + NativeFloat32List sublist(int start, [int? end]) { var stop = _checkValidRange(start, end, this.length); var source = JS('NativeFloat32List', '#.subarray(#, #)', this, start, stop); return _create1(source); @@ -806,9 +1009,13 @@ final class NativeFloat64List extends NativeTypedArrayOfDouble Type get runtimeType => Float64List; - Float64List asUnmodifiableView() => _UnmodifiableFloat64ListView(this); + Float64List asUnmodifiableView() { + if (_isUnmodifiable()) return this; + return HArrayFlagsSet(_create3(_nativeBuffer, offsetInBytes, length), + ArrayFlags.unmodifiable); + } - Float64List sublist(int start, [int? end]) { + NativeFloat64List sublist(int start, [int? end]) { var stop = _checkValidRange(start, end, this.length); var source = JS('NativeFloat64List', '#.subarray(#, #)', this, start, stop); return _create1(source); @@ -849,9 +1056,13 @@ final class NativeInt16List extends NativeTypedArrayOfInt return JS('int', '#[#]', this, index); } - Int16List asUnmodifiableView() => _UnmodifiableInt16ListView(this); + Int16List asUnmodifiableView() { + if (_isUnmodifiable()) return this; + return HArrayFlagsSet(_create3(_nativeBuffer, offsetInBytes, length), + ArrayFlags.unmodifiable); + } - Int16List sublist(int start, [int? end]) { + NativeInt16List sublist(int start, [int? end]) { var stop = _checkValidRange(start, end, this.length); var source = JS('NativeInt16List', '#.subarray(#, #)', this, start, stop); return _create1(source); @@ -892,9 +1103,13 @@ final class NativeInt32List extends NativeTypedArrayOfInt return JS('int', '#[#]', this, index); } - Int32List asUnmodifiableView() => _UnmodifiableInt32ListView(this); + Int32List asUnmodifiableView() { + if (_isUnmodifiable()) return this; + return HArrayFlagsSet(_create3(_nativeBuffer, offsetInBytes, length), + ArrayFlags.unmodifiable); + } - Int32List sublist(int start, [int? end]) { + NativeInt32List sublist(int start, [int? end]) { var stop = _checkValidRange(start, end, this.length); var source = JS('NativeInt32List', '#.subarray(#, #)', this, start, stop); return _create1(source); @@ -935,9 +1150,13 @@ final class NativeInt8List extends NativeTypedArrayOfInt return JS('int', '#[#]', this, index); } - Int8List asUnmodifiableView() => _UnmodifiableInt8ListView(this); + Int8List asUnmodifiableView() { + if (_isUnmodifiable()) return this; + return HArrayFlagsSet(_create3(_nativeBuffer, offsetInBytes, length), + ArrayFlags.unmodifiable); + } - Int8List sublist(int start, [int? end]) { + NativeInt8List sublist(int start, [int? end]) { var stop = _checkValidRange(start, end, this.length); var source = JS('NativeInt8List', '#.subarray(#, #)', this, start, stop); return _create1(source); @@ -981,9 +1200,13 @@ final class NativeUint16List extends NativeTypedArrayOfInt return JS('JSUInt31', '#[#]', this, index); } - Uint16List asUnmodifiableView() => _UnmodifiableUint16ListView(this); + Uint16List asUnmodifiableView() { + if (_isUnmodifiable()) return this; + return HArrayFlagsSet(_create3(_nativeBuffer, offsetInBytes, length), + ArrayFlags.unmodifiable); + } - Uint16List sublist(int start, [int? end]) { + NativeUint16List sublist(int start, [int? end]) { var stop = _checkValidRange(start, end, this.length); var source = JS('NativeUint16List', '#.subarray(#, #)', this, start, stop); return _create1(source); @@ -1024,9 +1247,13 @@ final class NativeUint32List extends NativeTypedArrayOfInt return JS('JSUInt32', '#[#]', this, index); } - Uint32List asUnmodifiableView() => _UnmodifiableUint32ListView(this); + Uint32List asUnmodifiableView() { + if (_isUnmodifiable()) return this; + return HArrayFlagsSet(_create3(_nativeBuffer, offsetInBytes, length), + ArrayFlags.unmodifiable); + } - Uint32List sublist(int start, [int? end]) { + NativeUint32List sublist(int start, [int? end]) { var stop = _checkValidRange(start, end, this.length); var source = JS('NativeUint32List', '#.subarray(#, #)', this, start, stop); return _create1(source); @@ -1070,10 +1297,13 @@ final class NativeUint8ClampedList extends NativeTypedArrayOfInt return JS('JSUInt31', '#[#]', this, index); } - Uint8ClampedList asUnmodifiableView() => - _UnmodifiableUint8ClampedListView(this); + Uint8ClampedList asUnmodifiableView() { + if (_isUnmodifiable()) return this; + return HArrayFlagsSet(_create3(_nativeBuffer, offsetInBytes, length), + ArrayFlags.unmodifiable); + } - Uint8ClampedList sublist(int start, [int? end]) { + NativeUint8ClampedList sublist(int start, [int? end]) { var stop = _checkValidRange(start, end, this.length); var source = JS('NativeUint8ClampedList', '#.subarray(#, #)', this, start, stop); @@ -1128,9 +1358,13 @@ final class NativeUint8List extends NativeTypedArrayOfInt return JS('JSUInt31', '#[#]', this, index); } - Uint8List asUnmodifiableView() => _UnmodifiableUint8ListView(this); + Uint8List asUnmodifiableView() { + if (_isUnmodifiable()) return this; + return HArrayFlagsSet(_create3(_nativeBuffer, offsetInBytes, length), + ArrayFlags.unmodifiable); + } - Uint8List sublist(int start, [int? end]) { + NativeUint8List sublist(int start, [int? end]) { var stop = _checkValidRange(start, end, this.length); var source = JS('NativeUint8List', '#.subarray(#, #)', this, start, stop); return _create1(source); @@ -1888,335 +2122,3 @@ int _checkValidRange(int start, int? end, int length) { if (end == null) return length; return end; } - -/// A read-only view of a [ByteBuffer]. -final class _UnmodifiableByteBufferView implements ByteBuffer { - final ByteBuffer _data; - - _UnmodifiableByteBufferView(ByteBuffer data) : _data = data; - - int get lengthInBytes => _data.lengthInBytes; - - Uint8List asUint8List([int offsetInBytes = 0, int? length]) => - _UnmodifiableUint8ListView(_data.asUint8List(offsetInBytes, length)); - - Int8List asInt8List([int offsetInBytes = 0, int? length]) => - _UnmodifiableInt8ListView(_data.asInt8List(offsetInBytes, length)); - - Uint8ClampedList asUint8ClampedList([int offsetInBytes = 0, int? length]) => - _UnmodifiableUint8ClampedListView( - _data.asUint8ClampedList(offsetInBytes, length)); - - Uint16List asUint16List([int offsetInBytes = 0, int? length]) => - _UnmodifiableUint16ListView(_data.asUint16List(offsetInBytes, length)); - - Int16List asInt16List([int offsetInBytes = 0, int? length]) => - _UnmodifiableInt16ListView(_data.asInt16List(offsetInBytes, length)); - - Uint32List asUint32List([int offsetInBytes = 0, int? length]) => - _UnmodifiableUint32ListView(_data.asUint32List(offsetInBytes, length)); - - Int32List asInt32List([int offsetInBytes = 0, int? length]) => - _UnmodifiableInt32ListView(_data.asInt32List(offsetInBytes, length)); - - Uint64List asUint64List([int offsetInBytes = 0, int? length]) => - _UnmodifiableUint64ListView(_data.asUint64List(offsetInBytes, length)); - - Int64List asInt64List([int offsetInBytes = 0, int? length]) => - _UnmodifiableInt64ListView(_data.asInt64List(offsetInBytes, length)); - - Int32x4List asInt32x4List([int offsetInBytes = 0, int? length]) => - _UnmodifiableInt32x4ListView(_data.asInt32x4List(offsetInBytes, length)); - - Float32List asFloat32List([int offsetInBytes = 0, int? length]) => - _UnmodifiableFloat32ListView(_data.asFloat32List(offsetInBytes, length)); - - Float64List asFloat64List([int offsetInBytes = 0, int? length]) => - _UnmodifiableFloat64ListView(_data.asFloat64List(offsetInBytes, length)); - - Float32x4List asFloat32x4List([int offsetInBytes = 0, int? length]) => - _UnmodifiableFloat32x4ListView( - _data.asFloat32x4List(offsetInBytes, length)); - - Float64x2List asFloat64x2List([int offsetInBytes = 0, int? length]) => - _UnmodifiableFloat64x2ListView( - _data.asFloat64x2List(offsetInBytes, length)); - - ByteData asByteData([int offsetInBytes = 0, int? length]) => - _UnmodifiableByteDataView(_data.asByteData(offsetInBytes, length)); -} - -/// A read-only view of a [ByteData]. -final class _UnmodifiableByteDataView implements ByteData { - final ByteData _data; - - _UnmodifiableByteDataView(ByteData data) : _data = data; - - ByteData asUnmodifiableView() => this; - - int getInt8(int byteOffset) => _data.getInt8(byteOffset); - - void setInt8(int byteOffset, int value) => _unsupported(); - - int getUint8(int byteOffset) => _data.getUint8(byteOffset); - - void setUint8(int byteOffset, int value) => _unsupported(); - - int getInt16(int byteOffset, [Endian endian = Endian.big]) => - _data.getInt16(byteOffset, endian); - - void setInt16(int byteOffset, int value, [Endian endian = Endian.big]) => - _unsupported(); - - int getUint16(int byteOffset, [Endian endian = Endian.big]) => - _data.getUint16(byteOffset, endian); - - void setUint16(int byteOffset, int value, [Endian endian = Endian.big]) => - _unsupported(); - - int getInt32(int byteOffset, [Endian endian = Endian.big]) => - _data.getInt32(byteOffset, endian); - - void setInt32(int byteOffset, int value, [Endian endian = Endian.big]) => - _unsupported(); - - int getUint32(int byteOffset, [Endian endian = Endian.big]) => - _data.getUint32(byteOffset, endian); - - void setUint32(int byteOffset, int value, [Endian endian = Endian.big]) => - _unsupported(); - - int getInt64(int byteOffset, [Endian endian = Endian.big]) => - _data.getInt64(byteOffset, endian); - - void setInt64(int byteOffset, int value, [Endian endian = Endian.big]) => - _unsupported(); - - int getUint64(int byteOffset, [Endian endian = Endian.big]) => - _data.getUint64(byteOffset, endian); - - void setUint64(int byteOffset, int value, [Endian endian = Endian.big]) => - _unsupported(); - - double getFloat32(int byteOffset, [Endian endian = Endian.big]) => - _data.getFloat32(byteOffset, endian); - - void setFloat32(int byteOffset, double value, [Endian endian = Endian.big]) => - _unsupported(); - - double getFloat64(int byteOffset, [Endian endian = Endian.big]) => - _data.getFloat64(byteOffset, endian); - - void setFloat64(int byteOffset, double value, [Endian endian = Endian.big]) => - _unsupported(); - - int get elementSizeInBytes => _data.elementSizeInBytes; - - int get offsetInBytes => _data.offsetInBytes; - - int get lengthInBytes => _data.lengthInBytes; - - ByteBuffer get buffer => _UnmodifiableByteBufferView(_data.buffer); - - void _unsupported() { - throw UnsupportedError("An UnmodifiableByteDataView may not be modified"); - } -} - -mixin _UnmodifiableListMixin, TD extends TypedData> { - L get _list; - TD get _data => (_list as TD); - - int get length => _list.length; - - N operator [](int index) => _list[index]; - - int get elementSizeInBytes => _data.elementSizeInBytes; - - int get offsetInBytes => _data.offsetInBytes; - - int get lengthInBytes => _data.lengthInBytes; - - ByteBuffer get buffer => _UnmodifiableByteBufferView(_data.buffer); - - L _createList(int length); - - L sublist(int start, [int? end]) { - // NNBD: Spurious error at `end`, `checkValidRange` is legacy. - int endIndex = RangeError.checkValidRange(start, end!, length); - int sublistLength = endIndex - start; - L result = _createList(sublistLength); - result.setRange(0, sublistLength, _list, start); - return result; - } -} - -/// View of a [Uint8List] that disallows modification. -final class _UnmodifiableUint8ListView extends UnmodifiableListBase - with _UnmodifiableListMixin - implements Uint8List { - final Uint8List _list; - _UnmodifiableUint8ListView(Uint8List list) : _list = list; - - Uint8List asUnmodifiableView() => this; - - Uint8List _createList(int length) => Uint8List(length); -} - -/// View of a [Int8List] that disallows modification. -final class _UnmodifiableInt8ListView extends UnmodifiableListBase - with _UnmodifiableListMixin - implements Int8List { - final Int8List _list; - _UnmodifiableInt8ListView(Int8List list) : _list = list; - - Int8List asUnmodifiableView() => this; - - Int8List _createList(int length) => Int8List(length); -} - -/// View of a [Uint8ClampedList] that disallows modification. -final class _UnmodifiableUint8ClampedListView extends UnmodifiableListBase - with _UnmodifiableListMixin - implements Uint8ClampedList { - final Uint8ClampedList _list; - _UnmodifiableUint8ClampedListView(Uint8ClampedList list) : _list = list; - - Uint8ClampedList asUnmodifiableView() => this; - - Uint8ClampedList _createList(int length) => Uint8ClampedList(length); -} - -/// View of a [Uint16List] that disallows modification. -final class _UnmodifiableUint16ListView extends UnmodifiableListBase - with _UnmodifiableListMixin - implements Uint16List { - final Uint16List _list; - _UnmodifiableUint16ListView(Uint16List list) : _list = list; - - Uint16List asUnmodifiableView() => this; - - Uint16List _createList(int length) => Uint16List(length); -} - -/// View of a [Int16List] that disallows modification. -final class _UnmodifiableInt16ListView extends UnmodifiableListBase - with _UnmodifiableListMixin - implements Int16List { - final Int16List _list; - _UnmodifiableInt16ListView(Int16List list) : _list = list; - - Int16List asUnmodifiableView() => this; - - Int16List _createList(int length) => Int16List(length); -} - -/// View of a [Uint32List] that disallows modification. -final class _UnmodifiableUint32ListView extends UnmodifiableListBase - with _UnmodifiableListMixin - implements Uint32List { - final Uint32List _list; - _UnmodifiableUint32ListView(Uint32List list) : _list = list; - - Uint32List asUnmodifiableView() => this; - - Uint32List _createList(int length) => Uint32List(length); -} - -/// View of a [Int32List] that disallows modification. -final class _UnmodifiableInt32ListView extends UnmodifiableListBase - with _UnmodifiableListMixin - implements Int32List { - final Int32List _list; - _UnmodifiableInt32ListView(Int32List list) : _list = list; - - Int32List asUnmodifiableView() => this; - - Int32List _createList(int length) => Int32List(length); -} - -/// View of a [Uint64List] that disallows modification. -final class _UnmodifiableUint64ListView extends UnmodifiableListBase - with _UnmodifiableListMixin - implements Uint64List { - final Uint64List _list; - _UnmodifiableUint64ListView(Uint64List list) : _list = list; - - Uint64List asUnmodifiableView() => this; - - Uint64List _createList(int length) => Uint64List(length); -} - -/// View of a [Int64List] that disallows modification. -final class _UnmodifiableInt64ListView extends UnmodifiableListBase - with _UnmodifiableListMixin - implements Int64List { - final Int64List _list; - _UnmodifiableInt64ListView(Int64List list) : _list = list; - - Int64List asUnmodifiableView() => this; - - Int64List _createList(int length) => Int64List(length); -} - -/// View of a [Int32x4List] that disallows modification. -final class _UnmodifiableInt32x4ListView extends UnmodifiableListBase - with _UnmodifiableListMixin - implements Int32x4List { - final Int32x4List _list; - _UnmodifiableInt32x4ListView(Int32x4List list) : _list = list; - - Int32x4List asUnmodifiableView() => this; - - Int32x4List _createList(int length) => Int32x4List(length); -} - -/// View of a [Float32x4List] that disallows modification. -final class _UnmodifiableFloat32x4ListView - extends UnmodifiableListBase - with _UnmodifiableListMixin - implements Float32x4List { - final Float32x4List _list; - _UnmodifiableFloat32x4ListView(Float32x4List list) : _list = list; - - Float32x4List asUnmodifiableView() => this; - - Float32x4List _createList(int length) => Float32x4List(length); -} - -/// View of a [Float64x2List] that disallows modification. -final class _UnmodifiableFloat64x2ListView - extends UnmodifiableListBase - with _UnmodifiableListMixin - implements Float64x2List { - final Float64x2List _list; - _UnmodifiableFloat64x2ListView(Float64x2List list) : _list = list; - - Float64x2List asUnmodifiableView() => this; - - Float64x2List _createList(int length) => Float64x2List(length); -} - -/// View of a [Float32List] that disallows modification. -final class _UnmodifiableFloat32ListView extends UnmodifiableListBase - with _UnmodifiableListMixin - implements Float32List { - final Float32List _list; - _UnmodifiableFloat32ListView(Float32List list) : _list = list; - - Float32List asUnmodifiableView() => this; - - Float32List _createList(int length) => Float32List(length); -} - -/// View of a [Float64List] that disallows modification. -final class _UnmodifiableFloat64ListView extends UnmodifiableListBase - with _UnmodifiableListMixin - implements Float64List { - final Float64List _list; - _UnmodifiableFloat64ListView(Float64List list) : _list = list; - - Float64List asUnmodifiableView() => this; - - Float64List _createList(int length) => Float64List(length); -} diff --git a/sdk/lib/_internal/js_runtime/lib/synced/array_flags.dart b/sdk/lib/_internal/js_runtime/lib/synced/array_flags.dart new file mode 100644 index 00000000000..d8ca41ea66f --- /dev/null +++ b/sdk/lib/_internal/js_runtime/lib/synced/array_flags.dart @@ -0,0 +1,63 @@ +// Copyright (c) 2024, 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. + +/// Values for the 'array flags' property that is attached to native JSArray and +/// typed data objects to encode restrictions. +/// +/// We encode the restrictions as two bits - one for potentially growable +/// objects that are restricted to be fixed length, and a second for objects +/// with potentially modifiable contents that are restricted to have +/// unmodifiable contents. We only use three combinations (00 = growable, 01 = +/// fixed-length, 11 = unmodifiable), since the core List class does not have a +/// variant that is an 'append-only' list (would be 10). +/// +/// We use the same scheme for typed data so that the same check works for +/// modifiability checks for `a[i] = v` when `a` is a JSarray or Uint8Array. +/// +/// `const` JSArray values are tagged with an additional bit for future use in +/// avoiding copying. We could also use the `const` bit for unmodifiable typed +/// data for which there in no modifable view of the underlying buffer. +/// +/// Given the absense of append-only lists, we could have used a more +/// numerically compact scheme (0 = growable, 1 = fixed-length, 2 = +/// unmodifiable, 3 = const). This compact scheme requires comparison +/// +/// if (x.flags > 0) +/// +/// rather than mask-testing +/// +/// if (x.flags & 1) +/// +/// The unrestricted flags (0) are usually encoded as a missing property, i.e., +/// `undefined`, which is converted to NaN for '>', but converted to the more +/// comfortable '0' for '&'. We hope that there is less to go potentially go +/// wrong with JavaScript engine slow paths with the bitmask. +class ArrayFlags { + /// Value of array flags that marks a JSArray as being fixed-length. This is + /// not used on typed data since that is always fixed-length. + static const int fixedLength = fixedLengthCheck; + + /// Value of array flags that marks a JSArray or typed-data object as + /// unmodifiable. Includes the 'fixed-length' bit, since all unmodifiable + /// lists are also fixed length. + static const int unmodifiable = fixedLengthCheck | unmodifiableCheck; + + /// Value of array flags that marks a JSArray as a constant (transitively + /// unmodifiable). + static const int constant = + fixedLengthCheck | unmodifiableCheck | constantCheck; + + /// Default value of array flags when there is no flags property. This value + /// is not stored on the flags property. + static const int none = 0; + + /// Bit to check for fixed-length JSArray. + static const int fixedLengthCheck = 1; + + /// Bit to check for unmodifiable JSArray and typed data. + static const int unmodifiableCheck = 2; + + /// Bit to check for constant JSArray. + static const int constantCheck = 4; +} diff --git a/sdk/lib/libraries.json b/sdk/lib/libraries.json index 280a59819c7..5afeeb78181 100644 --- a/sdk/lib/libraries.json +++ b/sdk/lib/libraries.json @@ -394,6 +394,9 @@ }, "_dart2js_common": { "libraries": { + "_array_flags": { + "uri": "_internal/js_runtime/lib/synced/array_flags.dart" + }, "async": { "uri": "async/async.dart", "patches": "_internal/js_runtime/lib/async_patch.dart" diff --git a/sdk/lib/libraries.yaml b/sdk/lib/libraries.yaml index e06c5cda285..3614fc8f3b9 100644 --- a/sdk/lib/libraries.yaml +++ b/sdk/lib/libraries.yaml @@ -318,6 +318,9 @@ dart2js_server: _dart2js_common: libraries: + _array_flags: + uri: "_internal/js_runtime/lib/synced/array_flags.dart" + async: uri: "async/async.dart" patches: "_internal/js_runtime/lib/async_patch.dart" diff --git a/tests/web/consistent_unmodifiable_typed_data_error_test.dart b/tests/web/consistent_unmodifiable_typed_data_error_test.dart new file mode 100644 index 00000000000..5ac81c2cb5a --- /dev/null +++ b/tests/web/consistent_unmodifiable_typed_data_error_test.dart @@ -0,0 +1,217 @@ +// Copyright (c) 2024, 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:typed_data'; +import 'package:expect/expect.dart'; + +// Test that unmodifiable typed data operations on optimized and slow paths +// produce the same error. + +@pragma('dart2js:never-inline') +@pragma('dart2js:assumeDynamic') +confuse(x) => x; + +void check2(String name, String name1, f1(), String name2, f2()) { + Error? trap(part, f) { + try { + f(); + } on Error catch (e) { + return e; + } + Expect.fail('should throw: $name.$part'); + } + + var e1 = trap(name1, f1); + var e2 = trap(name2, f2); + var s1 = '$e1'; + var s2 = '$e2'; + Expect.equals(s1, s2, '\n $name.$name1: "$s1"\n $name.$name2: "$s2"\n'); +} + +void check(String name, f1(), f2(), [f3()?, f4()?]) { + check2(name, 'f1', f1, 'f2', f2); + if (f3 != null) check2(name, 'f1', f1, 'f3', f3); + if (f4 != null) check2(name, 'f1', f1, 'f4', f4); +} + +void main() { + ByteData a = ByteData(100); + ByteData b = a.asUnmodifiableView(); + ByteData c = confuse(true) ? b : a; + + dynamic d = confuse(true) ? b : const [1]; + + void setInt8Test() { + void f1() { + d.setInt8(0, 1); // dynamic receiver. + } + + void f2() { + b.setInt8(0, 1); // unmodifiable receiver + } + + void f3() { + c.setInt8(0, 1); // potentially unmodifiable receiver + } + + check('setInt8', f1, f2, f3); + } + + void setInt16Test() { + void f1() { + d.setInt16(0, 1); // dynamic receiver. + } + + void f2() { + b.setInt16(0, 1); // unmodifiable receiver + } + + void f3() { + c.setInt16(0, 1); // potentially unmodifiable receiver + } + + check('setInt16', f1, f2, f3); + } + + void setInt32Test() { + void f1() { + d.setInt32(0, 1); // dynamic receiver. + } + + void f2() { + b.setInt32(0, 1); // unmodifiable receiver + } + + void f3() { + c.setInt32(0, 1); // potentially unmodifiable receiver + } + + check('setInt32', f1, f2, f3); + } + + void setInt64Test() { + void f1() { + d.setInt64(0, 1); // dynamic receiver. + } + + void f2() { + b.setInt64(0, 1); // unmodifiable receiver + } + + void f3() { + c.setInt64(0, 1); // potentially unmodifiable receiver + } + + check('setInt64', f1, f2, f3); + } + + void setUint8Test() { + void f1() { + d.setUint8(0, 1); // dynamic receiver. + } + + void f2() { + b.setUint8(0, 1); // unmodifiable receiver + } + + void f3() { + c.setUint8(0, 1); // potentially unmodifiable receiver + } + + check('setUint8', f1, f2, f3); + } + + void setUint16Test() { + void f1() { + d.setUint16(0, 1); // dynamic receiver. + } + + void f2() { + b.setUint16(0, 1); // unmodifiable receiver + } + + void f3() { + c.setUint16(0, 1); // potentially unmodifiable receiver + } + + check('setUint16', f1, f2, f3); + } + + void setUint32Test() { + void f1() { + d.setUint32(0, 1); // dynamic receiver. + } + + void f2() { + b.setUint32(0, 1); // unmodifiable receiver + } + + void f3() { + c.setUint32(0, 1); // potentially unmodifiable receiver + } + + check('setUint32', f1, f2, f3); + } + + void setUint64Test() { + void f1() { + d.setUint64(0, 1); // dynamic receiver. + } + + void f2() { + b.setUint64(0, 1); // unmodifiable receiver + } + + void f3() { + c.setUint64(0, 1); // potentially unmodifiable receiver + } + + check('setUint64', f1, f2, f3); + } + + void setFloat32Test() { + void f1() { + d.setFloat32(0, 1.23); // dynamic receiver. + } + + void f2() { + b.setFloat32(0, 1.23); // unmodifiable receiver + } + + void f3() { + c.setFloat32(0, 1.23); // potentially unmodifiable receiver + } + + check('setFloat32', f1, f2, f3); + } + + void setFloat64Test() { + void f1() { + d.setFloat64(0, 1.23); // dynamic receiver. + } + + void f2() { + b.setFloat64(0, 1.23); // unmodifiable receiver + } + + void f3() { + c.setFloat64(0, 1.23); // potentially unmodifiable receiver + } + + check('setFloat64', f1, f2, f3); + } + + setInt8Test(); + setInt16Test(); + setInt32Test(); + setInt64Test(); + + setUint8Test(); + setUint16Test(); + setUint32Test(); + setUint64Test(); + + setFloat32Test(); + setFloat64Test(); +} diff --git a/tests/web/consistent_unmodifiable_typed_list_error_test.dart b/tests/web/consistent_unmodifiable_typed_list_error_test.dart new file mode 100644 index 00000000000..e330d4b64d4 --- /dev/null +++ b/tests/web/consistent_unmodifiable_typed_list_error_test.dart @@ -0,0 +1,154 @@ +// Copyright (c) 2024, 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:typed_data'; +import 'package:expect/expect.dart'; + +// Test that unmodifiable typed list assignments on optimized and slow paths +// produce the same error. + +@pragma('dart2js:never-inline') +@pragma('dart2js:assumeDynamic') +confuse(x) => x; + +void check2(String name, String name1, f1(), String name2, f2()) { + Error? trap(part, f) { + try { + f(); + } on Error catch (e) { + return e; + } + Expect.fail('should throw: $name.$part'); + } + + var e1 = trap(name1, f1); + var e2 = trap(name2, f2); + var s1 = '$e1'; + var s2 = '$e2'; + Expect.equals(s1, s2, '\n $name.$name1: "$s1"\n $name.$name2: "$s2"\n'); +} + +void check(String name, f1(), f2(), [f3()?, f4()?]) { + check2(name, 'f1', f1, 'f2', f2); + if (f3 != null) check2(name, 'f1', f1, 'f3', f3); + if (f4 != null) check2(name, 'f1', f1, 'f4', f4); +} + +void testUint8List() { + Uint8List a = Uint8List(100); + Uint8List b = a.asUnmodifiableView(); + Uint8List c = confuse(true) ? b : a; + + dynamic d = confuse(true) ? b : const [1]; + + void f1() { + d[0] = 0; // dynamic receiver. + } + + void f2() { + b[0] = 1; // unmodifiable receiver + } + + void f3() { + c[0] = 1; // potentially unmodifiable receiver + } + + check('Uint8List', f1, f2, f3); +} + +void testInt16List() { + Int16List a = Int16List(100); + Int16List b = a.asUnmodifiableView(); + Int16List c = confuse(true) ? b : a; + + dynamic d = confuse(true) ? b : const [1]; + + void f1() { + d[0] = 0; // dynamic receiver. + } + + void f2() { + b[0] = 1; // unmodifiable receiver + } + + void f3() { + c[0] = 1; // potentially unmodifiable receiver + } + + check('Int16List', f1, f2, f3); +} + +void testFloat32List() { + Float32List a = Float32List(100); + Float32List b = a.asUnmodifiableView(); + Float32List c = confuse(true) ? b : a; + + dynamic d = confuse(true) ? b : const [1]; + + void f1() { + d[0] = 0; // dynamic receiver. + } + + void f2() { + b[0] = 1; // unmodifiable receiver + } + + void f3() { + c[0] = 1; // potentially unmodifiable receiver + } + + check('Float32List', f1, f2, f3); +} + +void testFloat64List() { + Float64List a = Float64List(100); + Float64List b = a.asUnmodifiableView(); + Float64List c = confuse(true) ? b : a; + + dynamic d = confuse(true) ? b : const [1]; + + void f1() { + d[0] = 0; // dynamic receiver. + } + + void f2() { + b[0] = 1; // unmodifiable receiver + } + + void f3() { + c[0] = 1; // potentially unmodifiable receiver + } + + check('Float64List', f1, f2, f3); +} + +void testFloat64x2List() { + Float64x2List a = Float64x2List(100); + Float64x2List b = a.asUnmodifiableView(); + Float64x2List c = confuse(true) ? b : a; + + dynamic d = confuse(true) ? b : const [1]; + + void f1() { + d[0] = a.last; // dynamic receiver. + } + + void f2() { + b[0] = a.last; // unmodifiable receiver + } + + void f3() { + c[0] = a.last; // potentially unmodifiable receiver + } + + check('Float64List', f1, f2, f3); +} + +main() { + testUint8List(); + testInt16List(); + testFloat32List(); + testFloat64List(); + testFloat64x2List(); +}