Version 3.0.0-233.0.dev

Merge 95e6dba347b3db04f7cf4958032db9610df5084a into dev
diff --git a/pkg/_fe_analyzer_shared/test/flow_analysis/flow_analysis_test.dart b/pkg/_fe_analyzer_shared/test/flow_analysis/flow_analysis_test.dart
index 7b4046f..4c9fded 100644
--- a/pkg/_fe_analyzer_shared/test/flow_analysis/flow_analysis_test.dart
+++ b/pkg/_fe_analyzer_shared/test/flow_analysis/flow_analysis_test.dart
@@ -8080,6 +8080,99 @@
       });
     });
 
+    group('Wildcard pattern:', () {
+      group('covers matched type:', () {
+        test('without promotion candidate', () {
+          // In `if(<some int> case num _) ...`, the `else` branch should be
+          // unreachable because the type `num` fully covers the type `int`.
+          h.run([
+            ifCase(expr('int'), wildcard(type: 'num'), [
+              checkReachable(true),
+            ], [
+              checkReachable(false),
+            ]),
+          ]);
+        });
+
+        test('with promotion candidate', () {
+          // In `if(x case num _) ...`, the `else` branch should be unreachable
+          // because the type `num` fully covers the type `int`.
+          var x = Var('x');
+          h.run([
+            declare(x, type: 'int'),
+            ifCase(x.expr, wildcard(type: 'num'), [
+              checkReachable(true),
+              checkNotPromoted(x),
+            ], [
+              checkReachable(false),
+              checkNotPromoted(x),
+            ]),
+          ]);
+        });
+      });
+
+      group("doesn't cover matched type:", () {
+        test('without promotion candidate', () {
+          // In `if(<some num> case int _) ...`, the `else` branch should be
+          // reachable because the type `int` doesn't fully cover the type
+          // `num`.
+          h.run([
+            ifCase(expr('num'), wildcard(type: 'int'), [
+              checkReachable(true),
+            ], [
+              checkReachable(true),
+            ]),
+          ]);
+        });
+
+        group('with promotion candidate:', () {
+          test('without factor', () {
+            var x = Var('x');
+            h.run([
+              declare(x, type: 'num'),
+              ifCase(x.expr, wildcard(type: 'int'), [
+                checkReachable(true),
+                checkPromoted(x, 'int'),
+              ], [
+                checkReachable(true),
+                checkNotPromoted(x),
+              ]),
+            ]);
+          });
+
+          test('with factor', () {
+            var x = Var('x');
+            h.run([
+              declare(x, type: 'int?'),
+              ifCase(x.expr, wildcard(type: 'Null'), [
+                checkReachable(true),
+                checkPromoted(x, 'Null'),
+              ], [
+                checkReachable(true),
+                checkPromoted(x, 'int'),
+              ]),
+            ]);
+          });
+        });
+      });
+
+      test("Subpattern doesn't promote scrutinee", () {
+        var x = Var('x');
+        h.run([
+          declare(x, initializer: expr('Object')),
+          ifCase(
+              x.expr,
+              objectPattern(
+                  requiredType: 'num',
+                  fields: [wildcard(type: 'int').recordField('sign')]),
+              [
+                checkPromoted(x, 'num'),
+                // TODO(paulberry): should promote `x.sign` to `int`.
+              ]),
+        ]);
+      });
+    });
+
     test('Pattern inside guard', () {
       // Roughly equivalent Dart code:
       //     FutureOr<int> x = ...;
diff --git a/pkg/dart2wasm/lib/class_info.dart b/pkg/dart2wasm/lib/class_info.dart
index 911f715..07fe27e 100644
--- a/pkg/dart2wasm/lib/class_info.dart
+++ b/pkg/dart2wasm/lib/class_info.dart
@@ -35,6 +35,8 @@
   static const typeIsDeclaredNullable = 2;
   static const interfaceTypeTypeArguments = 4;
   static const functionTypeNamedParameters = 8;
+  static const recordTypeNames = 3;
+  static const recordTypeFieldTypes = 4;
   static const typedListBaseLength = 2;
   static const typedListArray = 3;
   static const typedListViewTypedData = 3;
@@ -47,6 +49,7 @@
   static const suspendStateTargetIndex = 6;
   static const syncStarIteratorCurrent = 3;
   static const syncStarIteratorYieldStarIterable = 4;
+  static const recordFieldBase = 2;
 
   static void validate(Translator translator) {
     void check(Class cls, String name, int expectedIndex) {
@@ -73,6 +76,9 @@
         FieldIndex.interfaceTypeTypeArguments);
     check(translator.functionTypeClass, "namedParameters",
         FieldIndex.functionTypeNamedParameters);
+    check(translator.recordTypeClass, "names", FieldIndex.recordTypeNames);
+    check(translator.recordTypeClass, "fieldTypes",
+        FieldIndex.recordTypeFieldTypes);
     check(translator.suspendStateClass, "_iterator",
         FieldIndex.suspendStateIterator);
     check(translator.suspendStateClass, "_context",
@@ -174,6 +180,10 @@
   int _nextClassId = 0;
   late final ClassInfo topInfo;
 
+  /// Maps number of record fields to the struct type to be used for a record
+  /// shape class with that many fields.
+  final Map<int, w.StructType> _recordStructs = {};
+
   /// Wasm field type for fields with type [_Type]. Fields of this type are
   /// added to classes for type parameters.
   ///
@@ -282,6 +292,26 @@
     translator.classForHeapType.putIfAbsent(info.struct, () => info!);
   }
 
+  void _initializeRecordClass(Class cls) {
+    final numFields = cls.fields.length;
+
+    final struct = _recordStructs.putIfAbsent(
+        numFields,
+        () => m.addStructType(
+              'Record$numFields',
+              superType: translator.recordInfo.struct,
+            ));
+
+    final ClassInfo superInfo = translator.recordInfo;
+
+    final info =
+        ClassInfo(cls, _nextClassId++, superInfo.depth + 1, struct, superInfo);
+
+    translator.classes.add(info);
+    translator.classInfo[cls] = info;
+    translator.classForHeapType.putIfAbsent(info.struct, () => info);
+  }
+
   void _computeRepresentation(ClassInfo info) {
     info.repr = upperBound(info.implementedBy);
   }
@@ -333,6 +363,30 @@
     }
   }
 
+  void _generateRecordFields(ClassInfo info) {
+    final struct = info.struct;
+    final ClassInfo superInfo = info.superInfo!;
+    assert(identical(superInfo, translator.recordInfo));
+
+    // Different record classes can share the same struct, check if the struct
+    // is already initialized
+    if (struct.fields.isEmpty) {
+      // Copy fields from superclass
+      for (w.FieldType fieldType in superInfo.struct.fields) {
+        info._addField(fieldType);
+      }
+
+      for (Field _ in info.cls!.fields) {
+        info._addField(w.FieldType(topInfo.nullableType));
+      }
+    }
+
+    int fieldIdx = superInfo.struct.fields.length;
+    for (Field field in info.cls!.fields) {
+      translator.fieldIndex[field] = fieldIdx++;
+    }
+  }
+
   /// Create class info and Wasm struct for all classes.
   void collect() {
     _initializeTop();
@@ -347,9 +401,18 @@
     // parameters.
     _initialize(translator.typeClass);
 
+    // Initialize the record base class if we have record classes.
+    if (translator.recordClasses.isNotEmpty) {
+      _initialize(translator.coreTypes.recordClass);
+    }
+
     for (Library library in translator.component.libraries) {
       for (Class cls in library.classes) {
-        _initialize(cls);
+        if (cls.superclass == translator.coreTypes.recordClass) {
+          _initializeRecordClass(cls);
+        } else {
+          _initialize(cls);
+        }
       }
     }
 
@@ -364,7 +427,11 @@
     // Now that the representation types for all classes have been computed,
     // fill in the types of the fields in the generated Wasm structs.
     for (ClassInfo info in translator.classes) {
-      _generateFields(info);
+      if (info.superInfo == translator.recordInfo) {
+        _generateRecordFields(info);
+      } else {
+        _generateFields(info);
+      }
     }
 
     // Add hidden fields of typed_data classes.
diff --git a/pkg/dart2wasm/lib/code_generator.dart b/pkg/dart2wasm/lib/code_generator.dart
index 8f2172f..271194b 100644
--- a/pkg/dart2wasm/lib/code_generator.dart
+++ b/pkg/dart2wasm/lib/code_generator.dart
@@ -8,6 +8,7 @@
 import 'package:dart2wasm/dynamic_forwarders.dart';
 import 'package:dart2wasm/intrinsics.dart';
 import 'package:dart2wasm/param_info.dart';
+import 'package:dart2wasm/records.dart';
 import 'package:dart2wasm/reference_extensions.dart';
 import 'package:dart2wasm/sync_star.dart';
 import 'package:dart2wasm/translator.dart';
@@ -1319,7 +1320,11 @@
     late final w.ValueType nullableType;
     late final w.ValueType nonNullableType;
     late final void Function() compare;
-    if (node.cases.every((c) => c.expressions.isEmpty && c.isDefault)) {
+    if (node.cases.every((c) =>
+        c.expressions.isEmpty && c.isDefault ||
+        c.expressions.every((e) =>
+            e is NullLiteral ||
+            e is ConstantExpression && e.constant is NullConstant))) {
       // default-only switch
       nonNullableType = w.RefType.eq(nullable: false);
       nullableType = w.RefType.eq(nullable: true);
@@ -2864,6 +2869,55 @@
     return nonNullableTypeType;
   }
 
+  @override
+  w.ValueType visitRecordLiteral(RecordLiteral node, w.ValueType expectedType) {
+    final ClassInfo recordClassInfo =
+        translator.getRecordClassInfo(node.recordType);
+    translator.functions.allocateClass(recordClassInfo.classId);
+
+    b.i32_const(recordClassInfo.classId);
+    b.i32_const(initialIdentityHash);
+    for (Expression positional in node.positional) {
+      wrap(positional, translator.topInfo.nullableType);
+    }
+    for (NamedExpression named in node.named) {
+      wrap(named.value, translator.topInfo.nullableType);
+    }
+    b.struct_new(recordClassInfo.struct);
+
+    return recordClassInfo.nonNullableType;
+  }
+
+  @override
+  w.ValueType visitRecordIndexGet(
+      RecordIndexGet node, w.ValueType expectedType) {
+    final RecordShape recordShape = RecordShape.fromType(node.receiverType);
+    final ClassInfo recordClassInfo =
+        translator.getRecordClassInfo(node.receiverType);
+    translator.functions.allocateClass(recordClassInfo.classId);
+
+    wrap(node.receiver, translator.topInfo.nonNullableType);
+    b.ref_cast(w.RefType(recordClassInfo.struct, nullable: false));
+    b.struct_get(
+        recordClassInfo.struct, recordShape.getPositionalIndex(node.index));
+
+    return translator.topInfo.nullableType;
+  }
+
+  @override
+  w.ValueType visitRecordNameGet(RecordNameGet node, w.ValueType expectedType) {
+    final RecordShape recordShape = RecordShape.fromType(node.receiverType);
+    final ClassInfo recordClassInfo =
+        translator.getRecordClassInfo(node.receiverType);
+    translator.functions.allocateClass(recordClassInfo.classId);
+
+    wrap(node.receiver, translator.topInfo.nonNullableType);
+    b.ref_cast(w.RefType(recordClassInfo.struct, nullable: false));
+    b.struct_get(recordClassInfo.struct, recordShape.getNameIndex(node.name));
+
+    return translator.topInfo.nullableType;
+  }
+
   /// Generate type checker method for a setter.
   ///
   /// This function will be called by a setter forwarder in a dynamic set to
diff --git a/pkg/dart2wasm/lib/compile.dart b/pkg/dart2wasm/lib/compile.dart
index 4570716..4029b8c 100644
--- a/pkg/dart2wasm/lib/compile.dart
+++ b/pkg/dart2wasm/lib/compile.dart
@@ -31,6 +31,8 @@
 
 import 'package:dart2wasm/compiler_options.dart' as compiler;
 import 'package:dart2wasm/js_runtime_generator.dart';
+import 'package:dart2wasm/record_class_generator.dart';
+import 'package:dart2wasm/records.dart';
 import 'package:dart2wasm/target.dart';
 import 'package:dart2wasm/translator.dart';
 
@@ -91,6 +93,9 @@
   JSRuntimeFinalizer jsRuntimeFinalizer =
       createJSRuntimeFinalizer(component, coreTypes, classHierarchy);
 
+  final Map<RecordShape, Class> recordClasses =
+      generateRecordClasses(component, coreTypes);
+
   globalTypeFlow.transformComponent(target, coreTypes, component,
       treeShakeSignatures: true,
       treeShakeWriteOnlyFields: true,
@@ -102,7 +107,8 @@
     return true;
   }());
 
-  var translator = Translator(component, coreTypes, options.translatorOptions);
+  var translator = Translator(
+      component, coreTypes, recordClasses, options.translatorOptions);
 
   String? depFile = options.depFile;
   if (depFile != null) {
diff --git a/pkg/dart2wasm/lib/constants.dart b/pkg/dart2wasm/lib/constants.dart
index a26fa3e..3891551 100644
--- a/pkg/dart2wasm/lib/constants.dart
+++ b/pkg/dart2wasm/lib/constants.dart
@@ -789,6 +789,28 @@
         b.i64_const(environmentIndex);
         b.struct_new(info.struct);
       });
+    } else if (type is RecordType) {
+      final names = ListConstant(
+          InterfaceType(
+              translator.coreTypes.stringClass, Nullability.nonNullable),
+          type.named.map((t) => StringConstant(t.name)).toList());
+      ensureConstant(names);
+      final fieldTypes = constants.makeTypeList(
+          type.positional.followedBy(type.named.map((n) => n.type)).toList());
+      ensureConstant(fieldTypes);
+      return createConstant(constant, info.nonNullableType, (function, b) {
+        b.i32_const(info.classId);
+        b.i32_const(initialIdentityHash);
+        b.i32_const(types.encodedNullability(type));
+        final namesExpectedType =
+            info.struct.fields[FieldIndex.recordTypeNames].type.unpacked;
+        constants.instantiateConstant(function, b, names, namesExpectedType);
+        final typeListExpectedType =
+            info.struct.fields[FieldIndex.recordTypeFieldTypes].type.unpacked;
+        constants.instantiateConstant(
+            function, b, fieldTypes, typeListExpectedType);
+        b.struct_new(info.struct);
+      });
     } else {
       assert(type is VoidType ||
           type is NeverType ||
@@ -819,4 +841,29 @@
       b.struct_new(info.struct);
     });
   }
