-
Notifications
You must be signed in to change notification settings - Fork 1.1k
isVolatile incorrect for intersection types #50
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
Comments
Volatile checking needs to take all intersections into account; previously these could be discarded through needsChecking. Plus several refactorings and additions. 1) Module vals now have Final and Stable flags set 2) All logic around isVolatile is now in TypeOps; some of it was moved from Types. 3) Added stability checking to Select and SelectFromType typings. Todo: We should find a better name for isVolatile. Maybe define the negation instead under the name "isRealizable"?.
Reopening to keep the test case around for the time we have refchecks written. |
@smarter In this example, if we remove |
@odersky : does this still need to be kept open? |
Yes, the error is not detected. I'll put a test in pending/neg. |
Note that instead of an unimplemented lazy val, we could also have a null val. @RossTate and I found a similar issue that also affects scalac: |
Good point. I think this would have a fix analogous to lazy vals: We need a Then for lazy val x: T = ... T must in each case be agp. Wdyt? On Fri, Jan 29, 2016 at 7:12 AM, Nada Amin [email protected] wrote:
Martin Odersky |
Not sure I understand how the proposal would work.
trait A { type L <: Nothing }
trait B { type L >: Any}
def id1(b: B, x: Any): b.L = x
def id2(p: B with A)(x: Any): Nothing = id1(p, x)
def id3(x: Any): Nothing = id2(null)(x)
println(id3("oh")) |
One option would be to make On Fri, Jan 29, 2016 at 9:41 AM, Nada Amin [email protected] wrote:
Martin Odersky |
Note that you can also have expression evaluate to
This code is type correct for any T, |
@DarkDimius Yes, I know. We need to fix initialization separately. One thing after another. Right now, we need to classify programs that access uninitialized data as potentially unsound. |
@odersky -- to enforce |
@namin I think if you have But on second thought we should wait until we address the problem at the root with the null-safe type system we planned. Then |
@odersky -- maybe you're right about The second proposal feels right long-term... |
Yet another variation (relevant to trait A { type L <: Nothing }
trait B { type L >: Any}
def id1(b: B, x: Any): b.L = x
def id2[C, A1 >: C <: A, B1 >: C <: B](c: C, x: Any): Nothing = {
val c1: B1 with A1 = c
id1(c1, x)
}
def id3(x: Any): Nothing = id2[Null, A, B](null, x)
println(id3("oh")) |
@odersky what is the proposed definition of 'always good bounds'? We know that previous definitions of realizability are not preserved through narrowing, which (I believe) is the problem we're observing here. Another variation (with either trait A { type L <: Nothing }
trait B { type L >: Any}
trait U {
val p: B
// or: lazy val p: B
def brand(x: Any): p.L = x
}
trait V extends U {
val p: A & B = null
// or: lazy val p: A & B = ???
}
val v = new V {}
v.brand("boom!") |
@TiarkRompf -- cool example. In scalac (with |
@namin Well, slight variations break in scalac as well: trait A { type L <: Nothing }
trait B { type L >: Any}
trait U {
type X <: B
val p: X
def brand(x: Any): p.L = x
}
trait V extends U {
type X = B with A
val p: X = null
// or: lazy val p: X = ???
}
val v = new V {}
v.brand("boom!"): Nothing |
@namin Does It looks to me we also have to enforce that val's used in paths are final. |
It wouldn't translate to Scala. Java's wildcards generate implicit constraints, whereas Scala's do not. Here's a simpler example:
This type checks in Java because Java infers that the wildcard in But the high-level strategy of this example is that same as what @namin and I came up with for Scala: rather than writing the inconsistent bounds directly, where the compiler would check for consistency, instead figure out some way to have inconsistent bounds be generated, where the compiler would not check for consistency. In my Java example, I do this using implicit constraints (and use the |
Good observations, @RossTate, and interesting to see how this plays out in Java. I think one important bit we've learnt from the DOT proofs is that most static predicates (like realizability of types) are not preserved by narrowing. So whenever we want to use realizability as a safeguard (instead of knowing that a type must be inhabited through evaluation) we're on thin ice, and we have to be very careful that narrowing (through function calls or inheritance) is ruled out. For top-level lazy vals the restrictions we have now look reasonable to me (non-overridable definition of the lazy val, non-overridable definition of its type). But for the other bits I don't the fix is quite there yet (cc @odersky). The length > 1 path example seems to be unchanged (?), and the by-name one blows up with a little more indirection: trait A { type L <: Nothing }
trait B { type L >: Any }
def f(x: => B)(y: Any):x.L = y
def f1(x: => A & B)(y: Any):Nothing = f(x)(y)
f1(???)("boom!"): Nothing When I said these issues were easy to fix I thought of a more conservative solution, i.e. of disallowing by-name values in paths, and restricting lazy vals to be endpoints in paths (but of course this doesn't rule out solutions based on more elaborate checks). |
I agree. I think it's safe to say that you can't soundly have bounded virtual types, intersection types, and a (lazily) inhabitable bottom type. |
@TiarkRompf You need to compile with -strict. We check fields for Cheers
On Tue, Feb 2, 2016 at 7:33 PM, Ross Tate [email protected] wrote:
Martin Odersky |
@odersky ok, got it now. The nested path fails with -strict, but the second by-name example still goes through. |
@TiarkRompf Which example did you mean? On Tue, Feb 2, 2016 at 8:41 PM, Tiark Rompf [email protected]
Martin Odersky |
This one: trait A { type L <: Nothing }
trait B { type L >: Any }
def f(x: => B)(y: Any):x.L = y
def f1(x: => A & B)(y: Any):Nothing = f(x)(y)
f1(???)("boom!"): Nothing |
@TiarkRompf Ah I overlooked that. But it seems anyway we should reject all cbn parameters as paths because they can have different values on each call. Do you agree? |
Yes, agreed. |
I just thought I'd mention this here. I tried running the unsound null example in earlier versions of Scala (<=2.9.3) but interestingly, it doesn't compile due to restrictions on dependent method types. Does that suggest a static null containment strategy only for arguments of dependent methods, or am I missing something? When I try to convert to OO in order to avoid dependent methods, I get the usual checks for good bounds in objects. |
Ah, nevermind. As @odersky hinted, the dependent method type is just one more restriction rather than essential. @TiarkRompf already had some earlier examples with no dependent methods: #50 (comment) Here's an example that compiles with all major Scala releases listed in the download page, including 2.5 to 2.9. object unsound_legacy {
trait LowerBound[T] {
type M >: T;
}
trait UpperBound[U] {
type M <: U;
}
trait Upcast[T] {
type X <: LowerBound[T]
def compute: X
final val ub: X = compute
def upcast(t: T): ub.M = t
}
class Coerce[T,U] extends Upcast[T] {
type X = LowerBound[T] with UpperBound[U]
override def compute = null
def coerce(t: T): U = upcast(t)
}
def main(args : Array[String]) : Unit = {
val zero : String = (new Coerce[Int,String]).coerce(0)
println("...")
}
} |
Yes, @olhotak -- Scala and Dotty have deferred any fixes for null paths. In Dotty, the fixes have focused on lazy paths. |
@olhotak I think you need |
@namin and me had a look at the
isVolatile
method inTypes
/TypeOps
.If we try the following example:
It's accepted by dotty, which means that we can cast Int to String...
I added debug output for the results of
isVolatile
, and it printsBut as we understood
isVolatile
, it should returntrue
for all types which are possibly uninhabited.The and-case in
needsChecking
looks bad:because we cannot just look seperately at
l
andr
, but we should check if there are conflicting members inl
andr
.In Scala, this is not a problem, because we only have
with
, which is asymmetric, so the members ofr
override those ofl
and in our example,o.X
is onlyInt
.From a theoretical point of view, we are bothered by the fact that
!isVolatile
is not preserved by weakening, i.e.A <: B and !isVolatile(B)
does not imply!isVolatile(A)
so we doubt if a
!isVolatile
judgment would be useful in a typesafety proof (needs more thinking...).Additionally, if we replace the implementation of
needsChecking
(which seems to be just an optimization) by thiswe get a
java.util.NoSuchElementException: head of empty list
, so it's not just an optimization...The text was updated successfully, but these errors were encountered: