From 8de00e213755d358ef5322af6f5ca97fe711776b Mon Sep 17 00:00:00 2001 From: Daco Harkes Date: Thu, 7 Mar 2024 18:33:58 +0000 Subject: [PATCH] [vm] Introduce pragma `vm:deeply-immutable` This CL introduces a way to mark all instances of a class as deeply immutable. In order to statically verify that all instances of a deeply immutable class are immutable, a deeply immutable classes must have the following properties: 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). Note that instances of some classes in the VM are deeply immutable while their class cannot be marked immutable. * SendPort, Capability, RegExp, and StackTrace are not `final` and can be implemented by external code. * UnmodifiableTypedDataViews do not have a public type. (It was recently deprecated.) See runtime/docs/deeply_immutable.md for more details. Use case: This enables attaching a `Dart_FinalizableHandle` to a deeply immutable object and the deeply immutable object with other isolates in the same isolate group. (Note that `NativeFinalizer`s live in an isolate, and not an isolate group. So this should currently _not_ be used with `NativeFinalizer`s. See https://github.com/dart-lang/sdk/issues/55062 for making a `NativeFinalizer.shared(` that would live in an isolate group instead of in an isolate.) Implementation details: Before this CL, the `ImmutableBit` in the object header was only ever set to true for predefined class ids (and for const objects). After this CL, the bit can also be set to true for non const instances of user-defined classes. The object allocation and initialization code has been changed to deal with this new case. The immutability of a class is saved in the class state bits. On object allocation and initialization the immutability bit is read from the class for non-predefined class ids. TEST=runtime/tests/vm/dart/isolates/fast_object_copy2_test.dart TEST=runtime/vm/isolate_reload_test.cc TEST=tests/lib/isolate/deeply_immutable_* Bug: https://github.com/dart-lang/sdk/issues/55120 Bug: https://github.com/dart-lang/sdk/issues/54885 Change-Id: Ib97fe589cb4f81673cb928c93e3093838d82132d Cq-Include-Trybots: luci.dart.try:vm-aot-android-release-arm64c-try,vm-aot-android-release-arm_x64-try,vm-aot-linux-debug-x64-try,vm-aot-linux-debug-x64c-try,vm-aot-mac-release-arm64-try,vm-aot-mac-release-x64-try,vm-aot-obfuscate-linux-release-x64-try,vm-aot-optimization-level-linux-release-x64-try,vm-appjit-linux-debug-x64-try,vm-asan-linux-release-x64-try,vm-checked-mac-release-arm64-try,vm-eager-optimization-linux-release-ia32-try,vm-eager-optimization-linux-release-x64-try,vm-ffi-android-debug-arm-try,vm-ffi-android-debug-arm64c-try,vm-ffi-qemu-linux-release-arm-try,vm-ffi-qemu-linux-release-riscv64-try,vm-fuchsia-release-x64-try,vm-kernel-linux-debug-x64-try,vm-kernel-precomp-linux-release-x64-try,vm-linux-debug-ia32-try,vm-linux-debug-x64-try,vm-linux-debug-x64c-try,vm-mac-debug-arm64-try,vm-mac-debug-x64-try,vm-msan-linux-release-x64-try,vm-reload-linux-debug-x64-try,vm-reload-rollback-linux-debug-x64-try,vm-ubsan-linux-release-x64-try Cq-Include-Trybots: dart-internal/g3.dart-internal.try:g3-cbuild-try Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/354902 Commit-Queue: Daco Harkes Reviewed-by: Martin Kustermann --- .../lib/src/messages/codes_generated.dart | 67 +++++ pkg/front_end/lib/src/api_unstable/vm.dart | 5 + pkg/front_end/messages.status | 5 + pkg/front_end/messages.yaml | 30 +++ .../test/spell_checking_list_messages.txt | 6 + pkg/vm/lib/modular/target/vm.dart | 8 + .../transformations/deeply_immutable.dart | 156 ++++++++++++ runtime/docs/deeply_immutable.md | 79 ++++++ runtime/docs/pragmas.md | 1 + .../dart/isolates/fast_object_copy2_test.dart | 53 ++++ runtime/vm/app_snapshot.cc | 1 + runtime/vm/class_id.h | 46 +++- runtime/vm/compiler/runtime_api.cc | 2 +- runtime/vm/isolate_reload_test.cc | 204 ++++++++++++++++ runtime/vm/kernel_loader.cc | 11 + runtime/vm/kernel_loader.h | 4 +- runtime/vm/object.cc | 23 +- runtime/vm/object.h | 24 +- runtime/vm/object_graph_copy.cc | 3 + runtime/vm/object_reload.cc | 20 ++ runtime/vm/raw_object.h | 24 +- sdk/lib/_internal/vm/lib/core_patch.dart | 1 + sdk/lib/_internal/vm/lib/double.dart | 1 + sdk/lib/_internal/vm/lib/double_patch.dart | 1 + sdk/lib/_internal/vm/lib/ffi_patch.dart | 1 + sdk/lib/_internal/vm/lib/integers.dart | 3 + sdk/lib/_internal/vm/lib/string_patch.dart | 6 + .../_internal/vm/lib/typed_data_patch.dart | 6 + .../_internal/vm_shared/lib/bool_patch.dart | 1 + .../vm_shared/lib/integers_patch.dart | 1 + .../_internal/vm_shared/lib/null_patch.dart | 1 + .../vmspecific_static_checks_test.dart | 2 + .../null/inherit_static_errors_test.dart | 4 + .../lib/isolate/deeply_immutable_2_test.dart | 36 +++ .../isolate/deeply_immutable_no_ffi_test.dart | 38 +++ tests/lib/isolate/deeply_immutable_test.dart | 229 ++++++++++++++++++ .../restricted_types_error_test.dart | 6 + 37 files changed, 1092 insertions(+), 17 deletions(-) create mode 100644 pkg/vm/lib/modular/transformations/deeply_immutable.dart create mode 100644 runtime/docs/deeply_immutable.md create mode 100644 tests/lib/isolate/deeply_immutable_2_test.dart create mode 100644 tests/lib/isolate/deeply_immutable_no_ffi_test.dart create mode 100644 tests/lib/isolate/deeply_immutable_test.dart 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 329a45bfcea9..8204fcee17ea 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 c613142caba2..56a0d506c576 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 7200367bcced..d29b36b6c08c 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 f70cf387185b..5a8ad6edb284 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 9a2f0d6a77fd..611c2ec6ecfa 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 6f2611f110c2..a2111460dea1 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 000000000000..8a4c408732d5 --- /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 000000000000..b5889b719f90 --- /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 0d853b750d63..71a72e267942 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 e4394e1f7cde..6cd4ba25f85f 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 b885272a7493..ea30800495dc 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 38c5f10e82bd..8cf980b62390 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 e982c589c9df..b603fb6097b9 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 7211be4d96c0..1ad1ffa854a1 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 22bf67735367..08245750ab4d 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 29881a2ddf23..944fc94c508f 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 a6f68cc2d817..cb22353ee994 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 24fd6e5ca1bb..6fa62017ee5a 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 1a38992aa2c0..498d7e870951 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 6b71bb416a75..edb8d24c364d 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 b3e4073c7493..82d2badf5942 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 0bc6e0823549..c9c23650ac49 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 eb09a80285fa..15540ca32ce3 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 11f27dd7bf04..54d0dc92f603 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 2f5f2005c8da..5f1d62530627 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 dad8e42a32e5..8e83c7254108 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 42b3c97ffafd..ba8c99857eec 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 50a12571331c..72611daf754f 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 71ed85004b73..338263a106c2 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 981b6c12f7e8..c39c25a27d73 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 6e986d2c7457..eb24722330a0 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 2f0db246a99a..d7cd9f3ac0d5 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 099abb9bf7f7..aae684ae5e89 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 000000000000..86c7d89d9955 --- /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 000000000000..81ad735ee6e2 --- /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 000000000000..a5f9b7e1ed66 --- /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 06ca5fb5a660..63ff1ef14ed3 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