+
+  @override
+  ConstantInfo? visitRecordConstant(RecordConstant constant) {
+    final ClassInfo recordClassInfo =
+        translator.getRecordClassInfo(constant.recordType);
+    translator.functions.allocateClass(recordClassInfo.classId);
+
+    final List<Constant> arguments = constant.positional.toList();
+    arguments.addAll(constant.named.values);
+
+    for (Constant argument in arguments) {
+      ensureConstant(argument);
+    }
+
+    return createConstant(constant, recordClassInfo.nonNullableType,
+        lazy: false, (function, b) {
+      b.i32_const(recordClassInfo.classId);
+      b.i32_const(initialIdentityHash);
+      for (Constant argument in arguments) {
+        constants.instantiateConstant(
+            function, b, argument, translator.topInfo.nullableType);
+      }
+      b.struct_new(recordClassInfo.struct);
+    });
+  }
 }
diff --git a/pkg/dart2wasm/lib/dispatch_table.dart b/pkg/dart2wasm/lib/dispatch_table.dart
index 1ac87a6..6bed33a 100644
--- a/pkg/dart2wasm/lib/dispatch_table.dart
+++ b/pkg/dart2wasm/lib/dispatch_table.dart
@@ -250,10 +250,16 @@
     final cls = member.enclosingClass;
     final isWasmType = cls != null && translator.isWasmType(cls);
 
