Skip to content

Commit 56ad7cb

Browse files
committed
Fix Singleton
Allow to constrain type variables to be singletons by a context bound [X: Singleton] instead of an unsound supertype [X <: Singleton]. This fixes the soundness hole of singletons.
1 parent d87a5dd commit 56ad7cb

File tree

13 files changed

+175
-45
lines changed

13 files changed

+175
-45
lines changed

compiler/src/dotty/tools/dotc/core/ConstraintHandling.scala

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -647,9 +647,9 @@ trait ConstraintHandling {
647647
* At this point we also drop the @Repeated annotation to avoid inferring type arguments with it,
648648
* as those could leak the annotation to users (see run/inferred-repeated-result).
649649
*/
650-
def widenInferred(inst: Type, bound: Type, widenUnions: Boolean)(using Context): Type =
650+
def widenInferred(inst: Type, bound: Type, widen: Widen)(using Context): Type =
651651
def widenOr(tp: Type) =
652-
if widenUnions then
652+
if widen == Widen.Unions then
653653
val tpw = tp.widenUnion
654654
if tpw ne tp then
655655
if tpw.isTransparent() then
@@ -667,14 +667,10 @@ trait ConstraintHandling {
667667
val tpw = tp.widenSingletons(skipSoftUnions)
668668
if (tpw ne tp) && (tpw <:< bound) then tpw else tp
669669

670-
def isSingleton(tp: Type): Boolean = tp match
671-
case WildcardType(optBounds) => optBounds.exists && isSingleton(optBounds.bounds.hi)
672-
case _ => isSubTypeWhenFrozen(tp, defn.SingletonType)
673-
674670
val wideInst =
675-
if isSingleton(bound) then inst
671+
if widen == Widen.None || bound.isSingletonBounded(frozen = true) then inst
676672
else
677-
val widenedFromSingle = widenSingle(inst, skipSoftUnions = widenUnions)
673+
val widenedFromSingle = widenSingle(inst, skipSoftUnions = widen == Widen.Unions)
678674
val widenedFromUnion = widenOr(widenedFromSingle)
679675
val widened = dropTransparentTraits(widenedFromUnion, bound)
680676
widenIrreducible(widened)
@@ -713,18 +709,18 @@ trait ConstraintHandling {
713709
* The instance type is not allowed to contain references to types nested deeper
714710
* than `maxLevel`.
715711
*/
716-
def instanceType(param: TypeParamRef, fromBelow: Boolean, widenUnions: Boolean, maxLevel: Int)(using Context): Type = {
712+
def instanceType(param: TypeParamRef, fromBelow: Boolean, widen: Widen, maxLevel: Int)(using Context): Type = {
717713
val approx = approximation(param, fromBelow, maxLevel).simplified
718714
if fromBelow then
719-
val widened = widenInferred(approx, param, widenUnions)
715+
val widened = widenInferred(approx, param, widen)
720716
// Widening can add extra constraints, in particular the widened type might
721717
// be a type variable which is now instantiated to `param`, and therefore
722718
// cannot be used as an instantiation of `param` without creating a loop.
723719
// If that happens, we run `instanceType` again to find a new instantiation.
724720
// (we do not check for non-toplevel occurrences: those should never occur
725721
// since `addOneBound` disallows recursive lower bounds).
726722
if constraint.occursAtToplevel(param, widened) then
727-
instanceType(param, fromBelow, widenUnions, maxLevel)
723+
instanceType(param, fromBelow, widen, maxLevel)
728724
else
729725
widened
730726
else

compiler/src/dotty/tools/dotc/core/Definitions.scala

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -59,10 +59,10 @@ class Definitions {
5959
private def enterCompleteClassSymbol(owner: Symbol, name: TypeName, flags: FlagSet, parents: List[TypeRef], decls: Scope) =
6060
newCompleteClassSymbol(owner, name, flags | Permanent | NoInits | Open, parents, decls).entered
6161

62-
private def enterTypeField(cls: ClassSymbol, name: TypeName, flags: FlagSet, scope: MutableScope) =
62+
private def enterTypeField(cls: ClassSymbol, name: TypeName, flags: FlagSet, scope: MutableScope): TypeSymbol =
6363
scope.enter(newPermanentSymbol(cls, name, flags, TypeBounds.empty))
6464

65-
private def enterTypeParam(cls: ClassSymbol, name: TypeName, flags: FlagSet, scope: MutableScope) =
65+
private def enterTypeParam(cls: ClassSymbol, name: TypeName, flags: FlagSet, scope: MutableScope): TypeSymbol =
6666
enterTypeField(cls, name, flags | ClassTypeParamCreationFlags, scope)
6767

6868
private def enterSyntheticTypeParam(cls: ClassSymbol, paramFlags: FlagSet, scope: MutableScope, suffix: String = "T0") =
@@ -538,9 +538,11 @@ class Definitions {
538538
@tu lazy val SingletonClass: ClassSymbol =
539539
// needed as a synthetic class because Scala 2.x refers to it in classfiles
540540
// but does not define it as an explicit class.
541-
enterCompleteClassSymbol(
542-
ScalaPackageClass, tpnme.Singleton, PureInterfaceCreationFlags | Final,
543-
List(AnyType), EmptyScope)
541+
val cls = enterCompleteClassSymbol(
542+
ScalaPackageClass, tpnme.Singleton, PureInterfaceCreationFlags | Final | Erased,
543+
List(AnyType))
544+
enterTypeField(cls, tpnme.Self, Deferred, cls.info.decls.openForMutations)
545+
cls
544546
@tu lazy val SingletonType: TypeRef = SingletonClass.typeRef
545547

546548
@tu lazy val MaybeCapabilityAnnot: ClassSymbol =

compiler/src/dotty/tools/dotc/core/TypeComparer.scala

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3257,8 +3257,8 @@ object TypeComparer {
32573257
def subtypeCheckInProgress(using Context): Boolean =
32583258
comparing(_.subtypeCheckInProgress)
32593259

3260-
def instanceType(param: TypeParamRef, fromBelow: Boolean, widenUnions: Boolean, maxLevel: Int = Int.MaxValue)(using Context): Type =
3261-
comparing(_.instanceType(param, fromBelow, widenUnions, maxLevel))
3260+
def instanceType(param: TypeParamRef, fromBelow: Boolean, widen: Widen, maxLevel: Int = Int.MaxValue)(using Context): Type =
3261+
comparing(_.instanceType(param, fromBelow, widen: Widen, maxLevel))
32623262

32633263
def approximation(param: TypeParamRef, fromBelow: Boolean, maxLevel: Int = Int.MaxValue)(using Context): Type =
32643264
comparing(_.approximation(param, fromBelow, maxLevel))
@@ -3278,8 +3278,8 @@ object TypeComparer {
32783278
def addToConstraint(tl: TypeLambda, tvars: List[TypeVar])(using Context): Boolean =
32793279
comparing(_.addToConstraint(tl, tvars))
32803280

3281-
def widenInferred(inst: Type, bound: Type, widenUnions: Boolean)(using Context): Type =
3282-
comparing(_.widenInferred(inst, bound, widenUnions))
3281+
def widenInferred(inst: Type, bound: Type, widen: Widen)(using Context): Type =
3282+
comparing(_.widenInferred(inst, bound, widen: Widen))
32833283

32843284
def dropTransparentTraits(tp: Type, bound: Type)(using Context): Type =
32853285
comparing(_.dropTransparentTraits(tp, bound))

compiler/src/dotty/tools/dotc/core/TypeOps.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -540,7 +540,7 @@ object TypeOps:
540540
val lo = TypeComparer.instanceType(
541541
tp.origin,
542542
fromBelow = variance > 0 || variance == 0 && tp.hasLowerBound,
543-
widenUnions = tp.widenUnions)(using mapCtx)
543+
tp.widenPolicy)(using mapCtx)
544544
val lo1 = apply(lo)
545545
if (lo1 ne lo) lo1 else tp
546546
case _ =>

compiler/src/dotty/tools/dotc/core/Types.scala

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,6 @@ import CaptureSet.{CompareResult, IdempotentCaptRefMap, IdentityCaptRefMap}
4444
import scala.annotation.internal.sharable
4545
import scala.annotation.threadUnsafe
4646

47-
48-
4947
object Types extends TypeUtils {
5048

5149
@sharable private var nextId = 0
@@ -329,6 +327,21 @@ object Types extends TypeUtils {
329327
/** Is this type a (possibly aliased) singleton type? */
330328
def isSingleton(using Context): Boolean = dealias.isInstanceOf[SingletonType]
331329

330+
/** Is this upper-bounded by a (possibly aliased) singleton type?
331+
* Overridden in TypeVar
332+
*/
333+
def isSingletonBounded(frozen: Boolean)(using Context): Boolean = this.dealias.normalized match
334+
case tp: SingletonType => tp.isStable
335+
case tp: TypeRef =>
336+
tp.name == tpnme.Singleton && tp.symbol == defn.SingletonClass
337+
|| tp.superType.isSingletonBounded(frozen)
338+
case tp: TypeVar if !tp.isInstantiated =>
339+
if frozen then tp frozen_<:< defn.SingletonType else tp <:< defn.SingletonType
340+
case tp: HKTypeLambda => false
341+
case tp: TypeProxy => tp.superType.isSingletonBounded(frozen)
342+
case AndType(tpL, tpR) => tpL.isSingletonBounded(frozen) || tpR.isSingletonBounded(frozen)
343+
case _ => false
344+
332345
/** Is this type of kind `AnyKind`? */
333346
def hasAnyKind(using Context): Boolean = {
334347
@tailrec def loop(tp: Type): Boolean = tp match {
@@ -4921,7 +4934,11 @@ object Types extends TypeUtils {
49214934
* @param creatorState the typer state in which the variable was created.
49224935
* @param initNestingLevel the initial nesting level of the type variable. (c.f. nestingLevel)
49234936
*/
4924-
final class TypeVar private(initOrigin: TypeParamRef, creatorState: TyperState | Null, val initNestingLevel: Int) extends CachedProxyType with ValueType {
4937+
final class TypeVar private(
4938+
initOrigin: TypeParamRef,
4939+
creatorState: TyperState | Null,
4940+
val initNestingLevel: Int,
4941+
precise: Boolean) extends CachedProxyType with ValueType {
49254942
private var currentOrigin = initOrigin
49264943

49274944
def origin: TypeParamRef = currentOrigin
@@ -5009,7 +5026,7 @@ object Types extends TypeUtils {
50095026
}
50105027

50115028
def typeToInstantiateWith(fromBelow: Boolean)(using Context): Type =
5012-
TypeComparer.instanceType(origin, fromBelow, widenUnions, nestingLevel)
5029+
TypeComparer.instanceType(origin, fromBelow, widenPolicy, nestingLevel)
50135030

50145031
/** Instantiate variable from the constraints over its `origin`.
50155032
* If `fromBelow` is true, the variable is instantiated to the lub
@@ -5026,7 +5043,10 @@ object Types extends TypeUtils {
50265043
instantiateWith(tp)
50275044

50285045
/** Widen unions when instantiating this variable in the current context? */
5029-
def widenUnions(using Context): Boolean = !ctx.typerState.constraint.isHard(this)
5046+
def widenPolicy(using Context): Widen =
5047+
if precise then Widen.None
5048+
else if ctx.typerState.constraint.isHard(this) then Widen.Singletons
5049+
else Widen.Unions
50305050

50315051
/** For uninstantiated type variables: the entry in the constraint (either bounds or
50325052
* provisional instance value)
@@ -5067,8 +5087,17 @@ object Types extends TypeUtils {
50675087
}
50685088
}
50695089
object TypeVar:
5070-
def apply(using Context)(initOrigin: TypeParamRef, creatorState: TyperState | Null, nestingLevel: Int = ctx.nestingLevel) =
5071-
new TypeVar(initOrigin, creatorState, nestingLevel)
5090+
def apply(using Context)(
5091+
initOrigin: TypeParamRef,
5092+
creatorState: TyperState | Null,
5093+
nestingLevel: Int = ctx.nestingLevel,
5094+
precise: Boolean = false) =
5095+
new TypeVar(initOrigin, creatorState, nestingLevel, precise)
5096+
5097+
enum Widen:
5098+
case None // no widening
5099+
case Singletons // widen singletons but not unions
5100+
case Unions // widen singletons and unions
50725101

50735102
type TypeVars = SimpleIdentitySet[TypeVar]
50745103

compiler/src/dotty/tools/dotc/typer/Namer.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2083,7 +2083,7 @@ class Namer { typer: Typer =>
20832083
if defaultTp.exists then TypeOps.SimplifyKeepUnchecked() else null)
20842084
match
20852085
case ctp: ConstantType if sym.isInlineVal => ctp
2086-
case tp => TypeComparer.widenInferred(tp, pt, widenUnions = true)
2086+
case tp => TypeComparer.widenInferred(tp, pt, Widen.Unions)
20872087

20882088
// Replace aliases to Unit by Unit itself. If we leave the alias in
20892089
// it would be erased to BoxedUnit.

compiler/src/dotty/tools/dotc/typer/ProtoTypes.scala

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -701,6 +701,12 @@ object ProtoTypes {
701701
case FunProto((arg: untpd.TypedSplice) :: Nil, _) => arg.isExtensionReceiver
702702
case _ => false
703703

704+
object SingletonConstrained:
705+
def unapply(tp: Type)(using Context): Option[Type] = tp.dealias match
706+
case RefinedType(parent, tpnme.Self, TypeAlias(tp))
707+
if parent.typeSymbol == defn.SingletonClass => Some(tp)
708+
case _ => None
709+
704710
/** Add all parameters of given type lambda `tl` to the constraint's domain.
705711
* If the constraint contains already some of these parameters in its domain,
706712
* make a copy of the type lambda and add the copy's type parameters instead.
@@ -713,26 +719,41 @@ object ProtoTypes {
713719
tl: TypeLambda, owningTree: untpd.Tree,
714720
alwaysAddTypeVars: Boolean,
715721
nestingLevel: Int = ctx.nestingLevel
716-
): (TypeLambda, List[TypeVar]) = {
722+
): (TypeLambda, List[TypeVar]) =
717723
val state = ctx.typerState
718724
val addTypeVars = alwaysAddTypeVars || !owningTree.isEmpty
719725
if (tl.isInstanceOf[PolyType])
720726
assert(!ctx.typerState.isCommittable || addTypeVars,
721727
s"inconsistent: no typevars were added to committable constraint ${state.constraint}")
722728
// hk type lambdas can be added to constraints without typevars during match reduction
729+
val added = state.constraint.ensureFresh(tl)
730+
731+
def singletonConstrainedRefs(tp: Type): Set[TypeParamRef] = tp match
732+
case tp: MethodType if tp.isContextualMethod =>
733+
val ownBounds =
734+
for case SingletonConstrained(ref: TypeParamRef) <- tp.paramInfos
735+
yield ref
736+
ownBounds.toSet ++ singletonConstrainedRefs(tp.resType)
737+
case tp: LambdaType =>
738+
singletonConstrainedRefs(tp.resType)
739+
case _ =>
740+
Set.empty
741+
742+
val singletonRefs = singletonConstrainedRefs(added)
743+
def isSingleton(ref: TypeParamRef) = singletonRefs.contains(ref)
723744

724-
def newTypeVars(tl: TypeLambda): List[TypeVar] =
725-
for paramRef <- tl.paramRefs
726-
yield
727-
val tvar = TypeVar(paramRef, state, nestingLevel)
745+
def newTypeVars: List[TypeVar] =
746+
for paramRef <- added.paramRefs yield
747+
val tvar = TypeVar(paramRef, state, nestingLevel, precise = isSingleton(paramRef))
728748
state.ownedVars += tvar
729749
tvar
730750

731-
val added = state.constraint.ensureFresh(tl)
732-
val tvars = if addTypeVars then newTypeVars(added) else Nil
751+
val tvars = if addTypeVars then newTypeVars else Nil
733752
TypeComparer.addToConstraint(added, tvars)
753+
for paramRef <- added.paramRefs do
754+
if isSingleton(paramRef) then paramRef <:< defn.SingletonType
734755
(added, tvars)
735-
}
756+
end constrained
736757

737758
def constrained(tl: TypeLambda, owningTree: untpd.Tree)(using Context): (TypeLambda, List[TypeVar]) =
738759
constrained(tl, owningTree,

compiler/src/dotty/tools/dotc/typer/Synthesizer.scala

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,16 @@ class Synthesizer(typer: Typer)(using @constructorOnly c: Context):
236236
EmptyTreeNoError
237237
end synthesizedValueOf
238238

239+
val synthesizedSingleton: SpecialHandler = (formal, span) => formal match
240+
case SingletonConstrained(tp) =>
241+
if tp.isSingletonBounded(frozen = false) then
242+
withNoErrors:
243+
ref(defn.Compiletime_erasedValue).appliedToType(formal).withSpan(span)
244+
else
245+
withErrors(i"$tp is not a singleton")
246+
case _ =>
247+
EmptyTreeNoError
248+
239249
/** Create an anonymous class `new Object { type MirroredMonoType = ... }`
240250
* and mark it with given attachment so that it is made into a mirror at PostTyper.
241251
*/
@@ -535,7 +545,7 @@ class Synthesizer(typer: Typer)(using @constructorOnly c: Context):
535545
val tparams = poly.paramRefs
536546
val variances = childClass.typeParams.map(_.paramVarianceSign)
537547
val instanceTypes = tparams.lazyZip(variances).map((tparam, variance) =>
538-
TypeComparer.instanceType(tparam, fromBelow = variance < 0, widenUnions = true)
548+
TypeComparer.instanceType(tparam, fromBelow = variance < 0, Widen.Unions)
539549
)
540550
val instanceType = resType.substParams(poly, instanceTypes)
541551
// this is broken in tests/run/i13332intersection.scala,
@@ -737,6 +747,7 @@ class Synthesizer(typer: Typer)(using @constructorOnly c: Context):
737747
defn.MirrorClass -> synthesizedMirror,
738748
defn.ManifestClass -> synthesizedManifest,
739749
defn.OptManifestClass -> synthesizedOptManifest,
750+
defn.SingletonClass -> synthesizedSingleton,
740751
)
741752

742753
def tryAll(formal: Type, span: Span)(using Context): TreeWithErrors =

compiler/src/dotty/tools/dotc/typer/Typer.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3239,8 +3239,8 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
32393239
val app1 = typed(app, if ctx.mode.is(Mode.Pattern) then pt else defn.TupleXXLClass.typeRef)
32403240
if (ctx.mode.is(Mode.Pattern)) app1
32413241
else {
3242-
val elemTpes = elems.lazyZip(pts).map((elem, pt) =>
3243-
TypeComparer.widenInferred(elem.tpe, pt, widenUnions = true))
3242+
val elemTpes = elems.lazyZip(pts).map: (elem, pt) =>
3243+
TypeComparer.widenInferred(elem.tpe, pt, Widen.Unions)
32443244
val resTpe = TypeOps.nestedPairs(elemTpes)
32453245
app1.cast(resTpe)
32463246
}

docs/_docs/reference/experimental/typeclasses.md

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ nightlyOf: https://docs.scala-lang.org/scala3/reference/experimental/typeclasses
77

88
# Some Proposed Changes for Better Support of Type Classes
99

10-
Martin Odersky, 8.1.2024
10+
Martin Odersky, 8.1.2024, edited 5.4.2024
1111

1212
A type class in Scala is a pattern where we define
1313

@@ -27,6 +27,8 @@ under source version `future` if the additional experimental language import `mo
2727
scala compile -source:future -language:experimental.modularity
2828
```
2929

30+
It is intended to turn features described here into proposals under the Scala improvement process. A first installment is SIP 64, which covers some syntactic changes, names for context bounds, multiple context bounds and deferred givens. The order of exposition described in this note is different from the planned proposals of SIPs. This doc is not a guide on how to sequence details, but instead wants to present a vision of what is possible. For instance, we start here with a feature (Self types and `is` syntax) that has turned out to be controversial and that will probably be proposed only late in the sequence of SIPs.
31+
3032
## Generalizing Context Bounds
3133

3234
The only place in Scala's syntax where the type class pattern is relevant is
@@ -54,6 +56,8 @@ requires that `Ordering` is a trait or class with a single type parameter (which
5456

5557
trait Monoid extends SemiGroup:
5658
def unit: Self
59+
object Monoid:
60+
def unit[M](using m: Monoid { type Self = M}): M
5761

5862
trait Functor:
5963
type Self[A]
@@ -129,14 +133,17 @@ We introduce a standard type alias `is` in the Scala package or in `Predef`, def
129133
infix type is[A <: AnyKind, B <: {type Self <: AnyKind}] = B { type Self = A }
130134
```
131135

132-
This makes writing instance definitions quite pleasant. Examples:
136+
This makes writing instance definitions and using clauses quite pleasant. Examples:
133137

134138
```scala
135139
given Int is Ord ...
136140
given Int is Monoid ...
137141

138142
type Reader = [X] =>> Env => X
139143
given Reader is Monad ...
144+
145+
object Monoid:
146+
def unit[M](using m: M is Monoid): M
140147
```
141148

142149
(more examples will follow below)
@@ -682,7 +689,7 @@ With the improvements proposed here, the library can now be expressed quite clea
682689

683690
## Suggested Improvements unrelated to Type Classes
684691

685-
The following improvements elsewhere would make sense alongside the suggested changes to type classes. But they are currently not part of this proposal or implementation.
692+
The following two improvements elsewhere would make sense alongside the suggested changes to type classes. But only the first (fixing singleton) forms a part of this proposal and is implemented.
686693

687694
### Fixing Singleton
688695

@@ -704,7 +711,7 @@ Then, instead of using an unsound upper bound we can use a context bound:
704711
def f[X: Singleton](x: X) = ...
705712
```
706713

707-
The context bound would be treated specially by the compiler so that no using clause is generated at runtime.
714+
The context bound is treated specially by the compiler so that no using clause is generated at runtime (this is straightforward, using the erased definitions mechanism).
708715

709716
_Aside_: This can also lead to a solution how to express precise type variables. We can introduce another special type class `Precise` and use it like this:
710717

library/src/scala/runtime/stdLibPatches/Predef.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,6 @@ object Predef:
7373
*
7474
* which is what is needed for a context bound `[A: TC]`.
7575
*/
76-
infix type is[A <: AnyKind, B <: {type Self <: AnyKind}] = B { type Self = A }
76+
infix type is[A <: AnyKind, B <: Any{type Self <: AnyKind}] = B { type Self = A }
7777

7878
end Predef

0 commit comments

Comments
 (0)