This SIP is about extending what we can do with AnyVal and Universal traits, and add Universal Abstract Classes.
Motivation
In the near future, the JVM will end up getting runtime value classes. This means that for Scala, we can improve our support for value classes, and allow more classes to become descendants of AnyVal.
This could also affect Scala Native, and let Scala Native do more optimizations at compile time.
Design
I think I have a somewhat concrete idea of syntax except for two parts, which I’ll get to later. The current syntax is aligned with our current implementation of universal traits and value classes, but with some fuzzyness around two edge cases.
The design is based around AnyVal, Any, and AnyRef.
Runtime Value Classes
Concrete classes where their only members are def or val may extend AnyVal directly, or with special syntax may extend a universal abstract class and declare themselves as value classes. Classes that do this are called “Runtime Value Classes” (to distinguish from our current Value Classes). These classes are implicitly final, and when targeting a JVM version that supports value classes, are emitted as java value classes.
If the class has only one val argument and no other storage fields, it will need an extra marker trait Value (final name TBD) to be considered a runtime value class and not a SIP-15 value class.
TODO: JVM requires strict init on final fields in a value class, but that conflicts with current scala constructor semantics?
Runtime Value Classes may only extend Universal abstract classes and may only mixin Universal traits.
Universal Abstract Classes
Abstract classes with only val and def members may extend Any, or another universal abstract class. Abstract classes that do this are called “Universal abstract classes”, and when targeting a JVM version that supports value classes, are emitted as java value abstract classes.
Non-universal abstract classes may extend from universal abstract classes, but they must use special syntax to denote that they are stripping themselves of their universality.
Universal abstract classes may only extend other Universal abstract classes, and may only mixin Universal traits. Non-universal abstract classes can extend universal abstract classes and non-universal abstract classes, and may mixin either universal traits or non-universal traits.
Universal Traits
Traits with only val and def members may extend Any, or may extend an universal abstract class. Traits that do this are called “Universal traits”.
Non-universal traits may extend universal traits, but they must modify their supertype to be a non-universal type.
Universal traits may only extend other Universal traits, and may only set their supertype to be of a universal type. Non-universal traits can extend both Universal and Non-universal traits.
Java Interop
A universal change in behavior, regardless of classfile version, is importing interfaces as ambiguously universal traits. When directly extended or mixed in by traits or abstract classes, it’s considered non-universal, and thus the trait/abstract class will be non-universal. However, it’s still allowed to extend these interfaces in a universal trait/abstract class, you just have to explicitly specify your supertype as Any or another universal abstract class. These can also be extended by runtime value classes. This is similar to how the compiler currently treats java.io.Serializable and java.lang.Comparable.
When importing java classfiles of a version that supports JEP 401 (value classes and objects), it will make all abstract classes that don’t have the ACC_IDENTITY flag extend Any, and any abstract class that does have the ACC_IDENTITY flag but extends an abstract class that doesn’t to use the special syntax to denote stripping of universality.
Concrete classes without the ACC_IDENTITY flag will be made to extend AnyVal, or use the special syntax to refine its super universal abstract class as a value class.
Stdlib
Some classes in the stdlib will be assumed to be runtime value classes/universal traits:
scala.Equals
scala.Product
scala.Product1 - scala.Product22
scala.Tuple
scala.NonEmptyTuple
scala.Tuple1 - scala.Tuple22
scala.Unit* (when boxed)
scala.collection.IterableOnce
scala.Option* (may not do anything at runtime due to it not being a single class?)
scala.Some* (may not help because optimization doesn't like instanceof?)
I’m likely forgetting some easy candidates, but these are the ones I could think of.
Syntax
Here’s the syntax that I’m completely sold on:
// universal abstract class
abstract class Foo extends Any
// UAC extending UAC
abstract class Biz extends Foo
// universal trait
trait Bar extends Any
// non-universal trait extending universal trait
trait Baz extends AnyRef, Bar
// value class without UAC parent
class Buzz(val foo: Int, val bar: Int) extends AnyVal
Now there are two cases that I don’t have a completely good idea on; A non-universal abstract class extending a universal abstract class, and a value class with a universal abstract class parent.
I think the syntax of these two should be mirrors, because in effect they are doing a similar thing: refining the parent type to be more specific - in the first case, going from Any to AnyRef, and in the second, going from Any to AnyVal.
The “trait extending a universal abstract class but not being universal” would fall under the first case, and would share the same syntax.
Here’s some options:
Intersection types
abstract class NonUniversal extends (Foo & AnyRef)
class ValueClass extends (Foo & AnyVal)
Just let AnyRef/AnyVal be in trait position
abstract class NonUniversal extends Foo, AnyRef
class ValueClass extends Foo, AnyVal
Special type, like into
abstract class NonUniversal extends ref[Foo]
class ValueClass extends value[Foo]
Soft modifiers
reference abstract class NonUniversal extends Foo
value class ValueClass extends Foo
I think the type system will end up treating all these cases as the first one, as that’s what makes the most sense in the underlying types.
Limitations
On Scala.js, this likely won’t do anything as there is no native mechanism for unboxing there.
If targeting a JVM version that doesn’t support value classes, this may end up causing issues in dependent libraries and applications. It should probably be a requirement to target the correct JVM version so the classfiles and the TASTy match, as otherwise you’d end up with value classes extending non-value abstract classes at runtime and that would likely cause an exception.
Right now, the compiler assumes that all subtypes of AnyVal are non nullable. However, the current most recent java implementation assumes that value classes are indeed nullable, so when importing java classfiles it could lead to unsoundness. However, for implicit nulls, this doesn’t seem that much worse than the current state of things.
Feedback?
I’m probably forgetting something important that would need to be looked at more, please tell me if anything stands out.