+    // TODO(51363): Dynamic call metadata is not accurate for record fields, so
+    // we consider them to be dynamically called.
+    final isRecordMember =
+        member.enclosingClass?.superclass == translator.recordInfo.cls;
+
     final calledDynamically = !isWasmType &&
-        (metadata.getterCalledDynamically ||
-            metadata.methodOrSetterCalledDynamically ||
-            member.name.text == "call");
+            (metadata.getterCalledDynamically ||
+                metadata.methodOrSetterCalledDynamically ||
+                member.name.text == "call") ||
+        isRecordMember;
 
     final selector = _selectorInfo.putIfAbsent(
         selectorId,
diff --git a/pkg/dart2wasm/lib/kernel_nodes.dart b/pkg/dart2wasm/lib/kernel_nodes.dart
index b6f1932..7b3e869 100644
--- a/pkg/dart2wasm/lib/kernel_nodes.dart
+++ b/pkg/dart2wasm/lib/kernel_nodes.dart
@@ -77,6 +77,7 @@
   late final Class stackTraceClass = index.getClass("dart:core", "StackTrace");
   late final Class typeUniverseClass =
       index.getClass("dart:core", "_TypeUniverse");
+  late final Class recordTypeClass = index.getClass("dart:core", "_RecordType");
 
   // dart:core sync* support classes
   late final Class suspendStateClass =
diff --git a/pkg/dart2wasm/lib/record_class_generator.dart b/pkg/dart2wasm/lib/record_class_generator.dart
new file mode 100644
index 0000000..0d7ecbb
--- /dev/null
+++ b/pkg/dart2wasm/lib/record_class_generator.dart
@@ -0,0 +1,442 @@
+// Copyright (c) 2023, 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:kernel/ast.dart';
+import 'package:kernel/core_types.dart';
+
+import 'package:dart2wasm/records.dart';
+
+/// Generates a class extending `Record` for each record shape in the
+/// [Component].
+///
+/// Shape of a record is described by the [RecordShape] type.
+///
+/// Example: for the record `(1, a: 'hi', false)`, this generates:
+///
+/// ```
+/// @pragma('wasm:entry-point')
+/// class Record_2_a {
+///   @pragma('wasm:entry-point')
+///   final Object? $1;
+///
+///   @pragma('wasm:entry-point')
+///   final Object? $2;
+///
+///   @pragma('wasm:entry-point')
+///   final Object? a;
+///
+///   @pragma('wasm:entry-point')
+///   Record_2_a(this.$1, this.$2, this.a);
+///
+///   @pragma('wasm:entry-point')
+///   _Type get _runtimeType =>
+///     // Uses of `runtimeType` below will be fixed with #51134.
+///     _RecordType(
+///       const ["a"],
+///       [$1.runtimeType, $2.runtimeType, a.runtimeType]);
+///
+///   @pragma('wasm:entry-point')
+///   Type get runtimeType => _runtimeType;
+///
+///   @pragma('wasm:entry-point')
+///   String toString() =>
+///     "(" + $1 + ", " + $2 + ", " + "a: " + a + ")";
+///
+///   @pragma('wasm:entry-point')
+///   bool operator ==(Object other) {
+///     if (other is! Record_2_a) return false;
+///     if ($1 != other.$1) return false;
+///     if ($2 != other.$2) return false;
+///     if (a != other.a) return false;
+///     return true;
+///   }
+///
+///   @pragma('wasm:entry-point')
+///   int hashCode =>
+///     Object.hash(shapeID, $1, $2, a);
+/// }
+/// ```
+Map<RecordShape, Class> generateRecordClasses(
+    Component component, CoreTypes coreTypes) {
+  final Map<RecordShape, Class> recordClasses = {};
+  final recordClassGenerator = _RecordClassGenerator(recordClasses, coreTypes);
+  final visitor = _RecordVisitor(recordClassGenerator);
+  component.libraries.forEach(visitor.visitLibrary);
+  return recordClasses;
+}
+
+class _RecordClassGenerator {
+  final CoreTypes coreTypes;
+  final Map<RecordShape, Class> classes;
+
+  late final Class recordRuntimeTypeClass =
+      coreTypes.index.getClass('dart:core', '_RecordType');
+
+  late final Class internalRuntimeTypeClass =
+      coreTypes.index.getClass('dart:core', '_Type');
+
+  late final Constructor recordRuntimeTypeConstructor =
+      recordRuntimeTypeClass.constructors.single;
+
+  late final Procedure objectHashProcedure =
+      coreTypes.index.getProcedure('dart:core', 'Object', 'hash');
+
+  late final Procedure objectHashAllProcedure =
+      coreTypes.index.getProcedure('dart:core', 'Object', 'hashAll');
+
+  late final Procedure objectRuntimeTypeProcedure =
+      coreTypes.index.getProcedure('dart:core', 'Object', 'get:runtimeType');
+
+  late final Procedure objectToStringProcedure =
+      coreTypes.index.getProcedure('dart:core', 'Object', 'toString');
+
+  late final Procedure objectEqualsProcedure = coreTypes.objectEquals;
+
+  late final Procedure stringPlusProcedure =
+      coreTypes.index.getProcedure('dart:core', 'String', '+');
+
+  DartType get nullableObjectType => coreTypes.objectNullableRawType;
+
+  DartType get internalRuntimeTypeType =>
+      InterfaceType(internalRuntimeTypeClass, Nullability.nonNullable);
+
+  DartType get nonNullableStringType => coreTypes.stringNonNullableRawType;
+
+  DartType get boolType => coreTypes.boolNonNullableRawType;
+
+  DartType get intType => coreTypes.intNonNullableRawType;
+
+  DartType get runtimeTypeType => coreTypes.typeNonNullableRawType;
+
+  Library get library => coreTypes.coreLibrary;
+
+  _RecordClassGenerator(this.classes, this.coreTypes);
+
+  void generateClassForRecordType(RecordType recordType) {
+    final shape = RecordShape.fromType(recordType);
+    final id = classes.length;
+    classes.putIfAbsent(shape, () => _generateClass(shape, id));
+  }
+
+  /// Add a `@pragma('wasm:entry-point')` annotation to an annotatable.
+  T _addWasmEntryPointPragma<T extends Annotatable>(T node) => node
+    ..addAnnotation(ConstantExpression(
+        InstanceConstant(coreTypes.pragmaClass.reference, [], {
+      coreTypes.pragmaName.fieldReference: StringConstant("wasm:entry-point"),
+      coreTypes.pragmaOptions.fieldReference: NullConstant(),
+    })));
+
+  Class _generateClass(RecordShape shape, int id) {
+    final fields = _generateFields(shape);
+
+    String className = 'Record_${shape.positionals}';
+    if (shape.names.isNotEmpty) {
+      className = className + '_${shape.names.join('_')}';
+    }
+
+    final cls = _addWasmEntryPointPragma(Class(
+      name: className,
+      isAbstract: false,
+      isAnonymousMixin: false,
+      supertype: Supertype(coreTypes.recordClass, []),
+      constructors: [_generateConstructor(shape, fields)],
+      procedures: [
+        _generateHashCode(fields, id),
+        _generateToString(shape, fields),
+      ],
+      fields: fields,
+      fileUri: library.fileUri,
+    ));
+    library.addClass(cls);
+    cls.addProcedure(_generateEquals(shape, fields, cls));
+    final internalRuntimeType = _generateInternalRuntimeType(shape, fields);
+    // With `_runtimeType` we also need to override `runtimeType`, as
+    // `Object.runtimeType` is implemented as a direct call to
+    // `Object._runtimeType` instead of a virtual call.
+    final runtimeType = _generateRuntimeType(internalRuntimeType);
+    cls.addProcedure(internalRuntimeType);
+    cls.addProcedure(runtimeType);
+    return cls;
+  }
+
+  List<Field> _generateFields(RecordShape shape) {
+    final List<Field> fields = [];
+
+    for (int i = 0; i < shape.positionals; i += 1) {
+      fields.add(_addWasmEntryPointPragma(Field.immutable(
+        Name('\$${i + 1}', library),
+        isFinal: true,
+        fileUri: library.fileUri,
+      )));
+    }
+
+    for (String name in shape.names) {
+      fields.add(_addWasmEntryPointPragma(Field.immutable(
+        Name(name, library),
+        isFinal: true,
+        fileUri: library.fileUri,
+      )));
+    }
+
+    return fields;
+  }
+
+  /// Generate a constructor with name `_`. Named fields are passed in sorted
+  /// order.
+  Constructor _generateConstructor(RecordShape shape, List<Field> fields) {
+    final List<VariableDeclaration> positionalParameters =
+        List.generate(fields.length, (i) => VariableDeclaration('field$i'));
+
+    final List<Initializer> initializers = List.generate(
+        fields.length,
+        (i) =>
+            FieldInitializer(fields[i], VariableGet(positionalParameters[i])));
+
+    final function =
+        FunctionNode(null, positionalParameters: positionalParameters);
+
+    return _addWasmEntryPointPragma(Constructor(function,
+        name: Name('_', library),
+        isConst: true,
+        initializers: initializers,
+        fileUri: library.fileUri));
+  }
+
+  /// Generate `int get hashCode` member.
+  Procedure _generateHashCode(List<Field> fields, int shapeId) {
+    final Expression returnValue;
+
+    if (fields.isEmpty) {
+      returnValue = IntLiteral(shapeId);
+    } else {
+      final List<Expression> arguments = [];
+      arguments.add(IntLiteral(shapeId));
+      for (Field field in fields) {
+        arguments.add(InstanceGet(
+            InstanceAccessKind.Instance, ThisExpression(), field.name,
+            interfaceTarget: field, resultType: nullableObjectType));
+      }
+      if (fields.length <= 20) {
+        // Object.hash(field1, field2, ...)
+        returnValue =
+            StaticInvocation(objectHashProcedure, Arguments(arguments));
+      } else {
+        // Object.hashAll([field1, field2, ...])
+        returnValue = StaticInvocation(
+            objectHashAllProcedure, Arguments([ListLiteral(arguments)]));
+      }
+    }
+
+    return _addWasmEntryPointPragma(Procedure(
+      Name('hashCode', library),
+      ProcedureKind.Getter,
+      FunctionNode(ReturnStatement(returnValue), returnType: intType),
+      fileUri: library.fileUri,
+    ));
+  }
+
+  /// Generate `String toString()` member.
+  Procedure _generateToString(RecordShape shape, List<Field> fields) {
+    final List<Expression> stringExprs = [];
+
+    Expression fieldToStringExpression(Field field) => InstanceInvocation(
+        InstanceAccessKind.Object,
+        InstanceGet(InstanceAccessKind.Instance, ThisExpression(), field.name,
+            interfaceTarget: field, resultType: nullableObjectType),
+        Name('toString'),
+        Arguments([]),
+        interfaceTarget: objectToStringProcedure,
+        functionType: FunctionType(
+          [],
+          nonNullableStringType,
+          Nullability.nonNullable,
+        ));
+
+    int fieldIdx = 0;
+
+    for (; fieldIdx < shape.positionals; fieldIdx += 1) {
+      final Field field = fields[fieldIdx];
+      stringExprs.add(fieldToStringExpression(field));
+      if (fieldIdx != shape.numFields - 1) {
+        stringExprs.add(StringLiteral(', '));
+      }
+    }
+
+    for (String name in shape.names) {
+      final Field field = fields[fieldIdx];
+      stringExprs.add(StringLiteral('$name: '));
+      stringExprs.add(fieldToStringExpression(field));
+      if (fieldIdx != shape.numFields - 1) {
+        stringExprs.add(StringLiteral(', '));
+      }
+      fieldIdx += 1;
+    }
+
+    stringExprs.add(StringLiteral(')'));
+
+    final Expression stringExpression = stringExprs.fold(
+        StringLiteral('('),
+        (string, next) => InstanceInvocation(
+              InstanceAccessKind.Instance,
+              string,
+              Name('+'),
+              Arguments([next]),
+              interfaceTarget: stringPlusProcedure,
+              functionType: FunctionType(
+                [nonNullableStringType],
+                nonNullableStringType,
+                Nullability.nonNullable,
+              ),
+            ));
+
+    return _addWasmEntryPointPragma(Procedure(
+      Name('toString', library),
+      ProcedureKind.Method,
+      FunctionNode(ReturnStatement(stringExpression)),
+      fileUri: library.fileUri,
+    ));
+  }
+
+  /// Generate `bool operator ==` member.
+  Procedure _generateEquals(RecordShape shape, List<Field> fields, Class cls) {
+    final equalsFunctionType = FunctionType(
+      [nullableObjectType],
+      boolType,
+      Nullability.nonNullable,
+    );
+
+    final VariableDeclaration parameter =
+        VariableDeclaration('other', type: nullableObjectType);
+
+    final List<Statement> statements = [];
+
+    statements.add(IfStatement(
+      Not(IsExpression(
+          VariableGet(parameter), InterfaceType(cls, Nullability.nonNullable))),
+      ReturnStatement(BoolLiteral(false)),
+      null,
+    ));
+
+    // Compare fields.
+    for (Field field in fields) {
+      statements.add(IfStatement(
+        Not(EqualsCall(
+          InstanceGet(InstanceAccessKind.Instance, ThisExpression(), field.name,
+              interfaceTarget: field, resultType: nullableObjectType),
+          InstanceGet(
+              InstanceAccessKind.Instance, VariableGet(parameter), field.name,
+              interfaceTarget: field, resultType: nullableObjectType),
+          interfaceTarget: objectEqualsProcedure,
+          functionType: equalsFunctionType,
+        )),
+        ReturnStatement(BoolLiteral(false)),
+        null,
+      ));
+    }
+
+    statements.add(ReturnStatement(BoolLiteral(true)));
+
+    final FunctionNode function = FunctionNode(
+      Block(statements),
+      positionalParameters: [parameter],
+      returnType: boolType,
+    );
+
+    return _addWasmEntryPointPragma(Procedure(
+      Name('==', library),
+      ProcedureKind.Operator,
+      function,
+      fileUri: library.fileUri,
+    ));
+  }
+
+  /// Generate `_Type get _runtimeType` member.
+  Procedure _generateInternalRuntimeType(
+      RecordShape shape, List<Field> fields) {
+    final List<Statement> statements = [];
+
+    // const ["name1", "name2", ...]
+    final fieldNamesList = ConstantExpression(ListConstant(
+        nonNullableStringType,
+        shape.names.map((name) => StringConstant(name)).toList()));
+
+    // Generate `this.field.runtimeType` for a given field.
+    // TODO(51134): We shouldn't use user-provided runtimeType below.
+    Expression fieldRuntimeTypeExpr(Field field) => InstanceGet(
+          InstanceAccessKind.Object,
+          InstanceGet(InstanceAccessKind.Instance, ThisExpression(), field.name,
+              interfaceTarget: field, resultType: nullableObjectType),
+          objectRuntimeTypeProcedure.name,
+          interfaceTarget: objectRuntimeTypeProcedure,
+          resultType: runtimeTypeType,
+        );
+
+    // [this.$1.runtimeType, this.x.runtimeType, ...]
+    final fieldTypesList = ListLiteral(
+      fields.map(fieldRuntimeTypeExpr).toList(),
+      typeArgument: runtimeTypeType,
+    );
+
+    statements.add(ReturnStatement(ConstructorInvocation(
+        recordRuntimeTypeConstructor,
+        Arguments([
+          fieldNamesList,
+          fieldTypesList,
+          BoolLiteral(false), // declared nullable
+        ]))));
+
+    final FunctionNode function = FunctionNode(
+      Block(statements),
+      positionalParameters: [],
+      returnType:
+          InterfaceType(recordRuntimeTypeClass, Nullability.nonNullable),
+    );
+
+    return _addWasmEntryPointPragma(Procedure(
+      Name('_runtimeType', library),
+      ProcedureKind.Getter,
+      function,
+      fileUri: library.fileUri,
+    ));
+  }
+
+  /// Generate `Type get runtimeType member = _runtimeType;`.
+  Procedure _generateRuntimeType(Procedure internalRuntimeType) {
+    final FunctionNode function = FunctionNode(
+      ReturnStatement(InstanceGet(InstanceAccessKind.Instance, ThisExpression(),
+          internalRuntimeType.name,
+          interfaceTarget: internalRuntimeType,
+          resultType: internalRuntimeTypeType)),
+      positionalParameters: [],
+      returnType: runtimeTypeType,
+    );
+
+    return _addWasmEntryPointPragma(Procedure(
+      Name('runtimeType', library),
+      ProcedureKind.Getter,
+      function,
+      fileUri: library.fileUri,
+    ));
+  }
+}
+
+class _RecordVisitor extends RecursiveVisitor<void> {
+  final _RecordClassGenerator classGenerator;
+  final Set<Constant> constantCache = Set.identity();
+
+  _RecordVisitor(this.classGenerator);
+
+  @override
+  void visitRecordType(RecordType node) {
+    classGenerator.generateClassForRecordType(node);
+    super.visitRecordType(node);
+  }
+
+  @override
+  void defaultConstantReference(Constant node) {
+    if (constantCache.add(node)) {
+      node.visitChildren(this);
+    }
+  }
+}
diff --git a/pkg/dart2wasm/lib/records.dart b/pkg/dart2wasm/lib/records.dart
new file mode 100644
index 0000000..f658f16
--- /dev/null
+++ b/pkg/dart2wasm/lib/records.dart
@@ -0,0 +1,76 @@
+// Copyright (c) 2023, 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:collection' show SplayTreeMap;
+
+import 'package:dart2wasm/class_info.dart';
+
+import 'package:kernel/ast.dart';
+
+/// Describes shape of a record as the number of positionals + set of field
+/// names.
+class RecordShape {
+  /// Number of positional fields.
+  final int positionals;
+
+  /// Maps names of the named fields in the record to their indices in the
+  /// record payload.
+  final SplayTreeMap<String, int> _names;
+
+  /// Names of named fields, sorted.
+  Iterable<String> get names => _names.keys;
+
+  /// Total number of fields.
+  int get numFields => positionals + _names.length;
+
+  RecordShape.fromType(RecordType recordType)
+      : positionals = recordType.positional.length,
+        // RecordType.named is already sorted
+        _names = SplayTreeMap.fromIterables(
+            recordType.named.map((ty) => ty.name),
+            Iterable.generate(recordType.named.length,
+                (i) => i + recordType.positional.length));
+
+  @override
+  String toString() => 'Record(positionals: $positionals, names: $_names)';
+
+  @override
+  bool operator ==(Object other) {
+    if (other is! RecordShape) {
+      return false;
+    }
+
+    if (positionals != other.positionals) {
+      return false;
+    }
+
+    if (_names.length != other._names.length) {
+      return false;
+    }
+
+    final names1Iter = _names.keys.iterator;
+    final names2Iter = other._names.keys.iterator;
+    while (names1Iter.moveNext()) {
+      names2Iter.moveNext();
+      if (names1Iter.current != names2Iter.current) {
+        return false;
+      }
+    }
+
+    return true;
+  }
+
+  @override
+  int get hashCode => Object.hash(positionals, Object.hashAll(_names.keys));
+
+  /// Struct index of a positional field.
+  int getPositionalIndex(int position) => FieldIndex.recordFieldBase + position;
+
+  /// Struct index of a named field.
+  int getNameIndex(String name) =>
+      FieldIndex.recordFieldBase +
+      (_names[name] ??
+          (throw 'RecordImplementation.getNameIndex: '
+              'name $name not in record: $this'));
+}
diff --git a/pkg/dart2wasm/lib/translator.dart b/pkg/dart2wasm/lib/translator.dart
index f8983c5..31e30a6 100644
--- a/pkg/dart2wasm/lib/translator.dart
+++ b/pkg/dart2wasm/lib/translator.dart
@@ -14,6 +14,7 @@
 import 'package:dart2wasm/globals.dart';
 import 'package:dart2wasm/kernel_nodes.dart';
 import 'package:dart2wasm/param_info.dart';
+import 'package:dart2wasm/records.dart';
 import 'package:dart2wasm/reference_extensions.dart';
 import 'package:dart2wasm/types.dart';
 
@@ -99,6 +100,10 @@
   late final w.Memory ffiMemory = m.importMemory("ffi", "memory",
       options.importSharedMemory, 0, options.sharedMemoryMaxPages);
 
+  /// Maps record shapes to the record class for the shape. Classes generated
+  /// by `record_class_generator` library.
+  final Map<RecordShape, Class> recordClasses;
+
   // Caches for when identical source constructs need a common representation.
   final Map<w.StorageType, w.ArrayType> arrayTypeCache = {};
   final Map<w.BaseFunction, w.DefinedGlobal> functionRefCache = {};
@@ -109,6 +114,7 @@
   late final ClassInfo objectInfo = classInfo[coreTypes.objectClass]!;
   late final ClassInfo closureInfo = classInfo[closureClass]!;
   late final ClassInfo stackTraceInfo = classInfo[stackTraceClass]!;
+  late final ClassInfo recordInfo = classInfo[coreTypes.recordClass]!;
   late final w.ArrayType listArrayType = (classInfo[listBaseClass]!
           .struct
           .fields[FieldIndex.listArray]
@@ -203,7 +209,7 @@
     topInfo.nullableType
   ]);
 
