Repository: cashapp/redwood
Pull request #462 (2022-10-17 19:48:56)

Make tags unsigned in protocol and schema model
Refs #418


Patch:
diff --git a/redwood-cli/src/main/kotlin/app/cash/redwood/generator/composeGeneration.kt b/redwood-cli/src/main/kotlin/app/cash/redwood/generator/composeGeneration.kt
index a11d221e8..f1427186e 100644
--- a/redwood-cli/src/main/kotlin/app/cash/redwood/generator/composeGeneration.kt
+++ b/redwood-cli/src/main/kotlin/app/cash/redwood/generator/composeGeneration.kt
@@ -175,7 +175,7 @@ internal fun generateComposable(
               }
               is Children -> {
                 childrenLambda.apply {
-                  add("%M(%L) {\n", syntheticChildren, trait.tag)
+                  add("%M(%LU) {\n", syntheticChildren, trait.tag)
                   indent()
                   trait.scope?.let { scope ->
                     add("%T.", ClassName(schema.composePackage(), scope.simpleName!! + "Impl"))
diff --git a/redwood-cli/src/main/kotlin/app/cash/redwood/generator/diffConsumingGeneration.kt b/redwood-cli/src/main/kotlin/app/cash/redwood/generator/diffConsumingGeneration.kt
index 1823ab995..260ca63d6 100644
--- a/redwood-cli/src/main/kotlin/app/cash/redwood/generator/diffConsumingGeneration.kt
+++ b/redwood-cli/src/main/kotlin/app/cash/redwood/generator/diffConsumingGeneration.kt
@@ -35,6 +35,7 @@ import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
 import com.squareup.kotlinpoet.PropertySpec
 import com.squareup.kotlinpoet.TypeName
 import com.squareup.kotlinpoet.TypeSpec
+import com.squareup.kotlinpoet.U_INT
 import com.squareup.kotlinpoet.asTypeName
 
 /*
@@ -116,7 +117,7 @@ internal fun generateDiffConsumingWidgetFactory(schema: Schema, host: Schema = s
                   for (widget in schema.widgets.sortedBy { it.tag }) {
                     addStatement("%L -> %N()", widget.tag, widget.type.flatName)
                   }
-                  for (dependency in schema.dependencies.sortedBy { it.widgets.firstOrNull()?.tag ?: 0 }) {
+                  for (dependency in schema.dependencies.sortedBy { it.widgets.firstOrNull()?.tag ?: 0U }) {
                     for (widget in dependency.widgets.sortedBy { it.tag }) {
                       addStatement("%L -> %N.%N()", widget.tag, dependency.name, widget.type.flatName)
                     }
@@ -275,14 +276,14 @@ internal fun generateDiffConsumingWidget(schema: Schema, widget: Widget, host: S
                       }
 
                       addStatement(
-                        "%L -> delegate.%N(json.decodeFromJsonElement(serializer_%L, diff.value))",
+                        "%LU -> delegate.%N(json.decodeFromJsonElement(serializer_%L, diff.value))",
                         trait.tag,
                         trait.name,
                         serializerId,
                       )
                     }
                     is Event -> {
-                      beginControlFlow("%L ->", trait.tag)
+                      beginControlFlow("%LU ->", trait.tag)
                       beginControlFlow(
                         "val %N: %T = if (diff.value.%M.%M)",
                         trait.name,
@@ -296,13 +297,13 @@ internal fun generateDiffConsumingWidget(schema: Schema, widget: Widget, host: S
                           nextSerializerId++
                         }
                         addStatement(
-                          "{ eventSink.sendEvent(%T(diff.id, %L, json.encodeToJsonElement(serializer_%L, it))) }",
+                          "{ eventSink.sendEvent(%T(diff.id, %LU, json.encodeToJsonElement(serializer_%L, it))) }",
                           eventType,
                           trait.tag,
                           serializerId,
                         )
                       } else {
-                        addStatement("{ eventSink.sendEvent(%T(diff.id, %L)) }", eventType, trait.tag)
+                        addStatement("{ eventSink.sendEvent(%T(diff.id, %LU)) }", eventType, trait.tag)
                       }
                       nextControlFlow("else")
                       addStatement("null")
@@ -331,13 +332,13 @@ internal fun generateDiffConsumingWidget(schema: Schema, widget: Widget, host: S
           addFunction(
             FunSpec.builder("children")
               .addModifiers(OVERRIDE)
-              .addParameter("tag", INT)
+              .addParameter("tag", U_INT)
               .returns(childrenOfT.copy(nullable = true))
               .apply {
                 if (childrens.isNotEmpty()) {
                   beginControlFlow("return when (tag)")
                   for (children in childrens) {
-                    addStatement("%L -> delegate.%N", children.tag, children.name)
+                    addStatement("%LU -> delegate.%N", children.tag, children.name)
                   }
                   beginControlFlow("else ->")
                   addStatement("mismatchHandler.onUnknownChildren(%L, tag)", widget.tag)
diff --git a/redwood-cli/src/main/kotlin/app/cash/redwood/generator/diffProducingGeneration.kt b/redwood-cli/src/main/kotlin/app/cash/redwood/generator/diffProducingGeneration.kt
index e0befd398..7f067a284 100644
--- a/redwood-cli/src/main/kotlin/app/cash/redwood/generator/diffProducingGeneration.kt
+++ b/redwood-cli/src/main/kotlin/app/cash/redwood/generator/diffProducingGeneration.kt
@@ -190,7 +190,7 @@ internal fun generateDiffProducingWidget(schema: Schema, widget: Widget, host: S
                   FunSpec.builder(trait.name)
                     .addModifiers(OVERRIDE)
                     .addParameter(trait.name, traitTypeName)
-                    .addStatement("appendDiff(%T(this.id, %L, json.encodeToJsonElement(serializer_%L, %N)))", propertyDiff, trait.tag, serializerId, trait.name)
+                    .addStatement("appendDiff(%T(this.id, %LU, json.encodeToJsonElement(serializer_%L, %N)))", propertyDiff, trait.tag, serializerId, trait.name)
                     .build(),
                 )
               }
@@ -207,7 +207,7 @@ internal fun generateDiffProducingWidget(schema: Schema, widget: Widget, host: S
                     .addParameter(trait.name, trait.lambdaType)
                     .addStatement("val %1NSet = %1N != null", trait.name)
                     .beginControlFlow("if (%1NSet != (this.%1N != null))", trait.name)
-                    .addStatement("appendDiff(%T(this.id, %L, %M(%NSet)))", propertyDiff, trait.tag, JsonPrimitive, trait.name)
+                    .addStatement("appendDiff(%T(this.id, %LU, %M(%NSet)))", propertyDiff, trait.tag, JsonPrimitive, trait.name)
                     .endControlFlow()
                     .addStatement("this.%1N = %1N", trait.name)
                     .build(),
@@ -240,13 +240,13 @@ internal fun generateDiffProducingWidget(schema: Schema, widget: Widget, host: S
                       nextSerializerId++
                     }
                     addStatement(
-                      "%L -> %N?.invoke(json.decodeFromJsonElement(serializer_%L, event.value))",
+                      "%LU -> %N?.invoke(json.decodeFromJsonElement(serializer_%L, event.value))",
                       event.tag,
                       event.name,
                       serializerId,
                     )
                   } else {
-                    addStatement("%L -> %N?.invoke()", event.tag, event.name)
+                    addStatement("%LU -> %N?.invoke()", event.tag, event.name)
                   }
                 }
               }
diff --git a/redwood-protocol-compose/src/commonMain/kotlin/app/cash/redwood/protocol/compose/ProtocolApplier.kt b/redwood-protocol-compose/src/commonMain/kotlin/app/cash/redwood/protocol/compose/ProtocolApplier.kt
index 44879c9d0..eb22af0b9 100644
--- a/redwood-protocol-compose/src/commonMain/kotlin/app/cash/redwood/protocol/compose/ProtocolApplier.kt
+++ b/redwood-protocol-compose/src/commonMain/kotlin/app/cash/redwood/protocol/compose/ProtocolApplier.kt
@@ -45,7 +45,7 @@ import app.cash.redwood.protocol.PropertyDiff
  */
 @Composable
 @Suppress("FunctionName") // Hiding from auto-complete.
-public fun _SyntheticChildren(tag: Int, content: @Composable () -> Unit) {
+public fun _SyntheticChildren(tag: UInt, content: @Composable () -> Unit) {
   ComposeNode<DiffProducingChildrenWidget.Intermediate, Applier<*>>(
     factory = {
       DiffProducingChildrenWidget.Intermediate(tag)
@@ -62,9 +62,9 @@ public fun _SyntheticChildren(tag: Int, content: @Composable () -> Unit) {
  * creation of the protocol diffs. This is safe because these types never appears in the node map.
  */
 private sealed class DiffProducingChildrenWidget(
-  val tag: Int,
+  val tag: UInt,
 ) : AbstractDiffProducingWidget(-1) {
-  class Intermediate(tag: Int) : DiffProducingChildrenWidget(tag)
+  class Intermediate(tag: UInt) : DiffProducingChildrenWidget(tag)
   class Root : DiffProducingChildrenWidget(RootChildrenTag) {
     init {
       id = RootId
diff --git a/redwood-protocol-compose/src/commonMain/kotlin/app/cash/redwood/protocol/compose/ProtocolMismatchHandler.kt b/redwood-protocol-compose/src/commonMain/kotlin/app/cash/redwood/protocol/compose/ProtocolMismatchHandler.kt
index d8de4b40b..901eeacee 100644
--- a/redwood-protocol-compose/src/commonMain/kotlin/app/cash/redwood/protocol/compose/ProtocolMismatchHandler.kt
+++ b/redwood-protocol-compose/src/commonMain/kotlin/app/cash/redwood/protocol/compose/ProtocolMismatchHandler.kt
@@ -24,13 +24,13 @@ import kotlin.jvm.JvmField
  */
 public interface ProtocolMismatchHandler {
   /** Handle a request to process an unknown event [tag] for the specified widget [kind]. */
-  public fun onUnknownEvent(kind: Int, tag: Int)
+  public fun onUnknownEvent(kind: Int, tag: UInt)
 
   public companion object {
     /** A [ProtocolMismatchHandler] which throws [IllegalArgumentException] for all callbacks. */
     @JvmField
     public val Throwing: ProtocolMismatchHandler = object : ProtocolMismatchHandler {
-      override fun onUnknownEvent(kind: Int, tag: Int) {
+      override fun onUnknownEvent(kind: Int, tag: UInt) {
         throw IllegalArgumentException("Unknown event tag $tag for widget kind $kind")
       }
     }
diff --git a/redwood-protocol-widget/src/commonMain/kotlin/app/cash/redwood/protocol/widget/DiffConsumingWidget.kt b/redwood-protocol-widget/src/commonMain/kotlin/app/cash/redwood/protocol/widget/DiffConsumingWidget.kt
index 429afe70b..c7aa559b9 100644
--- a/redwood-protocol-widget/src/commonMain/kotlin/app/cash/redwood/protocol/widget/DiffConsumingWidget.kt
+++ b/redwood-protocol-widget/src/commonMain/kotlin/app/cash/redwood/protocol/widget/DiffConsumingWidget.kt
@@ -35,7 +35,7 @@ public interface DiffConsumingWidget<T : Any> : Widget<T> {
    * If `null` is returned, the caller should make every effort to ignore these children and
    * continue executing.
    */
-  public fun children(tag: Int): Widget.Children<T>?
+  public fun children(tag: UInt): Widget.Children<T>?
 
   public interface Factory<T : Any> {
     /**
diff --git a/redwood-protocol-widget/src/commonMain/kotlin/app/cash/redwood/protocol/widget/ProtocolDisplay.kt b/redwood-protocol-widget/src/commonMain/kotlin/app/cash/redwood/protocol/widget/ProtocolDisplay.kt
index bf77ee98a..707bbdbdf 100644
--- a/redwood-protocol-widget/src/commonMain/kotlin/app/cash/redwood/protocol/widget/ProtocolDisplay.kt
+++ b/redwood-protocol-widget/src/commonMain/kotlin/app/cash/redwood/protocol/widget/ProtocolDisplay.kt
@@ -95,7 +95,7 @@ private class ProtocolDisplayRoot<T : Any>(
     throw AssertionError("unexpected: $diff")
   }
 
-  override fun children(tag: Int) = when (tag) {
+  override fun children(tag: UInt) = when (tag) {
     RootChildrenTag -> children
     else -> throw AssertionError("unexpected: $tag")
   }
diff --git a/redwood-protocol-widget/src/commonMain/kotlin/app/cash/redwood/protocol/widget/ProtocolMismatchHandler.kt b/redwood-protocol-widget/src/commonMain/kotlin/app/cash/redwood/protocol/widget/ProtocolMismatchHandler.kt
index 62731bc2b..8aea82aa5 100644
--- a/redwood-protocol-widget/src/commonMain/kotlin/app/cash/redwood/protocol/widget/ProtocolMismatchHandler.kt
+++ b/redwood-protocol-widget/src/commonMain/kotlin/app/cash/redwood/protocol/widget/ProtocolMismatchHandler.kt
@@ -30,10 +30,10 @@ public interface ProtocolMismatchHandler {
   public fun onUnknownLayoutModifier(tag: Int)
 
   /** Handle a request to manipulate unknown children [tag] for the specified widget [kind]. */
-  public fun onUnknownChildren(kind: Int, tag: Int)
+  public fun onUnknownChildren(kind: Int, tag: UInt)
 
   /** Handle a request to set an unknown property [tag] for the specified widget [kind]. */
-  public fun onUnknownProperty(kind: Int, tag: Int)
+  public fun onUnknownProperty(kind: Int, tag: UInt)
 
   public companion object {
     /** A [ProtocolMismatchHandler] which throws [IllegalArgumentException] for all callbacks. */
@@ -47,11 +47,11 @@ public interface ProtocolMismatchHandler {
         throw IllegalArgumentException("Unknown layout modifier tag $tag")
       }
 
-      override fun onUnknownChildren(kind: Int, tag: Int) {
+      override fun onUnknownChildren(kind: Int, tag: UInt) {
         throw IllegalArgumentException("Unknown children tag $tag for widget kind $kind")
       }
 
-      override fun onUnknownProperty(kind: Int, tag: Int) {
+      override fun onUnknownProperty(kind: Int, tag: UInt) {
         throw IllegalArgumentException("Unknown property tag $tag for widget kind $kind")
       }
     }
diff --git a/redwood-protocol/src/commonMain/kotlin/app/cash/redwood/protocol/protocol.kt b/redwood-protocol/src/commonMain/kotlin/app/cash/redwood/protocol/protocol.kt
index b7abfdaac..44cb8d33f 100644
--- a/redwood-protocol/src/commonMain/kotlin/app/cash/redwood/protocol/protocol.kt
+++ b/redwood-protocol/src/commonMain/kotlin/app/cash/redwood/protocol/protocol.kt
@@ -34,7 +34,7 @@ public data class Event(
   /** Identifier for the widget from which this event originated. */
   val id: ULong,
   /** Identifies which event occurred on the widget with [id]. */
-  val tag: Int,
+  val tag: UInt,
   val value: JsonElement = JsonNull,
 )
 
@@ -50,7 +50,7 @@ public data class PropertyDiff(
   /** Identifier for the widget whose property has changed. */
   val id: ULong,
   /** Identifies which property changed on the widget with [id]. */
-  val tag: Int,
+  val tag: UInt,
   val value: JsonElement = JsonNull,
 )
 
@@ -71,20 +71,20 @@ public sealed class ChildrenDiff {
   public abstract val id: ULong
 
   /** Identifies which group of children changed on the widget with [id]. */
-  public abstract val tag: Int
+  public abstract val tag: UInt
 
   @Serializable
   @SerialName("clear")
   public object Clear : ChildrenDiff() {
     override val id: ULong get() = RootId
-    override val tag: Int get() = RootChildrenTag
+    override val tag: UInt get() = RootChildrenTag
   }
 
   @Serializable
   @SerialName("insert")
   public data class Insert(
     override val id: ULong,
-    override val tag: Int,
+    override val tag: UInt,
     val childId: ULong,
     val kind: Int,
     val index: Int,
@@ -94,7 +94,7 @@ public sealed class ChildrenDiff {
   @SerialName("move")
   public data class Move(
     override val id: ULong,
-    override val tag: Int,
+    override val tag: UInt,
     val fromIndex: Int,
     val toIndex: Int,
     val count: Int,
@@ -104,7 +104,7 @@ public sealed class ChildrenDiff {
   @SerialName("remove")
   public data class Remove(
     override val id: ULong,
-    override val tag: Int,
+    override val tag: UInt,
     val index: Int,
     val count: Int,
     val removedIds: List<ULong>,
@@ -118,6 +118,6 @@ public sealed class ChildrenDiff {
 
   public companion object {
     public const val RootId: ULong = 0UL
-    public const val RootChildrenTag: Int = 1
+    public const val RootChildrenTag: UInt = 1U
   }
 }
diff --git a/redwood-schema/src/main/kotlin/app/cash/redwood/schema/parser/schema.kt b/redwood-schema/src/main/kotlin/app/cash/redwood/schema/parser/schema.kt
index e95bfdebe..671f79f30 100644
--- a/redwood-schema/src/main/kotlin/app/cash/redwood/schema/parser/schema.kt
+++ b/redwood-schema/src/main/kotlin/app/cash/redwood/schema/parser/schema.kt
@@ -28,34 +28,34 @@ public data class Schema(
 )
 
 public data class Widget(
-  val tag: Int,
+  val tag: UInt,
   /** Either a 'data class' or 'object'. */
   val type: KClass<*>,
   /** Non-empty list for a 'data class' [type] or empty list for 'object' [type]. */
   val traits: List<Trait>,
 ) {
   public sealed class Trait {
-    public abstract val tag: Int
+    public abstract val tag: UInt
     public abstract val name: String
     public abstract val defaultExpression: String?
   }
 
   public data class Property(
-    override val tag: Int,
+    override val tag: UInt,
     override val name: String,
     val type: KType,
     override val defaultExpression: String?,
   ) : Trait()
 
   public data class Event(
-    override val tag: Int,
+    override val tag: UInt,
     override val name: String,
     override val defaultExpression: String?,
     val parameterType: KType?,
   ) : Trait()
 
   public data class Children(
-    override val tag: Int,
+    override val tag: UInt,
     override val name: String,
     val scope: KClass<*>? = null,
   ) : Trait() {
@@ -64,7 +64,7 @@ public data class Widget(
 }
 
 public data class LayoutModifier(
-  val tag: Int,
+  val tag: UInt,
   val scopes: List<KClass<*>>,
   /** Either a 'data class' or 'object'. */
   val type: KClass<*>,
diff --git a/redwood-schema/src/main/kotlin/app/cash/redwood/schema/parser/schemaParser.kt b/redwood-schema/src/main/kotlin/app/cash/redwood/schema/parser/schemaParser.kt
index 5c69afcb8..d8a7d76b8 100644
--- a/redwood-schema/src/main/kotlin/app/cash/redwood/schema/parser/schemaParser.kt
+++ b/redwood-schema/src/main/kotlin/app/cash/redwood/schema/parser/schemaParser.kt
@@ -32,16 +32,16 @@ private val childrenType = Function::class.starProjectedType
 private val eventType = Function::class.starProjectedType
 private val optionalEventType = eventType.withNullability(true)
 
-private const val maxSchemaTag = 2_000
-private const val maxMemberTag = 1_000_000
+private const val maxSchemaTag = 4_000U
+private const val maxMemberTag = 1_000_000U
 
 private val KClass<*>.schemaAnnotation: SchemaAnnotation get() {
   return requireNotNull(findAnnotation()) { "Schema $qualifiedName missing @Schema annotation" }
 }
 
-public fun parseSchema(schemaType: KClass<*>, tag: Int = 0): Schema {
+public fun parseSchema(schemaType: KClass<*>, tag: UInt = 0U): Schema {
   val schemaAnnotation = schemaType.schemaAnnotation
-  require(tag in 0..maxSchemaTag) { "Schema tag must be in range [0, $maxSchemaTag]: $tag" }
+  require(tag in 0U..maxSchemaTag) { "Schema tag must be in range [0, $maxSchemaTag]: $tag" }
 
   val memberTypes = schemaAnnotation.members
 
@@ -139,10 +139,10 @@ public fun parseSchema(schemaType: KClass<*>, tag: Int = 0): Schema {
 
   val dependencies = schemaAnnotation.dependencies
     .map {
-      require(it.tag in 1..maxSchemaTag) {
+      require(it.tag in 1..maxSchemaTag.toInt()) {
         "Dependency ${it.schema.qualifiedName} tag must be in range (0, $maxSchemaTag]: ${it.tag}"
       }
-      val schema = parseSchema(it.schema, it.tag)
+      val schema = parseSchema(it.schema, it.tag.toUIntExact())
       require(schema.dependencies.isEmpty()) {
         "Schema dependency ${it.schema.qualifiedName} also has its own dependencies. " +
           "For now, only a single level of dependencies is supported."
@@ -180,14 +180,14 @@ public fun parseSchema(schemaType: KClass<*>, tag: Int = 0): Schema {
 }
 
 private fun parseWidget(
-  schemaTag: Int,
+  schemaTag: UInt,
   memberType: KClass<*>,
   annotation: WidgetAnnotation,
 ): Widget {
-  require(annotation.tag in 1 until maxMemberTag) {
+  require(annotation.tag in 1 until maxMemberTag.toInt()) {
     "@Widget ${memberType.qualifiedName} tag must be in range [1, $maxMemberTag): ${annotation.tag}"
   }
-  val tag = schemaTag * maxMemberTag + annotation.tag
+  val tag = schemaTag * maxMemberTag + annotation.tag.toUIntExact()
 
   val traits = if (memberType.isData) {
     memberType.primaryConstructor!!.parameters.map {
@@ -201,9 +201,9 @@ private fun parseWidget(
           require(arguments.size <= 1) {
             "@Property ${memberType.qualifiedName}#${it.name} lambda type can only have zero or one arguments. Found: $arguments"
           }
-          Widget.Event(property.tag, it.name!!, defaultExpression, arguments.singleOrNull()?.type)
+          Widget.Event(property.tag.toUIntExact(), it.name!!, defaultExpression, arguments.singleOrNull()?.type)
         } else {
-          Widget.Property(property.tag, it.name!!, it.type, defaultExpression)
+          Widget.Property(property.tag.toUIntExact(), it.name!!, it.type, defaultExpression)
         }
       } else if (children != null) {
         require(it.type.isSubtypeOf(childrenType)) {
@@ -223,7 +223,7 @@ private fun parseWidget(
         require(arguments.isEmpty()) {
           "@Children ${memberType.qualifiedName}#${it.name} lambda type must not have any arguments. Found: $arguments"
         }
-        Widget.Children(children.tag, it.name!!, scope)
+        Widget.Children(children.tag.toUIntExact(), it.name!!, scope)
       } else {
         throw IllegalArgumentException("Unannotated parameter \"${it.name}\" on ${memberType.qualifiedName}")
       }
@@ -270,14 +270,14 @@ private fun parseWidget(
 }
 
 private fun parseLayoutModifier(
-  schemaTag: Int,
+  schemaTag: UInt,
   memberType: KClass<*>,
   annotation: LayoutModifierAnnotation,
 ): LayoutModifier {
-  require(annotation.tag in 1 until maxMemberTag) {
+  require(annotation.tag in 1 until maxMemberTag.toInt()) {
     "@LayoutModifier ${memberType.qualifiedName} tag must be in range [1, $maxMemberTag): ${annotation.tag}"
   }
-  val tag = schemaTag * maxMemberTag + annotation.tag
+  val tag = schemaTag * maxMemberTag + annotation.tag.toUIntExact()
 
   val properties = if (memberType.isData) {
     memberType.primaryConstructor!!.parameters.map {
diff --git a/redwood-schema/src/main/kotlin/app/cash/redwood/schema/parser/utils.kt b/redwood-schema/src/main/kotlin/app/cash/redwood/schema/parser/utils.kt
new file mode 100644
index 000000000..c1f2dfe13
--- /dev/null
+++ b/redwood-schema/src/main/kotlin/app/cash/redwood/schema/parser/utils.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2022 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package app.cash.redwood.schema.parser
+
+// https://youtrack.jetbrains.com/issue/KT-54465
+internal fun Int.toUIntExact(): UInt {
+  if (this < 0) {
+    throw ArithmeticException()
+  }
+  return toUInt()
+}


Test patch:
diff --git a/redwood-cli/src/test/kotlin/app/cash/redwood/generator/DiffProducingGenerationTest.kt b/redwood-cli/src/test/kotlin/app/cash/redwood/generator/DiffProducingGenerationTest.kt
index 77fc682b7..c71b678f5 100644
--- a/redwood-cli/src/test/kotlin/app/cash/redwood/generator/DiffProducingGenerationTest.kt
+++ b/redwood-cli/src/test/kotlin/app/cash/redwood/generator/DiffProducingGenerationTest.kt
@@ -43,7 +43,7 @@ class DiffProducingGenerationTest {
     assertThat(fileSpec.toString()).contains(
       """
       |  public override fun id(id: String): Unit {
-      |    appendDiff(PropertyDiff(this.id, 2, json.encodeToJsonElement(serializer_0, id)))
+      |    appendDiff(PropertyDiff(this.id, 2U, json.encodeToJsonElement(serializer_0, id)))
       |  }
       |
       """.trimMargin(),
diff --git a/redwood-protocol-compose/src/commonTest/kotlin/app/cash/redwood/protocol/compose/DiffProducingWidgetFactoryTest.kt b/redwood-protocol-compose/src/commonTest/kotlin/app/cash/redwood/protocol/compose/DiffProducingWidgetFactoryTest.kt
index 530c6b1ab..43625c8f8 100644
--- a/redwood-protocol-compose/src/commonTest/kotlin/app/cash/redwood/protocol/compose/DiffProducingWidgetFactoryTest.kt
+++ b/redwood-protocol-compose/src/commonTest/kotlin/app/cash/redwood/protocol/compose/DiffProducingWidgetFactoryTest.kt
@@ -55,7 +55,7 @@ class DiffProducingWidgetFactoryTest {
 
     val expected = Diff(
       propertyDiffs = listOf(
-        PropertyDiff(1U, 2, JsonPrimitive("PT10S")),
+        PropertyDiff(1U, 2U, JsonPrimitive("PT10S")),
       ),
     )
     assertEquals(expected, diffSink.diffs.single())
@@ -158,7 +158,7 @@ class DiffProducingWidgetFactoryTest {
       argument = it
     }
 
-    diffProducingWidget.sendEvent(Event(1U, 4, JsonPrimitive("PT10S")))
+    diffProducingWidget.sendEvent(Event(1U, 4U, JsonPrimitive("PT10S")))
 
     assertEquals(10.seconds, argument)
   }
@@ -167,7 +167,7 @@ class DiffProducingWidgetFactoryTest {
     val factory = DiffProducingExampleSchemaWidgetFactory()
     val button = factory.Button() as AbstractDiffProducingWidget
 
-    val event = Event(1U, 3456543)
+    val event = Event(1U, 3456543U)
     val t = assertFailsWith<IllegalArgumentException> {
       button.sendEvent(event)
     }
@@ -180,7 +180,7 @@ class DiffProducingWidgetFactoryTest {
     val factory = DiffProducingExampleSchemaWidgetFactory(mismatchHandler = handler)
     val button = factory.Button() as AbstractDiffProducingWidget
 
-    button.sendEvent(Event(1U, 3456543))
+    button.sendEvent(Event(1U, 3456543U))
 
     assertEquals("Unknown event 3456543 for 4", handler.events.single())
   }
diff --git a/redwood-protocol-compose/src/commonTest/kotlin/app/cash/redwood/protocol/compose/ProtocolTest.kt b/redwood-protocol-compose/src/commonTest/kotlin/app/cash/redwood/protocol/compose/ProtocolTest.kt
index 6a202d3a6..2903cc87b 100644
--- a/redwood-protocol-compose/src/commonTest/kotlin/app/cash/redwood/protocol/compose/ProtocolTest.kt
+++ b/redwood-protocol-compose/src/commonTest/kotlin/app/cash/redwood/protocol/compose/ProtocolTest.kt
@@ -85,9 +85,9 @@ class ProtocolTest {
       Diff(
         childrenDiffs = listOf(
           ChildrenDiff.Insert(RootId, RootChildrenTag, 1U, 1 /* row */, 0),
-          ChildrenDiff.Insert(1U, 1, 2U, 3 /* text */, 0),
-          ChildrenDiff.Insert(1U, 1, 3U, 1 /* row */, 1),
-          ChildrenDiff.Insert(3U, 1, 4U, 3 /* text */, 0),
+          ChildrenDiff.Insert(1U, 1U, 2U, 3 /* text */, 0),
+          ChildrenDiff.Insert(1U, 1U, 3U, 1 /* row */, 1),
+          ChildrenDiff.Insert(3U, 1U, 4U, 3 /* text */, 0),
         ),
         layoutModifiers = listOf(
           LayoutModifiers(1U, JsonArray(listOf())),
@@ -96,8 +96,8 @@ class ProtocolTest {
           LayoutModifiers(4U, JsonArray(listOf())),
         ),
         propertyDiffs = listOf(
-          PropertyDiff(2U, 1 /* text */, JsonPrimitive("hey")),
-          PropertyDiff(4U, 1 /* text */, JsonPrimitive("hello")),
+          PropertyDiff(2U, 1U /* text */, JsonPrimitive("hey")),
+          PropertyDiff(4U, 1U /* text */, JsonPrimitive("hello")),
         ),
       ),
       diffs.removeFirst(),
@@ -140,37 +140,37 @@ class ProtocolTest {
           LayoutModifiers(1U, JsonArray(listOf())),
         ),
         propertyDiffs = listOf(
-          PropertyDiff(1U, 1 /* text */, JsonPrimitive("state: 0")),
-          PropertyDiff(1U, 2 /* onClick */, JsonPrimitive(true)),
+          PropertyDiff(1U, 1U /* text */, JsonPrimitive("state: 0")),
+          PropertyDiff(1U, 2U /* onClick */, JsonPrimitive(true)),
         ),
       ),
       diffs.removeFirst(),
     )
 
     // Invoke the onClick lambda to move the state from 0 to 1.
-    composition.sendEvent(Event(1U, 2))
+    composition.sendEvent(Event(1U, 2U))
     yield() // Allow state change to be handled.
 
     clock.awaitFrame()
     assertEquals(
       Diff(
         propertyDiffs = listOf(
-          PropertyDiff(1U, 1 /* text */, JsonPrimitive("state: 1")),
+          PropertyDiff(1U, 1U /* text */, JsonPrimitive("state: 1")),
         ),
       ),
       diffs.removeFirst(),
     )
 
     // Invoke the onClick lambda to move the state from 1 to 2.
-    composition.sendEvent(Event(1U, 2))
+    composition.sendEvent(Event(1U, 2U))
     yield() // Allow state change to be handled.
 
     clock.awaitFrame()
     assertEquals(
       Diff(
         propertyDiffs = listOf(
-          PropertyDiff(1U, 1 /* text */, JsonPrimitive("state: 2")),
-          PropertyDiff(1U, 2 /* text */, JsonPrimitive(false)),
+          PropertyDiff(1U, 1U /* text */, JsonPrimitive("state: 2")),
+          PropertyDiff(1U, 2U /* text */, JsonPrimitive(false)),
         ),
       ),
       diffs.removeFirst(),
@@ -184,7 +184,7 @@ class ProtocolTest {
     assertEquals(
       Diff(
         propertyDiffs = listOf(
-          PropertyDiff(1U, 1 /* text */, JsonPrimitive("state: 3")),
+          PropertyDiff(1U, 1U /* text */, JsonPrimitive("state: 3")),
         ),
       ),
       diffs.removeFirst(),
diff --git a/redwood-protocol-compose/src/commonTest/kotlin/app/cash/redwood/protocol/compose/RecordingProtocolMismatchHandler.kt b/redwood-protocol-compose/src/commonTest/kotlin/app/cash/redwood/protocol/compose/RecordingProtocolMismatchHandler.kt
index 834a044c8..8740c5ecc 100644
--- a/redwood-protocol-compose/src/commonTest/kotlin/app/cash/redwood/protocol/compose/RecordingProtocolMismatchHandler.kt
+++ b/redwood-protocol-compose/src/commonTest/kotlin/app/cash/redwood/protocol/compose/RecordingProtocolMismatchHandler.kt
@@ -18,7 +18,7 @@ package app.cash.redwood.protocol.compose
 class RecordingProtocolMismatchHandler : ProtocolMismatchHandler {
   val events = mutableListOf<String>()
 
-  override fun onUnknownEvent(kind: Int, tag: Int) {
+  override fun onUnknownEvent(kind: Int, tag: UInt) {
     events += "Unknown event $tag for $kind"
   }
 }
diff --git a/redwood-protocol-widget/src/commonTest/kotlin/app/cash/redwood/protocol/widget/DiffConsumingWidgetFactoryTest.kt b/redwood-protocol-widget/src/commonTest/kotlin/app/cash/redwood/protocol/widget/DiffConsumingWidgetFactoryTest.kt
index dbd0d5dc2..fe2dd9467 100644
--- a/redwood-protocol-widget/src/commonTest/kotlin/app/cash/redwood/protocol/widget/DiffConsumingWidgetFactoryTest.kt
+++ b/redwood-protocol-widget/src/commonTest/kotlin/app/cash/redwood/protocol/widget/DiffConsumingWidgetFactoryTest.kt
@@ -197,7 +197,7 @@ class DiffConsumingWidgetFactoryTest {
     val button = factory.create(4)!!
 
     val t = assertFailsWith<IllegalArgumentException> {
-      button.children(345432)
+      button.children(345432U)
     }
     assertEquals("Unknown children tag 345432 for widget kind 4", t.message)
   }
@@ -210,7 +210,7 @@ class DiffConsumingWidgetFactoryTest {
     )
 
     val button = factory.create(4)!!
-    assertNull(button.children(345432))
+    assertNull(button.children(345432U))
 
     assertEquals("Unknown children 345432 for 4", handler.events.single())
   }
@@ -231,7 +231,7 @@ class DiffConsumingWidgetFactoryTest {
     val textInput = factory.create(5)!!
 
     val throwingEventSink = EventSink { error(it) }
-    textInput.apply(PropertyDiff(1U, 2, JsonPrimitive("PT10S")), throwingEventSink)
+    textInput.apply(PropertyDiff(1U, 2U, JsonPrimitive("PT10S")), throwingEventSink)
 
     assertEquals(10.seconds, recordingTextInput.customType)
   }
@@ -240,7 +240,7 @@ class DiffConsumingWidgetFactoryTest {
     val factory = DiffConsumingExampleSchemaWidgetFactory(EmptyExampleSchemaWidgetFactory())
     val button = factory.create(4) as DiffConsumingWidget<*>
 
-    val diff = PropertyDiff(1U, 345432)
+    val diff = PropertyDiff(1U, 345432U)
     val eventSink = EventSink { throw UnsupportedOperationException() }
     val t = assertFailsWith<IllegalArgumentException> {
       button.apply(diff, eventSink)
@@ -256,7 +256,7 @@ class DiffConsumingWidgetFactoryTest {
     )
     val button = factory.create(4) as DiffConsumingWidget<*>
 
-    button.apply(PropertyDiff(1U, 345432)) { throw UnsupportedOperationException() }
+    button.apply(PropertyDiff(1U, 345432U)) { throw UnsupportedOperationException() }
 
     assertEquals("Unknown property 345432 for 4", handler.events.single())
   }
@@ -277,11 +277,11 @@ class DiffConsumingWidgetFactoryTest {
     val textInput = factory.create(5)!!
 
     val eventSink = RecordingEventSink()
-    textInput.apply(PropertyDiff(1U, 4, JsonPrimitive(true)), eventSink)
+    textInput.apply(PropertyDiff(1U, 4U, JsonPrimitive(true)), eventSink)
 
     recordingTextInput.onChangeCustomType!!.invoke(10.seconds)
 
-    assertEquals(Event(1U, 4, JsonPrimitive("PT10S")), eventSink.events.single())
+    assertEquals(Event(1U, 4U, JsonPrimitive("PT10S")), eventSink.events.single())
   }
 
   open class EmptyExampleSchemaWidgetFactory : ExampleSchemaWidgetFactory<Nothing> {
diff --git a/redwood-protocol-widget/src/commonTest/kotlin/app/cash/redwood/protocol/widget/RecordingProtocolMismatchHandler.kt b/redwood-protocol-widget/src/commonTest/kotlin/app/cash/redwood/protocol/widget/RecordingProtocolMismatchHandler.kt
index 8fce77db0..c83211ca9 100644
--- a/redwood-protocol-widget/src/commonTest/kotlin/app/cash/redwood/protocol/widget/RecordingProtocolMismatchHandler.kt
+++ b/redwood-protocol-widget/src/commonTest/kotlin/app/cash/redwood/protocol/widget/RecordingProtocolMismatchHandler.kt
@@ -26,11 +26,11 @@ class RecordingProtocolMismatchHandler : ProtocolMismatchHandler {
     events += "Unknown layout modifier $tag"
   }
 
-  override fun onUnknownChildren(kind: Int, tag: Int) {
+  override fun onUnknownChildren(kind: Int, tag: UInt) {
     events += "Unknown children $tag for $kind"
   }
 
-  override fun onUnknownProperty(kind: Int, tag: Int) {
+  override fun onUnknownProperty(kind: Int, tag: UInt) {
     events += "Unknown property $tag for $kind"
   }
 }
diff --git a/redwood-protocol/src/commonTest/kotlin/app/cash/redwood/protocol/ProtocolTest.kt b/redwood-protocol/src/commonTest/kotlin/app/cash/redwood/protocol/ProtocolTest.kt
index 5382d8bba..84d1b2d4e 100644
--- a/redwood-protocol/src/commonTest/kotlin/app/cash/redwood/protocol/ProtocolTest.kt
+++ b/redwood-protocol/src/commonTest/kotlin/app/cash/redwood/protocol/ProtocolTest.kt
@@ -31,13 +31,13 @@ class ProtocolTest {
   }
 
   @Test fun eventNonNullValue() {
-    val model = Event(1U, 2, JsonPrimitive("Hello"))
+    val model = Event(1U, 2U, JsonPrimitive("Hello"))
     val json = """{"id":1,"tag":2,"value":"Hello"}"""
     assertJsonRoundtrip(Event.serializer(), model, json)
   }
 
   @Test fun eventNullValue() {
-    val model = Event(1U, 2)
+    val model = Event(1U, 2U)
     val json = """{"id":1,"tag":2}"""
     assertJsonRoundtrip(Event.serializer(), model, json)
   }
@@ -46,9 +46,9 @@ class ProtocolTest {
     val model = Diff(
       childrenDiffs = listOf(
         ChildrenDiff.Clear,
-        ChildrenDiff.Insert(1U, 2, 3U, 4, 5),
-        ChildrenDiff.Move(1U, 2, 3, 4, 5),
-        ChildrenDiff.Remove(1U, 2, 3, 4, listOf(5U, 6U, 7U, 8U)),
+        ChildrenDiff.Insert(1U, 2U, 3U, 4, 5),
+        ChildrenDiff.Move(1U, 2U, 3, 4, 5),
+        ChildrenDiff.Remove(1U, 2U, 3, 4, listOf(5U, 6U, 7U, 8U)),
       ),
       layoutModifiers = listOf(
         LayoutModifiers(
@@ -64,8 +64,8 @@ class ProtocolTest {
         ),
       ),
       propertyDiffs = listOf(
-        PropertyDiff(1U, 2, JsonPrimitive("Hello")),
-        PropertyDiff(1U, 2, JsonNull),
+        PropertyDiff(1U, 2U, JsonPrimitive("Hello")),
+        PropertyDiff(1U, 2U, JsonNull),
       ),
     )
     val json = "" +
@@ -95,7 +95,7 @@ class ProtocolTest {
 
   @Test fun removeCountMustMatchListSize() {
     assertFailsWith<IllegalArgumentException>("Count 4 != Removed ID list size 3") {
-      ChildrenDiff.Remove(1U, 2, 3, 4, listOf(5U, 6U, 7U))
+      ChildrenDiff.Remove(1U, 2U, 3, 4, listOf(5U, 6U, 7U))
     }
   }
 
diff --git a/redwood-schema/src/test/kotlin/app/cash/redwood/schema/parser/SchemaParserTest.kt b/redwood-schema/src/test/kotlin/app/cash/redwood/schema/parser/SchemaParserTest.kt
index b0a80eaba..250d1bc6f 100644
--- a/redwood-schema/src/test/kotlin/app/cash/redwood/schema/parser/SchemaParserTest.kt
+++ b/redwood-schema/src/test/kotlin/app/cash/redwood/schema/parser/SchemaParserTest.kt
@@ -572,12 +572,12 @@ class SchemaParserTest {
     val schema = parseSchema(SchemaTag::class)
 
     val widget = schema.widgets.single()
-    assertThat(widget.tag).isEqualTo(1)
-    assertThat(widget.traits[0].tag).isEqualTo(1)
-    assertThat(widget.traits[1].tag).isEqualTo(1)
+    assertThat(widget.tag).isEqualTo(1U)
+    assertThat(widget.traits[0].tag).isEqualTo(1U)
+    assertThat(widget.traits[1].tag).isEqualTo(1U)
 
     val layoutModifier = schema.layoutModifiers.single()
-    assertThat(layoutModifier.tag).isEqualTo(1)
+    assertThat(layoutModifier.tag).isEqualTo(1U)
   }
 
   @Schema(
@@ -593,28 +593,24 @@ class SchemaParserTest {
     val dependency = schema.dependencies.single()
 
     val widget = dependency.widgets.single()
-    assertThat(widget.tag).isEqualTo(4_000_001)
-    assertThat(widget.traits[0].tag).isEqualTo(1)
-    assertThat(widget.traits[1].tag).isEqualTo(1)
+    assertThat(widget.tag).isEqualTo(4_000_001U)
+    assertThat(widget.traits[0].tag).isEqualTo(1U)
+    assertThat(widget.traits[1].tag).isEqualTo(1U)
 
     val layoutModifier = dependency.layoutModifiers.single()
-    assertThat(layoutModifier.tag).isEqualTo(4_000_001)
+    assertThat(layoutModifier.tag).isEqualTo(4_000_001U)
   }
 
   @Test fun schemaTagOutOfRangeThrows() {
     assertThrows<IllegalArgumentException> {
-      parseSchema(SchemaTag::class, tag = 2001)
-    }.hasMessageThat().isEqualTo("Schema tag must be in range [0, 2000]: 2001")
-
-    assertThrows<IllegalArgumentException> {
-      parseSchema(SchemaTag::class, tag = -1)
-    }.hasMessageThat().isEqualTo("Schema tag must be in range [0, 2000]: -1")
+      parseSchema(SchemaTag::class, tag = 4001U)
+    }.hasMessageThat().isEqualTo("Schema tag must be in range [0, 4000]: 4001")
   }
 
   @Schema(
     members = [],
     dependencies = [
-      Dependency(2001, SchemaTag::class),
+      Dependency(4001, SchemaTag::class),
     ],
   )
   object SchemaDependencyTagTooHigh
@@ -631,13 +627,13 @@ class SchemaParserTest {
     assertThrows<IllegalArgumentException> {
       parseSchema(SchemaDependencyTagTooHigh::class)
     }.hasMessageThat().isEqualTo(
-      "Dependency app.cash.redwood.schema.parser.SchemaParserTest.SchemaTag tag must be in range (0, 2000]: 2001",
+      "Dependency app.cash.redwood.schema.parser.SchemaParserTest.SchemaTag tag must be in range (0, 4000]: 4001",
     )
 
     assertThrows<IllegalArgumentException> {
       parseSchema(SchemaDependencyTagTooLow::class)
     }.hasMessageThat().isEqualTo(
-      "Dependency app.cash.redwood.schema.parser.SchemaParserTest.SchemaTag tag must be in range (0, 2000]: 0",
+      "Dependency app.cash.redwood.schema.parser.SchemaParserTest.SchemaTag tag must be in range (0, 4000]: 0",
     )
   }