[vm/compiler] Avoid load forwarding for weak fields

TEST=runtime/tests/vm/dart/weak_field_load_forwarding_il_test.dart
Fixes https://github.com/dart-lang/sdk/issues/55518

Change-Id: I4576b25ba8f88fe5bb1bd319baa5980fe22cccce
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/364201
Reviewed-by: Ryan Macnak <rmacnak@google.com>
Reviewed-by: Slava Egorov <vegorov@google.com>
Commit-Queue: Alexander Markov <alexmarkov@google.com>
diff --git a/runtime/tests/vm/dart/weak_field_load_forwarding_il_test.dart b/runtime/tests/vm/dart/weak_field_load_forwarding_il_test.dart
new file mode 100644
index 0000000..a4bee40
--- /dev/null
+++ b/runtime/tests/vm/dart/weak_field_load_forwarding_il_test.dart
@@ -0,0 +1,105 @@
+// 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.
+
+// Verify that load forwarding doesn't happen for weak fields
+// (values of weak fields can be set to null by the GC).
+// Regression test for https://github.com/dart-lang/sdk/issues/55518.
+
+import 'package:expect/expect.dart';
+import 'package:vm/testing/il_matchers.dart';
+
+class StrongRef {
+  Object? target;
+  StrongRef(this.target);
+}
+
+@pragma('vm:never-inline')
+@pragma('vm:testing:print-flow-graph')
+bool loadLoadStrongRef(StrongRef obj) {
+  var v1 = obj.target;
+  var v2 = obj.target;
+  return identical(v1, v2);
+}
+
+void matchIL$loadLoadStrongRef(FlowGraph graph) {
+  graph.match([
+    match.block('Graph', [
+      'true' << match.Constant(value: true),
+    ]),
+    match.block('Function', [
+      'obj' << match.Parameter(index: 0),
+      match.DartReturn('true'),
+    ]),
+  ]);
+}
+
+@pragma('vm:never-inline')
+@pragma('vm:testing:print-flow-graph')
+bool loadLoadWeakReference(WeakReference obj) {
+  var v1 = obj.target;
+  var v2 = obj.target;
+  return identical(v1, v2);
+}
+
+void matchIL$loadLoadWeakReference(FlowGraph graph) {
+  graph.match([
+    match.block('Graph'),
+    match.block('Function', [
+      'obj' << match.Parameter(index: 0),
+      'v1' << match.LoadField('obj', slot: 'WeakReference.target'),
+      'v2' << match.LoadField('obj', slot: 'WeakReference.target'),
+      'res' << match.StrictCompare('v1', 'v2', kind: '==='),
+      match.DartReturn('res'),
+    ]),
+  ]);
+}
+
+@pragma('vm:never-inline')
+@pragma('vm:testing:print-flow-graph')
+bool storeLoadStrongRef(Object v1) {
+  final obj = StrongRef(v1);
+  var v2 = obj.target;
+  return identical(v1, v2);
+}
+
+void matchIL$storeLoadStrongRef(FlowGraph graph) {
+  graph.match([
+    match.block('Graph', [
+      'true' << match.Constant(value: true),
+    ]),
+    match.block('Function', [
+      'obj' << match.Parameter(index: 0),
+      match.DartReturn('true'),
+    ]),
+  ]);
+}
+
+@pragma('vm:never-inline')
+@pragma('vm:testing:print-flow-graph')
+bool storeLoadWeakReference(Object v1) {
+  final obj = WeakReference(v1);
+  var v2 = obj.target;
+  return identical(v1, v2);
+}
+
+void matchIL$storeLoadWeakReference(FlowGraph graph) {
+  graph.match([
+    match.block('Graph'),
+    match.block('Function', [
+      'v1' << match.Parameter(index: 0),
+      'obj' << match.AllocateObject(),
+      match.StoreField('obj', 'v1', slot: 'WeakReference.target'),
+      'v2' << match.LoadField('obj', slot: 'WeakReference.target'),
+      'res' << match.StrictCompare('v1', 'v2', kind: '==='),
+      match.DartReturn('res'),
+    ]),
+  ]);
+}
+
+void main(List<String> args) {
+  loadLoadStrongRef(StrongRef(Object()));
+  loadLoadWeakReference(WeakReference(Object()));
+  storeLoadStrongRef(Object());
+  storeLoadWeakReference(Object());
+}
diff --git a/runtime/vm/compiler/backend/redundancy_elimination.cc b/runtime/vm/compiler/backend/redundancy_elimination.cc
index 851635c..d43ab2f 100644
--- a/runtime/vm/compiler/backend/redundancy_elimination.cc
+++ b/runtime/vm/compiler/backend/redundancy_elimination.cc
@@ -1427,6 +1427,11 @@
       instr->AsDefinition()->MayCreateUnsafeUntaggedPointer()) {
     return false;
   }
