-
Notifications
You must be signed in to change notification settings - Fork 1
WIP SIP-23 #12
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
WIP SIP-23 #12
Conversation
|
|
@@ -1160,6 +1160,7 @@ trait Implicits { | |||
// necessary only to compile typetags used inside the Universe cake | |||
case ThisType(thisSym) => | |||
gen.mkAttributedThis(thisSym) | |||
// TODO SIP-23: TypeTag for LiteralType |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- This could serve as the vehicle for summoning the type's inhabitant.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not have a macro for this?
This looks amazing, @adriaanm! I'm playing with it at the moment. Got a few questions:
Hope this is helpful. I'm super excited to see this moving forward! 💯 👍 |
Thanks for the feedback -- very helpful! I have to work on my devoxx talk for the next couple of days, but will get back to coding on this soon. In the mean time, PRs accepted on my sip23 branch ;-) |
I'll definitely spend quite some time with it! |
Regarding the last rough edge, it's probably related to lack of type equality between ConstantType and LiteralType. Still mulling that one over. |
Type inference for vals becomes more specific, is this intended?
|
It is, though it's up for debate. Under the hood, a |
What's the plan for class literals? The corresponding
Again, I think tend to think we should not infer these types unless guided by a bound of |
I was planning to only support syntactic literals. Right now, Symbols are not supported. |
|
The problem with Symbols is similar to classOf, they are method calls underneath:
|
Do you have any snags in mind if we do? |
Just the general problem of accidentally inferring too specific a type for some public API that ties your hands down the track. IIUC, that's why we deconst in type inference today, rather than just to ensure side effects aren't folded away. I'd expect var and val infer the same type.
|
As a compromise, we could widen for non-local vals. I think it's useful to preserve precise types locally. |
Different rules for different contexts doesn't sound ideal to me. But we can give it some thought. |
It feels desirable to me. |
@odersky, what do you think? Is inferring |
While we ponder that one, here's a curious wrinkle:
|
Nice... covariant array ftw |
Probably a widen in varargs code? |
Thanks! |
Here's a failing test case with Java varargs. https://github.com/retronym/scala/compare/topic/sip23-java-varargs?expand=1 In
The latter type is incorrectly printed as |
This works as expected, but is the sort of thing we'll have to add test cases for:
|
This seems inconsistent:
vs
|
The spec for erasure (in "03. Types") currently says:
We should review that area of the spec and implementation holistically for that change and SIP-23. |
e.g., val x : -1 => -1 = ???
TODO: deriving this from type param bounds is not the best way, should consider all constraints that we encounter scala> def stable[T <: 1](x: T): T = x stable: [T <: 1](x: T)T scala> stable(1) res0: 1 = 1 scala> stable(2) <console>:9: error: inferred type arguments [2] do not conform to method stable's type parameter bounds [T <: 1] stable(2) ^ <console>:9: error: type mismatch; found : Int(2) required: T stable(2) ^ scala> def stable[T <: Singleton](x: T): T = x stable: [T <: Singleton](x: T)T scala> val x: Int = 2 x: Int = 2 scala> val y: x.type = x y: x.type = 2 scala> stable(y) res3: x.type = 2
this bootstraps
``` scala> val x = "a" x: "a" = a scala> x.asInstanceOf["a"] res1: "a" = a scala> "1".asInstanceOf["2"] x asInstanceOf Throwable java.lang.ClassCastException ... 33 elided scala> "1".asInstanceOf["1"] res3: "1" = 1 scala> 1.asInstanceOf[1] res4: 1 = 1 scala> 1.asInstanceOf[2] x asInstanceOf Throwable java.lang.ClassCastException ... 33 elided ```
f67a7bc
to
61a16f5
Compare
What's the perspective here regarding union types? If we have something like |
Likely, but union types are still too far in the future to know for sure how they'll interact with this feature. They are primarily about class types -- you could already experiment in dotty and see what p.type | q.type means, where p and q refer to strings... |
93cb02e
to
3410d25
Compare
7d6747a
to
43289bc
Compare
(Will reopen when I get a chance to work on this again!) |
- Avoid boxing the {Object, Int, ...}Ref itself by storing it in a val, not a var - Avoid box/unbox of primitive lazy expressions due which are added to "adapt" it to the erased type of the fictional syncronized method, by using a `return`. We have to add a dummy throw after the synchronized block, but this is elimnated by the always-on DCE in the code generator. ``` ⚡ qscalac -Xprint:mixin $(f "class C { def foo = { lazy val x = 42; x }}"); javap -private -c -cp . C [[syntax trees at end of mixin]] // a.scala package <empty> { class C extends Object { def foo(): Int = { lazy <artifact> val x$lzy: scala.runtime.LazyInt = new scala.runtime.LazyInt(); C.this.x$1(x$lzy) }; final private[this] def x$1(x$lzy$1: scala.runtime.LazyInt): Int = { x$lzy$1.synchronized({ if (x$lzy$1.initialized().unary_!()) { x$lzy$1.initialized_=(true); x$lzy$1.value_=(42) }; return x$lzy$1.value() }); throw null }; def <init>(): C = { C.super.<init>(); () } } } Compiled from "a.scala" public class C { public int foo(); Code: 0: new #12 // class scala/runtime/LazyInt 3: dup 4: invokespecial #16 // Method scala/runtime/LazyInt."<init>":()V 7: astore_1 8: aload_1 9: invokestatic #20 // Method x$1:(Lscala/runtime/LazyInt;)I 12: ireturn private static final int x$1(scala.runtime.LazyInt); Code: 0: aload_0 1: dup 2: astore_1 3: monitorenter 4: aload_0 5: invokevirtual scala#31 // Method scala/runtime/LazyInt.initialized:()Z 8: ifne 22 11: aload_0 12: iconst_1 13: invokevirtual scala#35 // Method scala/runtime/LazyInt.initialized_$eq:(Z)V 16: aload_0 17: bipush 42 19: invokevirtual scala#39 // Method scala/runtime/LazyInt.value_$eq:(I)V 22: aload_0 23: invokevirtual scala#42 // Method scala/runtime/LazyInt.value:()I 26: istore_2 27: goto 33 30: aload_1 31: monitorexit 32: athrow 33: aload_1 34: monitorexit 35: iload_2 36: ireturn Exception table: from to target type 4 30 30 Class java/lang/Throwable public C(); Code: 0: aload_0 1: invokespecial scala#43 // Method java/lang/Object."<init>":()V 4: return } ```
Top level modules in Scala currently desugar as: ``` class C; object O extends C { toString } ``` ``` public final class O$ extends C { public static final O$ MODULE$; public static {}; Code: 0: new #2 // class O$ 3: invokespecial #12 // Method "<init>":()V 6: return private O$(); Code: 0: aload_0 1: invokespecial #13 // Method C."<init>":()V 4: aload_0 5: putstatic #15 // Field MODULE$:LO$; 8: aload_0 9: invokevirtual #21 // Method java/lang/Object.toString:()Ljava/lang/String; 12: pop 13: return } ``` The static initalizer `<clinit>` calls the constructor `<init>`, which invokes superclass constructor, assigns `MODULE$= this`, and then runs the remainder of the object's constructor (`toString` in the example above.) It turns out that this relies on a bug in the JVM's verifier: assignment to a static final must occur lexically within the <clinit>, not from within `<init>` (even if the latter is happens to be called by the former). I'd like to move the assignment to <clinit> but that would change behaviour of "benign" cyclic references between modules. Example: ``` package p1; class CC { def foo = O.bar}; object O {new CC().foo; def bar = println(1)}; // Exiting paste mode, now interpreting. scala> p1.O 1 ``` This relies on the way that we assign MODULE$ field after the super class constructors are finished, but before the rest of the module constructor is called. Instead, this commit removes the ACC_FINAL bit from the field. It actually wasn't behaving as final at all, precisely the issue that the stricter verifier now alerts us to. ``` scala> :paste -raw // Entering paste mode (ctrl-D to finish) package p1; object O // Exiting paste mode, now interpreting. scala> val O1 = p1.O O1: p1.O.type = p1.O$@ee7d9f1 scala> scala.reflect.ensureAccessible(p1.O.getClass.getDeclaredConstructor()).newInstance() res0: p1.O.type = p1.O$@64cee07 scala> O1 eq p1.O res1: Boolean = false ``` We will still achieve safe publication of the assignment to other threads by virtue of the fact that `<clinit>` is executed within the scope of an initlization lock, as specified by: https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-5.html#jvms-5.5 Fixes scala/scala-dev#SD-194
WIP PR to centralize discussion, accept improvements/tests/polish/docs/...
Everyone is welcome to look for the checkboxes/TODO items here and in the discussion below and take them on! If you're not sure where/how to start, just ask!
Testing
Spec/docs
ConstantType
/LiteralType
andscala.Singleton
.Type inference / type checking
x.isInstanceOf[3]
should expand tox == 3
, andx.asInstanceOf[3]
should beif (x != 3) throw new ClassCastException(..)
Corner cases
val t: 1 = t
(NOTE: t has value 0...)Design space
()
asLiteralType
? (I say 'no': () is not a syntactic literal, it's also too deeply ingrained to incorporate here)Feature interaction