-  Translator(this.component, this.coreTypes, this.options)
+  Translator(this.component, this.coreTypes, this.recordClasses, this.options)
       : libraries = component.libraries,
         hierarchy =
             ClassHierarchy(component, coreTypes) as ClosedWorldClassHierarchy {
@@ -551,6 +557,9 @@
     if (type is InlineType) {
       return translateStorageType(type.instantiatedRepresentationType);
     }
+    if (type is RecordType) {
+      return typeForInfo(getRecordClassInfo(type), type.isPotentiallyNullable);
+    }
     throw "Unsupported type ${type.runtimeType}";
   }
 
@@ -964,6 +973,9 @@
     b.struct_get(info.struct, FieldIndex.listLength);
     b.i32_wrap_i64();
   }
+
+  ClassInfo getRecordClassInfo(RecordType recordType) =>
+      classInfo[recordClasses[RecordShape.fromType(recordType)]!]!;
 }
 
 abstract class _FunctionGenerator {
diff --git a/pkg/dart2wasm/lib/types.dart b/pkg/dart2wasm/lib/types.dart
index 4fa44c8..3aebee1 100644
--- a/pkg/dart2wasm/lib/types.dart
+++ b/pkg/dart2wasm/lib/types.dart
@@ -46,6 +46,10 @@
   late final w.ValueType namedParametersExpectedType = classAndFieldToType(
       translator.functionTypeClass, FieldIndex.functionTypeNamedParameters);
 
+  /// Wasm value type of `_RecordType.names` field.
+  late final w.ValueType recordTypeNamesFieldExpectedType = classAndFieldToType(
+      translator.recordTypeClass, FieldIndex.recordTypeNames);
+
   /// A mapping from concrete subclass `classID` to [Map]s of superclass
   /// `classID` and the necessary substitutions which must be performed to test
   /// for a valid subtyping relationship.
@@ -269,6 +273,9 @@
             type.positionalParameters.every(_isTypeConstant) &&
             type.namedParameters.every((n) => _isTypeConstant(n.type))) ||
         type is InterfaceType && type.typeArguments.every(_isTypeConstant) ||
