Skip to content

Commit f713652

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 f444b46 commit f713652

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
@@ -545,7 +545,7 @@ object TypeOps:
545545
val lo = TypeComparer.instanceType(
546546
tp.origin,
547547
fromBelow = variance > 0 || variance == 0 && tp.hasLowerBound,
548-
widenUnions = tp.widenUnions)(using mapCtx)
548+
tp.widenPolicy)(using mapCtx)
549549
val lo1 = apply(lo)
550550
if (lo1 ne lo) lo1 else tp
551551
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
@@ -330,6 +328,21 @@ object Types extends TypeUtils {
330328
/** Is this type a (possibly aliased) singleton type? */
331329
def isSingleton(using Context): Boolean = dealias.isInstanceOf[SingletonType]
332330

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

49304947
def origin: TypeParamRef = currentOrigin
@@ -5012,7 +5029,7 @@ object Types extends TypeUtils {
50125029
}
50135030

50145031
def typeToInstantiateWith(fromBelow: Boolean)(using Context): Type =
5015-
TypeComparer.instanceType(origin, fromBelow, widenUnions, nestingLevel)
5032+
TypeComparer.instanceType(origin, fromBelow, widenPolicy, nestingLevel)
50165033

50175034
/** Instantiate variable from the constraints over its `origin`.
50185035
* If `fromBelow` is true, the variable is instantiated to the lub
@@ -5029,7 +5046,10 @@ object Types extends TypeUtils {
50295046
instantiateWith(tp)
50305047

50315048
/** Widen unions when instantiating this variable in the current context? */
5032-
def widenUnions(using Context): Boolean = !ctx.typerState.constraint.isHard(this)
5049+
def widenPolicy(using Context): Widen =
5050+
if precise then Widen.None
5051+
else if ctx.typerState.constraint.isHard(this) then Widen.Singletons
5052+
else Widen.Unions
50335053

50345054
/** For uninstantiated type variables: the entry in the constraint (either bounds or
50355055
* provisional instance value)
@@ -5070,8 +5090,17 @@ object Types extends TypeUtils {
50705090
}
50715091
}
50725092
object TypeVar:
5073-
def apply(using Context)(initOrigin: TypeParamRef, creatorState: TyperState | Null, nestingLevel: Int = ctx.nestingLevel) =
5074-
new TypeVar(initOrigin, creatorState, nestingLevel)
5093+
def apply(using Context)(
5094+
initOrigin: TypeParamRef,
5095+
creatorState: TyperState | Null,
5096+
nestingLevel: Int = ctx.nestingLevel,
5097+
precise: Boolean = false) =
5098+
new TypeVar(initOrigin, creatorState, nestingLevel, precise)
5099+
5100+
enum Widen:
5101+
case None // no widening
5102+
case Singletons // widen singletons but not unions
5103+
case Unions // widen singletons and unions
50755104

50765105
type TypeVars = SimpleIdentitySet[TypeVar]
50775106

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

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

20922092
// Replace aliases to Unit by Unit itself. If we leave the alias in
20932093
// 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
@@ -237,6 +237,16 @@ class Synthesizer(typer: Typer)(using @constructorOnly c: Context):
237237
EmptyTreeNoError
238238
end synthesizedValueOf
239239

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

743754
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
@@ -3321,8 +3321,8 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
33213321
val app1 = typed(app, if ctx.mode.is(Mode.Pattern) then pt else defn.TupleXXLClass.typeRef)
33223322
if ctx.mode.is(Mode.Pattern) then app1
33233323
else
3324-
val elemTpes = elems.lazyZip(pts).map((elem, pt) =>
3325-
TypeComparer.widenInferred(elem.tpe, pt, widenUnions = true))
3324+
val elemTpes = elems.lazyZip(pts).map: (elem, pt) =>
3325+
TypeComparer.widenInferred(elem.tpe, pt, Widen.Unions)
33263326
val resTpe = TypeOps.nestedPairs(elemTpes)
33273327
app1.cast(resTpe)
33283328

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
@@ -77,6 +77,6 @@ object Predef:
7777
*
7878
* which is what is needed for a context bound `[A: TC]`.
7979
*/
80-
infix type is[A <: AnyKind, B <: {type Self <: AnyKind}] = B { type Self = A }
80+
infix type is[A <: AnyKind, B <: Any{type Self <: AnyKind}] = B { type Self = A }
8181

8282
end Predef

0 commit comments

Comments
 (0)