Skip to content

Commit

Permalink
Fix Singleton
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
odersky committed Apr 5, 2024
1 parent 16638c9 commit fccba2b
Show file tree
Hide file tree
Showing 13 changed files with 175 additions and 45 deletions.
18 changes: 7 additions & 11 deletions compiler/src/dotty/tools/dotc/core/ConstraintHandling.scala
Original file line number Diff line number Diff line change
Expand Up @@ -647,9 +647,9 @@ trait ConstraintHandling {
* At this point we also drop the @Repeated annotation to avoid inferring type arguments with it,
* as those could leak the annotation to users (see run/inferred-repeated-result).
*/
def widenInferred(inst: Type, bound: Type, widenUnions: Boolean)(using Context): Type =
def widenInferred(inst: Type, bound: Type, widen: Widen)(using Context): Type =
def widenOr(tp: Type) =
if widenUnions then
if widen == Widen.Unions then
val tpw = tp.widenUnion
if tpw ne tp then
if tpw.isTransparent() then
Expand All @@ -667,14 +667,10 @@ trait ConstraintHandling {
val tpw = tp.widenSingletons(skipSoftUnions)
if (tpw ne tp) && (tpw <:< bound) then tpw else tp

def isSingleton(tp: Type): Boolean = tp match
case WildcardType(optBounds) => optBounds.exists && isSingleton(optBounds.bounds.hi)
case _ => isSubTypeWhenFrozen(tp, defn.SingletonType)

val wideInst =
if isSingleton(bound) then inst
if widen == Widen.None || bound.isSingletonBounded(frozen = true) then inst
else
val widenedFromSingle = widenSingle(inst, skipSoftUnions = widenUnions)
val widenedFromSingle = widenSingle(inst, skipSoftUnions = widen == Widen.Unions)
val widenedFromUnion = widenOr(widenedFromSingle)
val widened = dropTransparentTraits(widenedFromUnion, bound)
widenIrreducible(widened)
Expand Down Expand Up @@ -711,18 +707,18 @@ trait ConstraintHandling {
* The instance type is not allowed to contain references to types nested deeper
* than `maxLevel`.
*/
def instanceType(param: TypeParamRef, fromBelow: Boolean, widenUnions: Boolean, maxLevel: Int)(using Context): Type = {
def instanceType(param: TypeParamRef, fromBelow: Boolean, widen: Widen, maxLevel: Int)(using Context): Type = {
val approx = approximation(param, fromBelow, maxLevel).simplified
if fromBelow then
val widened = widenInferred(approx, param, widenUnions)
val widened = widenInferred(approx, param, widen)
// Widening can add extra constraints, in particular the widened type might
// be a type variable which is now instantiated to `param`, and therefore
// cannot be used as an instantiation of `param` without creating a loop.
// If that happens, we run `instanceType` again to find a new instantiation.
// (we do not check for non-toplevel occurrences: those should never occur
// since `addOneBound` disallows recursive lower bounds).
if constraint.occursAtToplevel(param, widened) then
instanceType(param, fromBelow, widenUnions, maxLevel)
instanceType(param, fromBelow, widen, maxLevel)
else
widened
else
Expand Down
12 changes: 7 additions & 5 deletions compiler/src/dotty/tools/dotc/core/Definitions.scala
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,10 @@ class Definitions {
private def enterCompleteClassSymbol(owner: Symbol, name: TypeName, flags: FlagSet, parents: List[TypeRef], decls: Scope) =
newCompleteClassSymbol(owner, name, flags | Permanent | NoInits | Open, parents, decls).entered

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

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

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

@tu lazy val MaybeCapabilityAnnot: ClassSymbol =
Expand Down
8 changes: 4 additions & 4 deletions compiler/src/dotty/tools/dotc/core/TypeComparer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3253,8 +3253,8 @@ object TypeComparer {
def subtypeCheckInProgress(using Context): Boolean =
comparing(_.subtypeCheckInProgress)

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

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

def widenInferred(inst: Type, bound: Type, widenUnions: Boolean)(using Context): Type =
comparing(_.widenInferred(inst, bound, widenUnions))
def widenInferred(inst: Type, bound: Type, widen: Widen)(using Context): Type =
comparing(_.widenInferred(inst, bound, widen: Widen))

def dropTransparentTraits(tp: Type, bound: Type)(using Context): Type =
comparing(_.dropTransparentTraits(tp, bound))
Expand Down
2 changes: 1 addition & 1 deletion compiler/src/dotty/tools/dotc/core/TypeOps.scala
Original file line number Diff line number Diff line change
Expand Up @@ -538,7 +538,7 @@ object TypeOps:
val lo = TypeComparer.instanceType(
tp.origin,
fromBelow = variance > 0 || variance == 0 && tp.hasLowerBound,
widenUnions = tp.widenUnions)(using mapCtx)
tp.widenPolicy)(using mapCtx)
val lo1 = apply(lo)
if (lo1 ne lo) lo1 else tp
case _ =>
Expand Down
43 changes: 36 additions & 7 deletions compiler/src/dotty/tools/dotc/core/Types.scala
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,6 @@ import CaptureSet.{CompareResult, IdempotentCaptRefMap, IdentityCaptRefMap}
import scala.annotation.internal.sharable
import scala.annotation.threadUnsafe



object Types extends TypeUtils {

@sharable private var nextId = 0
Expand Down Expand Up @@ -328,6 +326,21 @@ object Types extends TypeUtils {
/** Is this type a (possibly aliased) singleton type? */
def isSingleton(using Context): Boolean = dealias.isInstanceOf[SingletonType]

/** Is this upper-bounded by a (possibly aliased) singleton type?
* Overridden in TypeVar
*/
def isSingletonBounded(frozen: Boolean)(using Context): Boolean = this.dealias.normalized match
case tp: SingletonType => tp.isStable
case tp: TypeRef =>
tp.name == tpnme.Singleton && tp.symbol == defn.SingletonClass
|| tp.superType.isSingletonBounded(frozen)
case tp: TypeVar if !tp.isInstantiated =>
if frozen then tp frozen_<:< defn.SingletonType else tp <:< defn.SingletonType
case tp: HKTypeLambda => false
case tp: TypeProxy => tp.superType.isSingletonBounded(frozen)
case AndType(tpL, tpR) => tpL.isSingletonBounded(frozen) || tpR.isSingletonBounded(frozen)
case _ => false

/** Is this type of kind `AnyKind`? */
def hasAnyKind(using Context): Boolean = {
@tailrec def loop(tp: Type): Boolean = tp match {
Expand Down Expand Up @@ -4856,7 +4869,11 @@ object Types extends TypeUtils {
* @param creatorState the typer state in which the variable was created.
* @param initNestingLevel the initial nesting level of the type variable. (c.f. nestingLevel)
*/
final class TypeVar private(initOrigin: TypeParamRef, creatorState: TyperState | Null, val initNestingLevel: Int) extends CachedProxyType with ValueType {
final class TypeVar private(
initOrigin: TypeParamRef,
creatorState: TyperState | Null,
val initNestingLevel: Int,
precise: Boolean) extends CachedProxyType with ValueType {
private var currentOrigin = initOrigin

def origin: TypeParamRef = currentOrigin
Expand Down Expand Up @@ -4935,7 +4952,7 @@ object Types extends TypeUtils {
}

def typeToInstantiateWith(fromBelow: Boolean)(using Context): Type =
TypeComparer.instanceType(origin, fromBelow, widenUnions, nestingLevel)
TypeComparer.instanceType(origin, fromBelow, widenPolicy, nestingLevel)

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

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

/** For uninstantiated type variables: the entry in the constraint (either bounds or
* provisional instance value)
Expand Down Expand Up @@ -4993,8 +5013,17 @@ object Types extends TypeUtils {
}
}
object TypeVar:
def apply(using Context)(initOrigin: TypeParamRef, creatorState: TyperState | Null, nestingLevel: Int = ctx.nestingLevel) =
new TypeVar(initOrigin, creatorState, nestingLevel)
def apply(using Context)(
initOrigin: TypeParamRef,
creatorState: TyperState | Null,
nestingLevel: Int = ctx.nestingLevel,
precise: Boolean = false) =
new TypeVar(initOrigin, creatorState, nestingLevel, precise)

enum Widen:
case None // no widening
case Singletons // widen singletons but not unions
case Unions // widen singletons and unions

type TypeVars = SimpleIdentitySet[TypeVar]

Expand Down
2 changes: 1 addition & 1 deletion compiler/src/dotty/tools/dotc/typer/Namer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2072,7 +2072,7 @@ class Namer { typer: Typer =>
if defaultTp.exists then TypeOps.SimplifyKeepUnchecked() else null)
match
case ctp: ConstantType if sym.isInlineVal => ctp
case tp => TypeComparer.widenInferred(tp, pt, widenUnions = true)
case tp => TypeComparer.widenInferred(tp, pt, Widen.Unions)

// Replace aliases to Unit by Unit itself. If we leave the alias in
// it would be erased to BoxedUnit.
Expand Down
37 changes: 29 additions & 8 deletions compiler/src/dotty/tools/dotc/typer/ProtoTypes.scala
Original file line number Diff line number Diff line change
Expand Up @@ -701,6 +701,12 @@ object ProtoTypes {
case FunProto((arg: untpd.TypedSplice) :: Nil, _) => arg.isExtensionReceiver
case _ => false

object SingletonConstrained:
def unapply(tp: Type)(using Context): Option[Type] = tp.dealias match
case RefinedType(parent, tpnme.Self, TypeAlias(tp))
if parent.typeSymbol == defn.SingletonClass => Some(tp)
case _ => None

/** Add all parameters of given type lambda `tl` to the constraint's domain.
* If the constraint contains already some of these parameters in its domain,
* make a copy of the type lambda and add the copy's type parameters instead.
Expand All @@ -713,26 +719,41 @@ object ProtoTypes {
tl: TypeLambda, owningTree: untpd.Tree,
alwaysAddTypeVars: Boolean,
nestingLevel: Int = ctx.nestingLevel
): (TypeLambda, List[TypeVar]) = {
): (TypeLambda, List[TypeVar]) =
val state = ctx.typerState
val addTypeVars = alwaysAddTypeVars || !owningTree.isEmpty
if (tl.isInstanceOf[PolyType])
assert(!ctx.typerState.isCommittable || addTypeVars,
s"inconsistent: no typevars were added to committable constraint ${state.constraint}")
// hk type lambdas can be added to constraints without typevars during match reduction
val added = state.constraint.ensureFresh(tl)

def singletonConstrainedRefs(tp: Type): Set[TypeParamRef] = tp match
case tp: MethodType if tp.isContextualMethod =>
val ownBounds =
for case SingletonConstrained(ref: TypeParamRef) <- tp.paramInfos
yield ref
ownBounds.toSet ++ singletonConstrainedRefs(tp.resType)
case tp: LambdaType =>
singletonConstrainedRefs(tp.resType)
case _ =>
Set.empty

val singletonRefs = singletonConstrainedRefs(added)
def isSingleton(ref: TypeParamRef) = singletonRefs.contains(ref)

def newTypeVars(tl: TypeLambda): List[TypeVar] =
for paramRef <- tl.paramRefs
yield
val tvar = TypeVar(paramRef, state, nestingLevel)
def newTypeVars: List[TypeVar] =
for paramRef <- added.paramRefs yield
val tvar = TypeVar(paramRef, state, nestingLevel, precise = isSingleton(paramRef))
state.ownedVars += tvar
tvar

val added = state.constraint.ensureFresh(tl)
val tvars = if addTypeVars then newTypeVars(added) else Nil
val tvars = if addTypeVars then newTypeVars else Nil
TypeComparer.addToConstraint(added, tvars)
for paramRef <- added.paramRefs do
if isSingleton(paramRef) then paramRef <:< defn.SingletonType
(added, tvars)
}
end constrained

def constrained(tl: TypeLambda, owningTree: untpd.Tree)(using Context): (TypeLambda, List[TypeVar]) =
constrained(tl, owningTree,
Expand Down
13 changes: 12 additions & 1 deletion compiler/src/dotty/tools/dotc/typer/Synthesizer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,16 @@ class Synthesizer(typer: Typer)(using @constructorOnly c: Context):
EmptyTreeNoError
end synthesizedValueOf

val synthesizedSingleton: SpecialHandler = (formal, span) => formal match
case SingletonConstrained(tp) =>
if tp.isSingletonBounded(frozen = false) then
withNoErrors:
ref(defn.Compiletime_erasedValue).appliedToType(formal).withSpan(span)
else
withErrors(i"$tp is not a singleton")
case _ =>
EmptyTreeNoError

/** Create an anonymous class `new Object { type MirroredMonoType = ... }`
* and mark it with given attachment so that it is made into a mirror at PostTyper.
*/
Expand Down Expand Up @@ -533,7 +543,7 @@ class Synthesizer(typer: Typer)(using @constructorOnly c: Context):
val tparams = poly.paramRefs
val variances = childClass.typeParams.map(_.paramVarianceSign)
val instanceTypes = tparams.lazyZip(variances).map((tparam, variance) =>
TypeComparer.instanceType(tparam, fromBelow = variance < 0, widenUnions = true)
TypeComparer.instanceType(tparam, fromBelow = variance < 0, Widen.Unions)
)
val instanceType = resType.substParams(poly, instanceTypes)
// this is broken in tests/run/i13332intersection.scala,
Expand Down Expand Up @@ -735,6 +745,7 @@ class Synthesizer(typer: Typer)(using @constructorOnly c: Context):
defn.MirrorClass -> synthesizedMirror,
defn.ManifestClass -> synthesizedManifest,
defn.OptManifestClass -> synthesizedOptManifest,
defn.SingletonClass -> synthesizedSingleton,
)

def tryAll(formal: Type, span: Span)(using Context): TreeWithErrors =
Expand Down
4 changes: 2 additions & 2 deletions compiler/src/dotty/tools/dotc/typer/Typer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3235,8 +3235,8 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
val app1 = typed(app, if ctx.mode.is(Mode.Pattern) then pt else defn.TupleXXLClass.typeRef)
if (ctx.mode.is(Mode.Pattern)) app1
else {
val elemTpes = elems.lazyZip(pts).map((elem, pt) =>
TypeComparer.widenInferred(elem.tpe, pt, widenUnions = true))
val elemTpes = elems.lazyZip(pts).map: (elem, pt) =>
TypeComparer.widenInferred(elem.tpe, pt, Widen.Unions)
val resTpe = TypeOps.nestedPairs(elemTpes)
app1.cast(resTpe)
}
Expand Down
15 changes: 11 additions & 4 deletions docs/_docs/reference/experimental/typeclasses.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ nightlyOf: https://docs.scala-lang.org/scala3/reference/experimental/typeclasses

# Some Proposed Changes for Better Support of Type Classes

Martin Odersky, 8.1.2024
Martin Odersky, 8.1.2024, edited 5.4.2024

A type class in Scala is a pattern where we define

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

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.

## Generalizing Context Bounds

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

trait Monoid extends SemiGroup:
def unit: Self
object Monoid:
def unit[M](using m: Monoid { type Self = M}): M

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

This makes writing instance definitions quite pleasant. Examples:
This makes writing instance definitions and using clauses quite pleasant. Examples:

```scala
given Int is Ord ...
given Int is Monoid ...

type Reader = [X] =>> Env => X
given Reader is Monad ...

object Monoid:
def unit[M](using m: M is Monoid): M
```

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

## Suggested Improvements unrelated to Type Classes

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.
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.

### Fixing Singleton

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

The context bound would be treated specially by the compiler so that no using clause is generated at runtime.
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).

_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:

Expand Down
2 changes: 1 addition & 1 deletion library/src/scala/runtime/stdLibPatches/Predef.scala
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,6 @@ object Predef:
*
* which is what is needed for a context bound `[A: TC]`.
*/
infix type is[A <: AnyKind, B <: {type Self <: AnyKind}] = B { type Self = A }
infix type is[A <: AnyKind, B <: Any{type Self <: AnyKind}] = B { type Self = A }

end Predef
Loading

0 comments on commit fccba2b

Please sign in to comment.