Skip to content

Commit fccba2b

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 16638c9 commit fccba2b

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)
@@ -711,18 +707,18 @@ trait ConstraintHandling {
711707
* The instance type is not allowed to contain references to types nested deeper
712708
* than `maxLevel`.
713709
*/
714-
def instanceType(param: TypeParamRef, fromBelow: Boolean, widenUnions: Boolean, maxLevel: Int)(using Context): Type = {
710+
def instanceType(param: TypeParamRef, fromBelow: Boolean, widen: Widen, maxLevel: Int)(using Context): Type = {
715711
val approx = approximation(param, fromBelow, maxLevel).simplified
716712
if fromBelow then
717-
val widened = widenInferred(approx, param, widenUnions)
713+
val widened = widenInferred(approx, param, widen)
718714
// Widening can add extra constraints, in particular the widened type might
719715
// be a type variable which is now instantiated to `param`, and therefore
720716
// cannot be used as an instantiation of `param` without creating a loop.
721717
// If that happens, we run `instanceType` again to find a new instantiation.
722718
// (we do not check for non-toplevel occurrences: those should never occur
723719
// since `addOneBound` disallows recursive lower bounds).
724720
if constraint.occursAtToplevel(param, widened) then
725-
instanceType(param, fromBelow, widenUnions, maxLevel)
721+
instanceType(param, fromBelow, widen, maxLevel)
726722
else
727723
widened
728724
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
@@ -3253,8 +3253,8 @@ object TypeComparer {
32533253
def subtypeCheckInProgress(using Context): Boolean =
32543254
comparing(_.subtypeCheckInProgress)
32553255

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

32593259
def approximation(param: TypeParamRef, fromBelow: Boolean, maxLevel: Int = Int.MaxValue)(using Context): Type =
32603260
comparing(_.approximation(param, fromBelow, maxLevel))
@@ -3274,8 +3274,8 @@ object TypeComparer {
32743274
def addToConstraint(tl: TypeLambda, tvars: List[TypeVar])(using Context): Boolean =
32753275
comparing(_.addToConstraint(tl, tvars))
32763276

3277-
def widenInferred(inst: Type, bound: Type, widenUnions: Boolean)(using Context): Type =
3278-
comparing(_.widenInferred(inst, bound, widenUnions))
3277+
def widenInferred(inst: Type, bound: Type, widen: Widen)(using Context): Type =
3278+
comparing(_.widenInferred(inst, bound, widen: Widen))
32793279

32803280
def dropTransparentTraits(tp: Type, bound: Type)(using Context): Type =
32813281
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
@@ -538,7 +538,7 @@ object TypeOps:
538538
val lo = TypeComparer.instanceType(
539539
tp.origin,
540540
fromBelow = variance > 0 || variance == 0 && tp.hasLowerBound,
541-
widenUnions = tp.widenUnions)(using mapCtx)
541+
tp.widenPolicy)(using mapCtx)
542542
val lo1 = apply(lo)
543543
if (lo1 ne lo) lo1 else tp
544544
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
@@ -328,6 +326,21 @@ object Types extends TypeUtils {
328326
/** Is this type a (possibly aliased) singleton type? */
329327
def isSingleton(using Context): Boolean = dealias.isInstanceOf[SingletonType]
330328

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

48624879
def origin: TypeParamRef = currentOrigin
@@ -4935,7 +4952,7 @@ object Types extends TypeUtils {
49354952
}
49364953

49374954
def typeToInstantiateWith(fromBelow: Boolean)(using Context): Type =
4938-
TypeComparer.instanceType(origin, fromBelow, widenUnions, nestingLevel)
4955+
TypeComparer.instanceType(origin, fromBelow, widenPolicy, nestingLevel)
49394956

49404957
/** Instantiate variable from the constraints over its `origin`.
49414958
* If `fromBelow` is true, the variable is instantiated to the lub
@@ -4952,7 +4969,10 @@ object Types extends TypeUtils {
49524969
instantiateWith(tp)
49534970

49544971
/** Widen unions when instantiating this variable in the current context? */
4955-
def widenUnions(using Context): Boolean = !ctx.typerState.constraint.isHard(this)
4972+
def widenPolicy(using Context): Widen =
4973+
if precise then Widen.None
4974+
else if ctx.typerState.constraint.isHard(this) then Widen.Singletons
4975+
else Widen.Unions
49564976

49574977
/** For uninstantiated type variables: the entry in the constraint (either bounds or
49584978
* provisional instance value)
@@ -4993,8 +5013,17 @@ object Types extends TypeUtils {
49935013
}
49945014
}
49955015
object TypeVar:
4996-
def apply(using Context)(initOrigin: TypeParamRef, creatorState: TyperState | Null, nestingLevel: Int = ctx.nestingLevel) =
4997-
new TypeVar(initOrigin, creatorState, nestingLevel)
5016+
def apply(using Context)(
5017+
initOrigin: TypeParamRef,
5018+
creatorState: TyperState | Null,
5019+
nestingLevel: Int = ctx.nestingLevel,
5020+
precise: Boolean = false) =
5021+
new TypeVar(initOrigin, creatorState, nestingLevel, precise)
5022+
5023+
enum Widen:
5024+
case None // no widening
5025+
case Singletons // widen singletons but not unions
5026+
case Unions // widen singletons and unions
49985027

49995028
type TypeVars = SimpleIdentitySet[TypeVar]
50005029

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

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

20772077
// Replace aliases to Unit by Unit itself. If we leave the alias in
20782078
// 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
*/
@@ -533,7 +543,7 @@ class Synthesizer(typer: Typer)(using @constructorOnly c: Context):
533543
val tparams = poly.paramRefs
534544
val variances = childClass.typeParams.map(_.paramVarianceSign)
535545
val instanceTypes = tparams.lazyZip(variances).map((tparam, variance) =>
536-
TypeComparer.instanceType(tparam, fromBelow = variance < 0, widenUnions = true)
546+
TypeComparer.instanceType(tparam, fromBelow = variance < 0, Widen.Unions)
537547
)
538548
val instanceType = resType.substParams(poly, instanceTypes)
539549
// this is broken in tests/run/i13332intersection.scala,
@@ -735,6 +745,7 @@ class Synthesizer(typer: Typer)(using @constructorOnly c: Context):
735745
defn.MirrorClass -> synthesizedMirror,
736746
defn.ManifestClass -> synthesizedManifest,
737747
defn.OptManifestClass -> synthesizedOptManifest,
748+
defn.SingletonClass -> synthesizedSingleton,
738749
)
739750

740751
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
@@ -3235,8 +3235,8 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
32353235
val app1 = typed(app, if ctx.mode.is(Mode.Pattern) then pt else defn.TupleXXLClass.typeRef)
32363236
if (ctx.mode.is(Mode.Pattern)) app1
32373237
else {
3238-
val elemTpes = elems.lazyZip(pts).map((elem, pt) =>
3239-
TypeComparer.widenInferred(elem.tpe, pt, widenUnions = true))
3238+
val elemTpes = elems.lazyZip(pts).map: (elem, pt) =>
3239+
TypeComparer.widenInferred(elem.tpe, pt, Widen.Unions)
32403240
val resTpe = TypeOps.nestedPairs(elemTpes)
32413241
app1.cast(resTpe)
32423242
}

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
@@ -71,6 +71,6 @@ object Predef:
7171
*
7272
* which is what is needed for a context bound `[A: TC]`.
7373
*/
74-
infix type is[A <: AnyKind, B <: {type Self <: AnyKind}] = B { type Self = A }
74+
infix type is[A <: AnyKind, B <: Any{type Self <: AnyKind}] = B { type Self = A }
7575

7676
end Predef

0 commit comments

Comments
 (0)