From 53417eb5c9f0f0c0aeeef628865becc9b46dcf7c Mon Sep 17 00:00:00 2001 From: Jesse Wilson Date: Wed, 21 Aug 2024 10:53:15 -0400 Subject: [PATCH 1/8] Optimize Modifier serialization in guest code I learned that encoding a type as a JsonElement and then encoding that as JSON is significantly less efficient than going directly from the type to JSON. This is due to some inefficient code in the internals of kotlinx.serialization. Skipping this intermediate step is useful on its own, but it also turns out to be significantly more efficient in practice. --- CHANGELOG.md | 1 + .../api/redwood-protocol-guest.api | 6 +- .../api/redwood-protocol-guest.klib.api | 6 +- .../guest/DefaultGuestProtocolAdapter.kt | 21 ++- .../protocol/guest/GuestProtocolAdapter.kt | 4 +- .../guest/ProtocolWidgetSystemFactory.kt | 11 ++ .../tooling/codegen/protocolCodegen.kt | 1 - .../codegen/protocolGuestGeneration.kt | 149 ++++++++++-------- .../redwood/treehouse/ProtocolBridgeJs.kt | 22 ++- .../treehouse/FastGuestProtocolAdapterTest.kt | 16 ++ 10 files changed, 155 insertions(+), 82 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a2b2b1023..3fba30de6e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ New: - Source-based schema parser is now the default. The `useFir` Gradle property has been removed. - Introduce a `LoadingStrategy` interface to manage `LazyList` preloading. +- Optimize encoding modifiers in Kotlin/JS. Changed: - In Treehouse, events from the UI are now serialized on a background thread. This means that there is both a delay and a thread change between when a UI binding sends an event and when that object is converted to JSON. All arguments to events must not be mutable and support property reads on any thread. Best practice is for all event arguments to be completely immutable. diff --git a/redwood-protocol-guest/api/redwood-protocol-guest.api b/redwood-protocol-guest/api/redwood-protocol-guest.api index 45d7c04a9c..87f08c83cc 100644 --- a/redwood-protocol-guest/api/redwood-protocol-guest.api +++ b/redwood-protocol-guest/api/redwood-protocol-guest.api @@ -4,7 +4,7 @@ public final class app/cash/redwood/protocol/guest/DefaultGuestProtocolAdapter : public synthetic fun (Lkotlinx/serialization/json/Json;Ljava/lang/String;Lapp/cash/redwood/protocol/guest/ProtocolWidgetSystemFactory;Lapp/cash/redwood/protocol/guest/ProtocolMismatchHandler;Lkotlin/jvm/internal/DefaultConstructorMarker;)V public fun appendAdd-ARs5Qwk (IIILapp/cash/redwood/protocol/guest/ProtocolWidget;)V public fun appendCreate-kyz2zXs (II)V - public fun appendModifierChange-z3jyS0k (ILjava/util/List;)V + public fun appendModifierChange-z3jyS0k (ILapp/cash/redwood/Modifier;)V public fun appendMove-HpxY78w (IIIII)V public fun appendPropertyChange-DxQz5cw (IILkotlinx/serialization/KSerializer;Ljava/lang/Object;)V public fun appendPropertyChange-M7EZMwg (III)V @@ -25,7 +25,7 @@ public final class app/cash/redwood/protocol/guest/DefaultGuestProtocolAdapter : public abstract interface class app/cash/redwood/protocol/guest/GuestProtocolAdapter : app/cash/redwood/protocol/EventSink { public abstract fun appendAdd-ARs5Qwk (IIILapp/cash/redwood/protocol/guest/ProtocolWidget;)V public abstract fun appendCreate-kyz2zXs (II)V - public abstract fun appendModifierChange-z3jyS0k (ILjava/util/List;)V + public abstract fun appendModifierChange-z3jyS0k (ILapp/cash/redwood/Modifier;)V public abstract fun appendMove-HpxY78w (IIIII)V public abstract fun appendPropertyChange-DxQz5cw (IILkotlinx/serialization/KSerializer;Ljava/lang/Object;)V public abstract fun appendPropertyChange-M7EZMwg (III)V @@ -81,6 +81,8 @@ public final class app/cash/redwood/protocol/guest/ProtocolWidgetChildren : app/ public abstract interface class app/cash/redwood/protocol/guest/ProtocolWidgetSystemFactory { public abstract fun create (Lapp/cash/redwood/protocol/guest/GuestProtocolAdapter;Lapp/cash/redwood/protocol/guest/ProtocolMismatchHandler;)Lapp/cash/redwood/widget/WidgetSystem; public static synthetic fun create$default (Lapp/cash/redwood/protocol/guest/ProtocolWidgetSystemFactory;Lapp/cash/redwood/protocol/guest/GuestProtocolAdapter;Lapp/cash/redwood/protocol/guest/ProtocolMismatchHandler;ILjava/lang/Object;)Lapp/cash/redwood/widget/WidgetSystem; + public abstract fun modifierSerializer (Lapp/cash/redwood/Modifier$Element;)Lkotlinx/serialization/KSerializer; + public abstract fun modifierTag-54iDFIs (Lapp/cash/redwood/Modifier$Element;)I } public final class app/cash/redwood/protocol/guest/VersionKt { diff --git a/redwood-protocol-guest/api/redwood-protocol-guest.klib.api b/redwood-protocol-guest/api/redwood-protocol-guest.klib.api index 180abbd8b8..db4f78588b 100644 --- a/redwood-protocol-guest/api/redwood-protocol-guest.klib.api +++ b/redwood-protocol-guest/api/redwood-protocol-guest.klib.api @@ -19,7 +19,7 @@ abstract interface app.cash.redwood.protocol.guest/GuestProtocolAdapter : app.ca abstract fun <#A1: kotlin/Any?> appendPropertyChange(app.cash.redwood.protocol/Id, app.cash.redwood.protocol/PropertyTag, kotlinx.serialization/KSerializer<#A1>, #A1) // app.cash.redwood.protocol.guest/GuestProtocolAdapter.appendPropertyChange|appendPropertyChange(app.cash.redwood.protocol.Id;app.cash.redwood.protocol.PropertyTag;kotlinx.serialization.KSerializer<0:0>;0:0){0§}[0] abstract fun appendAdd(app.cash.redwood.protocol/Id, app.cash.redwood.protocol/ChildrenTag, kotlin/Int, app.cash.redwood.protocol.guest/ProtocolWidget) // app.cash.redwood.protocol.guest/GuestProtocolAdapter.appendAdd|appendAdd(app.cash.redwood.protocol.Id;app.cash.redwood.protocol.ChildrenTag;kotlin.Int;app.cash.redwood.protocol.guest.ProtocolWidget){}[0] abstract fun appendCreate(app.cash.redwood.protocol/Id, app.cash.redwood.protocol/WidgetTag) // app.cash.redwood.protocol.guest/GuestProtocolAdapter.appendCreate|appendCreate(app.cash.redwood.protocol.Id;app.cash.redwood.protocol.WidgetTag){}[0] - abstract fun appendModifierChange(app.cash.redwood.protocol/Id, kotlin.collections/List) // app.cash.redwood.protocol.guest/GuestProtocolAdapter.appendModifierChange|appendModifierChange(app.cash.redwood.protocol.Id;kotlin.collections.List){}[0] + abstract fun appendModifierChange(app.cash.redwood.protocol/Id, app.cash.redwood/Modifier) // app.cash.redwood.protocol.guest/GuestProtocolAdapter.appendModifierChange|appendModifierChange(app.cash.redwood.protocol.Id;app.cash.redwood.Modifier){}[0] abstract fun appendMove(app.cash.redwood.protocol/Id, app.cash.redwood.protocol/ChildrenTag, kotlin/Int, kotlin/Int, kotlin/Int) // app.cash.redwood.protocol.guest/GuestProtocolAdapter.appendMove|appendMove(app.cash.redwood.protocol.Id;app.cash.redwood.protocol.ChildrenTag;kotlin.Int;kotlin.Int;kotlin.Int){}[0] abstract fun appendPropertyChange(app.cash.redwood.protocol/Id, app.cash.redwood.protocol/PropertyTag, kotlin/Boolean) // app.cash.redwood.protocol.guest/GuestProtocolAdapter.appendPropertyChange|appendPropertyChange(app.cash.redwood.protocol.Id;app.cash.redwood.protocol.PropertyTag;kotlin.Boolean){}[0] abstract fun appendPropertyChange(app.cash.redwood.protocol/Id, app.cash.redwood.protocol/PropertyTag, kotlin/UInt) // app.cash.redwood.protocol.guest/GuestProtocolAdapter.appendPropertyChange|appendPropertyChange(app.cash.redwood.protocol.Id;app.cash.redwood.protocol.PropertyTag;kotlin.UInt){}[0] @@ -53,7 +53,9 @@ abstract interface app.cash.redwood.protocol.guest/ProtocolWidget : app.cash.red } abstract interface app.cash.redwood.protocol.guest/ProtocolWidgetSystemFactory { // app.cash.redwood.protocol.guest/ProtocolWidgetSystemFactory|null[0] + abstract fun <#A1: app.cash.redwood/Modifier.Element> modifierSerializer(#A1): kotlinx.serialization/KSerializer<#A1>? // app.cash.redwood.protocol.guest/ProtocolWidgetSystemFactory.modifierSerializer|modifierSerializer(0:0){0§}[0] abstract fun create(app.cash.redwood.protocol.guest/GuestProtocolAdapter, app.cash.redwood.protocol.guest/ProtocolMismatchHandler = ...): app.cash.redwood.widget/WidgetSystem // app.cash.redwood.protocol.guest/ProtocolWidgetSystemFactory.create|create(app.cash.redwood.protocol.guest.GuestProtocolAdapter;app.cash.redwood.protocol.guest.ProtocolMismatchHandler){}[0] + abstract fun modifierTag(app.cash.redwood/Modifier.Element): app.cash.redwood.protocol/ModifierTag // app.cash.redwood.protocol.guest/ProtocolWidgetSystemFactory.modifierTag|modifierTag(app.cash.redwood.Modifier.Element){}[0] } final class app.cash.redwood.protocol.guest/DefaultGuestProtocolAdapter : app.cash.redwood.protocol.guest/GuestProtocolAdapter { // app.cash.redwood.protocol.guest/DefaultGuestProtocolAdapter|null[0] @@ -71,7 +73,7 @@ final class app.cash.redwood.protocol.guest/DefaultGuestProtocolAdapter : app.ca final fun <#A1: kotlin/Any?> appendPropertyChange(app.cash.redwood.protocol/Id, app.cash.redwood.protocol/PropertyTag, kotlinx.serialization/KSerializer<#A1>, #A1) // app.cash.redwood.protocol.guest/DefaultGuestProtocolAdapter.appendPropertyChange|appendPropertyChange(app.cash.redwood.protocol.Id;app.cash.redwood.protocol.PropertyTag;kotlinx.serialization.KSerializer<0:0>;0:0){0§}[0] final fun appendAdd(app.cash.redwood.protocol/Id, app.cash.redwood.protocol/ChildrenTag, kotlin/Int, app.cash.redwood.protocol.guest/ProtocolWidget) // app.cash.redwood.protocol.guest/DefaultGuestProtocolAdapter.appendAdd|appendAdd(app.cash.redwood.protocol.Id;app.cash.redwood.protocol.ChildrenTag;kotlin.Int;app.cash.redwood.protocol.guest.ProtocolWidget){}[0] final fun appendCreate(app.cash.redwood.protocol/Id, app.cash.redwood.protocol/WidgetTag) // app.cash.redwood.protocol.guest/DefaultGuestProtocolAdapter.appendCreate|appendCreate(app.cash.redwood.protocol.Id;app.cash.redwood.protocol.WidgetTag){}[0] - final fun appendModifierChange(app.cash.redwood.protocol/Id, kotlin.collections/List) // app.cash.redwood.protocol.guest/DefaultGuestProtocolAdapter.appendModifierChange|appendModifierChange(app.cash.redwood.protocol.Id;kotlin.collections.List){}[0] + final fun appendModifierChange(app.cash.redwood.protocol/Id, app.cash.redwood/Modifier) // app.cash.redwood.protocol.guest/DefaultGuestProtocolAdapter.appendModifierChange|appendModifierChange(app.cash.redwood.protocol.Id;app.cash.redwood.Modifier){}[0] final fun appendMove(app.cash.redwood.protocol/Id, app.cash.redwood.protocol/ChildrenTag, kotlin/Int, kotlin/Int, kotlin/Int) // app.cash.redwood.protocol.guest/DefaultGuestProtocolAdapter.appendMove|appendMove(app.cash.redwood.protocol.Id;app.cash.redwood.protocol.ChildrenTag;kotlin.Int;kotlin.Int;kotlin.Int){}[0] final fun appendPropertyChange(app.cash.redwood.protocol/Id, app.cash.redwood.protocol/PropertyTag, kotlin/Boolean) // app.cash.redwood.protocol.guest/DefaultGuestProtocolAdapter.appendPropertyChange|appendPropertyChange(app.cash.redwood.protocol.Id;app.cash.redwood.protocol.PropertyTag;kotlin.Boolean){}[0] final fun appendPropertyChange(app.cash.redwood.protocol/Id, app.cash.redwood.protocol/PropertyTag, kotlin/UInt) // app.cash.redwood.protocol.guest/DefaultGuestProtocolAdapter.appendPropertyChange|appendPropertyChange(app.cash.redwood.protocol.Id;app.cash.redwood.protocol.PropertyTag;kotlin.UInt){}[0] diff --git a/redwood-protocol-guest/src/commonMain/kotlin/app/cash/redwood/protocol/guest/DefaultGuestProtocolAdapter.kt b/redwood-protocol-guest/src/commonMain/kotlin/app/cash/redwood/protocol/guest/DefaultGuestProtocolAdapter.kt index b766196077..7c11c79e1c 100644 --- a/redwood-protocol-guest/src/commonMain/kotlin/app/cash/redwood/protocol/guest/DefaultGuestProtocolAdapter.kt +++ b/redwood-protocol-guest/src/commonMain/kotlin/app/cash/redwood/protocol/guest/DefaultGuestProtocolAdapter.kt @@ -15,6 +15,7 @@ */ package app.cash.redwood.protocol.guest +import app.cash.redwood.Modifier import app.cash.redwood.RedwoodCodegenApi import app.cash.redwood.protocol.Change import app.cash.redwood.protocol.ChangesSink @@ -41,7 +42,7 @@ import kotlinx.serialization.json.JsonPrimitive public class DefaultGuestProtocolAdapter( public override val json: Json = Json.Default, hostVersion: RedwoodVersion, - widgetSystemFactory: ProtocolWidgetSystemFactory, + private val widgetSystemFactory: ProtocolWidgetSystemFactory, private val mismatchHandler: ProtocolMismatchHandler = ProtocolMismatchHandler.Throwing, ) : GuestProtocolAdapter { private var nextValue = Id.Root.value + 1 @@ -111,13 +112,23 @@ public class DefaultGuestProtocolAdapter( changes.add(PropertyChange(id, tag, JsonPrimitive(value))) } - public override fun appendModifierChange( - id: Id, - elements: List, - ) { + override fun appendModifierChange(id: Id, value: Modifier) { + val elements = mutableListOf() + + value.forEach { element -> + elements += modifierElement(element) + } + changes.add(ModifierChange(id, elements)) } + private fun modifierElement(element: T): ModifierElement { + val tag = widgetSystemFactory.modifierTag(element) + val serializer = widgetSystemFactory.modifierSerializer(element) + ?: return ModifierElement(tag) + return ModifierElement(tag, json.encodeToJsonElement(serializer, element)) + } + public override fun appendAdd( id: Id, tag: ChildrenTag, diff --git a/redwood-protocol-guest/src/commonMain/kotlin/app/cash/redwood/protocol/guest/GuestProtocolAdapter.kt b/redwood-protocol-guest/src/commonMain/kotlin/app/cash/redwood/protocol/guest/GuestProtocolAdapter.kt index 1c16de6d93..40300dbda1 100644 --- a/redwood-protocol-guest/src/commonMain/kotlin/app/cash/redwood/protocol/guest/GuestProtocolAdapter.kt +++ b/redwood-protocol-guest/src/commonMain/kotlin/app/cash/redwood/protocol/guest/GuestProtocolAdapter.kt @@ -15,12 +15,12 @@ */ package app.cash.redwood.protocol.guest +import app.cash.redwood.Modifier import app.cash.redwood.RedwoodCodegenApi import app.cash.redwood.protocol.ChangesSink import app.cash.redwood.protocol.ChildrenTag import app.cash.redwood.protocol.EventSink import app.cash.redwood.protocol.Id -import app.cash.redwood.protocol.ModifierElement import app.cash.redwood.protocol.PropertyTag import app.cash.redwood.protocol.WidgetTag import app.cash.redwood.widget.Widget @@ -106,7 +106,7 @@ public interface GuestProtocolAdapter : EventSink { @RedwoodCodegenApi public fun appendModifierChange( id: Id, - elements: List, + value: Modifier, ) @RedwoodCodegenApi diff --git a/redwood-protocol-guest/src/commonMain/kotlin/app/cash/redwood/protocol/guest/ProtocolWidgetSystemFactory.kt b/redwood-protocol-guest/src/commonMain/kotlin/app/cash/redwood/protocol/guest/ProtocolWidgetSystemFactory.kt index e8eef5ac70..79c668915f 100644 --- a/redwood-protocol-guest/src/commonMain/kotlin/app/cash/redwood/protocol/guest/ProtocolWidgetSystemFactory.kt +++ b/redwood-protocol-guest/src/commonMain/kotlin/app/cash/redwood/protocol/guest/ProtocolWidgetSystemFactory.kt @@ -15,7 +15,11 @@ */ package app.cash.redwood.protocol.guest +import app.cash.redwood.Modifier +import app.cash.redwood.RedwoodCodegenApi +import app.cash.redwood.protocol.ModifierTag import app.cash.redwood.widget.WidgetSystem +import kotlinx.serialization.KSerializer public interface ProtocolWidgetSystemFactory { /** Create a new [WidgetSystem] connected to a host via [guestAdapter]. */ @@ -23,4 +27,11 @@ public interface ProtocolWidgetSystemFactory { guestAdapter: GuestProtocolAdapter, mismatchHandler: ProtocolMismatchHandler = ProtocolMismatchHandler.Throwing, ): WidgetSystem + + @RedwoodCodegenApi + public fun modifierTag(element: Modifier.Element): ModifierTag + + /** Returns null if the modifier is stateless and should serialize as null. */ + @RedwoodCodegenApi + public fun modifierSerializer(element: T): KSerializer? } diff --git a/redwood-tooling-codegen/src/main/kotlin/app/cash/redwood/tooling/codegen/protocolCodegen.kt b/redwood-tooling-codegen/src/main/kotlin/app/cash/redwood/tooling/codegen/protocolCodegen.kt index 8e73107c0f..90eb1eb393 100644 --- a/redwood-tooling-codegen/src/main/kotlin/app/cash/redwood/tooling/codegen/protocolCodegen.kt +++ b/redwood-tooling-codegen/src/main/kotlin/app/cash/redwood/tooling/codegen/protocolCodegen.kt @@ -37,7 +37,6 @@ internal fun ProtocolSchemaSet.generateFileSpecs(type: ProtocolCodegenType): Lis when (type) { Guest -> { add(generateProtocolWidgetSystemFactory(this@generateFileSpecs)) - add(generateComposeProtocolModifierSerialization(this@generateFileSpecs)) for (dependency in all) { add(generateProtocolWidgetFactory(dependency, host = schema)) generateProtocolModifierSerializers(dependency, host = schema)?.let { add(it) } diff --git a/redwood-tooling-codegen/src/main/kotlin/app/cash/redwood/tooling/codegen/protocolGuestGeneration.kt b/redwood-tooling-codegen/src/main/kotlin/app/cash/redwood/tooling/codegen/protocolGuestGeneration.kt index 5bdf2539a3..fb400f06f5 100644 --- a/redwood-tooling-codegen/src/main/kotlin/app/cash/redwood/tooling/codegen/protocolGuestGeneration.kt +++ b/redwood-tooling-codegen/src/main/kotlin/app/cash/redwood/tooling/codegen/protocolGuestGeneration.kt @@ -38,10 +38,9 @@ import com.squareup.kotlinpoet.INT import com.squareup.kotlinpoet.KModifier.INTERNAL import com.squareup.kotlinpoet.KModifier.OVERRIDE import com.squareup.kotlinpoet.KModifier.PRIVATE -import com.squareup.kotlinpoet.LIST +import com.squareup.kotlinpoet.KModifier.PUBLIC import com.squareup.kotlinpoet.LONG import com.squareup.kotlinpoet.LambdaTypeName -import com.squareup.kotlinpoet.MemberName import com.squareup.kotlinpoet.NOTHING import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy import com.squareup.kotlinpoet.PropertySpec @@ -49,6 +48,7 @@ import com.squareup.kotlinpoet.SHORT import com.squareup.kotlinpoet.STRING import com.squareup.kotlinpoet.TypeName import com.squareup.kotlinpoet.TypeSpec +import com.squareup.kotlinpoet.TypeVariableName import com.squareup.kotlinpoet.UNIT import com.squareup.kotlinpoet.U_INT import com.squareup.kotlinpoet.joinToCode @@ -66,6 +66,10 @@ object ExampleProtocolWidgetSystemFactory : ProtocolWidgetSystemFactory { RedwoodLayout = ProtocolRedwoodLayoutWidgetFactory(guestAdapter, mismatchHandler), ) } + + public override fun modifierTag(element: Modifier.Element): ModifierTag { ... } + + public override fun modifierSerializer(element: T): KSerializer { ... } } */ internal fun generateProtocolWidgetSystemFactory( @@ -96,6 +100,8 @@ internal fun generateProtocolWidgetSystemFactory( } .build(), ) + .addFunction(modifierTag(schemaSet)) + .addFunction(modifierSerializer(schemaSet)) .build(), ) } @@ -197,10 +203,7 @@ internal class ProtocolButton( override var modifier: Modifier get() = throw AssertionError() set(value) { - val json = buildJsonArray { - value.forEach { element -> add(element.toJsonElement(guestAdapter.json)) - } - guestAdapter.appendModifierChange(id, guestAdapter.json)) + guestAdapter.appendModifierChange(id, value) } override fun text(text: String?) { @@ -417,7 +420,7 @@ internal fun generateProtocolWidget( .setter( FunSpec.setterBuilder() .addParameter("value", Redwood.Modifier) - .addStatement("guestAdapter.appendModifierChange(id, value.%M(guestAdapter.json))", host.modifierToProtocol) + .addStatement("guestAdapter.appendModifierChange(id, value)") .build(), ) .build(), @@ -689,66 +692,89 @@ internal fun generateProtocolModifierSerializers( } } -internal fun generateComposeProtocolModifierSerialization( +/* +@RedwoodCodegenApi +public override fun modifierTag(element: Modifier.Element): ModifierTag = when (element) { + is CustomType -> ModifierTag(3) + is CustomTypeStateless -> ModifierTag(4) + else -> throw AssertionError() +} +*/ +internal fun modifierTag( schemaSet: ProtocolSchemaSet, -): FileSpec { - val schema = schemaSet.schema - val name = schema.modifierToProtocol.simpleName - return buildFileSpec(schema.guestProtocolPackage(), "modifierSerialization") { - addAnnotation(suppressDeprecations) - addFunction( - FunSpec.builder(name) - .addModifiers(INTERNAL) - .receiver(Redwood.Modifier) - .addParameter("json", KotlinxSerialization.Json) - .returns(LIST.parameterizedBy(Protocol.ModifierElement)) - .beginControlFlow("return %M", Stdlib.buildList) - .addStatement( - "this@%L.forEach { element -> add(element.%M(json)) }", - name, - schema.modifierToProtocol, +): FunSpec { + return FunSpec.builder("modifierTag") + .addAnnotation(Redwood.RedwoodCodegenApi) + .addModifiers(PUBLIC, OVERRIDE) + .addParameter("element", Redwood.ModifierElement) + .returns(Protocol.ModifierTag) + .beginControlFlow("return when (element)") + .apply { + val allModifiers = schemaSet.allModifiers() + for ((localSchema, modifier) in allModifiers) { + val modifierType = localSchema.modifierType(modifier) + addStatement( + "is %T -> %T(%L)", + modifierType, + Protocol.ModifierTag, + modifier.tag, ) - .endControlFlow() + } + } + .addStatement("else -> throw %T()", Stdlib.AssertionError) + .endControlFlow() + .build() +} + +/* +@RedwoodCodegenApi +@Suppress("UNCHECKED_CAST") +public override fun modifierSerializer(element: T): KSerializer? = + when (element) { + is CustomType -> CustomTypeSerializer + is CustomTypeStateless -> null + else -> throw AssertionError() + } as KSerializer? +*/ +internal fun modifierSerializer( + schemaSet: ProtocolSchemaSet, +): FunSpec { + val schema = schemaSet.schema + val allModifiers = schemaSet.allModifiers() + val t = TypeVariableName("T", Redwood.ModifierElement) + + return FunSpec.builder("modifierSerializer") + .addAnnotation(Redwood.RedwoodCodegenApi) + .addAnnotation( + AnnotationSpec.builder(Suppress::class) + .addMember("%S", "UNCHECKED_CAST") .build(), ) - addFunction( - FunSpec.builder(schema.modifierToProtocol) - .addModifiers(PRIVATE) - .receiver(Redwood.ModifierElement) - .addParameter("json", KotlinxSerialization.Json) - .returns(Protocol.ModifierElement) - .beginControlFlow("return when (this)") - .apply { - val modifier = schemaSet.allModifiers() - if (modifier.isEmpty()) { - addAnnotation( - AnnotationSpec.builder(Suppress::class) - .addMember("%S, %S", "UNUSED_PARAMETER", "UNUSED_EXPRESSION") - .build(), + .addModifiers(PUBLIC, OVERRIDE) + .addTypeVariable(t) + .addParameter("element", t) + .returns(KotlinxSerialization.KSerializer.parameterizedBy(t).copy(nullable = true)) + .addCode("return when·(element)·{⇥\n") + .apply { + for ((localSchema, modifier) in allModifiers) { + val modifierType = localSchema.modifierType(modifier) + when { + modifier.properties.isEmpty() -> { + addStatement("is %T -> null", modifierType) + } + else -> { + addStatement( + "is %T -> %T", + modifierType, + localSchema.modifierSerializer(modifier, schema), ) - } else { - for ((localSchema, modifier) in modifier) { - val modifierType = localSchema.modifierType(modifier) - val surrogate = localSchema.modifierSerializer(modifier, schema) - if (modifier.properties.isEmpty()) { - addStatement( - "is %T -> %T(%T(%L))", - modifierType, - Protocol.ModifierElement, - Protocol.ModifierTag, - modifier.tag, - ) - } else { - addStatement("is %T -> %T.encode(json, this)", modifierType, surrogate) - } - } } } - .addStatement("else -> throw %T()", Stdlib.AssertionError) - .endControlFlow() - .build(), - ) - } + } + } + .addStatement("else -> throw %T()", Stdlib.AssertionError) + .addCode("⇤} as %T?\n", KotlinxSerialization.KSerializer.parameterizedBy(t)) + .build() } private fun Schema.protocolWidgetFactoryType(host: Schema): ClassName { @@ -762,6 +788,3 @@ private fun Schema.protocolWidgetType(widget: Widget, host: Schema): ClassName { private fun Schema.modifierSerializer(modifier: Modifier, host: Schema): ClassName { return ClassName(guestProtocolPackage(host), modifier.type.flatName + "Serializer") } - -internal val Schema.modifierToProtocol: MemberName get() = - MemberName(guestProtocolPackage(), "toProtocol") diff --git a/redwood-treehouse-guest/src/jsMain/kotlin/app/cash/redwood/treehouse/ProtocolBridgeJs.kt b/redwood-treehouse-guest/src/jsMain/kotlin/app/cash/redwood/treehouse/ProtocolBridgeJs.kt index f244724654..442cc1c723 100644 --- a/redwood-treehouse-guest/src/jsMain/kotlin/app/cash/redwood/treehouse/ProtocolBridgeJs.kt +++ b/redwood-treehouse-guest/src/jsMain/kotlin/app/cash/redwood/treehouse/ProtocolBridgeJs.kt @@ -15,13 +15,13 @@ */ package app.cash.redwood.treehouse +import app.cash.redwood.Modifier import app.cash.redwood.RedwoodCodegenApi import app.cash.redwood.protocol.Change import app.cash.redwood.protocol.ChangesSink import app.cash.redwood.protocol.ChildrenTag import app.cash.redwood.protocol.Event import app.cash.redwood.protocol.Id -import app.cash.redwood.protocol.ModifierElement import app.cash.redwood.protocol.PropertyTag import app.cash.redwood.protocol.RedwoodVersion import app.cash.redwood.protocol.WidgetTag @@ -49,7 +49,7 @@ internal actual fun GuestProtocolAdapter( internal class FastGuestProtocolAdapter( override val json: Json = Json.Default, hostVersion: RedwoodVersion, - widgetSystemFactory: ProtocolWidgetSystemFactory, + private val widgetSystemFactory: ProtocolWidgetSystemFactory, private val mismatchHandler: ProtocolMismatchHandler = ProtocolMismatchHandler.Throwing, ) : GuestProtocolAdapter { private var nextValue = Id.Root.value + 1 @@ -138,12 +138,20 @@ internal class FastGuestProtocolAdapter( changes.push(js("""["property",{"id":id,"tag":tag,"value":value}]""")) } - override fun appendModifierChange( - id: Id, - elements: List, - ) { + override fun appendModifierChange(id: Id, value: Modifier) { + val elements = js("[]") + + value.forEach { element -> + val tag = widgetSystemFactory.modifierTag(element) + val serializer = widgetSystemFactory.modifierSerializer(element) + val value = when { + serializer == null -> null + else -> json.encodeToDynamic(serializer, element) + } + elements.push(js("""[tag,value]""")) + } + val id = id - val elements = Json.encodeToDynamic(elements) changes.push(js("""["modifier",{"id":id,"elements":elements}]""")) } diff --git a/redwood-treehouse-guest/src/jsTest/kotlin/app/cash/redwood/treehouse/FastGuestProtocolAdapterTest.kt b/redwood-treehouse-guest/src/jsTest/kotlin/app/cash/redwood/treehouse/FastGuestProtocolAdapterTest.kt index 0c568ba4b0..6c7cf955e4 100644 --- a/redwood-treehouse-guest/src/jsTest/kotlin/app/cash/redwood/treehouse/FastGuestProtocolAdapterTest.kt +++ b/redwood-treehouse-guest/src/jsTest/kotlin/app/cash/redwood/treehouse/FastGuestProtocolAdapterTest.kt @@ -24,11 +24,13 @@ import app.cash.redwood.protocol.guest.guestRedwoodVersion import app.cash.redwood.widget.Widget import assertk.assertThat import assertk.assertions.isEqualTo +import com.example.redwood.testapp.compose.TestScope import com.example.redwood.testapp.compose.backgroundColor import com.example.redwood.testapp.protocol.guest.TestSchemaProtocolWidgetSystemFactory import com.example.redwood.testapp.widget.TestSchemaWidgetSystem import kotlin.test.Test import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.KSerializer import kotlinx.serialization.builtins.serializer @@ -62,6 +64,20 @@ class FastGuestProtocolAdapterTest { } } + @Test fun consistentWithDefaultGuestProtocolAdapterForModifiers() { + assertChangesEqual { root, widgetSystem -> + with(object : TestScope {}) { + val button = widgetSystem.TestSchema.Button() + button.modifier = Modifier + .backgroundColor(0xff0000u) + .customType(5.seconds) + .customTypeWithDefault(10.seconds, "sup") + .customTypeStateless() + root.insert(0, button) + } + } + } + /** Test our special case for https://github.com/Kotlin/kotlinx.serialization/issues/2713 */ @Test fun consistentWithDefaultGuestProtocolAdapterForUint() { assertChangesEqual { root, widgetSystem -> From 40196118414b4ac7932d7a2a257b5fbf61ea3462 Mon Sep 17 00:00:00 2001 From: Jesse Wilson Date: Wed, 21 Aug 2024 12:25:26 -0400 Subject: [PATCH 2/8] Fix sample code --- .../app/cash/redwood/tooling/codegen/protocolGuestGeneration.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/redwood-tooling-codegen/src/main/kotlin/app/cash/redwood/tooling/codegen/protocolGuestGeneration.kt b/redwood-tooling-codegen/src/main/kotlin/app/cash/redwood/tooling/codegen/protocolGuestGeneration.kt index fb400f06f5..1cc1c18b51 100644 --- a/redwood-tooling-codegen/src/main/kotlin/app/cash/redwood/tooling/codegen/protocolGuestGeneration.kt +++ b/redwood-tooling-codegen/src/main/kotlin/app/cash/redwood/tooling/codegen/protocolGuestGeneration.kt @@ -69,7 +69,7 @@ object ExampleProtocolWidgetSystemFactory : ProtocolWidgetSystemFactory { public override fun modifierTag(element: Modifier.Element): ModifierTag { ... } - public override fun modifierSerializer(element: T): KSerializer { ... } + public override fun modifierSerializer(element: T): KSerializer? { ... } } */ internal fun generateProtocolWidgetSystemFactory( From 30d3268d62a785850d2af941232013f21c97f3bf Mon Sep 17 00:00:00 2001 From: Jesse Wilson Date: Wed, 21 Aug 2024 12:29:38 -0400 Subject: [PATCH 3/8] Delete unused generated code --- .../codegen/protocolGuestGeneration.kt | 21 +------------------ 1 file changed, 1 insertion(+), 20 deletions(-) diff --git a/redwood-tooling-codegen/src/main/kotlin/app/cash/redwood/tooling/codegen/protocolGuestGeneration.kt b/redwood-tooling-codegen/src/main/kotlin/app/cash/redwood/tooling/codegen/protocolGuestGeneration.kt index 1cc1c18b51..5160ba3b84 100644 --- a/redwood-tooling-codegen/src/main/kotlin/app/cash/redwood/tooling/codegen/protocolGuestGeneration.kt +++ b/redwood-tooling-codegen/src/main/kotlin/app/cash/redwood/tooling/codegen/protocolGuestGeneration.kt @@ -207,7 +207,7 @@ internal class ProtocolButton( } override fun text(text: String?) { - guestAdapter.appendPropertyChange(this.id, PropertyTag(1), guestAdapter.json.encodeToJsonElement(serializer_0, text)) + this.guestAdapter.appendPropertyChange(this.id, PropertyTag(1), serializer_0, text) } override fun onClick(onClick: (() -> Unit)?) { @@ -494,11 +494,6 @@ internal object GrowSerializer : KSerializer { override fun deserialize(decoder: Decoder): Grow { throw AssertionError() } - - fun encode(json: Json, value: Grow): ModifierElement { - val element = json.encodeToJsonElement(this, value) - return ModifierElement(ModifierTag(3), element) - } } */ internal fun generateProtocolModifierSerializers( @@ -672,20 +667,6 @@ internal fun generateProtocolModifierSerializers( .addStatement("throw %T()", Stdlib.AssertionError) .build(), ) - .addFunction( - FunSpec.builder("encode") - .addParameter("json", KotlinxSerialization.Json) - .addParameter("value", modifierType) - .returns(Protocol.ModifierElement) - .addStatement("val element = json.encodeToJsonElement(this, value)") - .addStatement( - "return %T(%T(%L), element)", - Protocol.ModifierElement, - Protocol.ModifierTag, - modifier.tag, - ) - .build(), - ) .build(), ) } From 71a4c192bfa9f8eab3dc6ca5b905809f9f55c5e6 Mon Sep 17 00:00:00 2001 From: Jesse Wilson Date: Wed, 21 Aug 2024 13:55:22 -0400 Subject: [PATCH 4/8] Drop an obsolete test --- .../tooling/codegen/ProtocolGuestGenerationTest.kt | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/redwood-tooling-codegen/src/test/kotlin/app/cash/redwood/tooling/codegen/ProtocolGuestGenerationTest.kt b/redwood-tooling-codegen/src/test/kotlin/app/cash/redwood/tooling/codegen/ProtocolGuestGenerationTest.kt index ba70b500ef..a5b6e9ff04 100644 --- a/redwood-tooling-codegen/src/test/kotlin/app/cash/redwood/tooling/codegen/ProtocolGuestGenerationTest.kt +++ b/redwood-tooling-codegen/src/test/kotlin/app/cash/redwood/tooling/codegen/ProtocolGuestGenerationTest.kt @@ -18,12 +18,9 @@ package app.cash.redwood.tooling.codegen import app.cash.redwood.schema.Property import app.cash.redwood.schema.Schema import app.cash.redwood.schema.Widget -import app.cash.redwood.tooling.schema.ProtocolSchemaSet import app.cash.redwood.tooling.schema.parseTestSchema -import assertk.all import assertk.assertThat import assertk.assertions.contains -import com.example.redwood.testapp.TestSchema import org.junit.Test class ProtocolGuestGenerationTest { @@ -51,14 +48,4 @@ class ProtocolGuestGenerationTest { """.trimMargin(), ) } - - @Test fun `dependency layout modifier are included in serialization`() { - val schemaSet = ProtocolSchemaSet.load(TestSchema::class) - - val fileSpec = generateComposeProtocolModifierSerialization(schemaSet) - assertThat(fileSpec.toString()).all { - contains("is TestRowVerticalAlignment -> TestRowVerticalAlignmentSerializer.encode(json, this)") - contains("is Grow -> GrowSerializer.encode(json, this)") - } - } } From 51d0f03b7cae826168dca7cca889dc0993c3c4bb Mon Sep 17 00:00:00 2001 From: Jesse Wilson Date: Mon, 26 Aug 2024 15:07:04 -0400 Subject: [PATCH 5/8] Return a pair from ProtocolWidgetSystemFactory --- .../guest/DefaultGuestProtocolAdapter.kt | 5 +- .../guest/ProtocolWidgetSystemFactory.kt | 11 ++- .../codegen/protocolGuestGeneration.kt | 88 ++++++------------- .../app/cash/redwood/tooling/codegen/types.kt | 3 +- .../redwood/treehouse/ProtocolBridgeJs.kt | 3 +- 5 files changed, 39 insertions(+), 71 deletions(-) diff --git a/redwood-protocol-guest/src/commonMain/kotlin/app/cash/redwood/protocol/guest/DefaultGuestProtocolAdapter.kt b/redwood-protocol-guest/src/commonMain/kotlin/app/cash/redwood/protocol/guest/DefaultGuestProtocolAdapter.kt index 7c11c79e1c..a5c5b70ced 100644 --- a/redwood-protocol-guest/src/commonMain/kotlin/app/cash/redwood/protocol/guest/DefaultGuestProtocolAdapter.kt +++ b/redwood-protocol-guest/src/commonMain/kotlin/app/cash/redwood/protocol/guest/DefaultGuestProtocolAdapter.kt @@ -123,9 +123,8 @@ public class DefaultGuestProtocolAdapter( } private fun modifierElement(element: T): ModifierElement { - val tag = widgetSystemFactory.modifierTag(element) - val serializer = widgetSystemFactory.modifierSerializer(element) - ?: return ModifierElement(tag) + val (tag, serializer) = widgetSystemFactory.modifierTagAndSerializationStrategy(element) + if (serializer == null) return ModifierElement(tag) return ModifierElement(tag, json.encodeToJsonElement(serializer, element)) } diff --git a/redwood-protocol-guest/src/commonMain/kotlin/app/cash/redwood/protocol/guest/ProtocolWidgetSystemFactory.kt b/redwood-protocol-guest/src/commonMain/kotlin/app/cash/redwood/protocol/guest/ProtocolWidgetSystemFactory.kt index 79c668915f..efa42ad287 100644 --- a/redwood-protocol-guest/src/commonMain/kotlin/app/cash/redwood/protocol/guest/ProtocolWidgetSystemFactory.kt +++ b/redwood-protocol-guest/src/commonMain/kotlin/app/cash/redwood/protocol/guest/ProtocolWidgetSystemFactory.kt @@ -19,7 +19,7 @@ import app.cash.redwood.Modifier import app.cash.redwood.RedwoodCodegenApi import app.cash.redwood.protocol.ModifierTag import app.cash.redwood.widget.WidgetSystem -import kotlinx.serialization.KSerializer +import kotlinx.serialization.SerializationStrategy public interface ProtocolWidgetSystemFactory { /** Create a new [WidgetSystem] connected to a host via [guestAdapter]. */ @@ -28,10 +28,9 @@ public interface ProtocolWidgetSystemFactory { mismatchHandler: ProtocolMismatchHandler = ProtocolMismatchHandler.Throwing, ): WidgetSystem + /** The serialization strategy is null if the modifier is stateless. */ @RedwoodCodegenApi - public fun modifierTag(element: Modifier.Element): ModifierTag - - /** Returns null if the modifier is stateless and should serialize as null. */ - @RedwoodCodegenApi - public fun modifierSerializer(element: T): KSerializer? + public fun modifierTagAndSerializationStrategy( + element: T, + ): Pair?> } diff --git a/redwood-tooling-codegen/src/main/kotlin/app/cash/redwood/tooling/codegen/protocolGuestGeneration.kt b/redwood-tooling-codegen/src/main/kotlin/app/cash/redwood/tooling/codegen/protocolGuestGeneration.kt index 5160ba3b84..b23678cead 100644 --- a/redwood-tooling-codegen/src/main/kotlin/app/cash/redwood/tooling/codegen/protocolGuestGeneration.kt +++ b/redwood-tooling-codegen/src/main/kotlin/app/cash/redwood/tooling/codegen/protocolGuestGeneration.kt @@ -41,7 +41,6 @@ import com.squareup.kotlinpoet.KModifier.PRIVATE import com.squareup.kotlinpoet.KModifier.PUBLIC import com.squareup.kotlinpoet.LONG import com.squareup.kotlinpoet.LambdaTypeName -import com.squareup.kotlinpoet.NOTHING import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy import com.squareup.kotlinpoet.PropertySpec import com.squareup.kotlinpoet.SHORT @@ -69,7 +68,9 @@ object ExampleProtocolWidgetSystemFactory : ProtocolWidgetSystemFactory { public override fun modifierTag(element: Modifier.Element): ModifierTag { ... } - public override fun modifierSerializer(element: T): KSerializer? { ... } + public override fun modifierTagAndSerializationStrategy( + element: T + ): Pair?> { ... } } */ internal fun generateProtocolWidgetSystemFactory( @@ -100,8 +101,7 @@ internal fun generateProtocolWidgetSystemFactory( } .build(), ) - .addFunction(modifierTag(schemaSet)) - .addFunction(modifierSerializer(schemaSet)) + .addFunction(modifierTagAndSerializationStrategy(schemaSet)) .build(), ) } @@ -479,7 +479,7 @@ private fun workAroundLazyListPlaceholderRemoveCrash( ): Boolean = widget.type.names in placeholderParentTypeNames && trait.name == "placeholder" /* -internal object GrowSerializer : KSerializer { +internal object GrowSerializer : SerializationStrategy { override val descriptor = buildClassSerialDescriptor("app.cash.redwood.layout.Grow") { element("value") @@ -607,7 +607,7 @@ internal fun generateProtocolModifierSerializers( addType( TypeSpec.objectBuilder(serializerType) .addModifiers(INTERNAL) - .addSuperinterface(KotlinxSerialization.KSerializer.parameterizedBy(modifierType)) + .addSuperinterface(KotlinxSerialization.SerializationStrategy.parameterizedBy(modifierType)) .addProperty( PropertySpec.builder("descriptor", KotlinxSerialization.SerialDescriptor) .addModifiers(OVERRIDE) @@ -659,72 +659,35 @@ internal fun generateProtocolModifierSerializers( .addStatement("composite.endStructure(descriptor)") .build(), ) - .addFunction( - FunSpec.builder("deserialize") - .addModifiers(OVERRIDE) - .addParameter("decoder", KotlinxSerialization.Decoder) - .returns(NOTHING) - .addStatement("throw %T()", Stdlib.AssertionError) - .build(), - ) .build(), ) } } } -/* -@RedwoodCodegenApi -public override fun modifierTag(element: Modifier.Element): ModifierTag = when (element) { - is CustomType -> ModifierTag(3) - is CustomTypeStateless -> ModifierTag(4) - else -> throw AssertionError() -} -*/ -internal fun modifierTag( - schemaSet: ProtocolSchemaSet, -): FunSpec { - return FunSpec.builder("modifierTag") - .addAnnotation(Redwood.RedwoodCodegenApi) - .addModifiers(PUBLIC, OVERRIDE) - .addParameter("element", Redwood.ModifierElement) - .returns(Protocol.ModifierTag) - .beginControlFlow("return when (element)") - .apply { - val allModifiers = schemaSet.allModifiers() - for ((localSchema, modifier) in allModifiers) { - val modifierType = localSchema.modifierType(modifier) - addStatement( - "is %T -> %T(%L)", - modifierType, - Protocol.ModifierTag, - modifier.tag, - ) - } - } - .addStatement("else -> throw %T()", Stdlib.AssertionError) - .endControlFlow() - .build() -} - /* @RedwoodCodegenApi @Suppress("UNCHECKED_CAST") -public override fun modifierSerializer(element: T): KSerializer? = +public override fun modifierTagAndSerializationStrategy( + element: T, +): Pair?> = when (element) { - is CustomType -> CustomTypeSerializer - is CustomTypeStateless -> null + is CustomType -> ModifierTag(3) to CustomTypeSerializer + is CustomTypeStateless -> ModifierTag(4) to null else -> throw AssertionError() - } as KSerializer? + } */ -internal fun modifierSerializer( +internal fun modifierTagAndSerializationStrategy( schemaSet: ProtocolSchemaSet, ): FunSpec { val schema = schemaSet.schema val allModifiers = schemaSet.allModifiers() val t = TypeVariableName("T", Redwood.ModifierElement) - - return FunSpec.builder("modifierSerializer") + val returnType = Stdlib.Pair.parameterizedBy( + Protocol.ModifierTag, + KotlinxSerialization.KSerializer.parameterizedBy(t).copy(nullable = true), + ) + return FunSpec.builder("modifierTagAndSerializationStrategy") .addAnnotation(Redwood.RedwoodCodegenApi) .addAnnotation( AnnotationSpec.builder(Suppress::class) @@ -734,19 +697,26 @@ internal fun modifierSerializer( .addModifiers(PUBLIC, OVERRIDE) .addTypeVariable(t) .addParameter("element", t) - .returns(KotlinxSerialization.KSerializer.parameterizedBy(t).copy(nullable = true)) + .returns(returnType) .addCode("return when·(element)·{⇥\n") .apply { for ((localSchema, modifier) in allModifiers) { val modifierType = localSchema.modifierType(modifier) when { modifier.properties.isEmpty() -> { - addStatement("is %T -> null", modifierType) + addStatement( + "is %T -> %T(%L) to null", + modifierType, + Protocol.ModifierTag, + modifier.tag, + ) } else -> { addStatement( - "is %T -> %T", + "is %T -> %T(%L) to %T", modifierType, + Protocol.ModifierTag, + modifier.tag, localSchema.modifierSerializer(modifier, schema), ) } @@ -754,7 +724,7 @@ internal fun modifierSerializer( } } .addStatement("else -> throw %T()", Stdlib.AssertionError) - .addCode("⇤} as %T?\n", KotlinxSerialization.KSerializer.parameterizedBy(t)) + .addCode("⇤} as %T\n", returnType) .build() } diff --git a/redwood-tooling-codegen/src/main/kotlin/app/cash/redwood/tooling/codegen/types.kt b/redwood-tooling-codegen/src/main/kotlin/app/cash/redwood/tooling/codegen/types.kt index 56305e7104..b356339a2a 100644 --- a/redwood-tooling-codegen/src/main/kotlin/app/cash/redwood/tooling/codegen/types.kt +++ b/redwood-tooling-codegen/src/main/kotlin/app/cash/redwood/tooling/codegen/types.kt @@ -112,7 +112,7 @@ internal object Stdlib { val ExperimentalObjCName = ClassName("kotlin.experimental", "ExperimentalObjCName") val List = ClassName("kotlin.collections", "List") val ObjCName = ClassName("kotlin.native", "ObjCName") - val buildList = MemberName("kotlin.collections", "buildList") + val Pair = ClassName("kotlin", "Pair") val listOf = MemberName("kotlin.collections", "listOf") } @@ -124,6 +124,7 @@ internal object KotlinxSerialization { val ExperimentalSerializationApi = ClassName("kotlinx.serialization", "ExperimentalSerializationApi") val KSerializer = ClassName("kotlinx.serialization", "KSerializer") val Serializable = ClassName("kotlinx.serialization", "Serializable") + val SerializationStrategy = ClassName("kotlinx.serialization", "SerializationStrategy") val serializer = MemberName("kotlinx.serialization", "serializer") val SerialDescriptor = ClassName("kotlinx.serialization.descriptors", "SerialDescriptor") diff --git a/redwood-treehouse-guest/src/jsMain/kotlin/app/cash/redwood/treehouse/ProtocolBridgeJs.kt b/redwood-treehouse-guest/src/jsMain/kotlin/app/cash/redwood/treehouse/ProtocolBridgeJs.kt index 442cc1c723..916e73129b 100644 --- a/redwood-treehouse-guest/src/jsMain/kotlin/app/cash/redwood/treehouse/ProtocolBridgeJs.kt +++ b/redwood-treehouse-guest/src/jsMain/kotlin/app/cash/redwood/treehouse/ProtocolBridgeJs.kt @@ -142,8 +142,7 @@ internal class FastGuestProtocolAdapter( val elements = js("[]") value.forEach { element -> - val tag = widgetSystemFactory.modifierTag(element) - val serializer = widgetSystemFactory.modifierSerializer(element) + val (tag, serializer) = widgetSystemFactory.modifierTagAndSerializationStrategy(element) val value = when { serializer == null -> null else -> json.encodeToDynamic(serializer, element) From 0c9b89fa425cfdb8cc18eacf1aa25fc68381f980 Mon Sep 17 00:00:00 2001 From: Jesse Wilson Date: Mon, 26 Aug 2024 15:29:22 -0400 Subject: [PATCH 6/8] Generate the tag and serializer as a single symbol --- .../codegen/protocolGuestGeneration.kt | 78 +++++++++++-------- 1 file changed, 45 insertions(+), 33 deletions(-) diff --git a/redwood-tooling-codegen/src/main/kotlin/app/cash/redwood/tooling/codegen/protocolGuestGeneration.kt b/redwood-tooling-codegen/src/main/kotlin/app/cash/redwood/tooling/codegen/protocolGuestGeneration.kt index b23678cead..6e939ead27 100644 --- a/redwood-tooling-codegen/src/main/kotlin/app/cash/redwood/tooling/codegen/protocolGuestGeneration.kt +++ b/redwood-tooling-codegen/src/main/kotlin/app/cash/redwood/tooling/codegen/protocolGuestGeneration.kt @@ -41,6 +41,7 @@ import com.squareup.kotlinpoet.KModifier.PRIVATE import com.squareup.kotlinpoet.KModifier.PUBLIC import com.squareup.kotlinpoet.LONG import com.squareup.kotlinpoet.LambdaTypeName +import com.squareup.kotlinpoet.MemberName import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy import com.squareup.kotlinpoet.PropertySpec import com.squareup.kotlinpoet.SHORT @@ -479,7 +480,8 @@ private fun workAroundLazyListPlaceholderRemoveCrash( ): Boolean = widget.type.names in placeholderParentTypeNames && trait.name == "placeholder" /* -internal object GrowSerializer : SerializationStrategy { +internal val GrowTagAndSerializer: Pair?> = + ModifierTag(1_000_001) to object : SerializationStrategy { override val descriptor = buildClassSerialDescriptor("app.cash.redwood.layout.Grow") { element("value") @@ -500,15 +502,14 @@ internal fun generateProtocolModifierSerializers( schema: ProtocolSchema, host: ProtocolSchema, ): FileSpec? { - val serializableModifiers = schema.modifiers.filter { it.properties.isNotEmpty() } - if (serializableModifiers.isEmpty()) { + if (schema.modifiers.isEmpty()) { return null } return buildFileSpec(schema.guestProtocolPackage(host), "modifierSerializers") { addAnnotation(suppressDeprecations) - for (modifier in serializableModifiers) { - val serializerType = schema.modifierSerializer(modifier, host) + for (modifier in schema.modifiers) { + val serializerTagAndSerializer = schema.modifierTagAndSerializer(modifier, host) val modifierType = schema.modifierType(modifier) var nextSerializerId = 0 @@ -604,10 +605,11 @@ internal fun generateProtocolModifierSerializers( } } - addType( - TypeSpec.objectBuilder(serializerType) - .addModifiers(INTERNAL) - .addSuperinterface(KotlinxSerialization.SerializationStrategy.parameterizedBy(modifierType)) + val serializationStrategyType = + KotlinxSerialization.SerializationStrategy.parameterizedBy(modifierType) + val serializationStrategyValue = if (modifier.properties.isNotEmpty()) { + TypeSpec.anonymousClassBuilder() + .addSuperinterface(serializationStrategyType) .addProperty( PropertySpec.builder("descriptor", KotlinxSerialization.SerialDescriptor) .addModifiers(OVERRIDE) @@ -638,7 +640,11 @@ internal fun generateProtocolModifierSerializers( parameters += CodeBlock.of("emptyArray()") } - initializer("%T(%L)", KotlinxSerialization.ContextualSerializer, parameters.joinToCode()) + initializer( + "%T(%L)", + KotlinxSerialization.ContextualSerializer, + parameters.joinToCode(), + ) } .build(), ) @@ -659,6 +665,26 @@ internal fun generateProtocolModifierSerializers( .addStatement("composite.endStructure(descriptor)") .build(), ) + .build() + } else { + null + } + + addProperty( + PropertySpec.builder( + name = serializerTagAndSerializer.simpleName, + type = Stdlib.Pair.parameterizedBy( + Protocol.ModifierTag, + serializationStrategyType.copy(nullable = true), + ), + INTERNAL, + ) + .initializer( + "%T(%L) to %L", + Protocol.ModifierTag, + modifier.tag, + serializationStrategyValue, + ) .build(), ) } @@ -672,8 +698,8 @@ public override fun modifierTagAndSerializationStrategy( element: T, ): Pair?> = when (element) { - is CustomType -> ModifierTag(3) to CustomTypeSerializer - is CustomTypeStateless -> ModifierTag(4) to null + is CustomType -> CustomTypeTagAndSerializer + is CustomTypeStateless -> CustomTypeStatelessTagAndSerializer else -> throw AssertionError() } */ @@ -702,25 +728,11 @@ internal fun modifierTagAndSerializationStrategy( .apply { for ((localSchema, modifier) in allModifiers) { val modifierType = localSchema.modifierType(modifier) - when { - modifier.properties.isEmpty() -> { - addStatement( - "is %T -> %T(%L) to null", - modifierType, - Protocol.ModifierTag, - modifier.tag, - ) - } - else -> { - addStatement( - "is %T -> %T(%L) to %T", - modifierType, - Protocol.ModifierTag, - modifier.tag, - localSchema.modifierSerializer(modifier, schema), - ) - } - } + addStatement( + "is %T -> %M", + modifierType, + localSchema.modifierTagAndSerializer(modifier, schema), + ) } } .addStatement("else -> throw %T()", Stdlib.AssertionError) @@ -736,6 +748,6 @@ private fun Schema.protocolWidgetType(widget: Widget, host: Schema): ClassName { return ClassName(guestProtocolPackage(host), "Protocol${widget.type.flatName}") } -private fun Schema.modifierSerializer(modifier: Modifier, host: Schema): ClassName { - return ClassName(guestProtocolPackage(host), modifier.type.flatName + "Serializer") +private fun Schema.modifierTagAndSerializer(modifier: Modifier, host: Schema): MemberName { + return MemberName(guestProtocolPackage(host), modifier.type.flatName + "TagAndSerializer") } From e4aef176d4ec6f2bc5e726a5dd543775f7820493 Mon Sep 17 00:00:00 2001 From: Jesse Wilson Date: Mon, 26 Aug 2024 15:32:15 -0400 Subject: [PATCH 7/8] Don't encode ',null' unnecessarily --- .../app/cash/redwood/treehouse/ProtocolBridgeJs.kt | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/redwood-treehouse-guest/src/jsMain/kotlin/app/cash/redwood/treehouse/ProtocolBridgeJs.kt b/redwood-treehouse-guest/src/jsMain/kotlin/app/cash/redwood/treehouse/ProtocolBridgeJs.kt index 916e73129b..8961fcea84 100644 --- a/redwood-treehouse-guest/src/jsMain/kotlin/app/cash/redwood/treehouse/ProtocolBridgeJs.kt +++ b/redwood-treehouse-guest/src/jsMain/kotlin/app/cash/redwood/treehouse/ProtocolBridgeJs.kt @@ -143,11 +143,15 @@ internal class FastGuestProtocolAdapter( value.forEach { element -> val (tag, serializer) = widgetSystemFactory.modifierTagAndSerializationStrategy(element) - val value = when { - serializer == null -> null - else -> json.encodeToDynamic(serializer, element) + when { + serializer != null -> { + val value = json.encodeToDynamic(serializer, element) + elements.push(js("""[tag,value]""")) + } + else -> { + elements.push(js("""[tag]""")) + } } - elements.push(js("""[tag,value]""")) } val id = id From cedf07f15a615f63cd9c49d846e637f002fa12e7 Mon Sep 17 00:00:00 2001 From: Jesse Wilson Date: Mon, 26 Aug 2024 17:47:50 -0400 Subject: [PATCH 8/8] apiDump --- redwood-protocol-guest/api/redwood-protocol-guest.api | 3 +-- redwood-protocol-guest/api/redwood-protocol-guest.klib.api | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/redwood-protocol-guest/api/redwood-protocol-guest.api b/redwood-protocol-guest/api/redwood-protocol-guest.api index 87f08c83cc..4a14797de5 100644 --- a/redwood-protocol-guest/api/redwood-protocol-guest.api +++ b/redwood-protocol-guest/api/redwood-protocol-guest.api @@ -81,8 +81,7 @@ public final class app/cash/redwood/protocol/guest/ProtocolWidgetChildren : app/ public abstract interface class app/cash/redwood/protocol/guest/ProtocolWidgetSystemFactory { public abstract fun create (Lapp/cash/redwood/protocol/guest/GuestProtocolAdapter;Lapp/cash/redwood/protocol/guest/ProtocolMismatchHandler;)Lapp/cash/redwood/widget/WidgetSystem; public static synthetic fun create$default (Lapp/cash/redwood/protocol/guest/ProtocolWidgetSystemFactory;Lapp/cash/redwood/protocol/guest/GuestProtocolAdapter;Lapp/cash/redwood/protocol/guest/ProtocolMismatchHandler;ILjava/lang/Object;)Lapp/cash/redwood/widget/WidgetSystem; - public abstract fun modifierSerializer (Lapp/cash/redwood/Modifier$Element;)Lkotlinx/serialization/KSerializer; - public abstract fun modifierTag-54iDFIs (Lapp/cash/redwood/Modifier$Element;)I + public abstract fun modifierTagAndSerializationStrategy (Lapp/cash/redwood/Modifier$Element;)Lkotlin/Pair; } public final class app/cash/redwood/protocol/guest/VersionKt { diff --git a/redwood-protocol-guest/api/redwood-protocol-guest.klib.api b/redwood-protocol-guest/api/redwood-protocol-guest.klib.api index db4f78588b..257fa3136f 100644 --- a/redwood-protocol-guest/api/redwood-protocol-guest.klib.api +++ b/redwood-protocol-guest/api/redwood-protocol-guest.klib.api @@ -53,9 +53,8 @@ abstract interface app.cash.redwood.protocol.guest/ProtocolWidget : app.cash.red } abstract interface app.cash.redwood.protocol.guest/ProtocolWidgetSystemFactory { // app.cash.redwood.protocol.guest/ProtocolWidgetSystemFactory|null[0] - abstract fun <#A1: app.cash.redwood/Modifier.Element> modifierSerializer(#A1): kotlinx.serialization/KSerializer<#A1>? // app.cash.redwood.protocol.guest/ProtocolWidgetSystemFactory.modifierSerializer|modifierSerializer(0:0){0§}[0] + abstract fun <#A1: app.cash.redwood/Modifier.Element> modifierTagAndSerializationStrategy(#A1): kotlin/Pair?> // app.cash.redwood.protocol.guest/ProtocolWidgetSystemFactory.modifierTagAndSerializationStrategy|modifierTagAndSerializationStrategy(0:0){0§}[0] abstract fun create(app.cash.redwood.protocol.guest/GuestProtocolAdapter, app.cash.redwood.protocol.guest/ProtocolMismatchHandler = ...): app.cash.redwood.widget/WidgetSystem // app.cash.redwood.protocol.guest/ProtocolWidgetSystemFactory.create|create(app.cash.redwood.protocol.guest.GuestProtocolAdapter;app.cash.redwood.protocol.guest.ProtocolMismatchHandler){}[0] - abstract fun modifierTag(app.cash.redwood/Modifier.Element): app.cash.redwood.protocol/ModifierTag // app.cash.redwood.protocol.guest/ProtocolWidgetSystemFactory.modifierTag|modifierTag(app.cash.redwood.Modifier.Element){}[0] } final class app.cash.redwood.protocol.guest/DefaultGuestProtocolAdapter : app.cash.redwood.protocol.guest/GuestProtocolAdapter { // app.cash.redwood.protocol.guest/DefaultGuestProtocolAdapter|null[0]