Version 2.17.0-8.0.dev

Merge commit '0a9fb1723e393a1442c555b5c18249b652a06e33' into 'dev'
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b72adbc..0776b8b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,11 @@
 - Add `Finalizer` and `WeakReference` which can potentially detect when
   objects are "garbage collected".
 
+#### `dart:ffi`
+
+- Add `ref=` and `[]=` methods to the `StructPointer` and `UnionPointer`
+  extensions. They copy a compound instance into a native memory region.
+
 #### `dart:indexed_db`
 
 - `IdbFactory.supportsDatabaseNames` has been deprecated. It will always return
diff --git a/pkg/_fe_analyzer_shared/pubspec.yaml b/pkg/_fe_analyzer_shared/pubspec.yaml
index 9b35add..16c442b 100644
--- a/pkg/_fe_analyzer_shared/pubspec.yaml
+++ b/pkg/_fe_analyzer_shared/pubspec.yaml
@@ -1,5 +1,5 @@
 name: _fe_analyzer_shared
-version: 32.0.0
+version: 33.0.0
 description: Logic that is shared between the front_end and analyzer packages.
 homepage: https://github.com/dart-lang/sdk/tree/master/pkg/_fe_analyzer_shared
 
diff --git a/pkg/analysis_server/test/services/completion/dart/completion_check.dart b/pkg/analysis_server/test/services/completion/dart/completion_check.dart
index 4196283..3cd03b5 100644
--- a/pkg/analysis_server/test/services/completion/dart/completion_check.dart
+++ b/pkg/analysis_server/test/services/completion/dart/completion_check.dart
@@ -59,6 +59,7 @@
 
 extension CompletionResponseExtension
     on CheckTarget<CompletionResponseForTesting> {
+  @useResult
   CheckTarget<bool> get isIncomplete {
     return nest(
       value.isIncomplete,
@@ -66,6 +67,7 @@
     );
   }
 
+  @useResult
   CheckTarget<int> get replacementLength {
     return nest(
       value.replacementLength,
@@ -73,6 +75,7 @@
     );
   }
 
+  @useResult
   CheckTarget<int> get replacementOffset {
     return nest(
       value.replacementOffset,
@@ -80,6 +83,7 @@
     );
   }
 
+  @useResult
   CheckTarget<List<CompletionSuggestionForTesting>> get suggestions {
     var suggestions = value.suggestions.map((e) {
       return CompletionSuggestionForTesting(
@@ -117,6 +121,7 @@
 
 extension CompletionSuggestionExtension
     on CheckTarget<CompletionSuggestionForTesting> {
+  @useResult
   CheckTarget<String> get completion {
     return nest(
       value.suggestion.completion,
@@ -124,6 +129,7 @@
     );
   }
 
+  @useResult
   CheckTarget<String?> get defaultArgumentListString {
     return nest(
       value.suggestion.defaultArgumentListString,
@@ -131,6 +137,7 @@
     );
   }
 
+  @useResult
   CheckTarget<List<int>?> get defaultArgumentListTextRanges {
     return nest(
       value.suggestion.defaultArgumentListTextRanges,
@@ -138,6 +145,7 @@
     );
   }
 
+  @useResult
   CheckTarget<String?> get docComplete {
     return nest(
       value.suggestion.docComplete,
@@ -145,6 +153,7 @@
     );
   }
 
+  @useResult
   CheckTarget<String?> get docSummary {
     return nest(
       value.suggestion.docSummary,
@@ -152,6 +161,7 @@
     );
   }
 
+  @useResult
   CheckTarget<Element?> get element {
     return nest(
       value.suggestion.element,
@@ -207,6 +217,7 @@
     element.isNotNull.kind.isTopLevelVariable;
   }
 
+  @useResult
   CheckTarget<CompletionSuggestionKind> get kind {
     return nest(
       value.suggestion.kind,
@@ -214,6 +225,7 @@
     );
   }
 
+  @useResult
   CheckTarget<String?> get libraryUriToImport {
     return nest(
       value.suggestion.isNotImported == true
@@ -223,6 +235,7 @@
     );
   }
 
+  @useResult
   CheckTarget<String?> get parameterType {
     return nest(
       value.suggestion.parameterType,
@@ -231,6 +244,7 @@
   }
 
   /// Return the effective replacement length.
+  @useResult
   CheckTarget<int> get replacementLength {
     return nest(
       value.replacementLength,
@@ -239,6 +253,7 @@
   }
 
   /// Return the effective replacement offset.
+  @useResult
   CheckTarget<int> get replacementOffset {
     return nest(
       value.replacementOffset,
@@ -246,6 +261,7 @@
     );
   }
 
+  @useResult
   CheckTarget<String?> get returnType {
     return nest(
       value.suggestion.returnType,
@@ -253,6 +269,7 @@
     );
   }
 
+  @useResult
   CheckTarget<int> get selectionLength {
     return nest(
       value.suggestion.selectionLength,
@@ -260,6 +277,7 @@
     );
   }
 
+  @useResult
   CheckTarget<int> get selectionOffset {
     return nest(
       value.suggestion.selectionOffset,
@@ -297,27 +315,9 @@
   }
 }
 
-extension CompletionSuggestionListExtension
-    on CheckTarget<List<CompletionSuggestionForTesting>> {
-  CheckTarget<Iterable<String>> get completions {
-    return nest(
-      value.map((e) => e.suggestion.completion).toList(),
-      (selected) => 'has completions ${valueStr(selected)}',
-    );
-  }
-
-  CheckTarget<Iterable<CompletionSuggestionForTesting>> get withElementClass {
-    return nest(
-      value.where((e) {
-        return e.suggestion.element?.kind == ElementKind.CLASS;
-      }).toList(),
-      (selected) => 'withElementClass ${valueStr(selected)}',
-    );
-  }
-}
-
 extension CompletionSuggestionsExtension
     on CheckTarget<Iterable<CompletionSuggestionForTesting>> {
+  @useResult
   CheckTarget<List<String>> get completions {
     return nest(
       value.map((e) => e.suggestion.completion).toList(),
@@ -325,6 +325,7 @@
     );
   }
 
+  @useResult
   CheckTarget<Iterable<CompletionSuggestionForTesting>> get namedArguments {
     var result = value
         .where((suggestion) =>
@@ -336,9 +337,20 @@
       (selected) => 'named arguments ${valueStr(selected)}',
     );
   }
+
+  @useResult
+  CheckTarget<Iterable<CompletionSuggestionForTesting>> get withElementClass {
+    return nest(
+      value.where((e) {
+        return e.suggestion.element?.kind == ElementKind.CLASS;
+      }).toList(),
+      (selected) => 'withElementClass ${valueStr(selected)}',
+    );
+  }
 }
 
 extension ElementExtension on CheckTarget<Element> {
+  @useResult
   CheckTarget<ElementKind> get kind {
     return nest(
       value.kind,
@@ -346,6 +358,7 @@
     );
   }
 
+  @useResult
   CheckTarget<String> get name {
     return nest(
       value.name,
diff --git a/pkg/analyzer/CHANGELOG.md b/pkg/analyzer/CHANGELOG.md
index 1d47825..c0df15a 100644
--- a/pkg/analyzer/CHANGELOG.md
+++ b/pkg/analyzer/CHANGELOG.md
@@ -1,7 +1,8 @@
-## 3.1.0-dev
+## 3.1.0
 * New internal API for `package:dart_style`.
 * Removed deprecated non-API `MockSdk` class.
 * Removed deprecated `AnalysisDriver` constructor.
+* Updated the current language version to `2.17`.
 
 ## 3.0.0
 * Removed deprecated `DartType.aliasElement/aliasArguments`.
diff --git a/pkg/analyzer/pubspec.yaml b/pkg/analyzer/pubspec.yaml
index e706822..547e31e 100644
--- a/pkg/analyzer/pubspec.yaml
+++ b/pkg/analyzer/pubspec.yaml
@@ -1,5 +1,5 @@
 name: analyzer
-version: 3.1.0-dev
+version: 3.1.0
 description: This package provides a library that performs static analysis of Dart code.
 homepage: https://github.com/dart-lang/sdk/tree/main/pkg/analyzer
 
@@ -7,7 +7,7 @@
   sdk: '>=2.14.0 <3.0.0'
 
 dependencies:
-  _fe_analyzer_shared: ^32.0.0
+  _fe_analyzer_shared: ^33.0.0
   cli_util: ^0.3.0
   collection: ^1.15.0
   convert: ^3.0.0
diff --git a/pkg/dartdev/lib/src/commands/doc.dart b/pkg/dartdev/lib/src/commands/doc.dart
index d039183..8faff48 100644
--- a/pkg/dartdev/lib/src/commands/doc.dart
+++ b/pkg/dartdev/lib/src/commands/doc.dart
@@ -12,20 +12,20 @@
 import '../core.dart';
 import '../sdk.dart';
 
-/// A command to create a new project from a set of templates.
+/// A command to generate documentation for a project.
 class DocCommand extends DartdevCommand {
   static const String cmdName = 'doc';
 
-  DocCommand({bool verbose = false})
-      : super(
-          cmdName,
-          'Generate HTML API documentation from Dart documentation comments.',
-          verbose,
-        ) {
+  static const String cmdDescription = '''
+Generate API documentation for Dart projects.
+
+For additional documentation generation options, see the 'dartdoc_options.yaml' file documentation at https://dart.dev/go/dartdoc-options-file.''';
+
+  DocCommand({bool verbose = false}) : super(cmdName, cmdDescription, verbose) {
     argParser.addOption(
       'output-dir',
       abbr: 'o',
-      defaultsTo: path.join('.', 'doc', 'api'),
+      defaultsTo: path.join('doc', 'api'),
       help: 'Output directory',
     );
     argParser.addFlag(
@@ -75,8 +75,9 @@
 
     // Call dartdoc.
     if (verbose) {
-      log.stdout('Calling dartdoc with the following options: $options');
+      log.stdout('Using the following options: $options');
     }
+
     final packageConfigProvider = PhysicalPackageConfigProvider();
     final packageBuilder = PubPackageBuilder(
         config, pubPackageMetaProvider, packageConfigProvider);
diff --git a/pkg/vm/lib/transformations/ffi/common.dart b/pkg/vm/lib/transformations/ffi/common.dart
index 1152655..4446159 100644
--- a/pkg/vm/lib/transformations/ffi/common.dart
+++ b/pkg/vm/lib/transformations/ffi/common.dart
@@ -194,10 +194,14 @@
   final Procedure offsetByMethod;
   final Procedure elementAtMethod;
   final Procedure addressGetter;
-  final Procedure structPointerRef;
-  final Procedure structPointerElemAt;
-  final Procedure unionPointerRef;
-  final Procedure unionPointerElemAt;
+  final Procedure structPointerGetRef;
+  final Procedure structPointerSetRef;
+  final Procedure structPointerGetElemAt;
+  final Procedure structPointerSetElemAt;
+  final Procedure unionPointerGetRef;
+  final Procedure unionPointerSetRef;
+  final Procedure unionPointerGetElemAt;
+  final Procedure unionPointerSetElemAt;
   final Procedure structArrayElemAt;
   final Procedure unionArrayElemAt;
   final Procedure arrayArrayElemAt;
@@ -371,14 +375,22 @@
         arrayConstructor = index.getConstructor('dart:ffi', 'Array', '_'),
         fromAddressInternal =
             index.getTopLevelProcedure('dart:ffi', '_fromAddress'),
-        structPointerRef =
+        structPointerGetRef =
             index.getProcedure('dart:ffi', 'StructPointer', 'get:ref'),
-        structPointerElemAt =
+        structPointerSetRef =
+            index.getProcedure('dart:ffi', 'StructPointer', 'set:ref'),
+        structPointerGetElemAt =
             index.getProcedure('dart:ffi', 'StructPointer', '[]'),
-        unionPointerRef =
+        structPointerSetElemAt =
+            index.getProcedure('dart:ffi', 'StructPointer', '[]='),
+        unionPointerGetRef =
             index.getProcedure('dart:ffi', 'UnionPointer', 'get:ref'),
-        unionPointerElemAt =
+        unionPointerSetRef =
+            index.getProcedure('dart:ffi', 'UnionPointer', 'set:ref'),
+        unionPointerGetElemAt =
             index.getProcedure('dart:ffi', 'UnionPointer', '[]'),
+        unionPointerSetElemAt =
+            index.getProcedure('dart:ffi', 'UnionPointer', '[]='),
         structArrayElemAt = index.getProcedure('dart:ffi', 'StructArray', '[]'),
         unionArrayElemAt = index.getProcedure('dart:ffi', 'UnionArray', '[]'),
         arrayArrayElemAt = index.getProcedure('dart:ffi', 'ArrayArray', '[]'),
diff --git a/pkg/vm/lib/transformations/ffi/use_sites.dart b/pkg/vm/lib/transformations/ffi/use_sites.dart
index d0e44333..ba1a0b1 100644
--- a/pkg/vm/lib/transformations/ffi/use_sites.dart
+++ b/pkg/vm/lib/transformations/ffi/use_sites.dart
@@ -169,15 +169,24 @@
           fileOffset: node.fileOffset,
         );
       }
-      if (target == structPointerRef ||
-          target == structPointerElemAt ||
-          target == unionPointerRef ||
-          target == unionPointerElemAt) {
+      if (target == structPointerGetRef ||
+          target == structPointerGetElemAt ||
+          target == unionPointerGetRef ||
+          target == unionPointerGetElemAt) {
         final DartType nativeType = node.arguments.types[0];
 
         _ensureNativeTypeValid(nativeType, node, allowCompounds: true);
 
-        return _replaceRef(node);
+        return _replaceGetRef(node);
+      } else if (target == structPointerSetRef ||
+          target == structPointerSetElemAt ||
+          target == unionPointerSetRef ||
+          target == unionPointerSetElemAt) {
+        final DartType nativeType = node.arguments.types[0];
+
+        _ensureNativeTypeValid(nativeType, node, allowCompounds: true);
+
+        return _replaceSetRef(node);
       } else if (target == structArrayElemAt || target == unionArrayElemAt) {
         final DartType nativeType = node.arguments.types[0];
 
@@ -533,7 +542,7 @@
     return StaticGet(field);
   }
 
-  Expression _replaceRef(StaticInvocation node) {
+  Expression _replaceGetRef(StaticInvocation node) {
     final dartType = node.arguments.types[0];
     final clazz = (dartType as InterfaceType).classNode;
     final constructor = clazz.constructors
@@ -555,6 +564,38 @@
     return ConstructorInvocation(constructor, Arguments([pointer]));
   }
 
+  /// Replaces a `.ref=` or `[]=` on a compound pointer extension with a memcopy
+  /// call.
+  Expression _replaceSetRef(StaticInvocation node) {
+    final target = node.arguments.positional[0]; // Receiver of extension
+
+    final Expression source, targetOffset;
+
+    if (node.arguments.positional.length == 3) {
+      // []= call, args are (receiver, index, source)
+      source = getCompoundTypedDataBaseField(
+          node.arguments.positional[2], node.fileOffset);
+      targetOffset = multiply(node.arguments.positional[1],
+          _inlineSizeOf(node.arguments.types[0] as InterfaceType)!);
+    } else {
+      // .ref= call, args are (receiver, source)
+      source = getCompoundTypedDataBaseField(
+          node.arguments.positional[1], node.fileOffset);
+      targetOffset = ConstantExpression(IntConstant(0));
+    }
+
+    return StaticInvocation(
+      memCopy,
+      Arguments([
+        target,
+        targetOffset,
+        source,
+        ConstantExpression(IntConstant(0)),
+        _inlineSizeOf(node.arguments.types[0] as InterfaceType)!,
+      ]),
+    );
+  }
+
   Expression _replaceRefArray(StaticInvocation node) {
     final dartType = node.arguments.types[0];
     final clazz = (dartType as InterfaceType).classNode;
diff --git a/pkg/vm/testcases/transformations/ffi/compound_copies.dart b/pkg/vm/testcases/transformations/ffi/compound_copies.dart
new file mode 100644
index 0000000..4717b9b
--- /dev/null
+++ b/pkg/vm/testcases/transformations/ffi/compound_copies.dart
@@ -0,0 +1,27 @@
+import 'dart:ffi';
+
+import 'package:ffi/ffi.dart';
+
+class Coordinate extends Struct {
+  @Int64()
+  external int x;
+
+  @Int64()
+  external int y;
+
+  void copyInto(Pointer<Coordinate> ptr) {
+    ptr.ref = this;
+  }
+}
+
+class SomeUnion extends Union {
+  external Coordinate coordinate;
+  @Int64()
+  external int id;
+
+  void copyIntoAtIndex(Pointer<SomeUnion> ptr, int index) {
+    ptr[index] = this;
+  }
+}
+
+void main() {}
diff --git a/pkg/vm/testcases/transformations/ffi/compound_copies.dart.expect b/pkg/vm/testcases/transformations/ffi/compound_copies.dart.expect
new file mode 100644
index 0000000..98ee713
--- /dev/null
+++ b/pkg/vm/testcases/transformations/ffi/compound_copies.dart.expect
@@ -0,0 +1,87 @@
+library #lib /*isNonNullableByDefault*/;
+import self as self;
+import "dart:core" as core;
+import "dart:ffi" as ffi;
+import "dart:typed_data" as typ;
+import "dart:_internal" as _in;
+
+import "dart:ffi";
+import "package:ffi/ffi.dart";
+
+@#C6
+class Coordinate extends ffi::Struct {
+  synthetic constructor •() → self::Coordinate
+    : super ffi::Struct::•()
+    ;
+  constructor #fromTypedDataBase(core::Object #typedDataBase) → self::Coordinate
+    : super ffi::Struct::_fromTypedDataBase(#typedDataBase)
+    ;
+  @#C7
+  get x() → core::int
+    return ffi::_loadInt64(this.{ffi::_Compound::_typedDataBase}{core::Object}, #C9.{core::List::[]}(ffi::_abi()){(core::int) → core::int*});
+  @#C7
+  set x(core::int #externalFieldValue) → void
+    return ffi::_storeInt64(this.{ffi::_Compound::_typedDataBase}{core::Object}, #C9.{core::List::[]}(ffi::_abi()){(core::int) → core::int*}, #externalFieldValue);
+  @#C7
+  get y() → core::int
+    return ffi::_loadInt64(this.{ffi::_Compound::_typedDataBase}{core::Object}, #C11.{core::List::[]}(ffi::_abi()){(core::int) → core::int*});
+  @#C7
+  set y(core::int #externalFieldValue) → void
+    return ffi::_storeInt64(this.{ffi::_Compound::_typedDataBase}{core::Object}, #C11.{core::List::[]}(ffi::_abi()){(core::int) → core::int*}, #externalFieldValue);
+  method copyInto(ffi::Pointer<self::Coordinate> ptr) → void {
+    ffi::_memCopy(ptr, #C8, this.{ffi::_Compound::_typedDataBase}{core::Object}, #C8, self::Coordinate::#sizeOf);
+  }
+  @#C13
+  static get #sizeOf() → core::int*
+    return #C15.{core::List::[]}(ffi::_abi()){(core::int) → core::int*};
+}
+@#C19
+class SomeUnion extends ffi::Union {
+  synthetic constructor •() → self::SomeUnion
+    : super ffi::Union::•()
+    ;
+  constructor #fromTypedDataBase(core::Object #typedDataBase) → self::SomeUnion
+    : super ffi::Union::_fromTypedDataBase(#typedDataBase)
+    ;
+  get coordinate() → self::Coordinate
+    return new self::Coordinate::#fromTypedDataBase( block {
+      core::Object #typedDataBase = this.{ffi::_Compound::_typedDataBase}{core::Object};
+      core::int #offset = #C9.{core::List::[]}(ffi::_abi()){(core::int) → core::int*};
+    } =>#typedDataBase is ffi::Pointer<dynamic> ?{core::Object} ffi::_fromAddress<self::Coordinate>(#typedDataBase.{ffi::Pointer::address}{core::int}.{core::num::+}(#offset){(core::num) → core::num}) : let typ::TypedData #typedData = _in::unsafeCast<typ::TypedData>(#typedDataBase) in #typedData.{typ::TypedData::buffer}{typ::ByteBuffer}.{typ::ByteBuffer::asUint8List}(#typedData.{typ::TypedData::offsetInBytes}{core::int}.{core::num::+}(#offset){(core::num) → core::num}, #C15.{core::List::[]}(ffi::_abi()){(core::int) → core::int*}){([core::int, core::int?]) → typ::Uint8List});
+  set coordinate(self::Coordinate #externalFieldValue) → void
+    return ffi::_memCopy(this.{ffi::_Compound::_typedDataBase}{core::Object}, #C9.{core::List::[]}(ffi::_abi()){(core::int) → core::int*}, #externalFieldValue.{ffi::_Compound::_typedDataBase}{core::Object}, #C8, #C15.{core::List::[]}(ffi::_abi()){(core::int) → core::int*});
+  @#C7
+  get id() → core::int
+    return ffi::_loadInt64(this.{ffi::_Compound::_typedDataBase}{core::Object}, #C9.{core::List::[]}(ffi::_abi()){(core::int) → core::int*});
+  @#C7
+  set id(core::int #externalFieldValue) → void
+    return ffi::_storeInt64(this.{ffi::_Compound::_typedDataBase}{core::Object}, #C9.{core::List::[]}(ffi::_abi()){(core::int) → core::int*}, #externalFieldValue);
+  method copyIntoAtIndex(ffi::Pointer<self::SomeUnion> ptr, core::int index) → void {
+    ffi::_memCopy(ptr, index.{core::num::*}(self::SomeUnion::#sizeOf){(core::num) → core::num}, this.{ffi::_Compound::_typedDataBase}{core::Object}, #C8, self::SomeUnion::#sizeOf);
+  }
+  @#C13
+  static get #sizeOf() → core::int*
+    return #C15.{core::List::[]}(ffi::_abi()){(core::int) → core::int*};
+}
+static method main() → void {}
+constants  {
+  #C1 = "vm:ffi:struct-fields"
+  #C2 = TypeLiteralConstant(ffi::Int64)
+  #C3 = <core::Type>[#C2, #C2]
+  #C4 = null
+  #C5 = ffi::_FfiStructLayout {fieldTypes:#C3, packing:#C4}
+  #C6 = core::pragma {name:#C1, options:#C5}
+  #C7 = ffi::Int64 {}
+  #C8 = 0
+  #C9 = <core::int*>[#C8, #C8, #C8, #C8, #C8, #C8, #C8, #C8, #C8, #C8, #C8, #C8, #C8, #C8, #C8, #C8, #C8, #C8]
+  #C10 = 8
+  #C11 = <core::int*>[#C10, #C10, #C10, #C10, #C10, #C10, #C10, #C10, #C10, #C10, #C10, #C10, #C10, #C10, #C10, #C10, #C10, #C10]
+  #C12 = "vm:prefer-inline"
+  #C13 = core::pragma {name:#C12, options:#C4}
+  #C14 = 16
+  #C15 = <core::int*>[#C14, #C14, #C14, #C14, #C14, #C14, #C14, #C14, #C14, #C14, #C14, #C14, #C14, #C14, #C14, #C14, #C14, #C14]
+  #C16 = TypeLiteralConstant(self::Coordinate)
+  #C17 = <core::Type>[#C16, #C2]
+  #C18 = ffi::_FfiStructLayout {fieldTypes:#C17, packing:#C4}
+  #C19 = core::pragma {name:#C1, options:#C18}
+}
diff --git a/sdk/lib/_internal/vm/lib/ffi_patch.dart b/sdk/lib/_internal/vm/lib/ffi_patch.dart
index cec0bfe..7ecccf0 100644
--- a/sdk/lib/_internal/vm/lib/ffi_patch.dart
+++ b/sdk/lib/_internal/vm/lib/ffi_patch.dart
@@ -924,8 +924,16 @@
       throw "UNREACHABLE: This case should have been rewritten in the CFE.";
 
   @patch
+  set ref(T value) =>
+      throw "UNREACHABLE: This case should have been rewritten in the CFE";
+
+  @patch
   T operator [](int index) =>
       throw "UNREACHABLE: This case should have been rewritten in the CFE.";
+
+  @patch
+  void operator []=(int index, T value) =>
+      throw "UNREACHABLE: This case should have been rewritten in the CFE.";
 }
 
 extension UnionPointer<T extends Union> on Pointer<T> {
@@ -934,8 +942,16 @@
       throw "UNREACHABLE: This case should have been rewritten in the CFE.";
 
   @patch
+  set ref(T value) =>
+      throw "UNREACHABLE: This case should have been rewritten in the CFE";
+
+  @patch
   T operator [](int index) =>
       throw "UNREACHABLE: This case should have been rewritten in the CFE.";
+
+  @patch
+  void operator []=(int index, T value) =>
+      throw "UNREACHABLE: This case should have been rewritten in the CFE.";
 }
 
 extension AbiSpecificIntegerPointer<T extends AbiSpecificInteger>
diff --git a/sdk/lib/ffi/ffi.dart b/sdk/lib/ffi/ffi.dart
index a94f104..b1d53a6 100644
--- a/sdk/lib/ffi/ffi.dart
+++ b/sdk/lib/ffi/ffi.dart
@@ -648,14 +648,20 @@
 
 /// Extension on [Pointer] specialized for the type argument [Struct].
 extension StructPointer<T extends Struct> on Pointer<T> {
-  /// Creates a reference to access the fields of this struct backed by native
-  /// memory at [address].
+  /// A Dart view of the struct referenced by this pointer.
   ///
+  /// Reading [ref] creates a reference accessing the fields of this struct
+  /// backed by native memory at [address].
   /// The [address] must be aligned according to the struct alignment rules of
   /// the platform.
   ///
-  /// This extension method must be invoked with a compile-time constant [T].
+  /// Assigning to [ref] copies contents of the struct into the native memory
+  /// starting at [address].
+  ///
+  /// This extension method must be invoked on a receiver of type `Pointer<T>`
+  /// where `T` is a compile-time constant type.
   external T get ref;
+  external set ref(T value);
 
   /// Creates a reference to access the fields of this struct backed by native
   /// memory at `address + sizeOf<T>() * index`.
@@ -663,20 +669,34 @@
   /// The [address] must be aligned according to the struct alignment rules of
   /// the platform.
   ///
-  /// This extension method must be invoked with a compile-time constant [T].
+  /// This extension method must be invoked on a receiver of type `Pointer<T>`
+  /// where `T` is a compile-time constant type.
   external T operator [](int index);
+
+  /// Copies the [value] struct into native memory, starting at
+  /// `address * sizeOf<T>() * index`.
+  ///
+  /// This extension method must be invoked on a receiver of type `Pointer<T>`
+  /// where `T` is a compile-time constant type.
+  external void operator []=(int index, T value);
 }
 
 /// Extension on [Pointer] specialized for the type argument [Union].
 extension UnionPointer<T extends Union> on Pointer<T> {
-  /// Creates a reference to access the fields of this union backed by native
-  /// memory at [address].
+  /// A Dart view of the union referenced by this pointer.
   ///
+  /// Reading [ref] creates a reference accessing the fields of this union
+  /// backed by native memory at [address].
   /// The [address] must be aligned according to the union alignment rules of
   /// the platform.
   ///
-  /// This extension method must be invoked with a compile-time constant [T].
+  /// Assigning to [ref] copies contents of the union into the native memory
+  /// starting at [address].
+  ///
+  /// This extension method must be invoked on a receiver of type `Pointer<T>`
+  /// where `T` is a compile-time constant type.
   external T get ref;
+  external set ref(T value);
 
   /// Creates a reference to access the fields of this union backed by native
   /// memory at `address + sizeOf<T>() * index`.
@@ -684,8 +704,16 @@
   /// The [address] must be aligned according to the union alignment rules of
   /// the platform.
   ///
-  /// This extension method must be invoked with a compile-time constant [T].
+  /// This extension method must be invoked on a receiver of type `Pointer<T>`
+  /// where `T` is a compile-time constant type.
   external T operator [](int index);
+
+  /// Copies the [value] union into native memory, starting at
+  /// `address * sizeOf<T>() * index`.
+  ///
+  /// This extension method must be invoked on a receiver of type `Pointer<T>`
+  /// where `T` is a compile-time constant type.
+  external void operator []=(int index, T value);
 }
 
 /// Extension on [Pointer] specialized for the type argument
@@ -713,13 +741,15 @@
 
 /// Bounds checking indexing methods on [Array]s of [Struct].
 extension StructArray<T extends Struct> on Array<T> {
-  /// This extension method must be invoked with a compile-time constant [T].
+  /// This extension method must be invoked on a receiver of type `Pointer<T>`
+  /// where `T` is a compile-time constant type.
   external T operator [](int index);
 }
 
 /// Bounds checking indexing methods on [Array]s of [Union].
 extension UnionArray<T extends Union> on Array<T> {
-  /// This extension method must be invoked with a compile-time constant [T].
+  /// This extension method must be invoked on a receiver of type `Pointer<T>`
+  /// where `T` is a compile-time constant type.
   external T operator [](int index);
 }
 
diff --git a/tests/ffi/extension_methods_test.dart b/tests/ffi/extension_methods_test.dart
index b8f187e..fb71378 100644
--- a/tests/ffi/extension_methods_test.dart
+++ b/tests/ffi/extension_methods_test.dart
@@ -11,6 +11,7 @@
   for (int i = 0; i < 100; i++) {
     testStoreLoad();
     testReifiedGeneric();
+    testCompoundLoadAndStore();
   }
 }
 
@@ -50,6 +51,14 @@
   foo.a = 1;
   Expect.equals(1, foo.a);
   calloc.free(p3);
+
+  final p4 = calloc<Foo>(2);
+  Foo src = p4[1];
+  src.a = 2;
+  p4.ref = src;
+  Foo dst = p4.ref;
+  Expect.equals(2, dst.a);
+  calloc.free(p4);
 }
 
 testReifiedGeneric() {
@@ -59,7 +68,37 @@
   calloc.free(p);
 }
 
+testCompoundLoadAndStore() {
+  final foos = calloc<Foo>(10);
+  final reference = foos.ref..a = 10;
+
+  for (var i = 1; i < 9; i++) {
+    foos[i] = reference;
+    Expect.isTrue(foos[i].a == 10);
+
+    foos.elementAt(i).ref = reference;
+    Expect.isTrue(foos.elementAt(i).ref.a == 10);
+  }
+
+  final bars = calloc<Bar>(10);
+  bars[0].foo = reference;
+
+  for (var i = 1; i < 9; i++) {
+    bars[i] = bars[0];
+    Expect.isTrue(bars.elementAt(i).ref.foo.a == 10);
+  }
+
+  calloc.free(foos);
+  calloc.free(bars);
+}
+
 class Foo extends Struct {
   @Int8()
   external int a;
 }
+
+class Bar extends Union {
+  external Foo foo;
+  @Int32()
+  external int baz;
+}
diff --git a/tests/ffi_2/extension_methods_test.dart b/tests/ffi_2/extension_methods_test.dart
index 9d96f41..cb124af 100644
--- a/tests/ffi_2/extension_methods_test.dart
+++ b/tests/ffi_2/extension_methods_test.dart
@@ -13,6 +13,7 @@
   for (int i = 0; i < 100; i++) {
     testStoreLoad();
     testReifiedGeneric();
+    testCompoundLoadAndStore();
   }
 }
 
@@ -52,6 +53,14 @@
   foo.a = 1;
   Expect.equals(1, foo.a);
   calloc.free(p3);
+
+  final p4 = calloc<Foo>(2);
+  Foo src = p4[1];
+  src.a = 2;
+  p4.ref = src;
+  Foo dst = p4.ref;
+  Expect.equals(2, dst.a);
+  calloc.free(p4);
 }
 
 testReifiedGeneric() {
@@ -61,7 +70,37 @@
   calloc.free(p);
 }
 
+testCompoundLoadAndStore() {
+  final foos = calloc<Foo>(10);
+  final reference = foos.ref..a = 10;
+
+  for (var i = 1; i < 9; i++) {
+    foos[i] = reference;
+    Expect.isTrue(foos[i].a == 10);
+
+    foos.elementAt(i).ref = reference;
+    Expect.isTrue(foos.elementAt(i).ref.a == 10);
+  }
+
+  final bars = calloc<Bar>(10);
+  bars[0].foo = reference;
+
+  for (var i = 1; i < 9; i++) {
+    bars[i] = bars[0];
+    Expect.isTrue(bars.elementAt(i).ref.foo.a == 10);
+  }
+
+  calloc.free(foos);
+  calloc.free(bars);
+}
+
 class Foo extends Struct {
   @Int8()
   int a;
 }
+
+class Bar extends Union {
+  Foo foo;
+  @Int32()
+  int baz;
+}
diff --git a/tools/VERSION b/tools/VERSION
index 1c51070..eafe4ae 100644
--- a/tools/VERSION
+++ b/tools/VERSION
@@ -27,5 +27,5 @@
 MAJOR 2
 MINOR 17
 PATCH 0
-PRERELEASE 7
+PRERELEASE 8
 PRERELEASE_PATCH 0
\ No newline at end of file