Skip to content

@stableABI SIP. #654

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Feb 13, 2017
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
168 changes: 168 additions & 0 deletions sips/pending/_posts/2017-01-13-binary-compatibility.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
---
layout: sip
title: SIP XX - Improving binary compatibility with @binaryCompatible
disqus: true
---

__Dmitry Petrashko__

__first submitted 13 January 2017__

## Introduction ##
Copy link
Contributor

@xeno-by xeno-by Jan 17, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that this document would benefit from the definitions of the following terms:

  1. Binary compatibility (both the intransitive and transitive variations used in the text below)
  2. Public API
  3. Binary signature
  4. The notion of signatures being different from those written by their authors.

I'd suggest having a small example that introduces and illustrates these notions.


Scala is a language which evolves fast and thus made a decision to only promise binary compatibility across minor releases.
Copy link
Contributor

@xeno-by xeno-by Jan 17, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a very apt description. I wonder whether there are publications/talks that provide more context on the matter.

Copy link
Contributor

@xeno-by xeno-by Jan 17, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's a minor release and what's a major release? There are different conventions that people follow in our community.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll add a link defining both terms. Thank you.

At the same time, there is a demand to develop APIs that live longer than a major release cycle of Scala.
This SIP introduces an annotation `@binaryCompatible` that checks that `what you write is what you get`.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with other commenters that a different name would be beneficial. To me, saying that something is binary compatible, immediately raises the question "with what?". This annotation doesn't answer that question.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While I agree that the @binaryCompatible name isn't perfect, I didn't yet find a better one.
As this annotation is intended to be rarely used and only by advanced users, I don't think we should concentrate that much on the name, but I welcome name suggestions.

It will fail compilation in case emitted methods or their signatures
are different from those written by users.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/users/authors/

As long as signatures of methods in source is not changed, `@binaryCompatible` annotated class
will be compatible across major version of Scala.

## Use Cases
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we number these use cases, i.e. have this section look as follows:

1) Use case 1 
<explanation>

2) Use case 2
<explanation>

etc

In case there's a need to develop an API that will be used by clients compiled using different major versions of Scala,
the current approach is to either develop them in Java or to use best guess to restrict what Scala features should be used.
There's also a different approach which is used by SBT: instead of publishing a binary `compiler-interface`, sources are published instead
that would be locally compiled.

There's also a use-case of defining a class which is supposed to be also used from Java.
`@binaryCompatible` will ensure that there are no not-expected methods that would show up in members of a class or an interface.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/not-expected/unexpected/


Dotty currently uses java defined interfaces as public API for IntelliJ in order to ensure binary compatibility.
These interfaces can be replaced by `@binaryCompatible` annotated traits to reach the same goal.

## Design Guidelines
`@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.
Thus this is designed as an advanced feature that is used rarely and thus is intentionally verbose.
It's designed to provide strong guarantees, in some cases sacrificing ease of use.

The limitations enforced by `@binaryCompatible` are designed to be an overapproximation:
instead of permitting a list of features known to be compatible, `@binaryCompatible` enforces a stronger
check which is sufficient to promise binary compatibility.

## Overview ##
In order for a class or a trait to succeed compilation with the `@binaryCompatible` annotation it has to be:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about objects?

- defined on the top level;
Copy link
Contributor

@xeno-by xeno-by Jan 17, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why limit this to just top-level classes? If I recall correctly, MiMa also flags changes to nested classes and synthetic stuff generated for things like new {} and old-style lambdas.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why limit this to just top-level classes?

  1. Allowing to define @stableABI inner classes will most likely fail compilation due to second reason, as they will get successors synthesized to access the outer class;
  2. there are several subtleties in how outer class can affect compilation of inner class, eg by converting private method to public;
  3. use-case of an inner class can be easily handled by defining a top-level supper class and making inner class extend it;

In order to have a more user-friendly behavior than 1 & 2, and given presence of a simple workaround, I've decided to prohibit it.

- use a subset of Scala that during compilation does not require changes to public API of the class, including
- synthesizing new members, either concrete or abstract;
- changing binary signatures of existing members, either concrete or abstract;
Copy link
Contributor

@xeno-by xeno-by Jan 17, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are changes to the public API of class's companion also prohibited?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added.


`@binaryCompatible` does not change the compilation scheme of a class:
compiling a class previously annotated with the `@binaryCompatible`, will produce the same bytecode with or without `@binaryCompatible` annotation.

Below are several examples of classes and traits that succeed compilation with `@binaryCompatible`
```scala
{% highlight scala %}
@binaryCompatible
trait AbstractFile {
def name(): String

def path(): String

def jfile(): Optional[File]
}

@binaryCompatible
trait SourceFile extends AbstractFile {
def content(): Array[Char]
}

