From ac7aac93f31a1068f3ddc5fba6e206c1a6afbed7 Mon Sep 17 00:00:00 2001 From: Xavientois Date: Mon, 11 Apr 2022 14:59:55 -0400 Subject: [PATCH] Check method arguments with parametricity when static When a global static is called, allow for a cold argument if the corresponding parameter is not `Matchable`. --- .../tools/dotc/transform/init/Semantic.scala | 48 +++++++++++++++---- tests/init/neg/early-promote.scala | 4 +- tests/init/neg/enum-desugared.check | 11 ----- tests/init/neg/enum.check | 9 ---- tests/init/neg/inner-case.scala | 4 +- tests/init/neg/inner-new.scala | 6 +-- .../neg/insert-cold-subtype-to-array.scala | 8 ++++ tests/init/neg/leak-warm.check | 13 +++-- tests/init/neg/leak-warm.scala | 4 +- tests/init/neg/some-this.scala | 2 + tests/init/{neg => pos}/enum-desugared.scala | 2 +- .../enum.scala => pos/enum-ordinal.scala} | 0 tests/init/pos/inner-enum-multi-variant.scala | 3 ++ tests/init/pos/inner-enum.scala | 4 ++ 14 files changed, 74 insertions(+), 44 deletions(-) delete mode 100644 tests/init/neg/enum-desugared.check delete mode 100644 tests/init/neg/enum.check create mode 100644 tests/init/neg/insert-cold-subtype-to-array.scala create mode 100644 tests/init/neg/some-this.scala rename tests/init/{neg => pos}/enum-desugared.scala (94%) rename tests/init/{neg/enum.scala => pos/enum-ordinal.scala} (100%) create mode 100644 tests/init/pos/inner-enum-multi-variant.scala create mode 100644 tests/init/pos/inner-enum.scala diff --git a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala index 25ea09c3ade4..924b9497e76c 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala @@ -410,8 +410,8 @@ object Semantic { def select(f: Symbol, source: Tree): Contextual[Result] = value.select(f, source) ++ errors - def call(meth: Symbol, args: List[ArgInfo], superType: Type, source: Tree): Contextual[Result] = - value.call(meth, args, superType, source) ++ errors + def call(meth: Symbol, args: List[ArgInfo], receiver: Type, superType: Type, source: Tree): Contextual[Result] = + value.call(meth, args, receiver, superType, source) ++ errors def callConstructor(ctor: Symbol, args: List[ArgInfo], source: Tree): Contextual[Result] = value.callConstructor(ctor, args, source) ++ errors @@ -587,7 +587,7 @@ object Semantic { } } - def call(meth: Symbol, args: List[ArgInfo], superType: Type, source: Tree, needResolve: Boolean = true): Contextual[Result] = log("call " + meth.show + ", args = " + args, printer, (_: Result).show) { + def call(meth: Symbol, args: List[ArgInfo], receiver: Type, superType: Type, source: Tree, needResolve: Boolean = true): Contextual[Result] = log("call " + meth.show + ", args = " + args, printer, (_: Result).show) { def checkArgs = args.flatMap(_.promote) def isSyntheticApply(meth: Symbol) = @@ -600,6 +600,27 @@ object Semantic { || (meth eq defn.Object_ne) || (meth eq defn.Any_isInstanceOf) + def checkArgsWithParametricity() = + val methodType = atPhaseBeforeTransforms { meth.info.stripPoly } + var allArgsPromote = true + val allParamTypes = methodType.paramInfoss.flatten.map(_.repeatedToSingle) + val errors = allParamTypes.zip(args).flatMap { (info, arg) => + val errors = arg.promote + allArgsPromote = allArgsPromote && errors.isEmpty + info match + case typeParamRef: TypeParamRef => + val bounds = typeParamRef.underlying.bounds + val isWithinBounds = bounds.lo <:< defn.NothingType && defn.AnyType <:< bounds.hi + def otherParamContains = allParamTypes.exists { param => param != info && param.typeSymbol != defn.ClassTagClass && info.occursIn(param) } + // A non-hot method argument is allowed if the corresponding parameter type is a + // type parameter T with Any as its upper bound and Nothing as its lower bound. + // the other arguments should either correspond to a parameter type that is T + // or that does not contain T as a component. + if isWithinBounds && !otherParamContains then Nil else errors + case _ => errors + } + (errors, allArgsPromote) + // fast track if the current object is already initialized if promoted.isCurrentObjectPromoted then Result(Hot, Nil) else if isAlwaysSafe(meth) then Result(Hot, Nil) @@ -610,7 +631,14 @@ object Semantic { val klass = meth.owner.companionClass.asClass instantiate(klass, klass.primaryConstructor, args, source) else - Result(Hot, checkArgs) + if receiver.typeSymbol.isStaticOwner then + val (errors, allArgsPromote) = checkArgsWithParametricity() + if allArgsPromote || errors.nonEmpty then + Result(Hot, errors) + else + Result(Cold, errors) + else + Result(Hot, checkArgs) case Cold => val error = CallCold(meth, source, trace.toVector) @@ -666,7 +694,7 @@ object Semantic { } case RefSet(refs) => - val resList = refs.map(_.call(meth, args, superType, source)) + val resList = refs.map(_.call(meth, args, receiver, superType, source)) val value2 = resList.map(_.value).join val errors = resList.flatMap(_.errors) Result(value2, errors) @@ -946,7 +974,7 @@ object Semantic { locally { given Trace = trace2 val args = member.info.paramInfoss.flatten.map(_ => ArgInfo(Hot, EmptyTree)) - val res = warm.call(member, args, superType = NoType, source = member.defTree) + val res = warm.call(member, args, receiver = NoType, superType = NoType, source = member.defTree) buffer ++= res.ensureHot(msg, source).errors } else @@ -1126,14 +1154,14 @@ object Semantic { case Select(supert: Super, _) => val SuperType(thisTp, superTp) = supert.tpe val thisValue2 = resolveThis(thisTp.classSymbol.asClass, thisV, klass, ref) - Result(thisValue2, errors).call(ref.symbol, args, superTp, expr) + Result(thisValue2, errors).call(ref.symbol, args, thisTp, superTp, expr) case Select(qual, _) => val res = eval(qual, thisV, klass) ++ errors if ref.symbol.isConstructor then res.callConstructor(ref.symbol, args, source = expr) else - res.call(ref.symbol, args, superType = NoType, source = expr) + res.call(ref.symbol, args, receiver = qual.tpe, superType = NoType, source = expr) case id: Ident => id.tpe match @@ -1142,13 +1170,13 @@ object Semantic { val enclosingClass = id.symbol.owner.enclosingClass.asClass val thisValue2 = resolveThis(enclosingClass, thisV, klass, id) // local methods are not a member, but we can reuse the method `call` - thisValue2.call(id.symbol, args, superType = NoType, expr, needResolve = false) + thisValue2.call(id.symbol, args, receiver = NoType, superType = NoType, expr, needResolve = false) case TermRef(prefix, _) => val res = cases(prefix, thisV, klass, id) ++ errors if id.symbol.isConstructor then res.callConstructor(id.symbol, args, source = expr) else - res.call(id.symbol, args, superType = NoType, source = expr) + res.call(id.symbol, args, receiver = prefix, superType = NoType, source = expr) case Select(qualifier, name) => val qualRes = eval(qualifier, thisV, klass) diff --git a/tests/init/neg/early-promote.scala b/tests/init/neg/early-promote.scala index ac1a7c8fe82e..fd226df347fb 100644 --- a/tests/init/neg/early-promote.scala +++ b/tests/init/neg/early-promote.scala @@ -10,7 +10,7 @@ class Y { val n = 10 val x = new X - List(x.b) // unsafe promotion + println(x.b) // unsafe promotion } @@ -24,7 +24,7 @@ class A { // checking A def c = new C } val b = new B() - List(b) // error: the checker simply issue warnings for objects that contain inner classes + println(b) // error: the checker simply issue warnings for objects that contain inner classes val af = 42 } diff --git a/tests/init/neg/enum-desugared.check b/tests/init/neg/enum-desugared.check deleted file mode 100644 index 3deb11a7f311..000000000000 --- a/tests/init/neg/enum-desugared.check +++ /dev/null @@ -1,11 +0,0 @@ --- Error: tests/init/neg/enum-desugared.scala:17:10 -------------------------------------------------------------------- -17 | Array(this.LazyErrorId, this.NoExplanationID) // error - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | Cannot prove that the value is fully initialized. Only initialized values may be used as arguments. - | - | The unsafe promotion may cause the following problem: - | Calling the external method method name may cause initialization errors. Calling trace: - | -> Array(this.LazyErrorId, this.NoExplanationID) // error [ enum-desugared.scala:17 ] - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | -> override def productPrefix: String = this.name() [ enum-desugared.scala:29 ] - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/tests/init/neg/enum.check b/tests/init/neg/enum.check deleted file mode 100644 index 655d9b74e863..000000000000 --- a/tests/init/neg/enum.check +++ /dev/null @@ -1,9 +0,0 @@ --- Error: tests/init/neg/enum.scala:4:8 -------------------------------------------------------------------------------- -4 | NoExplanationID // error - | ^ - | Cannot prove that the value is fully initialized. Only initialized values may be used as arguments. - | - | The unsafe promotion may cause the following problem: - | Calling the external method method name may cause initialization errors. Calling trace: - | -> NoExplanationID // error [ enum.scala:4 ] - | ^ diff --git a/tests/init/neg/inner-case.scala b/tests/init/neg/inner-case.scala index fa4ea0250884..20a9b1d2684f 100644 --- a/tests/init/neg/inner-case.scala +++ b/tests/init/neg/inner-case.scala @@ -4,8 +4,8 @@ class Foo { } val a = Inner(5) // ok - println(a) // error + println(a) // error var count = 0 - println(a) // ok + println(a) // ok } \ No newline at end of file diff --git a/tests/init/neg/inner-new.scala b/tests/init/neg/inner-new.scala index 016179a1d7fc..d09dc5193dbf 100644 --- a/tests/init/neg/inner-new.scala +++ b/tests/init/neg/inner-new.scala @@ -3,9 +3,9 @@ class Foo { def f() = count + 1 } - val a = new Inner // ok - println(a) // error + val a = new Inner // ok + println(a) // error var count = 0 - println(a) // ok + println(a) // ok } \ No newline at end of file diff --git a/tests/init/neg/insert-cold-subtype-to-array.scala b/tests/init/neg/insert-cold-subtype-to-array.scala new file mode 100644 index 000000000000..82eee1935a95 --- /dev/null +++ b/tests/init/neg/insert-cold-subtype-to-array.scala @@ -0,0 +1,8 @@ +object A: + def foo[T, S <: T](x: S, array: Array[T]): Unit = array(0) = x + +class B: + var a = new Array[B](2) + A.foo(this, a) // error + println(a(0).i) + val i = 99 \ No newline at end of file diff --git a/tests/init/neg/leak-warm.check b/tests/init/neg/leak-warm.check index 7eac95bf8de7..dd5b8cad92dc 100644 --- a/tests/init/neg/leak-warm.check +++ b/tests/init/neg/leak-warm.check @@ -1,4 +1,9 @@ --- Error: tests/init/neg/leak-warm.scala:18:26 ------------------------------------------------------------------------- -18 | val l: List[A] = List(c, d) // error - | ^^^^ - | Cannot prove that the value is fully initialized. Only initialized values may be used as arguments. +-- Error: tests/init/neg/leak-warm.scala:19:18 ------------------------------------------------------------------------- +19 | val l2 = l.map(_.m()) // error + | ^^^^^^^^^^^^ + | Call method leakWarm.l.map[leakWarm.A#B]( + | { + | def $anonfun(_$1: leakWarm.A): leakWarm.A#B = _$1.m() + | closure($anonfun) + | } + | ) on a value with an unknown initialization. diff --git a/tests/init/neg/leak-warm.scala b/tests/init/neg/leak-warm.scala index f12b22dce8d9..bc5539ce9c0b 100644 --- a/tests/init/neg/leak-warm.scala +++ b/tests/init/neg/leak-warm.scala @@ -15,6 +15,6 @@ object leakWarm { } val c = new C(1, 2) val d = new D(3, 4) - val l: List[A] = List(c, d) // error - val l2 = l.map(_.m()) + val l: List[A] = List(c, d) + val l2 = l.map(_.m()) // error } diff --git a/tests/init/neg/some-this.scala b/tests/init/neg/some-this.scala new file mode 100644 index 000000000000..f572b997c168 --- /dev/null +++ b/tests/init/neg/some-this.scala @@ -0,0 +1,2 @@ +class X: + val some = Some(this) // error \ No newline at end of file diff --git a/tests/init/neg/enum-desugared.scala b/tests/init/pos/enum-desugared.scala similarity index 94% rename from tests/init/neg/enum-desugared.scala rename to tests/init/pos/enum-desugared.scala index 8c1f3662f926..ead44cd7a939 100644 --- a/tests/init/neg/enum-desugared.scala +++ b/tests/init/pos/enum-desugared.scala @@ -14,7 +14,7 @@ object ErrorMessageID { final val NoExplanationID = $new(1, "NoExplanationID") private[this] val $values: Array[ErrorMessageID] = - Array(this.LazyErrorId, this.NoExplanationID) // error + Array(this.LazyErrorId, this.NoExplanationID) def values: Array[ErrorMessageID] = $values.clone() diff --git a/tests/init/neg/enum.scala b/tests/init/pos/enum-ordinal.scala similarity index 100% rename from tests/init/neg/enum.scala rename to tests/init/pos/enum-ordinal.scala diff --git a/tests/init/pos/inner-enum-multi-variant.scala b/tests/init/pos/inner-enum-multi-variant.scala new file mode 100644 index 000000000000..b7e27b81e819 --- /dev/null +++ b/tests/init/pos/inner-enum-multi-variant.scala @@ -0,0 +1,3 @@ +class Outer: + enum Color: + case Red, Blue \ No newline at end of file diff --git a/tests/init/pos/inner-enum.scala b/tests/init/pos/inner-enum.scala new file mode 100644 index 000000000000..120014852e14 --- /dev/null +++ b/tests/init/pos/inner-enum.scala @@ -0,0 +1,4 @@ +class Outer: + enum MyEnum { + case Case + } \ No newline at end of file