From ac644e42754a8df39fbd6c37f220fbb245cb7219 Mon Sep 17 00:00:00 2001 From: odersky Date: Thu, 14 Jul 2022 16:56:47 +0200 Subject: [PATCH 1/2] Don't ignore expected types of `New`. A problem arises if we typecheck an expression like `new C()` with expected type `C[U]` where `C` is defined like this ```scala class C[X](using X)() ``` In this case, we'd like to propagate `U` as the type instance of `X` before we resolve the using clause. To do this, we have to keep the expected result type `C[X]` for typechecking the function part `new C`. Previously, that type was wrapped in an IgnoredProto. The problem was detected now since a class C with just the using clause and no empty parameter clause was previously expanded to ```scala class C[X]()(using X) ``` but is now expanded to ```scala class C[X](using X)() ``` Under the previous expansion, we type checked and `new C()` before looking for an argument of the using clause, so the problem did not arise. --- .../dotty/tools/dotc/typer/Applications.scala | 11 +++++- .../src/dotty/tools/dotc/typer/Typer.scala | 36 +++++++++---------- tests/pos/i15664.scala | 25 +++++++++++++ 3 files changed, 52 insertions(+), 20 deletions(-) create mode 100644 tests/pos/i15664.scala diff --git a/compiler/src/dotty/tools/dotc/typer/Applications.scala b/compiler/src/dotty/tools/dotc/typer/Applications.scala index afe8bc2ff390..512c4beba934 100644 --- a/compiler/src/dotty/tools/dotc/typer/Applications.scala +++ b/compiler/src/dotty/tools/dotc/typer/Applications.scala @@ -892,8 +892,17 @@ trait Applications extends Compatibility { def typedApply(tree: untpd.Apply, pt: Type)(using Context): Tree = { def realApply(using Context): Tree = { + val resultProto = tree.fun match + case Select(New(_), _) => pt + // Don't ignore expected types of `new` expressions. If we have a `new C()` + // with expected type `C[T]` we want to use the type to instantiate `C` + // immediately. This is necessary since `C` might _also_ have using clauses + // that we want to instantiate with the best available type. See i15664.scala. + case _ => IgnoredProto(pt) + // Do ignore other expected result types, since there might be an implicit conversion + // on the result. We could drop this if we disallow unrestricted implicit conversions. val originalProto = - new FunProto(tree.args, IgnoredProto(pt))(this, tree.applyKind)(using argCtx(tree)) + new FunProto(tree.args, resultProto)(this, tree.applyKind)(using argCtx(tree)) record("typedApply") val fun1 = typedExpr(tree.fun, originalProto) diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index d28c2a9a8d3b..6a02623be953 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -3304,25 +3304,23 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer // try an implicit conversion or given extension if ctx.mode.is(Mode.ImplicitsEnabled) && !tree.name.isConstructorName && qual.tpe.isValueType then try - trace(i"try insert impl on qualifier $tree $pt") { - val selProto = selectionProto - inferView(qual, selProto) match - case SearchSuccess(found, _, _, isExtension) => - if isExtension then return found - else - checkImplicitConversionUseOK(found) - return withoutMode(Mode.ImplicitsEnabled)(typedSelect(tree, pt, found)) - case failure: SearchFailure => - if failure.isAmbiguous then - return - if !inSelect // in a selection we will do the canDefineFurther afterwards - && canDefineFurther(qual.tpe.widen) - then - tryExtensionOrConversion(tree, pt, mbrProto, qual, locked, compat, inSelect) - else - err.typeMismatch(qual, selProto, failure.reason) // TODO: report NotAMember instead, but need to be aware of failure - rememberSearchFailure(qual, failure) - } + val selProto = selectionProto + trace(i"try insert impl on qualifier $tree $pt") { inferView(qual, selProto) } match + case SearchSuccess(found, _, _, isExtension) => + if isExtension then return found + else + checkImplicitConversionUseOK(found) + return withoutMode(Mode.ImplicitsEnabled)(typedSelect(tree, pt, found)) + case failure: SearchFailure => + if failure.isAmbiguous then + return + if !inSelect // in a selection we will do the canDefineFurther afterwards + && canDefineFurther(qual.tpe.widen) + then + tryExtensionOrConversion(tree, pt, mbrProto, qual, locked, compat, inSelect) + else + err.typeMismatch(qual, selProto, failure.reason) // TODO: report NotAMember instead, but need to be aware of failure + rememberSearchFailure(qual, failure) catch case ex: TypeError => nestedFailure(ex) EmptyTree diff --git a/tests/pos/i15664.scala b/tests/pos/i15664.scala new file mode 100644 index 000000000000..7db8fe8cf23f --- /dev/null +++ b/tests/pos/i15664.scala @@ -0,0 +1,25 @@ +trait CpsMonad[F[_]]: + type Context <: CpsMonadContext[F] +type Aux[F[_], C <: CpsMonadContext[F]] = CpsMonad[F] { type Context = C } +trait CpsMonadContext[F[_]] +trait CpsMonadInstanceContext[F[_]] extends CpsMonad[F]: + type Context = CpsMonadInstanceContextBody[F] +class CpsMonadInstanceContextBody[F[_]](m: CpsMonadInstanceContext[F]) extends CpsMonadContext[F] +class InferAsyncArg[F[_], C <: CpsMonadContext[F]](using val am: Aux[F, C]) + +sealed abstract class ZManaged[-R, +E, +A] +type RManaged[-R, +A] = ZManaged[R, Throwable, A] + +type ForZManaged[R, E] = [X] =>> ZManaged[R, E, X] +given zManagedCpsMonad[R, E]: CpsMonadInstanceContext[ForZManaged[R, E]] = ??? + +// Usage +def failing[R, E](using + CpsMonad[ForZManaged[R, E]] +): InferAsyncArg[ForZManaged[R, E], CpsMonadInstanceContextBody[ForZManaged[R, E]]] = + new InferAsyncArg() + +def compiling[R, E](using + CpsMonad[ForZManaged[R, E]] +) = + new InferAsyncArg[ForZManaged[R, E], CpsMonadInstanceContextBody[ForZManaged[R, E]]] From 83f5d73ec6f5d1f308a801cf68ab03bdda05390a Mon Sep 17 00:00:00 2001 From: odersky Date: Thu, 14 Jul 2022 17:18:04 +0200 Subject: [PATCH 2/2] Do ignore expected prototypes of `New` It turns out an sjsJUnitTest fails otherwise, since that one _does_ insert an implicit conversion after a `new`. This is all goes to show that what we do to deal with implicit conversions is completely crazy. It's a black art when to propagate exected types. There's often no better or worse. Almost any change we do can heal some code and break some other code. I believe the only way to get our of this swamp is to get rid of unrestricted implicit conversions. --- compiler/src/dotty/tools/dotc/typer/Applications.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/typer/Applications.scala b/compiler/src/dotty/tools/dotc/typer/Applications.scala index 512c4beba934..53e08fcb707d 100644 --- a/compiler/src/dotty/tools/dotc/typer/Applications.scala +++ b/compiler/src/dotty/tools/dotc/typer/Applications.scala @@ -893,8 +893,8 @@ trait Applications extends Compatibility { def realApply(using Context): Tree = { val resultProto = tree.fun match - case Select(New(_), _) => pt - // Don't ignore expected types of `new` expressions. If we have a `new C()` + case Select(New(_), _) if pt.isInstanceOf[ValueType] => pt + // Don't ignore expected value types of `new` expressions. If we have a `new C()` // with expected type `C[T]` we want to use the type to instantiate `C` // immediately. This is necessary since `C` might _also_ have using clauses // that we want to instantiate with the best available type. See i15664.scala.