[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 <sra@google.com>
Reviewed-by: Mayank Patke <fishythefish@google.com>
diff --git a/pkg/compiler/lib/src/common/elements.dart b/pkg/compiler/lib/src/common/elements.dart
index 04e82de..3976400 100644
--- a/pkg/compiler/lib/src/common/elements.dart
+++ b/pkg/compiler/lib/src/common/elements.dart
@@ -820,6 +820,9 @@
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 f80e8d6..a4da9b5 100644
--- a/pkg/compiler/lib/src/js_backend/namer.dart
+++ b/pkg/compiler/lib/src/js_backend/namer.dart
@@ -2192,6 +2192,8 @@
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 5b9d608..5c6ab76 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 @@
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 9749e04..1f5ed26 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 @@
//
// 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 @@
'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 7870c0d..74ac4ee 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 @@
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 @@
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 dea373b..444b9b8 100644
--- a/pkg/compiler/lib/src/kernel/dart2js_target.dart
+++ b/pkg/compiler/lib/src/kernel/dart2js_target.dart
@@ -255,6 +255,7 @@
// compile-platform should just specify which libraries to compile instead.
const requiredLibraries = <String, List<String>>{
'dart2js': [
+ 'dart:_array_flags',
'dart:_async_status_codes',
'dart:_dart2js_only',
'dart:_dart2js_runtime_metrics',
@@ -297,6 +298,7 @@
'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 5d10b1e..63e4850 100644
--- a/pkg/compiler/lib/src/ssa/builder.dart
+++ b/pkg/compiler/lib/src/ssa/builder.dart
@@ -4631,6 +4631,12 @@
_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 @@
..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<HInstruction> 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<HInstruction> 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<HInstruction> 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 5785b8d..4fa574d 100644
--- a/pkg/compiler/lib/src/ssa/codegen.dart
+++ b/pkg/compiler/lib/src/ssa/codegen.dart
@@ -682,14 +682,15 @@
// 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 @@
}
}
- 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 @@
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 @@
// 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 @@
_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 dcee553..9f990d2 100644
--- a/pkg/compiler/lib/src/ssa/codegen_helpers.dart
+++ b/pkg/compiler/lib/src/ssa/codegen_helpers.dart
@@ -1041,6 +1041,20 @@
}
}
+ @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 05fb3d4..0e6fc59 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 @@
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 @@
}
}
+ 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 d8ab7bb..e55719b 100644
--- a/pkg/compiler/lib/src/ssa/nodes.dart
+++ b/pkg/compiler/lib/src/ssa/nodes.dart
@@ -130,6 +130,10 @@
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 @@
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 @@
lateWriteOnceCheck,
lateInitializeOnceCheck,
charCodeAt,
+ arrayFlagsGet,
+ arrayFlagsCheck,
}
abstract class HInstruction implements SpannableWithEntity {
@@ -1607,11 +1620,21 @@
/// 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();
}
@@ -1625,6 +1648,9 @@
HInstruction get checkedInput => inputs[0];
@override
+ HInstruction get constrainedInput => checkedInput;
+
+ @override
bool isJsStatement() => true;
@override
@@ -4674,6 +4700,122 @@
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<R>(HVisitor<R> 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<R>(HVisitor<R> 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<R>(HVisitor<R> 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 c2132f5..624c66b 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 @@
void visitGraph(HGraph visitee) {
_graph = visitee;
visitDominatorTree(visitee);
+ finalizeArrayFlagEffects();
}
@override
@@ -1618,11 +1621,21 @@
// 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 @@
@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 @@
}
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<HArrayFlagsGet>? _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 397f2d5..e0d09f9 100644
--- a/pkg/compiler/lib/src/ssa/tracer.dart
+++ b/pkg/compiler/lib/src/ssa/tracer.dart
@@ -781,4 +781,22 @@
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 11569df..082036c 100644
--- a/pkg/compiler/lib/src/ssa/types_propagation.dart
+++ b/pkg/compiler/lib/src/ssa/types_propagation.dart
@@ -503,4 +503,12 @@
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 895e628..d1457e8 100644
--- a/pkg/compiler/lib/src/ssa/variable_allocator.dart
+++ b/pkg/compiler/lib/src/ssa/variable_allocator.dart
@@ -225,14 +225,15 @@
// 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 @@
// 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 @@
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 @@
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 0000000..84b3054
--- /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 0000000..caca8fa
--- /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<int> test1(List<int> 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<int> test2(List<int> a) {
+ a.add(100);
+ return a;
+}
+
+@pragma('dart2js:never-inline')
+bool isEven(int i) => i.isEven;
+
+@pragma('dart2js:never-inline')
+/*member: maybeUnmodifiable:ignore*/
+List<int> maybeUnmodifiable() {
+ List<int> 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 0000000..61a9ed8
--- /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<int> list1(List<int> 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<int> maybeUnmodifiableList() {
+ var d = List<int>.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 28a23bb..3b76788 100644
--- a/pkg/compiler/test/optimization/data/index_assign.dart
+++ b/pkg/compiler/test/optimization/data/index_assign.dart
@@ -47,7 +47,8 @@
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 416d81b..2fe2fdb 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 @@
// Test derived from language/generic_methods_dynamic_test/05
-/*spec.class: global#JSArray:deps=[ArrayIterator,List],explicit=[JSArray,JSArray.E,JSArray<ArrayIterator.E>],implicit=[JSArray.E],needsArgs,test*/
+/*spec.class: global#JSArray:deps=[ArrayIterator,List],explicit=[JSArray.E,JSArray<ArrayIterator.E>],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<B>,List<Object>,List<Object?>,List<String>?,List<markFixedList.T>],needsArgs,test*/
-/*prod.class: global#List:deps=[C.bar],explicit=[List<B>],needsArgs,test*/
+/*spec.class: global#List:deps=[C.bar,JSArray.markFixedList],explicit=[List,List<B>,List<Object>,List<Object?>,List<String>?,List<markFixedList.T>],needsArgs,test*/
+/*prod.class: global#List:deps=[C.bar],explicit=[List,List<B>],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 a7d1ed6..58e938f 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<Object>,List<Object?>,List<String>?,List<markFixedList.T>],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<ArrayIterator.E>],implicit=[JSArray.E],needsArgs,test*/
+/*spec.class: global#JSArray:deps=[ArrayIterator,List],explicit=[JSArray.E,JSArray<ArrayIterator.E>],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 1628fdd..646d0f3 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<Object>,List<Object?>,List<String>?,List<markFixedList.T>],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<ArrayIterator.E>],implicit=[JSArray.E],needsArgs,test*/
+/*spec.class: global#JSArray:deps=[ArrayIterator,List],explicit=[JSArray.E,JSArray<ArrayIterator.E>],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 9b3c422..0dd6869 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<ArrayIterator.E>],implicit=[JSArray.E],needsArgs,test*/
+/*spec.class: global#JSArray:deps=[ArrayIterator,List],explicit=[JSArray.E,JSArray<ArrayIterator.E>],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 0000000..d8ca41e
--- /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 75772bd..6d01aab 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 @@
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 @@
/// 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<T>(
+ 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<T>(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 f430efa..71fa151 100644
--- a/sdk/lib/_internal/js_runtime/lib/interceptors.dart
+++ b/sdk/lib/_internal/js_runtime/lib/interceptors.dart
@@ -8,7 +8,15 @@
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 00d4fe0..03ccb14 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 @@
if (length < 0 || length > maxJSArrayLength) {
throw RangeError.range(length, 0, maxJSArrayLength, 'length');
}
- return JSArray<E>.markFixed(JS('', 'new Array(#)', length));
+ return JSArray<E>.markFixed(JS('JSArray', 'new Array(#)', length));
}
/// Returns a fresh JavaScript Array, marked as fixed-length. The Array is
@@ -61,7 +61,7 @@
if (length < 0 || length > maxJSArrayLength) {
throw RangeError.range(length, 0, maxJSArrayLength, 'length');
}
- return JSArray<E>.markFixed(JS('', 'new Array(#)', length));
+ return JSArray<E>.markFixed(JS('JSArray', 'new Array(#)', length));
}
/// Returns a fresh growable JavaScript Array of zero length length.
@@ -78,7 +78,7 @@
if ((length is! int) || (length < 0)) {
throw ArgumentError('Length must be a non-negative integer: $length');
}
- return JSArray<E>.markGrowable(JS('', 'new Array(#)', length));
+ return JSArray<E>.markGrowable(JS('JSArray', 'new Array(#)', length));
}
/// Returns a fresh growable JavaScript Array with initial length. The Array
@@ -95,7 +95,7 @@
if ((length is! int) || (length < 0)) {
throw ArgumentError('Length must be a non-negative integer: $length');
}
- return JSArray<E>.markGrowable(JS('', 'new Array(#)', length));
+ return JSArray<E>.markGrowable(JS('JSArray', 'new Array(#)', length));
}
/// Constructor for adding type parameters to an existing JavaScript Array.
@@ -118,29 +118,24 @@
factory JSArray.markGrowable(allocation) =>
JS('JSExtendableArray', '#', JSArray<E>.typed(allocation));
+ @pragma('dart2js:prefer-inline')
static List<T> markFixedList<T>(List<T> 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<T> markUnmodifiableList<T>(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 @@
}
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<R> cast<R>() => List.castFrom<E, R>(this);
@@ -790,11 +783,14 @@
}
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<int, E> asMap() {
diff --git a/sdk/lib/_internal/js_runtime/lib/js_helper.dart b/sdk/lib/_internal/js_runtime/lib/js_helper.dart
index 6f15c9e..13d35a8 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 @@
show
DART_CLOSURE_TO_JS,
getInterceptor,
+ ArrayFlags,
+ HArrayFlagsGet,
JS,
JS_BUILTIN,
JS_CONST,
@@ -1249,6 +1251,47 @@
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 1fb2edb..dd3f016 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 @@
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 @@
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 @@
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 @@
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 NativeFloat32x4List extends Object
with ListMixin<Float32x4>, FixedLengthListMixin<Float32x4>
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 @@
_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 @@
}
}
+/// View of a [Float32x4List] that disallows modification.
+final class _UnmodifiableFloat32x4ListView extends NativeFloat32x4List
+ with UnmodifiableListMixin<Float32x4> {
+ _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<Int32x4>, FixedLengthListMixin<Int32x4>
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<Int32x4> list)
: _storage = NativeInt32List(list.length * 4) {
@@ -242,7 +257,7 @@
_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 @@
}
}
+/// View of a [Int32x4List] that disallows modification.
+final class _UnmodifiableInt32x4ListView extends NativeInt32x4List
+ with UnmodifiableListMixin<Int32x4> {
+ _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<Float64x2>, FixedLengthListMixin<Float64x2>
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 @@
_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 @@
}
}
+/// View of a [Float64x2List] that disallows modification.
+final class _UnmodifiableFloat64x2ListView extends NativeFloat64x2List
+ with UnmodifiableListMixin<Float64x2> {
+ _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 @@
@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 @@
_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 @@
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 @@
@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 @@
@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 @@
@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 @@
@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 @@
@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 @@
@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 @@
///
/// 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 @@
///
/// 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 @@
///
/// 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 @@
///
/// 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 @@
///
/// 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 @@
///
/// 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 @@
///
/// 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 @@
///
/// 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 @@
}
void operator []=(int index, double value) {
+ _checkMutable('[]=');
_checkValidIndex(index, this, this.length);
JS('void', '#[#] = #', this, index, value);
}
void setRange(int start, int end, Iterable<double> iterable,
[int skipCount = 0]) {
+ _checkMutable('setRange');
if (iterable is NativeTypedArrayOfDouble) {
_setRangeFast(start, end, iterable, skipCount);
return;
@@ -736,12 +933,14 @@
// 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<int> iterable,
[int skipCount = 0]) {
+ _checkMutable('setRange');
if (iterable is NativeTypedArrayOfInt) {
_setRangeFast(start, end, iterable, skipCount);
return;
@@ -768,9 +967,13 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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<N, L extends List<N>, 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<int>
- with _UnmodifiableListMixin<int, Uint8List, Uint8List>
- 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<int>
- with _UnmodifiableListMixin<int, Int8List, Int8List>
- 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<int>
- with _UnmodifiableListMixin<int, Uint8ClampedList, Uint8ClampedList>
- 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<int>
- with _UnmodifiableListMixin<int, Uint16List, Uint16List>
- 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<int>
- with _UnmodifiableListMixin<int, Int16List, Int16List>
- 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<int>
- with _UnmodifiableListMixin<int, Uint32List, Uint32List>
- 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<int>
- with _UnmodifiableListMixin<int, Int32List, Int32List>
- 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<int>
- with _UnmodifiableListMixin<int, Uint64List, Uint64List>
- 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<int>
- with _UnmodifiableListMixin<int, Int64List, Int64List>
- 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<Int32x4>
- with _UnmodifiableListMixin<Int32x4, Int32x4List, Int32x4List>
- 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<Float32x4>
- with _UnmodifiableListMixin<Float32x4, Float32x4List, Float32x4List>
- 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<Float64x2>
- with _UnmodifiableListMixin<Float64x2, Float64x2List, Float64x2List>
- 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<double>
- with _UnmodifiableListMixin<double, Float32List, Float32List>
- 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<double>
- with _UnmodifiableListMixin<double, Float64List, Float64List>
- 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 0000000..d8ca41e
--- /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 280a598..5afeeb7 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 e06c5cd..3614fc8 100644
--- a/sdk/lib/libraries.yaml
+++ b/sdk/lib/libraries.yaml
@@ -318,6 +318,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/tests/web/consistent_unmodifiable_typed_data_error_test.dart b/tests/web/consistent_unmodifiable_typed_data_error_test.dart
new file mode 100644
index 0000000..5ac81c2
--- /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 0000000..e330d4b
--- /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();
+}