+        (type is RecordType &&
+            type.positional.every(_isTypeConstant) &&
+            type.named.every((n) => _isTypeConstant(n.type))) ||
         type is TypeParameterType && isFunctionTypeParameter(type) ||
         type is InlineType &&
             _isTypeConstant(type.instantiatedRepresentationType);
@@ -303,6 +310,8 @@
       }
     } else if (type is InlineType) {
       return classForType(type.instantiatedRepresentationType);
+    } else if (type is RecordType) {
+      return translator.recordTypeClass;
     }
     throw "Unexpected DartType: $type";
   }
@@ -322,6 +331,21 @@
     _makeTypeList(codeGen, type.typeArguments);
   }
 
+  void _makeRecordType(CodeGenerator codeGen, RecordType type) {
+    codeGen.b.i32_const(encodedNullability(type));
+    translator.constants.instantiateConstant(
+        codeGen.function,
+        codeGen.b,
+        ListConstant(
+          InterfaceType(
+              translator.coreTypes.stringClass, Nullability.nonNullable),
+          type.named.map((t) => StringConstant(t.name)).toList(),
+        ),
+        recordTypeNamesFieldExpectedType);
+    _makeTypeList(codeGen,
+        type.positional.followedBy(type.named.map((t) => t.type)).toList());
+  }
+
   /// Normalizes a Dart type. Many rules are already applied for us, but we
   /// still have to manually normalize [FutureOr].
   DartType normalize(DartType type) {
@@ -422,7 +446,8 @@
         type is InlineType ||
         type is InterfaceType ||
         type is FutureOrType ||
-        type is FunctionType);
+        type is FunctionType ||
+        type is RecordType);
     if (type is TypeParameterType) {
       assert(!isFunctionTypeParameter(type));
       codeGen.instantiateTypeParameter(type.parameter);
@@ -449,6 +474,8 @@
       _makeInterfaceType(codeGen, type);
     } else if (type is FunctionType) {
       _makeFunctionType(codeGen, type);
+    } else if (type is RecordType) {
+      _makeRecordType(codeGen, type);
     } else {
       throw '`$type` should have already been handled.';
     }
