From e6173755ad705b1883a553018cf4949c34430e5c Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Fri, 24 Jul 2026 15:00:55 -0400 Subject: [PATCH 1/3] Drop the now-unused modifier-args JSON encoder --- Sources/ComposeUI/Materializer.swift | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/Sources/ComposeUI/Materializer.swift b/Sources/ComposeUI/Materializer.swift index b94d2b2..a1663d7 100644 --- a/Sources/ComposeUI/Materializer.swift +++ b/Sources/ComposeUI/Materializer.swift @@ -86,7 +86,9 @@ public enum Materializer { ) } - /// Encodes a prop value as a JSON literal (`"text"`, `42`, `true`, `[…]`). + /// Encodes an array-valued prop as a JSON literal (`["a",1,true]`), the one + /// value shape the typed slots don't carry. Scalars never reach this — they + /// cross typed — so the only recursion is through nested array elements. static func jsonLiteral(_ value: PropValue) -> String { switch value { case .string(let string): @@ -102,11 +104,6 @@ public enum Materializer { } } - static func jsonObjectLiteral(_ args: [String: PropValue]) -> String { - let members = args.map { "\(escapeJSON($0.key)):\(jsonLiteral($0.value))" } - return "{" + members.joined(separator: ",") + "}" - } - /// Minimal JSON string escaping (quotes, backslashes, control characters). static func escapeJSON(_ string: String) -> String { var out = "\"" From d1b5459cf21ab2d302fbf476da8f694402f51f3f Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Fri, 24 Jul 2026 15:18:53 -0400 Subject: [PATCH 2/3] Rebuild homogeneous arrays from typed pools instead of JSON --- .../kotlin/com/pureswift/swiftui/ViewNode.kt | 53 ++++++++++++++----- 1 file changed, 41 insertions(+), 12 deletions(-) diff --git a/composeui/src/commonMain/kotlin/com/pureswift/swiftui/ViewNode.kt b/composeui/src/commonMain/kotlin/com/pureswift/swiftui/ViewNode.kt index d396c88..af822dd 100644 --- a/composeui/src/commonMain/kotlin/com/pureswift/swiftui/ViewNode.kt +++ b/composeui/src/commonMain/kotlin/com/pureswift/swiftui/ViewNode.kt @@ -3,6 +3,7 @@ package com.pureswift.swiftui import androidx.compose.runtime.Immutable import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonArray import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.booleanOrNull @@ -43,12 +44,15 @@ data class ViewNode( /// Bridge constructor: the Swift materializer builds nodes through this, /// crossing JNI with flat arrays (one call per node, arrays as single - /// arguments). Scalar values cross typed — a kind tag selects between the - /// string slot and the bits slot (doubles bit-cast into the long, so both - /// 64-bit callback ids and doubles cross exactly) — so no value takes the - /// JSON parser except array-valued props. Modifier args flatten into one - /// run of slots, `modifierArgCounts` giving each modifier's share. - /// Negative count/provider mean "absent". + /// arguments). Every value crosses typed — a kind tag selects the string + /// slot or the bits slot (doubles bit-cast into the long, so both 64-bit + /// callback ids and doubles cross exactly). Homogeneous arrays ride the + /// same slot: the kind marks the element type and the bits slot packs + /// (offset << 32 | count) into the node's shared string or long pool. Only + /// a heterogeneous/nested array (alert `buttons`, `searches`) still crosses + /// as a JSON literal. Modifier args flatten into one run of slots, + /// `modifierArgCounts` giving each modifier's share. Negative count/provider + /// mean "absent". constructor( type: String, id: String, @@ -62,6 +66,8 @@ data class ViewNode( argKinds: IntArray, argStrings: Array, argBits: LongArray, + stringPool: Array, + longPool: LongArray, children: Array, count: Int, itemProviderId: Long, @@ -69,7 +75,7 @@ data class ViewNode( type = type, id = id, props = JsonObject(propKeys.indices.associate { - propKeys[it] to jsonValue(propKinds[it], propStrings[it], propBits[it]) + propKeys[it] to jsonValue(propKinds[it], propStrings[it], propBits[it], stringPool, longPool) }), modifiers = buildList { var base = 0 @@ -77,7 +83,7 @@ data class ViewNode( val argCount = modifierArgCounts[m] add(ModifierNode(modifierKinds[m], JsonObject((0 until argCount).associate { val i = base + it - argKeys[i] to jsonValue(argKinds[i], argStrings[i], argBits[i]) + argKeys[i] to jsonValue(argKinds[i], argStrings[i], argBits[i], stringPool, longPool) }))) base += argCount } @@ -88,20 +94,43 @@ data class ViewNode( ) companion object { - // value kinds used by the bridge constructor + // scalar value kinds private const val KIND_STRING = 0 private const val KIND_DOUBLE = 1 private const val KIND_BOOL = 2 private const val KIND_INT = 3 private const val KIND_JSON = 4 - - private fun jsonValue(kind: Int, string: String, bits: Long): kotlinx.serialization.json.JsonElement = + // homogeneous-array kinds: bits packs (offset << 32 | count) into a pool + private const val KIND_STRING_ARRAY = 5 + private const val KIND_DOUBLE_ARRAY = 6 + private const val KIND_BOOL_ARRAY = 7 + private const val KIND_INT_ARRAY = 8 + + private fun jsonValue( + kind: Int, + string: String, + bits: Long, + stringPool: Array, + longPool: LongArray, + ): kotlinx.serialization.json.JsonElement = when (kind) { KIND_STRING -> JsonPrimitive(string) KIND_DOUBLE -> JsonPrimitive(Double.fromBits(bits)) KIND_BOOL -> JsonPrimitive(bits != 0L) KIND_INT -> JsonPrimitive(bits) - else -> Json.parseToJsonElement(string) // arrays only + KIND_JSON -> Json.parseToJsonElement(string) // nested/mixed arrays only + else -> { + val offset = (bits ushr 32).toInt() + val count = (bits and 0xFFFFFFFFL).toInt() + JsonArray((0 until count).map { i -> + when (kind) { + KIND_STRING_ARRAY -> JsonPrimitive(stringPool[offset + i]) + KIND_DOUBLE_ARRAY -> JsonPrimitive(Double.fromBits(longPool[offset + i])) + KIND_BOOL_ARRAY -> JsonPrimitive(longPool[offset + i] != 0L) + else -> JsonPrimitive(longPool[offset + i]) // KIND_INT_ARRAY + } + }) + } } } From 1425c9884cc8b01ac1b4b5d46e3319c877d514ed Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Fri, 24 Jul 2026 15:18:53 -0400 Subject: [PATCH 3/3] Materialize homogeneous arrays into shared typed pools --- Sources/ComposeUI/KotlinBindings.swift | 10 +++- Sources/ComposeUI/Materializer.swift | 81 +++++++++++++++++++++++--- 2 files changed, 79 insertions(+), 12 deletions(-) diff --git a/Sources/ComposeUI/KotlinBindings.swift b/Sources/ComposeUI/KotlinBindings.swift index 5021723..3540874 100644 --- a/Sources/ComposeUI/KotlinBindings.swift +++ b/Sources/ComposeUI/KotlinBindings.swift @@ -14,9 +14,11 @@ import SwiftJava open class ViewNodeObject: JavaObject { /// The bridge constructor: one JNI call per node, arrays crossing as - /// single arguments. Scalars cross typed — a kind tag per value selects - /// the string slot or the bits slot (doubles bit-cast into the long) — - /// so only array-valued props take the JSON path. Negative count/provider + /// single arguments. Every value crosses typed — a kind tag per value + /// selects the string slot or the bits slot (doubles bit-cast into the + /// long). Homogeneous arrays ride the same slot with the bits packing + /// (offset << 32 | count) into the node's shared string or long pool; only + /// nested/heterogeneous arrays take the JSON path. Negative count/provider /// mean "absent". @JavaMethod @_nonoverride public convenience init( @@ -32,6 +34,8 @@ open class ViewNodeObject: JavaObject { _ argKinds: [Int32], _ argStrings: [String], _ argBits: [Int64], + _ stringPool: [String], + _ longPool: [Int64], _ children: [ViewNodeObject?], _ count: Int32, _ itemProviderId: Int64, diff --git a/Sources/ComposeUI/Materializer.swift b/Sources/ComposeUI/Materializer.swift index a1663d7..013468c 100644 --- a/Sources/ComposeUI/Materializer.swift +++ b/Sources/ComposeUI/Materializer.swift @@ -11,17 +11,37 @@ import SwiftJava public enum Materializer { - // value kinds understood by the Kotlin bridge constructor + // scalar value kinds understood by the Kotlin bridge constructor private static let kindString: Int32 = 0 private static let kindDouble: Int32 = 1 private static let kindBool: Int32 = 2 private static let kindInt: Int32 = 3 private static let kindJSON: Int32 = 4 + // homogeneous-array kinds: the bits slot packs (offset << 32 | count) into + // the node's shared string or long pool + private static let kindStringArray: Int32 = 5 + private static let kindDoubleArray: Int32 = 6 + private static let kindBoolArray: Int32 = 7 + private static let kindIntArray: Int32 = 8 - /// One scalar's typed slots: strings ride the string slot; numbers and - /// bools ride the bits slot (doubles bit-cast, so 64-bit callback ids and - /// doubles both cross exactly); only arrays fall back to JSON text. - private static func slots(for value: PropValue) -> (kind: Int32, string: String, bits: Int64) { + /// One node's array pools, filled while its values are encoded and handed + /// to the constructor. Homogeneous arrays append their elements here and + /// carry a packed (offset, count) reference in place of a JSON string. + private struct Pools { + var strings: [String] = [] + var longs: [Int64] = [] + } + + private static func pack(offset: Int, count: Int) -> Int64 { + (Int64(offset) << 32) | Int64(count) + } + + /// One value's typed slots: strings ride the string slot; numbers and bools + /// ride the bits slot (doubles bit-cast, so 64-bit callback ids and doubles + /// both cross exactly); a homogeneous array appends to a pool and packs its + /// (offset, count) into the bits slot; only a nested/mixed array falls back + /// to JSON text. + private static func slots(for value: PropValue, pools: inout Pools) -> (kind: Int32, string: String, bits: Int64) { switch value { case .string(let string): return (kindString, string, 0) @@ -31,19 +51,60 @@ public enum Materializer { return (kindBool, "", bool ? 1 : 0) case .int(let int): return (kindInt, "", Int64(int)) - case .array: - return (kindJSON, jsonLiteral(value), 0) + case .array(let elements): + return arraySlot(elements, value: value, pools: &pools) + } + } + + /// Routes an array to a typed pool when its elements share one scalar kind; + /// otherwise (nested or mixed — alert `buttons`, `searches`) to JSON. + private static func arraySlot( + _ elements: [PropValue], + value: PropValue, + pools: inout Pools + ) -> (kind: Int32, string: String, bits: Int64) { + func mapAll(_ transform: (PropValue) -> T?) -> [T]? { + var out: [T] = [] + out.reserveCapacity(elements.count) + for element in elements { + guard let mapped = transform(element) else { return nil } + out.append(mapped) + } + return out + } + guard !elements.isEmpty else { return (kindJSON, "[]", 0) } + if let strings = mapAll({ if case let .string(s) = $0 { s } else { nil } }) { + let bits = pack(offset: pools.strings.count, count: strings.count) + pools.strings.append(contentsOf: strings) + return (kindStringArray, "", bits) + } + if let ints = mapAll({ if case let .int(i) = $0 { Int64(i) } else { nil } }) { + let bits = pack(offset: pools.longs.count, count: ints.count) + pools.longs.append(contentsOf: ints) + return (kindIntArray, "", bits) + } + if let doubles = mapAll({ if case let .double(d) = $0 { Int64(bitPattern: d.bitPattern) } else { nil } }) { + let bits = pack(offset: pools.longs.count, count: doubles.count) + pools.longs.append(contentsOf: doubles) + return (kindDoubleArray, "", bits) + } + if let bools = mapAll({ if case let .bool(b) = $0 { Int64(b ? 1 : 0) } else { nil } }) { + let bits = pack(offset: pools.longs.count, count: bools.count) + pools.longs.append(contentsOf: bools) + return (kindBoolArray, "", bits) } + return (kindJSON, jsonLiteral(value), 0) } /// Builds the Kotlin mirror of an IR tree, depth-first. public static func materialize(_ node: RenderNode) -> ViewNodeObject { + var pools = Pools() var propKeys: [String] = [] var propKinds: [Int32] = [] var propStrings: [String] = [] var propBits: [Int64] = [] for (key, value) in node.props { - let slot = slots(for: value) + let slot = slots(for: value, pools: &pools) propKeys.append(key) propKinds.append(slot.kind) propStrings.append(slot.string) @@ -59,7 +120,7 @@ public enum Materializer { modifierKinds.append(modifier.kind) modifierArgCounts.append(Int32(modifier.args.count)) for (key, value) in modifier.args { - let slot = slots(for: value) + let slot = slots(for: value, pools: &pools) argKeys.append(key) argKinds.append(slot.kind) argStrings.append(slot.string) @@ -80,6 +141,8 @@ public enum Materializer { argKinds, argStrings, argBits, + pools.strings, + pools.longs, children, Int32(node.count ?? -1), Int64(-1)