arrow-meta icon indicating copy to clipboard operation
arrow-meta copied to clipboard

Line of thought for KEEP-87

Open truizlop opened this issue 5 years ago • 0 comments

This issue is to keep track of the line of thought that I followed in the implementation of KEEP-87. Every step I took will be linked to one or more commits containing the implementation of the ideas outlined that, hopefully, will be easily relatable to how things are done with the compiler plugin.

These are the steps I took to implement KEEP-87.

1. Add Type classes feature to the language (link)

In order to be able to support the new feature and enable the new syntax, I had to create a language feature. The new keywords that are introduced are linked to this feature and I'd assume they are only enabled if this feature is selected. This is necessary in the case of experimental features, as can be seen in the other declarations in the same file.

Plugin version: I believe this won't be necessary in the compiler plugin, as we are not modifying the language and the application of the plugin is optional.

2. Add new keywords with and extension (link)

New tokens for the keywords with and extension were added. This automatically adds highlighting in IntelliJ IDEA.

Plugin version: As this will be implemented with annotations @with and @extension, we may not need to do anything.

3. Define applicability of with and extension (link)

I had to specify which elements can be modified with the keywords with and extension, namely value parameters (with) and classes/objects (extension).

Plugin version: similar to the previous point, since these keywords are added as annotations, I assume you have already constrained their applicability in the annotation definition.

4. Add flags for with and extension (link)

Internal representations for ClassDescriptor and ValueParameterDescriptor (together with a big cascade of changes related to different implementations of these interfaces) needed to be modified to encode the presence of the modifiers extension and with.

Plugin version: as we have the possibility to inspect which annotations are present during compile time, I guess this is not needed if that information is carried into ClassDescriptor and ValueParameterDescriptor (which, if I remember correctly, is present).

5. Disable reporting of unused values / classes (link)

Implicit arguments and type class instances are not explicitly used and therefore are marked with a warning as unused. These warnings are silenced.

Plugin version: I would say this could be out of the scope of this plugin, but should be possible to mark something as used if it is resolved by the resolution algorithm. I'd say this has lower priority than other tasks.

6. Modify the resolution algorithm to include implicit parameters in scope

Consider the following example:

interface Semigroup<A> {
  fun combine(x: A, y: A): A

  companion object {
    extension object IntSemigroup: Semigroup<Int> {
      override func combine(x: Int, y: Int): Int = x + y
    }
  }
}

fun combine(x: Int, y: Int): Int = x * y

fun dup(x: Int, with semigroup: Semigroup<Int>): Int = combine(x, x)
fun dup2(x: Int, semigroup: Semigroup<Int>): Int = combine(x, x)
fun dup3(x: Int, semigroup: Semigroup<Int>): Int = semigroup.combine(x, x)

val first = dup(5) // Should be 10
val second = dup(5, Semigroup.IntSemigroup) // Should be 10
val third = dup2(5, Semigroup.IntSemigroup) // Should be 25
val forth = dup3(5, Semigroup.IntSemigroup) // Should be 10

Calls need to be resolved in a different manner if there are implicit parameters in the scope:

  • In dup, the function call to combine must be the one defined in Semigroup, shadowing the combine function defined at package level.
  • In dup2 we have a semigroup parameter, but it is not marked as with, so the resolved combine is the one at package level.
  • In dup3 we are making explicit reference to the combine function from Semigroup.

What I did in this case was to simulate the presence of with(parameter) blocks wrapping up the function body, but being careful to respect the scope of this.

Plugin version: I modified BodyResolver, but not sure how it can be done in the case of the plugin.

7. Generate bytecode to use implicit parameters in function body (link)

Once that implicit parameters are correctly added to the scope, they need to be properly loaded in the stack for the function body.

Plugin version: not sure how the plugin API handles this.

8. Resolve and instantiate implicit parameters in function calls (link)

In an early version the resolution algorithm to find a candidate instance for a given parameter was done in the code generation stage. The algorithm had to be moved to an early stage of the compilation process in order to be able to report errors properly.

The description of this algorithm can be read in the text of the PR submitted to JetBrains.

Once candidates are uniquely resolved, they need to be instantiated in their corresponding position of the function call:

  • For extension object, there is a single instance of them that is loaded.
  • For extension class, their constructor needs to be invoked with the parameters they require. Most likely those parameters will be implicit as well, so the algorithm for resolution has to consider those cases.

Plugin version: for the first part, the resolution algorithm should be very similar to the one that is already implemented, as it is based on *Descriptors. For the second part, it is up to the possibilities offered to generated bytecode.

9. Other generalizations to the resolution algorithm and error reporting

  • Orphan instances in subpackages
  • Using implicits in closures / inlined lambdas
  • Resolving concrete instances (e.g. fun combine(x: Int, with IntSemigroup))
  • Resolving looking into the type / type class hierarchy

truizlop avatar Jun 06 '19 14:06 truizlop