+  if (auto* load = instr->AsLoadField()) {
+    if (load->slot().is_weak()) {
+      return false;
+    }
+  }
   return instr->IsLoadField() || instr->IsLoadIndexed() ||
          instr->IsLoadStaticField();
 }
diff --git a/runtime/vm/compiler/backend/slot.cc b/runtime/vm/compiler/backend/slot.cc
index cd2e0ea..92b6aa2 100644
--- a/runtime/vm/compiler/backend/slot.cc
+++ b/runtime/vm/compiler/backend/slot.cc
@@ -68,14 +68,18 @@
 
 Slot* SlotCache::CreateNativeSlot(Slot::Kind kind) {
   switch (kind) {
-#define FIELD_FINAL true
-#define FIELD_VAR false
+#define FIELD_FLAGS_FINAL                                                      \
+  (Slot::IsImmutableBit::encode(true) | Slot::IsWeakBit::encode(false))
+#define FIELD_FLAGS_VAR                                                        \
+  (Slot::IsImmutableBit::encode(false) | Slot::IsWeakBit::encode(false))
+#define FIELD_FLAGS_WEAK                                                       \
+  (Slot::IsImmutableBit::encode(false) | Slot::IsWeakBit::encode(true))
 #define DEFINE_NULLABLE_TAGGED_NATIVE_DART_FIELD(ClassName, UnderlyingType,    \
                                                  FieldName, cid, mutability)   \
   case Slot::Kind::k##ClassName##_##FieldName:                                 \
     return new (zone_) Slot(                                                   \
         Slot::Kind::k##ClassName##_##FieldName,                                \
-        (Slot::IsImmutableBit::encode(FIELD_##mutability) |                    \
+        (FIELD_FLAGS_##mutability |                                            \
          Slot::IsCompressedBit::encode(                                        \
              ClassName::ContainsCompressedPointers())),                        \
         compiler::target::ClassName::FieldName##_offset(),                     \
@@ -94,7 +98,7 @@
   case Slot::Kind::k##ClassName##_##FieldName:                                 \
     return new (zone_) Slot(                                                   \
         Slot::Kind::k##ClassName##_##FieldName,                                \
-        (Slot::IsImmutableBit::encode(FIELD_##mutability) |                    \
+        (FIELD_FLAGS_##mutability |                                            \
          Slot::IsCompressedBit::encode(                                        \
              ClassName::ContainsCompressedPointers())),                        \
         compiler::target::ClassName::FieldName##_offset(),                     \
@@ -115,8 +119,7 @@
   case Slot::Kind::k##ClassName##_##FieldName:                                 \
     return new (zone_)                                                         \
         Slot(Slot::Kind::k##ClassName##_##FieldName,                           \
-             Slot::IsImmutableBit::encode(FIELD_##mutability) |                \
-                 Slot::IsNonTaggedBit::encode(true),                           \
+             FIELD_FLAGS_##mutability | Slot::IsNonTaggedBit::encode(true),    \
              compiler::target::ClassName::FieldName##_offset(),                \
              #ClassName "." #FieldName,                                        \
              CompileType::FromUnboxedRepresentation(kUnboxed##representation), \
@@ -131,7 +134,7 @@
   case Slot::Kind::k##ClassName##_##FieldName:                                 \
     return new (zone_)                                                         \
         Slot(Slot::Kind::k##ClassName##_##FieldName,                           \
-             Slot::IsImmutableBit::encode(FIELD_##mutability) |                \
+             FIELD_FLAGS_##mutability |                                        \
                  Slot::MayContainInnerPointerBit::encode(GcMayMove) |          \
                  Slot::IsNonTaggedBit::encode(true),                           \
              compiler::target::ClassName::FieldName##_offset(),                \
@@ -146,8 +149,7 @@
   case Slot::Kind::k##ClassName##_##FieldName:                                 \
     return new (zone_) Slot(                                                   \
         Slot::Kind::k##ClassName##_##FieldName,                                \
-        Slot::IsImmutableBit::encode(FIELD_##mutability) |                     \
-            Slot::HasUntaggedInstanceBit::encode(true),                        \
+        FIELD_FLAGS_##mutability | Slot::HasUntaggedInstanceBit::encode(true), \
         compiler::target::ClassName::FieldName##_offset(),                     \
         #ClassName "." #FieldName,                                             \
         CompileType(CompileType::kCanBeNull, CompileType::kCannotBeSentinel,   \
@@ -164,8 +166,7 @@
   case Slot::Kind::k##ClassName##_##FieldName:                                 \
     return new (zone_)                                                         \
         Slot(Slot::Kind::k##ClassName##_##FieldName,                           \
-             Slot::IsImmutableBit::encode(FIELD_##mutability) |                \
-                 Slot::IsNonTaggedBit::encode(true) |                          \
+             FIELD_FLAGS_##mutability | Slot::IsNonTaggedBit::encode(true) |   \
                  Slot::HasUntaggedInstanceBit::encode(true),                   \
              compiler::target::ClassName::FieldName##_offset(),                \
              #ClassName "." #FieldName,                                        \
@@ -181,7 +182,7 @@
   case Slot::Kind::k##ClassName##_##FieldName:                                 \
     return new (zone_)                                                         \
         Slot(Slot::Kind::k##ClassName##_##FieldName,                           \
-             Slot::IsImmutableBit::encode(FIELD_##mutability) |                \
+             FIELD_FLAGS_##mutability |                                        \
                  Slot::MayContainInnerPointerBit::encode(gc_may_move) |        \
                  Slot::IsNonTaggedBit::encode(true) |                          \
                  Slot::HasUntaggedInstanceBit::encode(true),                   \
@@ -192,8 +193,9 @@
 
 #undef DEFINE_UNTAGGED_NATIVE_NONDART_FIELD
 
-#undef FIELD_VAR
-#undef FIELD_FINAL
+#undef FIELD_FLAGS_FINAL
+#undef FIELD_FLAGS_VAR
+#undef FIELD_FLAGS_WEAK
     default:
       UNREACHABLE();
   }
diff --git a/runtime/vm/compiler/backend/slot.h b/runtime/vm/compiler/backend/slot.h
index 89a5735..c6bb983 100644
--- a/runtime/vm/compiler/backend/slot.h
+++ b/runtime/vm/compiler/backend/slot.h
@@ -41,7 +41,7 @@
 // The list of slots that correspond to nullable boxed fields of native
 // Dart objects in the following format:
 //
-//     V(class_name, underlying_type, field_name, exact_type, FINAL|VAR)
+//     V(class_name, underlying_type, field_name, exact_type, FINAL|VAR|WEAK)
 //
 // - class_name and field_name specify the name of the host class and the name
 //   of the field respectively;
@@ -50,17 +50,17 @@
 //   would only yield instances of this type);
 // - the last component specifies whether field behaves like a final field
 //   (i.e. initialized once at construction time and does not change after
-//   that) or like a non-final field.
+//   that), ordinary mutable field or a weak field (can be modified by GC).
 #define NULLABLE_TAGGED_NATIVE_DART_SLOTS_LIST(V)                              \
   V(Array, UntaggedArray, type_arguments, TypeArguments, FINAL)                \
   V(Finalizer, UntaggedFinalizer, type_arguments, TypeArguments, FINAL)        \
   V(FinalizerBase, UntaggedFinalizerBase, all_entries, Set, VAR)               \
   V(FinalizerBase, UntaggedFinalizerBase, detachments, Dynamic, VAR)           \
   V(FinalizerBase, UntaggedFinalizer, entries_collected, FinalizerEntry, VAR)  \
-  V(FinalizerEntry, UntaggedFinalizerEntry, value, Dynamic, VAR)               \
-  V(FinalizerEntry, UntaggedFinalizerEntry, detach, Dynamic, VAR)              \
+  V(FinalizerEntry, UntaggedFinalizerEntry, value, Dynamic, WEAK)              \
+  V(FinalizerEntry, UntaggedFinalizerEntry, detach, Dynamic, WEAK)             \
   V(FinalizerEntry, UntaggedFinalizerEntry, token, Dynamic, VAR)               \
-  V(FinalizerEntry, UntaggedFinalizerEntry, finalizer, FinalizerBase, VAR)     \
+  V(FinalizerEntry, UntaggedFinalizerEntry, finalizer, FinalizerBase, WEAK)    \
   V(FinalizerEntry, UntaggedFinalizerEntry, next, FinalizerEntry, VAR)         \
   V(Function, UntaggedFunction, signature, FunctionType, FINAL)                \
   V(Context, UntaggedContext, parent, Context, FINAL)                          \
@@ -81,9 +81,9 @@
   V(TypeParameters, UntaggedTypeParameters, flags, Array, FINAL)               \
   V(TypeParameters, UntaggedTypeParameters, bounds, TypeArguments, FINAL)      \
   V(TypeParameters, UntaggedTypeParameters, defaults, TypeArguments, FINAL)    \
-  V(WeakProperty, UntaggedWeakProperty, key, Dynamic, VAR)                     \
-  V(WeakProperty, UntaggedWeakProperty, value, Dynamic, VAR)                   \
-  V(WeakReference, UntaggedWeakReference, target, Dynamic, VAR)                \
+  V(WeakProperty, UntaggedWeakProperty, key, Dynamic, WEAK)                    \
+  V(WeakProperty, UntaggedWeakProperty, value, Dynamic, WEAK)                  \
+  V(WeakReference, UntaggedWeakReference, target, Dynamic, WEAK)               \
   V(WeakReference, UntaggedWeakReference, type_arguments, TypeArguments, FINAL)
 
 // The list of slots that correspond to non-nullable boxed fields of native
@@ -322,7 +322,7 @@
 // guaranteed to be a integer. This includes nullable integer slots, since
 // those slots may return a non-integer value (null). Such uses can
 // only use the following arguments for each entry:
-//     V(class_name, _, field_name, exact_type, FINAL|VAR)
+//     V(class_name, _, field_name, exact_type, FINAL|VAR|WEAK)
 #define TAGGED_NONINT_NATIVE_SLOTS_LIST(V)                                     \
   NULLABLE_TAGGED_NATIVE_DART_SLOTS_LIST(V)                                    \
   NONNULLABLE_NONINT_TAGGED_NATIVE_DART_SLOTS_LIST(V)                          \
@@ -331,7 +331,7 @@
 // For uses that need any native slot that is not guaranteed to contain an
 // integer, whether a Dart object or unboxed. Such uses can only use the
 // following arguments for each entry:
-//     V(class_name, _, field_name, _, FINAL|VAR)
+//     V(class_name, _, field_name, _, FINAL|VAR|WEAK)
 #define NOT_INT_NATIVE_SLOTS_LIST(V)                                           \
   TAGGED_NONINT_NATIVE_SLOTS_LIST(V)                                           \
   UNTAGGED_NATIVE_SLOTS_LIST(V)
@@ -339,7 +339,7 @@
 // For uses that need any native slot on Dart objects that contains a Dart
 // object (e.g., for write barrier purposes). Such uses can use the following
 // arguments for each entry:
-//     V(class_name, underlying_class, field_name, exact_type, FINAL|VAR)
+//     V(class_name, underlying_class, field_name, exact_type, FINAL|VAR|WEAK)
 #define TAGGED_NATIVE_DART_SLOTS_LIST(V)                                       \
   NULLABLE_TAGGED_NATIVE_DART_SLOTS_LIST(V)                                    \
   NONNULLABLE_INT_TAGGED_NATIVE_DART_SLOTS_LIST(V)                             \
@@ -355,14 +355,14 @@
 
 // For uses that need any native slot that contains a Dart object. Such uses can
 // only use the following arguments for each entry:
-//     V(class_name, _, field_name, exact_type, FINAL|VAR)
+//     V(class_name, _, field_name, exact_type, FINAL|VAR|WEAK)
 #define TAGGED_NATIVE_SLOTS_LIST(V)                                            \
   TAGGED_INT_NATIVE_SLOTS_LIST(V)                                              \
   TAGGED_NONINT_NATIVE_SLOTS_LIST(V)
 
 // For uses that need all native slots. Such uses can only use the following
 // arguments for each entry:
-//     V(class_name, _, field_name, _, FINAL|VAR)
+//     V(class_name, _, field_name, _, FINAL|VAR|WEAK)
 #define NATIVE_SLOTS_LIST(V)                                                   \
   TAGGED_NATIVE_SLOTS_LIST(V)                                                  \
   NOT_TAGGED_NATIVE_SLOTS_LIST(V)
@@ -520,6 +520,8 @@
 
   bool is_immutable() const { return IsImmutableBit::decode(flags_); }
 
+  bool is_weak() const { return IsWeakBit::decode(flags_); }
+
   // Returns true if properties of this slot were based on the guarded state
   // of the corresponding Dart field.
   bool is_guarded_field() const { return IsGuardedBit::decode(flags_); }
@@ -620,8 +622,9 @@
   CompileType type_;
 
   using IsImmutableBit = BitField<decltype(flags_), bool, 0, 1>;
-  using IsGuardedBit =
+  using IsWeakBit =
       BitField<decltype(flags_), bool, IsImmutableBit::kNextBit, 1>;
+  using IsGuardedBit = BitField<decltype(flags_), bool, IsWeakBit::kNextBit, 1>;
   using IsCompressedBit =
       BitField<decltype(flags_), bool, IsGuardedBit::kNextBit, 1>;
   // Stores whether a field isn't tagged so that tagged is the default value