diff --git a/pkg/dds/CHANGELOG.md b/pkg/dds/CHANGELOG.md
index 91a7c0c..108e830 100644
--- a/pkg/dds/CHANGELOG.md
+++ b/pkg/dds/CHANGELOG.md
@@ -1,3 +1,6 @@
+# 2.7.5
+- Updated `vm_service` version to >=9.0.0 <12.0.0.
+
 # 2.7.4
 - [DAP] Added support for `,d` (decimal), `,h` (hex) and `,nq` (no quotes) format specifiers to be used as suffixes to evaluation requests.
 - [DAP] Added support for `format.hex` in `variablesRequest` and `evaluateRequest`.
diff --git a/pkg/dds/pubspec.yaml b/pkg/dds/pubspec.yaml
index f297d5f..0c386bf 100644
--- a/pkg/dds/pubspec.yaml
+++ b/pkg/dds/pubspec.yaml
@@ -1,5 +1,5 @@
 name: dds
-version: 2.7.4
+version: 2.7.5
 description: >-
   A library used to spawn the Dart Developer Service, used to communicate with
   a Dart VM Service instance.
@@ -26,7 +26,7 @@
   sse: ^4.0.0
   stack_trace: ^1.10.0
   stream_channel: ^2.0.0
-  vm_service: '>=9.0.0 <11.0.0'
+  vm_service: '>=9.0.0 <12.0.0'
   web_socket_channel: ^2.0.0
 
 # We use 'any' version constraints here as we get our package versions from
