diff --git a/compiler/src/dotty/tools/backend/jvm/DottyBackendInterface.scala b/compiler/src/dotty/tools/backend/jvm/DottyBackendInterface.scala index 411e2a90fd14..c363fe20de67 100644 --- a/compiler/src/dotty/tools/backend/jvm/DottyBackendInterface.scala +++ b/compiler/src/dotty/tools/backend/jvm/DottyBackendInterface.scala @@ -116,58 +116,62 @@ object DottyBackendInterface { ctx.requiredModule(className) } - extension symExtensions on (sym: Symbol) { - - def isInterface(using Context): Boolean = (sym.is(PureInterface)) || sym.is(Trait) - - def isStaticConstructor(using Context): Boolean = (sym.isStaticMember && sym.isClassConstructor) || (sym.name eq nme.STATIC_CONSTRUCTOR) - - def isStaticMember(using Context): Boolean = (sym ne NoSymbol) && - (sym.is(JavaStatic) || sym.isScalaStatic) - // guard against no sumbol cause this code is executed to select which call type(static\dynamic) to use to call array.clone - - /** - * True for module classes of modules that are top-level or owned only by objects. Module classes - * for such objects will get a MODULE$ flag and a corresponding static initializer. - */ - def isStaticModuleClass(using Context): Boolean = - (sym.is(Module)) && { - // scalac uses atPickling here - // this would not work if modules are created after pickling - // for example by specialization - val original = toDenot(sym).initial - val validity = original.validFor - atPhase(validity.phaseId) { - toDenot(sym).isStatic + given symExtensions as AnyRef: + extension (sym: Symbol): + + def isInterface(using Context): Boolean = (sym.is(PureInterface)) || sym.is(Trait) + + def isStaticConstructor(using Context): Boolean = (sym.isStaticMember && sym.isClassConstructor) || (sym.name eq nme.STATIC_CONSTRUCTOR) + + def isStaticMember(using Context): Boolean = (sym ne NoSymbol) && + (sym.is(JavaStatic) || sym.isScalaStatic) + // guard against no sumbol cause this code is executed to select which call type(static\dynamic) to use to call array.clone + + /** + * True for module classes of modules that are top-level or owned only by objects. Module classes + * for such objects will get a MODULE$ flag and a corresponding static initializer. + */ + def isStaticModuleClass(using Context): Boolean = + (sym.is(Module)) && { + // scalac uses atPickling here + // this would not work if modules are created after pickling + // for example by specialization + val original = toDenot(sym).initial + val validity = original.validFor + atPhase(validity.phaseId) { + toDenot(sym).isStatic + } } - } - def originalLexicallyEnclosingClass(using Context): Symbol = - // used to populate the EnclosingMethod attribute. - // it is very tricky in presence of classes(and annonymous classes) defined inside supper calls. - if (sym.exists) { - val validity = toDenot(sym).initial.validFor - atPhase(validity.phaseId) { - toDenot(sym).lexicallyEnclosingClass + def originalLexicallyEnclosingClass(using Context): Symbol = + // used to populate the EnclosingMethod attribute. + // it is very tricky in presence of classes(and annonymous classes) defined inside supper calls. + if (sym.exists) { + val validity = toDenot(sym).initial.validFor + atPhase(validity.phaseId) { + toDenot(sym).lexicallyEnclosingClass + } + } else NoSymbol + + /** + * True for module classes of package level objects. The backend will generate a mirror class for + * such objects. + */ + def isTopLevelModuleClass(using Context): Boolean = + sym.is(ModuleClass) && + atPhase(flattenPhase) { + toDenot(sym).owner.is(PackageClass) } - } else NoSymbol - - /** - * True for module classes of package level objects. The backend will generate a mirror class for - * such objects. - */ - def isTopLevelModuleClass(using Context): Boolean = - sym.is(ModuleClass) && - atPhase(flattenPhase) { - toDenot(sym).owner.is(PackageClass) - } - def javaSimpleName(using Context): String = toDenot(sym).name.mangledString - def javaClassName(using Context): String = toDenot(sym).fullName.mangledString - def javaBinaryName(using Context): String = javaClassName.replace('.', '/') - } + def javaSimpleName(using Context): String = toDenot(sym).name.mangledString + def javaClassName(using Context): String = toDenot(sym).fullName.mangledString + def javaBinaryName(using Context): String = javaClassName.replace('.', '/') + + end extension + + end symExtensions private val primitiveCompilationUnits = Set( "Unit.scala", diff --git a/compiler/src/dotty/tools/dotc/core/Comments.scala b/compiler/src/dotty/tools/dotc/core/Comments.scala index 7e5b770eff9a..90700ac2e46b 100644 --- a/compiler/src/dotty/tools/dotc/core/Comments.scala +++ b/compiler/src/dotty/tools/dotc/core/Comments.scala @@ -15,8 +15,8 @@ object Comments { val ContextDoc: Key[ContextDocstrings] = new Key[ContextDocstrings] /** Decorator for getting docbase out of context */ - extension CommentsContext on (c: Context): - def docCtx: Option[ContextDocstrings] = c.property(ContextDoc) + given CommentsContext as AnyRef: + extension (c: Context) def docCtx: Option[ContextDocstrings] = c.property(ContextDoc) /** Context for Docstrings, contains basic functionality for getting * docstrings via `Symbol` and expanding templates diff --git a/compiler/src/dotty/tools/dotc/core/Contexts.scala b/compiler/src/dotty/tools/dotc/core/Contexts.scala index 8ee1a67850da..551a2cf2037e 100644 --- a/compiler/src/dotty/tools/dotc/core/Contexts.scala +++ b/compiler/src/dotty/tools/dotc/core/Contexts.scala @@ -603,15 +603,17 @@ object Contexts { def setDebug: this.type = setSetting(base.settings.Ydebug, true) } - extension ops on (c: Context): - def addNotNullInfo(info: NotNullInfo) = - c.withNotNullInfos(c.notNullInfos.extendWith(info)) + given ops as AnyRef: + extension (c: Context): + def addNotNullInfo(info: NotNullInfo) = + c.withNotNullInfos(c.notNullInfos.extendWith(info)) - def addNotNullRefs(refs: Set[TermRef]) = - c.addNotNullInfo(NotNullInfo(refs, Set())) + def addNotNullRefs(refs: Set[TermRef]) = + c.addNotNullInfo(NotNullInfo(refs, Set())) - def withNotNullInfos(infos: List[NotNullInfo]): Context = - if c.notNullInfos eq infos then c else c.fresh.setNotNullInfos(infos) + def withNotNullInfos(infos: List[NotNullInfo]): Context = + if c.notNullInfos eq infos then c else c.fresh.setNotNullInfos(infos) + end ops // TODO: Fix issue when converting ModeChanges and FreshModeChanges to extension givens implicit class ModeChanges(val c: Context) extends AnyVal { diff --git a/compiler/src/dotty/tools/dotc/core/Decorators.scala b/compiler/src/dotty/tools/dotc/core/Decorators.scala index e1ad1130c1db..03a216a28949 100644 --- a/compiler/src/dotty/tools/dotc/core/Decorators.scala +++ b/compiler/src/dotty/tools/dotc/core/Decorators.scala @@ -161,13 +161,15 @@ object Decorators { def & (ys: List[T]): List[T] = xs filter (ys contains _) } - extension ListOfListDecorator on [T, U](xss: List[List[T]]): - def nestedMap(f: T => U): List[List[U]] = - xss.map(_.map(f)) - def nestedMapConserve(f: T => U): List[List[U]] = - xss.mapconserve(_.mapconserve(f)) - def nestedZipWithConserve(yss: List[List[U]])(f: (T, U) => T): List[List[T]] = - xss.zipWithConserve(yss)((xs, ys) => xs.zipWithConserve(ys)(f)) + given ListOfListDecorator as AnyRef: + extension [T, U](xss: List[List[T]]): + def nestedMap(f: T => U): List[List[U]] = + xss.map(_.map(f)) + def nestedMapConserve(f: T => U): List[List[U]] = + xss.mapconserve(_.mapconserve(f)) + def nestedZipWithConserve(yss: List[List[U]])(f: (T, U) => T): List[List[T]] = + xss.zipWithConserve(yss)((xs, ys) => xs.zipWithConserve(ys)(f)) + end extension implicit class TextToString(val text: Text) extends AnyVal { def show(using Context): String = text.mkString(ctx.settings.pageWidth.value, ctx.settings.printLines.value) diff --git a/compiler/src/dotty/tools/dotc/parsing/Scanners.scala b/compiler/src/dotty/tools/dotc/parsing/Scanners.scala index ca564bf50409..22a51d42deba 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Scanners.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Scanners.scala @@ -30,7 +30,7 @@ object Cbufs { object Cbuf: def apply(): Cbuf = new StringBuilder(TargetCapacity) - extension StringBuilderOps on (buf: Cbuf): + extension (buf: Cbuf): def clear(): Unit = { if buf.capacity() > TargetCapacity then buf.setLength(TargetCapacity) @@ -48,6 +48,7 @@ object Cbufs { def isEmpty: Boolean = buf.length() == 0 def length: Int = buf.length() def last: Char = buf.charAt(buf.length() - 1) + end extension } import Cbufs._ diff --git a/compiler/src/dotty/tools/dotc/semanticdb/Scala3.scala b/compiler/src/dotty/tools/dotc/semanticdb/Scala3.scala index 5ffc6cc244a5..f38113bdaaf3 100644 --- a/compiler/src/dotty/tools/dotc/semanticdb/Scala3.scala +++ b/compiler/src/dotty/tools/dotc/semanticdb/Scala3.scala @@ -67,63 +67,64 @@ object Scala3: end Symbols - extension NameOps on (name: Name): - def isWildcard = name match - case nme.WILDCARD | WILDCARDTypeName => true - case _ => name.is(NameKinds.WildcardParamName) + given NameOps as AnyRef: + extension (name: Name): + def isWildcard = name match + case nme.WILDCARD | WILDCARDTypeName => true + case _ => name.is(NameKinds.WildcardParamName) - def isScala2PackageObjectName: Boolean = name match - case name: Names.TermName => name == nme.PACKAGE - case name: Names.TypeName => - name.toTermName match - case NameKinds.ModuleClassName(original) => original.isScala2PackageObjectName - case _ => false + def isScala2PackageObjectName: Boolean = name match + case name: Names.TermName => name == nme.PACKAGE + case name: Names.TypeName => + name.toTermName match + case NameKinds.ModuleClassName(original) => original.isScala2PackageObjectName + case _ => false - def isEmptyNumbered: Boolean = - !name.is(NameKinds.WildcardParamName) - && { name match - case NameKinds.AnyNumberedName(nme.EMPTY, _) => true - case _ => false - } + def isEmptyNumbered: Boolean = + !name.is(NameKinds.WildcardParamName) + && { name match + case NameKinds.AnyNumberedName(nme.EMPTY, _) => true + case _ => false + } + end NameOps - // end NameOps + given SymbolOps as AnyRef: + extension (sym: Symbol): - extension SymbolOps on (sym: Symbol): + def ifExists(using Context): Option[Symbol] = if sym.exists then Some(sym) else None - def ifExists(using Context): Option[Symbol] = if sym.exists then Some(sym) else None + def isScala2PackageObject(using Context): Boolean = + sym.name.isScala2PackageObjectName && sym.owner.is(Package) && sym.is(Module) - def isScala2PackageObject(using Context): Boolean = - sym.name.isScala2PackageObjectName && sym.owner.is(Package) && sym.is(Module) + def isAnonymous(using Context): Boolean = + sym.isAnonymousClass + || sym.isAnonymousModuleVal + || sym.isAnonymousFunction - def isAnonymous(using Context): Boolean = - sym.isAnonymousClass - || sym.isAnonymousModuleVal - || sym.isAnonymousFunction + def matchingSetter(using Context): Symbol = - def matchingSetter(using Context): Symbol = + val setterName = sym.name.toTermName.setterName - val setterName = sym.name.toTermName.setterName + extension (t: Type) inline def matchingType = t.paramInfoss match + case (arg::Nil)::Nil => t.resultType == defn.UnitType && arg == sym.info + case _ => false - extension (t: Type) inline def matchingType = t.paramInfoss match - case (arg::Nil)::Nil => t.resultType == defn.UnitType && arg == sym.info - case _ => false + sym.owner.info.decls.find(s => s.name == setterName && s.info.matchingType) - sym.owner.info.decls.find(s => s.name == setterName && s.info.matchingType) + /** Is symbol global? Non-global symbols get localN names */ + def isGlobal(using Context): Boolean = + sym.is(Package) + || !sym.isSelfSym && (sym.is(Param) || sym.owner.isClass) && sym.owner.isGlobal - /** Is symbol global? Non-global symbols get localN names */ - def isGlobal(using Context): Boolean = - sym.is(Package) - || !sym.isSelfSym && (sym.is(Param) || sym.owner.isClass) && sym.owner.isGlobal + def isLocalWithinSameName(using Context): Boolean = + sym.exists && !sym.isGlobal && sym.name == sym.owner.name - def isLocalWithinSameName(using Context): Boolean = - sym.exists && !sym.isGlobal && sym.name == sym.owner.name + /** Synthetic symbols that are not anonymous or numbered empty ident */ + def isSyntheticWithIdent(using Context): Boolean = + sym.is(Synthetic) && !sym.isAnonymous && !sym.name.isEmptyNumbered - /** Synthetic symbols that are not anonymous or numbered empty ident */ - def isSyntheticWithIdent(using Context): Boolean = - sym.is(Synthetic) && !sym.isAnonymous && !sym.name.isEmptyNumbered - - // end SymbolOps + end SymbolOps object LocalSymbol: @@ -144,70 +145,69 @@ object Scala3: case '/' | '.' | '#' | ']' | ')' => true case _ => false - extension StringOps on (symbol: String): - - def isSymbol: Boolean = !symbol.isEmpty - def isRootPackage: Boolean = RootPackage == symbol - def isEmptyPackage: Boolean = EmptyPackage == symbol - - def isGlobal: Boolean = !symbol.isEmpty && !symbol.isMulti && symbol.last.isGlobalTerminal - def isLocal: Boolean = !symbol.isEmpty && !symbol.isMulti && !symbol.last.isGlobalTerminal - def isMulti: Boolean = symbol startsWith ";" - - def isConstructor: Boolean = ctor matches symbol - def isPackage: Boolean = !symbol.isEmpty && !symbol.isMulti && symbol.last == '/' - def isTerm: Boolean = !symbol.isEmpty && !symbol.isMulti && symbol.last == '.' - def isType: Boolean = !symbol.isEmpty && !symbol.isMulti && symbol.last == '#' - def isTypeParameter: Boolean = !symbol.isEmpty && !symbol.isMulti && symbol.last == ']' - def isParameter: Boolean = !symbol.isEmpty && !symbol.isMulti && symbol.last == ')' - - def unescapeUnicode = - unicodeEscape.replaceAllIn(symbol, m => String.valueOf(Integer.parseInt(m.group(1), 16).toChar)) - - def isJavaIdent = - isJavaIdentifierStart(symbol.head) && symbol.tail.forall(isJavaIdentifierPart) - - // end StringOps - - extension InfoOps on (info: SymbolInformation): - - def isAbstract: Boolean = (info.properties & SymbolInformation.Property.ABSTRACT.value) != 0 - def isFinal: Boolean = (info.properties & SymbolInformation.Property.FINAL.value) != 0 - def isSealed: Boolean = (info.properties & SymbolInformation.Property.SEALED.value) != 0 - def isImplicit: Boolean = (info.properties & SymbolInformation.Property.IMPLICIT.value) != 0 - def isLazy: Boolean = (info.properties & SymbolInformation.Property.LAZY.value) != 0 - def isCase: Boolean = (info.properties & SymbolInformation.Property.CASE.value) != 0 - def isCovariant: Boolean = (info.properties & SymbolInformation.Property.COVARIANT.value) != 0 - def isContravariant: Boolean = (info.properties & SymbolInformation.Property.CONTRAVARIANT.value) != 0 - def isPrimary: Boolean = (info.properties & SymbolInformation.Property.PRIMARY.value) != 0 - def isVal: Boolean = (info.properties & SymbolInformation.Property.VAL.value) != 0 - def isVar: Boolean = (info.properties & SymbolInformation.Property.VAR.value) != 0 - def isStatic: Boolean = (info.properties & SymbolInformation.Property.STATIC.value) != 0 - def isEnum: Boolean = (info.properties & SymbolInformation.Property.ENUM.value) != 0 - def isDefault: Boolean = (info.properties & SymbolInformation.Property.DEFAULT.value) != 0 - - def isUnknownKind: Boolean = info.kind.isUnknownKind - def isLocal: Boolean = info.kind.isLocal - def isField: Boolean = info.kind.isField - def isMethod: Boolean = info.kind.isMethod - def isConstructor: Boolean = info.kind.isConstructor - def isMacro: Boolean = info.kind.isMacro - def isType: Boolean = info.kind.isType - def isParameter: Boolean = info.kind.isParameter - def isSelfParameter: Boolean = info.kind.isSelfParameter - def isTypeParameter: Boolean = info.kind.isTypeParameter - def isObject: Boolean = info.kind.isObject - def isPackage: Boolean = info.kind.isPackage - def isPackageObject: Boolean = info.kind.isPackageObject - def isClass: Boolean = info.kind.isClass - def isTrait: Boolean = info.kind.isTrait - def isInterface: Boolean = info.kind.isInterface - - // end InfoOps - - extension RangeOps on (range: Range): - def hasLength = range.endLine > range.startLine || range.endCharacter > range.startCharacter - // end RangeOps + given StringOps as AnyRef: + extension (symbol: String): + def isSymbol: Boolean = !symbol.isEmpty + def isRootPackage: Boolean = RootPackage == symbol + def isEmptyPackage: Boolean = EmptyPackage == symbol + + def isGlobal: Boolean = !symbol.isEmpty && !symbol.isMulti && symbol.last.isGlobalTerminal + def isLocal: Boolean = !symbol.isEmpty && !symbol.isMulti && !symbol.last.isGlobalTerminal + def isMulti: Boolean = symbol startsWith ";" + + def isConstructor: Boolean = ctor matches symbol + def isPackage: Boolean = !symbol.isEmpty && !symbol.isMulti && symbol.last == '/' + def isTerm: Boolean = !symbol.isEmpty && !symbol.isMulti && symbol.last == '.' + def isType: Boolean = !symbol.isEmpty && !symbol.isMulti && symbol.last == '#' + def isTypeParameter: Boolean = !symbol.isEmpty && !symbol.isMulti && symbol.last == ']' + def isParameter: Boolean = !symbol.isEmpty && !symbol.isMulti && symbol.last == ')' + + def unescapeUnicode = + unicodeEscape.replaceAllIn(symbol, m => String.valueOf(Integer.parseInt(m.group(1), 16).toChar)) + + def isJavaIdent = + isJavaIdentifierStart(symbol.head) && symbol.tail.forall(isJavaIdentifierPart) + end StringOps + + given InfoOps as AnyRef: + extension (info: SymbolInformation): + def isAbstract: Boolean = (info.properties & SymbolInformation.Property.ABSTRACT.value) != 0 + def isFinal: Boolean = (info.properties & SymbolInformation.Property.FINAL.value) != 0 + def isSealed: Boolean = (info.properties & SymbolInformation.Property.SEALED.value) != 0 + def isImplicit: Boolean = (info.properties & SymbolInformation.Property.IMPLICIT.value) != 0 + def isLazy: Boolean = (info.properties & SymbolInformation.Property.LAZY.value) != 0 + def isCase: Boolean = (info.properties & SymbolInformation.Property.CASE.value) != 0 + def isCovariant: Boolean = (info.properties & SymbolInformation.Property.COVARIANT.value) != 0 + def isContravariant: Boolean = (info.properties & SymbolInformation.Property.CONTRAVARIANT.value) != 0 + def isPrimary: Boolean = (info.properties & SymbolInformation.Property.PRIMARY.value) != 0 + def isVal: Boolean = (info.properties & SymbolInformation.Property.VAL.value) != 0 + def isVar: Boolean = (info.properties & SymbolInformation.Property.VAR.value) != 0 + def isStatic: Boolean = (info.properties & SymbolInformation.Property.STATIC.value) != 0 + def isEnum: Boolean = (info.properties & SymbolInformation.Property.ENUM.value) != 0 + def isDefault: Boolean = (info.properties & SymbolInformation.Property.DEFAULT.value) != 0 + + def isUnknownKind: Boolean = info.kind.isUnknownKind + def isLocal: Boolean = info.kind.isLocal + def isField: Boolean = info.kind.isField + def isMethod: Boolean = info.kind.isMethod + def isConstructor: Boolean = info.kind.isConstructor + def isMacro: Boolean = info.kind.isMacro + def isType: Boolean = info.kind.isType + def isParameter: Boolean = info.kind.isParameter + def isSelfParameter: Boolean = info.kind.isSelfParameter + def isTypeParameter: Boolean = info.kind.isTypeParameter + def isObject: Boolean = info.kind.isObject + def isPackage: Boolean = info.kind.isPackage + def isPackageObject: Boolean = info.kind.isPackageObject + def isClass: Boolean = info.kind.isClass + def isTrait: Boolean = info.kind.isTrait + def isInterface: Boolean = info.kind.isInterface + end InfoOps + + given RangeOps as AnyRef: + extension (range: Range): + def hasLength = range.endLine > range.startLine || range.endCharacter > range.startCharacter + end RangeOps /** Sort symbol occurrences by their start position. */ given OccurrenceOrdering as Ordering[SymbolOccurrence] = (x, y) => diff --git a/compiler/src/dotty/tools/dotc/typer/Nullables.scala b/compiler/src/dotty/tools/dotc/typer/Nullables.scala index 3be662346197..4775b765c019 100644 --- a/compiler/src/dotty/tools/dotc/typer/Nullables.scala +++ b/compiler/src/dotty/tools/dotc/typer/Nullables.scala @@ -161,199 +161,204 @@ object Nullables: // TODO: Add constant pattern if the constant type is not nullable case _ => false - extension notNullInfoOps on (infos: List[NotNullInfo]): + given notNullInfoOps as AnyRef: + extension (infos: List[NotNullInfo]): + + /** Do the current not-null infos imply that `ref` is not null? + * Not-null infos are as a history where earlier assertions and retractions replace + * later ones (i.e. it records the assignment history in reverse, with most recent first) + */ + @tailrec def impliesNotNull(ref: TermRef): Boolean = infos match + case info :: infos1 => + if info.asserted.contains(ref) then true + else if info.retracted.contains(ref) then false + else infos1.impliesNotNull(ref) + case _ => + false + + /** Add `info` as the most recent entry to the list of null infos. Assertions + * or retractions in `info` supersede infos in existing entries of `infos`. + */ + def extendWith(info: NotNullInfo) = + if info.isEmpty + || info.asserted.forall(infos.impliesNotNull(_)) + && !info.retracted.exists(infos.impliesNotNull(_)) + then infos + else info :: infos + + /** Retract all references to mutable variables */ + def retractMutables(using Context) = + val mutables = infos.foldLeft(Set[TermRef]())((ms, info) => + ms.union(info.asserted.filter(_.symbol.is(Mutable)))) + infos.extendWith(NotNullInfo(Set(), mutables)) - /** Do the current not-null infos imply that `ref` is not null? - * Not-null infos are as a history where earlier assertions and retractions replace - * later ones (i.e. it records the assignment history in reverse, with most recent first) - */ - @tailrec def impliesNotNull(ref: TermRef): Boolean = infos match - case info :: infos1 => - if info.asserted.contains(ref) then true - else if info.retracted.contains(ref) then false - else infos1.impliesNotNull(ref) - case _ => - false - - /** Add `info` as the most recent entry to the list of null infos. Assertions - * or retractions in `info` supersede infos in existing entries of `infos`. - */ - def extendWith(info: NotNullInfo) = - if info.isEmpty - || info.asserted.forall(infos.impliesNotNull(_)) - && !info.retracted.exists(infos.impliesNotNull(_)) - then infos - else info :: infos - - /** Retract all references to mutable variables */ - def retractMutables(using Context) = - val mutables = infos.foldLeft(Set[TermRef]())((ms, info) => - ms.union(info.asserted.filter(_.symbol.is(Mutable)))) - infos.extendWith(NotNullInfo(Set(), mutables)) end notNullInfoOps - extension refOps on (ref: TermRef): - - /** Is the use of a mutable variable out of order - * - * Whether to generate and use flow typing on a specific _use_ of a local mutable variable? - * We only want to do flow typing on a use that belongs to the same method as the definition - * of the local variable. - * For example, in the following code, even `x` is not assigned to by a closure, but we can only - * use flow typing in one of the occurrences (because the other occurrence happens within a nested - * closure). - * ```scala - * var x: String|Null = ??? - * def y = { - * if (x != null) { - * // not safe to use the fact (x != null) here - * // since y can be executed at the same time as the outer block - * val _: String = x - * } - * } - * if (x != null) { - * val a: String = x // ok to use the fact here - * x = null - * } - * ``` - * - * Another example: - * ```scala - * var x: String|Null = ??? - * if (x != null) { - * def f: String = { - * val y: String = x // error: the use of x is out of order - * y - * } - * x = null - * val y: String = f // danger - * } - * ``` - */ - def usedOutOfOrder(using Context): Boolean = - val refSym = ref.symbol - val refOwner = refSym.owner - - @tailrec def recur(s: Symbol): Boolean = - s != NoSymbol - && s != refOwner - && (s.isOneOf(Lazy | Method) // not at the rhs of lazy ValDef or in a method (or lambda) - || s.isClass // not in a class - // TODO: need to check by-name parameter - || recur(s.owner)) - - refSym.is(Mutable) // if it is immutable, we don't need to check the rest conditions - && refOwner.isTerm - && recur(ctx.owner) + given refOps as AnyRef: + extension (ref: TermRef): + + /** Is the use of a mutable variable out of order + * + * Whether to generate and use flow typing on a specific _use_ of a local mutable variable? + * We only want to do flow typing on a use that belongs to the same method as the definition + * of the local variable. + * For example, in the following code, even `x` is not assigned to by a closure, but we can only + * use flow typing in one of the occurrences (because the other occurrence happens within a nested + * closure). + * ```scala + * var x: String|Null = ??? + * def y = { + * if (x != null) { + * // not safe to use the fact (x != null) here + * // since y can be executed at the same time as the outer block + * val _: String = x + * } + * } + * if (x != null) { + * val a: String = x // ok to use the fact here + * x = null + * } + * ``` + * + * Another example: + * ```scala + * var x: String|Null = ??? + * if (x != null) { + * def f: String = { + * val y: String = x // error: the use of x is out of order + * y + * } + * x = null + * val y: String = f // danger + * } + * ``` + */ + def usedOutOfOrder(using Context): Boolean = + val refSym = ref.symbol + val refOwner = refSym.owner + + @tailrec def recur(s: Symbol): Boolean = + s != NoSymbol + && s != refOwner + && (s.isOneOf(Lazy | Method) // not at the rhs of lazy ValDef or in a method (or lambda) + || s.isClass // not in a class + // TODO: need to check by-name parameter + || recur(s.owner)) + + refSym.is(Mutable) // if it is immutable, we don't need to check the rest conditions + && refOwner.isTerm + && recur(ctx.owner) end refOps - extension treeOps on (tree: Tree): - - /* The `tree` with added nullability attachment */ - def withNotNullInfo(info: NotNullInfo): tree.type = - if !info.isEmpty then tree.putAttachment(NNInfo, info) - tree - - /* The nullability info of `tree` */ - def notNullInfo(using Context): NotNullInfo = - stripInlined(tree).getAttachment(NNInfo) match - case Some(info) if !ctx.erasedTypes => info - case _ => NotNullInfo.empty - - /* The nullability info of `tree`, assuming it is a condition that evaluates to `c` */ - def notNullInfoIf(c: Boolean)(using Context): NotNullInfo = - val cond = tree.notNullConditional - if cond.isEmpty then tree.notNullInfo - else tree.notNullInfo.seq(NotNullInfo(if c then cond.ifTrue else cond.ifFalse, Set())) - - /** The paths that are known to be not null if the condition represented - * by `tree` yields `true` or `false`. Two empty sets if `tree` is not - * a condition. - */ - def notNullConditional(using Context): NotNullConditional = - stripBlock(tree).getAttachment(NNConditional) match - case Some(cond) if !ctx.erasedTypes => cond - case _ => NotNullConditional.empty - - /** The current context augmented with nullability information of `tree` */ - def nullableContext(using Context): Context = - val info = tree.notNullInfo - if info.isEmpty then ctx else ctx.addNotNullInfo(info) - - /** The current context augmented with nullability information, - * assuming the result of the condition represented by `tree` is the same as - * the value of `c`. - */ - def nullableContextIf(c: Boolean)(using Context): Context = - val info = tree.notNullInfoIf(c) - if info.isEmpty then ctx else ctx.addNotNullInfo(info) - - /** The context to use for the arguments of the function represented by `tree`. - * This is the current context, augmented with nullability information - * of the left argument, if the application is a boolean `&&` or `||`. - */ - def nullableInArgContext(using Context): Context = tree match - case Select(x, _) if !ctx.erasedTypes => - if tree.symbol == defn.Boolean_&& then x.nullableContextIf(true) - else if tree.symbol == defn.Boolean_|| then x.nullableContextIf(false) - else ctx - case _ => ctx - - /** The `tree` augmented with nullability information in an attachment. - * The following operations lead to nullability info being recorded: - * - * 1. Null tests using `==`, `!=`, `eq`, `ne`, if the compared entity is - * a path (i.e. a stable TermRef) - * 2. Boolean &&, ||, ! - */ - def computeNullable()(using Context): tree.type = - def setConditional(ifTrue: Set[TermRef], ifFalse: Set[TermRef]) = - tree.putAttachment(NNConditional, NotNullConditional(ifTrue, ifFalse)) - if !ctx.erasedTypes && analyzedOps.contains(tree.symbol.name.toTermName) then - tree match - case CompareNull(TrackedRef(ref), testEqual) => - if testEqual then setConditional(Set(), Set(ref)) - else setConditional(Set(ref), Set()) - case Apply(Select(x, _), y :: Nil) => - val xc = x.notNullConditional - val yc = y.notNullConditional - if !(xc.isEmpty && yc.isEmpty) then - if tree.symbol == defn.Boolean_&& then - setConditional(xc.ifTrue | yc.ifTrue, xc.ifFalse & yc.ifFalse) - else if tree.symbol == defn.Boolean_|| then - setConditional(xc.ifTrue & yc.ifTrue, xc.ifFalse | yc.ifFalse) - case Select(x, _) if tree.symbol == defn.Boolean_! => - val xc = x.notNullConditional - if !xc.isEmpty then - setConditional(xc.ifFalse, xc.ifTrue) - case _ => - tree - - /** Compute nullability information for this tree and all its subtrees */ - def computeNullableDeeply()(using Context): Unit = - new TreeTraverser { - def traverse(tree: Tree)(using Context) = - traverseChildren(tree) - tree.computeNullable() - }.traverse(tree) + given treeOps as AnyRef: + extension (tree: Tree): + + /* The `tree` with added nullability attachment */ + def withNotNullInfo(info: NotNullInfo): tree.type = + if !info.isEmpty then tree.putAttachment(NNInfo, info) + tree + + /* The nullability info of `tree` */ + def notNullInfo(using Context): NotNullInfo = + stripInlined(tree).getAttachment(NNInfo) match + case Some(info) if !ctx.erasedTypes => info + case _ => NotNullInfo.empty + + /* The nullability info of `tree`, assuming it is a condition that evaluates to `c` */ + def notNullInfoIf(c: Boolean)(using Context): NotNullInfo = + val cond = tree.notNullConditional + if cond.isEmpty then tree.notNullInfo + else tree.notNullInfo.seq(NotNullInfo(if c then cond.ifTrue else cond.ifFalse, Set())) + + /** The paths that are known to be not null if the condition represented + * by `tree` yields `true` or `false`. Two empty sets if `tree` is not + * a condition. + */ + def notNullConditional(using Context): NotNullConditional = + stripBlock(tree).getAttachment(NNConditional) match + case Some(cond) if !ctx.erasedTypes => cond + case _ => NotNullConditional.empty + + /** The current context augmented with nullability information of `tree` */ + def nullableContext(using Context): Context = + val info = tree.notNullInfo + if info.isEmpty then ctx else ctx.addNotNullInfo(info) + + /** The current context augmented with nullability information, + * assuming the result of the condition represented by `tree` is the same as + * the value of `c`. + */ + def nullableContextIf(c: Boolean)(using Context): Context = + val info = tree.notNullInfoIf(c) + if info.isEmpty then ctx else ctx.addNotNullInfo(info) + + /** The context to use for the arguments of the function represented by `tree`. + * This is the current context, augmented with nullability information + * of the left argument, if the application is a boolean `&&` or `||`. + */ + def nullableInArgContext(using Context): Context = tree match + case Select(x, _) if !ctx.erasedTypes => + if tree.symbol == defn.Boolean_&& then x.nullableContextIf(true) + else if tree.symbol == defn.Boolean_|| then x.nullableContextIf(false) + else ctx + case _ => ctx + + /** The `tree` augmented with nullability information in an attachment. + * The following operations lead to nullability info being recorded: + * + * 1. Null tests using `==`, `!=`, `eq`, `ne`, if the compared entity is + * a path (i.e. a stable TermRef) + * 2. Boolean &&, ||, ! + */ + def computeNullable()(using Context): tree.type = + def setConditional(ifTrue: Set[TermRef], ifFalse: Set[TermRef]) = + tree.putAttachment(NNConditional, NotNullConditional(ifTrue, ifFalse)) + if !ctx.erasedTypes && analyzedOps.contains(tree.symbol.name.toTermName) then + tree match + case CompareNull(TrackedRef(ref), testEqual) => + if testEqual then setConditional(Set(), Set(ref)) + else setConditional(Set(ref), Set()) + case Apply(Select(x, _), y :: Nil) => + val xc = x.notNullConditional + val yc = y.notNullConditional + if !(xc.isEmpty && yc.isEmpty) then + if tree.symbol == defn.Boolean_&& then + setConditional(xc.ifTrue | yc.ifTrue, xc.ifFalse & yc.ifFalse) + else if tree.symbol == defn.Boolean_|| then + setConditional(xc.ifTrue & yc.ifTrue, xc.ifFalse | yc.ifFalse) + case Select(x, _) if tree.symbol == defn.Boolean_! => + val xc = x.notNullConditional + if !xc.isEmpty then + setConditional(xc.ifFalse, xc.ifTrue) + case _ => + tree + + /** Compute nullability information for this tree and all its subtrees */ + def computeNullableDeeply()(using Context): Unit = + new TreeTraverser { + def traverse(tree: Tree)(using Context) = + traverseChildren(tree) + tree.computeNullable() + }.traverse(tree) end treeOps - extension assignOps on (tree: Assign): - def computeAssignNullable()(using Context): tree.type = tree.lhs match - case TrackedRef(ref) => - val rhstp = tree.rhs.typeOpt - if ctx.explicitNulls && ref.isNullableUnion then - if rhstp.isNullType || rhstp.isNullableUnion then - // If the type of rhs is nullable (`T|Null` or `Null`), then the nullability of the - // lhs variable is no longer trackable. We don't need to check whether the type `T` - // is correct here, as typer will check it. - tree.withNotNullInfo(NotNullInfo(Set(), Set(ref))) - else - // If the initial type is nullable and the assigned value is non-null, - // we add it to the NotNull. - tree.withNotNullInfo(NotNullInfo(Set(ref), Set())) - else tree - case _ => tree + given assignOps as AnyRef: + extension (tree: Assign): + def computeAssignNullable()(using Context): tree.type = tree.lhs match + case TrackedRef(ref) => + val rhstp = tree.rhs.typeOpt + if ctx.explicitNulls && ref.isNullableUnion then + if rhstp.isNullType || rhstp.isNullableUnion then + // If the type of rhs is nullable (`T|Null` or `Null`), then the nullability of the + // lhs variable is no longer trackable. We don't need to check whether the type `T` + // is correct here, as typer will check it. + tree.withNotNullInfo(NotNullInfo(Set(), Set(ref))) + else + // If the initial type is nullable and the assigned value is non-null, + // we add it to the NotNull. + tree.withNotNullInfo(NotNullInfo(Set(ref), Set())) + else tree + case _ => tree end assignOps private val analyzedOps = Set(nme.EQ, nme.NE, nme.eq, nme.ne, nme.ZAND, nme.ZOR, nme.UNARY_!) diff --git a/library/src/dotty/DottyPredef.scala b/library/src/dotty/DottyPredef.scala index c58408ec3434..f06730568ec3 100644 --- a/library/src/dotty/DottyPredef.scala +++ b/library/src/dotty/DottyPredef.scala @@ -82,7 +82,7 @@ object DottyPredef { * * Note that `.nn` performs a checked cast, so if invoked on a null value it'll throw an NPE. */ - def[T] (x: T|Null) nn: x.type & T = + extension [T](x: T | Null) def nn: x.type & T = if (x == null) throw new NullPointerException("tried to cast away nullability, but value is null") else x.asInstanceOf[x.type & T] }