From 077c5990cc9004558279cdc758d15d53be80ad7c Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Thu, 21 Jan 2021 13:11:11 +0100 Subject: [PATCH 1/4] Make sure language imports are detectable syntactically Make sure that something is a language import if and only if it looks like one. i.e. is of one of the forms import language.xyz import scala.language.xyz import _root_.scala.language.xyz --- .../src/dotty/tools/dotc/ast/TreeInfo.scala | 16 ++++++++++ .../src/dotty/tools/dotc/core/Contexts.scala | 9 ++---- .../dotty/tools/dotc/parsing/Parsers.scala | 6 +--- .../src/dotty/tools/dotc/typer/Checking.scala | 10 ++++-- .../dotty/tools/dotc/typer/ImportInfo.scala | 31 ++++++++++++------- tests/neg/language-import.scala | 23 ++++++++++++++ 6 files changed, 69 insertions(+), 26 deletions(-) create mode 100644 tests/neg/language-import.scala diff --git a/compiler/src/dotty/tools/dotc/ast/TreeInfo.scala b/compiler/src/dotty/tools/dotc/ast/TreeInfo.scala index 62ab6d2ba844..93f0875ce41b 100644 --- a/compiler/src/dotty/tools/dotc/ast/TreeInfo.scala +++ b/compiler/src/dotty/tools/dotc/ast/TreeInfo.scala @@ -251,6 +251,22 @@ trait TreeInfo[T >: Untyped <: Type] { self: Trees.Instance[T] => case TypeDefs(_) => true case _ => isUsingClause(params) + private val languageImportParts = List(nme.language, nme.scala, nme.ROOTPKG) + private val languageImportNested = Set[Name](nme.experimental) + + /** Strip the name of any local object in `scala.language` from `path` */ + def stripLanguageNested(path: Tree): Tree = path match + case Select(qual, name) if languageImportNested.contains(name) => qual + case _ => path + + /** Does this `path` look like a language import? */ + def isLanguageImport(path: Tree): Boolean = + def parts(tree: Tree): List[Name] = tree match + case tree: RefTree => tree.name :: parts(tree.qualifier) + case EmptyTree => Nil + case _ => EmptyTermName :: Nil + !path.isEmpty && languageImportParts.startsWith(parts(stripLanguageNested(path))) + /** The underlying pattern ignoring any bindings */ def unbind(x: Tree): Tree = unsplice(x) match { case Bind(_, y) => unbind(y) diff --git a/compiler/src/dotty/tools/dotc/core/Contexts.scala b/compiler/src/dotty/tools/dotc/core/Contexts.scala index d504809a9a2e..c5630dd012b6 100644 --- a/compiler/src/dotty/tools/dotc/core/Contexts.scala +++ b/compiler/src/dotty/tools/dotc/core/Contexts.scala @@ -474,13 +474,8 @@ object Contexts { else fresh.setOwner(exprOwner) /** A new context that summarizes an import statement */ - def importContext(imp: Import[?], sym: Symbol): FreshContext = { - val impNameOpt = imp.expr match { - case ref: RefTree[?] => Some(ref.name.asTermName) - case _ => None - } - fresh.setImportInfo(ImportInfo(sym, imp.selectors, impNameOpt)) - } + def importContext(imp: Import[?], sym: Symbol): FreshContext = + fresh.setImportInfo(ImportInfo(sym, imp.selectors, imp.expr)) /** Is the debug option set? */ def debug: Boolean = base.settings.Ydebug.value diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index f299d73758a1..ceea91bb31d5 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -3068,11 +3068,7 @@ object Parsers { /** Create an import node and handle source version imports */ def mkImport(outermost: Boolean = false): ImportConstr = (tree, selectors) => - val isLanguageImport = tree match - case Ident(nme.language) => true - case Select(Ident(nme.scala), nme.language) => true - case _ => false - if isLanguageImport then + if isLanguageImport(tree) then for case ImportSelector(id @ Ident(imported), EmptyTree, _) <- selectors if allSourceVersionNames.contains(imported) diff --git a/compiler/src/dotty/tools/dotc/typer/Checking.scala b/compiler/src/dotty/tools/dotc/typer/Checking.scala index 0c654c3fb024..f0dd02bdaf9a 100644 --- a/compiler/src/dotty/tools/dotc/typer/Checking.scala +++ b/compiler/src/dotty/tools/dotc/typer/Checking.scala @@ -734,9 +734,15 @@ trait Checking { } /** Check that `path` is a legal prefix for an import clause */ - def checkLegalImportPath(path: Tree)(using Context): Unit = { + def checkLegalImportPath(path: Tree)(using Context): Unit = checkLegalImportOrExportPath(path, "import prefix") - } + val normPath = stripLanguageNested(path) + if isLanguageImport(normPath) then + if normPath.symbol != defn.LanguageModule then + report.error(em"import looks like a language import, but refers to something else: ${normPath.symbol.showLocated}", path.srcPos) + else + if normPath.tpe.classSymbols.contains(defn.LanguageModule.moduleClass) then + report.error(em"no aliases can be used to refer to a language import", path.srcPos) /** Check that `path` is a legal prefix for an export clause */ def checkLegalExportPath(path: Tree, selectors: List[untpd.ImportSelector])(using Context): Unit = diff --git a/compiler/src/dotty/tools/dotc/typer/ImportInfo.scala b/compiler/src/dotty/tools/dotc/typer/ImportInfo.scala index 167a3a3b3bcb..a766617536a3 100644 --- a/compiler/src/dotty/tools/dotc/typer/ImportInfo.scala +++ b/compiler/src/dotty/tools/dotc/typer/ImportInfo.scala @@ -33,7 +33,7 @@ object ImportInfo { val expr = tpd.Ident(ref.refFn()) // refFn must be called in the context of ImportInfo.sym tpd.Import(expr, selectors).symbol - ImportInfo(sym, selectors, None, isRootImport = true) + ImportInfo(sym, selectors, untpd.EmptyTree, isRootImport = true) extension (c: Context) def withRootImports(rootRefs: List[RootRef])(using Context): Context = @@ -42,22 +42,26 @@ object ImportInfo { def withRootImports: Context = given Context = c c.withRootImports(defn.rootImportFns) - } /** Info relating to an import clause * @param sym The import symbol defined by the clause * @param selectors The selector clauses - * @param symNameOpt Optionally, the name of the import symbol. None for root imports. + * @param qualifier The import qualifier, or EmptyTree for root imports. * Defined for all explicit imports from ident or select nodes. * @param isRootImport true if this is one of the implicit imports of scala, java.lang, * scala.Predef in the start context, false otherwise. */ class ImportInfo(symf: Context ?=> Symbol, val selectors: List[untpd.ImportSelector], - symNameOpt: Option[TermName], + val qualifier: untpd.Tree, val isRootImport: Boolean = false) extends Showable { + private def symNameOpt = qualifier match { + case ref: untpd.RefTree => Some(ref.name.asTermName) + case _ => None + } + def sym(using Context): Symbol = { if (mySym == null) { mySym = symf @@ -177,6 +181,8 @@ class ImportInfo(symf: Context ?=> Symbol, assert(myUnimported != null) myUnimported + private val isLanguageImport: Boolean = untpd.isLanguageImport(qualifier) + private var myUnimported: Symbol = _ private var myOwner: Symbol = null @@ -185,14 +191,15 @@ class ImportInfo(symf: Context ?=> Symbol, /** Does this import clause or a preceding import clause import `owner.feature`? */ def featureImported(feature: TermName, owner: Symbol)(using Context): Boolean = - def compute = - val isImportOwner = site.typeSymbol.eq(owner) - if isImportOwner && forwardMapping.contains(feature) then true - else if isImportOwner && excluded.contains(feature) then false - else - var c = ctx.outer - while c.importInfo eq ctx.importInfo do c = c.outer - (c.importInfo != null) && c.importInfo.featureImported(feature, owner)(using c) + def compute: Boolean = + if isLanguageImport then + val isImportOwner = site.typeSymbol.eq(owner) + if isImportOwner then + if forwardMapping.contains(feature) then return true + if excluded.contains(feature) then return false + var c = ctx.outer + while c.importInfo eq ctx.importInfo do c = c.outer + (c.importInfo != null) && c.importInfo.featureImported(feature, owner)(using c) if myOwner.ne(owner) || !myResults.contains(feature) then myOwner = owner diff --git a/tests/neg/language-import.scala b/tests/neg/language-import.scala new file mode 100644 index 000000000000..cb9fccc02061 --- /dev/null +++ b/tests/neg/language-import.scala @@ -0,0 +1,23 @@ +object a with + val l = _root_.scala.language + import l.noAutoTupling // error + import l.experimental.genericNumberLiterals // error + val scala = c + import scala.language.noAutoTupling // error + val language = b + import language.experimental.genericNumberLiterals // error + +object b with + val strictEquality = 22 + object experimental with + val genericNumberLiterals = 22 + +object c with + val language = b + import b.strictEquality + +object d with + import language.experimental.genericNumberLiterals // ok + import scala.language.noAutoTupling // ok + import _root_.scala.language.strictEquality // ok + From 1369a146e4b35617d3789062875bd6b54717ac0b Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Sat, 23 Jan 2021 13:52:15 +0100 Subject: [PATCH 2/4] Simplifications Avoid a notation of "owner" when checking for a language feature. Instead, allow qualified names for language features, e.g. `experimental.dependent". --- .../src/dotty/tools/dotc/ast/TreeInfo.scala | 36 ++++++---- .../src/dotty/tools/dotc/config/Feature.scala | 51 ++++++-------- .../src/dotty/tools/dotc/typer/Checking.scala | 20 ++++-- .../dotty/tools/dotc/typer/ImportInfo.scala | 68 ++++++++++++------- .../tools/dotc/typer/ImportSuggestions.scala | 2 +- .../src/dotty/tools/dotc/typer/Typer.scala | 8 +-- 6 files changed, 105 insertions(+), 80 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/ast/TreeInfo.scala b/compiler/src/dotty/tools/dotc/ast/TreeInfo.scala index 93f0875ce41b..515f379cf8e2 100644 --- a/compiler/src/dotty/tools/dotc/ast/TreeInfo.scala +++ b/compiler/src/dotty/tools/dotc/ast/TreeInfo.scala @@ -251,21 +251,27 @@ trait TreeInfo[T >: Untyped <: Type] { self: Trees.Instance[T] => case TypeDefs(_) => true case _ => isUsingClause(params) - private val languageImportParts = List(nme.language, nme.scala, nme.ROOTPKG) - private val languageImportNested = Set[Name](nme.experimental) - - /** Strip the name of any local object in `scala.language` from `path` */ - def stripLanguageNested(path: Tree): Tree = path match - case Select(qual, name) if languageImportNested.contains(name) => qual - case _ => path - - /** Does this `path` look like a language import? */ - def isLanguageImport(path: Tree): Boolean = - def parts(tree: Tree): List[Name] = tree match - case tree: RefTree => tree.name :: parts(tree.qualifier) - case EmptyTree => Nil - case _ => EmptyTermName :: Nil - !path.isEmpty && languageImportParts.startsWith(parts(stripLanguageNested(path))) + /** If `path` looks like a language import, `Some(name)` where name + * is `experimental` if that sub-module is imported, and the empty + * term name otherwise. + */ + def languageImport(path: Tree): Option[TermName] = path match + case Select(p1, nme.experimental) => + languageImport(p1) match + case Some(EmptyTermName) => Some(nme.experimental) + case _ => None + case p1: RefTree if p1.name == nme.language => + p1.qualifier match + case EmptyTree => Some(EmptyTermName) + case p2: RefTree if p2.name == nme.scala => + p2.qualifier match + case EmptyTree => Some(EmptyTermName) + case Ident(nme.ROOTPKG) => Some(EmptyTermName) + case _ => None + case _ => None + case _ => None + + def isLanguageImport(path: Tree): Boolean = languageImport(path).isDefined /** The underlying pattern ignoring any bindings */ def unbind(x: Tree): Tree = unsplice(x) match { diff --git a/compiler/src/dotty/tools/dotc/config/Feature.scala b/compiler/src/dotty/tools/dotc/config/Feature.scala index 1984066f4f23..df428c1e1bec 100644 --- a/compiler/src/dotty/tools/dotc/config/Feature.scala +++ b/compiler/src/dotty/tools/dotc/config/Feature.scala @@ -9,12 +9,17 @@ import Decorators.{_, given} import util.SrcPos import SourceVersion._ import reporting.Message +import NameKinds.QualifiedName object Feature: - private val dependent = "dependent".toTermName - private val namedTypeArguments = "namedTypeArguments".toTermName - private val genericNumberLiterals = "genericNumberLiterals".toTermName + private def experimental(str: String): TermName = + QualifiedName(nme.experimental, str.toTermName) + + private val Xdependent = experimental("dependent") + private val XnamedTypeArguments = experimental("namedTypeArguments") + private val XgenericNumberLiterals = experimental("genericNumberLiterals") + private val Xmacros = experimental("macros") /** Is `feature` enabled by by a command-line setting? The enabling setting is * @@ -23,12 +28,8 @@ object Feature: * where is the fully qualified name of `owner`, followed by a ".", * but subtracting the prefix `scala.language.` at the front. */ - def enabledBySetting(feature: TermName, owner: Symbol = NoSymbol)(using Context): Boolean = - def toPrefix(sym: Symbol): String = - if !sym.exists || sym == defn.LanguageModule.moduleClass then "" - else toPrefix(sym.owner) + sym.name.stripModuleClassSuffix + "." - val prefix = if owner ne NoSymbol then toPrefix(owner) else "" - ctx.base.settings.language.value.contains(prefix + feature) + def enabledBySetting(feature: TermName)(using Context): Boolean = + ctx.base.settings.language.value.contains(feature.toString) /** Is `feature` enabled by by an import? This is the case if the feature * is imported by a named import @@ -39,39 +40,31 @@ object Feature: * * import owner.{ feature => _ } */ - def enabledByImport(feature: TermName, owner: Symbol = NoSymbol)(using Context): Boolean = - atPhase(typerPhase) { - ctx.importInfo != null - && ctx.importInfo.featureImported(feature, - if owner.exists then owner else defn.LanguageModule.moduleClass) - } + def enabledByImport(feature: TermName)(using Context): Boolean = + //atPhase(typerPhase) { + ctx.importInfo != null && ctx.importInfo.featureImported(feature) + //} /** Is `feature` enabled by either a command line setting or an import? * @param feature The name of the feature * @param owner The prefix symbol (nested in `scala.language`) where the * feature is defined. */ - def enabled(feature: TermName, owner: Symbol = NoSymbol)(using Context): Boolean = - enabledBySetting(feature, owner) || enabledByImport(feature, owner) + def enabled(feature: TermName)(using Context): Boolean = + enabledBySetting(feature) || enabledByImport(feature) /** Is auto-tupling enabled? */ - def autoTuplingEnabled(using Context): Boolean = - !enabled(nme.noAutoTupling) + def autoTuplingEnabled(using Context): Boolean = !enabled(nme.noAutoTupling) - def dynamicsEnabled(using Context): Boolean = - enabled(nme.dynamics) + def dynamicsEnabled(using Context): Boolean = enabled(nme.dynamics) - def dependentEnabled(using Context) = - enabled(dependent, defn.LanguageExperimentalModule.moduleClass) + def dependentEnabled(using Context) = enabled(Xdependent) - def namedTypeArgsEnabled(using Context) = - enabled(namedTypeArguments, defn.LanguageExperimentalModule.moduleClass) + def namedTypeArgsEnabled(using Context) = enabled(XnamedTypeArguments) - def genericNumberLiteralsEnabled(using Context) = - enabled(genericNumberLiterals, defn.LanguageExperimentalModule.moduleClass) + def genericNumberLiteralsEnabled(using Context) = enabled(XgenericNumberLiterals) - def scala2ExperimentalMacroEnabled(using Context) = - enabled("macros".toTermName, defn.LanguageExperimentalModule.moduleClass) + def scala2ExperimentalMacroEnabled(using Context) = enabled(Xmacros) def sourceVersionSetting(using Context): SourceVersion = SourceVersion.valueOf(ctx.settings.source.value) diff --git a/compiler/src/dotty/tools/dotc/typer/Checking.scala b/compiler/src/dotty/tools/dotc/typer/Checking.scala index f0dd02bdaf9a..32bcbc0aab4b 100644 --- a/compiler/src/dotty/tools/dotc/typer/Checking.scala +++ b/compiler/src/dotty/tools/dotc/typer/Checking.scala @@ -736,13 +736,19 @@ trait Checking { /** Check that `path` is a legal prefix for an import clause */ def checkLegalImportPath(path: Tree)(using Context): Unit = checkLegalImportOrExportPath(path, "import prefix") - val normPath = stripLanguageNested(path) - if isLanguageImport(normPath) then - if normPath.symbol != defn.LanguageModule then - report.error(em"import looks like a language import, but refers to something else: ${normPath.symbol.showLocated}", path.srcPos) - else - if normPath.tpe.classSymbols.contains(defn.LanguageModule.moduleClass) then - report.error(em"no aliases can be used to refer to a language import", path.srcPos) + languageImport(path) match + case Some(prefix) => + val required = + if prefix == nme.experimental then defn.LanguageExperimentalModule + else defn.LanguageModule + if path.symbol != required then + report.error(em"import looks like a language import, but refers to something else: ${path.symbol.showLocated}", path.srcPos) + case None => + val foundClasses = path.tpe.classSymbols + if foundClasses.contains(defn.LanguageModule.moduleClass) + || foundClasses.contains(defn.LanguageExperimentalModule.moduleClass) + then + report.error(em"no aliases can be used to refer to a language import", path.srcPos) /** Check that `path` is a legal prefix for an export clause */ def checkLegalExportPath(path: Tree, selectors: List[untpd.ImportSelector])(using Context): Unit = diff --git a/compiler/src/dotty/tools/dotc/typer/ImportInfo.scala b/compiler/src/dotty/tools/dotc/typer/ImportInfo.scala index a766617536a3..3d43cc5976c8 100644 --- a/compiler/src/dotty/tools/dotc/typer/ImportInfo.scala +++ b/compiler/src/dotty/tools/dotc/typer/ImportInfo.scala @@ -13,6 +13,7 @@ import config.SourceVersion import StdNames.nme import printing.Texts.Text import ProtoTypes.NoViewsAllowed.normalizedCompatible +import NameKinds.QualifiedName import Decorators._ object ImportInfo { @@ -45,7 +46,7 @@ object ImportInfo { } /** Info relating to an import clause - * @param sym The import symbol defined by the clause + * @param symf A function that computes the import symbol defined by the clause * @param selectors The selector clauses * @param qualifier The import qualifier, or EmptyTree for root imports. * Defined for all explicit imports from ident or select nodes. @@ -62,7 +63,7 @@ class ImportInfo(symf: Context ?=> Symbol, case _ => None } - def sym(using Context): Symbol = { + def importSym(using Context): Symbol = { if (mySym == null) { mySym = symf assert(mySym != null) @@ -72,7 +73,7 @@ class ImportInfo(symf: Context ?=> Symbol, private var mySym: Symbol = _ /** The (TermRef) type of the qualifier of the import clause */ - def site(using Context): Type = sym.info match { + def site(using Context): Type = importSym.info match { case ImportType(expr) => expr.tpe case _ => NoType } @@ -185,27 +186,46 @@ class ImportInfo(symf: Context ?=> Symbol, private var myUnimported: Symbol = _ - private var myOwner: Symbol = null - private var myResults: SimpleIdentityMap[TermName, java.lang.Boolean] = SimpleIdentityMap.empty - - /** Does this import clause or a preceding import clause import `owner.feature`? */ - def featureImported(feature: TermName, owner: Symbol)(using Context): Boolean = - - def compute: Boolean = - if isLanguageImport then - val isImportOwner = site.typeSymbol.eq(owner) - if isImportOwner then - if forwardMapping.contains(feature) then return true - if excluded.contains(feature) then return false - var c = ctx.outer - while c.importInfo eq ctx.importInfo do c = c.outer - (c.importInfo != null) && c.importInfo.featureImported(feature, owner)(using c) - - if myOwner.ne(owner) || !myResults.contains(feature) then - myOwner = owner - myResults = myResults.updated(feature, compute) - myResults(feature) - end featureImported + private var featureCache: SimpleIdentityMap[TermName, java.lang.Boolean] = SimpleIdentityMap.empty + + /** Does this import clause or a preceding import clause enable or disable `feature`? + * @param feature See featureImported for a description + * @return Some(true) if `feature` is imported + * Some(false) if `feature` is excluded + * None if `feature` is not mentioned, or this is not a language import + */ + def mentionsFeature(feature: TermName)(using Context): Option[Boolean] = + def test(prefix: TermName, feature: TermName): Option[Boolean] = + untpd.languageImport(qualifier) match + case Some(`prefix`) => + if forwardMapping.contains(feature) then Some(true) + else if excluded.contains(feature) then Some(false) + else None + case _ => None + feature match + case QualifiedName(prefix, name) => test(prefix, name) + case _ => test(EmptyTermName, feature) + + /** Does this import clause or a preceding import clause enable `feature`? + * + * @param feature a possibly quailified name, e.g. + * strictEquality + * experimental.genericNumberLiterals + * + * An excluded feature such as `strictEquality => _` in a language import + * means that preceding imports are not considered and the feature is not imported. + */ + def featureImported(feature: TermName)(using Context): Boolean = + if !featureCache.contains(feature) then + featureCache = featureCache.updated(feature, + mentionsFeature(feature) match + case Some(bv) => bv + case None => + var c = ctx.outer + while c.importInfo eq ctx.importInfo do c = c.outer + (c.importInfo != null) && c.importInfo.featureImported(feature)(using c) + ) + featureCache(feature) def toText(printer: Printer): Text = printer.toText(this) } diff --git a/compiler/src/dotty/tools/dotc/typer/ImportSuggestions.scala b/compiler/src/dotty/tools/dotc/typer/ImportSuggestions.scala index 4c3e8b0980dd..20564f56e490 100644 --- a/compiler/src/dotty/tools/dotc/typer/ImportSuggestions.scala +++ b/compiler/src/dotty/tools/dotc/typer/ImportSuggestions.scala @@ -122,7 +122,7 @@ trait ImportSuggestions: .flatMap(sym => rootsIn(sym.termRef)) val imported = if ctx.importInfo eq ctx.outer.importInfo then Nil - else ctx.importInfo.sym.info match + else ctx.importInfo.importSym.info match case ImportType(expr) => rootsOnPath(expr.tpe) case _ => Nil defined ++ imported ++ recur(using ctx.outer) diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index b9eff97f1a5c..60afb279c1f6 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -206,7 +206,7 @@ class Typer extends Namer else found def selection(imp: ImportInfo, name: Name, checkBounds: Boolean): Type = - imp.sym.info match + imp.importSym.info match case ImportType(expr) => val pre = expr.tpe var denot = pre.memberBasedOnFlags(name, required, excluded) @@ -222,8 +222,8 @@ class Typer extends Namer if unimported.isEmpty || !unimported.contains(pre.termSymbol) then return pre.select(name, denot) case _ => - if imp.sym.isCompleting then - report.warning(i"cyclic ${imp.sym}, ignored", pos) + if imp.importSym.isCompleting then + report.warning(i"cyclic ${imp.importSym}, ignored", pos) NoType /** The type representing a named import with enclosing name when imported @@ -393,7 +393,7 @@ class Typer extends Namer val namedImp = namedImportRef(curImport) if (namedImp.exists) recurAndCheckNewOrShadowed(namedImp, NamedImport, ctx)(using outer) - else if (isPossibleImport(WildImport) && !curImport.sym.isCompleting) { + else if (isPossibleImport(WildImport) && !curImport.importSym.isCompleting) { val wildImp = wildImportRef(curImport) if (wildImp.exists) recurAndCheckNewOrShadowed(wildImp, WildImport, ctx)(using outer) From 4a9edc52ff528f1eb4c23f5628d0f5da9bcda719 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Sat, 23 Jan 2021 14:00:08 +0100 Subject: [PATCH 3/4] Add mode bit to track whether we are in safe nulls mode The bit is set if - -Yexplicit-nulls is set - we are not in an `import.unsafeNulls` scope --- compiler/src/dotty/tools/dotc/Run.scala | 4 +++- compiler/src/dotty/tools/dotc/core/Contexts.scala | 10 +++++++++- compiler/src/dotty/tools/dotc/core/Mode.scala | 3 +++ compiler/src/dotty/tools/dotc/core/StdNames.scala | 1 + library/src/scala/runtime/stdLibPatches/language.scala | 2 ++ 5 files changed, 18 insertions(+), 2 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/Run.scala b/compiler/src/dotty/tools/dotc/Run.scala index 0edc668cd614..d37a6be2b3ec 100644 --- a/compiler/src/dotty/tools/dotc/Run.scala +++ b/compiler/src/dotty/tools/dotc/Run.scala @@ -71,11 +71,13 @@ class Run(comp: Compiler, ictx: Context) extends ImplicitRunInfo with Constraint .setPeriod(Period(comp.nextRunId, FirstPhaseId)) .setScope(rootScope) rootScope.enter(ctx.definitions.RootPackage)(using bootstrap) - val start = bootstrap.fresh + var start = bootstrap.fresh .setOwner(defn.RootClass) .setTyper(new Typer) .addMode(Mode.ImplicitsEnabled) .setTyperState(ctx.typerState.fresh(ctx.reporter)) + if ctx.settings.YexplicitNulls.value then + start = start.addMode(Mode.SafeNulls) ctx.initialize()(using start) // re-initialize the base context with start start.setRun(this) } diff --git a/compiler/src/dotty/tools/dotc/core/Contexts.scala b/compiler/src/dotty/tools/dotc/core/Contexts.scala index c5630dd012b6..e4b8fd844506 100644 --- a/compiler/src/dotty/tools/dotc/core/Contexts.scala +++ b/compiler/src/dotty/tools/dotc/core/Contexts.scala @@ -27,6 +27,7 @@ import collection.mutable import printing._ import config.{JavaPlatform, SJSPlatform, Platform, ScalaSettings} import classfile.ReusableDataReader +import StdNames.nme import scala.annotation.internal.sharable @@ -627,7 +628,14 @@ object Contexts { def setRun(run: Run): this.type = updateStore(runLoc, run) def setProfiler(profiler: Profiler): this.type = updateStore(profilerLoc, profiler) def setNotNullInfos(notNullInfos: List[NotNullInfo]): this.type = updateStore(notNullInfosLoc, notNullInfos) - def setImportInfo(importInfo: ImportInfo): this.type = updateStore(importInfoLoc, importInfo) + def setImportInfo(importInfo: ImportInfo): this.type = + importInfo.mentionsFeature(nme.unsafeNulls) match + case Some(true) => + setMode(this.mode &~ Mode.SafeNulls) + case Some(false) if ctx.settings.YexplicitNulls.value => + setMode(this.mode | Mode.SafeNulls) + case _ => + updateStore(importInfoLoc, importInfo) def setTypeAssigner(typeAssigner: TypeAssigner): this.type = updateStore(typeAssignerLoc, typeAssigner) def setProperty[T](key: Key[T], value: T): this.type = diff --git a/compiler/src/dotty/tools/dotc/core/Mode.scala b/compiler/src/dotty/tools/dotc/core/Mode.scala index 99fbdc2621a9..864fcd79fe69 100644 --- a/compiler/src/dotty/tools/dotc/core/Mode.scala +++ b/compiler/src/dotty/tools/dotc/core/Mode.scala @@ -119,4 +119,7 @@ object Mode { /** Are we resolving a TypeTest node? */ val InTypeTest: Mode = newMode(27, "InTypeTest") + + /** Are we enforcing null safety */ + val SafeNulls = newMode(28, "SafeNulls") } diff --git a/compiler/src/dotty/tools/dotc/core/StdNames.scala b/compiler/src/dotty/tools/dotc/core/StdNames.scala index 98978fec6778..3570be66e8e3 100644 --- a/compiler/src/dotty/tools/dotc/core/StdNames.scala +++ b/compiler/src/dotty/tools/dotc/core/StdNames.scala @@ -613,6 +613,7 @@ object StdNames { val unapplySeq: N = "unapplySeq" val unbox: N = "unbox" val universe: N = "universe" + val unsafeNulls: N = "unsafeNulls" val update: N = "update" val updateDynamic: N = "updateDynamic" val using: N = "using" diff --git a/library/src/scala/runtime/stdLibPatches/language.scala b/library/src/scala/runtime/stdLibPatches/language.scala index 91f1c88b977a..2799a1969809 100644 --- a/library/src/scala/runtime/stdLibPatches/language.scala +++ b/library/src/scala/runtime/stdLibPatches/language.scala @@ -77,6 +77,8 @@ object language: */ object adhocExtensions + object unsafeNulls + /** Set source version to 3.0-migration. * * @see [[https://scalacenter.github.io/scala-3-migration-guide/docs/scala-3-migration-mode]] From 1f993b72d12b7895fb2719c9f408526872708a6f Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Sun, 31 Jan 2021 12:55:11 +0100 Subject: [PATCH 4/4] Refine initial SafeNulls setting --- compiler/src/dotty/tools/dotc/Run.scala | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/Run.scala b/compiler/src/dotty/tools/dotc/Run.scala index d37a6be2b3ec..4ee8475b41d0 100644 --- a/compiler/src/dotty/tools/dotc/Run.scala +++ b/compiler/src/dotty/tools/dotc/Run.scala @@ -24,6 +24,8 @@ import printing.XprintMode import parsing.Parsers.Parser import parsing.JavaParsers.JavaParser import typer.ImplicitRunInfo +import config.Feature +import StdNames.nme import java.io.{BufferedWriter, OutputStreamWriter} import java.nio.charset.StandardCharsets @@ -76,7 +78,7 @@ class Run(comp: Compiler, ictx: Context) extends ImplicitRunInfo with Constraint .setTyper(new Typer) .addMode(Mode.ImplicitsEnabled) .setTyperState(ctx.typerState.fresh(ctx.reporter)) - if ctx.settings.YexplicitNulls.value then + if ctx.settings.YexplicitNulls.value && !Feature.enabledBySetting(nme.unsafeNulls) then start = start.addMode(Mode.SafeNulls) ctx.initialize()(using start) // re-initialize the base context with start start.setRun(this)