diff --git a/sdk/lib/_internal/wasm/lib/class_id.dart b/sdk/lib/_internal/wasm/lib/class_id.dart
index 9be0749..6b247f5 100644
--- a/sdk/lib/_internal/wasm/lib/class_id.dart
+++ b/sdk/lib/_internal/wasm/lib/class_id.dart
@@ -42,6 +42,8 @@
   external static int get cidGrowableList;
   @pragma("wasm:class-id", "dart.core#_ImmutableList")
   external static int get cidImmutableList;
+  @pragma("wasm:class-id", "dart.core#Record")
+  external static int get cidRecord;
 
   // Class IDs for RTI Types.
   @pragma("wasm:class-id", "dart.core#_NeverType")
@@ -62,6 +64,8 @@
   external static int get cidFunctionTypeParameterType;
   @pragma("wasm:class-id", "dart.core#_InterfaceTypeParameterType")
   external static int get cidInterfaceTypeParameterType;
+  @pragma("wasm:class-id", "dart.core#_RecordType")
+  external static int get cidRecordType;
 
   // Dummy, only used by VM-specific hash table code.
   static final int numPredefinedCids = 1;
diff --git a/sdk/lib/_internal/wasm/lib/core_patch.dart b/sdk/lib/_internal/wasm/lib/core_patch.dart
index 487d58a..5bad2fc 100644
--- a/sdk/lib/_internal/wasm/lib/core_patch.dart
+++ b/sdk/lib/_internal/wasm/lib/core_patch.dart
@@ -63,6 +63,7 @@
 part "list.dart";
 part "named_parameters.dart";
 part "object_patch.dart";
