Skip to content
Draft
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
2 changes: 0 additions & 2 deletions community-build/src/scala/dotty/communitybuild/projects.scala
Original file line number Diff line number Diff line change
Expand Up @@ -123,8 +123,6 @@ object SbtCommunityProject:
def scalacOptions = List(
"-Xcheck-macros",
"-Wsafe-init",
"-Yexplicit-nulls",
"-language:unsafeNulls",
)

object projects:
Expand Down
2 changes: 1 addition & 1 deletion compiler/src/dotty/tools/dotc/Run.scala
Original file line number Diff line number Diff line change
Expand Up @@ -606,7 +606,7 @@ extends ImplicitRunInfo, ConstraintRunInfo, cc.CaptureRunInfo {
.setTyper(new Typer)
.addMode(Mode.ImplicitsEnabled)
.setTyperState(ctx.typerState.fresh(ctx.reporter))
if ctx.settings.YexplicitNulls.value && !Feature.enabledBySetting(nme.unsafeNulls) then
if ctx.settings.YexplicitNulls.value || Feature.enabledBySetting(nme.safeNulls) then
start = start.addMode(Mode.SafeNulls)
ctx.initialize()(using start) // re-initialize the base context with start

Expand Down
1 change: 1 addition & 0 deletions compiler/src/dotty/tools/dotc/config/Feature.scala
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ object Feature:
(nme.noAutoTupling, "Disable automatic tupling"),
(nme.dynamics, "Allow direct or indirect subclasses of scala.Dynamic"),
(nme.unsafeNulls, "Enable unsafe nulls for explicit nulls"),
(nme.safeNulls, "Enable safe nulls for explicit nulls"),
(nme.postfixOps, "Allow postfix operators (not recommended)"),
(nme.strictEquality, "Enable strict equality (disable canEqualAny)"),
(nme.implicitConversions, "Allow implicit conversions without warnings"),
Expand Down
4 changes: 3 additions & 1 deletion compiler/src/dotty/tools/dotc/config/ScalaSettings.scala
Original file line number Diff line number Diff line change
Expand Up @@ -561,7 +561,8 @@ private sealed trait YSettings:
val YmagicOffsetHeader: Setting[String] = StringSetting(ForkSetting, "Ymagic-offset-header", "header", "Specify the magic header comment that marks the start of the actual code in generated wrapper scripts. Example: -Ymagic-offset-header:SOURCE_CODE_START. Then, in the source, the magic comment `///SOURCE_CODE_START:<ORIGINAL_FILE_PATH>` marks the start of user code. The comment should be suffixed by `:<ORIGINAL_FILE_PATH>` to indicate the original file.", "")

// Experimental language features
val YexplicitNulls: Setting[Boolean] = BooleanSetting(ForkSetting, "Yexplicit-nulls", "Make reference types non-nullable. Nullable types can be expressed with unions: e.g. String|Null.")
val YexplicitNulls: Setting[Boolean] = BooleanSetting(ForkSetting, "Yexplicit-nulls", "Since explicit nulls is enabled by default, this flag now enables safe nulls for explicit-nulls")
val YnoExplicitNulls: Setting[Boolean] = BooleanSetting(ForkSetting, "Yno-explicit-nulls", "Make reference types implictly nullable.")
val YnoFlexibleTypes: Setting[Boolean] = BooleanSetting(ForkSetting, "Yno-flexible-types", "Disable turning nullable Java return types and parameter types into flexible types, which behave like abstract types with a nullable lower bound and non-nullable upper bound.")
val YflexifyTasty: Setting[Boolean] = BooleanSetting(ForkSetting, "Yflexify-tasty", "Apply flexification to Scala code compiled without -Yexplicit-nulls, when reading from tasty.")
val YsafeInitGlobal: Setting[Boolean] = BooleanSetting(ForkSetting, "Ysafe-init-global", "Check safe initialization of global objects.")
Expand All @@ -577,6 +578,7 @@ private sealed trait YSettings:
val YexplainLowlevel: Setting[Boolean] = BooleanSetting(ForkSetting, "Yexplain-lowlevel", "When explaining type errors, show types at a lower level.")
val YnoDoubleBindings: Setting[Boolean] = BooleanSetting(ForkSetting, "Yno-double-bindings", "Assert no namedtype is bound twice (should be enabled only if program is error-free).")
val YshowVarBounds: Setting[Boolean] = BooleanSetting(ForkSetting, "Yshow-var-bounds", "Print type variables with their bounds.")
val YhideFlexibleTypes: Setting[Boolean] = BooleanSetting(ForkSetting, "Yhide-flexible-types", "Print flexible types as their base type. (T instead of (T)?)")

val Yinstrument: Setting[Boolean] = BooleanSetting(ForkSetting, "Yinstrument", "Add instrumentation code that counts allocations and closure creations.")
val YinstrumentDefs: Setting[Boolean] = BooleanSetting(ForkSetting, "Yinstrument-defs", "Add instrumentation code that counts method calls; needs -Yinstrument to be set, too.")
Expand Down
10 changes: 6 additions & 4 deletions compiler/src/dotty/tools/dotc/core/Contexts.scala
Original file line number Diff line number Diff line change
Expand Up @@ -483,13 +483,13 @@ object Contexts {
fresh.setSetting(ctx.settings.color, "never")

/** Is the explicit nulls option set? */
def explicitNulls: Boolean = base.settings.YexplicitNulls.value
def explicitNulls: Boolean = !base.settings.YnoExplicitNulls.value

/** Is the flexible types option set? */
def flexibleTypes: Boolean = base.settings.YexplicitNulls.value && !base.settings.YnoFlexibleTypes.value
def flexibleTypes: Boolean = explicitNulls && !base.settings.YnoFlexibleTypes.value

/** Is the flexify tasty option set? */
def flexifyTasty: Boolean = base.settings.YexplicitNulls.value && base.settings.YflexifyTasty.value
def flexifyTasty: Boolean = explicitNulls && base.settings.YflexifyTasty.value

/** Is the best-effort option set? */
def isBestEffort: Boolean = base.settings.YbestEffort.value
Expand Down Expand Up @@ -729,7 +729,9 @@ object Contexts {
importInfo.mentionsFeature(nme.unsafeNulls) match
case Some(true) =>
setMode(this.mode &~ Mode.SafeNulls)
case Some(false) if ctx.settings.YexplicitNulls.value =>
case _ =>
importInfo.mentionsFeature(nme.safeNulls) match
case Some(true) if explicitNulls =>
setMode(this.mode | Mode.SafeNulls)
case _ =>
updateStore(importInfoLoc, importInfo)
Expand Down
22 changes: 15 additions & 7 deletions compiler/src/dotty/tools/dotc/core/Definitions.scala
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,7 @@ class Definitions {
cls.info.decls.openForMutations.useSynthesizer(
name =>
if (name.isTypeName && name.isSyntheticFunction) newFunctionNType(name.asTypeName)
else if (name == tpnme.Null) synthesizeNullClass()
else NoSymbol)
cls
}
Expand Down Expand Up @@ -477,11 +478,7 @@ class Definitions {
@tu lazy val NothingClass: ClassSymbol = enterCompleteClassSymbol(
ScalaPackageClass, tpnme.Nothing, AbstractFinal, List(AnyType))
def NothingType: TypeRef = NothingClass.typeRef
@tu lazy val NullClass: ClassSymbol = {
// When explicit-nulls is enabled, Null becomes a direct subtype of Any and Matchable
val parents = if ctx.explicitNulls then AnyType :: MatchableType :: Nil else ObjectType :: Nil
enterCompleteClassSymbol(ScalaPackageClass, tpnme.Null, AbstractFinal, parents)
}
@tu lazy val NullClass: ClassSymbol = requiredClass("scala.Null")
def NullType: TypeRef = NullClass.typeRef

/*
Expand All @@ -492,6 +489,18 @@ class Definitions {
*/
@tu lazy val RuntimeNothingClass: Symbol = requiredClass("scala.runtime.Nothing$")
@tu lazy val RuntimeNullClass: Symbol = requiredClass("scala.runtime.Null$")
private def synthesizeNullClass(): ClassSymbol = {
// When explicit-nulls is enabled, Null becomes a direct subtype of AnyVal
val completer = new LazyType {
def complete(denot: SymDenotation)(using Context): Unit = {
val cls = denot.asClass.classSymbol
val parents = if ctx.explicitNulls then AnyValType :: Nil else ObjectType :: Nil
val decls = newScope
denot.info = ClassInfo(ScalaPackageClass.thisType, cls, parents, decls)
}
}
newPermanentClassSymbol(ScalaPackageClass, tpnme.Null, AbstractFinal, completer)
}

@tu lazy val InvokerModule = requiredModule("scala.runtime.coverage.Invoker")
@tu lazy val InvokedMethodRef = InvokerModule.requiredMethodRef("invoked")
Expand Down Expand Up @@ -2190,7 +2199,6 @@ class Definitions {
orType,
RepeatedParamClass,
ByNameParamClass2x,
NullClass,
NothingClass,
SingletonClass,
CBCompanion,
Expand All @@ -2204,7 +2212,7 @@ class Definitions {
@tu lazy val syntheticCoreMethods: List[TermSymbol] =
AnyMethods ++ ObjectMethods ++ List(String_+, throwMethod, spreadMethod)

@tu lazy val reservedScalaClassNames: Set[Name] = syntheticScalaClasses.map(_.name).toSet
@tu lazy val reservedScalaClassNames: Set[Name] = syntheticScalaClasses.map(_.name).toSet + tpnme.Null

private var isInitialized = false

Expand Down
1 change: 1 addition & 0 deletions compiler/src/dotty/tools/dotc/core/StdNames.scala
Original file line number Diff line number Diff line change
Expand Up @@ -659,6 +659,7 @@ object StdNames {
val unbox: N = "unbox"
val universe: N = "universe"
val unsafeNulls: N = "unsafeNulls"
val safeNulls: N = "safeNulls"
val update: N = "update"
val updateDynamic: N = "updateDynamic"
val uses: N = "uses"
Expand Down
6 changes: 4 additions & 2 deletions compiler/src/dotty/tools/dotc/core/SymDenotations.scala
Original file line number Diff line number Diff line change
Expand Up @@ -899,15 +899,17 @@ object SymDenotations {
/** Is this symbol a class of which `null` is a value? */
final def isNullableClass(using Context): Boolean =
if ctx.mode.is(Mode.SafeNulls) && !ctx.phase.erasedTypes
then symbol == defn.NullClass || symbol == defn.AnyClass || symbol == defn.MatchableClass
then symbol == defn.NullClass || symbol == defn.AnyClass || symbol == defn.AnyValClass || symbol == defn.MatchableClass
else isNullableClassAfterErasure

/** Is this symbol a class of which `null` is a value after erasure?
* For example, if `-Yexplicit-nulls` is set, `String` is not nullable before erasure,
* but it becomes nullable after erasure.
*/
final def isNullableClassAfterErasure(using Context): Boolean =
isClass && !isValueClass && !is(ModuleClass) && symbol != defn.NothingClass
// `Null` is a value class under `-Yexplicit-nulls` (it extends `AnyVal`), but
// `null` is still a value of it, so it must be treated as a nullable class.
isClass && (!isValueClass || symbol == defn.NullClass) && !is(ModuleClass) && symbol != defn.NothingClass

/** Is `pre` the same as C.this, where C is exactly the owner of this symbol,
* or, if this symbol is protected, a subclass of the owner?
Expand Down
1 change: 1 addition & 0 deletions compiler/src/dotty/tools/dotc/core/SymUtils.scala
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ class SymUtils:
!d.isRefinementClass &&
d.isValueClass &&
(d.initial.symbol ne defn.AnyValClass) && // Compare the initial symbol because AnyVal does not exist after erasure
(d.initial.symbol ne defn.NullClass) &&
!d.isPrimitiveValueClass
}

Expand Down
10 changes: 8 additions & 2 deletions compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala
Original file line number Diff line number Diff line change
Expand Up @@ -421,7 +421,10 @@ class TreeUnpickler(reader: TastyReader,
if nothingButMods(end) then AliasingBounds(readVariances(lo))
else
val hi = readVariances(readType())
createNullableTypeBounds(lo, hi)
if (ctx.flexifyTasty && !explicitNulls)
createNullableTypeBounds(lo, hi)
else
TypeBounds(lo, hi)
case ANNOTATEDtype =>
val parent = readType()
val ann =
Expand Down Expand Up @@ -1698,7 +1701,10 @@ class TreeUnpickler(reader: TastyReader,
val lo = readTpt()
val hi = if currentAddr == end then lo else readTpt()
val alias = if currentAddr == end then EmptyTree else readTpt()
createNullableTypeBoundsTree(lo, hi, alias)
if (ctx.flexifyTasty && !explicitNulls)
createNullableTypeBoundsTree(lo, hi, alias)
else
TypeBoundsTree(lo, hi, alias)
case QUOTE =>
Quote(readTree(), Nil).withBodyType(readType())
case SPLICE =>
Expand Down
5 changes: 4 additions & 1 deletion compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala
Original file line number Diff line number Diff line change
Expand Up @@ -335,7 +335,10 @@ class PlainPrinter(_ctx: Context) extends Printer {
case _ =>
toTextLocal(tpe) ~ " " ~ toText(annot)
case FlexibleType(_, tpe) =>
"(" ~ toText(tpe) ~ ")?"
if (ctx.settings.YhideFlexibleTypes.value) then
toText(tpe)
else
"(" ~ toText(tpe) ~ ")?"
case tp: TypeVar =>
def toTextCaret(tp: Type) = if printDebug then toTextLocal(tp) ~ Str("^") else toText(tp)
if (tp.isInstantiated)
Expand Down
2 changes: 1 addition & 1 deletion compiler/src/dotty/tools/dotc/transform/Pickler.scala
Original file line number Diff line number Diff line change
Expand Up @@ -291,7 +291,7 @@ class Pickler extends Phase {
val attributes = Attributes(
sourceFile = sourceRelativePath,
scala2StandardLibrary = Feature.shouldBehaveAsScala2,
explicitNulls = ctx.settings.YexplicitNulls.value,
explicitNulls = ctx.explicitNulls,
captureChecked = Feature.ccEnabled,
withPureFuns = Feature.pureFunsEnabled,
isJava = isJavaAttr,
Expand Down
4 changes: 2 additions & 2 deletions compiler/src/dotty/tools/dotc/typer/Applications.scala
Original file line number Diff line number Diff line change
Expand Up @@ -997,9 +997,9 @@ trait Applications extends Compatibility {
// However, for overload resolution, we want to check applicability:
// "could this work with some type instantiation?" (yes, if ? = String)
def wildcardArgOK =
argtpe match
argtpe.stripNull() match
case at @ AppliedType(tycon1, args1) if at.hasWildcardArg =>
formal match
formal.stripNull() match
case AppliedType(tycon2, args2)
if tycon1 =:= tycon2 && args1.length == args2.length =>
// We need to handle all 4 cases, in addition to
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 @@ -233,7 +233,7 @@ class CompilationTests {
val compilationTest = withCoverage(aggregateTests(
compileFilesInDir("tests/explicit-nulls/pos", explicitNullsOptions),
compileFilesInDir("tests/explicit-nulls/flexible-types-common", explicitNullsOptions),
compileFilesInDir("tests/explicit-nulls/unsafe-common", explicitNullsOptions `and` "-language:unsafeNulls" `and` "-Yno-flexible-types"),
compileFilesInDir("tests/explicit-nulls/unsafe-common", defaultOptions `and` "-Yno-flexible-types"),
))
runWithCoverageOrFallback[PosTestWithCoverage](compilationTest, "Pos")

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 @@ -68,7 +68,7 @@ object TestConfiguration {
Properties.scalaXml
))

lazy val replWithStagingClasspath =
lazy val replWithStagingClasspath =
replClassPath + File.pathSeparator + mkClasspath(List(Properties.dottyStaging))

def mkClasspath(classpaths: List[String]): String =
Expand Down Expand Up @@ -107,7 +107,7 @@ object TestConfiguration {
val picklingWithCompilerOptions =
picklingOptions.and("-Yexplicit-nulls").withClasspath(withCompilerClasspath).withRunClasspath(withCompilerClasspath)

val explicitNullsOptions = defaultOptions `and` "-Yexplicit-nulls"
val explicitNullsOptions = defaultOptions `and` "-language:safeNulls"

val oldSyntax = defaultOptions `and` "-old-syntax"
val newSyntax = defaultOptions `and` "-new-syntax"
Expand Down
6 changes: 3 additions & 3 deletions docs/_docs/internals/explicit-nulls.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,11 @@ The explicit-nulls flag is currently disabled by default. It can be enabled via

## Type Hierarchy

We change the type hierarchy so that `Null` is only a subtype of `Any` by:
We change the type hierarchy so that `Null` is only a subtype of `AnyVal` by:

- modifying the notion of what is a nullable class (`isNullableClass`) in `SymDenotations`
to include _only_ `Null` and `Any`, which is used by `TypeComparer`
- changing the parent of `Null` in `Definitions` to point to `Any` and not `AnyRef`
to include _only_ `Null` and its superclasses, which is used by `TypeComparer`
- changing the parent of `Null` in `Definitions` to point to `AnyVal` and not `AnyRef`
- changing `isBottomType` and `isBottomClass` in `Definitions`

## Working with Nullable Unions
Expand Down
7 changes: 5 additions & 2 deletions docs/_docs/reference/experimental/explicit-nulls.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,17 @@ Originally, `Null` is a subtype of all reference types.

!["Original Type Hierarchy"](images/explicit-nulls/scalaHierarchyWithMatchable.png)

When explicit nulls is enabled, the type hierarchy changes so that `Null` is only
a subtype of `Any` and `Matchable`, as opposed to every reference type,
When explicit nulls is enabled, the type hierarchy changes so that `Null` is not
a subtype of every reference type anymore,
which means `null` is no longer a value of `AnyRef` and its subtypes.
Instead, it is a regular class that extends `AnyVal`.

This is the new type hierarchy:

!["Type Hierarchy for Explicit Nulls"](images/explicit-nulls/scalaHierarchyWithMatchableAndSafeNull.png)

TODO Adjust this image. Where is its source?

After erasure, `Null` remains a subtype of all reference types (as forced by the JVM).

## Working with `Null`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -197,12 +197,12 @@ class CompletionTest {

@Test def importJavaStaticMethod: Unit = {
code"""import java.lang.System.lineSep${m1}"""
.completion(("lineSeparator", Method, "(): String"))
.completion(("lineSeparator", Method, "(): (String)?"))
}

@Test def importJavaStaticField: Unit = {
code"""import java.lang.System.ou${m1}"""
.completion(("out", Field, "java.io.PrintStream"))
.completion(("out", Field, "(java.io.PrintStream)?"))
}

@Test def importFromExplicitAndSyntheticPackageObject: Unit = {
Expand Down
4 changes: 2 additions & 2 deletions library/src/scala/collection/immutable/RedBlackTree.scala
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ private[collection] object RedBlackTree {
} else tree.black
}
/** Creates a new balanced tree where `newLeft` replaces `tree.left`.
* tree and newLeft are never null
* tree and newLeft are never null
*
* @tparam A1 the key type of the tree
* @tparam B the original value type of the tree
Expand Down Expand Up @@ -120,7 +120,7 @@ private[collection] object RedBlackTree {
}
}
/** Creates a new balanced tree where `newRight` replaces `tree.right`.
* tree and newRight are never null
* tree and newRight are never null
*
* @tparam A1 the key type of the tree
* @tparam B the original value type of the tree
Expand Down
3 changes: 3 additions & 0 deletions library/src/scala/language.scala
Original file line number Diff line number Diff line change
Expand Up @@ -447,6 +447,9 @@ object language {
@compileTimeOnly("`unsafeNulls` can only be used at compile time in import statements")
object unsafeNulls

@compileTimeOnly("`safeNulls` can only be used at compile time in import statements")
object safeNulls

@compileTimeOnly("`future` can only be used at compile time in import statements")
object future

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,12 @@ final class InferredMethodProvider(
val path =
Interactive.pathTo(driver.openedTrees(uri), pos)(using driver.currentCtx)

val newctx = driver.currentCtx.fresh.setCompilationUnit(unit)
val newctx = driver.currentCtx.fresh
.setCompilationUnit(unit)
.setSettings(driver.currentCtx.settings.YhideFlexibleTypes.updateIn(
driver.currentCtx.settingsState.reinitializedCopy(),
true
))
val indexedContext = IndexedContext(pos, path, newctx)
import indexedContext.ctx

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,12 @@ final class InferredTypeProvider(
driver.run(uri, source)
val unit = driver.currentCtx.run.nn.units.head
val pos = driver.sourcePosition(params)
val newctx = driver.currentCtx.fresh.setCompilationUnit(unit)
val newctx = driver.currentCtx.fresh
.setCompilationUnit(unit)
.setSettings(driver.currentCtx.settings.YhideFlexibleTypes.updateIn(
driver.currentCtx.settingsState.reinitializedCopy(),
true
))
val path =
Interactive.pathTo(newctx.compilationUnit.tpdTree, pos.span)(using newctx)
val indexedCtx = IndexedContext(pos, path, newctx)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ class CompletionProvider(
case Some(unit) =>
val newctx = ctx.fresh
.setCompilationUnit(unit)
.setSettings(ctx.settings.YhideFlexibleTypes.updateIn(ctx.settingsState.reinitializedCopy(), true))
.setProfiler(Profiler()(using ctx))
.withPhase(Phases.typerPhase(using ctx))
val tpdPath0 = Interactive.pathTo(unit.tpdTree, pos.span)(using newctx)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,11 @@ class HoverDocSuite extends BaseHoverSuite:
|""".stripMargin,
"""|**Expression type**:
|```scala
|java.util.List[Int]
|(java.util.List[Int])?
|```
|**Symbol signature**:
|```scala
|final def emptyList[T](): java.util.List[T]
|final def emptyList[T](): (java.util.List[T])?
|```
|Found documentation for java/util/Collections#emptyList().
|""".stripMargin
Expand All @@ -57,7 +57,7 @@ class HoverDocSuite extends BaseHoverSuite:
|}
""".stripMargin,
"""|```scala
|def substring(beginIndex: Int): String
|def substring(beginIndex: Int): (String)?
|```
|Found documentation for java/lang/String#substring().
|""".stripMargin
Expand Down
Loading
Loading