Skip to content

Commit aa988a6

Browse files
authored
Merge pull request #654 from DarkDimius/bincompat
`@stableABI` SIP.
2 parents af0be46 + 810637b commit aa988a6

File tree

1 file changed

+168
-0
lines changed

1 file changed

+168
-0
lines changed
+168
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
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

Comments
 (0)