From 688ed4f168d2bd40acf478f637d27f6e03c1a7d2 Mon Sep 17 00:00:00 2001 From: Matt Bovel Date: Wed, 27 Nov 2024 23:01:26 +0000 Subject: [PATCH 01/17] Do not lift annotation arguments (bis) --- .../dotty/tools/dotc/typer/Applications.scala | 2 ++ .../dependent-annot-default-args.check | 23 +++++++++++++++++++ .../dependent-annot-default-args.scala | 10 ++++++++ 3 files changed, 35 insertions(+) diff --git a/compiler/src/dotty/tools/dotc/typer/Applications.scala b/compiler/src/dotty/tools/dotc/typer/Applications.scala index 41e48f7595dc..ff6c71f57355 100644 --- a/compiler/src/dotty/tools/dotc/typer/Applications.scala +++ b/compiler/src/dotty/tools/dotc/typer/Applications.scala @@ -963,6 +963,8 @@ trait Applications extends Compatibility { case (arg: NamedArg, _) => arg case (arg, name) => NamedArg(name, arg) } + else if isAnnotConstr(methRef.symbol) then + typedArgs else if !sameSeq(args, orderedArgs) && !typedArgs.forall(isSafeArg) then // need to lift arguments to maintain evaluation order in the // presence of argument reorderings. diff --git a/tests/printing/dependent-annot-default-args.check b/tests/printing/dependent-annot-default-args.check index 44c1fe31e2d1..f457d5d62edb 100644 --- a/tests/printing/dependent-annot-default-args.check +++ b/tests/printing/dependent-annot-default-args.check @@ -8,16 +8,39 @@ package { final module class annot() extends AnyRef() { this: annot.type => def $lessinit$greater$default$2: Any @uncheckedVariance = 42 } + class annot2(x: Any, y: Array[Any]) extends annotation.Annotation() { + private[this] val x: Any + private[this] val y: Array[Any] + } + final lazy module val annot2: annot2 = new annot2() + final module class annot2() extends AnyRef() { this: annot2.type => + def $lessinit$greater$default$1: Any @uncheckedVariance = -1 + def $lessinit$greater$default$2: Array[Any] @uncheckedVariance = + Array.apply[Any](["Hello" : Any]*)(scala.reflect.ClassTag.Any) + } final lazy module val dependent-annot-default-args$package: dependent-annot-default-args$package = new dependent-annot-default-args$package() final module class dependent-annot-default-args$package() extends Object() { this: dependent-annot-default-args$package.type => def f(x: Int): Int @annot(x) = x + def f2(x: Int): + Int @annot2( + y = Array.apply[Any](["Hello",x : Any]*)(scala.reflect.ClassTag.Any)) + = x def test: Unit = { val y: Int = ??? val z: Int @annot(y) = f(y) + val z2: + Int @annot2( + y = Array.apply[Any](["Hello",y : Any]*)(scala.reflect.ClassTag.Any) + ) + = f2(y) + @annot(44) val z3: Int = 45 + @annot2( + y = Array.apply[Any](["Hello",y : Any]*)(scala.reflect.ClassTag.Any)) + val z4: Int = 45 () } } diff --git a/tests/printing/dependent-annot-default-args.scala b/tests/printing/dependent-annot-default-args.scala index 7ddce711fedc..11fc9ef52cc9 100644 --- a/tests/printing/dependent-annot-default-args.scala +++ b/tests/printing/dependent-annot-default-args.scala @@ -1,5 +1,15 @@ class annot(x: Any, y: Any = 42) extends annotation.Annotation +class annot2(x: Any = -1, y: Array[Any] = Array("Hello")) extends annotation.Annotation + def f(x: Int): Int @annot(x) = x +def f2(x: Int): Int @annot2(y = Array("Hello", x)) = x + def test = val y: Int = ??? + val z = f(y) + val z2 = f2(y) + + @annot(44) val z3 = 45 + @annot2(y = Array("Hello", y)) val z4 = 45 + From 483920640f016c7ccdb31144df7ca602d99297fa Mon Sep 17 00:00:00 2001 From: odersky Date: Thu, 5 Dec 2024 16:45:53 +0100 Subject: [PATCH 02/17] Widen singleton types when computing fields from .Fields Fixes #22018 --- .../src/dotty/tools/dotc/typer/Typer.scala | 2 +- tests/pos/i22018.scala | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 tests/pos/i22018.scala diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index 6bb5d1ee70ff..1a3f5c2d97c3 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -868,7 +868,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer && pt != LhsProto then val pre = if !TypeOps.isLegalPrefix(qual.tpe) then SkolemType(qual.tpe) else qual.tpe - val fieldsType = pre.select(tpnme.Fields).dealias.simplified + val fieldsType = pre.select(tpnme.Fields).widenDealias.simplified val fields = fieldsType.namedTupleElementTypes typr.println(i"try dyn select $qual, $selName, $fields") fields.find(_._1 == selName) match diff --git a/tests/pos/i22018.scala b/tests/pos/i22018.scala new file mode 100644 index 000000000000..14f4733732be --- /dev/null +++ b/tests/pos/i22018.scala @@ -0,0 +1,18 @@ +import scala.language.experimental.namedTuples + +class SelectableNT[A <: NamedTuple.AnyNamedTuple](val nt: A) extends Selectable: + type Fields = A + def selectDynamic(x: String) = ??? + +object Test: + + val a = (name = "foo", age = 1) + + val sa = SelectableNT(a) + sa.name // ok + + type B = a.type + val b: B = a + + val sb = SelectableNT(b) + sb.name // fails \ No newline at end of file From 585dda9587bc95afb936eb18751fbf850cf3209c Mon Sep 17 00:00:00 2001 From: noti0na1 Date: Sat, 21 Sep 2024 19:54:39 +0200 Subject: [PATCH 03/17] Refactor NotNullInfo to record every reference which is retracted once. --- .../dotty/tools/dotc/typer/Nullables.scala | 32 +++++++--- .../src/dotty/tools/dotc/typer/Typer.scala | 15 ++++- tests/explicit-nulls/neg/i21380c.scala | 6 +- tests/explicit-nulls/neg/i21619.scala | 62 +++++++++++++++++++ 4 files changed, 100 insertions(+), 15 deletions(-) create mode 100644 tests/explicit-nulls/neg/i21619.scala diff --git a/compiler/src/dotty/tools/dotc/typer/Nullables.scala b/compiler/src/dotty/tools/dotc/typer/Nullables.scala index 3f071dad2d03..74623ed7b4e9 100644 --- a/compiler/src/dotty/tools/dotc/typer/Nullables.scala +++ b/compiler/src/dotty/tools/dotc/typer/Nullables.scala @@ -52,15 +52,19 @@ object Nullables: val hiTree = if(hiTpe eq hi.typeOpt) hi else TypeTree(hiTpe) TypeBoundsTree(lo, hiTree, alias) - /** A set of val or var references that are known to be not null, plus a set of - * variable references that are not known (anymore) to be not null + /** A set of val or var references that are known to be not null, + * a set of variable references that are not known (anymore) to be not null, + * plus a set of variables that are known to be not null at any point. */ - case class NotNullInfo(asserted: Set[TermRef], retracted: Set[TermRef]): + case class NotNullInfo(asserted: Set[TermRef], retracted: Set[TermRef], onceRetracted: Set[TermRef]): assert((asserted & retracted).isEmpty) + assert(retracted.subsetOf(onceRetracted)) def isEmpty = this eq NotNullInfo.empty - def retractedInfo = NotNullInfo(Set(), retracted) + def retractedInfo = NotNullInfo(Set(), retracted, onceRetracted) + + def onceRetractedInfo = NotNullInfo(Set(), onceRetracted, onceRetracted) /** The sequential combination with another not-null info */ def seq(that: NotNullInfo): NotNullInfo = @@ -68,19 +72,29 @@ object Nullables: else if that.isEmpty then this else NotNullInfo( this.asserted.union(that.asserted).diff(that.retracted), - this.retracted.union(that.retracted).diff(that.asserted)) + this.retracted.union(that.retracted).diff(that.asserted), + this.onceRetracted.union(that.onceRetracted)) /** The alternative path combination with another not-null info. Used to merge * the nullability info of the two branches of an if. */ def alt(that: NotNullInfo): NotNullInfo = - NotNullInfo(this.asserted.intersect(that.asserted), this.retracted.union(that.retracted)) + NotNullInfo( + this.asserted.intersect(that.asserted), + this.retracted.union(that.retracted), + this.onceRetracted.union(that.onceRetracted)) + + def withOnceRetracted(that: NotNullInfo): NotNullInfo = + if that.isEmpty then this + else NotNullInfo(this.asserted, this.retracted, this.onceRetracted.union(that.onceRetracted)) object NotNullInfo: - val empty = new NotNullInfo(Set(), Set()) + val empty = new NotNullInfo(Set(), Set(), Set()) def apply(asserted: Set[TermRef], retracted: Set[TermRef]): NotNullInfo = - if asserted.isEmpty && retracted.isEmpty then empty - else new NotNullInfo(asserted, retracted) + apply(asserted, retracted, retracted) + def apply(asserted: Set[TermRef], retracted: Set[TermRef], onceRetracted: Set[TermRef]): NotNullInfo = + if asserted.isEmpty && onceRetracted.isEmpty then empty + else new NotNullInfo(asserted, retracted, onceRetracted) end NotNullInfo /** A pair of not-null sets, depending on whether a condition is `true` or `false` */ diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index 6bb5d1ee70ff..588e4188c7bc 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -1552,8 +1552,10 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer def thenPathInfo = cond1.notNullInfoIf(true).seq(result.thenp.notNullInfo) def elsePathInfo = cond1.notNullInfoIf(false).seq(result.elsep.notNullInfo) result.withNotNullInfo( - if result.thenp.tpe.isRef(defn.NothingClass) then elsePathInfo - else if result.elsep.tpe.isRef(defn.NothingClass) then thenPathInfo + if result.thenp.tpe.isRef(defn.NothingClass) then + elsePathInfo.withOnceRetracted(thenPathInfo) + else if result.elsep.tpe.isRef(defn.NothingClass) then + thenPathInfo.withOnceRetracted(elsePathInfo) else thenPathInfo.alt(elsePathInfo) ) end typedIf @@ -2350,10 +2352,17 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer }: @unchecked val cases2 = cases2x.asInstanceOf[List[CaseDef]] - var nni = expr2.notNullInfo.retractedInfo + // Since we don't know at which point the the exception is thrown in the body, + // we have to collect any reference that is once retracted. + var nni = expr2.notNullInfo.onceRetractedInfo + // It is possible to have non-exhaustive cases, and some exceptions are thrown and not caught. + // Therefore, the code in the finallizer and after the try block can only rely on the retracted + // info from the cases' body. if cases2.nonEmpty then nni = nni.seq(cases2.map(_.notNullInfo.retractedInfo).reduce(_.alt(_))) + val finalizer1 = typed(tree.finalizer, defn.UnitType)(using ctx.addNotNullInfo(nni)) nni = nni.seq(finalizer1.notNullInfo) + assignType(cpy.Try(tree)(expr2, cases2, finalizer1), expr2, cases2).withNotNullInfo(nni) } diff --git a/tests/explicit-nulls/neg/i21380c.scala b/tests/explicit-nulls/neg/i21380c.scala index f86a5638e4c8..de3cd5bafd6b 100644 --- a/tests/explicit-nulls/neg/i21380c.scala +++ b/tests/explicit-nulls/neg/i21380c.scala @@ -32,9 +32,9 @@ def test4: Int = case npe: NullPointerException => x = "" case _ => x = "" x.length // error - // Although the catch block here is exhaustive, - // it is possible that the exception is thrown and not caught. - // Therefore, the code after the try block can only rely on the retracted info. + // Although the catch block here is exhaustive, it is possible to have non-exhaustive cases, + // and some exceptions are thrown and not caught. Therefore, the code in the finallizer and + // after the try block can only rely on the retracted info from the cases' body. def test5: Int = var x: String | Null = null diff --git a/tests/explicit-nulls/neg/i21619.scala b/tests/explicit-nulls/neg/i21619.scala new file mode 100644 index 000000000000..1c93af707b73 --- /dev/null +++ b/tests/explicit-nulls/neg/i21619.scala @@ -0,0 +1,62 @@ +def test1: String = + var x: String | Null = null + x = "" + var i: Int = 1 + try + i match + case _ => + x = null + throw new Exception() + x = "" + catch + case e: Exception => + x.replace("", "") // error + +def test2: String = + var x: String | Null = null + x = "" + var i: Int = 1 + try + i match + case _ => + x = null + throw new Exception() + x = "" + catch + case e: Exception => + x = "e" + x.replace("", "") // error + +def test3: String = + var x: String | Null = null + x = "" + var i: Int = 1 + try + i match + case _ => + x = null + throw new Exception() + x = "" + catch + case e: Exception => + finally + x = "f" + x.replace("", "") // ok + +def test4: String = + var x: String | Null = null + x = "" + var i: Int = 1 + try + try + if i == 1 then + x = null + throw new Exception() + else + x = "" + catch + case _ => + x = "" + catch + case _ => + x.replace("", "") // error \ No newline at end of file From bcc9e68c778da3faa4dcb8b6c0258a48befff819 Mon Sep 17 00:00:00 2001 From: noti0na1 Date: Sun, 22 Sep 2024 17:03:39 +0200 Subject: [PATCH 04/17] Use a different rule for NotNullInfo --- .../dotty/tools/dotc/typer/Nullables.scala | 44 ++++++------------- .../src/dotty/tools/dotc/typer/Typer.scala | 34 +++++++------- tests/explicit-nulls/neg/i21619.scala | 19 +++++++- 3 files changed, 50 insertions(+), 47 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/typer/Nullables.scala b/compiler/src/dotty/tools/dotc/typer/Nullables.scala index 74623ed7b4e9..62d2ccfb7200 100644 --- a/compiler/src/dotty/tools/dotc/typer/Nullables.scala +++ b/compiler/src/dotty/tools/dotc/typer/Nullables.scala @@ -53,48 +53,35 @@ object Nullables: TypeBoundsTree(lo, hiTree, alias) /** A set of val or var references that are known to be not null, - * a set of variable references that are not known (anymore) to be not null, - * plus a set of variables that are known to be not null at any point. + * plus a set of variable references that are once assigned to null. */ - case class NotNullInfo(asserted: Set[TermRef], retracted: Set[TermRef], onceRetracted: Set[TermRef]): - assert((asserted & retracted).isEmpty) - assert(retracted.subsetOf(onceRetracted)) - + case class NotNullInfo(asserted: Set[TermRef], retracted: Set[TermRef]): def isEmpty = this eq NotNullInfo.empty - def retractedInfo = NotNullInfo(Set(), retracted, onceRetracted) - - def onceRetractedInfo = NotNullInfo(Set(), onceRetracted, onceRetracted) + def retractedInfo = NotNullInfo(Set(), retracted) /** The sequential combination with another not-null info */ def seq(that: NotNullInfo): NotNullInfo = if this.isEmpty then that else if that.isEmpty then this else NotNullInfo( - this.asserted.union(that.asserted).diff(that.retracted), - this.retracted.union(that.retracted).diff(that.asserted), - this.onceRetracted.union(that.onceRetracted)) + this.asserted.diff(that.retracted).union(that.asserted), + this.retracted.union(that.retracted)) /** The alternative path combination with another not-null info. Used to merge * the nullability info of the two branches of an if. */ def alt(that: NotNullInfo): NotNullInfo = - NotNullInfo( - this.asserted.intersect(that.asserted), - this.retracted.union(that.retracted), - this.onceRetracted.union(that.onceRetracted)) + NotNullInfo(this.asserted.intersect(that.asserted), this.retracted.union(that.retracted)) - def withOnceRetracted(that: NotNullInfo): NotNullInfo = - if that.isEmpty then this - else NotNullInfo(this.asserted, this.retracted, this.onceRetracted.union(that.onceRetracted)) + def withRetracted(that: NotNullInfo): NotNullInfo = + NotNullInfo(this.asserted, this.retracted.union(that.retracted)) object NotNullInfo: - val empty = new NotNullInfo(Set(), Set(), Set()) + val empty = new NotNullInfo(Set(), Set()) def apply(asserted: Set[TermRef], retracted: Set[TermRef]): NotNullInfo = - apply(asserted, retracted, retracted) - def apply(asserted: Set[TermRef], retracted: Set[TermRef], onceRetracted: Set[TermRef]): NotNullInfo = - if asserted.isEmpty && onceRetracted.isEmpty then empty - else new NotNullInfo(asserted, retracted, onceRetracted) + if asserted.isEmpty && retracted.isEmpty then empty + else new NotNullInfo(asserted, retracted) end NotNullInfo /** A pair of not-null sets, depending on whether a condition is `true` or `false` */ @@ -247,16 +234,13 @@ object Nullables: * 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 + if info.isEmpty 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)))) + val mutables = infos.foldLeft(Set[TermRef]()): + (ms, info) => ms.union(info.asserted.filter(_.symbol.is(Mutable))) infos.extendWith(NotNullInfo(Set(), mutables)) end extension diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index 588e4188c7bc..8a72076527b9 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -1553,9 +1553,9 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer def elsePathInfo = cond1.notNullInfoIf(false).seq(result.elsep.notNullInfo) result.withNotNullInfo( if result.thenp.tpe.isRef(defn.NothingClass) then - elsePathInfo.withOnceRetracted(thenPathInfo) + elsePathInfo.withRetracted(thenPathInfo) else if result.elsep.tpe.isRef(defn.NothingClass) then - thenPathInfo.withOnceRetracted(elsePathInfo) + thenPathInfo.withRetracted(elsePathInfo) else thenPathInfo.alt(elsePathInfo) ) end typedIf @@ -2150,9 +2150,9 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer def typedMatchFinish(tree: untpd.Match, sel: Tree, wideSelType: Type, cases: List[untpd.CaseDef], pt: Type)(using Context): Tree = { val cases1 = harmonic(harmonize, pt)(typedCases(cases, sel, wideSelType, pt.dropIfProto)) .asInstanceOf[List[CaseDef]] - var nni = sel.notNullInfo - if cases1.nonEmpty then nni = nni.seq(cases1.map(_.notNullInfo).reduce(_.alt(_))) - assignType(cpy.Match(tree)(sel, cases1), sel, cases1).withNotNullInfo(nni) + var nnInfo = sel.notNullInfo + if cases1.nonEmpty then nnInfo = nnInfo.seq(cases1.map(_.notNullInfo).reduce(_.alt(_))) + assignType(cpy.Match(tree)(sel, cases1), sel, cases1).withNotNullInfo(nnInfo) } def typedCases(cases: List[untpd.CaseDef], sel: Tree, wideSelType0: Type, pt: Type)(using Context): List[CaseDef] = @@ -2334,7 +2334,8 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer val capabilityProof = caughtExceptions.reduce(OrType(_, _, true)) untpd.Block(makeCanThrow(capabilityProof), expr) - def typedTry(tree: untpd.Try, pt: Type)(using Context): Try = { + def typedTry(tree: untpd.Try, pt: Type)(using Context): Try = + var nnInfo = NotNullInfo.empty val expr2 :: cases2x = harmonic(harmonize, pt) { // We want to type check tree.expr first to comput NotNullInfo, but `addCanThrowCapabilities` // uses the types of patterns in `tree.cases` to determine the capabilities. @@ -2346,25 +2347,26 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer val casesEmptyBody1 = tree.cases.mapconserve(cpy.CaseDef(_)(body = EmptyTree)) val casesEmptyBody2 = typedCases(casesEmptyBody1, EmptyTree, defn.ThrowableType, WildcardType) val expr1 = typed(addCanThrowCapabilities(tree.expr, casesEmptyBody2), pt.dropIfProto) - val casesCtx = ctx.addNotNullInfo(expr1.notNullInfo.retractedInfo) + + // Since we don't know at which point the the exception is thrown in the body, + // we have to collect any reference that is once retracted. + nnInfo = expr1.notNullInfo.retractedInfo + + val casesCtx = ctx.addNotNullInfo(nnInfo) val cases1 = typedCases(tree.cases, EmptyTree, defn.ThrowableType, pt.dropIfProto)(using casesCtx) expr1 :: cases1 }: @unchecked val cases2 = cases2x.asInstanceOf[List[CaseDef]] - // Since we don't know at which point the the exception is thrown in the body, - // we have to collect any reference that is once retracted. - var nni = expr2.notNullInfo.onceRetractedInfo // It is possible to have non-exhaustive cases, and some exceptions are thrown and not caught. // Therefore, the code in the finallizer and after the try block can only rely on the retracted // info from the cases' body. - if cases2.nonEmpty then nni = nni.seq(cases2.map(_.notNullInfo.retractedInfo).reduce(_.alt(_))) - - val finalizer1 = typed(tree.finalizer, defn.UnitType)(using ctx.addNotNullInfo(nni)) - nni = nni.seq(finalizer1.notNullInfo) + if cases2.nonEmpty then + nnInfo = nnInfo.seq(cases2.map(_.notNullInfo.retractedInfo).reduce(_.alt(_))) - assignType(cpy.Try(tree)(expr2, cases2, finalizer1), expr2, cases2).withNotNullInfo(nni) - } + val finalizer1 = typed(tree.finalizer, defn.UnitType)(using ctx.addNotNullInfo(nnInfo)) + nnInfo = nnInfo.seq(finalizer1.notNullInfo) + assignType(cpy.Try(tree)(expr2, cases2, finalizer1), expr2, cases2).withNotNullInfo(nnInfo) def typedTry(tree: untpd.ParsedTry, pt: Type)(using Context): Try = val cases: List[untpd.CaseDef] = tree.handler match diff --git a/tests/explicit-nulls/neg/i21619.scala b/tests/explicit-nulls/neg/i21619.scala index 1c93af707b73..244f993fd4e1 100644 --- a/tests/explicit-nulls/neg/i21619.scala +++ b/tests/explicit-nulls/neg/i21619.scala @@ -59,4 +59,21 @@ def test4: String = x = "" catch case _ => - x.replace("", "") // error \ No newline at end of file + x.replace("", "") // error + +def test5: Unit = + var x: String | Null = null + var y: String | Null = null + x = "" + y = "" + var i: Int = 1 + try + i match + case _ => + x = null + throw new Exception() + x = "" + catch + case _ => + val z1: String = x.replace("", "") // error + val z2: String = y.replace("", "") \ No newline at end of file From 05c630acf9dd8b2ecb98f34b1f40bfc8ddb55ce8 Mon Sep 17 00:00:00 2001 From: noti0na1 Date: Fri, 11 Oct 2024 06:24:24 +0200 Subject: [PATCH 05/17] Consider cases with Nothing type --- .../src/dotty/tools/dotc/typer/Typer.scala | 24 ++++++++++++------- tests/explicit-nulls/neg/i21380b.scala | 18 ++++++++++++++ 2 files changed, 34 insertions(+), 8 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index 8a72076527b9..5731b44368e6 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -1552,9 +1552,9 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer def thenPathInfo = cond1.notNullInfoIf(true).seq(result.thenp.notNullInfo) def elsePathInfo = cond1.notNullInfoIf(false).seq(result.elsep.notNullInfo) result.withNotNullInfo( - if result.thenp.tpe.isRef(defn.NothingClass) then + if result.thenp.tpe.isNothingType then elsePathInfo.withRetracted(thenPathInfo) - else if result.elsep.tpe.isRef(defn.NothingClass) then + else if result.elsep.tpe.isNothingType then thenPathInfo.withRetracted(elsePathInfo) else thenPathInfo.alt(elsePathInfo) ) @@ -2141,20 +2141,28 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer case1 } .asInstanceOf[List[CaseDef]] - var nni = sel.notNullInfo - if cases1.nonEmpty then nni = nni.seq(cases1.map(_.notNullInfo).reduce(_.alt(_))) - assignType(cpy.Match(tree)(sel, cases1), sel, cases1).cast(pt).withNotNullInfo(nni) + assignType(cpy.Match(tree)(sel, cases1), sel, cases1).cast(pt) + .withNotNullInfo(notNullInfoFromCases(sel.notNullInfo, cases1)) } // Overridden in InlineTyper for inline matches def typedMatchFinish(tree: untpd.Match, sel: Tree, wideSelType: Type, cases: List[untpd.CaseDef], pt: Type)(using Context): Tree = { val cases1 = harmonic(harmonize, pt)(typedCases(cases, sel, wideSelType, pt.dropIfProto)) .asInstanceOf[List[CaseDef]] - var nnInfo = sel.notNullInfo - if cases1.nonEmpty then nnInfo = nnInfo.seq(cases1.map(_.notNullInfo).reduce(_.alt(_))) - assignType(cpy.Match(tree)(sel, cases1), sel, cases1).withNotNullInfo(nnInfo) + assignType(cpy.Match(tree)(sel, cases1), sel, cases1) + .withNotNullInfo(notNullInfoFromCases(sel.notNullInfo, cases1)) } + private def notNullInfoFromCases(initInfo: NotNullInfo, cases: List[CaseDef])(using Context): NotNullInfo = + var nnInfo = initInfo + if cases.nonEmpty then + val (nothingCases, normalCases) = cases.partition(_.body.tpe.isNothingType) + nnInfo = nothingCases.foldLeft(nnInfo): + (nni, c) => nni.withRetracted(c.notNullInfo) + if normalCases.nonEmpty then + nnInfo = nnInfo.seq(normalCases.map(_.notNullInfo).reduce(_.alt(_))) + nnInfo + def typedCases(cases: List[untpd.CaseDef], sel: Tree, wideSelType0: Type, pt: Type)(using Context): List[CaseDef] = var caseCtx = ctx var wideSelType = wideSelType0 diff --git a/tests/explicit-nulls/neg/i21380b.scala b/tests/explicit-nulls/neg/i21380b.scala index 83e23053547c..e4d0caa9e32f 100644 --- a/tests/explicit-nulls/neg/i21380b.scala +++ b/tests/explicit-nulls/neg/i21380b.scala @@ -18,4 +18,22 @@ def test3(i: Int) = i match case 1 if x != null => () case _ => x = " " + x.trim() // ok + +def test4(i: Int) = + var x: String | Null = null + var y: String | Null = null + i match + case 1 => x = "1" + case _ => y = " " + x.trim() // error + +def test5(i: Int): String = + var x: String | Null = null + var y: String | Null = null + i match + case 1 => x = "1" + case _ => + y = " " + return y x.trim() // ok \ No newline at end of file From d44147bc6ff5496dab9cc1569274dfb4b673ee09 Mon Sep 17 00:00:00 2001 From: noti0na1 Date: Mon, 14 Oct 2024 15:21:03 +0200 Subject: [PATCH 06/17] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ondřej Lhoták --- compiler/src/dotty/tools/dotc/typer/Nullables.scala | 7 +++++-- compiler/src/dotty/tools/dotc/typer/Typer.scala | 2 +- tests/explicit-nulls/neg/i21380c.scala | 2 +- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/typer/Nullables.scala b/compiler/src/dotty/tools/dotc/typer/Nullables.scala index 62d2ccfb7200..e6d764dc1be4 100644 --- a/compiler/src/dotty/tools/dotc/typer/Nullables.scala +++ b/compiler/src/dotty/tools/dotc/typer/Nullables.scala @@ -52,8 +52,11 @@ object Nullables: val hiTree = if(hiTpe eq hi.typeOpt) hi else TypeTree(hiTpe) TypeBoundsTree(lo, hiTree, alias) - /** A set of val or var references that are known to be not null, - * plus a set of variable references that are once assigned to null. + /** A set of val or var references that are known to be not null + * after the tree finishes executing normally (non-exceptionally), + * plus a set of variable references that are ever assigned to null, + * and may therefore be null if execution of the tree is interrupted + * by an exception. */ case class NotNullInfo(asserted: Set[TermRef], retracted: Set[TermRef]): def isEmpty = this eq NotNullInfo.empty diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index 5731b44368e6..392a4e18c454 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -2367,7 +2367,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer val cases2 = cases2x.asInstanceOf[List[CaseDef]] // It is possible to have non-exhaustive cases, and some exceptions are thrown and not caught. - // Therefore, the code in the finallizer and after the try block can only rely on the retracted + // Therefore, the code in the finalizer and after the try block can only rely on the retracted // info from the cases' body. if cases2.nonEmpty then nnInfo = nnInfo.seq(cases2.map(_.notNullInfo.retractedInfo).reduce(_.alt(_))) diff --git a/tests/explicit-nulls/neg/i21380c.scala b/tests/explicit-nulls/neg/i21380c.scala index de3cd5bafd6b..9b7a721fbdf0 100644 --- a/tests/explicit-nulls/neg/i21380c.scala +++ b/tests/explicit-nulls/neg/i21380c.scala @@ -33,7 +33,7 @@ def test4: Int = case _ => x = "" x.length // error // Although the catch block here is exhaustive, it is possible to have non-exhaustive cases, - // and some exceptions are thrown and not caught. Therefore, the code in the finallizer and + // and some exceptions are thrown and not caught. Therefore, the code in the finalizer and // after the try block can only rely on the retracted info from the cases' body. def test5: Int = From f859afe8e4ace35e026600bb784664dcbcdbda98 Mon Sep 17 00:00:00 2001 From: noti0na1 Date: Fri, 1 Nov 2024 09:07:18 +0100 Subject: [PATCH 07/17] Add terminated info --- .../dotty/tools/dotc/typer/Nullables.scala | 43 ++++++++++++------- .../src/dotty/tools/dotc/typer/Typer.scala | 27 ++++-------- 2 files changed, 36 insertions(+), 34 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/typer/Nullables.scala b/compiler/src/dotty/tools/dotc/typer/Nullables.scala index e6d764dc1be4..30c65771d9c2 100644 --- a/compiler/src/dotty/tools/dotc/typer/Nullables.scala +++ b/compiler/src/dotty/tools/dotc/typer/Nullables.scala @@ -53,37 +53,45 @@ object Nullables: TypeBoundsTree(lo, hiTree, alias) /** A set of val or var references that are known to be not null - * after the tree finishes executing normally (non-exceptionally), + * after the tree finishes executing normally (non-exceptionally), * plus a set of variable references that are ever assigned to null, * and may therefore be null if execution of the tree is interrupted * by an exception. */ - case class NotNullInfo(asserted: Set[TermRef], retracted: Set[TermRef]): + case class NotNullInfo(asserted: Set[TermRef] | Null, retracted: Set[TermRef]): def isEmpty = this eq NotNullInfo.empty def retractedInfo = NotNullInfo(Set(), retracted) + def terminatedInfo = NotNullInfo(null, retracted) + /** The sequential combination with another not-null info */ def seq(that: NotNullInfo): NotNullInfo = if this.isEmpty then that else if that.isEmpty then this - else NotNullInfo( - this.asserted.diff(that.retracted).union(that.asserted), - this.retracted.union(that.retracted)) + else + val newAsserted = + if this.asserted == null || that.asserted == null then null + else this.asserted.diff(that.retracted).union(that.asserted) + val newRetracted = this.retracted.union(that.retracted) + NotNullInfo(newAsserted, newRetracted) /** The alternative path combination with another not-null info. Used to merge - * the nullability info of the two branches of an if. + * the nullability info of the branches of an if or match. */ def alt(that: NotNullInfo): NotNullInfo = - NotNullInfo(this.asserted.intersect(that.asserted), this.retracted.union(that.retracted)) - - def withRetracted(that: NotNullInfo): NotNullInfo = - NotNullInfo(this.asserted, this.retracted.union(that.retracted)) + val newAsserted = + if this.asserted == null then that.asserted + else if that.asserted == null then this.asserted + else this.asserted.intersect(that.asserted) + val newRetracted = this.retracted.union(that.retracted) + NotNullInfo(newAsserted, newRetracted) + end NotNullInfo object NotNullInfo: val empty = new NotNullInfo(Set(), Set()) - def apply(asserted: Set[TermRef], retracted: Set[TermRef]): NotNullInfo = - if asserted.isEmpty && retracted.isEmpty then empty + def apply(asserted: Set[TermRef] | Null, retracted: Set[TermRef]): NotNullInfo = + if asserted != null && asserted.isEmpty && retracted.isEmpty then empty else new NotNullInfo(asserted, retracted) end NotNullInfo @@ -227,7 +235,7 @@ object Nullables: */ @tailrec def impliesNotNull(ref: TermRef): Boolean = infos match case info :: infos1 => - if info.asserted.contains(ref) then true + if info.asserted != null && info.asserted.contains(ref) then true else if info.retracted.contains(ref) then false else infos1.impliesNotNull(ref) case _ => @@ -243,7 +251,9 @@ object Nullables: /** 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))) + (ms, info) => ms.union( + if info.asserted == null then Set.empty + else info.asserted.filter(_.symbol.is(Mutable))) infos.extendWith(NotNullInfo(Set(), mutables)) end extension @@ -516,7 +526,10 @@ object Nullables: && assignmentSpans.getOrElse(sym.span.start, Nil).exists(whileSpan.contains(_)) && ctx.notNullInfos.impliesNotNull(ref) - val retractedVars = ctx.notNullInfos.flatMap(_.asserted.filter(isRetracted)).toSet + val retractedVars = ctx.notNullInfos.flatMap(info => + if info.asserted == null then Set.empty + else info.asserted.filter(isRetracted) + ).toSet ctx.addNotNullInfo(NotNullInfo(Set(), retractedVars)) end whileContext diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index 392a4e18c454..5ec9dbbe28b9 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -1551,13 +1551,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer def thenPathInfo = cond1.notNullInfoIf(true).seq(result.thenp.notNullInfo) def elsePathInfo = cond1.notNullInfoIf(false).seq(result.elsep.notNullInfo) - result.withNotNullInfo( - if result.thenp.tpe.isNothingType then - elsePathInfo.withRetracted(thenPathInfo) - else if result.elsep.tpe.isNothingType then - thenPathInfo.withRetracted(elsePathInfo) - else thenPathInfo.alt(elsePathInfo) - ) + result.withNotNullInfo(thenPathInfo.alt(elsePathInfo)) end typedIf /** Decompose function prototype into a list of parameter prototypes and a result @@ -2154,14 +2148,9 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer } private def notNullInfoFromCases(initInfo: NotNullInfo, cases: List[CaseDef])(using Context): NotNullInfo = - var nnInfo = initInfo if cases.nonEmpty then - val (nothingCases, normalCases) = cases.partition(_.body.tpe.isNothingType) - nnInfo = nothingCases.foldLeft(nnInfo): - (nni, c) => nni.withRetracted(c.notNullInfo) - if normalCases.nonEmpty then - nnInfo = nnInfo.seq(normalCases.map(_.notNullInfo).reduce(_.alt(_))) - nnInfo + initInfo.seq(cases.map(_.notNullInfo).reduce(_.alt(_))) + else initInfo def typedCases(cases: List[untpd.CaseDef], sel: Tree, wideSelType0: Type, pt: Type)(using Context): List[CaseDef] = var caseCtx = ctx @@ -2251,7 +2240,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer def typedLabeled(tree: untpd.Labeled)(using Context): Labeled = { val bind1 = typedBind(tree.bind, WildcardType).asInstanceOf[Bind] val expr1 = typed(tree.expr, bind1.symbol.info) - assignType(cpy.Labeled(tree)(bind1, expr1)) + assignType(cpy.Labeled(tree)(bind1, expr1)).withNotNullInfo(expr1.notNullInfo.retractedInfo) } /** Type a case of a type match */ @@ -2301,7 +2290,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer // Hence no adaptation is possible, and we assume WildcardType as prototype. (from, proto) val expr1 = typedExpr(tree.expr orElse untpd.syntheticUnitLiteral.withSpan(tree.span), proto) - assignType(cpy.Return(tree)(expr1, from)) + assignType(cpy.Return(tree)(expr1, from)).withNotNullInfo(expr1.notNullInfo.terminatedInfo) end typedReturn def typedWhileDo(tree: untpd.WhileDo)(using Context): Tree = @@ -2388,15 +2377,15 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer def typedThrow(tree: untpd.Throw)(using Context): Tree = val expr1 = typed(tree.expr, defn.ThrowableType) val cap = checkCanThrow(expr1.tpe.widen, tree.span) - val res = Throw(expr1).withSpan(tree.span) + var res = Throw(expr1).withSpan(tree.span) if Feature.ccEnabled && !cap.isEmpty && !ctx.isAfterTyper then // Record access to the CanThrow capabulity recovered in `cap` by wrapping // the type of the `throw` (i.e. Nothing) in a `@requiresCapability` annotation. - Typed(res, + res = Typed(res, TypeTree( AnnotatedType(res.tpe, Annotation(defn.RequiresCapabilityAnnot, cap, tree.span)))) - else res + res.withNotNullInfo(expr1.notNullInfo.terminatedInfo) def typedSeqLiteral(tree: untpd.SeqLiteral, pt: Type)(using Context): SeqLiteral = { val elemProto = pt.stripNull().elemType match { From 158af7deed473826b5d16ade6b9472fd89948b6d Mon Sep 17 00:00:00 2001 From: noti0na1 Date: Fri, 6 Dec 2024 16:59:32 +0100 Subject: [PATCH 08/17] Fix deep NotNullInfo --- .../dotty/tools/dotc/typer/Applications.scala | 7 ++- .../dotty/tools/dotc/typer/Nullables.scala | 54 +++++++++++++------ .../src/dotty/tools/dotc/typer/Typer.scala | 1 - tests/explicit-nulls/neg/i21619.scala | 15 +++++- 4 files changed, 57 insertions(+), 20 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/typer/Applications.scala b/compiler/src/dotty/tools/dotc/typer/Applications.scala index 41e48f7595dc..96c38bcc80af 100644 --- a/compiler/src/dotty/tools/dotc/typer/Applications.scala +++ b/compiler/src/dotty/tools/dotc/typer/Applications.scala @@ -1134,7 +1134,7 @@ trait Applications extends Compatibility { case _ => () else () - fun1.tpe match { + val result = fun1.tpe match { case err: ErrorType => cpy.Apply(tree)(fun1, proto.typedArgs()).withType(err) case TryDynamicCallType => val isInsertedApply = fun1 match { @@ -1208,6 +1208,11 @@ trait Applications extends Compatibility { else tryWithImplicitOnQualifier(fun1, proto).getOrElse(fail)) } } + + if result.tpe.isNothingType then + val nnInfo = result.notNullInfo + result.withNotNullInfo(nnInfo.terminatedInfo) + else result } /** Convert expression like diff --git a/compiler/src/dotty/tools/dotc/typer/Nullables.scala b/compiler/src/dotty/tools/dotc/typer/Nullables.scala index 30c65771d9c2..2193866893f6 100644 --- a/compiler/src/dotty/tools/dotc/typer/Nullables.scala +++ b/compiler/src/dotty/tools/dotc/typer/Nullables.scala @@ -319,11 +319,29 @@ object Nullables: if !info.isEmpty then tree.putAttachment(NNInfo, info) tree + /* Collect the nullability info from parts of `tree` */ + def collectNotNullInfo(using Context): NotNullInfo = tree match + case Typed(expr, _) => + expr.notNullInfo + case Apply(fn, args) => + val argsInfo = args.map(_.notNullInfo) + val fnInfo = fn.notNullInfo + argsInfo.foldLeft(fnInfo)(_ seq _) + case TypeApply(fn, _) => + fn.notNullInfo + case _ => + // Other cases are handled specially in typer. + NotNullInfo.empty + /* The nullability info of `tree` */ def notNullInfo(using Context): NotNullInfo = - stripInlined(tree).getAttachment(NNInfo) match + val tree1 = stripInlined(tree) + tree1.getAttachment(NNInfo) match case Some(info) if !ctx.erasedTypes => info - case _ => NotNullInfo.empty + case _ => + val nnInfo = tree1.collectNotNullInfo + tree1.withNotNullInfo(nnInfo) + nnInfo /* The nullability info of `tree`, assuming it is a condition that evaluates to `c` */ def notNullInfoIf(c: Boolean)(using Context): NotNullInfo = @@ -404,21 +422,23 @@ object Nullables: end extension 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 + def computeAssignNullable()(using Context): tree.type = + var nnInfo = tree.rhs.notNullInfo + tree.lhs match + case TrackedRef(ref) if ctx.explicitNulls && ref.isNullableUnion => + nnInfo = nnInfo.seq: + val rhstp = tree.rhs.typeOpt + 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. + NotNullInfo(Set(), Set(ref)) + else + // If the initial type is nullable and the assigned value is non-null, + // we add it to the NotNull. + NotNullInfo(Set(ref), Set()) + case _ => + tree.withNotNullInfo(nnInfo) end extension private val analyzedOps = Set(nme.EQ, nme.NE, nme.eq, nme.ne, nme.ZAND, nme.ZOR, nme.UNARY_!) diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index 5ec9dbbe28b9..1e461a5e1cb7 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -1201,7 +1201,6 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer untpd.unsplice(tree.expr).putAttachment(AscribedToUnit, ()) typed(tree.expr, underlyingTreeTpe.tpe.widenSkolem) assignType(cpy.Typed(tree)(expr1, tpt), underlyingTreeTpe) - .withNotNullInfo(expr1.notNullInfo) } if (untpd.isWildcardStarArg(tree)) { diff --git a/tests/explicit-nulls/neg/i21619.scala b/tests/explicit-nulls/neg/i21619.scala index 244f993fd4e1..d7af27e3fe64 100644 --- a/tests/explicit-nulls/neg/i21619.scala +++ b/tests/explicit-nulls/neg/i21619.scala @@ -76,4 +76,17 @@ def test5: Unit = catch case _ => val z1: String = x.replace("", "") // error - val z2: String = y.replace("", "") \ No newline at end of file + val z2: String = y.replace("", "") + +def test6 = { + var x: String | Null = "" + var y: String = "" + x = "" + y = if (false) x else 1 match { + case _ => { + x = null + y + } + } + x.replace("", "") // error +} \ No newline at end of file From 00430c042a9031059aefe638caba7c7e2e8c49f5 Mon Sep 17 00:00:00 2001 From: noti0na1 Date: Sat, 7 Dec 2024 04:28:02 +0100 Subject: [PATCH 09/17] Treat asserted set of terminated NotNullInfo as universal set; fix test --- .../src/dotty/tools/dotc/core/Contexts.scala | 6 ++--- .../dotty/tools/dotc/typer/Nullables.scala | 22 ++++++++++--------- .../src/dotty/tools/dotc/typer/Typer.scala | 2 ++ .../pos/after-termination.scala | 17 ++++++++++++++ .../unsafe-common/unsafe-overload.scala | 12 +++++----- 5 files changed, 40 insertions(+), 19 deletions(-) create mode 100644 tests/explicit-nulls/pos/after-termination.scala diff --git a/compiler/src/dotty/tools/dotc/core/Contexts.scala b/compiler/src/dotty/tools/dotc/core/Contexts.scala index d69c7408d0b1..7f5779bb6127 100644 --- a/compiler/src/dotty/tools/dotc/core/Contexts.scala +++ b/compiler/src/dotty/tools/dotc/core/Contexts.scala @@ -777,13 +777,13 @@ object Contexts { extension (c: Context) def addNotNullInfo(info: NotNullInfo) = - c.withNotNullInfos(c.notNullInfos.extendWith(info)) + if c.explicitNulls then c.withNotNullInfos(c.notNullInfos.extendWith(info)) else c def addNotNullRefs(refs: Set[TermRef]) = - c.addNotNullInfo(NotNullInfo(refs, Set())) + if c.explicitNulls then c.addNotNullInfo(NotNullInfo(refs, Set())) else c def withNotNullInfos(infos: List[NotNullInfo]): Context = - if c.notNullInfos eq infos then c else c.fresh.setNotNullInfos(infos) + if !c.explicitNulls || (c.notNullInfos eq infos) then c else c.fresh.setNotNullInfos(infos) def relaxedOverrideContext: Context = c.withModeBits(c.mode &~ Mode.SafeNulls | Mode.RelaxedOverriding) diff --git a/compiler/src/dotty/tools/dotc/typer/Nullables.scala b/compiler/src/dotty/tools/dotc/typer/Nullables.scala index 2193866893f6..310ca999f4c5 100644 --- a/compiler/src/dotty/tools/dotc/typer/Nullables.scala +++ b/compiler/src/dotty/tools/dotc/typer/Nullables.scala @@ -235,7 +235,7 @@ object Nullables: */ @tailrec def impliesNotNull(ref: TermRef): Boolean = infos match case info :: infos1 => - if info.asserted != null && info.asserted.contains(ref) then true + if info.asserted == null || info.asserted.contains(ref) then true else if info.retracted.contains(ref) then false else infos1.impliesNotNull(ref) case _ => @@ -315,8 +315,8 @@ object Nullables: extension (tree: Tree) /* The `tree` with added nullability attachment */ - def withNotNullInfo(info: NotNullInfo): tree.type = - if !info.isEmpty then tree.putAttachment(NNInfo, info) + def withNotNullInfo(info: NotNullInfo)(using Context): tree.type = + if ctx.explicitNulls && !info.isEmpty then tree.putAttachment(NNInfo, info) tree /* Collect the nullability info from parts of `tree` */ @@ -335,13 +335,15 @@ object Nullables: /* The nullability info of `tree` */ def notNullInfo(using Context): NotNullInfo = - val tree1 = stripInlined(tree) - tree1.getAttachment(NNInfo) match - case Some(info) if !ctx.erasedTypes => info - case _ => - val nnInfo = tree1.collectNotNullInfo - tree1.withNotNullInfo(nnInfo) - nnInfo + if !ctx.explicitNulls then NotNullInfo.empty + else + val tree1 = stripInlined(tree) + tree1.getAttachment(NNInfo) match + case Some(info) if !ctx.erasedTypes => info + case _ => + val nnInfo = tree1.collectNotNullInfo + tree1.withNotNullInfo(nnInfo) + nnInfo /* The nullability info of `tree`, assuming it is a condition that evaluates to `c` */ def notNullInfoIf(c: Boolean)(using Context): NotNullInfo = diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index 1e461a5e1cb7..cea47817bb88 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -2849,6 +2849,8 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer val vdef1 = assignType(cpy.ValDef(vdef)(name, tpt1, rhs1), sym) postProcessInfo(vdef1, sym) vdef1.setDefTree + val nnInfo = rhs1.notNullInfo + vdef1.withNotNullInfo(if sym.is(Lazy) then nnInfo.retractedInfo else nnInfo) } private def retractDefDef(sym: Symbol)(using Context): Tree = diff --git a/tests/explicit-nulls/pos/after-termination.scala b/tests/explicit-nulls/pos/after-termination.scala new file mode 100644 index 000000000000..00a57e371281 --- /dev/null +++ b/tests/explicit-nulls/pos/after-termination.scala @@ -0,0 +1,17 @@ +class C(val x: Int, val next: C | Null) + +def test1(x: String | Null, c: C | Null): Int = + return 0 + // We know that the following code is unreachable, + // so we can treat `x`, `c`, and any variable/path non-nullable. + x.length + c.next.x + +def test2(x: String | Null, c: C | Null): Int = + throw new Exception() + x.length + c.next.x + +def fail(): Nothing = ??? + +def test3(x: String | Null, c: C | Null): Int = + fail() + x.length + c.next.x diff --git a/tests/explicit-nulls/unsafe-common/unsafe-overload.scala b/tests/explicit-nulls/unsafe-common/unsafe-overload.scala index e7e551f1bda1..21af320806d8 100644 --- a/tests/explicit-nulls/unsafe-common/unsafe-overload.scala +++ b/tests/explicit-nulls/unsafe-common/unsafe-overload.scala @@ -16,8 +16,8 @@ class S { val o: O = ??? locally { - def h1(hh: String => String) = ??? - def h2(hh: Array[String] => Array[String]) = ??? + def h1(hh: String => String): Unit = ??? + def h2(hh: Array[String] => Array[String]): Unit = ??? def f1(x: String | Null): String | Null = ??? def f2(x: Array[String | Null]): Array[String | Null] = ??? @@ -29,10 +29,10 @@ class S { } locally { - def h1(hh: String | Null => String | Null) = ??? - def h2(hh: Array[String | Null] => Array[String | Null]) = ??? + def h1(hh: String | Null => String | Null): Unit = ??? + def h2(hh: Array[String | Null] => Array[String | Null]): Unit = ??? def g1(x: String): String = ??? - def g2(x: Array[String]): Array[String] = ??? + def g2(x: Array[String]): Array[String] = ??? h1(g1) // error h1(o.g) // error @@ -51,7 +51,7 @@ class S { locally { def g1(x: String): String = ??? - def g2(x: Array[String]): Array[String] = ??? + def g2(x: Array[String]): Array[String] = ??? o.i(g1) // error o.i(g2) // error From 200c038a818ed41d8a07a18b540abd0748a99f12 Mon Sep 17 00:00:00 2001 From: noti0na1 Date: Tue, 10 Dec 2024 12:24:34 +0100 Subject: [PATCH 10/17] Comment on the empty cases in notNullInfoFromCases. --- compiler/src/dotty/tools/dotc/typer/Typer.scala | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index cea47817bb88..2c513a41a039 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -2147,9 +2147,11 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer } private def notNullInfoFromCases(initInfo: NotNullInfo, cases: List[CaseDef])(using Context): NotNullInfo = - if cases.nonEmpty then - initInfo.seq(cases.map(_.notNullInfo).reduce(_.alt(_))) - else initInfo + if cases.isEmpty then + // Empty cases is not allowed for match tree in the source code, + // but it can be generated by inlining: `tests/pos/i19198.scala`. + initInfo + else cases.map(_.notNullInfo).reduce(_.alt(_)) def typedCases(cases: List[untpd.CaseDef], sel: Tree, wideSelType0: Type, pt: Type)(using Context): List[CaseDef] = var caseCtx = ctx From ecf9be9532b9bd4729f4a6ec7358ef59ff96246a Mon Sep 17 00:00:00 2001 From: Wojciech Mazur Date: Tue, 10 Dec 2024 16:46:19 +0100 Subject: [PATCH 11/17] Fix Chocolatey publish workflow --- .github/workflows/publish-chocolatey.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish-chocolatey.yml b/.github/workflows/publish-chocolatey.yml index 3b31728a50ba..88a8a7913188 100644 --- a/.github/workflows/publish-chocolatey.yml +++ b/.github/workflows/publish-chocolatey.yml @@ -35,5 +35,5 @@ jobs: with: name: scala.nupkg - name: Publish the package to Chocolatey - run: choco push scala.nupkg --source https://push.chocolatey.org/ --api-key ${{ secrets.API-KEY }} + run: choco push scala.${{inputs.version}}.nupkg --source https://push.chocolatey.org/ --api-key ${{ secrets.API-KEY }} \ No newline at end of file From 70cc1a19da85f502fc58c8f0ed4fbe6ff9444e7d Mon Sep 17 00:00:00 2001 From: Rui Chen Date: Tue, 10 Dec 2024 15:38:45 -0500 Subject: [PATCH 12/17] fix: update `scala-cli.jar` path Signed-off-by: Rui Chen --- dist/libexec/cli-common-platform | 2 +- dist/libexec/cli-common-platform.bat | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dist/libexec/cli-common-platform b/dist/libexec/cli-common-platform index a5906e882bb4..e56f5221dbf2 100644 --- a/dist/libexec/cli-common-platform +++ b/dist/libexec/cli-common-platform @@ -1,3 +1,3 @@ #!/usr/bin/env bash -SCALA_CLI_CMD_BASH=("\"$JAVACMD\"" "-jar \"$PROG_HOME/bin/scala-cli.jar\"") +SCALA_CLI_CMD_BASH=("\"$JAVACMD\"" "-jar \"$PROG_HOME/libexec/scala-cli.jar\"") diff --git a/dist/libexec/cli-common-platform.bat b/dist/libexec/cli-common-platform.bat index 99103266c1d9..45b09f3460e6 100644 --- a/dist/libexec/cli-common-platform.bat +++ b/dist/libexec/cli-common-platform.bat @@ -2,4 +2,4 @@ @rem we need to escape % in the java command path, for some reason this doesnt work in common.bat set "_JAVACMD=!_JAVACMD:%%=%%%%!" -set SCALA_CLI_CMD_WIN="%_JAVACMD%" "-jar" "%_PROG_HOME%\bin\scala-cli.jar" \ No newline at end of file +set SCALA_CLI_CMD_WIN="%_JAVACMD%" "-jar" "%_PROG_HOME%\libexec\scala-cli.jar" From 004cfc5ed76ea34245ca30c9cc3872e86f9e6d5e Mon Sep 17 00:00:00 2001 From: Eugene Yokota Date: Wed, 11 Dec 2024 01:32:40 -0500 Subject: [PATCH 13/17] refactor: improve Given search preference warning **Problem** It wasn't clear what action users was suppose to take to suppress the new-from-3.6 Given search preference warning. **Solution** 1. This refactors the code to give the warning an error code E205. 2. In case of warnings, tell the user to choose -source 3.5 vs 3.7, or use nowarn annotation. --- .../tools/dotc/reporting/ErrorMessageID.scala | 1 + .../dotty/tools/dotc/reporting/messages.scala | 38 +++++++++++++++++++ .../dotty/tools/dotc/typer/Implicits.scala | 22 ++--------- tests/neg/given-triangle.check | 6 +-- tests/warn/i21036a.check | 11 ++++-- tests/warn/i21036b.check | 9 +++-- tests/warn/i21036c.scala | 7 ++++ 7 files changed, 66 insertions(+), 28 deletions(-) create mode 100644 tests/warn/i21036c.scala diff --git a/compiler/src/dotty/tools/dotc/reporting/ErrorMessageID.scala b/compiler/src/dotty/tools/dotc/reporting/ErrorMessageID.scala index 2c3774b59a9a..d3467fe70c52 100644 --- a/compiler/src/dotty/tools/dotc/reporting/ErrorMessageID.scala +++ b/compiler/src/dotty/tools/dotc/reporting/ErrorMessageID.scala @@ -218,6 +218,7 @@ enum ErrorMessageID(val isActive: Boolean = true) extends java.lang.Enum[ErrorMe case QuotedTypeMissingID // errorNumber: 202 case DeprecatedAssignmentSyntaxID // errorNumber: 203 case DeprecatedInfixNamedArgumentSyntaxID // errorNumber: 204 + case GivenSearchPriorityID // errorNumber: 205 def errorNumber = ordinal - 1 diff --git a/compiler/src/dotty/tools/dotc/reporting/messages.scala b/compiler/src/dotty/tools/dotc/reporting/messages.scala index b396aa62f599..75aa553827f2 100644 --- a/compiler/src/dotty/tools/dotc/reporting/messages.scala +++ b/compiler/src/dotty/tools/dotc/reporting/messages.scala @@ -3361,3 +3361,41 @@ class DeprecatedInfixNamedArgumentSyntax()(using Context) extends SyntaxMsg(Depr + Message.rewriteNotice("This", version = SourceVersion.`3.6-migration`) def explain(using Context) = "" + +class GivenSearchPriorityWarning( + pt: Type, + cmp: Int, + prev: Int, + winner: TermRef, + loser: TermRef, + isLastOldVersion: Boolean +)(using Context) extends Message(GivenSearchPriorityID): + def kind = MessageKind.PotentialIssue + def choice(nth: String, c: Int) = + if c == 0 then "none - it's ambiguous" + else s"the $nth alternative" + val (change, whichChoice) = + if isLastOldVersion + then ("will change in the future release", "Current choice ") + else ("has changed", "Previous choice") + def warningMessage: String = + i"""Given search preference for $pt between alternatives + | ${loser} + |and + | ${winner} + |$change. + |$whichChoice : ${choice("first", prev)} + |Choice from Scala 3.7 : ${choice("second", cmp)}""" + def migrationHints: String = + i"""Suppress this warning by choosing -source 3.5, -source 3.7, or + |by using @annotation.nowarn("id=205")""" + def ambiguousNote: String = + i""" + | + |Note: $warningMessage""" + def msg(using Context) = + i"""$warningMessage + | + |$migrationHints""" + + def explain(using Context) = "" diff --git a/compiler/src/dotty/tools/dotc/typer/Implicits.scala b/compiler/src/dotty/tools/dotc/typer/Implicits.scala index 228206d8fb1e..193cc443b4ae 100644 --- a/compiler/src/dotty/tools/dotc/typer/Implicits.scala +++ b/compiler/src/dotty/tools/dotc/typer/Implicits.scala @@ -549,10 +549,10 @@ object Implicits: /** An ambiguous implicits failure */ class AmbiguousImplicits(val alt1: SearchSuccess, val alt2: SearchSuccess, val expectedType: Type, val argument: Tree, val nested: Boolean = false) extends SearchFailureType: - private[Implicits] var priorityChangeWarnings: List[Message] = Nil + private[Implicits] var priorityChangeWarnings: List[GivenSearchPriorityWarning] = Nil def priorityChangeWarningNote(using Context): String = - priorityChangeWarnings.map(msg => s"\n\nNote: $msg").mkString + priorityChangeWarnings.map(_.ambiguousNote).mkString def msg(using Context): Message = var str1 = err.refStr(alt1.ref) @@ -1312,7 +1312,7 @@ trait Implicits: // A map that associates a priority change warning (between -source 3.6 and 3.7) // with the candidate refs mentioned in the warning. We report the associated // message if one of the critical candidates is part of the result of the implicit search. - val priorityChangeWarnings = mutable.ListBuffer[(/*critical:*/ List[TermRef], Message)]() + val priorityChangeWarnings = mutable.ListBuffer[(/*critical:*/ List[TermRef], GivenSearchPriorityWarning)]() val sv = Feature.sourceVersion val isLastOldVersion = sv.stable == SourceVersion.`3.6` @@ -1353,21 +1353,7 @@ trait Implicits: cmp match case 1 => (alt2, alt1) case -1 => (alt1, alt2) - def choice(nth: String, c: Int) = - if c == 0 then "none - it's ambiguous" - else s"the $nth alternative" - val (change, whichChoice) = - if isLastOldVersion - then ("will change", "Current choice ") - else ("has changed", "Previous choice") - val msg = - em"""Given search preference for $pt between alternatives - | ${loser.ref} - |and - | ${winner.ref} - |$change. - |$whichChoice : ${choice("first", prev)} - |New choice from Scala 3.7: ${choice("second", cmp)}""" + val msg = GivenSearchPriorityWarning(pt, cmp, prev, winner.ref, loser.ref, isLastOldVersion) val critical = alt1.ref :: alt2.ref :: Nil priorityChangeWarnings += ((critical, msg)) if isLastOldVersion then prev else cmp diff --git a/tests/neg/given-triangle.check b/tests/neg/given-triangle.check index f366c18e78f0..8a05ed4b3129 100644 --- a/tests/neg/given-triangle.check +++ b/tests/neg/given-triangle.check @@ -7,6 +7,6 @@ | (given_B : B) |and | (given_A : A) - |will change. - |Current choice : the first alternative - |New choice from Scala 3.7: the second alternative + |will change in the future release. + |Current choice : the first alternative + |Choice from Scala 3.7 : the second alternative diff --git a/tests/warn/i21036a.check b/tests/warn/i21036a.check index 63d611a6e246..6ce5b94d123f 100644 --- a/tests/warn/i21036a.check +++ b/tests/warn/i21036a.check @@ -1,10 +1,13 @@ --- Warning: tests/warn/i21036a.scala:7:17 ------------------------------------------------------------------------------ +-- [E205] Potential Issue Warning: tests/warn/i21036a.scala:7:17 ------------------------------------------------------- 7 |val y = summon[A] // warn | ^ | Given search preference for A between alternatives | (b : B) | and | (a : A) - | will change. - | Current choice : the first alternative - | New choice from Scala 3.7: the second alternative + | will change in the future release. + | Current choice : the first alternative + | Choice from Scala 3.7 : the second alternative + | + | Suppress this warning by choosing -source 3.5, -source 3.7, or + | by using @annotation.nowarn("id=205") diff --git a/tests/warn/i21036b.check b/tests/warn/i21036b.check index dfa19a0e9bb1..da0639438c86 100644 --- a/tests/warn/i21036b.check +++ b/tests/warn/i21036b.check @@ -1,4 +1,4 @@ --- Warning: tests/warn/i21036b.scala:7:17 ------------------------------------------------------------------------------ +-- [E205] Potential Issue Warning: tests/warn/i21036b.scala:7:17 ------------------------------------------------------- 7 |val y = summon[A] // warn | ^ | Given search preference for A between alternatives @@ -6,5 +6,8 @@ | and | (a : A) | has changed. - | Previous choice : the first alternative - | New choice from Scala 3.7: the second alternative + | Previous choice : the first alternative + | Choice from Scala 3.7 : the second alternative + | + | Suppress this warning by choosing -source 3.5, -source 3.7, or + | by using @annotation.nowarn("id=205") diff --git a/tests/warn/i21036c.scala b/tests/warn/i21036c.scala new file mode 100644 index 000000000000..4015cc8a84bb --- /dev/null +++ b/tests/warn/i21036c.scala @@ -0,0 +1,7 @@ +trait A +trait B extends A +given b: B = ??? +given a: A = ??? + +@annotation.nowarn("id=205") +val y = summon[A] // don't warn \ No newline at end of file From 74417916fd97990a77c80462758ab240e52aa586 Mon Sep 17 00:00:00 2001 From: Wojciech Mazur Date: Wed, 11 Dec 2024 17:54:57 +0100 Subject: [PATCH 14/17] Update reference, MiMa previous version and sync TASTy version (#22187) * Update reference version to 3.6.3-RC1 (from 3.6.0) * Update mima previous binary verison to 3.6.2 (instead of unofficial 3.6.1) * Set TASTy version to `28.7-experimental-1` - it should have been set when branching of 3.6.3. * We now document better how and when tasty version should be set * Add additional runtime test to ensure we don't emit invalid TASTy version during Release / NIGHTLY releases and the expected version set in build matches version defined in TastyFormat --- project/Build.scala | 63 +++++++++++++++++-- tasty/src/dotty/tools/tasty/TastyFormat.scala | 2 +- .../tools/tasty/BuildTastyVersionTest.scala | 26 ++++++++ 3 files changed, 85 insertions(+), 6 deletions(-) create mode 100644 tasty/test/dotty/tools/tasty/BuildTastyVersionTest.scala diff --git a/project/Build.scala b/project/Build.scala index f36171aabbcd..db3f149cbab6 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -93,11 +93,12 @@ object Build { /** Version of the Scala compiler used to build the artifacts. * Reference version should track the latest version pushed to Maven: - * - In main branch it should be the last RC version (using experimental TASTy required for non-bootstrapped tests) + * - In main branch it should be the last RC version * - In release branch it should be the last stable release - * 3.6.0-RC1 was released as 3.6.0 - it's having and experimental TASTy version + * + * Warning: Change of this variable needs to be consulted with `expectedTastyVersion` */ - val referenceVersion = "3.6.0" + val referenceVersion = "3.6.3-RC1" /** Version of the Scala compiler targeted in the current release cycle * Contains a version without RC/SNAPSHOT/NIGHTLY specific suffixes @@ -105,6 +106,8 @@ object Build { * * Should only be referred from `dottyVersion` or settings/tasks requiring simplified version string, * eg. `compatMode` or Windows native distribution version. + * + * Warning: Change of this variable might require updating `expectedTastyVersion` */ val developedVersion = "3.6.4" @@ -116,6 +119,25 @@ object Build { * During final, stable release is set exactly to `developedVersion`. */ val baseVersion = s"$developedVersion-RC1" + + /** The version of TASTY that should be emitted, checked in runtime test + * For defails on how TASTY version should be set see related discussions: + * - https://github.com/scala/scala3/issues/13447#issuecomment-912447107 + * - https://github.com/scala/scala3/issues/14306#issuecomment-1069333516 + * - https://github.com/scala/scala3/pull/19321 + * + * Simplified rules, given 3.$minor.$patch = $developedVersion + * - Major version is always 28 + * - TASTY minor version: + * - in main (NIGHTLY): {if $patch == 0 then $minor else ${minor + 1}} + * - in release branch is always equal to $minor + * - TASTY experimental version: + * - in main (NIGHTLY) is always experimental + * - in release candidate branch is experimental if {patch == 0} + * - in stable release is always non-experimetnal + */ + val expectedTastyVersion = "28.7-experimental-1" + checkReleasedTastyVersion() /** Final version of Scala compiler, controlled by environment variables. */ val dottyVersion = { @@ -149,9 +171,9 @@ object Build { * For a developedVersion `3.M.P` the mimaPreviousDottyVersion should be set to: * - `3.M.0` if `P > 0` * - `3.(M-1).0` if `P = 0` - * 3.6.1 is an exception from this rule - 3.6.0 was a broken release + * 3.6.2 is an exception from this rule - 3.6.0 was a broken release, 3.6.1 was hotfix (unstable) release */ - val mimaPreviousDottyVersion = "3.6.1" + val mimaPreviousDottyVersion = "3.6.2" /** LTS version against which we check binary compatibility. * @@ -2424,6 +2446,9 @@ object Build { settings(disableDocSetting). settings( versionScheme := Some("semver-spec"), + Test / envVars ++= Map( + "EXPECTED_TASTY_VERSION" -> expectedTastyVersion, + ), if (mode == Bootstrapped) Def.settings( commonMiMaSettings, mimaForwardIssueFilters := MiMaFilters.TastyCore.ForwardsBreakingChanges, @@ -2473,6 +2498,34 @@ object Build { case Bootstrapped => commonBootstrappedSettings }) } + + /* Tests TASTy version invariants during NIGHLY, RC or Stable releases */ + def checkReleasedTastyVersion(): Unit = { + lazy val (scalaMinor, scalaPatch, scalaIsRC) = baseVersion.split("\\.|-").take(4) match { + case Array("3", minor, patch) => (minor.toInt, patch.toInt, false) + case Array("3", minor, patch, _) => (minor.toInt, patch.toInt, true) + case other => sys.error(s"Invalid Scala base version string: $baseVersion") + } + lazy val (tastyMinor, tastyIsExperimental) = expectedTastyVersion.split("\\.|-").take(4) match { + case Array("28", minor) => (minor.toInt, false) + case Array("28", minor, "experimental", _) => (minor.toInt, true) + case other => sys.error(s"Invalid TASTy version string: $expectedTastyVersion") + } + + if(isNightly) { + assert(tastyIsExperimental, "TASTY needs to be experimental in nightly builds") + val expectedTastyMinor = if(scalaPatch == 0) scalaMinor else scalaMinor + 1 + assert(tastyMinor == expectedTastyMinor, "Invalid TASTy minor version") + } + + if(isRelease) { + assert(scalaMinor == tastyMinor, "Minor versions of TASTY vesion and Scala version should match in release builds") + if (scalaIsRC && scalaPatch == 0) + assert(tastyIsExperimental, "TASTy should be experimental when releasing a new minor version RC") + else + assert(!tastyIsExperimental, "Stable version cannot use experimental TASTY") + } + } } object ScaladocConfigs { diff --git a/tasty/src/dotty/tools/tasty/TastyFormat.scala b/tasty/src/dotty/tools/tasty/TastyFormat.scala index 8f5f9d57a8a5..8ff590fefec5 100644 --- a/tasty/src/dotty/tools/tasty/TastyFormat.scala +++ b/tasty/src/dotty/tools/tasty/TastyFormat.scala @@ -324,7 +324,7 @@ object TastyFormat { * compatibility, but remains backwards compatible, with all * preceding `MinorVersion`. */ - final val MinorVersion: Int = 6 + final val MinorVersion: Int = 7 /** Natural Number. The `ExperimentalVersion` allows for * experimentation with changes to TASTy without committing diff --git a/tasty/test/dotty/tools/tasty/BuildTastyVersionTest.scala b/tasty/test/dotty/tools/tasty/BuildTastyVersionTest.scala new file mode 100644 index 000000000000..d2e62e1f9eb0 --- /dev/null +++ b/tasty/test/dotty/tools/tasty/BuildTastyVersionTest.scala @@ -0,0 +1,26 @@ +package dotty.tools.tasty + +import org.junit.Assert._ +import org.junit.Test + +import TastyBuffer._ + +// Tests ensuring TASTY version emitted by compiler is matching expected TASTY version +class BuildTastyVersionTest { + + val CurrentTastyVersion = TastyVersion(TastyFormat.MajorVersion, TastyFormat.MinorVersion, TastyFormat.ExperimentalVersion) + + // Needs to be defined in build Test/envVars + val ExpectedTastyVersionEnvVar = "EXPECTED_TASTY_VERSION" + + @Test def testBuildTastyVersion(): Unit = { + val expectedVersion = sys.env.get(ExpectedTastyVersionEnvVar) + .getOrElse(fail(s"Env variable $ExpectedTastyVersionEnvVar not defined")) + .match { + case s"$major.$minor-experimental-$experimental" => TastyVersion(major.toInt, minor.toInt, experimental.toInt) + case s"$major.$minor" if minor.forall(_.isDigit) => TastyVersion(major.toInt, minor.toInt, 0) + case other => fail(s"Invalid TASTY version string: $other") + } + assertEquals(CurrentTastyVersion, expectedVersion) + } +} From 93ef8107c78d9a311f01716224130d7a7a5bd784 Mon Sep 17 00:00:00 2001 From: Matt Bovel Date: Thu, 12 Dec 2024 10:17:29 +0100 Subject: [PATCH 15/17] Remove tests/pos-with-compiler-cc from VSCode ignored files --- .vscode-template/settings.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.vscode-template/settings.json b/.vscode-template/settings.json index 257da27b118f..8cf2d29e3bae 100644 --- a/.vscode-template/settings.json +++ b/.vscode-template/settings.json @@ -9,7 +9,6 @@ "**/*.class": true, "**/*.tasty": true, "**/target/": true, - "community-build/community-projects": true, - "tests/pos-with-compiler-cc/dotc/**/*.scala": true + "community-build/community-projects": true } } From 5b3d82a41aafcaccab99bad95aa5a035a5dacabb Mon Sep 17 00:00:00 2001 From: Wojciech Mazur Date: Thu, 12 Dec 2024 15:21:33 +0100 Subject: [PATCH 16/17] Fix layout of released SDK archives, restore intermiediete top-level directory (#22199) Fixes #22194 Restores top-level directory `scala3-${version}` that is present in artifacts published before Scala 3.6, removed during hotfix 3.6.1 release. We now follow the [Well formed SDK archives layout](https://github.com/sdkman/sdkman-cli/wiki/Well-formed-SDK-archives). Removing the top-level directory even though at first glance looked like an improvement was in fact introducing problems to multiple package managers and build tools. --- .github/workflows/ci.yaml | 12 +++--------- project/Build.scala | 9 ++++++++- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index a2006e16c7e8..cc1eb5d40d97 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -801,19 +801,13 @@ jobs: distDir="$3" # Build binaries - ./project/scripts/sbt "${sbtProject}/Universal/stage" + ./project/scripts/sbt "all ${sbtProject}/Universal/packageBin ${sbtProject}/Universal/packageZipTarball" - outputPath="${distDir}/target/universal/stage" artifactName="scala3-${{ env.RELEASE_TAG }}${distroSuffix}" - zipArchive="${artifactName}.zip" - tarGzArchive="${artifactName}.tar.gz" - - cwd=$(pwd) - (cd $outputPath && zip -r ${zipArchive} . && mv ${zipArchive} "${cwd}/") - tar -czf ${tarGzArchive} -C "$outputPath" . # Caluclate SHA for each of archive files - for file in "${zipArchive}" "${tarGzArchive}"; do + for file in "${artifactName}.zip" "${artifactName}.tar.gz"; do + mv ${distDir}/target/universal/$file $file sha256sum "${file}" > "${file}.sha256" done } diff --git a/project/Build.scala b/project/Build.scala index db3f149cbab6..5aec4a4231a6 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -2257,7 +2257,14 @@ object Build { // ======== Universal / stage := (Universal / stage).dependsOn(republish).value, Universal / packageBin := (Universal / packageBin).dependsOn(republish).value, - Universal / packageZipTarball := (Universal / packageZipTarball).dependsOn(republish).value, + Universal / packageZipTarball := (Universal / packageZipTarball).dependsOn(republish) + .map { archiveFile => + // Rename .tgz to .tar.gz for consistency with previous versions + val renamedFile = archiveFile.getParentFile() / archiveFile.getName.replaceAll("\\.tgz$", ".tar.gz") + IO.move(archiveFile, renamedFile) + renamedFile + } + .value, // ======== Universal / mappings ++= directory(dist.base / "bin"), Universal / mappings ++= directory(republishRepo.value / "maven2"), From 6b9f9f797e78f32b135b7cc6bc941e10d073dd7f Mon Sep 17 00:00:00 2001 From: kasiaMarek Date: Fri, 6 Dec 2024 18:37:53 +0100 Subject: [PATCH 17/17] Add type parameters derived from enum to default param getters of enum cases --- .../src/dotty/tools/dotc/ast/Desugar.scala | 19 ++++++++++++------- tests/pos/i22137.scala | 5 +++++ 2 files changed, 17 insertions(+), 7 deletions(-) create mode 100644 tests/pos/i22137.scala diff --git a/compiler/src/dotty/tools/dotc/ast/Desugar.scala b/compiler/src/dotty/tools/dotc/ast/Desugar.scala index 56c153498f87..a95e64e24b85 100644 --- a/compiler/src/dotty/tools/dotc/ast/Desugar.scala +++ b/compiler/src/dotty/tools/dotc/ast/Desugar.scala @@ -694,15 +694,15 @@ object desugar { val originalTparams = constr1.leadingTypeParams val originalVparamss = asTermOnly(constr1.trailingParamss) lazy val derivedEnumParams = enumClass.typeParams.map(derivedTypeParamWithVariance) - val impliedTparams = - if (isEnumCase) { + val enumTParams = + if isEnumCase then val tparamReferenced = typeParamIsReferenced( - enumClass.typeParams, originalTparams, originalVparamss, parents) - if (originalTparams.isEmpty && (parents.isEmpty || tparamReferenced)) + enumClass.typeParams, originalTparams, originalVparamss, parents) + if originalTparams.isEmpty && (parents.isEmpty || tparamReferenced) then derivedEnumParams.map(tdef => tdef.withFlags(tdef.mods.flags | PrivateLocal)) - else originalTparams - } - else originalTparams + else Nil + else Nil + val impliedTparams = enumTParams ++ originalTparams if mods.is(Trait) then for vparams <- originalVparamss; vparam <- vparams do @@ -735,6 +735,11 @@ object desugar { derived.withAnnotations(Nil) val constr = cpy.DefDef(constr1)(paramss = joinParams(constrTparams, constrVparamss)) + if enumTParams.nonEmpty then + defaultGetters = defaultGetters.map: + case ddef: DefDef => + val tParams = enumTParams.map(tparam => toMethParam(tparam, KeepAnnotations.All)) + cpy.DefDef(ddef)(paramss = joinParams(tParams, ddef.trailingParamss)) val (normalizedBody, enumCases, enumCompanionRef) = { // Add constructor type parameters and evidence implicit parameters diff --git a/tests/pos/i22137.scala b/tests/pos/i22137.scala new file mode 100644 index 000000000000..b52dd9171146 --- /dev/null +++ b/tests/pos/i22137.scala @@ -0,0 +1,5 @@ +enum Parser[+Value]: + case Success(value: Value, issues: Seq[Failure] = Seq.empty) extends Parser[Value] + case Failure(exception: Throwable) extends Parser[Nothing] + +val v = Parser.Success(1) \ No newline at end of file