+part "record_patch.dart";
 part "regexp_patch.dart";
 part "stack_trace_patch.dart";
 part "stopwatch_patch.dart";
diff --git a/sdk/lib/_internal/wasm/lib/record_patch.dart b/sdk/lib/_internal/wasm/lib/record_patch.dart
new file mode 100644
index 0000000..7b78474
--- /dev/null
+++ b/sdk/lib/_internal/wasm/lib/record_patch.dart
@@ -0,0 +1,10 @@
+// Copyright (c) 2023, 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.
+
+part of "core_patch.dart";
+
+// `entry-point` needed to make sure the class will be in the class hierarchy
+// in programs without records.
+@pragma('wasm:entry-point')
+abstract class Record {}
diff --git a/sdk/lib/_internal/wasm/lib/type.dart b/sdk/lib/_internal/wasm/lib/type.dart
index 422c933..693b2ec 100644
--- a/sdk/lib/_internal/wasm/lib/type.dart
+++ b/sdk/lib/_internal/wasm/lib/type.dart
@@ -31,6 +31,7 @@
   bool get isFunctionTypeParameterType =>
       _testID(ClassID.cidFunctionTypeParameterType);
   bool get isFunction => _testID(ClassID.cidFunctionType);
+  bool get isRecord => _testID(ClassID.cidRecordType);
 
   T as<T>() => unsafeCast<T>(this);
 
@@ -433,6 +434,80 @@
   }
 }
 
+class _RecordType extends _Type {
+  final List<String> names;
+  final List<_Type> fieldTypes;
+
+  @pragma("wasm:entry-point")
+  _RecordType(this.names, this.fieldTypes, super.isDeclaredNullable);
+
+  @override
+  _Type get _asNonNullable => _RecordType(names, fieldTypes, false);
+
+  @override
+  _Type get _asNullable => _RecordType(names, fieldTypes, true);
+
+  @override
+  String toString() {
+    StringBuffer buffer = StringBuffer('(');
+
+    final int numPositionals = fieldTypes.length - names.length;
+    final int numNames = names.length;
+
+    for (int i = 0; i < numPositionals; i += 1) {
+      buffer.write(fieldTypes[i]);
+      if (i != fieldTypes.length - 1) {
+        buffer.write(', ');
+      }
+    }
+
+    if (names.isNotEmpty) {
+      buffer.write('{');
+      for (int i = 0; i < numNames; i += 1) {
+        final String fieldName = names[i];
+        final _Type fieldType = fieldTypes[numPositionals + i];
+        buffer.write(fieldType);
+        buffer.write(' ');
+        buffer.write(fieldName);
+        if (i != numNames - 1) {
+          buffer.write(', ');
+        }
+      }
+      buffer.write('}');
+    }
+
+    buffer.write(')');
+    if (isDeclaredNullable) {
+      buffer.write('?');
+    }
+    return buffer.toString();
+  }
+
+  @override
+  bool operator ==(Object other) {
+    if (other is! _RecordType) {
+      return false;
+    }
+
+    if (!_sameShape(other)) {
+      return false;
+    }
+
+    for (int fieldIdx = 0; fieldIdx < fieldTypes.length; fieldIdx += 1) {
+      if (fieldTypes[fieldIdx] != other.fieldTypes[fieldIdx]) {
+        return false;
+      }
+    }
+
+    return true;
+  }
+
+  bool _sameShape(_RecordType other) =>
+      fieldTypes.length == other.fieldTypes.length &&
+      // Name lists are constants and can be compared with `identical`.
+      identical(names, other.names);
+}
+
 external List<List<int>> _getTypeRulesSupers();
 external List<List<List<_Type>>> _getTypeRulesSubstitutions();
 external List<String> _getTypeNames();
@@ -793,6 +868,25 @@
     return true;
   }
 
+  bool isRecordSubtype(
+      _RecordType s, _Environment? sEnv, _RecordType t, _Environment? tEnv) {
+    // [s] <: [t] iff s and t have the same shape and fields of `s` are
+    // subtypes of the same field in `t` by index.
+    if (!s._sameShape(t)) {
+      return false;
+    }
+
+    final int numFields = s.fieldTypes.length;
+    for (int fieldIdx = 0; fieldIdx < numFields; fieldIdx += 1) {
+      if (!isSubtype(
+          s.fieldTypes[fieldIdx], sEnv, t.fieldTypes[fieldIdx], tEnv)) {
+        return false;
+      }
+    }
+
+    return true;
+  }
+
   // Subtype check based off of sdk/lib/_internal/js_runtime/lib/rti.dart.
   // Returns true if [s] is a subtype of [t], false otherwise.
   bool isSubtype(_Type s, _Environment? sEnv, _Type t, _Environment? tEnv) {
@@ -889,6 +983,18 @@
           s.as<_FunctionType>(), sEnv, t.as<_FunctionType>(), tEnv);
     }
 
+    // Records:
+    if (s.isRecord && t.isRecord) {
+      return isRecordSubtype(
+          s.as<_RecordType>(), sEnv, t.as<_RecordType>(), tEnv);
+    }
+
+    // Records are subtypes of the `Record` type:
+    if (s.isRecord && t.isInterface) {
+      final tInterfaceType = t.as<_InterfaceType>();
+      return tInterfaceType.classId == ClassID.cidRecord;
+    }
+
     // Interface Compositionality + Super-Interface:
     if (s.isInterface &&
         t.isInterface &&
diff --git a/tools/VERSION b/tools/VERSION
index a02aaff..6f4cbcd 100644
--- a/tools/VERSION
+++ b/tools/VERSION
@@ -27,5 +27,5 @@
 MAJOR 3
 MINOR 0
 PATCH 0
-PRERELEASE 232
+PRERELEASE 233
 PRERELEASE_PATCH 0