Skip to content

[Experiment] Introduce hard ConstantTypes #14360

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions compiler/src/dotty/tools/dotc/core/Hashable.scala
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,9 @@ trait Hashable {
protected final def doHash(bs: Binders, x1: Any, tp2: Type, tps3: List[Type]): Int =
finishHash(bs, hashing.mix(hashSeed, x1.hashCode), 1, tp2, tps3)

protected final def doHash(x1: Any, x2: Int): Int =
finishHash(hashing.mix(hashing.mix(hashSeed, x1.hashCode), x2), 1)

protected final def doHash(x1: Int, x2: Int): Int =
finishHash(hashing.mix(hashing.mix(hashSeed, x1), x2), 1)

Expand Down
32 changes: 22 additions & 10 deletions compiler/src/dotty/tools/dotc/core/Types.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1217,7 +1217,7 @@ object Types {
case _: TypeRef | _: MethodOrPoly => this // fast path for most frequent cases
case tp: TermRef => // fast path for next most frequent case
if tp.isOverloaded then tp else tp.underlying.widen
case tp: SingletonType => tp.underlying.widen
case tp: SingletonType if tp.isSoft => tp.underlying.widen
case tp: ExprType => tp.resultType.widen
case tp =>
val tp1 = tp.stripped
Expand All @@ -1230,7 +1230,7 @@ object Types {
* base type by applying one or more `underlying` dereferences.
*/
final def widenSingleton(using Context): Type = stripped match {
case tp: SingletonType if !tp.isOverloaded => tp.underlying.widenSingleton
case tp: SingletonType if tp.isSoft && !tp.isOverloaded => tp.underlying.widenSingleton
case _ => this
}

Expand Down Expand Up @@ -2025,8 +2025,12 @@ object Types {
/** A marker trait for types that are guaranteed to contain only a
* single non-null value (they might contain null in addition).
*/
trait SingletonType extends TypeProxy with ValueType {
trait SingletonType extends TypeProxy with ValueType with Softenable {
def isOverloaded(using Context): Boolean = false

/** Overriden in [[ConstantType]].
*/
override def isSoft = true
}

/** A trait for types that bind other types that refer to them.
Expand Down Expand Up @@ -2074,6 +2078,10 @@ object Types {
}
}

trait Softenable {
Copy link
Member Author

@mbovel mbovel Jan 26, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need a trait for that? Or could isSoft be a method of Type directly, returning true by default?

To do:

  • Find a better name
  • Documentation

Copy link
Member

@dwijnand dwijnand Jan 27, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's never consumed as its upper type, and there's no "shared implementation". So I'd just define/redefine it on SingletonType/ConstantType, and maybe link the docs if they're extensive. Just IMO 😄

Oh, otherwise I'd call it Softness or just IsSoft.

def isSoft: Boolean
}

// --- NamedTypes ------------------------------------------------------------------

abstract class NamedType extends CachedProxyType with ValueType { self =>
Expand Down Expand Up @@ -2855,15 +2863,15 @@ object Types {
abstract case class ConstantType(value: Constant) extends CachedProxyType with SingletonType {
override def underlying(using Context): Type = value.tpe

override def computeHash(bs: Binders): Int = doHash(value)
override def computeHash(bs: Binders): Int = doHash(value, if isSoft then 0 else 1)
}

final class CachedConstantType(value: Constant) extends ConstantType(value)
final class CachedConstantType(value: Constant, override val isSoft: Boolean = true) extends ConstantType(value)

object ConstantType {
def apply(value: Constant)(using Context): ConstantType = {
def apply(value: Constant, soft: Boolean = true)(using Context): ConstantType = {
assertUnerased()
unique(new CachedConstantType(value))
unique(new CachedConstantType(value, soft))
}
}

Expand Down Expand Up @@ -3206,9 +3214,8 @@ object Types {
TypeComparer.liftIfHK(tp1, tp2, AndType.make(_, _, checkValid = false), makeHk, _ | _)
}

abstract case class OrType(tp1: Type, tp2: Type) extends AndOrType {
abstract case class OrType(tp1: Type, tp2: Type) extends AndOrType, Softenable {
def isAnd: Boolean = false
def isSoft: Boolean
private var myBaseClassesPeriod: Period = Nowhere
private var myBaseClasses: List[ClassSymbol] = _
/** Base classes of are the intersection of the operand base classes. */
Expand Down Expand Up @@ -3282,7 +3289,12 @@ object Types {
else tp1.atoms | tp2.atoms
val tp1w = tp1.widenSingletons
val tp2w = tp2.widenSingletons
myWidened = if ((tp1 eq tp1w) && (tp2 eq tp2w)) this else tp1w | tp2w
myWidened =
if isSoft then
Copy link
Member Author

@mbovel mbovel Jan 26, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With this change, "a" |<hard> String would not be widened to String anymore. I could add something like || tp1w <:< tp2w || tp2w <:< tp1w here if we want that.

if ((tp1 eq tp1w) && (tp2 eq tp2w)) this else tp1w | tp2w
else
derivedOrType(tp1w, tp2w)

atomsRunId = ctx.runId

override def atoms(using Context): Atoms =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -438,7 +438,7 @@ class TreeUnpickler(reader: TastyReader,
case BYNAMEtype =>
ExprType(readType())
case _ =>
ConstantType(readConstant(tag))
ConstantType(readConstant(tag), soft = false)
}

if (tag < firstLengthTreeTag) readSimpleType() else readLengthType()
Expand Down
2 changes: 1 addition & 1 deletion compiler/src/dotty/tools/dotc/typer/Implicits.scala
Original file line number Diff line number Diff line change
Expand Up @@ -935,7 +935,7 @@ trait Implicits:
case TypeBounds(lo, hi) if lo.ne(hi) && !t.symbol.is(Opaque) => apply(hi)
case _ => t
}
case t: SingletonType =>
case t: SingletonType if t.isSoft =>
apply(t.widen)
case t: RefinedType =>
apply(t.parent)
Expand Down
7 changes: 6 additions & 1 deletion compiler/src/dotty/tools/dotc/typer/Typer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1879,7 +1879,12 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
def typedSingletonTypeTree(tree: untpd.SingletonTypeTree)(using Context): SingletonTypeTree = {
val ref1 = typedExpr(tree.ref)
checkStable(ref1.tpe, tree.srcPos, "singleton type")
assignType(cpy.SingletonTypeTree(tree)(ref1), ref1)

val ref2 = ref1.tpe match
case ConstantType(c) => ref1.withType(ConstantType(c, soft = false))
case _ => ref1

assignType(cpy.SingletonTypeTree(tree)(ref2), ref2)
}

def typedRefinedTypeTree(tree: untpd.RefinedTypeTree)(using Context): TypTree = {
Expand Down
12 changes: 12 additions & 0 deletions tests/pos/widen-singletons.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
object Test:
def is2(x: 2) = true

def testValType() =
val x: 2 = 2
val v = x
is2(v)

def testDefReturnType() =
def f(): 2 = 2
val v = f()
is2(v)
20 changes: 19 additions & 1 deletion tests/pos/widen-union.scala
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,30 @@ object Test2:
|| xs.corresponds(ys)(consistent(_, _)) // error, found: Any, required: Int | String

object Test3:

def g[X](x: X | String): Int = ???
def y: Boolean | String = ???
g[Boolean](y)
g(y)
g[Boolean](identity(y))
g(identity(y))

object TestSingletonsInUnions:
def is2Or3(a: 2 | 3) = true

def testValType() =
val x: 2 | 3 = 2
val v = x
is2Or3(v)

def testDefReturnType() =
def f(): 2 | 3 = 2
val v = f()
is2Or3(v)

def testSoftUnionInHardUnion() =
def isStringOr3(a: String | 3) = true

def f(x: String): x.type | 3 = 3
val b: Boolean = true
val v = f(if b then "a" else "b")
isStringOr3(v)