From 4c9d6de252aa31959a43f75e9610cbe9cf2e35ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=9C=D0=B8?= =?UTF-8?q?=D1=85=D0=B0=D0=B9=D0=BB=D0=BE=D0=B2=D0=B8=D1=87=20=D0=96=D0=B5?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D1=81=D0=BA=D0=B8=D0=B9?= <55230817+zhelenskiy@users.noreply.github.com> Date: Wed, 21 Dec 2022 17:54:39 +0100 Subject: [PATCH 01/22] Create multi-field-value-classes.md --- proposals/multi-field-value-classes.md | 599 +++++++++++++++++++++++++ 1 file changed, 599 insertions(+) create mode 100644 proposals/multi-field-value-classes.md diff --git a/proposals/multi-field-value-classes.md b/proposals/multi-field-value-classes.md new file mode 100644 index 000000000..2cb97e779 --- /dev/null +++ b/proposals/multi-field-value-classes.md @@ -0,0 +1,599 @@ +# Multi-field value classes KEEP draft + +# Multi-field value classes + +* **Type**: Design proposal +* **Author**: Evgeniy Zhelenskiy +* **Prototype**: Experimental in Kotlin/JVM since 1.8.20 + +## Summary + +Currently working [Inline Classes](https://github.com/Kotlin/KEEP/blob/master/proposals/inline-classes.md) allow making a performant identity-less wrapper for a value of some type. It is performant because it stores the inner value directly when possible and differentiates the types in compile type. + +```kotlin +@JvmInline +value class Password(val value: String) { + init { + require(isValidPassword(value)) { "$value is not a valid password" } + } +} +``` + +Using `Password` class instead of raw `String` helps you not to pass invalid password when validated one is expected and it helps not to pass it accidentally as `Username`. There is no overhead because password is represented as raw `String` in the compiled code and the runtime. + +However, Inline classes are very limited: it may be useful to escape creating wrappers because of performance issues for classes with multiple fields too. Inline classes generalization is named [Value classes](https://github.com/Kotlin/KEEP/blob/master/notes/value-classes.md) (shallow immutable classes without identity). Since single field value classes are inline classes, which are already implemented, this KEEP describes multi-field value classes (MFVC). + +Value classes are very similar to data classes but: + +* They do not have component functions and `copy` methods. +* They can only have `val` constructor parameters, while data classes can have `var` parameters as well. +* It is forbidden to use identity of value classes. +* Customizing equals is not allowed until “typed equals” feature is released. + +## Use cases + +The typical example of MFVC usage is applications that create a lot of small effective structures without identity, e.g. geometry structures: + +```kotlin +interface Figure +interface FigureWithPerimeter : Figure { + val perimeter: T +} +interface FigureWithArea : Figure { + val area: T +} + +@JvmInline +value class DPoint(val x: Double, val y: Double): Figure + +@JvmInline +value class DSegment(val p1: DPoint, p2: DPoint): FigureWithPerimeter { + inline val middle: DPoint + get() = DPoint((p1.x + p2.x) / 2.0, (p1.y + p2.y) / 2.0) + + val length: Double + get() = sqrt((p1.x - p2.x).square() + (p1.y - p2.y).square()) + + @TypedEquals + fun equals(other: DSegment) = + p1 == other.p1 && p2 == other.p2 || p1 == other.p2 && p2 == other.p1 + + override hashCode() = p1.hashCode() + p2.hashCode() + + override val perimeter get() = length +} + +@JvmInline +value class DRectangle( + val topLeftVertex: DPoint, val bottomRightVertex: DPoint +) : FigureWithPerimeter, FigureWithArea { + init { + require(topLeftVertex.x <= bottomRightVertex.x) { + "${topLeftVertex.x} > ${bottomRightVertex.x}" + } + require(topLeftVertex.y >= bottomRightVertex.y) { + "${topLeftVertex.y} < ${bottomRightVertex.y}" + } + } + + inline val topRightVertex: DPoint + get() = DPoint(bottomRightVertex.x, topLeftVertex.y) + inline val bottomLeftVertex: DPoint + get() = DPoint(topLeftVertex.x, bottomRightVertex.y) + + val width: Double + get() = bottomRightVertex.x - topLeftVertex.x + + val hight: Double + get() = topLeftVertex.y - bottomRightVertex.y + + override val area: Double + get() = height * width + + override val perimeter: Double + get() = 2 * (height + width) +} + +fun Double.square() = this * this +``` + +Another use case is creating some custom primitives for graphics: + +```kotlin +@JvmInline +value class Color( + val alpha: UByte, val red: UByte, val green: UByte, val blue: UByte +) + +@JvmInline +value class Gradient(val from: Color, val to: Color) + +fun Gradient.colorAt(percentage: Double): Color { + require(percentage in 0.0..100.0) { "Invalid percentage: $percentage" } + val c1 = 100.0 - percentage + val c2 = percentage + return Color( + alpha = (from.alpha.toDouble() * c1 + to.alpha.toDouble() * c2).toUByte(), + red = (from.red.toDouble() * c1 + to.red.toDouble() * c2).toUByte(), + green = (from.green.toDouble() * c1 + to.green.toDouble() * c2).toUByte(), + blue = (from.blue.toDouble() * c1 + to.blue.toDouble() * c2).toUByte(), + ) +} +``` + +Value classes can also be used to make named tuples. And those can be used to return multiple values that are not connected between each other (the syntax of named tuples is not real, it does not exist yet): + +```kotlin +fun > split(tree: Tree, key: T): (left: Tree, right: Tree) { + ... + return (left = l, right = r) + ... +} +``` + +## Description + +Value classes are declared using soft keyword `value` as inline classes are. In Kotlin/JVM they also need `@JvmInline` annotation (several examples are above) before [Valhalla project](https://openjdk.org/projects/valhalla/) release. After the release, Valhalla value classes would become the default and expected way to create ones in Kotlin/JVM because they offer more optimizations than it is possible to do for pre-Valhalla value classes. So the latter are marked with annotations. + +The reasons why the feature is implemented in Kotlin for pre-Valhalla JVMs too are: + +* Valhalla is only JVM, while Kotlin is not. +* Valhalla would hardly be adopted in Android in any nearest future because Android Runtime is still compatible with JVM 1.8. +* Adoption of new JVM’s is quite slow. + + +As any other class, value classes can declare member (except inner classes), be a member of some other class, have generics, extensions and other language features. + +MFVC are currently only supported in Kotlin/JVM so description below is related only to it. + +## Current limitations + +Limitations for MFVC are similar to the [limitations of inline classes](https://github.com/Kotlin/KEEP/blob/master/proposals/inline-classes.md#current-limitations) but primary constructors can have arbitrary number of parameters greater than 1 (case of 1 parameter is inline class). + +## Java interoperability + +Each variable of MFVC is stored as several variables corresponding to their state. The same is for fields. if some MFVC contains other MFVC fields, they are flattened recursively. It means that the actual representation of variable (field) of `DSegment` type (declared above) in the code is 4 `Double` variables (fields) corresponding to the both coordinates of its points. + +## Boxing + +Each multi-field value class has its own wrapper that is represented as a usual class on JVM. This wrapper is needed to box values of value class types and use it where it's impossible to use unboxed values. + +**Rules for boxing** are the following: + +* **Boxed when used as other type.** It means that if one uses MFVC as nullable type, implemented interface or `Any`, the boxing will happen: `dRectangle.area` doesn’t require the boxing while `(dRectangle as FigureWithArea).area` does. +* **When used as type parameter (including function types).** It happens because type parameters are erased in JVM. It is the same as for inline classes. However, there are exception for the rule: + * When type parameter upper bound is MFVC. + * When type parameter is a type parameter of inline function or lambda. +* **Returning from not inline function.** It happens because it is impossible to pass several objects between frames in JVM. The exception is chain of MFVC getters: the compiler replaces `wrapper.segment.p1.x` with getter `wrapper.`getSegment-p1-x`()` instead of `wrapper.getSegment().getP1().getX()`. And this complex getter escapes boxing when getter implementation was initially just reading the field. +* **Secondary constructors.** As they are replaced with non-inline function. +* **Passing returned boxed value to another function that expects boxed parameter.** Intermediate value isn’t unboxed due to optimization because it would be boxed back otherwise. +* **Lateinit variables and properties.** Because nullable types are stored. +* **Between suspension points.** Continuation is generic, and while the generics are erased, the value can be stored only boxed there. + +The compiler avoids boxing-unboxing chains when possible. + +## Generics + +MFVC’s with fields whose types are type arguments are allowed and they are mapped to the corresponding type parameters upper bounds as it is done for [Inline Classes](https://github.com/Kotlin/KEEP/blob/master/proposals/inline-classes.md#generic-inline-class-mapping). + +```kotlin +@JvmInline +value class Generic(val x: T, val y: R) +fun foo(g: Generic) {} +``` + +These MFVC would also benefit from reified generics because lots of boxing would be eliminated: + +```kotlin +@JvmInlinevalue class Reified(val x: T, val y: R) +fun foo(a: Reified, b: Reified) +``` + +## Kotlinx.serialization + +MFVC behave similarly to data classes during serialization and deserialization. + +## Reflection + +Kotlin reflection is not supported for MFVC yet but will be supported soon. + +## Methods from `kotlin.Any` + +As it is done for [inline classes](https://github.com/Kotlin/KEEP/blob/master/proposals/inline-classes.md#methods-from-kotlinany), compiler generates `toString`, `hashCode`, `equals` (but not component functions because positional destructuring is considered harmful). `toString` and `hashCode` can be customized by overriding, customization of equals will be possible by using feature “typed equals” (which takes the corresponding type as parameter instead of `Any?`) that is in development. + +## Arrays of Value Classes + +This paragraph is actual for both inline classes and multi-field value classes so they are generified to value classes here. Value arrays of value classes are reified classes which are not implemented yet. They also cause many other design questions because of necessity of value Lists etc. So there are no value arrays (`VArray'`s) for now and it is forbidden to use `vararg` for value classes. The same thing is already [written](https://github.com/Kotlin/KEEP/blob/master/proposals/inline-classes.md#arrays-of-inline-class-values) for Inline Classes. + +## Expect/Actual MFVC + +As other platforms are not supported yet, expect and actual modifiers do nothing for MFVC. + +## Mangling + +As for [Inline Classes](https://github.com/Kotlin/KEEP/blob/master/proposals/inline-classes.md#mangling), mangling is used to mitigate problems of overload resolution, non-public primary constructors and initialization blocks. MFVC methods, constructors and functions that take MFVC as parameters or receivers are also not accessible from Java. Secondary constructors with bodies are also enabled as experimental in 1.8.20 and stable in 1.9.0. + +[Mangling rules](https://github.com/Kotlin/KEEP/blob/master/proposals/inline-classes.md#mangling-rules) are the same as to Inline Classes but function return type does not take part in computing hash because MFVC return type is not replaced with any other as it is done for Inline Classes. + +## MFVC ABI (JVM) + +It is not always possible to pass multi-field value class (MFVC) values to functions as unboxed. The limitations are listed above in [Boxing](#Boxing) section. + +Also, there are strong limitations for returning MFVC from functions: it is forbidden to return multiple values from function in JVM. However, if we could never cope with it, all the optimizations would be useless: if a value was boxed once, there is no reason to escape boxing because using the existing box guarantees that no more boxes for this value are created. + +Fortunately, there are several workarounds: + +* Using shadow stacks. Benchmark showed that it is even slower than using a box that is rather cheap in modern JVMs. +* Using local variables for the calling site. It is only possible for inline functions as they are in the same stack frame. +* Getting result MFVC from arguments at the calling site. It is only possible for primary constructors of MFV. They are the main and initial source of all MFVCs. + +Primary constructors do two things: field initialization and side effects. They can be separated and the latter is stored in so-called `/*static*/ fun constructor-impl(...): Unit` which takes field values as parameters and performs side effects. So MFVC primary constructor calls can be replaced with storing arguments in variables and then calling the `constructor-impl` function on them. + +The `constructor-impl` must be generated even if it is empty because public ABI must not depend on the body inisides. + +### (Un)boxing functions + +Every MFVC can exist in 2 forms: boxed (single field, variable, parameter) and unboxed (several fields, variables, parameters). + +Conversion from unboxed one into boxed one is rather simple: calling static synthetic function `box-impl` with necessary type and value arguments. However, there are several points that make synthetic unboxing functions `unbox-impl-[...]` more complicated. + +#### Choice between `unbox-impl` and getter. + +```kotlin +@JvmInline +value class DPoint(val x: Double, val y: Double) + +``` + +The class above has 2 ways to access `x`: `DPoint::getX` and `DPoint::unbox-impl-x`. So, when generating some code it is necessary to choose method via some convention. + +Furthermore, `DPoint::getX` can be protected/internal or not even exist if `x` is private while it is necessary to be able to unbox publicly. + +Although `DPoint::getX` is much more readable than `DPoint::unbox-impl-x`, there is no need to read generated code. + +To sum up, when generating code, an unbox function should be always preferred to the getter. + +#### Part of a regular class + +Boxed MFVC can be not just a box itself but also a part of another regular class. It must also contain unbox functions for each flattened field whose property has default getter. + +Their visibility must be not less than the original property visibility. As they are synthetic, they cannot be accessed from Java. As they might be used in kotlin-reflect, they should be public. + +If the property had a setter it should be untied from the property as its flattened signature invalidates setters signature convention. + +```kotlin +@JvmInline +value class DPoint(val x: Double, val y: Double) +class SomeClass(var value: DPoint) +``` + +`SomeClass.setValue: (DPoint) -> Unit` becomes `SomeClass.setValue-: (Double, Double) -> Unit`, it has 2 parameters. + +#### Nested MFVC + +Both MFVC instances within a regular class and independent ones can be nested. + +```kotlin +@JvmInline +value class DPoint(val x: Double, val y: Double) +@JvmInline +value class DSegment(val p1: DPoint, val p2: DPoint) +class SomeClass(val segment: DSegment) +``` + +`DSegment` and `SomeClass` contain 4 `Double` fields. + +With nested MFVC we may also want to access some intermediate properties (`DSegment.p1`, `DSegment.p2` here), not just leaves (`DSegment.p1.x`, `DSegment.p1.y`, `DSegment.p2.x`, `DSegment.p2.y` here). + +A simple (not actually used) way for it is to generate `box-impl` invocations for each usage of the properties `p1` and `p2`: + +```kotlin +DPoint.`box-impl`(x.`unbox-impl-p2-x`(), x.`unbox-impl-p2-y`()) // x is DSegment here +``` + +instead of + +```kotlin +x.getP2() +``` + +This solution has a drawback that it generates a huge repeating code footprint. + +We could use existing public getter in this case, but it would not exist if the property was more nested. When using public getters consequently (`x.getMfvc1().getMfvc2().getX()`) useless intermediate boxes would be generated. + +```kotlin +@JvmInline +value class Mfvc2(val x: Int, val y: Int) + +@JvmInline +value class Mfvc1(val mfvc2: Mfvc2, val y: Int) + +data class Regular(val mfvc1: Mfvc1) { + val mfvc1Custom: Mfvc1 + get() { + println("42") + return mfvc1 + } +} + +val r: Regular = ... +r.mfvc1.mfvc2.x // is usual unbox can be used +r.mfvc1 // can use getter Regular::getMfvc1 +r.mfvc1.mfvc2 // option #1: Mfvc2.`box-impl`(r.`getX-0`(), r.`getX-1`()) creates large code footprint and cannot be overridden (see below) + // option #2: (x.getMfvc1()).getMfvc2() creates Mfvc1 box that can be escaped +``` + + +So, it is necessary to create intermediate MFVC unbox methods for nested MFVC properties. + +Furthermore, if they existed, they could be overridden to optimize nested access. The methods must also have default implementations that created intermediate boxes. This would allow such properties to be overridden in Java or by non-trivial getters. It would also help to save binary compatibility. As MFVC cannot have properties with custom getter and backing field simultaneously, the only cause of choice between effective and default implementation is whether the current regular class property getter is default or not. So, when it becomes default/not default subgetters of the current class change there implementation to effective/default and do not affect other classes. + +Naming unbox methods becomes more difficult with nested MFVC. A node in MFVC tree can be identified by its path from the root (e.g. `["mfvc1", "mfvc2", "x"]` for the example above). Each of elements of a path can be identified by its name or by its index. Better readability is necessary in stack traces. So, names are chosen. + +Actually called functions for the example above: + +```kotlin +r.mfvc1.mfvc2.x // r.getMfvc1-mfvc2-x() +r.mfvc1 // r.getMfvc1() +r.mfvc1.mfvc2 // r.getMfvc1-mfvc2() which does NOT create intermediate Mfvc1 +r.mfvc1Custom.mfvc2 // r.getMfvc1Custom-mfvc2() which DOES create intermediate Mfvc1 +``` + +Visibility of intermediate unbox methods must be also public as they are synthetic (so not accessible from Java) but needed for kotlin-reflect. + +Recompilation of a class that contains MFVC properties is only required if order and types of the MFVC leaves change. + +The mentioned example with `DPoint`, `DSegment` and `SomeClass` contains the following unbox function declarations: + +```kotlin +DPoint { + fun `unbox-impl-x`(): Double + fun `unbox-impl-y`(): Double +} + +DSegment { + fun `unbox-impl-p1`(): DPoint + fun `unbox-impl-p1-x`(): Double + fun `unbox-impl-p1-y`(): Double + fun `unbox-impl-p2`(): DPoint + fun `unbox-impl-p2-x`(): Double + fun `unbox-impl-p2-y`(): Double +} + +SomeClass { + fun `getSegment-p1`(): DPoint + fun `getSegment-p1-0`(): Double + fun `getSegment-p1-1`(): Double + fun `getSegment-p2`(): DPoint + fun `getSegment-p2-0`(): Double + fun `getSegment-p2-1`(): Double + fun getSegment(): DSegment +} +``` + +`getSegment` is not mangled as MFVCs, returned from functions, are always boxed. + +`f(x/*: DPoint*/)` can be compiled into `f(x.unbox-impl-x(), x.unbox-impl-y())` and `f(x.getX(), x.getY())`. If the `getX` or `getY` are not public necessity of unbox functions is obvious. But we actually always need them because if we call them instead of getters, we don't need to recompile existing calls when the name or visibility changes. + +#### Optimization + +When accessing a MFVC leaf from within the class it is better to use get-field directly than the unbox function as it is done for regular properties with backing fields. If the property is private, MFVC leaves will be also private and used only by kotlin-reflect. + +#### Overriding example: + +```kotlin +interface Interface { + val point: DPoint +} + +class DefaultGetter(override val point: DPoint) + +class NonDefaultGetter(private val pointImpl: DPoint) { + override val point: DPoint + get() { + println("Called") + return pointImpl + } +} + +fun f(x: Interface) = x.point.x * x.point.y +``` + +It is compiled to + +```kotlin +interface Interface { + fun getPoint(): DPoint + @JvmSynthetic + fun `getPoint-x`(): Double = getPoint().getX() + @JvmSynthetic + fun `getPoint-y`(): Double = getPoint().getY() +} + +class DefaultGetter { + ... + override fun getPoint(): DPoint { + return DPoint.`box-impl`(`getPoint-x`(), `getPoint-y`()) + } + + @JvmSynthetic + override fun `getPoint-x`(): Double { + return point-x // field + } + + @JvmSynthetic + override fun `getPoint-y`(): Double { + return point-y // field + } +} + +class NonDefaultGetter(private val pointImpl: DPoint) { + override fun getPoint(): DPoint { + println("Called") + return getPointImpl() + } + + @JvmSynthetic + override fun `getPoint-0`(): Double { + return getPoint().`unbox-impl-x`() + } + + @JvmSynthetic + override fun `getPoint-1`(): Double { + return getPoint().`unbox-impl-y`() + } +} + +fun f(x: Interface) = x.`getPoint-x`() * x.`getPoint-y`() +``` + +### Annotations + +Each property and its parts (field, getter, setter) can be annotated. MFVC properties are not exceptions. + +Annotating MFVC leaves does not cause any problems because they are not transformed while annotating other MFVC properties are. + +Annotating non-leaves is currently forbidden due to lack of use-cases. + +### Expression flattening + +As described above, the default representation of MFVC is flattened, so it is necessary to flatten existing expressions. +The easiest way is to take a box and then flatten it. but we should escape boxing when possible. + +* If the expression is a primary constructor call, we flatten each of subfields recursively to variables and then call `constructor-impl` function. The flattened expressions are the read accesses to the variables. +* If the expression already has some flattened representation, we can just use it. For example, it could be some getter call that can be replaced with multiple unboxing functions calls. +* If the expression is some block of statements, we flatten the last one recursively as it is the result of the block. +* `when`, `if` and `try` expressions results are also flattened. +* Otherwise, we should actually receive a boxed expression and then unbox it with unbox methods or field getting. + + +If we flatten not to variables themselves but we just want expressions, we may escape using variables when possible (keeping evaluation order the same): + +```kotlin +*// Before: +val a = 2 +val b = 3 +val c = b + 1 +// [a, b, c] + +// After: +val a = 2 +val b = 3 +// [a, b, b + 1]* +``` + +If the flattened representation is used to reset (not initialize) a variable/field, new value representation must be stored in variables. If an exception is thrown during evaluation of the new value, the old value must be kept. The tail of flattened expressions that cannot throw an error is inlined (variables are not created). + +If boxing result is not needed, boxing is not called. + +As the variables are used outside the expression they are used for the first time, there declaration is placed in the most inner block that contains all the usages. It is done to save variable slots. Also, if it is possible to combine declaration and initialization, it is done to remove default initialization. + +#### Examples + +##### Example 1 + +```kotlin +println("Some random actions that do not use the variables") +val a = DSegment(DPoint(0.0, 1.0), makePoint(2.0, 3.0)) +``` + +```kotlin +println("Some random actions that do not use the variables") +val `a-p1-x`: Double = 0.0 +val `a-p1-y`: Double = 1.0 +DPoint.`constructor-impl`(`a-p1-x`, `a-p1-y`) +val tmp0 = makePoint(2.0, 3.0) +val `a-p2-x`: Double = tmp0.`unbox-impl-x`() +val `a-p2-y`: Double = tmp0.`unbox-impl-y`() +DSegment.`constructor-impl`(`a-p1-x`, `a-p1-y`, `a-p2-x`, `a-p2-y`) +``` + +##### Example 2 + +```kotlin +var p = DPoint(doubleProducer(), 5.0) // no temporary variables +p = DPoint(doubleProducer(), 6.0) // temporary variables for the first argument +``` + +```kotlin +var `p-x` = doubleProducer() +var `p-y` = 5.0 +DPoint.`constructor-impl`(`p-x`, `p-y`) +val tmp0 = doubleProducer() +DPoint.`constructor-impl`(tmp0, 6.0) +`p-x` = tmp0 +`p-y` = 6.0 +``` + +##### Example 3 + +```kotlin +fun f() { + DPoint(100.0, 200.0) +} +``` + +```kotlin +fun f() { + val `tmp0-x` = 100.0 + val `tmp0-y` = 200.0 + DPoint.`constructor-impl`(`tmp0-x`, `tmp0-y`) + // no box-impl call here +} +``` + +##### Example 4 + +```kotlin +val x = if (condition) { + DPoint(1.0, 2.0) + DPoint(3.0, 4.0) +} else { + DPoint(5.0, 6.0) + DPoint(7.0, 8.0) +} +``` + +```kotlin +val `x-x`: Double +val `x-y`: Double +if (condition) { + val `tmp0-x`: Double = 1.0 + val `tmp0-y`: Double = 2.0 + DPoint.`constructor-impl`(`tmp0-x`, `tmp0-y`) + `x-x` = 3.0 + `x-y` = 4.0 + DPoint.`constructor-impl`(`x-x`, `x-y`) +} else { + val `tmp0-x`: Double = 5.0 + val `tmp0-y`: Double = 6.0 + DPoint.`constructor-impl`(`tmp0-x`, `tmp0-y`) + `x-x` = 7.0 + `x-y` = 8.0 + DPoint.`constructor-impl`(`x-x`, `x-y`) +} +``` + +### Function flattening + +If the function has parameters to be flattened, it is replaced with mangled function which is made static if it is a MFVC method replacement. + +If necessary, bridges are generated. + +#### Equality + +There is an additional function `equals-impl0` for equals replacement that takes both parameters flattened. It is called instead of `==` when possible. If the right argument is MFVC and the left one is literal null, right argument is not boxed. Also, if right argument is unboxed `MFVC`, and the left one is `MFVC?`, the compiler generates an if expression, that checks the left for being null and then uses `equals-impl0` if it is not. + +#### String concatenation + +Concatenating string and MFVC produces effective `toString` replacement function calls to escape boxing. + +## Further development + +* Compose support +* Kotlin Reflection +* Better debugger support +* Project Valhalla support +* Other backends + multiplatform support +* Reified value classes, VArrays, value interfaces and other JVM-related features from the [design notes](https://github.com/Kotlin/KEEP/blob/master/notes/value-classes.md). +* Frontend features from the [design notes](https://github.com/Kotlin/KEEP/blob/master/notes/value-classes.md). From fd046f20afa2491c4519b58b437aea5d227f58df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=9C=D0=B8?= =?UTF-8?q?=D1=85=D0=B0=D0=B9=D0=BB=D0=BE=D0=B2=D0=B8=D1=87=20=D0=96=D0=B5?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D1=81=D0=BA=D0=B8=D0=B9?= <55230817+zhelenskiy@users.noreply.github.com> Date: Wed, 21 Dec 2022 18:20:49 +0100 Subject: [PATCH 02/22] Attach multi-field value class issue --- proposals/multi-field-value-classes.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/proposals/multi-field-value-classes.md b/proposals/multi-field-value-classes.md index 2cb97e779..d506fc3ea 100644 --- a/proposals/multi-field-value-classes.md +++ b/proposals/multi-field-value-classes.md @@ -6,6 +6,8 @@ * **Author**: Evgeniy Zhelenskiy * **Prototype**: Experimental in Kotlin/JVM since 1.8.20 +Discussion of this proposal is held in [this issue](https://github.com/Kotlin/KEEP/issues/340). + ## Summary Currently working [Inline Classes](https://github.com/Kotlin/KEEP/blob/master/proposals/inline-classes.md) allow making a performant identity-less wrapper for a value of some type. It is performant because it stores the inner value directly when possible and differentiates the types in compile type. From dff9e07e87fbb96e38a226943e7ee302f6149042 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=9C=D0=B8?= =?UTF-8?q?=D1=85=D0=B0=D0=B9=D0=BB=D0=BE=D0=B2=D0=B8=D1=87=20=D0=96=D0=B5?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D1=81=D0=BA=D0=B8=D0=B9?= <55230817+zhelenskiy@users.noreply.github.com> Date: Wed, 21 Dec 2022 18:25:46 +0100 Subject: [PATCH 03/22] Fix typo in proposals/multi-field-value-classes.md Co-authored-by: Jake Wharton --- proposals/multi-field-value-classes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proposals/multi-field-value-classes.md b/proposals/multi-field-value-classes.md index d506fc3ea..ddcd069de 100644 --- a/proposals/multi-field-value-classes.md +++ b/proposals/multi-field-value-classes.md @@ -187,7 +187,7 @@ fun foo(g: Generic) {} These MFVC would also benefit from reified generics because lots of boxing would be eliminated: ```kotlin -@JvmInlinevalue class Reified(val x: T, val y: R) +@JvmInline value class Reified(val x: T, val y: R) fun foo(a: Reified, b: Reified) ``` From 5098db0acd46b3a7c0458e122e98e1f8f0502663 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=9C=D0=B8?= =?UTF-8?q?=D1=85=D0=B0=D0=B9=D0=BB=D0=BE=D0=B2=D0=B8=D1=87=20=D0=96=D0=B5?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D1=81=D0=BA=D0=B8=D0=B9?= <55230817+zhelenskiy@users.noreply.github.com> Date: Thu, 22 Dec 2022 14:51:34 +0100 Subject: [PATCH 04/22] Fix typo in proposals/multi-field-value-classes.md Co-authored-by: Andrew Mikhaylov --- proposals/multi-field-value-classes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proposals/multi-field-value-classes.md b/proposals/multi-field-value-classes.md index ddcd069de..e42f1d3c9 100644 --- a/proposals/multi-field-value-classes.md +++ b/proposals/multi-field-value-classes.md @@ -166,7 +166,7 @@ Each multi-field value class has its own wrapper that is represented as a usual * **When used as type parameter (including function types).** It happens because type parameters are erased in JVM. It is the same as for inline classes. However, there are exception for the rule: * When type parameter upper bound is MFVC. * When type parameter is a type parameter of inline function or lambda. -* **Returning from not inline function.** It happens because it is impossible to pass several objects between frames in JVM. The exception is chain of MFVC getters: the compiler replaces `wrapper.segment.p1.x` with getter `wrapper.`getSegment-p1-x`()` instead of `wrapper.getSegment().getP1().getX()`. And this complex getter escapes boxing when getter implementation was initially just reading the field. +* **Returning from not inline function.** It happens because it is impossible to pass several objects between frames in JVM. The exception is chain of MFVC getters: the compiler replaces `wrapper.segment.p1.x` with getter ``wrapper.`getSegment-p1-x`()`` instead of `wrapper.getSegment().getP1().getX()`. And this complex getter escapes boxing when getter implementation was initially just reading the field. * **Secondary constructors.** As they are replaced with non-inline function. * **Passing returned boxed value to another function that expects boxed parameter.** Intermediate value isn’t unboxed due to optimization because it would be boxed back otherwise. * **Lateinit variables and properties.** Because nullable types are stored. From feac62b35fe3c2c8b4c1fa70f8acbb1810f2183a Mon Sep 17 00:00:00 2001 From: "Evgeniy.Zhelenskiy" Date: Thu, 12 Jan 2023 17:09:28 +0100 Subject: [PATCH 05/22] Add more description about Valhalla and farther development --- proposals/multi-field-value-classes.md | 116 ++++++++++++++++--------- 1 file changed, 76 insertions(+), 40 deletions(-) diff --git a/proposals/multi-field-value-classes.md b/proposals/multi-field-value-classes.md index e42f1d3c9..89e688a99 100644 --- a/proposals/multi-field-value-classes.md +++ b/proposals/multi-field-value-classes.md @@ -8,9 +8,11 @@ Discussion of this proposal is held in [this issue](https://github.com/Kotlin/KEEP/issues/340). -## Summary +## Description + +### Inline Classes -Currently working [Inline Classes](https://github.com/Kotlin/KEEP/blob/master/proposals/inline-classes.md) allow making a performant identity-less wrapper for a value of some type. It is performant because it stores the inner value directly when possible and differentiates the types in compile type. +Currently working [Inline Classes](https://github.com/Kotlin/KEEP/blob/master/proposals/inline-classes.md) allow making a performant identity-less type-safe wrapper with human-readable `toString` for a value of some type. It is performant because it stores the inner value directly when possible and differentiates the types in compile time. ```kotlin @JvmInline @@ -23,18 +25,57 @@ value class Password(val value: String) { Using `Password` class instead of raw `String` helps you not to pass invalid password when validated one is expected and it helps not to pass it accidentally as `Username`. There is no overhead because password is represented as raw `String` in the compiled code and the runtime. -However, Inline classes are very limited: it may be useful to escape creating wrappers because of performance issues for classes with multiple fields too. Inline classes generalization is named [Value classes](https://github.com/Kotlin/KEEP/blob/master/notes/value-classes.md) (shallow immutable classes without identity). Since single field value classes are inline classes, which are already implemented, this KEEP describes multi-field value classes (MFVC). +### Multi-field value classes + +However, Inline classes are very limited: it may be useful to escape creating wrappers because of performance issues for classes with multiple fields too. Inline classes generalization is named [Value classes](https://github.com/Kotlin/KEEP/blob/master/notes/value-classes.md) (shallow immutable classes without identity). + +As well, as for Inline classes, one of the main aims of MFVC is the compiler optimizations that are possible due to the lack of fixed identity. Unfortunately, there are several limitations for optimization for JVM target described below in the [Boxing](#boxing) section. + +Multi-field value classes are the next part of the Value classes feature after Inline classes. + +### Value classes + +Value classes are effective shallow-immutable data classes without identity, copy functions and component functions. + +Shallow immutability is chosen to make it possible to implement internally mutable classes with immutable public API such as `Lazy`. However, there are domains where deep immutability is required, but allowing such requirement is currently outside the scope of the document. + +Value classes do not have `copy` methods due to their inconvenience for usage to mutate: +* They are verbose and hard to understand when combined with constructions such as `if`s, loops. +* They are unsuitable for nested structures: `a = a.copy(b = a.b.copy(c = f(a.b.c + 1)))` instead of `a.b.c = f(a.b.c)` for mutable classes. +* They cannot be used for operators: `a = a.copy(b = a.b + 2)` instead of `a.b += 2` for mutable classes. + +Not yet implemented lens syntax is going to be used for convenient mutating mutable fields/variables of immutable object types. Other applications of the shallow-copying function `copy` do not exist for shallow-immutable value classes. + +Value classes do not have component functions as they are used for positional destructuring which is considered harmful because of exposing of the internal state and order of the fields. It will be replaced with not yet implemented named destructuring syntax. + +Customizing `equals` is not allowed until “typed equals” feature is released. + +### Further development: `VArray`s and reification -Value classes are very similar to data classes but: +One of other important steps of Value classes feature are Value arrays (`VArray`s) and reified classes and reified not-inline functions. They significantly extend applicability and optimizations of Value classes, **but they require MFVC to be already implemented** for that. Their importance is caused by frequent usage of containers that store and use stored values: they are currently handle value classes as boxed. -* They do not have component functions and `copy` methods. -* They can only have `val` constructor parameters, while data classes can have `var` parameters as well. -* It is forbidden to use identity of value classes. -* Customizing equals is not allowed until “typed equals” feature is released. +`VArray` is the fundamental container and first reified class. `VArray`s solve the problem of effective arrays (with invariant type parameter) without necessity of declaring manual [`UIntArray`](https://github.com/JetBrains/kotlin/blob/30788566012c571aa1d3590912468d1ebe59983d/libraries/stdlib/unsigned/src/kotlin/UIntArray.kt#L15) for each type `UInt`. It also generifies existing `IntArray`, `LongArray`, `BooleanArray` etc. with scalable general `VArray` which maps to `VArray`, `VArray`, `VArray` correspondingly. + +Having `VArray`s and reified functions also allows to write generic functions, extensions that operate on generic `VArray`: +```kotlin +operator fun VArray.plus(other: VArray) = + VArray(this.size + other.size) { if (it < this.size) this[it] else other[it - this.size] } +``` + +Having reified functions for generics of primitive or value class types helps to escape boxing. For example, being used within non-reified generic function, `x/*int*/ + y/*int*/` becomes `Integer.valueOf(x/*Integer*/.value, y/*Integer*/.value)`. So, it is better to generate specializations for such classes if the type parameters are marked as reified. However, reifying all parameters as it is done in C++ and Rust leads to huge code footprint and exponential growth of compilation time while it gives no significant performance boost for reference types. + +`VArray` is not the only container or class the users may want to reify, thus reification of other classes shall also be possible in the future. + +### Project Valhalla + +There is [Project Valhalla](https://openjdk.org/projects/valhalla/) for JVM that suggests Value Classes that are similar to ones this proposal is about. It solves the same problems but on the runtime side (efficient user-defined identity-less objects, arrays of them, classes with them as fields, generic specialization). While the compiler is limited by JVM bytecode restrictions (cannot return several values from function) and uses the open world model (does not know all code that will be executed and usages of all classes, functions), the Valhalla-capable runtime is not limited and has closed world model (does know all executing code and usages of all classes, functions). It gives a great advantage in performing optimizations to the Valhalla-based Value classes. + +However, it requires usage of the capable runtime, which is impracticable condition for Android, where runtime is still compatible with JVM 1.8 that was released in 2014. That is why Kotlin/JVM compiler needs to provide the functionality independent of Valhalla Project. Nevertheless, the latter solution is preferred, so simple migration to it must be possible. ## Use cases +*Since single field value classes are inline classes, which are already implemented, this KEEP describes further only multi-field value classes (MFVC).* -The typical example of MFVC usage is applications that create a lot of small effective structures without identity, e.g. geometry structures: +The typical example of MFVC usage is applications that create a lot of small effective structures without identity, e.g. geometry structures, complex numbers, rational numbers: ```kotlin interface Figure @@ -60,7 +101,7 @@ value class DSegment(val p1: DPoint, p2: DPoint): FigureWithPerimeter { fun equals(other: DSegment) = p1 == other.p1 && p2 == other.p2 || p1 == other.p2 && p2 == other.p1 - override hashCode() = p1.hashCode() + p2.hashCode() + override fun hashCode() = p1.hashCode() + p2.hashCode() override val perimeter get() = length } @@ -133,16 +174,9 @@ fun > split(tree: Tree, key: T): (left: Tree, right: Tree } ``` -## Description - -Value classes are declared using soft keyword `value` as inline classes are. In Kotlin/JVM they also need `@JvmInline` annotation (several examples are above) before [Valhalla project](https://openjdk.org/projects/valhalla/) release. After the release, Valhalla value classes would become the default and expected way to create ones in Kotlin/JVM because they offer more optimizations than it is possible to do for pre-Valhalla value classes. So the latter are marked with annotations. - -The reasons why the feature is implemented in Kotlin for pre-Valhalla JVMs too are: - -* Valhalla is only JVM, while Kotlin is not. -* Valhalla would hardly be adopted in Android in any nearest future because Android Runtime is still compatible with JVM 1.8. -* Adoption of new JVM’s is quite slow. +## Syntax +Value classes are declared using soft keyword `value` as inline classes are. In Kotlin/JVM they also need `@JvmInline` annotation (several examples are above) before [Valhalla project](https://openjdk.org/projects/valhalla/) release. After the release, Valhalla value classes would become the default and expected way to create ones in Kotlin/JVM because they offer more optimizations than it is possible to do for pre-Valhalla value classes. So the latter are marked with annotations in Kotlin/JVM. As any other class, value classes can declare member (except inner classes), be a member of some other class, have generics, extensions and other language features. @@ -162,11 +196,11 @@ Each multi-field value class has its own wrapper that is represented as a usual **Rules for boxing** are the following: -* **Boxed when used as other type.** It means that if one uses MFVC as nullable type, implemented interface or `Any`, the boxing will happen: `dRectangle.area` doesn’t require the boxing while `(dRectangle as FigureWithArea).area` does. -* **When used as type parameter (including function types).** It happens because type parameters are erased in JVM. It is the same as for inline classes. However, there are exception for the rule: +* **Boxed when used as other type.** It means that if one uses MFVC as nullable type, implemented interface or `Any`, the boxing will happen: `dRectangle.area` does not require the boxing while `(dRectangle as FigureWithArea).area` does. +* **When used as type parameter (including function types).** It happens because type parameters are erased in JVM. It is the same as for inline classes. However, there are exceptions to the rule: * When type parameter upper bound is MFVC. - * When type parameter is a type parameter of inline function or lambda. -* **Returning from not inline function.** It happens because it is impossible to pass several objects between frames in JVM. The exception is chain of MFVC getters: the compiler replaces `wrapper.segment.p1.x` with getter ``wrapper.`getSegment-p1-x`()`` instead of `wrapper.getSegment().getP1().getX()`. And this complex getter escapes boxing when getter implementation was initially just reading the field. + * When type parameter is a type parameter of inline function or inline lambda. +* **Returning from not inline function.** *(This point does not apply to primary constructors of MFVC).* It happens because it is impossible to pass several objects between frames in JVM. The exception is a chain of MFVC getters: the compiler replaces `wrapper.segment.p1.x` with getter ``wrapper.`getSegment-p1-x`()`` instead of `wrapper.getSegment().getP1().getX()`. And this complex getter escapes boxing when getter implementation was initially just reading the field. * **Secondary constructors.** As they are replaced with non-inline function. * **Passing returned boxed value to another function that expects boxed parameter.** Intermediate value isn’t unboxed due to optimization because it would be boxed back otherwise. * **Lateinit variables and properties.** Because nullable types are stored. @@ -201,11 +235,11 @@ Kotlin reflection is not supported for MFVC yet but will be supported soon. ## Methods from `kotlin.Any` -As it is done for [inline classes](https://github.com/Kotlin/KEEP/blob/master/proposals/inline-classes.md#methods-from-kotlinany), compiler generates `toString`, `hashCode`, `equals` (but not component functions because positional destructuring is considered harmful). `toString` and `hashCode` can be customized by overriding, customization of equals will be possible by using feature “typed equals” (which takes the corresponding type as parameter instead of `Any?`) that is in development. +As it is done for [inline classes](https://github.com/Kotlin/KEEP/blob/master/proposals/inline-classes.md#methods-from-kotlinany), the compiler generates `toString`, `hashCode`, `equals` (but not component functions as described [above](#value-classes)). `toString` and `hashCode` can be customized by overriding, customization of equals will be possible by using feature “typed equals” (which takes the corresponding type as parameter instead of `Any?`) that is in development. ## Arrays of Value Classes -This paragraph is actual for both inline classes and multi-field value classes so they are generified to value classes here. Value arrays of value classes are reified classes which are not implemented yet. They also cause many other design questions because of necessity of value Lists etc. So there are no value arrays (`VArray'`s) for now and it is forbidden to use `vararg` for value classes. The same thing is already [written](https://github.com/Kotlin/KEEP/blob/master/proposals/inline-classes.md#arrays-of-inline-class-values) for Inline Classes. +This paragraph is actual for both inline classes and multi-field value classes, so they are generified to value classes here. Value arrays of value classes are reified classes which are not implemented yet. They also cause many other design questions because of necessity of value Lists etc. So there are no [value arrays](#further-development--varray-s-and-reification) (`VArray`s) for now and it is forbidden to use `vararg` for value classes. The same thing is already [written](https://github.com/Kotlin/KEEP/blob/master/proposals/inline-classes.md#arrays-of-inline-class-values) for Inline Classes. ## Expect/Actual MFVC @@ -219,19 +253,19 @@ As for [Inline Classes](https://github.com/Kotlin/KEEP/blob/master/proposals/inl ## MFVC ABI (JVM) -It is not always possible to pass multi-field value class (MFVC) values to functions as unboxed. The limitations are listed above in [Boxing](#Boxing) section. +It is not always possible to pass MFVC values to functions as unboxed. The limitations are listed above in [Boxing](#Boxing) section. -Also, there are strong limitations for returning MFVC from functions: it is forbidden to return multiple values from function in JVM. However, if we could never cope with it, all the optimizations would be useless: if a value was boxed once, there is no reason to escape boxing because using the existing box guarantees that no more boxes for this value are created. +Also, there are strong limitations for returning MFVC from functions: it is forbidden to return multiple values from function in JVM. However, if we could never cope with it, all the optimizations would be useless: if a value was boxed once, then there is no reason to escape boxing because using the existing box guarantees that no more boxes for this value are created. Fortunately, there are several workarounds: -* Using shadow stacks. Benchmark showed that it is even slower than using a box that is rather cheap in modern JVMs. +* Using shadow stacks. A benchmark showed that it is even slower than using a box that is rather cheap in modern JVMs. * Using local variables for the calling site. It is only possible for inline functions as they are in the same stack frame. -* Getting result MFVC from arguments at the calling site. It is only possible for primary constructors of MFV. They are the main and initial source of all MFVCs. +* Getting result MFVC from arguments at the calling site. It is only possible for primary constructors of MFVC. They are the main and initial source of all MFVCs. Primary constructors do two things: field initialization and side effects. They can be separated and the latter is stored in so-called `/*static*/ fun constructor-impl(...): Unit` which takes field values as parameters and performs side effects. So MFVC primary constructor calls can be replaced with storing arguments in variables and then calling the `constructor-impl` function on them. -The `constructor-impl` must be generated even if it is empty because public ABI must not depend on the body inisides. +The `constructor-impl` calls must be generated even if it is empty because public ABI must not depend on the body insides. ### (Un)boxing functions @@ -239,7 +273,7 @@ Every MFVC can exist in 2 forms: boxed (single field, variable, parameter) and Conversion from unboxed one into boxed one is rather simple: calling static synthetic function `box-impl` with necessary type and value arguments. However, there are several points that make synthetic unboxing functions `unbox-impl-[...]` more complicated. -#### Choice between `unbox-impl` and getter. +#### The choice between `unbox-impl` and getter. ```kotlin @JvmInline @@ -328,7 +362,7 @@ r.mfvc1.mfvc2 // option #1: Mfvc2.`box-impl`(r.`getX-0`(), r.`getX-1`()) creates So, it is necessary to create intermediate MFVC unbox methods for nested MFVC properties. -Furthermore, if they existed, they could be overridden to optimize nested access. The methods must also have default implementations that created intermediate boxes. This would allow such properties to be overridden in Java or by non-trivial getters. It would also help to save binary compatibility. As MFVC cannot have properties with custom getter and backing field simultaneously, the only cause of choice between effective and default implementation is whether the current regular class property getter is default or not. So, when it becomes default/not default subgetters of the current class change there implementation to effective/default and do not affect other classes. +Furthermore, if they existed, they could be overridden to optimize nested access. The methods must also have default implementations that create intermediate boxes. This would allow such properties to be overridden in Java or by non-trivial getters. It would also help to save binary compatibility. As MFVC cannot have properties with custom getter and backing field simultaneously, the only cause of choice between effective and default implementation is whether the current regular class property getter is default or not. So, when it becomes default/not default subgetters of the current class change there implementation to effective/default and do not affect other classes. Naming unbox methods becomes more difficult with nested MFVC. A node in MFVC tree can be identified by its path from the root (e.g. `["mfvc1", "mfvc2", "x"]` for the example above). Each of elements of a path can be identified by its name or by its index. Better readability is necessary in stack traces. So, names are chosen. @@ -375,11 +409,11 @@ SomeClass { `getSegment` is not mangled as MFVCs, returned from functions, are always boxed. -`f(x/*: DPoint*/)` can be compiled into `f(x.unbox-impl-x(), x.unbox-impl-y())` and `f(x.getX(), x.getY())`. If the `getX` or `getY` are not public necessity of unbox functions is obvious. But we actually always need them because if we call them instead of getters, we don't need to recompile existing calls when the name or visibility changes. +`f(x/*: DPoint*/)` can be compiled into `f(x.unbox-impl-x(), x.unbox-impl-y())` and `f(x.getX(), x.getY())`. If the `getX` or `getY` are not public, necessity of unbox functions is obvious. But we actually always need them because if we call them instead of getters, we do not need to recompile existing calls when the name or visibility changes. #### Optimization -When accessing a MFVC leaf from within the class it is better to use get-field directly than the unbox function as it is done for regular properties with backing fields. If the property is private, MFVC leaves will be also private and used only by kotlin-reflect. +When accessing a MFVC leaf from within the class, it is better to use get-field directly than the unbox function as it is done for regular properties with backing fields. If the property is private, MFVC leaves will be also private and used only by kotlin-reflect. #### Overriding example: @@ -451,7 +485,7 @@ fun f(x: Interface) = x.`getPoint-x`() * x.`getPoint-y`() ### Annotations -Each property and its parts (field, getter, setter) can be annotated. MFVC properties are not exceptions. +Each property and its parts (field, getter, setter) can be annotated. MFVC properties are not the exceptions. Annotating MFVC leaves does not cause any problems because they are not transformed while annotating other MFVC properties are. @@ -460,19 +494,19 @@ Annotating non-leaves is currently forbidden due to lack of use-cases. ### Expression flattening As described above, the default representation of MFVC is flattened, so it is necessary to flatten existing expressions. -The easiest way is to take a box and then flatten it. but we should escape boxing when possible. +The easiest way is to take a box and then flatten it, but we should escape boxing when possible. -* If the expression is a primary constructor call, we flatten each of subfields recursively to variables and then call `constructor-impl` function. The flattened expressions are the read accesses to the variables. +* If the expression is a primary constructor call, we flatten each of subfields recursively to variables and then call `constructor-impl` function on them. The flattened expressions are the read accesses to the variables. * If the expression already has some flattened representation, we can just use it. For example, it could be some getter call that can be replaced with multiple unboxing functions calls. * If the expression is some block of statements, we flatten the last one recursively as it is the result of the block. * `when`, `if` and `try` expressions results are also flattened. * Otherwise, we should actually receive a boxed expression and then unbox it with unbox methods or field getting. -If we flatten not to variables themselves but we just want expressions, we may escape using variables when possible (keeping evaluation order the same): +If we flatten not to variables themselves, but we just want expressions, we may escape using variables when possible (keeping evaluation order the same): ```kotlin -*// Before: +// Before: val a = 2 val b = 3 val c = b + 1 @@ -484,7 +518,7 @@ val b = 3 // [a, b, b + 1]* ``` -If the flattened representation is used to reset (not initialize) a variable/field, new value representation must be stored in variables. If an exception is thrown during evaluation of the new value, the old value must be kept. The tail of flattened expressions that cannot throw an error is inlined (variables are not created). +If the flattened representation is used to reset (not initialize) a variable/field, the new value representation must be firstly stored in temporary variables. If an exception is thrown during evaluation of the new value, the old value must be kept. The tail of flattened expressions that cannot throw an error is inlined (variables are not created). If boxing result is not needed, boxing is not called. @@ -586,6 +620,8 @@ If necessary, bridges are generated. There is an additional function `equals-impl0` for equals replacement that takes both parameters flattened. It is called instead of `==` when possible. If the right argument is MFVC and the left one is literal null, right argument is not boxed. Also, if right argument is unboxed `MFVC`, and the left one is `MFVC?`, the compiler generates an if expression, that checks the left for being null and then uses `equals-impl0` if it is not. +If there is a user-defined typed equals, no `equals-impl0` is generated, and the user function is renamed to `equals-impl0`. + #### String concatenation Concatenating string and MFVC produces effective `toString` replacement function calls to escape boxing. From 0c72dd1bd17e37d512a7998af573b64e6673cf67 Mon Sep 17 00:00:00 2001 From: "Evgeniy.Zhelenskiy" Date: Fri, 13 Jan 2023 07:13:49 +0100 Subject: [PATCH 06/22] Add examples to MFVC KEEP --- proposals/multi-field-value-classes.md | 54 ++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/proposals/multi-field-value-classes.md b/proposals/multi-field-value-classes.md index 89e688a99..6c70c497c 100644 --- a/proposals/multi-field-value-classes.md +++ b/proposals/multi-field-value-classes.md @@ -174,6 +174,60 @@ fun > split(tree: Tree, key: T): (left: Tree, right: Tree } ``` +### Existing applications + +As Kotlin language is widespread, there are existing pieces in code-bases, where user suffers from lack of the MFVC and further features. + +Links to graphics-related classes are shown without class names, because they are very specific and say to the framework. + +#### [Jetpack Compose](https://developer.android.com/jetpack/compose) + +Jetpack Compose is Android’s recommended modern toolkit for building native UI. It simplifies and accelerates UI development on Android. + +User interface requires frames to be rendered fast to keep proper FPS, thus the smallest possible number of intermediate allocations that may trigger GC and freeze the screen. + +Jetpack Compose uses lots of graphics primitives and several optimization strategies for them: + +If a graphical primitive contains single `Int`, `Long`, `Float`, `Double` inside, it is reasonably an inline class. + +If it contains two `Float`s or two `Int`s, it uses inline class with `Long` storage: [1](https://github.com/JetBrains/androidx/blob/458f93113905b5753b999df784aeb92dab51cf65/compose/ui/ui-unit/src/commonMain/kotlin/androidx/compose/ui/unit/Dp.kt#L261), [2](https://github.com/JetBrains/androidx/blob/458f93113905b5753b999df784aeb92dab51cf65/compose/ui/ui-unit/src/commonMain/kotlin/androidx/compose/ui/unit/Dp.kt#L379), [3](https://github.com/JetBrains/androidx/blob/458f93113905b5753b999df784aeb92dab51cf65/compose/ui/ui-unit/src/commonMain/kotlin/androidx/compose/ui/unit/IntOffset.kt#L46), [4](https://github.com/JetBrains/androidx/blob/458f93113905b5753b999df784aeb92dab51cf65/compose/ui/ui-unit/src/commonMain/kotlin/androidx/compose/ui/unit/IntSize.kt#L39), [5](https://github.com/JetBrains/androidx/blob/458f93113905b5753b999df784aeb92dab51cf65/compose/ui/ui-unit/src/commonMain/kotlin/androidx/compose/ui/unit/TextUnit.kt#L79), [6](https://github.com/JetBrains/androidx/blob/458f93113905b5753b999df784aeb92dab51cf65/compose/ui/ui-unit/src/commonMain/kotlin/androidx/compose/ui/unit/Constraints.kt#L54), [7](https://github.com/JetBrains/androidx/blob/458f93113905b5753b999df784aeb92dab51cf65/compose/ui/ui-geometry/src/commonMain/kotlin/androidx/compose/ui/geometry/Size.kt#L42), [8](https://github.com/JetBrains/androidx/blob/458f93113905b5753b999df784aeb92dab51cf65/compose/ui/ui-geometry/src/commonMain/kotlin/androidx/compose/ui/geometry/Offset.kt#L61), [9](https://github.com/JetBrains/androidx/blob/458f93113905b5753b999df784aeb92dab51cf65/compose/ui/ui-geometry/src/commonMain/kotlin/androidx/compose/ui/geometry/CornerRadius.kt#L44), [10](https://github.com/JetBrains/androidx/blob/458f93113905b5753b999df784aeb92dab51cf65/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Color.kt#L115), [11](https://github.com/JetBrains/androidx/blob/458f93113905b5753b999df784aeb92dab51cf65/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/colorspace/ColorModel.kt#L32), [12](https://github.com/JetBrains/androidx/blob/458f93113905b5753b999df784aeb92dab51cf65/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/TextRange.kt#L46). Some other classes could also benefit from the strategy: [1](https://github.com/JetBrains/androidx/blob/458f93113905b5753b999df784aeb92dab51cf65/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/colorspace/WhitePoint.kt#L26), [2](https://github.com/JetBrains/androidx/blob/458f93113905b5753b999df784aeb92dab51cf65/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/style/LineHeightStyle.kt#L39), [3](https://github.com/JetBrains/androidx/blob/458f93113905b5753b999df784aeb92dab51cf65/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/style/TextGeometricTransform.kt#L33), [4](https://github.com/JetBrains/androidx/blob/458f93113905b5753b999df784aeb92dab51cf65/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/style/TextIndent.kt#L32). + +Such solution is applicable only for 2 `Float`s, 2 `Int`s, 2..4 `Short`s, 2..4 `Char`s, 2..8 `Byte`s, 2..64 `Boolean`s and thus is very limited. It offers more optimization than MFVC can offer because it has only one underlying value that can be successfully returned from any function. However, it breaks one of the design goals of JVM - preventing [word tearing](https://docs.oracle.com/javase/specs/jls/se8/html/jls-17.html#jls-17.6), thus it is not done by default for MFVC. Nevertheless, if users find this packing strategy useful, it is possible to add annotation that will do it automatically. + +Jetpack compose has examples when this limitation is broken, and usual boxing `data class` (or its manual analogue without component/copy functions) is used: [1](https://github.com/JetBrains/androidx/blob/458f93113905b5753b999df784aeb92dab51cf65/compose/ui/ui-unit/src/commonMain/kotlin/androidx/compose/ui/unit/Dp.kt#L520), [2](https://github.com/JetBrains/androidx/blob/458f93113905b5753b999df784aeb92dab51cf65/compose/ui/ui-unit/src/commonMain/kotlin/androidx/compose/ui/unit/IntRect.kt#L33), [3](https://github.com/JetBrains/androidx/blob/458f93113905b5753b999df784aeb92dab51cf65/compose/ui/ui-geometry/src/commonMain/kotlin/androidx/compose/ui/geometry/Rect.kt#L32), [4](https://github.com/JetBrains/androidx/blob/458f93113905b5753b999df784aeb92dab51cf65/compose/ui/ui-geometry/src/commonMain/kotlin/androidx/compose/ui/geometry/RoundRect.kt#L29), [5](https://github.com/JetBrains/androidx/blob/458f93113905b5753b999df784aeb92dab51cf65/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Shadow.kt#L29), [6](https://github.com/JetBrains/androidx/blob/458f93113905b5753b999df784aeb92dab51cf65/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/PixelMap.kt#L36), [7](https://github.com/JetBrains/androidx/blob/458f93113905b5753b999df784aeb92dab51cf65/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/colorspace/TransferParameters.kt#L35), [8](https://github.com/JetBrains/androidx/blob/458f93113905b5753b999df784aeb92dab51cf65/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/Placeholder.kt#L37). + +[This class](https://github.com/JetBrains/androidx/blob/458f93113905b5753b999df784aeb92dab51cf65/compose/ui/ui-geometry/src/commonMain/kotlin/androidx/compose/ui/geometry/MutableRect.kt#L32) is mutable to allow convenient syntax and reuse the wrapper. If it was MFVC, the first problem would have been solved by lens syntax, the second one would not exist at all. + +[This class](https://github.com/JetBrains/androidx/blob/458f93113905b5753b999df784aeb92dab51cf65/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Vertices.kt#L23) suffers from lack of `VArray`s of Value classes and imitates them manually. + +### [KorGE](https://korge.org/) + +Korge is a modern multiplatform game engine for Kotlin. + +Game engines also require small number of allocations as Compose. Furthermore, it manipulates geometry objects and collects of them so it must do it effectively. Actually it does it by manual specialization for these hand-made classes. + +Specializations: [VectorArrayList](https://github.com/korlibs/korge/blob/109d582eb8b6810403f406ad28872b480bb9fb8b/korma/src/commonMain/kotlin/com/soywiz/korma/geom/VectorArrayList.kt#L33), [IntSegmentSet](https://github.com/korlibs/korge/blob/109d582eb8b6810403f406ad28872b480bb9fb8b/korma/src/commonMain/kotlin/com/soywiz/korma/segment/IntSegmentSet.kt#L14), [PointArrayList](https://github.com/korlibs/korge/blob/109d582eb8b6810403f406ad28872b480bb9fb8b/korma/src/commonMain/kotlin/com/soywiz/korma/geom/PointArrayList.kt#L96), [PointIntArrayList](https://github.com/korlibs/korge/blob/109d582eb8b6810403f406ad28872b480bb9fb8b/korma/src/commonMain/kotlin/com/soywiz/korma/geom/PointArrayList.kt#L295), [TriangleList](https://github.com/korlibs/korge/blob/109d582eb8b6810403f406ad28872b480bb9fb8b/korma/src/commonMain/kotlin/com/soywiz/korma/geom/triangle/Triangle.kt#L214). + +Graphics: [1](https://github.com/korlibs/korge/blob/109d582eb8b6810403f406ad28872b480bb9fb8b/korma/src/commonMain/kotlin/com/soywiz/korma/length/Length.kt#L199), [2](https://github.com/korlibs/korge/blob/109d582eb8b6810403f406ad28872b480bb9fb8b/korma/src/commonMain/kotlin/com/soywiz/korma/length/Length.kt#L215), [3](https://github.com/korlibs/korge/blob/109d582eb8b6810403f406ad28872b480bb9fb8b/korma/src/commonMain/kotlin/com/soywiz/korma/length/Length.kt#L217), [4](https://github.com/korlibs/korge/blob/109d582eb8b6810403f406ad28872b480bb9fb8b/korma/src/commonMain/kotlin/com/soywiz/korma/geom/Size.kt#L29), [5](https://github.com/korlibs/korge/blob/109d582eb8b6810403f406ad28872b480bb9fb8b/korma/src/commonMain/kotlin/com/soywiz/korma/geom/Size.kt#L82), [6](https://github.com/korlibs/korge/blob/109d582eb8b6810403f406ad28872b480bb9fb8b/korma/src/commonMain/kotlin/com/soywiz/korma/geom/Margin.kt#L33)\*, [7](https://github.com/korlibs/korge/blob/109d582eb8b6810403f406ad28872b480bb9fb8b/korma/src/commonMain/kotlin/com/soywiz/korma/geom/Margin.kt#L74)\*, [8](https://github.com/korlibs/korge/blob/109d582eb8b6810403f406ad28872b480bb9fb8b/korma/src/commonMain/kotlin/com/soywiz/korma/geom/Anchor.kt#L6). + +Mathematics: [Quaternion](https://github.com/korlibs/korge/blob/109d582eb8b6810403f406ad28872b480bb9fb8b/korma/src/commonMain/kotlin/com/soywiz/korma/geom/Quaternion.kt#L13)\*, [Rectangle](https://github.com/korlibs/korge/blob/109d582eb8b6810403f406ad28872b480bb9fb8b/korma/src/commonMain/kotlin/com/soywiz/korma/geom/Rectangle.kt#L13)\*, [RectangleInt](https://github.com/korlibs/korge/blob/109d582eb8b6810403f406ad28872b480bb9fb8b/korma/src/commonMain/kotlin/com/soywiz/korma/geom/Rectangle.kt#L364), [MutableRectCorners](https://github.com/korlibs/korge/blob/109d582eb8b6810403f406ad28872b480bb9fb8b/korma/src/commonMain/kotlin/com/soywiz/korma/geom/RectCorners.kt#L35)\*, [Ray3D](https://github.com/korlibs/korge/blob/109d582eb8b6810403f406ad28872b480bb9fb8b/korma/src/commonMain/kotlin/com/soywiz/korma/geom/Ray3D.kt#L3), [Ray](https://github.com/korlibs/korge/blob/109d582eb8b6810403f406ad28872b480bb9fb8b/korma/src/commonMain/kotlin/com/soywiz/korma/geom/Ray.kt#L3), [Point](https://github.com/korlibs/korge/blob/109d582eb8b6810403f406ad28872b480bb9fb8b/korma/src/commonMain/kotlin/com/soywiz/korma/geom/Point.kt#L111), [PointInt](https://github.com/korlibs/korge/blob/109d582eb8b6810403f406ad28872b480bb9fb8b/korma/src/commonMain/kotlin/com/soywiz/korma/geom/Point.kt#L361), [Matrix](https://github.com/korlibs/korge/blob/109d582eb8b6810403f406ad28872b480bb9fb8b/korma/src/commonMain/kotlin/com/soywiz/korma/geom/Matrix.kt#L18)\*, [Line](https://github.com/korlibs/korge/blob/109d582eb8b6810403f406ad28872b480bb9fb8b/korma/src/commonMain/kotlin/com/soywiz/korma/geom/Line.kt#L13), [LineIntersection](https://github.com/korlibs/korge/blob/109d582eb8b6810403f406ad28872b480bb9fb8b/korma/src/commonMain/kotlin/com/soywiz/korma/geom/Line.kt#L181), [EulerRotation](https://github.com/korlibs/korge/blob/109d582eb8b6810403f406ad28872b480bb9fb8b/korma/src/commonMain/kotlin/com/soywiz/korma/geom/EulerRotation.kt#L3)\*, [Circle](https://github.com/korlibs/korge/blob/109d582eb8b6810403f406ad28872b480bb9fb8b/korma/src/commonMain/kotlin/com/soywiz/korma/geom/Circle.kt#L12), [Bounding volume hierarchy](https://github.com/korlibs/korge/blob/109d582eb8b6810403f406ad28872b480bb9fb8b/korma/src/commonMain/kotlin/com/soywiz/korma/geom/AABB3D.kt#L7), [Edge](https://github.com/korlibs/korge/blob/109d582eb8b6810403f406ad28872b480bb9fb8b/korma/src/commonMain/kotlin/com/soywiz/korma/geom/vector/Edge.kt#L19)\*, [Edge](https://github.com/korlibs/korge/blob/109d582eb8b6810403f406ad28872b480bb9fb8b/korma/src/commonMain/kotlin/com/soywiz/korma/geom/triangle/Edge.kt#L9), [SegmentInt](https://github.com/korlibs/korge/blob/109d582eb8b6810403f406ad28872b480bb9fb8b/korma/src/commonMain/kotlin/com/soywiz/korma/geom/trapezoid/SegmentInt.kt#L5), [TrapezoidInt](https://github.com/korlibs/korge/blob/109d582eb8b6810403f406ad28872b480bb9fb8b/korma/src/commonMain/kotlin/com/soywiz/korma/geom/trapezoid/TrapezoidInt.kt#L17), [TriangleInt](https://github.com/korlibs/korge/blob/109d582eb8b6810403f406ad28872b480bb9fb8b/korma/src/commonMain/kotlin/com/soywiz/korma/geom/trapezoid/TriangleInt.kt#L3), [DoubleRangeExclusive](https://github.com/korlibs/korge/blob/109d582eb8b6810403f406ad28872b480bb9fb8b/korma/src/commonMain/kotlin/com/soywiz/korma/geom/range/OpenRange.kt#L5). + +Pools with reusable boxed wrappers: [PointPool](https://github.com/korlibs/korge/blob/109d582eb8b6810403f406ad28872b480bb9fb8b/korma/src/commonMain/kotlin/com/soywiz/korma/geom/PointPool.kt#L13), [MatrixPool](https://github.com/korlibs/korge/blob/109d582eb8b6810403f406ad28872b480bb9fb8b/korma/src/commonMain/kotlin/com/soywiz/korma/geom/Matrix.kt#L27). + +\* These classes are mutable to reuse boxes and for convenient mutating syntax as in [Jetpack Compose](#jetpack-compose). They still can be replaced with MFVC. + +### Other projects +* Time: [Timed value](https://github.com/JetBrains/kotlin/blob/30788566012c571aa1d3590912468d1ebe59983d/libraries/stdlib/src/kotlin/time/measureTime.kt#L68), [PickedTime](https://github.com/yuriykulikov/AlarmClock/blob/90f55fd2f36c0aded535226fda27dbb6bfa77227/app/src/main/java/com/better/alarm/presenter/PickedTime.kt#L3); +* Gaming: [Labyrinth](https://github.com/JetBrains/kotlin/blob/dd051c155640ff4b83f1cf1c730bd21387a49453/kotlin-native/performance/ring/src/main/kotlin/org/jetbrains/ring/CoordinatesSolver.kt#L26), [Chess](https://github.com/Kotlin-Polytech/KotlinAsFirst-Coursera/blob/39cc67d9b7a74b7f519ffd2e99dc1fd217a5a83f/src/lesson8/task2/Chess.kt#L9); +* [Graphics](https://github.com/CCBlueX/LiquidBounce/blob/f716f371b940e11445ddf791e8254d285ef955dc/src/main/kotlin/net/ccbluex/liquidbounce/render/engine/RenderTasks.kt#L124); +* [Tuples](https://github.com/airbnb/mavericks/blob/main/mvrx-common/src/main/java/com/airbnb/mvrx/MavericksTuples.kt); +* Metadata: [1](https://github.com/lingxiaoplus/BiliBili/blob/5a9aa28e06602f1d51e41553fb15d8914a75aa78/app/src/main/java/com/bilibili/lingxiao/play/model/VideoData.kt#L19), [2](https://github.com/wix/Detox/blob/085de8c4b8d6f691107aacd053176732faa13bfb/detox/android/detox/src/full/java/com/wix/detox/reactnative/ReactNativeInfo.kt#L6); +* Geometry: [1](https://github.com/RedApparat/Fotoapparat/blob/9454f3e4d1d222799049b00f3d84b274c3e02ee6/fotoapparat/src/main/java/io/fotoapparat/hardware/metering/PointF.kt#L6), [2](https://github.com/indy256/codelibrary/blob/master/kotlin/ConvexHull.kt), [3](https://github.com/Ramotion/navigation-toolbar-android/blob/4706c15209dff67e3f5ce191211fbb87dedfd13d/navigation-toolbar/src/main/kotlin/com/ramotion/navigationtoolbar/HeaderLayoutManager.kt#L70), [4](https://github.com/CCBlueX/LiquidBounce/blob/f716f371b940e11445ddf791e8254d285ef955dc/src/main/kotlin/net/ccbluex/liquidbounce/utils/aiming/RotationData.kt#L26), [5](https://github.com/data2viz/data2viz/blob/e549fa3bb4fd9ea73c27e261e38077b4cd819202/hierarchy/src/commonMain/kotlin/io/data2viz/hierarchy/pack/Enclose.kt#L24); +* [Credentials](https://github.com/http4k/http4k/blob/f7d6cf5d2e88bc66864dd180bc2faef203597e2d/http4k-core/src/main/kotlin/org/http4k/core/Credentials.kt#L3); +* Ranges: [Stdlib](https://github.com/JetBrains/kotlin/blob/30788566012c571aa1d3590912468d1ebe59983d/libraries/stdlib/src/kotlin/ranges/PrimitiveRanges.kt#L54), [Custom1](https://github.com/KronicDeth/intellij-elixir/blob/f4663b802aae61c833550feba199b9e0a06fba15/src/org/elixir_lang/NameArityRange.kt#L8), [Custom2](https://github.com/EmmyLua/IntelliJ-EmmyLua/blob/72e5fd324c3d31da99469ed08a48ae9656793f78/src/main/java/com/tang/intellij/lua/debugger/remote/commands/DefaultCommand.kt#L61); +* [Graphs](https://github.com/FunkyMuse/KAHelpers/blob/main/dataStructuresAndAlgorithms/src/main/java/com/crazylegend/datastructuresandalgorithms/graphs/Edge.kt); +* Other domain objects: [1](https://github.com/minecraft-dev/MinecraftDev/blob/3641836795b4ddffe79d7755de2852d5c55fa012/src/main/kotlin/platform/mcp/fabricloom/FabricLoomData.kt#L26), [2](https://github.com/spotify/heroic/blob/9a021a7a4acf643012cd0b2bfe8f59e7b6cfda89/heroic-component/src/main/java/com/spotify/heroic/metric/Point.kt#L24), [3](https://github.com/aws/aws-toolkit-jetbrains/blob/8dcb5bbc256dd30e2bd21db355300dfbcc5ba938/jetbrains-core/src/software/aws/toolkits/jetbrains/services/ecr/resources/EcrResources.kt#L28), [4](https://github.com/ethereum-lists/tokens/blob/9d8c86ab33fee60f54e77fb5001a2fbe32dea413/src/main/kotlin/org/ethereum/lists/tokens/model/Support.kt#L3), [5](https://github.com/microsoft/fluentui-android/blob/809bff2bff91cf20b821937f9a049dfe0b419f6b/fluentui_transients/src/main/java/com/microsoft/fluentui/tooltip/Tooltip.kt#L459). + ## Syntax Value classes are declared using soft keyword `value` as inline classes are. In Kotlin/JVM they also need `@JvmInline` annotation (several examples are above) before [Valhalla project](https://openjdk.org/projects/valhalla/) release. After the release, Valhalla value classes would become the default and expected way to create ones in Kotlin/JVM because they offer more optimizations than it is possible to do for pre-Valhalla value classes. So the latter are marked with annotations in Kotlin/JVM. From 023c7d253c193a1e8c2f8357902064558035fa96 Mon Sep 17 00:00:00 2001 From: "Evgeniy.Zhelenskiy" Date: Fri, 13 Jan 2023 16:07:00 +0100 Subject: [PATCH 07/22] Fix shallow immutability explanation of Value classes --- proposals/multi-field-value-classes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proposals/multi-field-value-classes.md b/proposals/multi-field-value-classes.md index 6c70c497c..6b923f6f2 100644 --- a/proposals/multi-field-value-classes.md +++ b/proposals/multi-field-value-classes.md @@ -37,7 +37,7 @@ Multi-field value classes are the next part of the Value classes feature after I Value classes are effective shallow-immutable data classes without identity, copy functions and component functions. -Shallow immutability is chosen to make it possible to implement internally mutable classes with immutable public API such as `Lazy`. However, there are domains where deep immutability is required, but allowing such requirement is currently outside the scope of the document. +Shallow immutability is chosen to make it possible to implement internally mutable classes with immutable public API such as using property of type `Lazy`: `value class SomeData(val x: Int, private val lazyY: Lazy) { val y get() = lazyY.value }`. However, there are domains where deep immutability is required, but allowing such requirement is currently outside the scope of the document. Value classes do not have `copy` methods due to their inconvenience for usage to mutate: * They are verbose and hard to understand when combined with constructions such as `if`s, loops. From 5c991c5f67673a43a436afb8603a2c2abbdd6f4b Mon Sep 17 00:00:00 2001 From: "Evgeniy.Zhelenskiy" Date: Fri, 13 Jan 2023 16:10:45 +0100 Subject: [PATCH 08/22] Remove draft title for MFVC --- proposals/multi-field-value-classes.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/proposals/multi-field-value-classes.md b/proposals/multi-field-value-classes.md index 6b923f6f2..c1a88f987 100644 --- a/proposals/multi-field-value-classes.md +++ b/proposals/multi-field-value-classes.md @@ -1,5 +1,3 @@ -# Multi-field value classes KEEP draft - # Multi-field value classes * **Type**: Design proposal From 69c6d8b388c00d58667e7e008914f201a04a71c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=9C=D0=B8?= =?UTF-8?q?=D1=85=D0=B0=D0=B9=D0=BB=D0=BE=D0=B2=D0=B8=D1=87=20=D0=96=D0=B5?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D1=81=D0=BA=D0=B8=D0=B9?= <55230817+zhelenskiy@users.noreply.github.com> Date: Tue, 17 Jan 2023 19:45:47 +0100 Subject: [PATCH 09/22] Rephrase proposals/multi-field-value-classes.md Co-authored-by: Ilmir Usmanov --- proposals/multi-field-value-classes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proposals/multi-field-value-classes.md b/proposals/multi-field-value-classes.md index c1a88f987..e38827017 100644 --- a/proposals/multi-field-value-classes.md +++ b/proposals/multi-field-value-classes.md @@ -10,7 +10,7 @@ Discussion of this proposal is held in [this issue](https://github.com/Kotlin/KE ### Inline Classes -Currently working [Inline Classes](https://github.com/Kotlin/KEEP/blob/master/proposals/inline-classes.md) allow making a performant identity-less type-safe wrapper with human-readable `toString` for a value of some type. It is performant because it stores the inner value directly when possible and differentiates the types in compile time. +Current [Inline Classes](https://github.com/Kotlin/KEEP/blob/master/proposals/inline-classes.md) are performant identity-less type-safe wrappers with human-readable `toString` for a value of some type. They are represented with raw (unboxed) value at runtime when possible, thus eliminating need for wrapper allocations. ```kotlin @JvmInline From d2619288c85886564a387a757a852be0671b0df5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=9C=D0=B8?= =?UTF-8?q?=D1=85=D0=B0=D0=B9=D0=BB=D0=BE=D0=B2=D0=B8=D1=87=20=D0=96=D0=B5?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D1=81=D0=BA=D0=B8=D0=B9?= <55230817+zhelenskiy@users.noreply.github.com> Date: Tue, 17 Jan 2023 19:46:19 +0100 Subject: [PATCH 10/22] Fix typos in proposals/multi-field-value-classes.md Co-authored-by: Ilmir Usmanov --- proposals/multi-field-value-classes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proposals/multi-field-value-classes.md b/proposals/multi-field-value-classes.md index e38827017..b6d55dc5f 100644 --- a/proposals/multi-field-value-classes.md +++ b/proposals/multi-field-value-classes.md @@ -27,7 +27,7 @@ Using `Password` class instead of raw `String` helps you not to pass invalid pas However, Inline classes are very limited: it may be useful to escape creating wrappers because of performance issues for classes with multiple fields too. Inline classes generalization is named [Value classes](https://github.com/Kotlin/KEEP/blob/master/notes/value-classes.md) (shallow immutable classes without identity). -As well, as for Inline classes, one of the main aims of MFVC is the compiler optimizations that are possible due to the lack of fixed identity. Unfortunately, there are several limitations for optimization for JVM target described below in the [Boxing](#boxing) section. +As well, as for Inline classes, one of the main aims of MFVC is compiler optimizations that are possible due to the lack of fixed identity. Unfortunately, there are several limitations for optimization for JVM target described below in the [Boxing](#boxing) section. Multi-field value classes are the next part of the Value classes feature after Inline classes. From fbd0967b7e6fa0ea649022047073aa964b150134 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=9C=D0=B8?= =?UTF-8?q?=D1=85=D0=B0=D0=B9=D0=BB=D0=BE=D0=B2=D0=B8=D1=87=20=D0=96=D0=B5?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D1=81=D0=BA=D0=B8=D0=B9?= <55230817+zhelenskiy@users.noreply.github.com> Date: Tue, 17 Jan 2023 19:47:59 +0100 Subject: [PATCH 11/22] Fix typo in proposals/multi-field-value-classes.md Co-authored-by: Ilmir Usmanov --- proposals/multi-field-value-classes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proposals/multi-field-value-classes.md b/proposals/multi-field-value-classes.md index b6d55dc5f..40a05e7b7 100644 --- a/proposals/multi-field-value-classes.md +++ b/proposals/multi-field-value-classes.md @@ -200,7 +200,7 @@ Jetpack compose has examples when this limitation is broken, and usual boxing `d ### [KorGE](https://korge.org/) -Korge is a modern multiplatform game engine for Kotlin. +KorGE is a modern multiplatform game engine for Kotlin. Game engines also require small number of allocations as Compose. Furthermore, it manipulates geometry objects and collects of them so it must do it effectively. Actually it does it by manual specialization for these hand-made classes. From f31434909efb4d71d3cf47f9fac85a8dc5f00c92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=9C=D0=B8?= =?UTF-8?q?=D1=85=D0=B0=D0=B9=D0=BB=D0=BE=D0=B2=D0=B8=D1=87=20=D0=96=D0=B5?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D1=81=D0=BA=D0=B8=D0=B9?= <55230817+zhelenskiy@users.noreply.github.com> Date: Tue, 17 Jan 2023 19:48:24 +0100 Subject: [PATCH 12/22] Fix proposals/multi-field-value-classes.md Co-authored-by: Ilmir Usmanov --- proposals/multi-field-value-classes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proposals/multi-field-value-classes.md b/proposals/multi-field-value-classes.md index 40a05e7b7..56c1daac2 100644 --- a/proposals/multi-field-value-classes.md +++ b/proposals/multi-field-value-classes.md @@ -196,7 +196,7 @@ Jetpack compose has examples when this limitation is broken, and usual boxing `d [This class](https://github.com/JetBrains/androidx/blob/458f93113905b5753b999df784aeb92dab51cf65/compose/ui/ui-geometry/src/commonMain/kotlin/androidx/compose/ui/geometry/MutableRect.kt#L32) is mutable to allow convenient syntax and reuse the wrapper. If it was MFVC, the first problem would have been solved by lens syntax, the second one would not exist at all. -[This class](https://github.com/JetBrains/androidx/blob/458f93113905b5753b999df784aeb92dab51cf65/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Vertices.kt#L23) suffers from lack of `VArray`s of Value classes and imitates them manually. +[Vertices](https://github.com/JetBrains/androidx/blob/458f93113905b5753b999df784aeb92dab51cf65/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Vertices.kt#L23) suffers from lack of `VArray`s of Value classes and imitates them manually. ### [KorGE](https://korge.org/) From 4774a2eebd07a51dace1cbe024a0f1bddea57f97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=9C=D0=B8?= =?UTF-8?q?=D1=85=D0=B0=D0=B9=D0=BB=D0=BE=D0=B2=D0=B8=D1=87=20=D0=96=D0=B5?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D1=81=D0=BA=D0=B8=D0=B9?= <55230817+zhelenskiy@users.noreply.github.com> Date: Tue, 17 Jan 2023 19:48:41 +0100 Subject: [PATCH 13/22] Fix proposals/multi-field-value-classes.md Co-authored-by: Ilmir Usmanov --- proposals/multi-field-value-classes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proposals/multi-field-value-classes.md b/proposals/multi-field-value-classes.md index 56c1daac2..6df75c74a 100644 --- a/proposals/multi-field-value-classes.md +++ b/proposals/multi-field-value-classes.md @@ -194,7 +194,7 @@ Such solution is applicable only for 2 `Float`s, 2 `Int`s, 2..4 `Short`s, 2..4 ` Jetpack compose has examples when this limitation is broken, and usual boxing `data class` (or its manual analogue without component/copy functions) is used: [1](https://github.com/JetBrains/androidx/blob/458f93113905b5753b999df784aeb92dab51cf65/compose/ui/ui-unit/src/commonMain/kotlin/androidx/compose/ui/unit/Dp.kt#L520), [2](https://github.com/JetBrains/androidx/blob/458f93113905b5753b999df784aeb92dab51cf65/compose/ui/ui-unit/src/commonMain/kotlin/androidx/compose/ui/unit/IntRect.kt#L33), [3](https://github.com/JetBrains/androidx/blob/458f93113905b5753b999df784aeb92dab51cf65/compose/ui/ui-geometry/src/commonMain/kotlin/androidx/compose/ui/geometry/Rect.kt#L32), [4](https://github.com/JetBrains/androidx/blob/458f93113905b5753b999df784aeb92dab51cf65/compose/ui/ui-geometry/src/commonMain/kotlin/androidx/compose/ui/geometry/RoundRect.kt#L29), [5](https://github.com/JetBrains/androidx/blob/458f93113905b5753b999df784aeb92dab51cf65/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Shadow.kt#L29), [6](https://github.com/JetBrains/androidx/blob/458f93113905b5753b999df784aeb92dab51cf65/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/PixelMap.kt#L36), [7](https://github.com/JetBrains/androidx/blob/458f93113905b5753b999df784aeb92dab51cf65/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/colorspace/TransferParameters.kt#L35), [8](https://github.com/JetBrains/androidx/blob/458f93113905b5753b999df784aeb92dab51cf65/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/Placeholder.kt#L37). -[This class](https://github.com/JetBrains/androidx/blob/458f93113905b5753b999df784aeb92dab51cf65/compose/ui/ui-geometry/src/commonMain/kotlin/androidx/compose/ui/geometry/MutableRect.kt#L32) is mutable to allow convenient syntax and reuse the wrapper. If it was MFVC, the first problem would have been solved by lens syntax, the second one would not exist at all. +[MutableRect](https://github.com/JetBrains/androidx/blob/458f93113905b5753b999df784aeb92dab51cf65/compose/ui/ui-geometry/src/commonMain/kotlin/androidx/compose/ui/geometry/MutableRect.kt#L32) is mutable to allow convenient syntax and reuse the wrapper. If it was MFVC, the first problem would have been solved by lens syntax, the second one would not exist at all. [Vertices](https://github.com/JetBrains/androidx/blob/458f93113905b5753b999df784aeb92dab51cf65/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Vertices.kt#L23) suffers from lack of `VArray`s of Value classes and imitates them manually. From b1ecc693aec55d334d97c8cddabda9f3141e3d40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=9C=D0=B8?= =?UTF-8?q?=D1=85=D0=B0=D0=B9=D0=BB=D0=BE=D0=B2=D0=B8=D1=87=20=D0=96=D0=B5?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D1=81=D0=BA=D0=B8=D0=B9?= <55230817+zhelenskiy@users.noreply.github.com> Date: Tue, 17 Jan 2023 19:54:36 +0100 Subject: [PATCH 14/22] Fix proposals/multi-field-value-classes.md Co-authored-by: Ilmir Usmanov --- proposals/multi-field-value-classes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proposals/multi-field-value-classes.md b/proposals/multi-field-value-classes.md index 6df75c74a..56fc05f05 100644 --- a/proposals/multi-field-value-classes.md +++ b/proposals/multi-field-value-classes.md @@ -52,7 +52,7 @@ Customizing `equals` is not allowed until “typed equals” feature is released One of other important steps of Value classes feature are Value arrays (`VArray`s) and reified classes and reified not-inline functions. They significantly extend applicability and optimizations of Value classes, **but they require MFVC to be already implemented** for that. Their importance is caused by frequent usage of containers that store and use stored values: they are currently handle value classes as boxed. -`VArray` is the fundamental container and first reified class. `VArray`s solve the problem of effective arrays (with invariant type parameter) without necessity of declaring manual [`UIntArray`](https://github.com/JetBrains/kotlin/blob/30788566012c571aa1d3590912468d1ebe59983d/libraries/stdlib/unsigned/src/kotlin/UIntArray.kt#L15) for each type `UInt`. It also generifies existing `IntArray`, `LongArray`, `BooleanArray` etc. with scalable general `VArray` which maps to `VArray`, `VArray`, `VArray` correspondingly. +`VArray` is the fundamental container and first reified class. `VArray`s solve the problem of effective arrays (with invariant type parameter) without necessity of declaring manual [`UIntArray`](https://github.com/JetBrains/kotlin/blob/master/libraries/stdlib/unsigned/src/kotlin/UIntArray.kt#L15) for each inline class type. It also generifies existing `IntArray`, `LongArray`, `BooleanArray` etc. with scalable general `VArray` which maps to `VArray`, `VArray`, `VArray` correspondingly. Having `VArray`s and reified functions also allows to write generic functions, extensions that operate on generic `VArray`: ```kotlin From f5c81f577274937a92c66a5843e4685f93f549d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=9C=D0=B8?= =?UTF-8?q?=D1=85=D0=B0=D0=B9=D0=BB=D0=BE=D0=B2=D0=B8=D1=87=20=D0=96=D0=B5?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D1=81=D0=BA=D0=B8=D0=B9?= <55230817+zhelenskiy@users.noreply.github.com> Date: Tue, 17 Jan 2023 20:02:31 +0100 Subject: [PATCH 15/22] Fix proposals/multi-field-value-classes.md Co-authored-by: Ilmir Usmanov --- proposals/multi-field-value-classes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proposals/multi-field-value-classes.md b/proposals/multi-field-value-classes.md index 56fc05f05..5af101f4f 100644 --- a/proposals/multi-field-value-classes.md +++ b/proposals/multi-field-value-classes.md @@ -192,7 +192,7 @@ If it contains two `Float`s or two `Int`s, it uses inline class with `Long` stor Such solution is applicable only for 2 `Float`s, 2 `Int`s, 2..4 `Short`s, 2..4 `Char`s, 2..8 `Byte`s, 2..64 `Boolean`s and thus is very limited. It offers more optimization than MFVC can offer because it has only one underlying value that can be successfully returned from any function. However, it breaks one of the design goals of JVM - preventing [word tearing](https://docs.oracle.com/javase/specs/jls/se8/html/jls-17.html#jls-17.6), thus it is not done by default for MFVC. Nevertheless, if users find this packing strategy useful, it is possible to add annotation that will do it automatically. -Jetpack compose has examples when this limitation is broken, and usual boxing `data class` (or its manual analogue without component/copy functions) is used: [1](https://github.com/JetBrains/androidx/blob/458f93113905b5753b999df784aeb92dab51cf65/compose/ui/ui-unit/src/commonMain/kotlin/androidx/compose/ui/unit/Dp.kt#L520), [2](https://github.com/JetBrains/androidx/blob/458f93113905b5753b999df784aeb92dab51cf65/compose/ui/ui-unit/src/commonMain/kotlin/androidx/compose/ui/unit/IntRect.kt#L33), [3](https://github.com/JetBrains/androidx/blob/458f93113905b5753b999df784aeb92dab51cf65/compose/ui/ui-geometry/src/commonMain/kotlin/androidx/compose/ui/geometry/Rect.kt#L32), [4](https://github.com/JetBrains/androidx/blob/458f93113905b5753b999df784aeb92dab51cf65/compose/ui/ui-geometry/src/commonMain/kotlin/androidx/compose/ui/geometry/RoundRect.kt#L29), [5](https://github.com/JetBrains/androidx/blob/458f93113905b5753b999df784aeb92dab51cf65/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Shadow.kt#L29), [6](https://github.com/JetBrains/androidx/blob/458f93113905b5753b999df784aeb92dab51cf65/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/PixelMap.kt#L36), [7](https://github.com/JetBrains/androidx/blob/458f93113905b5753b999df784aeb92dab51cf65/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/colorspace/TransferParameters.kt#L35), [8](https://github.com/JetBrains/androidx/blob/458f93113905b5753b999df784aeb92dab51cf65/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/Placeholder.kt#L37). +Jetpack Compose has examples when this limitation is broken, and usual boxing `data class` (or its manual analogue without component/copy functions) is used: [1](https://github.com/JetBrains/androidx/blob/458f93113905b5753b999df784aeb92dab51cf65/compose/ui/ui-unit/src/commonMain/kotlin/androidx/compose/ui/unit/Dp.kt#L520), [2](https://github.com/JetBrains/androidx/blob/458f93113905b5753b999df784aeb92dab51cf65/compose/ui/ui-unit/src/commonMain/kotlin/androidx/compose/ui/unit/IntRect.kt#L33), [3](https://github.com/JetBrains/androidx/blob/458f93113905b5753b999df784aeb92dab51cf65/compose/ui/ui-geometry/src/commonMain/kotlin/androidx/compose/ui/geometry/Rect.kt#L32), [4](https://github.com/JetBrains/androidx/blob/458f93113905b5753b999df784aeb92dab51cf65/compose/ui/ui-geometry/src/commonMain/kotlin/androidx/compose/ui/geometry/RoundRect.kt#L29), [5](https://github.com/JetBrains/androidx/blob/458f93113905b5753b999df784aeb92dab51cf65/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Shadow.kt#L29), [6](https://github.com/JetBrains/androidx/blob/458f93113905b5753b999df784aeb92dab51cf65/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/PixelMap.kt#L36), [7](https://github.com/JetBrains/androidx/blob/458f93113905b5753b999df784aeb92dab51cf65/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/colorspace/TransferParameters.kt#L35), [8](https://github.com/JetBrains/androidx/blob/458f93113905b5753b999df784aeb92dab51cf65/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/Placeholder.kt#L37). [MutableRect](https://github.com/JetBrains/androidx/blob/458f93113905b5753b999df784aeb92dab51cf65/compose/ui/ui-geometry/src/commonMain/kotlin/androidx/compose/ui/geometry/MutableRect.kt#L32) is mutable to allow convenient syntax and reuse the wrapper. If it was MFVC, the first problem would have been solved by lens syntax, the second one would not exist at all. From ecc530d5a41642c431d8dd47cc67a1591575470f Mon Sep 17 00:00:00 2001 From: "Evgeniy.Zhelenskiy" Date: Tue, 17 Jan 2023 20:14:28 +0100 Subject: [PATCH 16/22] Add comment about binary compatibility --- proposals/multi-field-value-classes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proposals/multi-field-value-classes.md b/proposals/multi-field-value-classes.md index 5af101f4f..38467ed66 100644 --- a/proposals/multi-field-value-classes.md +++ b/proposals/multi-field-value-classes.md @@ -228,7 +228,7 @@ Pools with reusable boxed wrappers: [PointPool](https://github.com/korlibs/korge ## Syntax -Value classes are declared using soft keyword `value` as inline classes are. In Kotlin/JVM they also need `@JvmInline` annotation (several examples are above) before [Valhalla project](https://openjdk.org/projects/valhalla/) release. After the release, Valhalla value classes would become the default and expected way to create ones in Kotlin/JVM because they offer more optimizations than it is possible to do for pre-Valhalla value classes. So the latter are marked with annotations in Kotlin/JVM. +Value classes are declared using soft keyword `value` as inline classes are. In Kotlin/JVM they also need `@JvmInline` annotation (several examples are above) before [Valhalla project](https://openjdk.org/projects/valhalla/) release. After the release, Valhalla value classes would become the default and expected way to create ones in Kotlin/JVM because they offer more optimizations than it is possible to do for pre-Valhalla value classes. So the latter are marked with annotations in Kotlin/JVM. Adding or removing the annotation will break binary compatibility. As any other class, value classes can declare member (except inner classes), be a member of some other class, have generics, extensions and other language features. From abe4e997a8c0a24ce615e3c784fb26d983f2207a Mon Sep 17 00:00:00 2001 From: "Evgeniy.Zhelenskiy" Date: Tue, 17 Jan 2023 20:23:42 +0100 Subject: [PATCH 17/22] Fix multi-field-value-classes.md --- proposals/multi-field-value-classes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proposals/multi-field-value-classes.md b/proposals/multi-field-value-classes.md index 38467ed66..91b7a4b37 100644 --- a/proposals/multi-field-value-classes.md +++ b/proposals/multi-field-value-classes.md @@ -190,7 +190,7 @@ If a graphical primitive contains single `Int`, `Long`, `Float`, `Double` inside If it contains two `Float`s or two `Int`s, it uses inline class with `Long` storage: [1](https://github.com/JetBrains/androidx/blob/458f93113905b5753b999df784aeb92dab51cf65/compose/ui/ui-unit/src/commonMain/kotlin/androidx/compose/ui/unit/Dp.kt#L261), [2](https://github.com/JetBrains/androidx/blob/458f93113905b5753b999df784aeb92dab51cf65/compose/ui/ui-unit/src/commonMain/kotlin/androidx/compose/ui/unit/Dp.kt#L379), [3](https://github.com/JetBrains/androidx/blob/458f93113905b5753b999df784aeb92dab51cf65/compose/ui/ui-unit/src/commonMain/kotlin/androidx/compose/ui/unit/IntOffset.kt#L46), [4](https://github.com/JetBrains/androidx/blob/458f93113905b5753b999df784aeb92dab51cf65/compose/ui/ui-unit/src/commonMain/kotlin/androidx/compose/ui/unit/IntSize.kt#L39), [5](https://github.com/JetBrains/androidx/blob/458f93113905b5753b999df784aeb92dab51cf65/compose/ui/ui-unit/src/commonMain/kotlin/androidx/compose/ui/unit/TextUnit.kt#L79), [6](https://github.com/JetBrains/androidx/blob/458f93113905b5753b999df784aeb92dab51cf65/compose/ui/ui-unit/src/commonMain/kotlin/androidx/compose/ui/unit/Constraints.kt#L54), [7](https://github.com/JetBrains/androidx/blob/458f93113905b5753b999df784aeb92dab51cf65/compose/ui/ui-geometry/src/commonMain/kotlin/androidx/compose/ui/geometry/Size.kt#L42), [8](https://github.com/JetBrains/androidx/blob/458f93113905b5753b999df784aeb92dab51cf65/compose/ui/ui-geometry/src/commonMain/kotlin/androidx/compose/ui/geometry/Offset.kt#L61), [9](https://github.com/JetBrains/androidx/blob/458f93113905b5753b999df784aeb92dab51cf65/compose/ui/ui-geometry/src/commonMain/kotlin/androidx/compose/ui/geometry/CornerRadius.kt#L44), [10](https://github.com/JetBrains/androidx/blob/458f93113905b5753b999df784aeb92dab51cf65/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Color.kt#L115), [11](https://github.com/JetBrains/androidx/blob/458f93113905b5753b999df784aeb92dab51cf65/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/colorspace/ColorModel.kt#L32), [12](https://github.com/JetBrains/androidx/blob/458f93113905b5753b999df784aeb92dab51cf65/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/TextRange.kt#L46). Some other classes could also benefit from the strategy: [1](https://github.com/JetBrains/androidx/blob/458f93113905b5753b999df784aeb92dab51cf65/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/colorspace/WhitePoint.kt#L26), [2](https://github.com/JetBrains/androidx/blob/458f93113905b5753b999df784aeb92dab51cf65/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/style/LineHeightStyle.kt#L39), [3](https://github.com/JetBrains/androidx/blob/458f93113905b5753b999df784aeb92dab51cf65/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/style/TextGeometricTransform.kt#L33), [4](https://github.com/JetBrains/androidx/blob/458f93113905b5753b999df784aeb92dab51cf65/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/style/TextIndent.kt#L32). -Such solution is applicable only for 2 `Float`s, 2 `Int`s, 2..4 `Short`s, 2..4 `Char`s, 2..8 `Byte`s, 2..64 `Boolean`s and thus is very limited. It offers more optimization than MFVC can offer because it has only one underlying value that can be successfully returned from any function. However, it breaks one of the design goals of JVM - preventing [word tearing](https://docs.oracle.com/javase/specs/jls/se8/html/jls-17.html#jls-17.6), thus it is not done by default for MFVC. Nevertheless, if users find this packing strategy useful, it is possible to add annotation that will do it automatically. +Such solution is applicable only for 2 `Float`s, 2 `Int`s, 2..4 `Short`s, 2..4 `Char`s, 2..8 `Byte`s, 2..64 `Boolean`s and thus is very limited. It offers more optimization than MFVC can offer because it has only one underlying value that can be successfully returned from any function. However, it breaks one of the design goals of JVM - preventing [word tearing](https://docs.oracle.com/javase/specs/jls/se8/html/jls-17.html#jls-17.6), thus it is not done by default for MFVC. Nevertheless, if users find this packing strategy useful, it is possible to add (not existing now) annotation that will do it automatically. Jetpack Compose has examples when this limitation is broken, and usual boxing `data class` (or its manual analogue without component/copy functions) is used: [1](https://github.com/JetBrains/androidx/blob/458f93113905b5753b999df784aeb92dab51cf65/compose/ui/ui-unit/src/commonMain/kotlin/androidx/compose/ui/unit/Dp.kt#L520), [2](https://github.com/JetBrains/androidx/blob/458f93113905b5753b999df784aeb92dab51cf65/compose/ui/ui-unit/src/commonMain/kotlin/androidx/compose/ui/unit/IntRect.kt#L33), [3](https://github.com/JetBrains/androidx/blob/458f93113905b5753b999df784aeb92dab51cf65/compose/ui/ui-geometry/src/commonMain/kotlin/androidx/compose/ui/geometry/Rect.kt#L32), [4](https://github.com/JetBrains/androidx/blob/458f93113905b5753b999df784aeb92dab51cf65/compose/ui/ui-geometry/src/commonMain/kotlin/androidx/compose/ui/geometry/RoundRect.kt#L29), [5](https://github.com/JetBrains/androidx/blob/458f93113905b5753b999df784aeb92dab51cf65/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Shadow.kt#L29), [6](https://github.com/JetBrains/androidx/blob/458f93113905b5753b999df784aeb92dab51cf65/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/PixelMap.kt#L36), [7](https://github.com/JetBrains/androidx/blob/458f93113905b5753b999df784aeb92dab51cf65/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/colorspace/TransferParameters.kt#L35), [8](https://github.com/JetBrains/androidx/blob/458f93113905b5753b999df784aeb92dab51cf65/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/Placeholder.kt#L37). From 70c65a3a65c986b545f656b962399590bc9385e8 Mon Sep 17 00:00:00 2001 From: "Evgeniy.Zhelenskiy" Date: Tue, 17 Jan 2023 20:35:23 +0100 Subject: [PATCH 18/22] Fix multi-field-value-classes.md --- proposals/multi-field-value-classes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proposals/multi-field-value-classes.md b/proposals/multi-field-value-classes.md index 91b7a4b37..3bab43ebb 100644 --- a/proposals/multi-field-value-classes.md +++ b/proposals/multi-field-value-classes.md @@ -46,7 +46,7 @@ Not yet implemented lens syntax is going to be used for convenient mutating muta Value classes do not have component functions as they are used for positional destructuring which is considered harmful because of exposing of the internal state and order of the fields. It will be replaced with not yet implemented named destructuring syntax. -Customizing `equals` is not allowed until “typed equals” feature is released. +Customizing `equals` is not allowed until “typed equals” feature is released. Customizing existing `equals` would lead to unavoidable boxing of the parameter. ### Further development: `VArray`s and reification From 98e153785dc2962c734274df6d0976cfff1038b6 Mon Sep 17 00:00:00 2001 From: "Evgeniy.Zhelenskiy" Date: Tue, 17 Jan 2023 20:54:38 +0100 Subject: [PATCH 19/22] Fix multi-field-value-classes.md --- proposals/multi-field-value-classes.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/proposals/multi-field-value-classes.md b/proposals/multi-field-value-classes.md index 3bab43ebb..c0b16e66f 100644 --- a/proposals/multi-field-value-classes.md +++ b/proposals/multi-field-value-classes.md @@ -68,7 +68,9 @@ Having reified functions for generics of primitive or value class types helps to There is [Project Valhalla](https://openjdk.org/projects/valhalla/) for JVM that suggests Value Classes that are similar to ones this proposal is about. It solves the same problems but on the runtime side (efficient user-defined identity-less objects, arrays of them, classes with them as fields, generic specialization). While the compiler is limited by JVM bytecode restrictions (cannot return several values from function) and uses the open world model (does not know all code that will be executed and usages of all classes, functions), the Valhalla-capable runtime is not limited and has closed world model (does know all executing code and usages of all classes, functions). It gives a great advantage in performing optimizations to the Valhalla-based Value classes. -However, it requires usage of the capable runtime, which is impracticable condition for Android, where runtime is still compatible with JVM 1.8 that was released in 2014. That is why Kotlin/JVM compiler needs to provide the functionality independent of Valhalla Project. Nevertheless, the latter solution is preferred, so simple migration to it must be possible. +Value classes without `@JvmInline` annotation will be supported and mapped to Valhalla Value classes after its release. + +However, these classes require usage of the capable runtime, which is impracticable condition for Android, where runtime is still compatible with JVM 1.8 that was released in 2014. That is why Kotlin/JVM compiler needs to provide the functionality independent of Valhalla Project. Nevertheless, the latter solution is preferred, so simple migration to it must be possible. ## Use cases *Since single field value classes are inline classes, which are already implemented, this KEEP describes further only multi-field value classes (MFVC).* From 5161072640741bcb194a1d78830ad6868df841b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=9C=D0=B8?= =?UTF-8?q?=D1=85=D0=B0=D0=B9=D0=BB=D0=BE=D0=B2=D0=B8=D1=87=20=D0=96=D0=B5?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D1=81=D0=BA=D0=B8=D0=B9?= <55230817+zhelenskiy@users.noreply.github.com> Date: Tue, 17 Jan 2023 21:01:55 +0100 Subject: [PATCH 20/22] Fix proposals/multi-field-value-classes.md Co-authored-by: Ilmir Usmanov --- proposals/multi-field-value-classes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proposals/multi-field-value-classes.md b/proposals/multi-field-value-classes.md index c0b16e66f..a9920ec9e 100644 --- a/proposals/multi-field-value-classes.md +++ b/proposals/multi-field-value-classes.md @@ -50,7 +50,7 @@ Customizing `equals` is not allowed until “typed equals” feature is released ### Further development: `VArray`s and reification -One of other important steps of Value classes feature are Value arrays (`VArray`s) and reified classes and reified not-inline functions. They significantly extend applicability and optimizations of Value classes, **but they require MFVC to be already implemented** for that. Their importance is caused by frequent usage of containers that store and use stored values: they are currently handle value classes as boxed. +One of other important steps of Value classes feature are Value arrays (`VArray`s) and reified classes and reified not-inline functions. They significantly extend applicability and optimizations of Value classes, **but they require MFVC to be already implemented** for that. Their importance is caused by frequent usage of containers that store and use stored values: they currently handle value classes as boxed. `VArray` is the fundamental container and first reified class. `VArray`s solve the problem of effective arrays (with invariant type parameter) without necessity of declaring manual [`UIntArray`](https://github.com/JetBrains/kotlin/blob/master/libraries/stdlib/unsigned/src/kotlin/UIntArray.kt#L15) for each inline class type. It also generifies existing `IntArray`, `LongArray`, `BooleanArray` etc. with scalable general `VArray` which maps to `VArray`, `VArray`, `VArray` correspondingly. From b818d6b8d82a31eec72ce83ba784839794fa7f65 Mon Sep 17 00:00:00 2001 From: "Evgeniy.Zhelenskiy" Date: Tue, 17 Jan 2023 21:08:46 +0100 Subject: [PATCH 21/22] Fix multi-field-value-classes.md --- proposals/multi-field-value-classes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proposals/multi-field-value-classes.md b/proposals/multi-field-value-classes.md index a9920ec9e..7b4e4f309 100644 --- a/proposals/multi-field-value-classes.md +++ b/proposals/multi-field-value-classes.md @@ -62,7 +62,7 @@ operator fun VArray.plus(other: VArray) = Having reified functions for generics of primitive or value class types helps to escape boxing. For example, being used within non-reified generic function, `x/*int*/ + y/*int*/` becomes `Integer.valueOf(x/*Integer*/.value, y/*Integer*/.value)`. So, it is better to generate specializations for such classes if the type parameters are marked as reified. However, reifying all parameters as it is done in C++ and Rust leads to huge code footprint and exponential growth of compilation time while it gives no significant performance boost for reference types. -`VArray` is not the only container or class the users may want to reify, thus reification of other classes shall also be possible in the future. +`VArray` is not the only container or class the users may want to reify, thus reification of other classes and interfaces (`List`, `ArrayList`, `Set`, `Point(x: T, y: T)`) shall also be possible in the future. ### Project Valhalla From eabdd245ed687b4114eefa1f4596097bf3b725c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=9C=D0=B8?= =?UTF-8?q?=D1=85=D0=B0=D0=B9=D0=BB=D0=BE=D0=B2=D0=B8=D1=87=20=D0=96=D0=B5?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D1=81=D0=BA=D0=B8=D0=B9?= <55230817+zhelenskiy@users.noreply.github.com> Date: Mon, 20 Feb 2023 08:00:23 +0100 Subject: [PATCH 22/22] Fix proposals/multi-field-value-classes.md Co-authored-by: Sergei Bulgakov --- proposals/multi-field-value-classes.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/proposals/multi-field-value-classes.md b/proposals/multi-field-value-classes.md index 7b4e4f309..b1473d039 100644 --- a/proposals/multi-field-value-classes.md +++ b/proposals/multi-field-value-classes.md @@ -151,10 +151,10 @@ value class Color( @JvmInline value class Gradient(val from: Color, val to: Color) -fun Gradient.colorAt(percentage: Double): Color { - require(percentage in 0.0..100.0) { "Invalid percentage: $percentage" } - val c1 = 100.0 - percentage - val c2 = percentage +fun Gradient.colorAt(ratio: Double): Color { + require(ratio in 0.0..1.0) { "Invalid ratio: $ratio" } + val c1 = 1.0 - ratio + val c2 = ratio return Color( alpha = (from.alpha.toDouble() * c1 + to.alpha.toDouble() * c2).toUByte(), red = (from.red.toDouble() * c1 + to.red.toDouble() * c2).toUByte(),