|
| 1 | +--- |
| 2 | +layout: sip |
| 3 | +title: SIP XX - Improving binary compatibility with @binaryCompatible |
| 4 | +disqus: true |
| 5 | +--- |
| 6 | + |
| 7 | +__Dmitry Petrashko__ |
| 8 | + |
| 9 | +__first submitted 13 January 2017__ |
| 10 | + |
| 11 | +## Introduction ## |
| 12 | + |
| 13 | +Scala is a language which evolves fast and thus made a decision to only promise binary compatibility across minor releases. |
| 14 | +At the same time, there is a demand to develop APIs that live longer than a major release cycle of Scala. |
| 15 | +This SIP introduces an annotation `@binaryCompatible` that checks that `what you write is what you get`. |
| 16 | +It will fail compilation in case emitted methods or their signatures |
| 17 | +are different from those written by users. |
| 18 | +As long as signatures of methods in source is not changed, `@binaryCompatible` annotated class |
| 19 | +will be compatible across major version of Scala. |
| 20 | + |
| 21 | +## Use Cases |
| 22 | +In case there's a need to develop an API that will be used by clients compiled using different major versions of Scala, |
| 23 | +the current approach is to either develop them in Java or to use best guess to restrict what Scala features should be used. |
| 24 | +There's also a different approach which is used by SBT: instead of publishing a binary `compiler-interface`, sources are published instead |
| 25 | +that would be locally compiled. |
| 26 | + |
| 27 | +There's also a use-case of defining a class which is supposed to be also used from Java. |
| 28 | +`@binaryCompatible` will ensure that there are no not-expected methods that would show up in members of a class or an interface. |
| 29 | + |
| 30 | +Dotty currently uses java defined interfaces as public API for IntelliJ in order to ensure binary compatibility. |
| 31 | +These interfaces can be replaced by `@binaryCompatible` annotated traits to reach the same goal. |
| 32 | + |
| 33 | +## Design Guidelines |
| 34 | +`@binaryCompatible` is a feature which is supposed to be used by a small subset of the ecosystem to be binary compatible across major versions of Scala. |
| 35 | +Thus this is designed as an advanced feature that is used rarely and thus is intentionally verbose. |
| 36 | +It's designed to provide strong guarantees, in some cases sacrificing ease of use. |
| 37 | + |
| 38 | +The limitations enforced by `@binaryCompatible` are designed to be an overapproximation: |
| 39 | +instead of permitting a list of features known to be compatible, `@binaryCompatible` enforces a stronger |
| 40 | +check which is sufficient to promise binary compatibility. |
| 41 | + |
| 42 | +## Overview ## |
| 43 | +In order for a class or a trait to succeed compilation with the `@binaryCompatible` annotation it has to be: |
| 44 | + - defined on the top level; |
| 45 | + - use a subset of Scala that during compilation does not require changes to public API of the class, including |
| 46 | + - synthesizing new members, either concrete or abstract; |
| 47 | + - changing binary signatures of existing members, either concrete or abstract; |
| 48 | + |
| 49 | +`@binaryCompatible` does not change the compilation scheme of a class: |
| 50 | + compiling a class previously annotated with the `@binaryCompatible`, will produce the same bytecode with or without `@binaryCompatible` annotation. |
| 51 | + |
| 52 | +Below are several examples of classes and traits that succeed compilation with `@binaryCompatible` |
| 53 | +```scala |
| 54 | +{% highlight scala %} |
| 55 | +@binaryCompatible |
| 56 | +trait AbstractFile { |
| 57 | + def name(): String |
| 58 | + |
| 59 | + def path(): String |
| 60 | + |
| 61 | + def jfile(): Optional[File] |
| 62 | +} |
| 63 | + |
| 64 | +@binaryCompatible |
| 65 | +trait SourceFile extends AbstractFile { |
| 66 | + def content(): Array[Char] |
| 67 | +} |
| 68 | + |
| 69 | +@binaryCompatible |
| 70 | +trait Diagnostic { |
| 71 | + def message(): String |
| 72 | + |
| 73 | + def level(): Int |
| 74 | + |
| 75 | + def position(): Optional[SourcePosition] |
| 76 | +} |
| 77 | + |
| 78 | +@binaryCompatible |
| 79 | +object Diagnostic { |
| 80 | + @static final val ERROR: Int = 2 |
| 81 | + @static final val WARNING: Int = 1 |
| 82 | + @static final val INFO: Int = 0 |
| 83 | +} |
| 84 | + |
| 85 | +@binaryCompatible |
| 86 | +class FeaturesInBodies { |
| 87 | + def apiMethod: Int = { |
| 88 | + // as body of the method isn't part of the public interface, one can use all features of Scala here. |
| 89 | + lazy val result = 0 // while lazy vals are prohibited in the class, they are allowed in the bodies of methods |
| 90 | + result |
| 91 | + } |
| 92 | +} |
| 93 | +{% endhighlight %} |
| 94 | +``` |
| 95 | + |
| 96 | +## Features that will fail compilation with `@binaryCompatible` |
| 97 | +The features listed below have complex encodings that may change in future versions. We prefer not to compromise on them. |
| 98 | +Most of those features can be simulated in a binary compatible way by writing a verbose re-impelemtation |
| 99 | +which won't rely on desugaring performed inside compiler. |
| 100 | +Note that while those features are prohibited in the public API, they can be safely used inside bodies of the methods. |
| 101 | + |
| 102 | + - public fields. Can be simulated by explicitly defining public getters and setters that access a private field; |
| 103 | + - lazy vals. Can be simulated by explicitly writing an implementation in source; |
| 104 | + - case classes. Can be simulated by explicitly defining getters and other members synthesized for a case class(`copy`, `productArity`, `apply`, `unApply`, `unapply`). |
| 105 | + |
| 106 | +The features listed below cannot be easily re-implemented in a class or trait annotated with `@binaryCompatible`. |
| 107 | + - default arguments; |
| 108 | + - default methods. See Addendum; |
| 109 | + - constant types(both explicit and inferred); |
| 110 | + - inline. |
| 111 | + |
| 112 | +## `@binaryCompatible` and Scala.js |
| 113 | + |
| 114 | +Allowing to write API-defining classes in Scala instead of Java will allow them to compile with Scala.js, |
| 115 | +which would have benefit of sharing the same source for two ecosystems. |
| 116 | + |
| 117 | +Scala.js currently is binary compatible as long as original bytecode compiled by Scala JVM is binary compatible. |
| 118 | +Providing stronger binary compatibility guarantees for JVM will automatically provide stronger guarantees for Scala.js. |
| 119 | + |
| 120 | + |
| 121 | +## Comparison with MiMa ## |
| 122 | +The Migration Manager for Scala (MiMa in short) is a tool for diagnosing binary incompatibilities for Scala libraries. |
| 123 | +MiMa allows to compare binary APIs of two already compiled classfiles and reports errors if APIs do not match perfectly. |
| 124 | + |
| 125 | +MiMa and `@binaryCompatible` complement each other, as `@binaryCompatible` helps to develop APIs that stay compatible |
| 126 | +across major versions, while MiMa checks that previously published artifacts indeed have the same API. |
| 127 | + |
| 128 | +`@binaryCompatible` does not compare the currently compiled class or trait against previous version, |
| 129 | +so introduction of new members won't be prohibited. This is a use-case for MiMa. |
| 130 | + |
| 131 | +MiMa does not indicate how hard, if possible, would it be to maintain compatibility of a class across future versions of Scala. |
| 132 | +Multiple features of Scala, most notably lazy vals and traits, has been compiled diffently by different Scala versions |
| 133 | +making porting existing compiled bytecode across versions very hard. |
| 134 | +MiMa will complain retroactively that the new version is incompatible with the old one. |
| 135 | +`@binaryCompatible` will instead indicate at compile time that the old version had used features whose encoding is prone to change. |
| 136 | +This provides early guidance and warning when designing long-living APIs before they are publicly released. |
| 137 | + |
| 138 | +## Compilation scheme ## |
| 139 | +No modification of typer or any existing phase is planned. The current proposed scheme introduces a late phase that runs before the very bytecode emission that checks that: |
| 140 | + - classes and traits annotated as `@binaryCompatible` are on the top level; |
| 141 | + - no non-private members where introduced inside classes and traits annotated as `@binaryCompatible` by compiler using phase travel; |
| 142 | + - no non-private members inside classes and traits annotated as `@binaryCompatible` has changed their signature from the one written by developer. |
| 143 | + |
| 144 | +The current prototype is implemented for Dotty and supports everything descibed in this SIP. |
| 145 | +The implementation is simple with less than 50 lines of non-boilerplate code. |
| 146 | +The current implementation has a scope for improvement of error messages that will report domain specific details for disallowed features, but it already prohibits them. |
| 147 | + |
| 148 | +## Addendum: Default methods ## |
| 149 | +By `default methods` we mean non-abstract methods defined and implemented by a trait. |
| 150 | + |
| 151 | +The way how those methods are implemented by compiler has changed substantially over years. |
| 152 | +At the same time, `invokeinterface` has always been a reliable way to invoke such a method, |
| 153 | +independently from how it was implemented under the hood. |
| 154 | + |
| 155 | +One might reason that, as there has been a reliable way to call methods on the binary level, |
| 156 | +it should be allowed to use them in binary compatible APIs. |
| 157 | + |
| 158 | +At the same time, the mixin composition protocol that is followed when a class inherits those traits has also |
| 159 | +changed substantially. |
| 160 | +The classes which have been correctly inheriting those traits compiled by previous versions of Scala |
| 161 | +may need recompilation if trait has been recompiled with a new major version of Scala. |
| 162 | + |
| 163 | +Thus, the authors of this SIP has decided not to allow default methods in the |
| 164 | +`@binaryCompatible` traits. |
| 165 | + |
| 166 | +## See Also ## |
| 167 | + * [dotty#1900](https://github.com/lampepfl/dotty/pull/1900) is an implementation for Dotty |
| 168 | + * [MiMa](https://github.com/typesafehub/migration-manager) |
0 commit comments