Add support for edition 2023 protos

Bug: b/339100248
Change-Id: I69b79e45e555cd749d26e17655eae8b4bf8d933b
diff --git a/.gitignore b/.gitignore
index 8114ef4..9aa08ca 100644
--- a/.gitignore
+++ b/.gitignore
@@ -273,8 +273,12 @@
 third_party/proguard/*
 third_party/proguardsettings.tar.gz
 third_party/proguardsettings/
+third_party/proto/runtime/edition2023
+third_party/proto/runtime/edition2023.tar.gz
 third_party/proto/runtime/legacy
 third_party/proto/runtime/legacy.tar.gz
+third_party/proto/test/edition2023
+third_party/proto/test/edition2023.tar.gz
 third_party/proto/test/proto2
 third_party/proto/test/proto2.tar.gz
 third_party/proto/test/proto3
diff --git a/d8_r8/commonBuildSrc/src/main/kotlin/DependenciesPlugin.kt b/d8_r8/commonBuildSrc/src/main/kotlin/DependenciesPlugin.kt
index 2620a18..99d3f6c 100644
--- a/d8_r8/commonBuildSrc/src/main/kotlin/DependenciesPlugin.kt
+++ b/d8_r8/commonBuildSrc/src/main/kotlin/DependenciesPlugin.kt
@@ -614,11 +614,21 @@
     Paths.get("third_party", "proguardsettings").toFile(),
     Paths.get("third_party", "proguardsettings.tar.gz.sha1").toFile(),
     DependencyType.X20)
+  val protoRuntimeEdition2023 = ThirdPartyDependency(
+    "protoRuntimeEdition2023",
+    Paths.get("third_party", "proto", "runtime", "edition2023").toFile(),
+    Paths.get("third_party", "proto", "runtime", "edition2023.tar.gz.sha1").toFile(),
+    DependencyType.X20)
   val protoRuntimeLegacy = ThirdPartyDependency(
     "protoRuntimeLegacy",
     Paths.get("third_party", "proto", "runtime", "legacy").toFile(),
     Paths.get("third_party", "proto", "runtime", "legacy.tar.gz.sha1").toFile(),
     DependencyType.X20)
+  val protoTestEdition2023 = ThirdPartyDependency(
+    "protoTestEdition2023",
+    Paths.get("third_party", "proto", "test", "edition2023").toFile(),
+    Paths.get("third_party", "proto", "test", "edition2023.tar.gz.sha1").toFile(),
+    DependencyType.X20)
   val protoTestProto2 = ThirdPartyDependency(
     "protoTestProto2",
     Paths.get("third_party", "proto", "test", "proto2").toFile(),
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/proto/schema/ProtoFieldType.java b/src/main/java/com/android/tools/r8/ir/analysis/proto/schema/ProtoFieldType.java
index b6ec3b3..157268e 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/proto/schema/ProtoFieldType.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/proto/schema/ProtoFieldType.java
@@ -24,14 +24,14 @@
   private static final int FIELD_IS_REQUIRED_MASK = 0x100;
   private static final int FIELD_ENFORCE_UTF8_MASK = 0x200;
   private static final int FIELD_NEEDS_IS_INITIALIZED_CHECK_MASK = 0x400;
-  private static final int FIELD_IS_MAP_FIELD_WITH_PROTO_2_ENUM_VALUE_MASK = 0x800;
+  private static final int FIELD_IS_LEGACY_ENUM_CLOSED_BIT_MASK = 0x800;
   private static final int FIELD_HAS_HAS_BIT_MASK = 0x1000;
 
   private final int id;
   private final boolean isRequired;
   private final boolean enforceUtf8Mask;
   private final boolean needsIsInitializedCheck;
-  private final boolean isMapFieldWithProto2EnumValue;
+  private final boolean isLegacyEnumClosedBit;
   private final boolean hasHasBit;
 
   ProtoFieldType(
@@ -39,13 +39,13 @@
       boolean isRequired,
       boolean enforceUtf8Mask,
       boolean needsIsInitializedCheck,
-      boolean isMapFieldWithProto2EnumValue,
+      boolean isLegacyEnumClosedBit,
       boolean hasHasBit) {
     this.id = id;
     this.isRequired = isRequired;
     this.enforceUtf8Mask = enforceUtf8Mask;
     this.needsIsInitializedCheck = needsIsInitializedCheck;
-    this.isMapFieldWithProto2EnumValue = isMapFieldWithProto2EnumValue;
+    this.isLegacyEnumClosedBit = isLegacyEnumClosedBit;
     this.hasHasBit = hasHasBit;
     assert isValid();
   }
@@ -58,7 +58,7 @@
           isBitInMaskSet(fieldTypeWithExtraBits, FIELD_IS_REQUIRED_MASK),
           isBitInMaskSet(fieldTypeWithExtraBits, FIELD_ENFORCE_UTF8_MASK),
           isBitInMaskSet(fieldTypeWithExtraBits, FIELD_NEEDS_IS_INITIALIZED_CHECK_MASK),
-          isBitInMaskSet(fieldTypeWithExtraBits, FIELD_IS_MAP_FIELD_WITH_PROTO_2_ENUM_VALUE_MASK),
+          isBitInMaskSet(fieldTypeWithExtraBits, FIELD_IS_LEGACY_ENUM_CLOSED_BIT_MASK),
           isBitInMaskSet(fieldTypeWithExtraBits, FIELD_HAS_HAS_BIT_MASK));
     } else {
       return new ProtoOneOfFieldType(
@@ -66,7 +66,7 @@
           isBitInMaskSet(fieldTypeWithExtraBits, FIELD_IS_REQUIRED_MASK),
           isBitInMaskSet(fieldTypeWithExtraBits, FIELD_ENFORCE_UTF8_MASK),
           isBitInMaskSet(fieldTypeWithExtraBits, FIELD_NEEDS_IS_INITIALIZED_CHECK_MASK),
-          isBitInMaskSet(fieldTypeWithExtraBits, FIELD_IS_MAP_FIELD_WITH_PROTO_2_ENUM_VALUE_MASK),
+          isBitInMaskSet(fieldTypeWithExtraBits, FIELD_IS_LEGACY_ENUM_CLOSED_BIT_MASK),
           isBitInMaskSet(fieldTypeWithExtraBits, FIELD_HAS_HAS_BIT_MASK));
     }
   }
@@ -98,8 +98,8 @@
     return id == MAP_ID;
   }
 
-  public boolean isMapFieldWithProto2EnumValue() {
-    return isMapFieldWithProto2EnumValue;
+  public boolean isLegacyEnumClosedBit() {
+    return isLegacyEnumClosedBit;
   }
 
   public boolean isMessage() {
@@ -147,9 +147,9 @@
       case ENUM_ID:
       case ENUM_LIST_ID:
       case ENUM_LIST_PACKAGED_ID:
-        return BooleanUtils.intValue(isProto2) + 1;
+        return BooleanUtils.intValue(isProto2 || isLegacyEnumClosedBit) + 1;
       case MAP_ID:
-        return BooleanUtils.intValue(isMapFieldWithProto2EnumValue) + 2;
+        return BooleanUtils.intValue(isLegacyEnumClosedBit) + 2;
       default:
         return 1;
     }
@@ -166,8 +166,8 @@
     if (needsIsInitializedCheck) {
       result |= FIELD_NEEDS_IS_INITIALIZED_CHECK_MASK;
     }
-    if (isMapFieldWithProto2EnumValue) {
-      result |= FIELD_IS_MAP_FIELD_WITH_PROTO_2_ENUM_VALUE_MASK;
+    if (isLegacyEnumClosedBit) {
+      result |= FIELD_IS_LEGACY_ENUM_CLOSED_BIT_MASK;
     }
     if (hasHasBit) {
       result |= FIELD_HAS_HAS_BIT_MASK;
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/proto/schema/ProtoOneOfFieldType.java b/src/main/java/com/android/tools/r8/ir/analysis/proto/schema/ProtoOneOfFieldType.java
index 1bc2ab5..9bfb3fd 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/proto/schema/ProtoOneOfFieldType.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/proto/schema/ProtoOneOfFieldType.java
@@ -15,15 +15,10 @@
       boolean isRequired,
       boolean enforceUtf8Mask,
       boolean needsIsInitializedCheck,
-      boolean isMapFieldWithProto2EnumValue,
+      boolean isLegacyEnumClosedBit,
       boolean hasHasBit) {
     super(
-        id,
-        isRequired,
-        enforceUtf8Mask,
-        needsIsInitializedCheck,
-        isMapFieldWithProto2EnumValue,
-        hasHasBit);
+        id, isRequired, enforceUtf8Mask, needsIsInitializedCheck, isLegacyEnumClosedBit, hasHasBit);
   }
 
   public ProtoFieldType getActualFieldType(ProtoFieldTypeFactory factory) {
@@ -69,7 +64,7 @@
       case GROUP_ID:
         return 1;
       case ENUM_ID:
-        return BooleanUtils.intValue(isProto2);
+        return BooleanUtils.intValue(isProto2 || isLegacyEnumClosedBit());
       default:
         return 0;
     }
diff --git a/src/main/java/com/android/tools/r8/utils/ThrowingConsumer.java b/src/main/java/com/android/tools/r8/utils/ThrowingConsumer.java
index 2b6eb66..1825fff 100644
--- a/src/main/java/com/android/tools/r8/utils/ThrowingConsumer.java
+++ b/src/main/java/com/android/tools/r8/utils/ThrowingConsumer.java
@@ -3,6 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.utils;
 
+import com.android.tools.r8.errors.Unreachable;
 import java.util.function.Consumer;
 
 /**
@@ -26,4 +27,10 @@
       throw runtimeException;
     }
   }
+
+  static <T, E extends Throwable> ThrowingConsumer<T, E> unreachable() {
+    return t -> {
+      throw new Unreachable();
+    };
+  }
 }
diff --git a/src/test/java/com/android/tools/r8/internal/proto/Proto2BuilderOnlyReferencedFromDynamicMethodTest.java b/src/test/java/com/android/tools/r8/internal/proto/Proto2BuilderOnlyReferencedFromDynamicMethodTest.java
index ed6f6fd..349f9fd 100644
--- a/src/test/java/com/android/tools/r8/internal/proto/Proto2BuilderOnlyReferencedFromDynamicMethodTest.java
+++ b/src/test/java/com/android/tools/r8/internal/proto/Proto2BuilderOnlyReferencedFromDynamicMethodTest.java
@@ -4,22 +4,27 @@
 
 package com.android.tools.r8.internal.proto;
 
+import static com.android.tools.r8.utils.codeinspector.Matchers.isAbsent;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isAbstract;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static org.hamcrest.CoreMatchers.containsString;
 import static org.hamcrest.CoreMatchers.not;
 import static org.hamcrest.MatcherAssert.assertThat;
-import static org.junit.Assert.assertFalse;
 
+import com.android.tools.r8.SingleTestRunResult;
 import com.android.tools.r8.TestParameters;
-import com.android.tools.r8.TestParametersCollection;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import java.util.List;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
 import org.junit.runners.Parameterized.Parameter;
 import org.junit.runners.Parameterized.Parameters;
 
+// TODO(b/339100248): Rename class to ProtoBuilderOnlyReferencedFromDynamicMethodTest.
+// TODO(b/339100248): Avoid creating edition2023 messages in proto2 package.
+//  Instead move proto2 and proto edition2023 messages to com.android.tools.r8.proto.
 @RunWith(Parameterized.class)
 public class Proto2BuilderOnlyReferencedFromDynamicMethodTest extends ProtoShrinkingTestBase {
 
@@ -28,16 +33,38 @@
   @Parameter(0)
   public TestParameters parameters;
 
-  @Parameters(name = "{0}")
-  public static TestParametersCollection data() {
-    return getTestParameters().withDefaultDexRuntime().withAllApiLevels().build();
+  @Parameter(1)
+  public ProtoRuntime protoRuntime;
+
+  @Parameter(2)
+  public ProtoTestSources protoTestSources;
+
+  @Parameters(name = "{0}, {1}, {2}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        getTestParameters().withDefaultDexRuntime().withAllApiLevels().build(),
+        ProtoRuntime.values(),
+        ProtoTestSources.getEdition2023AndProto2());
   }
 
   @Test
-  public void test() throws Exception {
+  public void testD8() throws Exception {
+    protoRuntime.assumeIsNewerThanOrEqualToMinimumRequiredRuntime(protoTestSources);
+    testForD8()
+        .addProgramFiles(protoRuntime.getProgramFiles())
+        .addProgramFiles(protoTestSources.getProgramFiles())
+        .setMinApi(parameters)
+        .run(parameters.getRuntime(), MAIN)
+        .apply(this::checkRunResult);
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    protoRuntime.assumeIsNewerThanOrEqualToMinimumRequiredRuntime(protoTestSources);
     testForR8(parameters.getBackend())
-        .apply(this::addProto2TestSources)
-        .apply(this::addLegacyRuntime)
+        .apply(protoRuntime::addRuntime)
+        .apply(protoRuntime::workaroundProtoMessageRemoval)
+        .addProgramFiles(protoTestSources.getProgramFiles())
         .addKeepMainRule(MAIN)
         .allowAccessModification()
         .allowDiagnosticMessages()
@@ -51,8 +78,7 @@
         .apply(this::inspectWarningMessages)
         .inspect(this::inspect)
         .run(parameters.getRuntime(), MAIN)
-        .assertSuccessWithOutputLines(
-            "false", "0", "false", "", "false", "0", "false", "0", "false", "");
+        .apply(this::checkRunResult);
   }
 
   private void inspect(CodeInspector outputInspector) {
@@ -63,9 +89,18 @@
     ClassSubject generatedMessageLiteBuilder =
         outputInspector.clazz("com.google.protobuf.GeneratedMessageLite$Builder");
     assertThat(generatedMessageLiteBuilder, isPresent());
-    assertFalse(generatedMessageLiteBuilder.isAbstract());
+    assertThat(generatedMessageLiteBuilder, not(isAbstract()));
     assertThat(
         outputInspector.clazz("com.android.tools.r8.proto2.TestProto$Primitives$Builder"),
-        not(isPresent()));
+        isAbsent());
+  }
+
+  private void checkRunResult(SingleTestRunResult<?> runResult) {
+    runResult.applyIf(
+        protoTestSources.getCorrespondingRuntime() == protoRuntime,
+        rr ->
+            rr.assertSuccessWithOutputLines(
+                "false", "0", "false", "", "false", "0", "false", "0", "false", ""),
+        rr -> rr.assertFailureWithErrorThatThrows(ArrayIndexOutOfBoundsException.class));
   }
 }
diff --git a/src/test/java/com/android/tools/r8/internal/proto/Proto2BuilderShrinkingTest.java b/src/test/java/com/android/tools/r8/internal/proto/Proto2BuilderShrinkingTest.java
index abedac8..3c6211e 100644
--- a/src/test/java/com/android/tools/r8/internal/proto/Proto2BuilderShrinkingTest.java
+++ b/src/test/java/com/android/tools/r8/internal/proto/Proto2BuilderShrinkingTest.java
@@ -4,22 +4,26 @@
 
 package com.android.tools.r8.internal.proto;
 
+import static com.android.tools.r8.utils.codeinspector.Matchers.isAbsent;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static org.hamcrest.CoreMatchers.containsString;
-import static org.hamcrest.CoreMatchers.not;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.Assert.assertTrue;
 
+import com.android.tools.r8.D8TestCompileResult;
 import com.android.tools.r8.R8TestCompileResult;
+import com.android.tools.r8.SingleTestRunResult;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.StringUtils;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
 import com.android.tools.r8.utils.codeinspector.InstructionSubject;
 import com.android.tools.r8.utils.codeinspector.MethodSubject;
 import com.google.common.collect.ImmutableList;
+import com.google.protobuf.InvalidProtocolBufferException;
 import java.util.List;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -27,6 +31,9 @@
 import org.junit.runners.Parameterized.Parameter;
 import org.junit.runners.Parameterized.Parameters;
 
+// TODO(b/339100248): Rename class to ProtoBuilderShrinkingTest.
+// TODO(b/339100248): Avoid creating edition2023 messages in proto2 package.
+//  Instead move proto2 and proto edition2023 messages to com.android.tools.r8.proto.
 @RunWith(Parameterized.class)
 public class Proto2BuilderShrinkingTest extends ProtoShrinkingTestBase {
 
@@ -76,19 +83,46 @@
   @Parameter(1)
   public TestParameters parameters;
 
-  @Parameters(name = "{1}, {0}")
+  @Parameter(2)
+  public ProtoRuntime protoRuntime;
+
+  @Parameter(3)
+  public ProtoTestSources protoTestSources;
+
+  @Parameters(name = "{1}, {2}, {3}, {0}")
   public static List<Object[]> data() {
     return buildParameters(
         MainClassesConfig.values(),
-        getTestParameters().withDefaultDexRuntime().withAllApiLevels().build());
+        getTestParameters().withDefaultDexRuntime().withAllApiLevels().build(),
+        ProtoRuntime.values(),
+        ProtoTestSources.getEdition2023AndProto2());
   }
 
   @Test
-  public void test() throws Exception {
-    R8TestCompileResult result =
+  public void testD8() throws Exception {
+    protoRuntime.assumeIsNewerThanOrEqualToMinimumRequiredRuntime(protoTestSources);
+    D8TestCompileResult compileResult =
+        testForD8()
+            .addProgramFiles(protoRuntime.getProgramFiles())
+            .addProgramFiles(protoTestSources.getProgramFiles())
+            .setMinApi(parameters)
+            .compile();
+
+    for (String main : config.getMainClasses()) {
+      compileResult
+          .run(parameters.getRuntime(), main)
+          .apply(runResult -> checkRunResult(runResult, main, false));
+    }
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    protoRuntime.assumeIsNewerThanOrEqualToMinimumRequiredRuntime(protoTestSources);
+    R8TestCompileResult compileResult =
         testForR8(parameters.getBackend())
-            .apply(this::addProto2TestSources)
-            .apply(this::addLegacyRuntime)
+            .apply(protoRuntime::addRuntime)
+            .apply(protoRuntime::workaroundProtoMessageRemoval)
+            .addProgramFiles(protoTestSources.getProgramFiles())
             .addKeepMainRules(config.getMainClasses())
             .allowAccessModification()
             .allowDiagnosticMessages()
@@ -104,7 +138,28 @@
             .inspect(this::inspect);
 
     for (String main : config.getMainClasses()) {
-      result.run(parameters.getRuntime(), main).assertSuccessWithOutput(getExpectedOutput(main));
+      compileResult
+          .run(parameters.getRuntime(), main)
+          .apply(runResult -> checkRunResult(runResult, main, true));
+    }
+  }
+
+  private void checkRunResult(SingleTestRunResult<?> runResult, String main, boolean isR8) {
+    if (main.equals("proto2.HasFlaggedOffExtensionBuilderTestClass")) {
+      runResult.applyIf(
+          isR8 && protoRuntime.isEdition2023() && parameters.getApiLevel() == AndroidApiLevel.O,
+          rr -> rr.assertFailureWithErrorThatThrows(IllegalAccessException.class),
+          !isR8 && protoRuntime.isEdition2023() && protoTestSources.isProto2(),
+          rr -> rr.assertFailureWithErrorThatThrows(InvalidProtocolBufferException.class),
+          rr -> rr.assertSuccessWithOutput(getExpectedOutput(main)));
+    } else {
+      runResult.applyIf(
+          protoTestSources.getCorrespondingRuntime() == protoRuntime,
+          rr -> rr.assertSuccessWithOutput(getExpectedOutput(main)),
+          main.equals("proto2.BuilderWithProtoBuilderSetterTestClass")
+              || main.equals("proto2.BuilderWithProtoSetterTestClass"),
+          rr -> rr.assertFailureWithErrorThatThrows(StringIndexOutOfBoundsException.class),
+          rr -> rr.assertFailureWithErrorThatThrows(ArrayIndexOutOfBoundsException.class));
     }
   }
 
@@ -193,16 +248,16 @@
     assertThat(
         outputInspector.clazz(
             "com.android.tools.r8.proto2.Shrinking$HasFlaggedOffExtension$Builder"),
-        not(isPresent()));
+        isAbsent());
     assertThat(
         outputInspector.clazz("com.android.tools.r8.proto2.TestProto$Primitives$Builder"),
-        not(isPresent()));
+        isAbsent());
     assertThat(
         outputInspector.clazz("com.android.tools.r8.proto2.TestProto$OuterMessage$Builder"),
-        not(isPresent()));
+        isAbsent());
     assertThat(
         outputInspector.clazz("com.android.tools.r8.proto2.TestProto$NestedMessage$Builder"),
-        not(isPresent()));
+        isAbsent());
   }
 
   private void verifyMethodToInvokeValuesAreAbsent(CodeInspector outputInspector) {
@@ -220,20 +275,23 @@
       assertThat(mainMethodSubject, isPresent());
 
       // Verify that the calls to GeneratedMessageLite.createBuilder() have been inlined.
-      assertTrue(
-          mainMethodSubject
-              .streamInstructions()
-              .filter(InstructionSubject::isInvoke)
-              .map(InstructionSubject::getMethod)
-              .allMatch(
-                  method ->
-                      method.getHolderType()
-                              != generatedMessageLiteClassSubject.getDexProgramClass().getType()
-                          || (isInitializedMethodSubject.isPresent()
-                              && method
-                                  == isInitializedMethodSubject
-                                      .getProgramMethod()
-                                      .getReference())));
+      // TODO(b/339100248): Investigate inadequate inlining with edition2023.
+      if (protoRuntime.isLegacy()) {
+        assertTrue(
+            mainMethodSubject
+                .streamInstructions()
+                .filter(InstructionSubject::isInvoke)
+                .map(InstructionSubject::getMethod)
+                .allMatch(
+                    method ->
+                        method.getHolderType()
+                                != generatedMessageLiteClassSubject.getDexProgramClass().getType()
+                            || (isInitializedMethodSubject.isPresent()
+                                && method
+                                    == isInitializedMethodSubject
+                                        .getProgramMethod()
+                                        .getReference())));
+      }
 
       // Verify that there are no accesses to MethodToInvoke after inlining createBuilder() -- and
       // specifically no accesses to MethodToInvoke.NEW_BUILDER.
diff --git a/src/test/java/com/android/tools/r8/internal/proto/Proto2ShrinkingTest.java b/src/test/java/com/android/tools/r8/internal/proto/Proto2ShrinkingTest.java
index 462515f..1a81f39 100644
--- a/src/test/java/com/android/tools/r8/internal/proto/Proto2ShrinkingTest.java
+++ b/src/test/java/com/android/tools/r8/internal/proto/Proto2ShrinkingTest.java
@@ -1,7 +1,6 @@
 // Copyright (c) 2019, the R8 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.
-
 package com.android.tools.r8.internal.proto;
 
 import static com.android.tools.r8.utils.codeinspector.Matchers.isAbsentIf;
@@ -10,8 +9,10 @@
 import static org.hamcrest.CoreMatchers.not;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assume.assumeTrue;
 
 import com.android.tools.r8.R8TestRunResult;
+import com.android.tools.r8.SingleTestRunResult;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.graph.DexItemFactory;
@@ -28,6 +29,9 @@
 import org.junit.runners.Parameterized.Parameter;
 import org.junit.runners.Parameterized.Parameters;
 
+// TODO(b/339100248): Rename class to ProtoShrinkingTest.
+// TODO(b/339100248): Avoid creating edition2023 messages in proto2 package.
+//  Instead move proto2 and proto edition2023 messages to com.android.tools.r8.proto.
 @RunWith(Parameterized.class)
 public class Proto2ShrinkingTest extends ProtoShrinkingTestBase {
 
@@ -51,6 +55,8 @@
   private static final String USES_ONLY_REPEATED_FIELDS =
       "com.android.tools.r8.proto2.Shrinking$UsesOnlyRepeatedFields";
 
+  private static final String MAIN = "proto2.TestClass";
+
   @Parameter(0)
   public boolean allowAccessModification;
 
@@ -60,21 +66,42 @@
   @Parameter(2)
   public TestParameters parameters;
 
-  @Parameters(name = "{2}, allow access modification: {0}, enable minification: {1}")
+  @Parameter(3)
+  public ProtoRuntime protoRuntime;
+
+  @Parameter(4)
+  public ProtoTestSources protoTestSources;
+
+  @Parameters(name = "{2}, {3}, {4}, allow access modification: {0}, enable minification: {1}")
   public static List<Object[]> data() {
     return buildParameters(
         BooleanUtils.values(),
         BooleanUtils.values(),
-        getTestParameters().withDefaultDexRuntime().withAllApiLevels().build());
+        getTestParameters().withDefaultDexRuntime().withAllApiLevels().build(),
+        ProtoRuntime.values(),
+        ProtoTestSources.getEdition2023AndProto2());
   }
 
   @Test
-  public void test() throws Exception {
+  public void testD8() throws Exception {
+    protoRuntime.assumeIsNewerThanOrEqualToMinimumRequiredRuntime(protoTestSources);
+    testForD8()
+        .addProgramFiles(protoRuntime.getProgramFiles())
+        .addProgramFiles(protoTestSources.getProgramFiles())
+        .setMinApi(parameters)
+        .run(parameters.getRuntime(), MAIN)
+        .apply(this::checkRunResult);
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    protoRuntime.assumeIsNewerThanOrEqualToMinimumRequiredRuntime(protoTestSources);
     R8TestRunResult result =
         testForR8(parameters.getBackend())
-            .apply(this::addProto2TestSources)
-            .apply(this::addLegacyRuntime)
-            .addKeepMainRule("proto2.TestClass")
+            .apply(protoRuntime::addRuntime)
+            .apply(protoRuntime::workaroundProtoMessageRemoval)
+            .addProgramFiles(protoTestSources.getProgramFiles())
+            .addKeepMainRule(MAIN)
             // TODO(b/173340579): This rule should not be needed to allow shrinking of
             //  PartiallyUsed$Enum.
             .addNoHorizontalClassMergingRule(PARTIALLY_USED + "$Enum$1")
@@ -92,15 +119,41 @@
             .apply(this::inspectWarningMessages)
             .inspect(
                 outputInspector -> {
-                  CodeInspector inputInspector = getProto2TestSourcesInspector();
+                  CodeInspector inputInspector = protoTestSources.getInspector();
                   verifyMapAndRequiredFieldsAreKept(inputInspector, outputInspector);
                   verifyUnusedExtensionsAreRemoved(inputInspector, outputInspector);
                   verifyUnusedFieldsAreRemoved(inputInspector, outputInspector);
                   verifyUnusedHazzerBitFieldsAreRemoved(inputInspector, outputInspector);
                   verifyUnusedTypesAreRemoved(inputInspector, outputInspector);
                 })
-            .run(parameters.getRuntime(), "proto2.TestClass")
-            .assertSuccessWithOutputLines(
+            .run(parameters.getRuntime(), MAIN)
+            .apply(this::checkRunResult);
+
+    if (protoTestSources.getCorrespondingRuntime() != protoRuntime) {
+      result.assertFailure();
+      return;
+    }
+
+    DexItemFactory dexItemFactory = new DexItemFactory();
+    ProtoApplicationStats original =
+        new ProtoApplicationStats(dexItemFactory, protoTestSources.getInspector());
+    ProtoApplicationStats actual =
+        new ProtoApplicationStats(dexItemFactory, result.inspector(), original);
+
+    assertEquals(
+        ImmutableSet.of(),
+        actual.getGeneratedExtensionRegistryStats().getSpuriouslyRetainedExtensionFields());
+
+    if (ToolHelper.isLocalDevelopment()) {
+      System.out.println(actual.getStats());
+    }
+  }
+
+  private void checkRunResult(SingleTestRunResult<?> runResult) {
+    runResult.applyIf(
+        protoTestSources.getCorrespondingRuntime() == protoRuntime,
+        rr ->
+            rr.assertSuccessWithOutputLines(
                 "--- roundtrip ---",
                 "true",
                 "123",
@@ -126,21 +179,8 @@
                 "true",
                 "10",
                 "10",
-                "10");
-
-    DexItemFactory dexItemFactory = new DexItemFactory();
-    ProtoApplicationStats original =
-        new ProtoApplicationStats(dexItemFactory, getProto2TestSourcesInspector());
-    ProtoApplicationStats actual =
-        new ProtoApplicationStats(dexItemFactory, result.inspector(), original);
-
-    assertEquals(
-        ImmutableSet.of(),
-        actual.getGeneratedExtensionRegistryStats().getSpuriouslyRetainedExtensionFields());
-
-    if (ToolHelper.isLocalDevelopment()) {
-      System.out.println(actual.getStats());
-    }
+                "10"),
+        rr -> rr.assertFailureWithErrorThatThrows(ArrayIndexOutOfBoundsException.class));
   }
 
   private void verifyMapAndRequiredFieldsAreKept(
@@ -370,11 +410,13 @@
   }
 
   @Test
-  public void testNoRewriting() throws Exception {
+  public void testR8NoRewriting() throws Exception {
+    protoRuntime.assumeIsNewerThanOrEqualToMinimumRequiredRuntime(protoTestSources);
     testForR8(parameters.getBackend())
-        .apply(this::addProto2TestSources)
-        .apply(this::addLegacyRuntime)
-        .addKeepMainRule("proto2.TestClass")
+        .apply(protoRuntime::addRuntime)
+        .apply(protoRuntime::workaroundProtoMessageRemoval)
+        .addProgramFiles(protoTestSources.getProgramFiles())
+        .addKeepMainRule(MAIN)
         // Retain all protos.
         .addKeepRules(keepAllProtosRule())
         // Retain the signature of dynamicMethod() and newMessageInfo().
@@ -392,15 +434,18 @@
         .apply(this::inspectWarningMessages)
         .inspect(
             inspector ->
-                assertRewrittenProtoSchemasMatch(getProto2TestSourcesInspector(), inspector));
+                assertRewrittenProtoSchemasMatch(protoTestSources.getInspector(), inspector));
   }
 
   @Test
-  public void testTwoExtensionRegistrys() throws Exception {
+  public void testR8TwoExtensionRegistries() throws Exception {
+    protoRuntime.assumeIsNewerThanOrEqualToMinimumRequiredRuntime(protoTestSources);
+    assumeTrue("Only proto2 has two extension registries", protoTestSources.isProto2());
     testForR8(parameters.getBackend())
-        .apply(this::addProto2TestSources)
-        .apply(this::addLegacyRuntime)
-        .addKeepMainRule("proto2.TestClass")
+        .apply(protoRuntime::addRuntime)
+        .apply(protoRuntime::workaroundProtoMessageRemoval)
+        .addProgramFiles(protoTestSources.getProgramFiles())
+        .addKeepMainRule(MAIN)
         .addKeepRules(findLiteExtensionByNumberInDuplicateCalledRule())
         // TODO(b/173340579): This rule should not be needed to allow shrinking of
         //  PartiallyUsed$Enum.
@@ -418,7 +463,7 @@
         .apply(this::inspectWarningMessages)
         .inspect(
             outputInspector -> {
-              CodeInspector inputInspector = getProto2TestSourcesInspector();
+              CodeInspector inputInspector = protoTestSources.getInspector();
               verifyUnusedExtensionsAreRemoved(inputInspector, outputInspector);
               verifyUnusedExtensionsAreRemoved(
                   inputInspector,
diff --git a/src/test/java/com/android/tools/r8/internal/proto/Proto3ShrinkingTest.java b/src/test/java/com/android/tools/r8/internal/proto/Proto3ShrinkingTest.java
index 49fdd08..21314c0 100644
--- a/src/test/java/com/android/tools/r8/internal/proto/Proto3ShrinkingTest.java
+++ b/src/test/java/com/android/tools/r8/internal/proto/Proto3ShrinkingTest.java
@@ -16,10 +16,12 @@
 import com.android.tools.r8.utils.BooleanUtils;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.google.common.collect.ImmutableList;
 import java.util.List;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
 
 @RunWith(Parameterized.class)
 public class Proto3ShrinkingTest extends ProtoShrinkingTestBase {
@@ -27,30 +29,39 @@
   private static final String PARTIALLY_USED =
       "com.android.tools.r8.proto3.Shrinking$PartiallyUsed";
 
-  private final boolean allowAccessModification;
-  private final boolean enableMinification;
-  private final TestParameters parameters;
+  @Parameter(0)
+  public boolean allowAccessModification;
 
-  @Parameterized.Parameters(name = "{2}, allow access modification: {0}, enable minification: {1}")
+  @Parameter(1)
+  public boolean enableMinification;
+
+  @Parameter(2)
+  public TestParameters parameters;
+
+  @Parameter(3)
+  public ProtoRuntime protoRuntime;
+
+  @Parameter(4)
+  public ProtoTestSources protoTestSources;
+
+  @Parameterized.Parameters(
+      name = "{2}, {3}, {4}, allow access modification: {0}, enable minification: {1}")
   public static List<Object[]> data() {
     return buildParameters(
         BooleanUtils.values(),
         BooleanUtils.values(),
-        getTestParameters().withDefaultDexRuntime().withAllApiLevels().build());
-  }
-
-  public Proto3ShrinkingTest(
-      boolean allowAccessModification, boolean enableMinification, TestParameters parameters) {
-    this.allowAccessModification = allowAccessModification;
-    this.enableMinification = enableMinification;
-    this.parameters = parameters;
+        getTestParameters().withDefaultDexRuntime().withAllApiLevels().build(),
+        ProtoRuntime.values(),
+        ImmutableList.of(ProtoTestSources.PROTO3));
   }
 
   @Test
-  public void test() throws Exception {
+  public void testR8() throws Exception {
+    protoRuntime.assumeIsNewerThanOrEqualToMinimumRequiredRuntime(protoTestSources);
     testForR8(parameters.getBackend())
-        .apply(this::addProto3TestSources)
-        .apply(this::addLegacyRuntime)
+        .apply(protoRuntime::addRuntime)
+        .apply(protoRuntime::workaroundProtoMessageRemoval)
+        .addProgramFiles(protoTestSources.getProgramFiles())
         .addKeepMainRule("proto3.TestClass")
         .allowAccessModification(allowAccessModification)
         .allowDiagnosticMessages()
@@ -65,7 +76,7 @@
         .assertAllWarningMessagesMatch(equalTo("Resource 'META-INF/MANIFEST.MF' already exists."))
         .inspect(
             outputInspector -> {
-              CodeInspector inputInspector = getProto3TestSourcesInspector();
+              CodeInspector inputInspector = protoTestSources.getInspector();
               verifyUnusedFieldsAreRemoved(inputInspector, outputInspector);
             })
         .run(parameters.getRuntime(), "proto3.TestClass")
@@ -93,10 +104,12 @@
   }
 
   @Test
-  public void testNoRewriting() throws Exception {
+  public void testR8NoRewriting() throws Exception {
+    protoRuntime.assumeIsNewerThanOrEqualToMinimumRequiredRuntime(protoTestSources);
     testForR8(parameters.getBackend())
-        .apply(this::addProto3TestSources)
-        .apply(this::addLegacyRuntime)
+        .apply(protoRuntime::addRuntime)
+        .apply(protoRuntime::workaroundProtoMessageRemoval)
+        .addProgramFiles(protoTestSources.getProgramFiles())
         .addKeepMainRule("proto3.TestClass")
         // Retain all protos.
         .addKeepRules(keepAllProtosRule())
@@ -118,6 +131,6 @@
                 containsString("required for default or static interface methods desugaring")))
         .inspect(
             inspector ->
-                assertRewrittenProtoSchemasMatch(getProto3TestSourcesInspector(), inspector));
+                assertRewrittenProtoSchemasMatch(protoTestSources.getInspector(), inspector));
   }
 }
diff --git a/src/test/java/com/android/tools/r8/internal/proto/ProtoRuntime.java b/src/test/java/com/android/tools/r8/internal/proto/ProtoRuntime.java
new file mode 100644
index 0000000..a9dd2b2
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/internal/proto/ProtoRuntime.java
@@ -0,0 +1,66 @@
+// Copyright (c) 2024, the R8 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.
+package com.android.tools.r8.internal.proto;
+
+import static org.junit.Assume.assumeTrue;
+
+import com.android.tools.r8.R8TestBuilder;
+import com.android.tools.r8.ToolHelper;
+import com.google.common.collect.ImmutableList;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Collection;
+
+public enum ProtoRuntime {
+  EDITION2023("edition2023", 1),
+  LEGACY("legacy", 0);
+
+  private final String runtimeName;
+  private final int syntheticVersionNumber;
+
+  ProtoRuntime(String runtimeName, int syntheticVersionNumber) {
+    this.runtimeName = runtimeName;
+    this.syntheticVersionNumber = syntheticVersionNumber;
+  }
+
+  public void addRuntime(R8TestBuilder<?> testBuilder) {
+    Path runtimeDir = Paths.get(ToolHelper.PROTO_RUNTIME_DIR, runtimeName);
+    testBuilder
+        .addProgramFiles(runtimeDir.resolve("libprotobuf_lite.jar"))
+        .addKeepRuleFiles(runtimeDir.resolve("lite_proguard.pgcfg"));
+  }
+
+  public Collection<Path> getKeepRuleFiles() {
+    Path runtimeDir = Paths.get(ToolHelper.PROTO_RUNTIME_DIR, runtimeName);
+    return ImmutableList.of(runtimeDir.resolve("lite_proguard.pgcfg"));
+  }
+
+  public Collection<Path> getProgramFiles() {
+    Path runtimeDir = Paths.get(ToolHelper.PROTO_RUNTIME_DIR, runtimeName);
+    return ImmutableList.of(runtimeDir.resolve("libprotobuf_lite.jar"));
+  }
+
+  public void assumeIsNewerThanOrEqualToMinimumRequiredRuntime(ProtoTestSources protoTestSources) {
+    assumeTrue(isNewerThanOrEqualTo(protoTestSources.getMinimumRequiredRuntime()));
+  }
+
+  public boolean isEdition2023() {
+    return this == EDITION2023;
+  }
+
+  public boolean isLegacy() {
+    return this == LEGACY;
+  }
+
+  public boolean isNewerThanOrEqualTo(ProtoRuntime protoRuntime) {
+    return syntheticVersionNumber >= protoRuntime.syntheticVersionNumber;
+  }
+
+  // The class com.google.protobuf.ProtoMessage is not present in newer proto lite runtimes.
+  public void workaroundProtoMessageRemoval(R8TestBuilder<?> testBuilder) {
+    if (isNewerThanOrEqualTo(ProtoRuntime.EDITION2023)) {
+      testBuilder.addDontWarn("com.google.protobuf.ProtoMessage");
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/internal/proto/ProtoShrinkingTestBase.java b/src/test/java/com/android/tools/r8/internal/proto/ProtoShrinkingTestBase.java
index cd21b9f..f881944 100644
--- a/src/test/java/com/android/tools/r8/internal/proto/ProtoShrinkingTestBase.java
+++ b/src/test/java/com/android/tools/r8/internal/proto/ProtoShrinkingTestBase.java
@@ -14,10 +14,8 @@
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
 
-import com.android.tools.r8.R8TestBuilder;
 import com.android.tools.r8.R8TestCompileResult;
 import com.android.tools.r8.TestBase;
-import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.ir.analysis.proto.GeneratedMessageLiteShrinker;
 import com.android.tools.r8.ir.analysis.proto.ProtoReferences;
@@ -28,13 +26,8 @@
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
 import com.android.tools.r8.utils.codeinspector.FoundClassSubject;
 import com.android.tools.r8.utils.codeinspector.FoundMethodSubject;
-import com.google.common.collect.ImmutableList;
 import it.unimi.dsi.fastutil.ints.IntArrayList;
 import it.unimi.dsi.fastutil.ints.IntList;
-import java.io.IOException;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.util.Collection;
 import java.util.HashMap;
 import java.util.Map;
 
@@ -57,47 +50,6 @@
     }
   }
 
-  void addLegacyRuntime(R8TestBuilder<?> testBuilder) {
-    Path runtimeDir = Paths.get(ToolHelper.PROTO_RUNTIME_DIR, "legacy");
-    addRuntime(testBuilder, runtimeDir);
-  }
-
-  private void addRuntime(R8TestBuilder<?> testBuilder, Path runtimeDir) {
-    testBuilder
-        .addProgramFiles(runtimeDir.resolve("libprotobuf_lite.jar"))
-        .addKeepRuleFiles(runtimeDir.resolve("lite_proguard.pgcfg"));
-  }
-
-  void addProto2TestSources(R8TestBuilder<?> testBuilder) {
-    testBuilder.addProgramFiles(getProto2TestSources());
-  }
-
-  void addProto3TestSources(R8TestBuilder<?> testBuilder) {
-    testBuilder.addProgramFiles(getProto3TestSources());
-  }
-
-  private Collection<Path> getProto2TestSources() {
-    Path testDir = Paths.get(ToolHelper.PROTO_TEST_DIR, "proto2");
-    return getTestSources(testDir);
-  }
-
-  private Collection<Path> getProto3TestSources() {
-    Path testDir = Paths.get(ToolHelper.PROTO_TEST_DIR, "proto3");
-    return getTestSources(testDir);
-  }
-
-  private Collection<Path> getTestSources(Path testDir) {
-    return ImmutableList.of(testDir.resolve("proto.jar"), testDir.resolve("test.jar"));
-  }
-
-  CodeInspector getProto2TestSourcesInspector() throws IOException {
-    return new CodeInspector(getProto2TestSources());
-  }
-
-  CodeInspector getProto3TestSourcesInspector() throws IOException {
-    return new CodeInspector(getProto3TestSources());
-  }
-
   static String findLiteExtensionByNumberInDuplicateCalledRule() {
     return StringUtils.lines(
         "-keep class com.google.protobuf.proto2_registryGeneratedExtensionRegistryLiteDuplicate {",
diff --git a/src/test/java/com/android/tools/r8/internal/proto/ProtoTestSources.java b/src/test/java/com/android/tools/r8/internal/proto/ProtoTestSources.java
new file mode 100644
index 0000000..2322434
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/internal/proto/ProtoTestSources.java
@@ -0,0 +1,78 @@
+// Copyright (c) 2024, the R8 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.
+package com.android.tools.r8.internal.proto;
+
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.errors.Unreachable;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.google.common.collect.ImmutableList;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Collection;
+
+public enum ProtoTestSources {
+  EDITION2023("edition2023"),
+  PROTO2("proto2"),
+  PROTO3("proto3");
+
+  private final String testName;
+
+  ProtoTestSources(String testName) {
+    this.testName = testName;
+  }
+
+  public static Collection<ProtoTestSources> getEdition2023AndProto2() {
+    return ImmutableList.of(EDITION2023, PROTO2);
+  }
+
+  public ProtoRuntime getCorrespondingRuntime() {
+    switch (this) {
+      case EDITION2023:
+        return ProtoRuntime.EDITION2023;
+      case PROTO2:
+      case PROTO3:
+        return ProtoRuntime.LEGACY;
+      default:
+        throw new Unreachable();
+    }
+  }
+
+  public CodeInspector getInspector() throws IOException {
+    return new CodeInspector(getProgramFiles());
+  }
+
+  public ProtoRuntime getMinimumRequiredRuntime() {
+    switch (this) {
+      case EDITION2023:
+        return ProtoRuntime.EDITION2023;
+      case PROTO2:
+      case PROTO3:
+        return ProtoRuntime.LEGACY;
+      default:
+        throw new Unreachable();
+    }
+  }
+
+  public Collection<Path> getProgramFiles() {
+    Path testDir = getTestDir();
+    ImmutableList.Builder<Path> builder =
+        ImmutableList.<Path>builder()
+            .add(testDir.resolve("proto.jar"), testDir.resolve("test.jar"));
+    Path registryJar = testDir.resolve("registry.jar");
+    if (Files.exists(registryJar)) {
+      builder.add(registryJar);
+    }
+    return builder.build();
+  }
+
+  public Path getTestDir() {
+    return Paths.get(ToolHelper.PROTO_TEST_DIR, testName);
+  }
+
+  public boolean isProto2() {
+    return this == PROTO2;
+  }
+}
diff --git a/src/test/testbase/java/com/android/tools/r8/TestRunResult.java b/src/test/testbase/java/com/android/tools/r8/TestRunResult.java
index 595b3b7..dc55969 100644
--- a/src/test/testbase/java/com/android/tools/r8/TestRunResult.java
+++ b/src/test/testbase/java/com/android/tools/r8/TestRunResult.java
@@ -54,16 +54,13 @@
   }
 
   public <S extends Throwable, T extends Throwable> RR applyIf(
-      boolean condition, ThrowingConsumer<RR, S> thenConsumer, ThrowingConsumer<RR, T> elseConsumer)
+      boolean condition1,
+      ThrowingConsumer<RR, S> thenConsumer1,
+      ThrowingConsumer<RR, T> elseConsumer)
       throws S, T {
-    return applyIf(
-        condition,
-        thenConsumer,
-        true,
-        elseConsumer,
-        r -> {
-          assert false;
-        });
+    boolean condition2 = false;
+    ThrowingConsumer<RR, RuntimeException> thenConsumer2 = ThrowingConsumer.unreachable();
+    return applyIf(condition1, thenConsumer1, condition2, thenConsumer2, elseConsumer);
   }
 
   public <S extends Throwable, T extends Throwable, U extends Throwable> RR applyIf(
@@ -73,16 +70,16 @@
       ThrowingConsumer<RR, T> thenConsumer2,
       ThrowingConsumer<RR, U> elseConsumer)
       throws S, T, U {
+    boolean condition3 = false;
+    ThrowingConsumer<RR, RuntimeException> thenConsumer3 = ThrowingConsumer.unreachable();
     return applyIf(
         condition1,
         thenConsumer1,
         condition2,
         thenConsumer2,
-        true,
-        elseConsumer,
-        r -> {
-          assert false;
-        });
+        condition3,
+        thenConsumer3,
+        elseConsumer);
   }
 
   public <S extends Throwable, T extends Throwable, U extends Throwable, V extends Throwable>
@@ -95,12 +92,45 @@
           ThrowingConsumer<RR, U> thenConsumer3,
           ThrowingConsumer<RR, V> elseConsumer)
           throws S, T, U, V {
+    boolean condition4 = false;
+    ThrowingConsumer<RR, RuntimeException> thenConsumer4 = ThrowingConsumer.unreachable();
+    return applyIf(
+        condition1,
+        thenConsumer1,
+        condition2,
+        thenConsumer2,
+        condition3,
+        thenConsumer3,
+        condition4,
+        thenConsumer4,
+        elseConsumer);
+  }
+
+  public <
+          S extends Throwable,
+          T extends Throwable,
+          U extends Throwable,
+          V extends Throwable,
+          W extends Throwable>
+      RR applyIf(
+          boolean condition1,
+          ThrowingConsumer<RR, S> thenConsumer1,
+          boolean condition2,
+          ThrowingConsumer<RR, T> thenConsumer2,
+          boolean condition3,
+          ThrowingConsumer<RR, U> thenConsumer3,
+          boolean condition4,
+          ThrowingConsumer<RR, W> thenConsumer4,
+          ThrowingConsumer<RR, V> elseConsumer)
+          throws S, T, U, V, W {
     if (condition1) {
       thenConsumer1.accept(self());
     } else if (condition2) {
       thenConsumer2.accept(self());
     } else if (condition3) {
       thenConsumer3.accept(self());
+    } else if (condition4) {
+      thenConsumer4.accept(self());
     } else {
       elseConsumer.accept(self());
     }
diff --git a/third_party/proto/runtime/edition2023.tar.gz.sha1 b/third_party/proto/runtime/edition2023.tar.gz.sha1
new file mode 100644
index 0000000..8c8140e
--- /dev/null
+++ b/third_party/proto/runtime/edition2023.tar.gz.sha1
@@ -0,0 +1 @@
+583591cbd3ee7d7dd5ca9e0fefc4f015fa7b6007
\ No newline at end of file
diff --git a/third_party/proto/test/edition2023.tar.gz.sha1 b/third_party/proto/test/edition2023.tar.gz.sha1
new file mode 100644
index 0000000..9a01bb3
--- /dev/null
+++ b/third_party/proto/test/edition2023.tar.gz.sha1
@@ -0,0 +1 @@
+c9ec54459ab7787744f41e403fe641582b2146d7
\ No newline at end of file