Skip to content

Enable explicit nulls for all compile #14946

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

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
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
26 changes: 10 additions & 16 deletions compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1194,10 +1194,6 @@ class TreeUnpickler(reader: TastyReader,
res.withAttachment(SuppressedApplyToNone, ())
else res

def simplifyLub(tree: Tree): Tree =
tree.overwriteType(tree.tpe.simplified)
tree

def readLengthTerm(): Tree = {
val end = readEnd()
val result =
Expand Down Expand Up @@ -1247,25 +1243,23 @@ class TreeUnpickler(reader: TastyReader,
val tpt = ifBefore(end)(readTpt(), EmptyTree)
Closure(Nil, meth, tpt)
case MATCH =>
simplifyLub(
if (nextByte == IMPLICIT) {
readByte()
InlineMatch(EmptyTree, readCases(end))
}
else if (nextByte == INLINE) {
readByte()
InlineMatch(readTerm(), readCases(end))
}
else Match(readTerm(), readCases(end)))
if (nextByte == IMPLICIT) {
readByte()
InlineMatch(EmptyTree, readCases(end))
}
else if (nextByte == INLINE) {
readByte()
InlineMatch(readTerm(), readCases(end))
}
else Match(readTerm(), readCases(end))
case RETURN =>
val from = readSymRef()
val expr = ifBefore(end)(readTerm(), EmptyTree)
Return(expr, Ident(from.termRef))
case WHILE =>
WhileDo(readTerm(), readTerm())
case TRY =>
simplifyLub(
Try(readTerm(), readCases(end), ifBefore(end)(readTerm(), EmptyTree)))
Try(readTerm(), readCases(end), ifBefore(end)(readTerm(), EmptyTree))
case SELECTouter =>
val levels = readNat()
readTerm().outerSelect(levels, SkolemType(readType()))
Expand Down
28 changes: 27 additions & 1 deletion compiler/src/dotty/tools/dotc/transform/Pickler.scala
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ class Pickler extends Phase {
private val beforePickling = new mutable.HashMap[ClassSymbol, String]
private val picklers = new mutable.HashMap[ClassSymbol, TastyPickler]

private val typeSimplifier = new TypeSimplifier

/** Drop any elements of this list that are linked module classes of other elements in the list */
private def dropCompanionModuleClasses(clss: List[ClassSymbol])(using Context): List[ClassSymbol] = {
val companionModuleClasses =
Expand Down Expand Up @@ -97,6 +99,13 @@ class Pickler extends Phase {
println(i"**** pickled info of $cls")
println(TastyPrinter.showContents(pickled, ctx.settings.color.value == "never"))
}
// println(i"**** pickled info of $cls")
// println(TastyPrinter.showContents(pickled, ctx.settings.color.value == "never"))
if cls.show.contains("MainGenericRunner") then
import java.nio.file.{Paths, Files}
import java.nio.charset.StandardCharsets

Files.write(Paths.get("debug.txt"), TastyPrinter.showContents(pickled, true).getBytes(StandardCharsets.UTF_8))
pickled
}(using ExecutionContext.global)
}
Expand Down Expand Up @@ -135,7 +144,7 @@ class Pickler extends Phase {
}
pickling.println("************* entered toplevel ***********")
for ((cls, unpickler) <- unpicklers) {
val unpickled = unpickler.rootTrees
val unpickled = typeSimplifier.transform(unpickler.rootTrees)
testSame(i"$unpickled%\n%", beforePickling(cls), cls)
}
}
Expand All @@ -151,4 +160,21 @@ class Pickler extends Phase {
|
| diff before-pickling.txt after-pickling.txt""".stripMargin)
end testSame

// Overwrite types of If, Match, and Try nodes with simplified types
// to avoid inconsistencies in unsafe nulls
class TypeSimplifier extends TreeMapWithPreciseStatContexts:
override def transform(tree: Tree)(using Context): Tree =
try tree match
case _: If | _: Match | _: Try =>
val newTree = super.transform(tree)
newTree.overwriteType(newTree.tpe.simplified)
newTree
case _ =>
super.transform(tree)
catch
case ex: TypeError =>
report.error(ex, tree.srcPos)
tree
end TypeSimplifier
}
4 changes: 4 additions & 0 deletions compiler/src/dotty/tools/dotc/transform/PostTyper.scala
Original file line number Diff line number Diff line change
Expand Up @@ -460,6 +460,10 @@ class PostTyper extends MacroTransform with IdentityDenotTransformer { thisPhase
)
case Block(_, Closure(_, _, tpt)) if ExpandSAMs.needsWrapperClass(tpt.tpe) =>
superAcc.withInvalidCurrentClass(super.transform(tree))
case _: If | _: Match | _: Try =>
val newTree = super.transform(tree)
newTree.overwriteType(newTree.tpe.simplified)
newTree
case tree =>
super.transform(tree)
}
Expand Down
2 changes: 1 addition & 1 deletion compiler/src/dotty/tools/dotc/typer/Nullables.scala
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ object Nullables:
ctx.explicitNulls && !ctx.mode.is(Mode.SafeNulls)

private def needNullifyHi(lo: Type, hi: Type)(using Context): Boolean =
ctx.explicitNulls
ctx.mode.is(Mode.SafeNulls)
&& lo.isExactlyNull // only nullify hi if lo is exactly Null type
&& hi.isValueType
// We cannot check if hi is nullable, because it can cause cyclic reference.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ object ClasspathFromClassloader {
* BEWARE: with exotic enough classloaders, this may not work at all or do
* the wrong thing.
*/
def apply(cl: ClassLoader): String = {
def apply(cl: ClassLoader | Null): String = {
val classpathBuff = List.newBuilder[String]
def collectClassLoaderPaths(cl: ClassLoader): Unit = {
if (cl != null) {
Expand Down
2 changes: 1 addition & 1 deletion compiler/src/dotty/tools/dotc/util/GenericHashMap.scala
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ abstract class GenericHashMap[Key, Value]
val v1 = value
v = v1
update(key, v1)
v.uncheckedNN
v

private def addOld(key: Key, value: Value): Unit =
Stats.record(statsItem("re-enter"))
Expand Down
13 changes: 6 additions & 7 deletions compiler/src/dotty/tools/dotc/util/HashSet.scala
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ class HashSet[T](initialCapacity: Int = 8, capacityMultiple: Int = 2) extends Mu
var idx = firstIndex(x)
var e: T | Null = entryAt(idx)
while e != null do
if isEqual(e.uncheckedNN, x) then return e
if isEqual(e, x) then return e
idx = nextIndex(idx)
e = entryAt(idx)
null
Expand All @@ -102,8 +102,7 @@ class HashSet[T](initialCapacity: Int = 8, capacityMultiple: Int = 2) extends Mu
var idx = firstIndex(x)
var e: T | Null = entryAt(idx)
while e != null do
// TODO: remove uncheckedNN when explicit-nulls is enabled for regule compiling
if isEqual(e.uncheckedNN, x) then return e.uncheckedNN
if isEqual(e, x) then return e
idx = nextIndex(idx)
e = entryAt(idx)
addEntryAt(idx, x)
Expand All @@ -115,20 +114,20 @@ class HashSet[T](initialCapacity: Int = 8, capacityMultiple: Int = 2) extends Mu
var idx = firstIndex(x)
var e: T | Null = entryAt(idx)
while e != null do
if isEqual(e.uncheckedNN, x) then
if isEqual(e, x) then
var hole = idx
while
idx = nextIndex(idx)
e = entryAt(idx)
e != null
do
val eidx = index(hash(e.uncheckedNN))
val eidx = index(hash(e))
if isDense
|| index(eidx - (hole + 1)) > index(idx - (hole + 1))
// entry `e` at `idx` can move unless `index(hash(e))` is in
// the (ring-)interval [hole + 1 .. idx]
then
setEntry(hole, e.uncheckedNN)
setEntry(hole, e)
hole = idx
table(hole) = null
used -= 1
Expand Down Expand Up @@ -156,7 +155,7 @@ class HashSet[T](initialCapacity: Int = 8, capacityMultiple: Int = 2) extends Mu
var idx = 0
while idx < oldTable.length do
val e: T | Null = oldTable(idx).asInstanceOf[T | Null]
if e != null then addOld(e.uncheckedNN)
if e != null then addOld(e)
idx += 1

protected def growTable(): Unit =
Expand Down
26 changes: 16 additions & 10 deletions compiler/src/dotty/tools/dotc/util/ReadOnlyMap.scala
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,25 @@ abstract class ReadOnlyMap[Key, Value]:

def isEmpty: Boolean = size == 0

def get(key: Key): Option[Value] = lookup(key) match
case null => None
case v => Some(v.uncheckedNN)

def getOrElse(key: Key, value: => Value) = lookup(key) match
case null => value
case v => v.uncheckedNN
def get(key: Key): Option[Value] =
val v = lookup(key)
v match
case null => None
case _ => Some(v)

def getOrElse(key: Key, value: => Value) =
val v = lookup(key)
v match
case null => value
case _ => v

def contains(key: Key): Boolean = lookup(key) != null

def apply(key: Key): Value = lookup(key) match
case null => throw new NoSuchElementException(s"$key")
case v => v.uncheckedNN
def apply(key: Key): Value =
val v = lookup(key)
v match
case null => throw new NoSuchElementException(s"$key")
case _ => v

def toArray: Array[(Key, Value)] =
val result = new Array[(Key, Value)](size)
Expand Down
2 changes: 1 addition & 1 deletion compiler/src/dotty/tools/dotc/util/WeakHashSet.scala
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ abstract class WeakHashSet[A <: AnyRef](initialCapacity: Int = 8, loadFactor: Do
case null => addEntryAt(bucket, elem, h, oldHead)
case _ =>
val entryElem = entry.get
if entryElem != null && isEqual(elem, entryElem) then entryElem.uncheckedNN
if entryElem != null && isEqual(elem, entryElem) then entryElem
else linkedListLoop(entry.tail)
}

Expand Down
2 changes: 1 addition & 1 deletion compiler/test/dotty/tools/dotc/CompilationTests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -249,9 +249,9 @@ class CompilationTests {
compileFilesInDir("tests/explicit-nulls/pos", explicitNullsOptions),
compileFilesInDir("tests/explicit-nulls/pos-separate", explicitNullsOptions),
compileFilesInDir("tests/explicit-nulls/pos-patmat", explicitNullsOptions and "-Xfatal-warnings"),
compileFilesInDir("tests/explicit-nulls/pos-pickling", explicitNullsOptions and "-Ytest-pickler" and "-Xprint-types"),
compileFilesInDir("tests/explicit-nulls/unsafe-common", explicitNullsOptions and "-language:unsafeNulls"),
compileFile("tests/explicit-nulls/pos-special/i14682.scala", explicitNullsOptions and "-Ysafe-init"),
compileFile("tests/explicit-nulls/pos-special/i14947.scala", explicitNullsOptions and "-Ytest-pickler" and "-Xprint-types"),
)
}.checkCompile()

Expand Down
2 changes: 1 addition & 1 deletion compiler/test/dotty/tools/dotc/TastyBootstrapTests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ class TastyBootstrapTests {
Properties.compilerInterface, Properties.scalaLibrary, Properties.scalaAsm,
Properties.dottyInterfaces, Properties.jlineTerminal, Properties.jlineReader,
).mkString(File.pathSeparator),
Array("-Ycheck-reentrant", "-language:postfixOps", "-Xsemanticdb")
Array("-Ycheck-reentrant", "-language:postfixOps", "-Xsemanticdb", "-Yexplicit-nulls")
)

val libraryDirs = List(Paths.get("library/src"), Paths.get("library/src-bootstrapped"))
Expand Down
4 changes: 2 additions & 2 deletions compiler/test/dotty/tools/vulpix/TestConfiguration.scala
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ object TestConfiguration {
val defaultOptions = TestFlags(basicClasspath, commonOptions)
val unindentOptions = TestFlags(basicClasspath, Array("-no-indent") ++ checkOptions ++ noCheckOptions ++ yCheckOptions)
val withCompilerOptions =
defaultOptions.withClasspath(withCompilerClasspath).withRunClasspath(withCompilerClasspath)
defaultOptions.withClasspath(withCompilerClasspath).withRunClasspath(withCompilerClasspath) and "-Yexplicit-nulls"
lazy val withStagingOptions =
defaultOptions.withClasspath(withStagingClasspath).withRunClasspath(withStagingClasspath)
lazy val withTastyInspectorOptions =
Expand All @@ -82,7 +82,7 @@ object TestConfiguration {
"-Yprint-pos-syms"
)
val picklingWithCompilerOptions =
picklingOptions.withClasspath(withCompilerClasspath).withRunClasspath(withCompilerClasspath)
picklingOptions.withClasspath(withCompilerClasspath).withRunClasspath(withCompilerClasspath) and "-Yexplicit-nulls"
val scala2CompatMode = defaultOptions.and("-source", "3.0-migration")
val explicitUTF8 = defaultOptions and ("-encoding", "UTF8")
val explicitUTF16 = defaultOptions and ("-encoding", "UTF16")
Expand Down
6 changes: 3 additions & 3 deletions project/Build.scala
Original file line number Diff line number Diff line change
Expand Up @@ -612,6 +612,9 @@ object Build {

Compile / mainClass := Some("dotty.tools.dotc.Main"),

// Note: bench/profiles/projects.yml should be updated accordingly.
Compile / scalacOptions ++= Seq("-Yexplicit-nulls"),

scala := {
val args: List[String] = spaceDelimited("<arg>").parsed.toList
val externalDeps = externalCompilerClasspathTask.value
Expand Down Expand Up @@ -774,9 +777,6 @@ object Build {
)
},

// Note: bench/profiles/projects.yml should be updated accordingly.
Compile / scalacOptions ++= Seq("-Yexplicit-nulls"),

repl := (Compile / console).value,
Compile / console / scalacOptions := Nil, // reset so that we get stock REPL behaviour! E.g. avoid -unchecked being enabled
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ class C:
if ??? then g else ""

def f3 =
import scala.language.unsafeNulls
(??? : Boolean) match
case true => g
case _ => ""
Expand Down
59 changes: 59 additions & 0 deletions tests/explicit-nulls/pos-pickling/match-case.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import scala.language.unsafeNulls

def a(): String | Null = ???
val b: String | Null = ???

val i: Int = ???

def f1 = i match {
case 0 => b
case _ => a()
}

def f2 = i match {
case 0 => a()
case _ => b
}

def f3 = i match {
case 0 => a()
case _ => "".trim
}

def f4 = i match {
case 0 => b
case _ => "".trim
}

def g1 = i match {
case 0 => a()
case 1 => ""
case _ => null
}

def g2 = i match {
case 0 => ""
case 1 => null
case _ => b
}

def g3 = i match {
case 0 => null
case 1 => b
case _ => ""
}

def h1(i: Int) = i match
case 0 => 0
case 1 => true
case 2 => i.toString
case _ => null

// This test still fails.
// Even without explicit nulls, the type of Match
// is (0, true, "2"), which is wrong.
// def h2(i: Int) = i match
// case 0 => 0
// case 1 => true
// case 2 => "2"
// case _ => null
35 changes: 35 additions & 0 deletions tests/explicit-nulls/pos-pickling/other-pickling.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import scala.language.unsafeNulls

def associatedFile: String | Null = ???

def source: String = {
val f = associatedFile
if (f != null) f else associatedFile
}

def defines(raw: String): List[String] = {
val ds: List[(Int, Int)] = ???
ds map { case (start, end) => raw.substring(start, end) }
}

abstract class DeconstructorCommon[T >: Null <: AnyRef] {
var field: T = null
def get: this.type = this
def isEmpty: Boolean = field eq null
def isDefined = !isEmpty
def unapply(s: T): this.type ={
field = s
this
}
}

def genBCode =
val bsmArgs: Array[Object | Null] | Null = null
val implMethod = bsmArgs(3).asInstanceOf[Integer].toInt
implMethod

val arrayApply = "a".split(" ")(2)

val globdir: String = if (??? : Boolean) then associatedFile.replaceAll("[\\\\/][^\\\\/]*$", "") else ""

def newInstOfC(c: Class[?]) = c.getConstructor().newInstance()
Loading