| # Applier target inferencing |
| |
| Composition produces a tree of nodes and uses an applier to build and make changes to the tree |
| implied by composition. If a composable function directly or indirectly calls `ComposeNode` then it |
| is tied to an applier that can handle the type of node being emitted by the |
| `ComposeNode`. `ComposeNode` contains a runtime check to ensure that the applier is of the expected |
| type. However, currently, there is no warning or error generated when a composition function is |
| called that expects a different applier than is provided. |
| |
| For Compose UI 1.0 this was not as important as there are only two appliers used by Compose UI, the |
| `UIApplier` and the `VectorApplier` and vector composable are rarely used so calling the incorrect |
| composable function is rare. However, as appliers proliferate, such as Wear projections, or menu |
| trees for Compose Desktop, the likelihood of calling an incorrect composable grows larger and |
| detecting these statically is more important. |
| |
| Requiring to specify the applier seems clumsy and inappropriate since, in 1.0, it wasn't necessary, |
| and especially since the inference rules are fairly simple. There is only one parameter that needs |
| to be determined, the type of the applier of the implied `$composer` parameter. From now on I will |
| be referring to this as the type of the applier when it technically is the type of the applier |
| instance used by the composer. In most cases the applier type is simply the applier type required |
| by composition functions it calls. For example, if a composable function calls `Text` it must be a |
| `UiApplier` composable because `Text` requires a `UIApplier`. |
| |
| ## Sketch of the algorithm |
| |
| The following Prolog program demonstrates how type type of the applier can be inferred from the |
| content of the function (https://swish.swi-prolog.org/p/Composer%20Inference.swinb): |
| |
| ``` |
| % An empty list can have any applier. |
| applier([], _). |
| |
| % A list elements must have identical appliers. |
| applier([H|T], A) :- applier(H, A), applier(T, A). |
| |
| % A layout has a uiApplier |
| applier(layout, uiApplier). |
| |
| % A layout with content has a uiApplier and its content must have a uiApplier. |
| applier(layout(C), uiApplier) :- applier(C, uiApplier). |
| |
| % A vector has a vector Applier. |
| applier(vector, vectorApplier). |
| |
| % A vector with content has a vector applier and its content must have a vector applier |
| applier(vector(C), vectorApplier) :- applier(C, vectorApplier). |
| ``` |
| |
| |
| The above corresponds to calling `ComposeNode` (from Layout.kt and Vector.kt) and can easily be |
| derived from the body of the call. Taking advantage of of Prolog's unification algorithm, this can |
| also express open composition functions like the `CompositionLocalProvider`, |
| |
| ``` |
| % provider provides information to its content for all appliers. |
| applier(provider(C), A) :- applier(C, A). |
| ``` |
| |
| This predicate binds `A` to whatever applier the content `C` requires. In other words, `A` is an |
| open applier bound by the lambda passed into `provider`. |
| |
| The above allows the validation that the composition function represented by, for example, |
| |
| ``` |
| program( |
| row([ |
| drawing([ |
| provider([ |
| circle, |
| square |
| ]) |
| ]) |
| ]) |
| ). |
| ``` |
| |
| |
| will not generate an applier runtime error (demonstrated in the link above). |
| |
| ## Declarations |
| |
| Inferring the applier type is translated into inferring one of two attributes for every composable |
| function or composable lambda type, `ComposableTarget` and `ComposableOpenTarget`. |
| |
| ``` |
| @Retention(AnnotationRetention.BINARY) |
| @Target( |
| AnnotationTarget.FUNCTION, |
| AnnotationTarget.PROPERTY_GETTER, |
| AnnotationTarget.TYPE, |
| AnnotationTarget.TYPE_PARAMETER, |
| ) |
| annotation class ComposableTarget(val applier: String) |
| ``` |
| |
| ``` |
| @Retention(AnnotationRetention.BINARY) |
| @Target( |
| AnnotationTarget.FUNCTION, |
| AnnotationTarget.PROPERTY_GETTER, |
| AnnotationTarget.TYPE, |
| AnnotationTarget.TYPE_PARAMETER, |
| ) |
| annotation class ComposableOpenTarget(val index: Int) |
| ``` |
| |
| ## Inferring appliers |
| |
| Every composable function has an applier scheme determined by the composable functions it calls or |
| by explicit declarations. The scheme for a function is the applier token or open token variable for |
| each `@Composable` function type in the description including the description itself and |
| corresponds to the applier required by the respective composable function. |
| |
| For the purposes of this document, the scheme is represented by a Prolog-like list with identifier |
| symbols being bound and [deBruijn](https://en.wikipedia.org/wiki/De_Bruijn_index) indices (written |
| as a backslash followed by the index such as `\1`) are unbound variables and unbound variables with |
| the same index bind together. A scheme is a list where the first element is the variable or token |
| for the `$composer` of the function followed by the schemes for each composable parameter (which |
| might also contain schemes for its parameters). |
| |
| For example, a composable function declared as, |
| |
| ``` |
| @Composable |
| @ComposableTarget("UI") |
| fun Text(value: String) { … } |
| ``` |
| |
| has the scheme `[UI]`. |
| |
| Open appliers, such as, |
| |
| ``` |
| @Composable |
| @ComposableOpenTarget(0) |
| fun Providers( |
| providers: vararg values: ProvidedValue<*>, |
| content: @ComposableOpenTarget(0) @Composable () -> Unit |
| ) { |
| … |
| content() |
| … |
| } |
| ``` |
| |
| Has the scheme `[\0, [\0]]` meaning the applier for `Providers` and the `content` lambda must be |
| the same but they can be any applier. Using these schemes an algorithm similar to the Prolog |
| algorithm can be implemented. That is, when type variables are bound, the variables are unified |
| using a normal unify algorithm. As backtracking is not needed so a simple mutating unify algorithm |
| can be used as once unification fails the failure is reported, the requested binding is ignored, |
| and the algorithm continues without backtracking. |
| |
| When inferring an applier scheme, |
| |
| 1. The applier schemes are determined for each composable call directly in the body of the |
| composable function. If the function is inferred recursively while its scheme is being inferred |
| then a scheme with all unique deBruijn indices matching the shape of the expected scheme is used |
| instead. |
| 2. A scheme is recursively inferred for all nested lambdas literals. |
| 3. Applier variables are created for the function and each composable lambda parameter of the |
| function. |
| 4. For each call, fresh applier variables are created and are bound as defined by the scheme |
| of the call; the symbols are bound and type variables with the same index in the scheme are |
| bound together. Then, |
| 1. the first fresh variable is bound to the function's variable |
| 2. the composable lambda parameters are bound to the lambda literals or variable references |
| 3. if a lambda expression is passed as a parameter it is treated as if it was called at the |
| point of the parameter expression evaluation using the scheme declared or inferred for the |
| type of the formal lambda parameter. If the expression is a lambda literal the variables are |
| bound to both the scheme inferred for the lambda literal and the scheme of the type. |
| 5. The scheme is created by resolving each applier variable for the function. If it is bound to a |
| token then the token is placed in the scheme. If it is bound to a still open variable then the |
| variable group is given a number (if it doesn't have one already) and the corresponding deBruijn |
| index is produced. A variable's group is the set of type variables that are bound together with |
| the type variable. An open type parameter can only be a member of one group as, once it is bound |
| to another variable, it either is bound to a token or the variable groups are merged together. |
| |
| For each function or type with an inferred scheme the declaration is augmented with attributes |
| where `CompositionTarget` is produced for tokens and `CompositionOpenTarget` is produced with the |
| deBruijn index. This records the information necessary to infer appliers across module boundaries. |
| |
| If any of the bindings fails (that is if it tries to unify to two different tokens) a diagnostic is |
| produced and the binding is ignored. Each variable can contain the location (e.g. PsiElement) that |
| it was created for. When the binding fails, the locations associated with each variable in the |
| binding group can be reported as being in error. |
| |
| # Implementation Notes |
| |
| ## `ComposableInferredTarget` |
| |
| Due to a limitation in how annotations are currentl handled in the Kotlin compiler, a plugin |
| cannot add annotation to types, specifically the lambda types in a composable function. To work |
| around this the plugin will infer a `ComposableInferredTarget` on the composable function which |
| contains the scheme, as described above, instead of adding annotations to the composable lambda |
| types in the function. For the target type checking described here `ComposableInferredTarget` |
| takes presedence over any other attriutes present on the function. The implementation dropped |
| the leading `\` from the deBruijn indexes, to save space, as they were unnecessary. |
| |
| ## Unification |
| |
| Because backtracking is not required, the unify algorithm implemented takes advantage of this |
| by using a circular-linked-list to track the binding variables (two circular-linked lists can |
| be concatenated together in `O(1)` time by swapping next pointer of just one element in each). |
| The bindings can be reversed (e.g. swapping the next pointers back), but the code is not |
| included to do so, so if backtracking is later required, `Bindings` would need to be updated |
| accordingly. Details are provided in the class. |