diff --git a/pkg/_fe_analyzer_shared/lib/src/messages/codes_generated.dart b/pkg/_fe_analyzer_shared/lib/src/messages/codes_generated.dart index 329a45bfcea..8204fcee17e 100644 --- a/pkg/_fe_analyzer_shared/lib/src/messages/codes_generated.dart +++ b/pkg/_fe_analyzer_shared/lib/src/messages/codes_generated.dart @@ -5994,6 +5994,73 @@ const MessageCode messageFfiCreateOfStructOrUnion = const MessageCode( r"""Subclasses of 'Struct' and 'Union' are backed by native memory, and can't be instantiated by a generative constructor. Try allocating it via allocation, or load from a 'Pointer'.""", ); +// DO NOT EDIT. THIS FILE IS GENERATED. SEE TOP OF FILE. +const Code codeFfiDeeplyImmutableClassesMustBeFinalOrSealed = + messageFfiDeeplyImmutableClassesMustBeFinalOrSealed; + +// DO NOT EDIT. THIS FILE IS GENERATED. SEE TOP OF FILE. +const MessageCode messageFfiDeeplyImmutableClassesMustBeFinalOrSealed = + const MessageCode( + "FfiDeeplyImmutableClassesMustBeFinalOrSealed", + problemMessage: r"""Deeply immutable classes must be final or sealed.""", + correctionMessage: r"""Try marking this class as final or sealed.""", +); + +// DO NOT EDIT. THIS FILE IS GENERATED. SEE TOP OF FILE. +const Code codeFfiDeeplyImmutableFieldsModifiers = + messageFfiDeeplyImmutableFieldsModifiers; + +// DO NOT EDIT. THIS FILE IS GENERATED. SEE TOP OF FILE. +const MessageCode messageFfiDeeplyImmutableFieldsModifiers = const MessageCode( + "FfiDeeplyImmutableFieldsModifiers", + problemMessage: + r"""Deeply immutable classes must only have final non-late instance fields.""", + correctionMessage: + r"""Add the 'final' modifier to this field, and remove 'late' modifier from this field.""", +); + +// DO NOT EDIT. THIS FILE IS GENERATED. SEE TOP OF FILE. +const Code codeFfiDeeplyImmutableFieldsMustBeDeeplyImmutable = + messageFfiDeeplyImmutableFieldsMustBeDeeplyImmutable; + +// DO NOT EDIT. THIS FILE IS GENERATED. SEE TOP OF FILE. +const MessageCode messageFfiDeeplyImmutableFieldsMustBeDeeplyImmutable = + const MessageCode( + "FfiDeeplyImmutableFieldsMustBeDeeplyImmutable", + problemMessage: + r"""Deeply immutable classes must only have deeply immutable instance fields. Deeply immutable types include 'int', 'double', 'bool', 'String', 'Pointer', 'Float32x4', 'Float64x2', 'Int32x4', and classes annotated with `@pragma('vm:deeply-immutable')`.""", + correctionMessage: + r"""Try changing the type of this field to a deeply immutable type or mark the type of this field as deeply immutable.""", +); + +// DO NOT EDIT. THIS FILE IS GENERATED. SEE TOP OF FILE. +const Code codeFfiDeeplyImmutableSubtypesMustBeDeeplyImmutable = + messageFfiDeeplyImmutableSubtypesMustBeDeeplyImmutable; + +// DO NOT EDIT. THIS FILE IS GENERATED. SEE TOP OF FILE. +const MessageCode messageFfiDeeplyImmutableSubtypesMustBeDeeplyImmutable = + const MessageCode( + "FfiDeeplyImmutableSubtypesMustBeDeeplyImmutable", + problemMessage: + r"""Subtypes of deeply immutable classes must be deeply immutable.""", + correctionMessage: + r"""Try marking this class deeply immutable by adding `@pragma('vm:deeply-immutable')`.""", +); + +// DO NOT EDIT. THIS FILE IS GENERATED. SEE TOP OF FILE. +const Code codeFfiDeeplyImmutableSupertypeMustBeDeeplyImmutable = + messageFfiDeeplyImmutableSupertypeMustBeDeeplyImmutable; + +// DO NOT EDIT. THIS FILE IS GENERATED. SEE TOP OF FILE. +const MessageCode messageFfiDeeplyImmutableSupertypeMustBeDeeplyImmutable = + const MessageCode( + "FfiDeeplyImmutableSupertypeMustBeDeeplyImmutable", + problemMessage: + r"""The super type of deeply immutable classes must be deeply immutable.""", + correctionMessage: + r"""Try marking the super class deeply immutable by adding `@pragma('vm:deeply-immutable')`.""", +); + // DO NOT EDIT. THIS FILE IS GENERATED. SEE TOP OF FILE. const Code codeFfiDefaultAssetDuplicate = messageFfiDefaultAssetDuplicate; diff --git a/pkg/front_end/lib/src/api_unstable/vm.dart b/pkg/front_end/lib/src/api_unstable/vm.dart index c613142caba..56a0d506c57 100644 --- a/pkg/front_end/lib/src/api_unstable/vm.dart +++ b/pkg/front_end/lib/src/api_unstable/vm.dart @@ -48,6 +48,11 @@ export '../fasta/codes/fasta_codes.dart' messageFfiAbiSpecificIntegerMappingInvalid, messageFfiAddressOfMustBeNative, messageFfiCreateOfStructOrUnion, + messageFfiDeeplyImmutableClassesMustBeFinalOrSealed, + messageFfiDeeplyImmutableFieldsModifiers, + messageFfiDeeplyImmutableFieldsMustBeDeeplyImmutable, + messageFfiDeeplyImmutableSubtypesMustBeDeeplyImmutable, + messageFfiDeeplyImmutableSupertypeMustBeDeeplyImmutable, messageFfiDefaultAssetDuplicate, messageFfiExceptionalReturnNull, messageFfiExpectedConstant, diff --git a/pkg/front_end/messages.status b/pkg/front_end/messages.status index 7200367bcce..d29b36b6c08 100644 --- a/pkg/front_end/messages.status +++ b/pkg/front_end/messages.status @@ -382,6 +382,11 @@ FfiAbiSpecificIntegerMappingInvalid/analyzerCode: Fail FfiCompoundImplementsFinalizable/analyzerCode: Fail FfiCreateOfStructOrUnion/analyzerCode: Fail FfiDartTypeMismatch/analyzerCode: Fail +FfiDeeplyImmutableClassesMustBeFinalOrSealed/analyzerCode: Fail +FfiDeeplyImmutableFieldsModifiers/analyzerCode: Fail +FfiDeeplyImmutableFieldsMustBeDeeplyImmutable/analyzerCode: Fail +FfiDeeplyImmutableSubtypesMustBeDeeplyImmutable/analyzerCode: Fail +FfiDeeplyImmutableSupertypeMustBeDeeplyImmutable/analyzerCode: Fail FfiEmptyStruct/analyzerCode: Fail FfiExceptionalReturnNull/analyzerCode: Fail FfiExpectedConstant/analyzerCode: Fail diff --git a/pkg/front_end/messages.yaml b/pkg/front_end/messages.yaml index f70cf387185..5a8ad6edb28 100644 --- a/pkg/front_end/messages.yaml +++ b/pkg/front_end/messages.yaml @@ -5165,6 +5165,36 @@ FfiDartTypeMismatch: problemMessage: "Expected '#type' to be a subtype of '#type2'." external: test/ffi_test.dart +FfiDeeplyImmutableClassesMustBeFinalOrSealed: + # Used by dart:ffi + problemMessage: 'Deeply immutable classes must be final or sealed.' + correctionMessage: 'Try marking this class as final or sealed.' + external: test/ffi_test.dart + +FfiDeeplyImmutableFieldsMustBeDeeplyImmutable: + # Used by dart:ffi + problemMessage: "Deeply immutable classes must only have deeply immutable instance fields. Deeply immutable types include 'int', 'double', 'bool', 'String', 'Pointer', 'Float32x4', 'Float64x2', 'Int32x4', and classes annotated with `@pragma('vm:deeply-immutable')`." + correctionMessage: 'Try changing the type of this field to a deeply immutable type or mark the type of this field as deeply immutable.' + external: test/ffi_test.dart + +FfiDeeplyImmutableFieldsModifiers: + # Used by dart:ffi + problemMessage: 'Deeply immutable classes must only have final non-late instance fields.' + correctionMessage: "Add the 'final' modifier to this field, and remove 'late' modifier from this field." + external: test/ffi_test.dart + +FfiDeeplyImmutableSubtypesMustBeDeeplyImmutable: + # Used by dart:ffi + problemMessage: 'Subtypes of deeply immutable classes must be deeply immutable.' + correctionMessage: "Try marking this class deeply immutable by adding `@pragma('vm:deeply-immutable')`." + external: test/ffi_test.dart + +FfiDeeplyImmutableSupertypeMustBeDeeplyImmutable: + # Used by dart:ffi + problemMessage: 'The super type of deeply immutable classes must be deeply immutable.' + correctionMessage: "Try marking the super class deeply immutable by adding `@pragma('vm:deeply-immutable')`." + external: test/ffi_test.dart + FfiDefaultAssetDuplicate: # Used by dart:ffi problemMessage: "There may be at most one @DefaultAsset annotation on a library." diff --git a/pkg/front_end/test/spell_checking_list_messages.txt b/pkg/front_end/test/spell_checking_list_messages.txt index 9a2f0d6a77f..611c2ec6ecf 100644 --- a/pkg/front_end/test/spell_checking_list_messages.txt +++ b/pkg/front_end/test/spell_checking_list_messages.txt @@ -45,6 +45,7 @@ dart:js_interop dart:js_interop_unsafe dart_runner dartbug.com +deeply defaultasset dname e.g @@ -55,6 +56,8 @@ extensiontype f ffi finality +float32x +float64x flutter_runner function.tojs futureor @@ -63,6 +66,7 @@ guarded guides h https +int32x interact interop intervening @@ -79,6 +83,7 @@ loadlibrary macro member(s) migrate +modifier mocking n name.#name @@ -106,6 +111,7 @@ patterns placing pointer`s pragma +pragma('vm:deeply preexisting pubspec.yaml r diff --git a/pkg/vm/lib/modular/target/vm.dart b/pkg/vm/lib/modular/target/vm.dart index 6f2611f110c..a2111460dea 100644 --- a/pkg/vm/lib/modular/target/vm.dart +++ b/pkg/vm/lib/modular/target/vm.dart @@ -11,6 +11,7 @@ import 'package:kernel/target/changed_structure_notifier.dart'; import 'package:kernel/target/targets.dart'; import '../transformations/call_site_annotator.dart' as callSiteAnnotator; +import '../transformations/deeply_immutable.dart' as deeply_immutable; import '../transformations/lowering.dart' as lowering show transformLibraries, transformProcedure; import '../transformations/mixin_full_resolution.dart' as transformMixins @@ -151,6 +152,13 @@ class VmTarget extends Target { ReferenceFromIndex? referenceFromIndex, {void Function(String msg)? logger, ChangedStructureNotifier? changedStructureNotifier}) { + deeply_immutable.validateLibraries( + libraries, + coreTypes, + diagnosticReporter, + ); + logger?.call("Validated deeply immutable"); + transformMixins.transformLibraries( this, coreTypes, hierarchy, libraries, referenceFromIndex); logger?.call("Transformed mixin applications"); diff --git a/pkg/vm/lib/modular/transformations/deeply_immutable.dart b/pkg/vm/lib/modular/transformations/deeply_immutable.dart new file mode 100644 index 00000000000..8a4c408732d --- /dev/null +++ b/pkg/vm/lib/modular/transformations/deeply_immutable.dart @@ -0,0 +1,156 @@ +// 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 'package:front_end/src/fasta/codes/fasta_codes.dart' + show + messageFfiDeeplyImmutableClassesMustBeFinalOrSealed, + messageFfiDeeplyImmutableFieldsModifiers, + messageFfiDeeplyImmutableFieldsMustBeDeeplyImmutable, + messageFfiDeeplyImmutableSubtypesMustBeDeeplyImmutable, + messageFfiDeeplyImmutableSupertypeMustBeDeeplyImmutable; +import 'package:kernel/ast.dart'; +import 'package:kernel/core_types.dart'; +import 'package:kernel/target/targets.dart' show DiagnosticReporter; + +void validateLibraries( + List libraries, + CoreTypes coreTypes, + DiagnosticReporter diagnosticReporter, +) { + final validator = DeeplyImmutableValidator( + coreTypes, + diagnosticReporter, + ); + for (final library in libraries) { + validator.visitLibrary(library); + } +} + +/// Implements the `vm:deeply-immutable` semantics. +class DeeplyImmutableValidator { + static const vmDeeplyImmutable = "vm:deeply-immutable"; + + final CoreTypes coreTypes; + final DiagnosticReporter diagnosticReporter; + final Class pragmaClass; + final Field pragmaName; + + DeeplyImmutableValidator( + this.coreTypes, + this.diagnosticReporter, + ) : pragmaClass = coreTypes.pragmaClass, + pragmaName = coreTypes.pragmaName; + + void visitLibrary(Library library) { + for (final cls in library.classes) { + visitClass(cls); + } + } + + void visitClass(Class node) { + _validateDeeplyImmutable(node); + } + + void _validateDeeplyImmutable(Class node) { + if (!_isDeeplyImmutableClass(node)) { + // If class is not marked deeply immutable, check that none of the super + // types is marked deeply immutable. + final classes = [ + if (node.superclass != null) node.superclass!, + for (final superType in node.implementedTypes) superType.classNode, + if (node.mixedInClass != null) node.mixedInClass!, + ]; + for (final superClass in classes) { + if (_isDeeplyImmutableClass(superClass)) { + diagnosticReporter.report( + messageFfiDeeplyImmutableSubtypesMustBeDeeplyImmutable, + node.fileOffset, + node.name.length, + node.location!.file, + ); + } + } + return; + } + + final superClass = node.superclass; + if (superClass != null && superClass != coreTypes.objectClass) { + if (!_isDeeplyImmutableClass(superClass)) { + diagnosticReporter.report( + messageFfiDeeplyImmutableSupertypeMustBeDeeplyImmutable, + node.fileOffset, + node.name.length, + node.location!.file, + ); + } + } + + // Don't allow implementing, extending or mixing in deeply immutable classes + // in other libraries. Adding a `vm:deeply-immutable` pragma to a class that + // might be implemented, extended or mixed in would break subtypes that are + // not marked deeply immutable. (We could consider relaxing this and + // allowing breaking subtypes upon adding the pragma.) + if (!(node.isFinal || node.isSealed)) { + diagnosticReporter.report( + messageFfiDeeplyImmutableClassesMustBeFinalOrSealed, + node.fileOffset, + node.name.length, + node.location!.file, + ); + } + + // All instance fields should be non-late final and deeply immutable. + for (final field in node.fields) { + if (field.isStatic) { + // Static fields are not part of instances. + continue; + } + if (!_isDeeplyImmutableDartType(field.type)) { + diagnosticReporter.report( + messageFfiDeeplyImmutableFieldsMustBeDeeplyImmutable, + field.fileOffset, + field.name.text.length, + field.location!.file, + ); + } + if (!field.isFinal || field.isLate) { + diagnosticReporter.report( + messageFfiDeeplyImmutableFieldsModifiers, + field.fileOffset, + field.name.text.length, + field.location!.file, + ); + } + } + } + + bool _isDeeplyImmutableDartType(DartType dartType) { + if (dartType is NullType) { + return true; + } + if (dartType is InterfaceType) { + final classNode = dartType.classNode; + return _isDeeplyImmutableClass(classNode); + } + if (dartType is TypeParameterType) { + return _isDeeplyImmutableDartType(dartType.bound); + } + return false; + } + + bool _isDeeplyImmutableClass(Class node) { + for (final annotation in node.annotations) { + if (annotation is ConstantExpression) { + final constant = annotation.constant; + if (constant is InstanceConstant && + constant.classNode == pragmaClass && + constant.fieldValues[pragmaName.fieldReference] == + StringConstant(vmDeeplyImmutable)) { + return true; + } + } + } + return false; + } +} diff --git a/runtime/docs/deeply_immutable.md b/runtime/docs/deeply_immutable.md new file mode 100644 index 00000000000..b5889b719f9 --- /dev/null +++ b/runtime/docs/deeply_immutable.md @@ -0,0 +1,79 @@ +# Deeply immutable instances and types + +The Dart VM has a concept of deeply immutable instances. + +Deeply immutable instances can be shared across isolates within the same group. + +## Deeply immutable types + +A deeply immutable type is a type for which all instances that have this type are deeply immutable. + +This is useful for static checks on classes annotated `@pragma('vm:deeply-immutable')`. +All the instance fields of such classes must have a deeply immutable type. + +A list of immutable types: + +* `bool` +* `double` +* `int` +* `Null` +* `String` +* `Float32x4` +* `Float64x2` +* `Int32x4` +* `Pointer` +* classes annotated with `@pragma('vm:deeply-immutable')` +* type parameters bound by a deeply immutable type + +## Deeply immutable instances without a deeply immutable type + +In addition to instances from deeply immutable types, +instances can also be deeply immutable while their type is not deeply immutable: + +* `SendPort` (implemented externally `package:isolate`, so cannot be `final` https://github.com/dart-lang/sdk/issues/54885#issuecomment-1967329435) +* `Capability` (has `SendPort` as subtype so cannot be `final`) +* `RegExp` (can be implemented externally, not `final`) +* `StackTrace` (can be implemented externally, not `final`) +* `Type` (can be implemented externally, not `final`) +* const object (the class can be deeply immutable) + +This means users cannot mark classes with fields typed with these types as `@pragma('vm:deeply-immutable')`. + +## Shallowly immutable instances + +The VM also has shallow immutability. + +* unmodifiable typed data views (the backing view might not be immutable) +* closures (the context might not be empty) + +## Implementation details + +### Deeply and shallowly immutable instances + +The `UntaggedObject::ImmutableBit` tracks whether an instance is deeply or shallowly immutable at runtime. +For shallow immutable objects, the VM needs to know the layout and what to check when to check for to check deep immutability at runtime. + +### Deeply immutable types + +The `Class::is_deeply_immutable` tracks whether all instances of a class are deeply immutable. + +This bit can be set in two ways: + +1. For recognized classes, in the VM initialization. +2. For classes with a Dart source, with the `vm:deeply-immutable` pragma. + +The `vm:deeply-immutable` pragma is added to classes of which their _type_ is deeply immutable. + +This puts the following restrictions on these classes: + +1. All instance fields must + 1. have a deeply immutable type, + 2. be final, and + 3. be non-late. +2. The class must be `final` or `sealed`. + This ensures no non-deeply-immutable subtypes are added by external code. +3. All subtypes must be deeply immutable. + This ensures 1.1. can be trusted. +4. The super type must be deeply immutable (except for Object). + +These restructions are enforced by [DeeplyImmutableValidator](../../pkg/vm/lib/transformations/ffi/deeply_immutable.dart). diff --git a/runtime/docs/pragmas.md b/runtime/docs/pragmas.md index 0d853b750d6..71a72e26794 100644 --- a/runtime/docs/pragmas.md +++ b/runtime/docs/pragmas.md @@ -19,6 +19,7 @@ These pragmas are part of the VM's API and are safe for use in external code. | `weak-tearoff-reference` | [Declaring a static weak reference intrinsic method.](compiler/pragmas_recognized_by_compiler.md#declaring-a-static-weak-reference-intrinsic-method) | | `vm:isolate-unsendable` | Marks a class, instances of which won't be allowed to be passed through ports or sent between isolates. | | `vm:awaiter-link` | [Specifying variable to follow for awaiter stack unwinding](awaiter_stack_traces.md) | +| `vm:deeply-immutable` | [Specifying a class and all its subtypes are deeply immutable](deeply_immutable.md) | ## Unsafe pragmas for general use diff --git a/runtime/tests/vm/dart/isolates/fast_object_copy2_test.dart b/runtime/tests/vm/dart/isolates/fast_object_copy2_test.dart index e4394e1f7cd..6cd4ba25f85 100644 --- a/runtime/tests/vm/dart/isolates/fast_object_copy2_test.dart +++ b/runtime/tests/vm/dart/isolates/fast_object_copy2_test.dart @@ -101,6 +101,30 @@ final sharableObjects = [ Float64x2(1.0, 2.0), StackTrace.current, Pointer.fromAddress(0xdeadbeef), + DeeplyImmutable( + someString: 'someString', + someNullableString: 'someString', + someInt: 3, + someDouble: 3.3, + someBool: false, + someNull: null, + someInt32x4: Int32x4(0, 1, 2, 3), + someFloat32x4: Float32x4(0.0, 1.1, 2.2, 3.3), + someFloat64x2: Float64x2(4.4, 5.5), + someDeeplyImmutable: DeeplyImmutable( + someString: 'someString', + someInt: 3, + someDouble: 3.3, + someBool: false, + someNull: null, + someInt32x4: Int32x4(0, 1, 2, 3), + someFloat32x4: Float32x4(0.0, 1.1, 2.2, 3.3), + someFloat64x2: Float64x2(4.4, 5.5), + someDeeplyImmutable: null, + somePointer: Pointer.fromAddress(0x8badf00d), + ), + somePointer: Pointer.fromAddress(0xdeadbeef), + ), ]; final copyableClosures = [ @@ -225,3 +249,32 @@ void msanUnpoison(Pointer pointer, int size) { pointer.cast(), size); } } + +@pragma('vm:deeply-immutable') +final class DeeplyImmutable { + final String someString; + final String? someNullableString; + final int someInt; + final double someDouble; + final bool someBool; + final Null someNull; + final Int32x4 someInt32x4; + final Float32x4 someFloat32x4; + final Float64x2 someFloat64x2; + final DeeplyImmutable? someDeeplyImmutable; + final Pointer somePointer; + + DeeplyImmutable({ + required this.someString, + this.someNullableString, + required this.someInt, + required this.someDouble, + required this.someBool, + required this.someNull, + required this.someInt32x4, + required this.someFloat32x4, + required this.someFloat64x2, + this.someDeeplyImmutable, + required this.somePointer, + }); +} diff --git a/runtime/vm/app_snapshot.cc b/runtime/vm/app_snapshot.cc index b885272a749..ea30800495d 100644 --- a/runtime/vm/app_snapshot.cc +++ b/runtime/vm/app_snapshot.cc @@ -879,6 +879,7 @@ void Deserializer::InitializeHeader(ObjectPtr raw, tags = UntaggedObject::NotMarkedBit::update(true, tags); tags = UntaggedObject::OldAndNotRememberedBit::update(true, tags); tags = UntaggedObject::NewBit::update(false, tags); + // TODO(https://dartbug.com/55136): Initialize the ImmutableBit. raw->untag()->tags_ = tags; } diff --git a/runtime/vm/class_id.h b/runtime/vm/class_id.h index 38c5f10e82b..8cf980b6239 100644 --- a/runtime/vm/class_id.h +++ b/runtime/vm/class_id.h @@ -309,7 +309,7 @@ inline bool IsInternalOnlyClassId(intptr_t index) { return index <= kLastInternalOnlyCid; } - // Make sure this function is updated when new Error types are added. +// Make sure this function is updated when new Error types are added. static const ClassId kFirstErrorCid = kErrorCid; static const ClassId kLastErrorCid = kUnwindErrorCid; COMPILE_ASSERT(kFirstErrorCid == kErrorCid && @@ -468,15 +468,41 @@ inline bool IsUnmodifiableTypedDataViewClassId(intptr_t index) { kTypedDataCidRemainderUnmodifiable); } -inline bool ShouldHaveImmutabilityBitSet(intptr_t index) { - return IsUnmodifiableTypedDataViewClassId(index) || IsStringClassId(index) || - index == kMintCid || index == kNeverCid || index == kSentinelCid || - index == kStackTraceCid || index == kDoubleCid || - index == kFloat32x4Cid || index == kFloat64x2Cid || - index == kInt32x4Cid || index == kSendPortCid || - index == kCapabilityCid || index == kRegExpCid || index == kBoolCid || - index == kNullCid || index == kPointerCid || index == kTypeCid || - index == kRecordTypeCid || index == kFunctionTypeCid; +// For predefined cids only. Refer to Class::is_deeply_immutable for +// instances of non-predefined classes. +// +// Having the `@pragma('vm:deeply-immutable')`, which means statically proven +// deeply immutable, implies true for this function. The other way around is not +// guaranteed, predefined classes can be marked deeply immutable in the VM while +// not having their subtypes or super type being deeply immutable. +// +// Keep consistent with runtime/docs/deeply_immutable.md. +inline bool IsDeeplyImmutableCid(intptr_t predefined_cid) { + ASSERT(predefined_cid < kNumPredefinedCids); + return IsStringClassId(predefined_cid) || predefined_cid == kNumberCid || + predefined_cid == kIntegerCid || predefined_cid == kSmiCid || + predefined_cid == kMintCid || predefined_cid == kNeverCid || + predefined_cid == kSentinelCid || predefined_cid == kStackTraceCid || + predefined_cid == kDoubleCid || predefined_cid == kFloat32x4Cid || + predefined_cid == kFloat64x2Cid || predefined_cid == kInt32x4Cid || + predefined_cid == kSendPortCid || predefined_cid == kCapabilityCid || + predefined_cid == kRegExpCid || predefined_cid == kBoolCid || + predefined_cid == kNullCid || predefined_cid == kPointerCid || + predefined_cid == kTypeCid || predefined_cid == kRecordTypeCid || + predefined_cid == kFunctionTypeCid; +} + +inline bool IsShallowlyImmutableCid(intptr_t predefined_cid) { + ASSERT(predefined_cid < kNumPredefinedCids); + // TODO(https://dartbug.com/55136): Mark kClosureCid as shallowly imutable. + return IsUnmodifiableTypedDataViewClassId(predefined_cid); +} + +// See documentation on ImmutableBit in raw_object.h +inline bool ShouldHaveImmutabilityBitSetCid(intptr_t predefined_cid) { + ASSERT(predefined_cid < kNumPredefinedCids); + return IsDeeplyImmutableCid(predefined_cid) || + IsShallowlyImmutableCid(predefined_cid); } inline bool IsFfiTypeClassId(intptr_t index) { diff --git a/runtime/vm/compiler/runtime_api.cc b/runtime/vm/compiler/runtime_api.cc index e982c589c9d..b603fb6097b 100644 --- a/runtime/vm/compiler/runtime_api.cc +++ b/runtime/vm/compiler/runtime_api.cc @@ -363,7 +363,7 @@ uword MakeTagWordForNewSpaceObject(classid_t cid, uword instance_size) { dart::UntaggedObject::AlwaysSetBit::encode(true) | dart::UntaggedObject::NotMarkedBit::encode(true) | dart::UntaggedObject::ImmutableBit::encode( - ShouldHaveImmutabilityBitSet(cid)); + dart::Object::ShouldHaveImmutabilityBitSet(cid)); } word Object::tags_offset() { diff --git a/runtime/vm/isolate_reload_test.cc b/runtime/vm/isolate_reload_test.cc index 7211be4d96c..1ad1ffa854a 100644 --- a/runtime/vm/isolate_reload_test.cc +++ b/runtime/vm/isolate_reload_test.cc @@ -4190,6 +4190,210 @@ TEST_CASE(IsolateReload_ShapeChange_Const_RemoveSlot) { "Library:'file:///test-lib' Class: A"); } +TEST_CASE(IsolateReload_DeeplyImmutableChange) { + const char* kScript = R"( + @pragma('vm:deeply-immutable') + final class A { + final int x; + A(this.x); + } + String main () { + A(123); + return 'okay'; + } + )"; + + Dart_Handle lib = TestCase::LoadTestScript(kScript, nullptr); + EXPECT_VALID(lib); + EXPECT_STREQ("okay", SimpleInvokeStr(lib, "main")); + + const char* kReloadScript = R"( + final class A { + final int x; + A(this.x); + } + String main () { + A(123); + return 'okay'; + } + )"; + + lib = TestCase::ReloadTestScript(kReloadScript); + EXPECT_ERROR(lib, + "Classes cannot change their @pragma('vm:deeply-immutable'): " + "Library:'file:///test-lib' Class: A"); +} + +TEST_CASE(IsolateReload_DeeplyImmutableChange_2) { + const char* kScript = R"( + final class A { + final int x; + A(this.x); + } + String main () { + A(123); + return 'okay'; + } + )"; + + Dart_Handle lib = TestCase::LoadTestScript(kScript, nullptr); + EXPECT_VALID(lib); + EXPECT_STREQ("okay", SimpleInvokeStr(lib, "main")); + + const char* kReloadScript = R"( + @pragma('vm:deeply-immutable') + final class A { + final int x; + A(this.x); + } + String main () { + A(123); + return 'okay'; + } + )"; + + lib = TestCase::ReloadTestScript(kReloadScript); + EXPECT_ERROR(lib, + "Classes cannot change their @pragma('vm:deeply-immutable'): " + "Library:'file:///test-lib' Class: A"); +} + +TEST_CASE(IsolateReload_DeeplyImmutableChange_MultiLib) { + // clang-format off + Dart_SourceFile sourcefiles[] = { + { + "file:///test-app.dart", + R"( + import 'test-lib.dart'; + + @pragma('vm:deeply-immutable') + final class A { + final B b; + A(this.b); + } + int main () { + A(B(123)); + return 42; + } + )", + }, + { + "file:///test-lib.dart", + R"( + @pragma('vm:deeply-immutable') + final class B { + final int x; + B(this.x); + } + )" + }}; + // clang-format on + + Dart_Handle lib = TestCase::LoadTestScriptWithDFE( + sizeof(sourcefiles) / sizeof(Dart_SourceFile), sourcefiles, + nullptr /* resolver */, true /* finalize */, true /* incrementally */); + EXPECT_VALID(lib); + Dart_Handle result = Dart_Invoke(lib, NewString("main"), 0, nullptr); + int64_t value = 0; + result = Dart_IntegerToInt64(result, &value); + EXPECT_VALID(result); + EXPECT_EQ(42, value); + + // clang-format off + Dart_SourceFile updated_sourcefiles[] = { + { + "file:///test-lib.dart", + R"( + final class B { + final int x; + B(this.x); + } + )" + }}; + // clang-format on + + { + const uint8_t* kernel_buffer = nullptr; + intptr_t kernel_buffer_size = 0; + char* error = TestCase::CompileTestScriptWithDFE( + "file:///test-app.dart", + sizeof(updated_sourcefiles) / sizeof(Dart_SourceFile), + updated_sourcefiles, &kernel_buffer, &kernel_buffer_size, + true /* incrementally */); + // This is rejected by class A being recompiled and the validator failing. + EXPECT(error != nullptr); + EXPECT_NULLPTR(kernel_buffer); + } +} + +TEST_CASE(IsolateReload_DeeplyImmutableChange_TypeBound) { + // clang-format off + Dart_SourceFile sourcefiles[] = { + { + "file:///test-app.dart", + R"( + import 'test-lib.dart'; + + @pragma('vm:deeply-immutable') + final class A { + final T b; + A(this.b); + } + int main () { + A(B(123)); + return 42; + } + )", + }, + { + "file:///test-lib.dart", + R"( + @pragma('vm:deeply-immutable') + final class B { + final int x; + B(this.x); + } + )" + }}; + // clang-format on + + Dart_Handle lib = TestCase::LoadTestScriptWithDFE( + sizeof(sourcefiles) / sizeof(Dart_SourceFile), sourcefiles, + nullptr /* resolver */, true /* finalize */, true /* incrementally */); + EXPECT_VALID(lib); + Dart_Handle result = Dart_Invoke(lib, NewString("main"), 0, nullptr); + int64_t value = 0; + result = Dart_IntegerToInt64(result, &value); + EXPECT_VALID(result); + EXPECT_EQ(42, value); + + // clang-format off + Dart_SourceFile updated_sourcefiles[] = { + { + "file:///test-lib.dart", + R"( + final class B { + final int x; + B(this.x); + } + )" + }}; + // clang-format on + + { + const uint8_t* kernel_buffer = nullptr; + intptr_t kernel_buffer_size = 0; + char* error = TestCase::CompileTestScriptWithDFE( + "file:///test-app.dart", + sizeof(updated_sourcefiles) / sizeof(Dart_SourceFile), + updated_sourcefiles, &kernel_buffer, &kernel_buffer_size, + true /* incrementally */); + // This is rejected by class A being recompiled and the validator failing. + EXPECT(error != nullptr); + EXPECT_NULLPTR(kernel_buffer); + } +} + TEST_CASE(IsolateReload_ConstToNonConstClass) { const char* kScript = R"( class A { diff --git a/runtime/vm/kernel_loader.cc b/runtime/vm/kernel_loader.cc index 22bf6773536..08245750ab4 100644 --- a/runtime/vm/kernel_loader.cc +++ b/runtime/vm/kernel_loader.cc @@ -1355,6 +1355,13 @@ void KernelLoader::LoadClass(const Library& library, if (IsolateUnsendablePragma::decode(pragma_bits)) { out_class->set_is_isolate_unsendable_due_to_pragma(true); } + if (DeeplyImmutablePragma::decode(pragma_bits)) { + out_class->set_is_deeply_immutable(true); + // Ensure that the pragma implies deeply immutability for VM recognized + // classes. + ASSERT(out_class->id() >= kNumPredefinedCids || + IsDeeplyImmutableCid(out_class->id())); + } if (HasPragma::decode(pragma_bits)) { out_class->set_has_pragma(true); } @@ -1754,6 +1761,10 @@ void KernelLoader::ReadVMAnnotations(const Library& library, "vm:isolate-unsendable")) { *pragma_bits = IsolateUnsendablePragma::update(true, *pragma_bits); } + if (constant_reader.IsStringConstant(name_index, + "vm:deeply-immutable")) { + *pragma_bits = DeeplyImmutablePragma::update(true, *pragma_bits); + } if (constant_reader.IsStringConstant(name_index, "vm:ffi:native")) { *pragma_bits = FfiNativePragma::update(true, *pragma_bits); } diff --git a/runtime/vm/kernel_loader.h b/runtime/vm/kernel_loader.h index 29881a2ddf2..944fc94c508 100644 --- a/runtime/vm/kernel_loader.h +++ b/runtime/vm/kernel_loader.h @@ -220,8 +220,10 @@ class KernelLoader : public ValueObject { BitField; using IsolateUnsendablePragma = BitField; - using FfiNativePragma = + using DeeplyImmutablePragma = BitField; + using FfiNativePragma = + BitField; void FinishTopLevelClassLoading(const Class& toplevel_class, const Library& library, diff --git a/runtime/vm/object.cc b/runtime/vm/object.cc index a6f68cc2d81..cb22353ee99 100644 --- a/runtime/vm/object.cc +++ b/runtime/vm/object.cc @@ -18,6 +18,7 @@ #include "vm/bootstrap.h" #include "vm/canonical_tables.h" #include "vm/class_finalizer.h" +#include "vm/class_id.h" #include "vm/closure_functions_cache.h" #include "vm/code_comments.h" #include "vm/code_descriptors.h" @@ -2711,6 +2712,15 @@ StringPtr Object::DictionaryName() const { return String::null(); } +bool Object::ShouldHaveImmutabilityBitSet(classid_t class_id) { + if (class_id < kNumPredefinedCids) { + return ShouldHaveImmutabilityBitSetCid(class_id); + } else { + return Class::IsDeeplyImmutable( + IsolateGroup::Current()->class_table()->At(class_id)); + } +} + void Object::InitializeObject(uword address, intptr_t class_id, intptr_t size, @@ -2814,7 +2824,7 @@ void Object::InitializeObject(uword address, tags = UntaggedObject::OldAndNotRememberedBit::update(is_old, tags); tags = UntaggedObject::NewBit::update(!is_old, tags); tags = UntaggedObject::ImmutableBit::update( - ShouldHaveImmutabilityBitSet(class_id), tags); + Object::ShouldHaveImmutabilityBitSet(class_id), tags); #if defined(HASH_IN_OBJECT_HEADER) tags = UntaggedObject::HashTag::update(0, tags); #endif @@ -3164,6 +3174,10 @@ ClassPtr Class::New(IsolateGroup* isolate_group, bool register_class) { // references, but do not recompute size. result.set_is_prefinalized(); } + if (FakeObject::kClassId < kNumPredefinedCids && + IsDeeplyImmutableCid(FakeObject::kClassId)) { + result.set_is_deeply_immutable(true); + } NOT_IN_PRECOMPILED(result.set_kernel_offset(0)); result.InitEmptyFields(); if (register_class) { @@ -3218,6 +3232,11 @@ void Class::set_is_isolate_unsendable_due_to_pragma(bool value) const { IsIsolateUnsendableDueToPragmaBit::update(value, state_bits())); } +void Class::set_is_deeply_immutable(bool value) const { + ASSERT(IsolateGroup::Current()->program_lock()->IsCurrentThreadWriter()); + set_state_bits(IsDeeplyImmutableBit::update(value, state_bits())); +} + void Class::set_is_future_subtype(bool value) const { ASSERT(IsolateGroup::Current()->program_lock()->IsCurrentThreadWriter()); set_state_bits(IsFutureSubtypeBit::update(value, state_bits())); @@ -5322,6 +5341,8 @@ ClassPtr Class::NewStringClass(intptr_t class_id, IsolateGroup* isolate_group) { result.set_next_field_offset(host_next_field_offset, target_next_field_offset); result.set_is_prefinalized(); + ASSERT(IsDeeplyImmutableCid(class_id)); + result.set_is_deeply_immutable(true); isolate_group->class_table()->Register(result); return result.ptr(); } diff --git a/runtime/vm/object.h b/runtime/vm/object.h index 24fd6e5ca1b..6fa62017ee5 100644 --- a/runtime/vm/object.h +++ b/runtime/vm/object.h @@ -657,6 +657,8 @@ class Object { kNo, }; + static bool ShouldHaveImmutabilityBitSet(classid_t class_id); + protected: friend ObjectPtr AllocateObject(intptr_t, intptr_t, intptr_t); @@ -2062,9 +2064,19 @@ class Class : public Object { // - super class / super interface classes are marked as unsendable. // - class has native fields. kIsIsolateUnsendableBit, - // True if this class has `@pragma('vm:isolate-unsendable') annotation or + // True if this class has `@pragma('vm:isolate-unsendable')` annotation or // base class or implemented interfaces has this bit. kIsIsolateUnsendableDueToPragmaBit, + // Will be set to 1 for the following classes: + // + // 1. Deeply immutable class. + // a. Statically guaranteed deeply immutable classes. + // `@pragma('vm:deeply-immutable')`. + // b. VM recognized deeply immutable classes. + // `IsDeeplyImmutableCid(intptr_t predefined_cid)`. + // + // See also ImmutableBit in raw_object.h. + kIsDeeplyImmutableBit, // This class is a subtype of Future. kIsFutureSubtypeBit, // This class has a non-abstract subtype which is a subtype of Future. @@ -2104,6 +2116,8 @@ class Class : public Object { class IsIsolateUnsendableDueToPragmaBit : public BitField { }; + class IsDeeplyImmutableBit + : public BitField {}; class IsFutureSubtypeBit : public BitField {}; class CanBeFutureBit : public BitField {}; @@ -2159,6 +2173,14 @@ class Class : public Object { return IsIsolateUnsendableDueToPragmaBit::decode(state_bits()); } + void set_is_deeply_immutable(bool value) const; + bool is_deeply_immutable() const { + return IsDeeplyImmutableBit::decode(state_bits()); + } + static bool IsDeeplyImmutable(ClassPtr clazz) { + return IsDeeplyImmutableBit::decode(clazz->untag()->state_bits_); + } + void set_is_future_subtype(bool value) const; bool is_future_subtype() const { ASSERT(is_type_finalized()); diff --git a/runtime/vm/object_graph_copy.cc b/runtime/vm/object_graph_copy.cc index 1a38992aa2c..498d7e87095 100644 --- a/runtime/vm/object_graph_copy.cc +++ b/runtime/vm/object_graph_copy.cc @@ -160,10 +160,13 @@ static bool CanShareObject(ObjectPtr obj, uword tags) { ->untag() ->IsImmutable(); } + // All other objects that have immutability bit set are deeply immutable. return true; } + // TODO(https://dartbug.com/55136): Mark Closures as shallowly imutable. + // And move this into the if above. if (cid == kClosureCid) { // We can share a closure iff it doesn't close over any state. return Closure::RawCast(obj)->untag()->context() == Object::null(); diff --git a/runtime/vm/object_reload.cc b/runtime/vm/object_reload.cc index 6b71bb416a7..edb8d24c364 100644 --- a/runtime/vm/object_reload.cc +++ b/runtime/vm/object_reload.cc @@ -374,6 +374,19 @@ class EnsureFinalizedError : public ClassReasonForCancelling { StringPtr ToString() { return String::New(error_.ToErrorCString()); } }; +class DeeplyImmutableChange : public ClassReasonForCancelling { + public: + DeeplyImmutableChange(Zone* zone, const Class& from, const Class& to) + : ClassReasonForCancelling(zone, from, to) {} + + private: + StringPtr ToString() { + return String::NewFormatted( + "Classes cannot change their @pragma('vm:deeply-immutable'): %s", + from_.ToCString()); + } +}; + class ConstToNonConstClass : public ClassReasonForCancelling { public: ConstToNonConstClass(Zone* zone, const Class& from, const Class& to) @@ -512,6 +525,13 @@ void Class::CheckReload(const Class& replacement, TIR_Print("Finalized replacement class for %s\n", ToCString()); } + if (is_deeply_immutable() != replacement.is_deeply_immutable()) { + context->group_reload_context()->AddReasonForCancelling( + new (context->zone()) + DeeplyImmutableChange(context->zone(), *this, replacement)); + return; // No reason to check other properties. + } + if (is_finalized() && is_const() && (constants() != Array::null()) && (Array::LengthOf(constants()) > 0)) { // Consts can't become non-consts. diff --git a/runtime/vm/raw_object.h b/runtime/vm/raw_object.h index b3e4073c749..82d2badf594 100644 --- a/runtime/vm/raw_object.h +++ b/runtime/vm/raw_object.h @@ -255,9 +255,27 @@ class UntaggedObject { class OldAndNotRememberedBit : public BitField {}; - // Will be set to 1 iff - // - is unmodifiable typed data view (backing store may be mutable) - // - is transitively immutable + // Will be set to 1 for the following instances: + // + // 1. Deeply immutable instances. + // `Class::is_deeply_immutable`. + // a. Statically guaranteed deeply immutable instances. + // `@pragma('vm:deeply-immutable')`. + // b. VM recognized deeply immutable instances. + // `IsDeeplyImmutableCid(intptr_t predefined_cid)`. + // 2. Shallowly unmodifiable instances. + // `IsShallowlyImmutableCid(intptr_t predefined_cid)` + // a. Unmodifiable typed data view (backing store may be mutable). + // b. Closures (the context may be modifiable). + // + // The bit is used in `CanShareObject` in object_graph_copy, where special + // care is taken to look at the shallow immutable instances. Shallow immutable + // instances always need special care in the VM because the VM needs to know + // what their fields are. + // + // The bit is also used to make typed data stores efficient. 2.a. + // + // See also Class::kIsDeeplyImmutableBit. class ImmutableBit : public BitField {}; class ReservedBit : public BitField {}; diff --git a/sdk/lib/_internal/vm/lib/core_patch.dart b/sdk/lib/_internal/vm/lib/core_patch.dart index 0bc6e082354..c9c23650ac4 100644 --- a/sdk/lib/_internal/vm/lib/core_patch.dart +++ b/sdk/lib/_internal/vm/lib/core_patch.dart @@ -84,6 +84,7 @@ part "uri_patch.dart"; part "weak_property.dart"; @patch +@pragma('vm:deeply-immutable') class num { num _addFromInteger(int other); num _subFromInteger(int other); diff --git a/sdk/lib/_internal/vm/lib/double.dart b/sdk/lib/_internal/vm/lib/double.dart index eb09a80285f..15540ca32ce 100644 --- a/sdk/lib/_internal/vm/lib/double.dart +++ b/sdk/lib/_internal/vm/lib/double.dart @@ -4,6 +4,7 @@ part of "core_patch.dart"; +@pragma('vm:deeply-immutable') @pragma("vm:entry-point") final class _Double implements double { @pragma("vm:recognized", "asm-intrinsic") diff --git a/sdk/lib/_internal/vm/lib/double_patch.dart b/sdk/lib/_internal/vm/lib/double_patch.dart index 11f27dd7bf0..54d0dc92f60 100644 --- a/sdk/lib/_internal/vm/lib/double_patch.dart +++ b/sdk/lib/_internal/vm/lib/double_patch.dart @@ -7,6 +7,7 @@ part of "core_patch.dart"; // VM implementation of double. @patch +@pragma('vm:deeply-immutable') @pragma("vm:entry-point") class double { @pragma("vm:external-name", "Double_parse") diff --git a/sdk/lib/_internal/vm/lib/ffi_patch.dart b/sdk/lib/_internal/vm/lib/ffi_patch.dart index 2f5f2005c8d..5f1d6253062 100644 --- a/sdk/lib/_internal/vm/lib/ffi_patch.dart +++ b/sdk/lib/_internal/vm/lib/ffi_patch.dart @@ -198,6 +198,7 @@ external dynamic _nativeIsolateLocalCallbackFunction( dynamic exceptionalReturn); @patch +@pragma('vm:deeply-immutable') @pragma("vm:entry-point") final class Pointer implements SizedNativeType { @patch diff --git a/sdk/lib/_internal/vm/lib/integers.dart b/sdk/lib/_internal/vm/lib/integers.dart index dad8e42a32e..8e83c725410 100644 --- a/sdk/lib/_internal/vm/lib/integers.dart +++ b/sdk/lib/_internal/vm/lib/integers.dart @@ -4,6 +4,7 @@ part of "core_patch.dart"; +@pragma('vm:deeply-immutable') abstract final class _IntegerImplementation implements int { @pragma("vm:recognized", "graph-intrinsic") @pragma("vm:non-nullable-result-type") @@ -552,6 +553,7 @@ abstract final class _IntegerImplementation implements int { } } +@pragma('vm:deeply-immutable') @pragma("vm:entry-point") final class _Smi extends _IntegerImplementation { factory _Smi._uninstantiable() { @@ -754,6 +756,7 @@ final class _Smi extends _IntegerImplementation { } // Represents integers that cannot be represented by Smi but fit into 64bits. +@pragma('vm:deeply-immutable') @pragma("vm:entry-point") final class _Mint extends _IntegerImplementation { factory _Mint._uninstantiable() { diff --git a/sdk/lib/_internal/vm/lib/string_patch.dart b/sdk/lib/_internal/vm/lib/string_patch.dart index 42b3c97ffaf..ba8c99857ee 100644 --- a/sdk/lib/_internal/vm/lib/string_patch.dart +++ b/sdk/lib/_internal/vm/lib/string_patch.dart @@ -10,6 +10,7 @@ const int _maxUtf16 = 0xffff; const int _maxUnicode = 0x10ffff; @patch +@pragma('vm:deeply-immutable') @pragma("vm:entry-point") class String { @patch @@ -59,6 +60,7 @@ class String { * [_StringBase] contains common methods used by concrete String * implementations, e.g., _OneByteString. */ +@pragma('vm:deeply-immutable') abstract final class _StringBase implements String { bool _isWhitespace(int codeUnit); @@ -997,6 +999,7 @@ int _clampedPositiveProduct(int a, int b) { return product; } +@pragma('vm:deeply-immutable') @pragma("vm:entry-point") final class _OneByteString extends _StringBase { factory _OneByteString._uninstantiable() { @@ -1335,6 +1338,7 @@ final class _OneByteString extends _StringBase { } } +@pragma('vm:deeply-immutable') @pragma("vm:entry-point") final class _TwoByteString extends _StringBase { factory _TwoByteString._uninstantiable() { @@ -1397,6 +1401,7 @@ final class _TwoByteString extends _StringBase { } } +@pragma('vm:deeply-immutable') @pragma("vm:entry-point") final class _ExternalOneByteString extends _StringBase { factory _ExternalOneByteString._uninstantiable() { @@ -1417,6 +1422,7 @@ final class _ExternalOneByteString extends _StringBase { } } +@pragma('vm:deeply-immutable') @pragma("vm:entry-point") final class _ExternalTwoByteString extends _StringBase { factory _ExternalTwoByteString._uninstantiable() { diff --git a/sdk/lib/_internal/vm/lib/typed_data_patch.dart b/sdk/lib/_internal/vm/lib/typed_data_patch.dart index 50a12571331..72611daf754 100644 --- a/sdk/lib/_internal/vm/lib/typed_data_patch.dart +++ b/sdk/lib/_internal/vm/lib/typed_data_patch.dart @@ -3776,6 +3776,7 @@ final class _ExternalFloat64x2Array extends _TypedList } @patch +@pragma('vm:deeply-immutable') class Float32x4 { @patch @pragma("vm:prefer-inline") @@ -3824,6 +3825,7 @@ class Float32x4 { external factory Float32x4.fromFloat64x2(Float64x2 v); } +@pragma('vm:deeply-immutable') @pragma("vm:entry-point") final class _Float32x4 implements Float32x4 { @pragma("vm:recognized", "graph-intrinsic") @@ -3977,6 +3979,7 @@ final class _Float32x4 implements Float32x4 { } @patch +@pragma('vm:deeply-immutable') class Int32x4 { @patch @pragma("vm:prefer-inline") @@ -4015,6 +4018,7 @@ class Int32x4 { external factory Int32x4.fromFloat32x4Bits(Float32x4 x); } +@pragma('vm:deeply-immutable') @pragma("vm:entry-point") final class _Int32x4 implements Int32x4 { @pragma("vm:external-name", "Int32x4_or") @@ -4155,6 +4159,7 @@ final class _Int32x4 implements Int32x4 { } @patch +@pragma('vm:deeply-immutable') class Float64x2 { @patch @pragma("vm:prefer-inline") @@ -4194,6 +4199,7 @@ class Float64x2 { external factory Float64x2.fromFloat32x4(Float32x4 v); } +@pragma('vm:deeply-immutable') @pragma("vm:entry-point") final class _Float64x2 implements Float64x2 { @pragma("vm:recognized", "graph-intrinsic") diff --git a/sdk/lib/_internal/vm_shared/lib/bool_patch.dart b/sdk/lib/_internal/vm_shared/lib/bool_patch.dart index 71ed85004b7..338263a106c 100644 --- a/sdk/lib/_internal/vm_shared/lib/bool_patch.dart +++ b/sdk/lib/_internal/vm_shared/lib/bool_patch.dart @@ -5,6 +5,7 @@ import "dart:_internal" show patch, checkNotNullable; @patch +@pragma('vm:deeply-immutable') @pragma("vm:entry-point") @pragma("wasm:entry-point") class bool { diff --git a/sdk/lib/_internal/vm_shared/lib/integers_patch.dart b/sdk/lib/_internal/vm_shared/lib/integers_patch.dart index 981b6c12f7e..c39c25a27d7 100644 --- a/sdk/lib/_internal/vm_shared/lib/integers_patch.dart +++ b/sdk/lib/_internal/vm_shared/lib/integers_patch.dart @@ -8,6 +8,7 @@ import "dart:typed_data" show Int64List; /// VM implementation of int. @patch +@pragma('vm:deeply-immutable') @pragma("vm:entry-point") class int { @patch diff --git a/sdk/lib/_internal/vm_shared/lib/null_patch.dart b/sdk/lib/_internal/vm_shared/lib/null_patch.dart index 6e986d2c745..eb24722330a 100644 --- a/sdk/lib/_internal/vm_shared/lib/null_patch.dart +++ b/sdk/lib/_internal/vm_shared/lib/null_patch.dart @@ -5,6 +5,7 @@ import "dart:_internal" show patch; @patch +@pragma('vm:deeply-immutable') @pragma("vm:entry-point") class Null { static const _HASH_CODE = 2011; // The year Dart was announced and a prime. diff --git a/tests/ffi/static_checks/vmspecific_static_checks_test.dart b/tests/ffi/static_checks/vmspecific_static_checks_test.dart index 2f0db246a99..d7cd9f3ac0d 100644 --- a/tests/ffi/static_checks/vmspecific_static_checks_test.dart +++ b/tests/ffi/static_checks/vmspecific_static_checks_test.dart @@ -807,6 +807,7 @@ class EPointer extends Pointer {} // [analyzer] COMPILE_TIME_ERROR.NO_GENERATIVE_CONSTRUCTORS_IN_SUPERCLASS // ^ // [cfe] The superclass, 'Pointer', has no unnamed constructor that takes no arguments. +// [cfe] Subtypes of deeply immutable classes must be deeply immutable. // Cannot implement native natives or Struct. @@ -889,6 +890,7 @@ class INativeFunction implements NativeFunction {} class IPointer implements Pointer {} // ^^^^^^^^ // [cfe] The non-abstract class 'IPointer' is missing implementations for these members: +// [cfe] Subtypes of deeply immutable classes must be deeply immutable. // [analyzer] COMPILE_TIME_ERROR.NON_ABSTRACT_CLASS_INHERITS_ABSTRACT_MEMBER // ^^^^^^^ // [cfe] The class 'Pointer' can't be implemented outside of its library because it's a final class. diff --git a/tests/language/null/inherit_static_errors_test.dart b/tests/language/null/inherit_static_errors_test.dart index 099abb9bf7f..aae684ae5e8 100644 --- a/tests/language/null/inherit_static_errors_test.dart +++ b/tests/language/null/inherit_static_errors_test.dart @@ -13,6 +13,7 @@ class BadExtends extends Null {} // ^ // [cfe] 'Null' is restricted and can't be extended or implemented. // [cfe] The superclass, 'Null', has no unnamed constructor that takes no arguments. +// [cfe] Subtypes of deeply immutable classes must be deeply immutable. // ^ // [cfe] 'Null' is restricted and can't be extended or implemented. @@ -21,6 +22,7 @@ class BadImplements implements Null {} // [analyzer] COMPILE_TIME_ERROR.SUBTYPE_OF_DISALLOWED_TYPE // ^ // [cfe] 'Null' is restricted and can't be extended or implemented. +// [cfe] Subtypes of deeply immutable classes must be deeply immutable. // ^ // [cfe] 'Null' is restricted and can't be extended or implemented. @@ -29,6 +31,7 @@ class BadMixin extends Object with Null {} // [analyzer] COMPILE_TIME_ERROR.SUBTYPE_OF_DISALLOWED_TYPE // ^ // [cfe] 'Null' is restricted and can't be extended or implemented. +// [cfe] Subtypes of deeply immutable classes must be deeply immutable. // ^ // [cfe] 'Null' is restricted and can't be extended or implemented. @@ -37,5 +40,6 @@ class BadMixin2 = Object with Null; // [analyzer] COMPILE_TIME_ERROR.SUBTYPE_OF_DISALLOWED_TYPE // ^ // [cfe] 'Null' is restricted and can't be extended or implemented. +// [cfe] Subtypes of deeply immutable classes must be deeply immutable. // ^ // [cfe] 'Null' is restricted and can't be extended or implemented. diff --git a/tests/lib/isolate/deeply_immutable_2_test.dart b/tests/lib/isolate/deeply_immutable_2_test.dart new file mode 100644 index 00000000000..86c7d89d995 --- /dev/null +++ b/tests/lib/isolate/deeply_immutable_2_test.dart @@ -0,0 +1,36 @@ +// 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:async'; +import 'dart:ffi'; +import 'dart:isolate'; + +class Base { + var myMutableField; +} + +@pragma('vm:deeply-immutable') +final class Foo extends Base {} +// ^^^ +// [cfe] The super type of deeply immutable classes must be deeply immutable. + +Future sendReceive(T o) async { + final r = ReceivePort(); + final si = StreamIterator(r); + + r.sendPort.send(o); + await si.moveNext(); + final o2 = si.current; + + si.cancel(); + return o2; +} + +main() async { + final o = Foo(); + final o2 = await sendReceive(o); + if (!identical(o, o2)) throw 'not identical'; + + throw 'we could share mutable objects - oh no!'; +} diff --git a/tests/lib/isolate/deeply_immutable_no_ffi_test.dart b/tests/lib/isolate/deeply_immutable_no_ffi_test.dart new file mode 100644 index 00000000000..81ad735ee6e --- /dev/null +++ b/tests/lib/isolate/deeply_immutable_no_ffi_test.dart @@ -0,0 +1,38 @@ +// 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. + +// These checks are not implemented in the analyzer. If we ever decide to +// implement the static checks in the analyzer, move this test into the +// static_checks subdir to prevent analyzer errors showing up in the IDE. + +import 'dart:async'; +import 'dart:isolate'; + +@pragma('vm:deeply-immutable') +final class Foo { + dynamic myMutableField; + // ^ + // [cfe] Deeply immutable classes must only have final non-late instance fields. + // [cfe] Deeply immutable classes must only have deeply immutable instance fields. Deeply immutable types include 'int', 'double', 'bool', 'String', 'Pointer', 'Float32x4', 'Float64x2', 'Int32x4', and classes annotated with `@pragma('vm:deeply-immutable')`. +} + +Future sendReceive(T o) async { + final r = ReceivePort(); + final si = StreamIterator(r); + + r.sendPort.send(o); + await si.moveNext(); + final o2 = si.current; + + si.cancel(); + return o2; +} + +main() async { + final o = Foo(); + final o2 = await sendReceive(o); + if (!identical(o, o2)) throw 'not identical'; + + throw 'we could share mutable objects - oh no!'; +} diff --git a/tests/lib/isolate/deeply_immutable_test.dart b/tests/lib/isolate/deeply_immutable_test.dart new file mode 100644 index 00000000000..a5f9b7e1ed6 --- /dev/null +++ b/tests/lib/isolate/deeply_immutable_test.dart @@ -0,0 +1,229 @@ +// 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. + +// These checks are not implemented in the analyzer. If we ever decide to +// implement the static checks in the analyzer, move this test into the +// static_checks subdir to prevent analyzer errors showing up in the IDE. + +import 'dart:ffi'; +import 'dart:typed_data'; + +void main() { + testInstantiateDeeplyImmutable(); +} + +@pragma('vm:deeply-immutable') +final class EmptyClass {} + +@pragma('vm:deeply-immutable') +final class Class1 { + Class1(this.a); + + int a; + // ^ + // [cfe] Deeply immutable classes must only have final non-late instance fields. +} + +@pragma('vm:deeply-immutable') +final class Class2 { + late final int a; + // ^ + // [cfe] Deeply immutable classes must only have final non-late instance fields. +} + +@pragma('vm:deeply-immutable') +final class Class3 { + Class3(this.a); + + final int a; +} + +@pragma('vm:deeply-immutable') +final class Class4 { + // Static fields are not part of the instance. + static late final int a; +} + +@pragma('vm:deeply-immutable') +final class Class5 { + // External fields are defined as setter/getter pairs. + external int a; +} + +final class NotDeeplyImmutable { + late int a; +} + +@pragma('vm:deeply-immutable') +final class Class6 { + Class6(this.a); + + final NotDeeplyImmutable a; + // ^ + // [cfe] Deeply immutable classes must only have deeply immutable instance fields. Deeply immutable types include 'int', 'double', 'bool', 'String', 'Pointer', 'Float32x4', 'Float64x2', 'Int32x4', and classes annotated with `@pragma('vm:deeply-immutable')`. +} + +void testInstantiateDeeplyImmutable() { + Class7( + someString: 'someString', + someNullableString: 'someString', + someInt: 3, + someDouble: 3.3, + someBool: false, + someNull: null, + someInt32x4: Int32x4(0, 1, 2, 3), + someFloat32x4: Float32x4(0.0, 1.1, 2.2, 3.3), + someFloat64x2: Float64x2(4.4, 5.5), + someClass7: Class7( + someString: 'someString', + someInt: 3, + someDouble: 3.3, + someBool: false, + someNull: null, + someInt32x4: Int32x4(0, 1, 2, 3), + someFloat32x4: Float32x4(0.0, 1.1, 2.2, 3.3), + someFloat64x2: Float64x2(4.4, 5.5), + someClass7: null, + somePointer: Pointer.fromAddress(0x8badf00d), + ), + somePointer: Pointer.fromAddress(0xdeadbeef), + ); +} + +@pragma('vm:deeply-immutable') +final class Class7 { + final String someString; + final String? someNullableString; + final int someInt; + final double someDouble; + final bool someBool; + final Null someNull; + final Int32x4 someInt32x4; + final Float32x4 someFloat32x4; + final Float64x2 someFloat64x2; + final Class7? someClass7; + final Pointer somePointer; + + // Note that UnmodifiableUint8ListView has been deprecated. Which means there + // currently is no way to intentionally have a typed data as a field in a + // class which is deeply immutable. + // See: https://github.com/dart-lang/sdk/issues/53218. + + // Note that RegExp, SendPort, and Capability can be implemented. So fields + // are not allowed to be of these types either. + + Class7({ + required this.someString, + this.someNullableString, + required this.someInt, + required this.someDouble, + required this.someBool, + required this.someNull, + required this.someInt32x4, + required this.someFloat32x4, + required this.someFloat64x2, + required this.someClass7, + required this.somePointer, + }); +} + +void testInstantiateImmutableHierarchy() { + Class8( + animal: Cat( + numberOfLegs: 4, + averageNumberOfMeowsPerDay: 42.0, + ), + ); + Class8( + animal: Dog( + numberOfLegs: 4, + averageNumberOfWoofsPerDay: 1337.0, + ), + ); +} + +@pragma('vm:deeply-immutable') +final class Animal { + final int numberOfLegs; + + Animal({ + required this.numberOfLegs, + }); +} + +@pragma('vm:deeply-immutable') +final class Cat extends Animal { + final double averageNumberOfMeowsPerDay; + + Cat({ + required super.numberOfLegs, + required this.averageNumberOfMeowsPerDay, + }); +} + +@pragma('vm:deeply-immutable') +final class Dog extends Animal { + final double averageNumberOfWoofsPerDay; + + Dog({ + required super.numberOfLegs, + required this.averageNumberOfWoofsPerDay, + }); +} + +@pragma('vm:deeply-immutable') +final class Class8 { + final Animal animal; + + Class8({ + required this.animal, + }); +} + +@pragma('vm:deeply-immutable') +abstract final class DeeplyImmutableInterface {} + +@pragma('vm:deeply-immutable') +final class Class9 implements DeeplyImmutableInterface {} + +@pragma('vm:deeply-immutable') +final class Class10 implements DeeplyImmutableInterface {} + +@pragma('vm:deeply-immutable') +sealed class Class11 {} + +@pragma('vm:deeply-immutable') +class NotSealedOrFinalClass {} +// ^^^^^^^^^^^^^^^^^^^^^ +// [cfe] Deeply immutable classes must be final or sealed. + +final class Class12 extends DeeplyImmutableInterface {} +// ^^^^^^^ +// [cfe] Subtypes of deeply immutable classes must be deeply immutable. + +final class Class13 implements DeeplyImmutableInterface { +// ^^^^^^^ +// [cfe] Subtypes of deeply immutable classes must be deeply immutable. +} + +@pragma('vm:deeply-immutable') +final class Class14 { + final T deeplyImmutable; + + Class14({required this.deeplyImmutable}); +} + +@pragma('vm:deeply-immutable') +final class Class15 { + final T notDeeplyImmutable; + // ^^^^^^^^^^^^^^^^^^ + // [cfe] Deeply immutable classes must only have deeply immutable instance fields. Deeply immutable types include 'int', 'double', 'bool', 'String', 'Pointer', 'Float32x4', 'Float64x2', 'Int32x4', and classes annotated with `@pragma('vm:deeply-immutable')`. + + Class15({required this.notDeeplyImmutable}); +} + +@pragma('vm:deeply-immutable') +abstract mixin class Class17 {} +// ^^^^^^^ +// [cfe] Deeply immutable classes must be final or sealed. diff --git a/tests/lib/typed_data/restricted_types_error_test.dart b/tests/lib/typed_data/restricted_types_error_test.dart index 06ca5fb5a66..63ff1ef14ed 100644 --- a/tests/lib/typed_data/restricted_types_error_test.dart +++ b/tests/lib/typed_data/restricted_types_error_test.dart @@ -228,36 +228,42 @@ abstract class CMFloat64x2List with Float64x2List {} abstract class CIInt32x4 implements Int32x4 {} // ^ // [cfe] 'Int32x4' is restricted and can't be extended or implemented. +// [cfe] Subtypes of deeply immutable classes must be deeply immutable. // ^^^^^^^ // [analyzer] COMPILE_TIME_ERROR.SUBTYPE_OF_DISALLOWED_TYPE abstract class CMInt32x4 with Int32x4 {} // ^ // [cfe] 'Int32x4' is restricted and can't be extended or implemented. +// [cfe] Subtypes of deeply immutable classes must be deeply immutable. // ^^^^^^^ // [analyzer] COMPILE_TIME_ERROR.SUBTYPE_OF_DISALLOWED_TYPE abstract class CIFloat32x4 implements Float32x4 {} // ^ // [cfe] 'Float32x4' is restricted and can't be extended or implemented. +// [cfe] Subtypes of deeply immutable classes must be deeply immutable. // ^^^^^^^^^ // [analyzer] COMPILE_TIME_ERROR.SUBTYPE_OF_DISALLOWED_TYPE abstract class CMFloat32x4 with Float32x4 {} // ^ // [cfe] 'Float32x4' is restricted and can't be extended or implemented. +// [cfe] Subtypes of deeply immutable classes must be deeply immutable. // ^^^^^^^^^ // [analyzer] COMPILE_TIME_ERROR.SUBTYPE_OF_DISALLOWED_TYPE abstract class CIFloat64x2 implements Float64x2 {} // ^ // [cfe] 'Float64x2' is restricted and can't be extended or implemented. +// [cfe] Subtypes of deeply immutable classes must be deeply immutable. // ^^^^^^^^^ // [analyzer] COMPILE_TIME_ERROR.SUBTYPE_OF_DISALLOWED_TYPE abstract class CMFloat64x2 with Float64x2 {} // ^ // [cfe] 'Float64x2' is restricted and can't be extended or implemented. +// [cfe] Subtypes of deeply immutable classes must be deeply immutable. // ^^^^^^^^^ // [analyzer] COMPILE_TIME_ERROR.SUBTYPE_OF_DISALLOWED_TYPE