From 7b9f0cbff6ab22868faa5a5f09625db4218006bc Mon Sep 17 00:00:00 2001 From: odersky Date: Sat, 12 Jul 2025 18:03:03 +0200 Subject: [PATCH 1/4] Deskolemize after fully defining type The order was the opposite before. That led to skolem types escaping in the constraint and then being installed in the inferred type of a val or def. --- .../dotty/tools/dotc/printing/PlainPrinter.scala | 10 +++++++--- compiler/src/dotty/tools/dotc/typer/Namer.scala | 3 ++- tests/pending/pos/i23489.scala | 13 +++++++++++++ 3 files changed, 22 insertions(+), 4 deletions(-) create mode 100644 tests/pending/pos/i23489.scala diff --git a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala index f6e60cd990d6..e32935c32d1c 100644 --- a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala @@ -459,9 +459,13 @@ class PlainPrinter(_ctx: Context) extends Printer { if (idx >= 0) selfRecName(idx + 1) else "{...}.this" // TODO move underlying type to an addendum, e.g. ... z3 ... where z3: ... case tp: SkolemType => - if (homogenizedView) toText(tp.info) - else if (ctx.settings.XprintTypes.value) "<" ~ toText(tp.repr) ~ ":" ~ toText(tp.info) ~ ">" - else toText(tp.repr) + def reprStr = toText(tp.repr) ~ hashStr(tp) + if homogenizedView then + toText(tp.info) + else if ctx.settings.XprintTypes.value then + "<" ~ reprStr ~ ":" ~ toText(tp.info) ~ ">" + else + reprStr } } diff --git a/compiler/src/dotty/tools/dotc/typer/Namer.scala b/compiler/src/dotty/tools/dotc/typer/Namer.scala index e4d237072041..3fd9088e4d9b 100644 --- a/compiler/src/dotty/tools/dotc/typer/Namer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Namer.scala @@ -2245,8 +2245,9 @@ class Namer { typer: Typer => // it would be erased to BoxedUnit. def dealiasIfUnit(tp: Type) = if (tp.isRef(defn.UnitClass)) defn.UnitType else tp - def cookedRhsType = dealiasIfUnit(rhsType).deskolemized + def cookedRhsType = dealiasIfUnit(rhsType) def lhsType = fullyDefinedType(cookedRhsType, "right-hand side", mdef.srcPos) + .deskolemized //if (sym.name.toString == "y") println(i"rhs = $rhsType, cooked = $cookedRhsType") if (inherited.exists) if sym.isInlineVal || isTracked then lhsType else inherited diff --git a/tests/pending/pos/i23489.scala b/tests/pending/pos/i23489.scala new file mode 100644 index 000000000000..aba88a92fce8 --- /dev/null +++ b/tests/pending/pos/i23489.scala @@ -0,0 +1,13 @@ +import scala.language.experimental.modularity + +class Box1[T <: Singleton](val x: T) +class Box2[T : Singleton](x: => T) +def id(x: Int): x.type = x +def readInt(): Int = ??? + +def Test = () + val x = Box1(id(readInt())) + + val _: Box1[? <: Int] = x + + val y = Box2(id(readInt())) // error \ No newline at end of file From 0ee4c208f2d554387c66b68767b6965fa7e6b4df Mon Sep 17 00:00:00 2001 From: odersky Date: Sat, 12 Jul 2025 18:47:24 +0200 Subject: [PATCH 2/4] Refactoring: pass an optional argument tree to safeSubstParam --- .../tools/dotc/transform/patmat/Space.scala | 5 +++- .../dotty/tools/dotc/typer/TypeAssigner.scala | 26 ++++++++++++------- .../src/dotty/tools/dotc/typer/Typer.scala | 2 +- 3 files changed, 21 insertions(+), 12 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala b/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala index ab5885e6278c..0fd87675eb56 100644 --- a/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala +++ b/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala @@ -570,7 +570,10 @@ object SpaceEngine { // Case unapplySeq: // 1. return the type `List[T]` where `T` is the element type of the unapplySeq return type `Seq[T]` - val resTp = wildApprox(ctx.typeAssigner.safeSubstMethodParams(mt, scrutineeTp :: Nil).finalResultType) + var resTp0 = mt.resultType + if mt.isResultDependent then + resTp0 = ctx.typeAssigner.safeSubstParam(resTp0, mt.paramRefs.head, scrutineeTp) + val resTp = wildApprox(resTp0).finalResultType val sig = if (resTp.isRef(defn.BooleanClass)) diff --git a/compiler/src/dotty/tools/dotc/typer/TypeAssigner.scala b/compiler/src/dotty/tools/dotc/typer/TypeAssigner.scala index 4d16a342f484..876969fda80b 100644 --- a/compiler/src/dotty/tools/dotc/typer/TypeAssigner.scala +++ b/compiler/src/dotty/tools/dotc/typer/TypeAssigner.scala @@ -12,6 +12,7 @@ import collection.mutable import reporting.* import Checking.{checkNoPrivateLeaks, checkNoWildcard} import cc.CaptureSet +import util.Property import transform.Splicer trait TypeAssigner { @@ -273,9 +274,9 @@ trait TypeAssigner { /** Substitute argument type `argType` for parameter `pref` in type `tp`, * skolemizing the argument type if it is not stable and `pref` occurs in `tp`. */ - def safeSubstParam(tp: Type, pref: ParamRef, argType: Type)(using Context): Type = { + def safeSubstParam(tp: Type, pref: ParamRef, argType: Type, arg: Tree | Null = null)(using Context): Type = { val tp1 = tp.substParam(pref, argType) - if ((tp1 eq tp) || argType.isStable) tp1 + if (tp1 eq tp) || argType.isStable then tp1 else tp.substParam(pref, SkolemType(argType.widen)) } @@ -283,23 +284,22 @@ trait TypeAssigner { * The number of parameters `params` may exceed the number of arguments. * In this case, only the common prefix is substituted. */ - def safeSubstParams(tp: Type, params: List[ParamRef], argTypes: List[Type])(using Context): Type = argTypes match { - case argType :: argTypes1 => - val tp1 = safeSubstParam(tp, params.head, argType) - safeSubstParams(tp1, params.tail, argTypes1) + def safeSubstParams(tp: Type, params: List[ParamRef], args: List[Tree])(using Context): Type = args match + case arg :: args1 => + val tp1 = safeSubstParam(tp, params.head, arg.tpe, arg) + safeSubstParams(tp1, params.tail, args1) case Nil => tp - } - def safeSubstMethodParams(mt: MethodType, argTypes: List[Type])(using Context): Type = - if mt.isResultDependent then safeSubstParams(mt.resultType, mt.paramRefs, argTypes) + def safeSubstMethodParams(mt: MethodType, args: List[Tree])(using Context): Type = + if mt.isResultDependent then safeSubstParams(mt.resultType, mt.paramRefs, args) else mt.resultType def assignType(tree: untpd.Apply, fn: Tree, args: List[Tree])(using Context): Apply = { val ownType = fn.tpe.widen match { case fntpe: MethodType => if fntpe.paramInfos.hasSameLengthAs(args) || ctx.phase.prev.relaxedTyping then - if fntpe.isResultDependent then safeSubstMethodParams(fntpe, args.tpes) + if fntpe.isResultDependent then safeSubstMethodParams(fntpe, args) else fntpe.resultType // fast path optimization else val erroringPhase = @@ -570,6 +570,12 @@ trait TypeAssigner { } object TypeAssigner extends TypeAssigner: + + /** An attachment on an argument in an application indicating that the argument's + * type was converted to the given skolem type. + */ + private[typer] val Skolemized = new Property.StickyKey[SkolemType] + def seqLitType(tree: untpd.SeqLiteral, elemType: Type)(using Context) = tree match case tree: untpd.JavaSeqLiteral => defn.ArrayOf(elemType) case _ => if ctx.erasedTypes then defn.SeqType else defn.SeqType.appliedTo(elemType) diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index 1a9de2837196..ff88a7f6b00b 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -4355,7 +4355,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer def inferArgsAfter(leading: Tree) = val formals2 = if wtp.isParamDependent && leading.tpe.exists then - formals1.mapconserve(f1 => safeSubstParam(f1, wtp.paramRefs(argIndex), leading.tpe)) + formals1.mapconserve(f1 => safeSubstParam(f1, wtp.paramRefs(argIndex), leading.tpe, leading)) else formals1 implicitArgs(formals2, argIndex + 1, pt) From 31cd17892cd25d428f365af0e09cf216b4e8dd41 Mon Sep 17 00:00:00 2001 From: odersky Date: Sat, 12 Jul 2025 23:41:38 +0200 Subject: [PATCH 3/4] Re-use SkolemType for arguments There are at least two instances where we might skolemize the type of an argument. One is in safeSubstParams when we compute a result type of a dependent method type application. The other is in constrainIfDependentParamRef where we constrain type variables representing dependent parameters. This used to give several skolem types that ended up in a common OrType. We now store computed skolem types in attachments of argument types and re-use them if possible. --- .../dotty/tools/dotc/typer/Inferencing.scala | 4 +++- .../dotty/tools/dotc/typer/TypeAssigner.scala | 19 +++++++++++++++++-- tests/{pending => }/pos/i23489.scala | 2 +- 3 files changed, 21 insertions(+), 4 deletions(-) rename tests/{pending => }/pos/i23489.scala (86%) diff --git a/compiler/src/dotty/tools/dotc/typer/Inferencing.scala b/compiler/src/dotty/tools/dotc/typer/Inferencing.scala index 520c8bf62ba4..b8b1c00a72b3 100644 --- a/compiler/src/dotty/tools/dotc/typer/Inferencing.scala +++ b/compiler/src/dotty/tools/dotc/typer/Inferencing.scala @@ -13,6 +13,7 @@ import Decorators._ import config.Printers.{gadts, typr} import annotation.tailrec import reporting.* +import TypeAssigner.skolemizeArgType import collection.mutable import scala.annotation.internal.sharable @@ -853,7 +854,8 @@ trait Inferencing { this: Typer => val arg = findArg(call) if !arg.isEmpty then var argType = arg.tpe.widenIfUnstable - if !argType.isSingleton then argType = SkolemType(argType) + if !argType.isSingleton then + argType = skolemizeArgType(argType, arg) argType <:< tvar case _ => end constrainIfDependentParamRef diff --git a/compiler/src/dotty/tools/dotc/typer/TypeAssigner.scala b/compiler/src/dotty/tools/dotc/typer/TypeAssigner.scala index 876969fda80b..f66ce6599d42 100644 --- a/compiler/src/dotty/tools/dotc/typer/TypeAssigner.scala +++ b/compiler/src/dotty/tools/dotc/typer/TypeAssigner.scala @@ -277,7 +277,7 @@ trait TypeAssigner { def safeSubstParam(tp: Type, pref: ParamRef, argType: Type, arg: Tree | Null = null)(using Context): Type = { val tp1 = tp.substParam(pref, argType) if (tp1 eq tp) || argType.isStable then tp1 - else tp.substParam(pref, SkolemType(argType.widen)) + else tp.substParam(pref, skolemizeArgType(argType.widen, arg)) } /** Substitute types of all arguments `args` for corresponding `params` in `tp`. @@ -574,7 +574,22 @@ object TypeAssigner extends TypeAssigner: /** An attachment on an argument in an application indicating that the argument's * type was converted to the given skolem type. */ - private[typer] val Skolemized = new Property.StickyKey[SkolemType] + private val Skolemized = new Property.StickyKey[SkolemType] + + /** A skolem type wrapping `argType`, associated with `arg` if it is non-null. + * Skolem types for the same arguments with equal underlying `argType`s are re-used. + */ + def skolemizeArgType(argType: Type, arg: tpd.Tree | Null)(using Context): Type = + if arg == null then + SkolemType(argType) + else + arg.getAttachment(Skolemized) match + case Some(sk @ SkolemType(tp)) if argType frozen_=:= tp => + sk + case _ => + val sk = SkolemType(argType) + arg.putAttachment(Skolemized, sk) + sk def seqLitType(tree: untpd.SeqLiteral, elemType: Type)(using Context) = tree match case tree: untpd.JavaSeqLiteral => defn.ArrayOf(elemType) diff --git a/tests/pending/pos/i23489.scala b/tests/pos/i23489.scala similarity index 86% rename from tests/pending/pos/i23489.scala rename to tests/pos/i23489.scala index aba88a92fce8..f4215416e5b8 100644 --- a/tests/pending/pos/i23489.scala +++ b/tests/pos/i23489.scala @@ -10,4 +10,4 @@ def Test = () val _: Box1[? <: Int] = x - val y = Box2(id(readInt())) // error \ No newline at end of file + val y = Box2(id(readInt())) From bfa02e249cbd3605fd11222590f69fa0d1e981fd Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 14 Jul 2025 18:24:40 +0200 Subject: [PATCH 4/4] Refinement: Make scheme work also if arguments are duplicated The new scheme works also if argument trees are not unique. Instead of storing skolem types directly in attachment of arguments, we store a map from arguments to their skolems in the enclosing application node. But we check then that the same argument tree does not appear several times in an application. If it does appear several times, we don't generate the map. Furthermore, we are also safe if the whole application tree is used several times, since for each call we generate new skolems and store them in the map. So different calls get different skolems. --- .../tools/dotc/transform/patmat/Space.scala | 2 +- .../dotty/tools/dotc/typer/Inferencing.scala | 36 +++++++---- .../dotty/tools/dotc/typer/TypeAssigner.scala | 59 +++++++++---------- .../src/dotty/tools/dotc/typer/Typer.scala | 2 +- 4 files changed, 54 insertions(+), 45 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala b/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala index 0fd87675eb56..e11138605f19 100644 --- a/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala +++ b/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala @@ -573,7 +573,7 @@ object SpaceEngine { var resTp0 = mt.resultType if mt.isResultDependent then resTp0 = ctx.typeAssigner.safeSubstParam(resTp0, mt.paramRefs.head, scrutineeTp) - val resTp = wildApprox(resTp0).finalResultType + val resTp = wildApprox(resTp0.finalResultType) val sig = if (resTp.isRef(defn.BooleanClass)) diff --git a/compiler/src/dotty/tools/dotc/typer/Inferencing.scala b/compiler/src/dotty/tools/dotc/typer/Inferencing.scala index b8b1c00a72b3..0b94ee095975 100644 --- a/compiler/src/dotty/tools/dotc/typer/Inferencing.scala +++ b/compiler/src/dotty/tools/dotc/typer/Inferencing.scala @@ -13,7 +13,7 @@ import Decorators._ import config.Printers.{gadts, typr} import annotation.tailrec import reporting.* -import TypeAssigner.skolemizeArgType +import TypeAssigner.SkolemizedArgs import collection.mutable import scala.annotation.internal.sharable @@ -840,23 +840,33 @@ trait Inferencing { this: Typer => if tvar.origin.paramName.is(NameKinds.DepParamName) then representedParamRef(tvar.origin) match case ref: TermParamRef => - def findArg(tree: Tree)(using Context): Tree = tree match - case Apply(fn, args) => + def findArg(tree: Tree)(using Context): Option[(Tree, Apply)] = tree match + case app @ Apply(fn, args) => if fn.tpe.widen eq ref.binder then - if ref.paramNum < args.length then args(ref.paramNum) - else EmptyTree + if ref.paramNum < args.length then Some((args(ref.paramNum), app)) + else None else findArg(fn) case TypeApply(fn, _) => findArg(fn) case Block(_, expr) => findArg(expr) case Inlined(_, _, expr) => findArg(expr) - case _ => EmptyTree - - val arg = findArg(call) - if !arg.isEmpty then - var argType = arg.tpe.widenIfUnstable - if !argType.isSingleton then - argType = skolemizeArgType(argType, arg) - argType <:< tvar + case _ => None + + findArg(call) match + case Some((arg, app)) => + var argType = arg.tpe.widenIfUnstable + if !argType.isSingleton then + argType = app.getAttachment(SkolemizedArgs) match + case Some(mapping) => + mapping.get(arg) match + case Some(sk @ SkolemType(at)) => + assert(argType frozen_=:= at) + sk + case _ => + SkolemType(argType) + case _ => + SkolemType(argType) + argType <:< tvar + case _ => case _ => end constrainIfDependentParamRef } diff --git a/compiler/src/dotty/tools/dotc/typer/TypeAssigner.scala b/compiler/src/dotty/tools/dotc/typer/TypeAssigner.scala index f66ce6599d42..f1ad0f8520f1 100644 --- a/compiler/src/dotty/tools/dotc/typer/TypeAssigner.scala +++ b/compiler/src/dotty/tools/dotc/typer/TypeAssigner.scala @@ -271,35 +271,43 @@ trait TypeAssigner { untpd.cpy.Super(tree)(qual, tree.mix) .withType(superType(qual.tpe, tree.mix, mixinClass, tree.srcPos)) + private type SkolemBuffer = mutable.ListBuffer[(Tree, SkolemType)] + /** Substitute argument type `argType` for parameter `pref` in type `tp`, * skolemizing the argument type if it is not stable and `pref` occurs in `tp`. + * If skolemization happens the new SkolemType is passed to `recordSkolem` + * provided the latter is non-null. */ - def safeSubstParam(tp: Type, pref: ParamRef, argType: Type, arg: Tree | Null = null)(using Context): Type = { + def safeSubstParam(tp: Type, pref: ParamRef, argType: Type, + recordSkolem: (SkolemType => Unit) | Null = null)(using Context): Type = val tp1 = tp.substParam(pref, argType) if (tp1 eq tp) || argType.isStable then tp1 - else tp.substParam(pref, skolemizeArgType(argType.widen, arg)) - } + else + val narrowed = SkolemType(argType.widen) + if recordSkolem != null then recordSkolem(narrowed) + tp.substParam(pref, narrowed) /** Substitute types of all arguments `args` for corresponding `params` in `tp`. * The number of parameters `params` may exceed the number of arguments. * In this case, only the common prefix is substituted. + * Skolems generated by `safeSubstParam` are stored in `skolems`. */ - def safeSubstParams(tp: Type, params: List[ParamRef], args: List[Tree])(using Context): Type = args match + private def safeSubstParams(tp: Type, params: List[ParamRef], + args: List[Tree], skolems: SkolemBuffer)(using Context): Type = args match case arg :: args1 => - val tp1 = safeSubstParam(tp, params.head, arg.tpe, arg) - safeSubstParams(tp1, params.tail, args1) + val tp1 = safeSubstParam(tp, params.head, arg.tpe, sk => skolems += ((arg, sk))) + safeSubstParams(tp1, params.tail, args1, skolems) case Nil => tp - def safeSubstMethodParams(mt: MethodType, args: List[Tree])(using Context): Type = - if mt.isResultDependent then safeSubstParams(mt.resultType, mt.paramRefs, args) - else mt.resultType - def assignType(tree: untpd.Apply, fn: Tree, args: List[Tree])(using Context): Apply = { + var skolems: SkolemBuffer | Null = null val ownType = fn.tpe.widen match { case fntpe: MethodType => if fntpe.paramInfos.hasSameLengthAs(args) || ctx.phase.prev.relaxedTyping then - if fntpe.isResultDependent then safeSubstMethodParams(fntpe, args) + if fntpe.isResultDependent then + skolems = new mutable.ListBuffer() + safeSubstParams(fntpe.resultType, fntpe.paramRefs, args, skolems.nn) else fntpe.resultType // fast path optimization else val erroringPhase = @@ -312,7 +320,13 @@ trait TypeAssigner { if (ctx.settings.Ydebug.value) new FatalError("").printStackTrace() errorType(err.takesNoParamsMsg(fn, ""), tree.srcPos) } - ConstFold.Apply(tree.withType(ownType)) + val app = tree.withType(ownType) + if skolems != null + && skolems.nn.nonEmpty // @notional why is `.nn` needed here? + && skolems.nn.size == skolems.nn.toSet.size // each skolemized argument is unique + then + app.putAttachment(SkolemizedArgs, skolems.nn.toMap) + ConstFold.Apply(app) } def assignType(tree: untpd.TypeApply, fn: Tree, args: List[Tree])(using Context): TypeApply = { @@ -571,25 +585,10 @@ trait TypeAssigner { object TypeAssigner extends TypeAssigner: - /** An attachment on an argument in an application indicating that the argument's - * type was converted to the given skolem type. + /** An attachment on an application indicating a map from arguments to the skolem types + * that were created in safeSubstParams. */ - private val Skolemized = new Property.StickyKey[SkolemType] - - /** A skolem type wrapping `argType`, associated with `arg` if it is non-null. - * Skolem types for the same arguments with equal underlying `argType`s are re-used. - */ - def skolemizeArgType(argType: Type, arg: tpd.Tree | Null)(using Context): Type = - if arg == null then - SkolemType(argType) - else - arg.getAttachment(Skolemized) match - case Some(sk @ SkolemType(tp)) if argType frozen_=:= tp => - sk - case _ => - val sk = SkolemType(argType) - arg.putAttachment(Skolemized, sk) - sk + private[typer] val SkolemizedArgs = new Property.Key[Map[tpd.Tree, SkolemType]] def seqLitType(tree: untpd.SeqLiteral, elemType: Type)(using Context) = tree match case tree: untpd.JavaSeqLiteral => defn.ArrayOf(elemType) diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index ff88a7f6b00b..1a9de2837196 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -4355,7 +4355,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer def inferArgsAfter(leading: Tree) = val formals2 = if wtp.isParamDependent && leading.tpe.exists then - formals1.mapconserve(f1 => safeSubstParam(f1, wtp.paramRefs(argIndex), leading.tpe, leading)) + formals1.mapconserve(f1 => safeSubstParam(f1, wtp.paramRefs(argIndex), leading.tpe)) else formals1 implicitArgs(formals2, argIndex + 1, pt)