From 461eda0f7d4ee066e4bcf52be9551e522d25aff3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mark=20Hu=CC=88sers?= Date: Fri, 15 Mar 2024 17:18:26 +0100 Subject: [PATCH] feature(VssProcessor): Add dataType to VssSignal VssNodeSpecModel: - Adds now an experimental annotation for some data types so warnings are properly ignored. - The generation code of the VssSignal now iterates through the member properties of the interface and generates the correct implementation. The VssHeartRate for integration tests is currently the only VssSignal which has a different dataType (UInt) than the value type (Int). --- .../databroker/JavaDataBrokerEngine.java | 2 +- .../kuksa/extension/AnyCopyExtension.kt | 16 +- .../kuksa/extension/DataPointExtension.kt | 8 +- .../extension/vss/VssNodeCopyExtension.kt | 15 +- .../org/eclipse/kuksa/vssNode/VssDriver.kt | 3 + .../eclipse/kuksa/vssNode/VssNodeCopyTest.kt | 6 +- .../org/eclipse/kuksa/vssNode/VssVehicle.kt | 3 + .../java/com/example/sample/JavaActivity.java | 2 +- .../eclipse/kuksa/vsscore/model/VssNode.kt | 24 ++- .../vssprocessor/spec/VssNodeSpecModel.kt | 198 ++++++++++++------ 10 files changed, 189 insertions(+), 88 deletions(-) diff --git a/app/src/main/java/org/eclipse/kuksa/testapp/databroker/JavaDataBrokerEngine.java b/app/src/main/java/org/eclipse/kuksa/testapp/databroker/JavaDataBrokerEngine.java index 920d0098..0c17f17b 100644 --- a/app/src/main/java/org/eclipse/kuksa/testapp/databroker/JavaDataBrokerEngine.java +++ b/app/src/main/java/org/eclipse/kuksa/testapp/databroker/JavaDataBrokerEngine.java @@ -26,8 +26,8 @@ import org.eclipse.kuksa.connectivity.databroker.DataBrokerConnection; import org.eclipse.kuksa.connectivity.databroker.DataBrokerConnector; import org.eclipse.kuksa.connectivity.databroker.listener.DisconnectListener; -import org.eclipse.kuksa.connectivity.databroker.listener.VssPathListener; import org.eclipse.kuksa.connectivity.databroker.listener.VssNodeListener; +import org.eclipse.kuksa.connectivity.databroker.listener.VssPathListener; import org.eclipse.kuksa.connectivity.databroker.request.FetchRequest; import org.eclipse.kuksa.connectivity.databroker.request.SubscribeRequest; import org.eclipse.kuksa.connectivity.databroker.request.UpdateRequest; diff --git a/kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/extension/AnyCopyExtension.kt b/kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/extension/AnyCopyExtension.kt index 8fb824a7..b6ea635f 100644 --- a/kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/extension/AnyCopyExtension.kt +++ b/kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/extension/AnyCopyExtension.kt @@ -19,9 +19,9 @@ package org.eclipse.kuksa.extension -import kotlin.reflect.KParameter import kotlin.reflect.full.instanceParameter import kotlin.reflect.full.memberFunctions +import kotlin.reflect.full.valueParameters /** * Uses reflection to create a copy with any constructor parameter which matches the given [paramToValue] map. @@ -37,16 +37,20 @@ internal fun T.copy(paramToValue: Map = emptyMap()): T { val copyFunction = instanceClass::memberFunctions.get().first { it.name == "copy" } val instanceParameter = copyFunction.instanceParameter ?: return this - val valueArgs = copyFunction.parameters - .filter { parameter -> - parameter.kind == KParameter.Kind.VALUE - }.mapNotNull { parameter -> + val valueArgs = copyFunction.valueParameters + .mapNotNull { parameter -> paramToValue[parameter.name]?.let { value -> parameter to value } } val parameterToInstance = mapOf(instanceParameter to this) val parameterToValue = parameterToInstance + valueArgs - val copy = copyFunction.callBy(parameterToValue) ?: this + + val copy: Any + try { + copy = copyFunction.callBy(parameterToValue) ?: this + } catch (e: IllegalArgumentException) { + throw IllegalArgumentException("${this::class.simpleName} copy parameters do not match: $paramToValue", e) + } return copy as T } diff --git a/kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/extension/DataPointExtension.kt b/kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/extension/DataPointExtension.kt index 8254d28d..7bea7313 100644 --- a/kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/extension/DataPointExtension.kt +++ b/kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/extension/DataPointExtension.kt @@ -35,14 +35,16 @@ val Types.Metadata.valueType: ValueCase get() = dataType.dataPointValueCase /** - * Converts the [VssSignal.value] into a [Datapoint] object. + * Converts the [VssSignal.value] into a [Datapoint] object. The [VssSignal.dataType] is used to derive the correct + * [ValueCase]. * * @throws IllegalArgumentException if the [VssSignal] could not be converted to a [Datapoint]. */ +@OptIn(ExperimentalUnsignedTypes::class) val VssSignal.datapoint: Datapoint get() { val stringValue = value.toString() - return when (value::class) { + return when (dataType) { String::class -> ValueCase.STRING.createDatapoint(stringValue) Boolean::class -> ValueCase.BOOL.createDatapoint(stringValue) Float::class -> ValueCase.FLOAT.createDatapoint(stringValue) @@ -54,6 +56,8 @@ val VssSignal.datapoint: Datapoint IntArray::class -> ValueCase.INT32_ARRAY.createDatapoint(stringValue) BooleanArray::class -> ValueCase.BOOL_ARRAY.createDatapoint(stringValue) LongArray::class -> ValueCase.INT64_ARRAY.createDatapoint(stringValue) + FloatArray::class -> ValueCase.FLOAT_ARRAY.createDatapoint(stringValue) + UIntArray::class -> ValueCase.UINT32_ARRAY.createDatapoint(stringValue) else -> throw IllegalArgumentException("Could not create datapoint for the value class: ${value::class}!") } diff --git a/kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/extension/vss/VssNodeCopyExtension.kt b/kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/extension/vss/VssNodeCopyExtension.kt index 3b668bf5..f3700021 100644 --- a/kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/extension/vss/VssNodeCopyExtension.kt +++ b/kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/extension/vss/VssNodeCopyExtension.kt @@ -27,7 +27,7 @@ import org.eclipse.kuksa.vsscore.model.VssSignal import org.eclipse.kuksa.vsscore.model.findHeritageLine import org.eclipse.kuksa.vsscore.model.heritage import org.eclipse.kuksa.vsscore.model.variableName -import kotlin.reflect.full.memberProperties +import kotlin.reflect.full.declaredMemberProperties /** * Creates a copy of the [VssNode] where the whole [VssNode.findHeritageLine] is replaced @@ -92,8 +92,8 @@ fun VssSignal.copy(datapoint: Datapoint): VssSignal { BOOL -> bool INT32 -> int32 INT64 -> int64 - UINT32 -> uint32.toUInt() - UINT64 -> uint64.toULong() + UINT32 -> uint32 + UINT64 -> uint64 FLOAT -> float DOUBLE -> double STRING_ARRAY -> stringArray.valuesList @@ -136,11 +136,12 @@ fun VssSignal.copy(datapoint: Datapoint): VssSignal { * Calls the generated copy method of the data class for the [VssSignal] and returns a new copy with the new [value]. * * @throws [IllegalArgumentException] if the copied types do not match. - * @throws [NoSuchElementException] if no copy method was found for the class. + * @throws [NoSuchElementException] if no copy method nor [valuePropertyName] was found for the class. */ -fun VssSignal.copy(value: T): VssSignal { - val memberProperties = VssSignal::class.memberProperties - val firstPropertyName = memberProperties.first().name +@JvmOverloads +fun VssSignal.copy(value: T, valuePropertyName: String = "value"): VssSignal { + val memberProperties = VssSignal::class.declaredMemberProperties + val firstPropertyName = memberProperties.first { it.name == valuePropertyName }.name val valueMap = mapOf(firstPropertyName to value) return this@copy.copy(valueMap) diff --git a/kuksa-sdk/src/test/kotlin/org/eclipse/kuksa/vssNode/VssDriver.kt b/kuksa-sdk/src/test/kotlin/org/eclipse/kuksa/vssNode/VssDriver.kt index d81fc36f..86e45109 100644 --- a/kuksa-sdk/src/test/kotlin/org/eclipse/kuksa/vssNode/VssDriver.kt +++ b/kuksa-sdk/src/test/kotlin/org/eclipse/kuksa/vssNode/VssDriver.kt @@ -64,6 +64,9 @@ data class VssDriver @JvmOverloads constructor( data class VssHeartRate @JvmOverloads constructor( override val `value`: Int = 0, ) : VssSignal { + override val dataType: KClass<*> + get() = UInt::class + override val comment: String get() = "" diff --git a/kuksa-sdk/src/test/kotlin/org/eclipse/kuksa/vssNode/VssNodeCopyTest.kt b/kuksa-sdk/src/test/kotlin/org/eclipse/kuksa/vssNode/VssNodeCopyTest.kt index 8fdc51ee..325cc442 100644 --- a/kuksa-sdk/src/test/kotlin/org/eclipse/kuksa/vssNode/VssNodeCopyTest.kt +++ b/kuksa-sdk/src/test/kotlin/org/eclipse/kuksa/vssNode/VssNodeCopyTest.kt @@ -78,8 +78,7 @@ class VssNodeCopyTest : BehaviorSpec({ } and("a changed invalid DataPoint") { - val newValue = 50 - val datapoint = Types.Datapoint.newBuilder().setUint32(newValue).build() + val datapoint = Types.Datapoint.newBuilder().setBool(false).build() `when`("a copy is done") { val exception = shouldThrow { @@ -87,7 +86,8 @@ class VssNodeCopyTest : BehaviorSpec({ } then("it should throw an IllegalArgumentException") { - exception.message shouldStartWith "argument type mismatch" + val signalName = driverHeartRate::class.simpleName + exception.message shouldStartWith "$signalName copy parameters do not match" } } } diff --git a/kuksa-sdk/src/test/kotlin/org/eclipse/kuksa/vssNode/VssVehicle.kt b/kuksa-sdk/src/test/kotlin/org/eclipse/kuksa/vssNode/VssVehicle.kt index ff1cad8a..a9faf7a3 100644 --- a/kuksa-sdk/src/test/kotlin/org/eclipse/kuksa/vssNode/VssVehicle.kt +++ b/kuksa-sdk/src/test/kotlin/org/eclipse/kuksa/vssNode/VssVehicle.kt @@ -69,6 +69,9 @@ data class VssPassenger( override val comment: String = "", override val value: Int = 80, ) : VssSignal { + override val dataType: KClass<*> + get() = UInt::class + override val parentClass: KClass<*> get() = VssPassenger::class } diff --git a/samples/src/main/java/com/example/sample/JavaActivity.java b/samples/src/main/java/com/example/sample/JavaActivity.java index 1271330f..87b54bd4 100644 --- a/samples/src/main/java/com/example/sample/JavaActivity.java +++ b/samples/src/main/java/com/example/sample/JavaActivity.java @@ -26,8 +26,8 @@ import org.eclipse.kuksa.connectivity.databroker.DataBrokerConnection; import org.eclipse.kuksa.connectivity.databroker.DataBrokerConnector; import org.eclipse.kuksa.connectivity.databroker.listener.DisconnectListener; -import org.eclipse.kuksa.connectivity.databroker.listener.VssPathListener; import org.eclipse.kuksa.connectivity.databroker.listener.VssNodeListener; +import org.eclipse.kuksa.connectivity.databroker.listener.VssPathListener; import org.eclipse.kuksa.connectivity.databroker.request.FetchRequest; import org.eclipse.kuksa.connectivity.databroker.request.SubscribeRequest; import org.eclipse.kuksa.connectivity.databroker.request.UpdateRequest; diff --git a/vss-core/src/main/kotlin/org/eclipse/kuksa/vsscore/model/VssNode.kt b/vss-core/src/main/kotlin/org/eclipse/kuksa/vsscore/model/VssNode.kt index b613f616..e4959bad 100644 --- a/vss-core/src/main/kotlin/org/eclipse/kuksa/vsscore/model/VssNode.kt +++ b/vss-core/src/main/kotlin/org/eclipse/kuksa/vsscore/model/VssNode.kt @@ -82,11 +82,33 @@ interface VssBranch : VssNode { * Some [VssNode] may have an additional [value] property. These are children [VssSignal] which do not have other * children. */ -interface VssSignal : VssNode { +interface VssSignal : VssNode { /** * A primitive type value. */ val value: T + + /** + * The VSS data type which is compatible with the data broker. This may differ from the [value] type because + * Java compatibility needs to be ensured and inline classes like [UInt] (Kotlin) are not known to Java. + * + * ### Example + * Vehicle.Driver.HeartRate: + * datatype: uint16 + * + * generates --> + * + * public data class VssHeartRate ( + * override val `value`: Int = 0, + * ) : VssSignal { + * override val dataType: KClass<*> + * get() = UInt:class + * } + * + * To ensure java compatibility [UInt] is not used here for Kotlin (inline class). + */ + val dataType: KClass<*> + get() = value::class } /** diff --git a/vss-processor/src/main/kotlin/org/eclipse/kuksa/vssprocessor/spec/VssNodeSpecModel.kt b/vss-processor/src/main/kotlin/org/eclipse/kuksa/vssprocessor/spec/VssNodeSpecModel.kt index bc99bfec..555fb3a4 100644 --- a/vss-processor/src/main/kotlin/org/eclipse/kuksa/vssprocessor/spec/VssNodeSpecModel.kt +++ b/vss-processor/src/main/kotlin/org/eclipse/kuksa/vssprocessor/spec/VssNodeSpecModel.kt @@ -20,10 +20,12 @@ package org.eclipse.kuksa.vssprocessor.spec import com.google.devtools.ksp.processing.KSPLogger +import com.squareup.kotlinpoet.AnnotationSpec import com.squareup.kotlinpoet.ClassName import com.squareup.kotlinpoet.FunSpec import com.squareup.kotlinpoet.KModifier import com.squareup.kotlinpoet.ParameterSpec +import com.squareup.kotlinpoet.ParameterizedTypeName import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.plusParameter import com.squareup.kotlinpoet.PropertySpec @@ -58,9 +60,11 @@ internal class VssNodeSpecModel( private val stringTypeName = String::class.asTypeName() private val vssNodeSetTypeName = Set::class.parameterizedBy(VssNode::class) - private val genericClassTypeName = KClass::class.asClassName().parameterizedBy(STAR).copy(nullable = true) + private val genericClassTypeName = KClass::class.asClassName().parameterizedBy(STAR) + private val genericClassTypeNameNullable = KClass::class.asClassName().parameterizedBy(STAR).copy(nullable = true) - private val datatypeProperty: TypeName + @OptIn(ExperimentalUnsignedTypes::class) + private val datatypeTypeName: TypeName get() { return when (datatype) { "string" -> String::class.asTypeName() @@ -73,19 +77,33 @@ internal class VssNodeSpecModel( "double" -> Double::class.asTypeName() "string[]" -> Array::class.parameterizedBy(String::class) "boolean[]" -> BooleanArray::class.asTypeName() - "uint8[]", "uint16[]", "uint32[]", "int8[]", "int16[]", "int32[]" -> IntArray::class.asTypeName() + "int8[]", "int16[]", "int32[]" -> IntArray::class.asTypeName() + "uint8[]", "uint16[]", "uint32[]" -> UIntArray::class.asTypeName() "int64[]", "uint64[]" -> LongArray::class.asTypeName() "float[]" -> FloatArray::class.asTypeName() else -> Any::class.asTypeName() } } + @OptIn(ExperimentalUnsignedTypes::class) + private val valueTypeName: TypeName + get() { + return when (datatypeTypeName) { + // Convert the following Kotlin types because they are incompatible with the @JvmOverloads annotation + UInt::class.asTypeName() -> Int::class.asTypeName() + ULong::class.asTypeName() -> Long::class.asTypeName() + UIntArray::class.asTypeName() -> IntArray::class.asTypeName() + else -> datatypeTypeName + } + } + /** * Returns valid default values as string literals. */ + @OptIn(ExperimentalUnsignedTypes::class) private val defaultValue: String get() { - return when (datatypeProperty) { + return when (valueTypeName) { String::class.asTypeName() -> "\"\"" Boolean::class.asTypeName() -> "false" Float::class.asTypeName() -> "0f" @@ -97,8 +115,10 @@ internal class VssNodeSpecModel( IntArray::class.asTypeName() -> "IntArray(0)" BooleanArray::class.asTypeName() -> "BooleanArray(0)" LongArray::class.asTypeName() -> "LongArray(0)" + UIntArray::class.asTypeName() -> "UIntArray(0)" + FloatArray::class.asTypeName() -> "FloatArray(0)" - else -> throw IllegalArgumentException("No default value found for $datatypeProperty!") + else -> throw IllegalArgumentException("No default value found for $valueTypeName!") } } @@ -113,22 +133,21 @@ internal class VssNodeSpecModel( val nestedChildSpecs = mutableListOf() val constructorBuilder = FunSpec.constructorBuilder() + .addAnnotation(JvmOverloads::class) val propertySpecs = mutableListOf() val superInterfaces = mutableSetOf(VssBranch::class.asTypeName()) // The last element in the chain should have a value like "isLocked". - if (childNodes.isEmpty()) { - val (valuePropertySpec, parameterSpec) = createValueSpec() - - constructorBuilder.addParameter(parameterSpec) - propertySpecs.add(valuePropertySpec) + val isVssSignal = childNodes.isEmpty() + if (isVssSignal) { + val (vssSignalTypeName, vssSignalPropertySpecs, vssSignalParameterSpec) = createVssSignalSpec() // Final leafs should ONLY implement the VssSignal interface superInterfaces.clear() - val vssSignalInterface = VssSignal::class - .asTypeName() - .plusParameter(datatypeProperty) - superInterfaces.add(vssSignalInterface) + superInterfaces.add(vssSignalTypeName) + + propertySpecs.addAll(vssSignalPropertySpecs) + vssSignalParameterSpec?.let { constructorBuilder.addParameter(it) } } val propertySpec = createVssNodeSpecs(className, packageName = packageName) @@ -162,11 +181,10 @@ internal class VssNodeSpecModel( } val defaultClassName = childNode.className - val defaultParameter = createDefaultParameterSpec( - mainClassPropertySpec.name, - defaultClassName, - uniquePackageName, - ) + val defaultParameter = ParameterSpec + .builder(mainClassPropertySpec.name, ClassName(uniquePackageName, defaultClassName)) + .defaultValue("%L()", defaultClassName) + .build() constructorBuilder.addParameter(defaultParameter) } @@ -186,31 +204,62 @@ internal class VssNodeSpecModel( .build() } - private fun createValueSpec(): Pair { - val valuePropertySpec = PropertySpec - .builder(PROPERTY_VALUE_NAME, datatypeProperty) - .initializer(PROPERTY_VALUE_NAME) + private fun createVssSignalSpec(): Triple, ParameterSpec?> { + val propertySpecs = mutableListOf() + var parameterSpec: ParameterSpec? = null + + val vssSignalMembers = VssSignal::class.declaredMemberProperties + vssSignalMembers.forEach { member -> + val memberName = member.name + when (val memberType = member.returnType.asTypeName()) { + genericClassTypeName -> { + val genericClassSpec = createGenericClassSpec( + memberName, + memberType, + datatypeTypeName.toString(), + ) + propertySpecs.add(genericClassSpec) + } + + else -> { + val (classPropertySpec, classParameterSpec) = createClassParamSpec( + memberName, + valueTypeName, + defaultValue, + ) + parameterSpec = classParameterSpec + propertySpecs.add(classPropertySpec) + } + } + } + + val typeName = VssSignal::class + .asTypeName() + .plusParameter(valueTypeName) + + return Triple(typeName, propertySpecs, parameterSpec) + } + + private fun createClassParamSpec( + memberName: String, + typeName: TypeName, + defaultValue: String, + ): Pair { + val propertySpec = PropertySpec + .builder(memberName, typeName) + .initializer(memberName) .addModifiers(KModifier.OVERRIDE) .build() // Adds a default value (mainly 0 or an empty string) val parameterSpec = ParameterSpec.builder( - valuePropertySpec.name, - valuePropertySpec.type, + propertySpec.name, + propertySpec.type, ).defaultValue("%L", defaultValue).build() - return Pair(valuePropertySpec, parameterSpec) + return Pair(propertySpec, parameterSpec) } - private fun createDefaultParameterSpec( - parameterName: String, - defaultClassName: String, - packageName: String, - ) = ParameterSpec - .builder(parameterName, ClassName(packageName, defaultClassName)) - .defaultValue("%L()", defaultClassName) - .build() - private fun createVssNodeSpecs( className: String, packageName: String, @@ -268,43 +317,14 @@ internal class VssNodeSpecModel( } private fun createVssNodeTreeSpecs(childNodes: List): List { - fun createSetSpec(memberName: String, memberType: TypeName): PropertySpec { - val vssNodeNamesJoined = childNodes.joinToString(", ") { it.variableName } - - return PropertySpec - .builder(memberName, memberType) - .mutable(false) - .addModifiers(KModifier.OVERRIDE) - .getter( - FunSpec.getterBuilder() - .addStatement("return setOf(%L)", vssNodeNamesJoined) - .build(), - ) - .build() - } - - fun createParentSpec(memberName: String, memberType: TypeName): PropertySpec { - val parentClass = if (parentClassName.isNotEmpty()) "$parentClassName::class" else "null" - return PropertySpec - .builder(memberName, memberType) - .mutable(false) - .addModifiers(KModifier.OVERRIDE) - .getter( - FunSpec.getterBuilder() - .addStatement("return %L", parentClass) - .build(), - ) - .build() - } - val propertySpecs = mutableListOf() val members = VssNode::class.declaredMemberProperties members.forEach { member -> val memberName = member.name when (val memberType = member.returnType.asTypeName()) { - vssNodeSetTypeName -> createSetSpec(memberName, memberType) - genericClassTypeName -> createParentSpec(memberName, memberType) + vssNodeSetTypeName -> createSetSpec(memberName, memberType, childNodes) + genericClassTypeNameNullable -> createGenericClassSpec(memberName, memberType, parentClassName) else -> null }?.let { propertySpec -> propertySpecs.add(propertySpec) @@ -314,6 +334,50 @@ internal class VssNodeSpecModel( return propertySpecs } + private fun createGenericClassSpec(memberName: String, memberType: TypeName, className: String): PropertySpec { + val parentClass = if (className.isNotEmpty()) "$className::class" else "null" + + val propertySpecBuilder = PropertySpec + .builder(memberName, memberType) + + // Removed the warning about ExperimentalUnsignedTypes + if (experimentalUnsignedTypes.contains(className)) { + val optInClassName = ClassName("kotlin", "OptIn") + val optInAnnotationSpec = AnnotationSpec.builder(optInClassName) + .addMember("ExperimentalUnsignedTypes::class") + .build() + + propertySpecBuilder.addAnnotation(optInAnnotationSpec) + } + + return propertySpecBuilder + .addModifiers(KModifier.OVERRIDE) + .getter( + FunSpec.getterBuilder() + .addStatement("return %L", parentClass) + .build(), + ) + .build() + } + + private fun createSetSpec( + memberName: String, + memberType: TypeName, + members: Collection, + ): PropertySpec { + val vssNodeNamesJoined = members.joinToString(", ") { it.variableName } + + return PropertySpec + .builder(memberName, memberType) + .addModifiers(KModifier.OVERRIDE) + .getter( + FunSpec.getterBuilder() + .addStatement("return setOf(%L)", vssNodeNamesJoined) + .build(), + ) + .build() + } + override fun equals(other: Any?): Boolean { if (other !is VssNodeSpecModel) return false @@ -329,7 +393,7 @@ internal class VssNodeSpecModel( } companion object { - private const val PROPERTY_VALUE_NAME = "value" + private val experimentalUnsignedTypes = setOf("kotlin.UIntArray") } }