@binaryCompatible
trait Diagnostic {
def message(): String

def level(): Int

def position(): Optional[SourcePosition]
}

@binaryCompatible
object Diagnostic {
@static final val ERROR: Int = 2
@static final val WARNING: Int = 1
@static final val INFO: Int = 0
}

@binaryCompatible
class FeaturesInBodies {
def apiMethod: Int = {
// as body of the method isn't part of the public interface, one can use all features of Scala here.
lazy val result = 0 // while lazy vals are prohibited in the class, they are allowed in the bodies of methods
result
}
}
{% endhighlight %}
```

## Features that will fail compilation with `@binaryCompatible`
The features listed below have complex encodings that may change in future versions. We prefer not to compromise on them.
Most of those features can be simulated in a binary compatible way by writing a verbose re-impelemtation
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo.

which won't rely on desugaring performed inside compiler.
Note that while those features are prohibited in the public API, they can be safely used inside bodies of the methods.

- public fields. Can be simulated by explicitly defining public getters and setters that access a private field;
- lazy vals. Can be simulated by explicitly writing an implementation in source;
- case classes. Can be simulated by explicitly defining getters and other members synthesized for a case class(`copy`, `productArity`, `apply`, `unApply`, `unapply`).

The features listed below cannot be easily re-implemented in a class or trait annotated with `@binaryCompatible`.
- default arguments;
- default methods. See Addendum;
- constant types(both explicit and inferred);
- inline.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why exclude inline?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The idea is that @stableABI classes don't need @scalaSignature or @tasty to compile against. In order for inline to work, we'll need to be able to read the trees from those sections and make assumptions about those trees, that can be dependent on compiler version.

I propose to go conservative route here, we'll have an opportunity to allow more features later, but we won't have an opportunity to remove them.


## `@binaryCompatible` and Scala.js

Allowing to write API-defining classes in Scala instead of Java will allow them to compile with Scala.js,
which would have benefit of sharing the same source for two ecosystems.

Scala.js currently is binary compatible as long as original bytecode compiled by Scala JVM is binary compatible.
Providing stronger binary compatibility guarantees for JVM will automatically provide stronger guarantees for Scala.js.


## Comparison with MiMa ##
The Migration Manager for Scala (MiMa in short) is a tool for diagnosing binary incompatibilities for Scala libraries.
MiMa allows to compare binary APIs of two already compiled classfiles and reports errors if APIs do not match perfectly.

MiMa and `@binaryCompatible` complement each other, as `@binaryCompatible` helps to develop APIs that stay compatible
across major versions, while MiMa checks that previously published artifacts indeed have the same API.

`@binaryCompatible` does not compare the currently compiled class or trait against previous version,
so introduction of new members won't be prohibited. This is a use-case for MiMa.

MiMa does not indicate how hard, if possible, would it be to maintain compatibility of a class across future versions of Scala.
Multiple features of Scala, most notably lazy vals and traits, has been compiled diffently by different Scala versions
making porting existing compiled bytecode across versions very hard.
MiMa will complain retroactively that the new version is incompatible with the old one.
`@binaryCompatible` will instead indicate at compile time that the old version had used features whose encoding is prone to change.
This provides early guidance and warning when designing long-living APIs before they are publicly released.

## Compilation scheme ##
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:
- classes and traits annotated as `@binaryCompatible` are on the top level;
- no non-private members where introduced inside classes and traits annotated as `@binaryCompatible` by compiler using phase travel;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's phase travel?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think Compilation scheme section needs explanation of common terms used inside both compilers.

- no non-private members inside classes and traits annotated as `@binaryCompatible` has changed their signature from the one written by developer.

The current prototype is implemented for Dotty and supports everything descibed in this SIP.
The implementation is simple with less than 50 lines of non-boilerplate code.
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.

## Addendum: Default methods ##
By `default methods` we mean non-abstract methods defined and implemented by a trait.

The way how those methods are implemented by compiler has changed substantially over years.
At the same time, `invokeinterface` has always been a reliable way to invoke such a method,
independently from how it was implemented under the hood.

One might reason that, as there has been a reliable way to call methods on the binary level,
it should be allowed to use them in binary compatible APIs.

At the same time, the mixin composition protocol that is followed when a class inherits those traits has also
changed substantially.
The classes which have been correctly inheriting those traits compiled by previous versions of Scala
may need recompilation if trait has been recompiled with a new major version of Scala.

Thus, the authors of this SIP has decided not to allow default methods in the
`@binaryCompatible` traits.

## See Also ##
* [dotty#1900](https://github.com/lampepfl/dotty/pull/1900) is an implementation for Dotty
* [MiMa](https://github.com/typesafehub/migration-manager)