Skip to content

[Experiment] Qualified Types #21586

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

Draft
wants to merge 13 commits into
base: main
Choose a base branch
from
Draft
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
61 changes: 59 additions & 2 deletions compiler/src/dotty/tools/dotc/ast/Desugar.scala
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import Symbols.*, StdNames.*, Trees.*, ContextOps.*
import Decorators.*
import Annotations.Annotation
import NameKinds.{UniqueName, ContextBoundParamName, ContextFunctionParamName, DefaultGetterName, WildcardParamName}
import typer.{Namer, Checking}
import typer.{Namer, Checking, ErrorReporting}
import util.{Property, SourceFile, SourcePosition, SrcPos, Chars}
import config.{Feature, Config}
import config.Feature.{sourceVersion, migrateTo3, enabled}
Expand Down Expand Up @@ -214,9 +214,10 @@ object desugar {
def valDef(vdef0: ValDef)(using Context): Tree =
val vdef @ ValDef(_, tpt, rhs) = vdef0
val valName = normalizeName(vdef, tpt).asTermName
val tpt1 = desugarQualifiedTypes(tpt, valName)
var mods1 = vdef.mods

val vdef1 = cpy.ValDef(vdef)(name = valName).withMods(mods1)
val vdef1 = cpy.ValDef(vdef)(name = valName, tpt = tpt1).withMods(mods1)

if isSetterNeeded(vdef) then
val setterParam = makeSyntheticParameter(tpt = SetterParamTree().watching(vdef))
Expand All @@ -233,6 +234,14 @@ object desugar {
else vdef1
end valDef

def caseDef(cdef: CaseDef)(using Context): CaseDef =
if Feature.qualifiedTypesEnabled then
val CaseDef(pat, guard, body) = cdef
val pat1 = DesugarQualifiedTypesInPatternMap().transform(pat)
cpy.CaseDef(cdef)(pat1, guard, body)
else
cdef

def mapParamss(paramss: List[ParamClause])
(mapTypeParam: TypeDef => TypeDef)
(mapTermParam: ValDef => ValDef)(using Context): List[ParamClause] =
Expand Down Expand Up @@ -2279,6 +2288,8 @@ object desugar {
case PatDef(mods, pats, tpt, rhs) =>
val pats1 = if (tpt.isEmpty) pats else pats map (Typed(_, tpt))
flatTree(pats1 map (makePatDef(tree, mods, _, rhs)))
case QualifiedTypeTree(parent, paramName, qualifier) =>
qualifiedType(parent, paramName.getOrElse(nme.WILDCARD), qualifier, tree.span)
case ext: ExtMethods =>
Block(List(ext), syntheticUnitLiteral.withSpan(ext.span))
case f: FunctionWithMods if f.hasErasedParams => makeFunctionWithValDefs(f, pt)
Expand Down Expand Up @@ -2457,4 +2468,50 @@ object desugar {
collect(tree)
buf.toList
}

/** Desugar subtrees that are `QualifiedTypeTree`s using `outerParamName` as
* the qualified parameter name.
*/
private def desugarQualifiedTypes(tpt: Tree, outerParamName: TermName)(using Context): Tree =
def transform(tree: Tree): Tree =
tree match
case QualifiedTypeTree(parent, None, qualifier) =>
qualifiedType(transform(parent), outerParamName, qualifier, tree.span)
case QualifiedTypeTree(parent, paramName, qualifier) =>
cpy.QualifiedTypeTree(tree)(transform(parent), paramName, qualifier)
case TypeApply(fn, args) =>
cpy.TypeApply(tree)(transform(fn), args)
case AppliedTypeTree(fn, args) =>
cpy.AppliedTypeTree(tree)(transform(fn), args)
case InfixOp(left, op, right) =>
cpy.InfixOp(tree)(transform(left), op, transform(right))
case Parens(arg) =>
cpy.Parens(tree)(transform(arg))
case _ =>
tree

if Feature.qualifiedTypesEnabled then
transform(tpt)
else
tpt

private class DesugarQualifiedTypesInPatternMap extends UntypedTreeMap:
override def transform(tree: Tree)(using Context): Tree =
tree match
case Typed(ident @ Ident(name: TermName), tpt) =>
cpy.Typed(tree)(ident, desugarQualifiedTypes(tpt, name))
case _ =>
super.transform(tree)

/** Returns the annotated type used to represent the qualified type with the
* given components:
* `parent @qualified[parent]((paramName: parent) => qualifier)`.
*/
def qualifiedType(parent: Tree, paramName: TermName, qualifier: Tree, span: Span)(using Context): Tree =
val param = makeParameter(paramName, parent, EmptyModifiers) // paramName: parent
val predicate = WildcardFunction(List(param), qualifier) // (paramName: parent) => qualifier
val qualifiedAnnot = scalaAnnotationDot(nme.qualified)
val annot = Apply(TypeApply(qualifiedAnnot, List(parent)), predicate).withSpan(span) // @qualified[parent](predicate)
Annotated(parent, annot).withSpan(span) // parent @qualified[parent](predicate)

}
13 changes: 13 additions & 0 deletions compiler/src/dotty/tools/dotc/ast/tpd.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1437,6 +1437,19 @@ object tpd extends Trees.Instance[Type] with TypedTreeInfo {
def unapply(ts: List[Tree]): Option[List[Tree]] =
if ts.nonEmpty && ts.head.isType then Some(ts) else None


/** An extractor for trees that are constant values. */
object ConstantTree:
def unapply(tree: Tree)(using Context): Option[Constant] =
tree match
case Inlined(_, Nil, expr) => unapply(expr)
case Typed(expr, _) => unapply(expr)
case Literal(c) if c.tag == Constants.NullTag => Some(c)
case _ =>
tree.tpe.widenTermRefExpr.normalized.simplified match
case ConstantType(c) => Some(c)
case _ => None

/** Split argument clauses into a leading type argument clause if it exists and
* remaining clauses
*/
Expand Down
15 changes: 15 additions & 0 deletions compiler/src/dotty/tools/dotc/ast/untpd.scala
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,13 @@ object untpd extends Trees.Instance[Untyped] with UntypedTreeInfo {
*/
case class CapturesAndResult(refs: List[Tree], parent: Tree)(implicit @constructorOnly src: SourceFile) extends TypTree

/** `{ x: parent with qualifier }` if `paramName == Some(x)`,
* `parent with qualifier` otherwise.
*
* Only relevant under `qualifiedTypes`.
*/
case class QualifiedTypeTree(parent: Tree, paramName: Option[TermName], qualifier: Tree)(implicit @constructorOnly src: SourceFile) extends TypTree

/** A type tree appearing somewhere in the untyped DefDef of a lambda, it will be typed using `tpFun`.
*
* @param isResult Is this the result type of the lambda? This is handled specially in `Namer#valOrDefDefSig`.
Expand Down Expand Up @@ -700,6 +707,10 @@ object untpd extends Trees.Instance[Untyped] with UntypedTreeInfo {
case tree: CapturesAndResult if (refs eq tree.refs) && (parent eq tree.parent) => tree
case _ => finalize(tree, untpd.CapturesAndResult(refs, parent))

def QualifiedTypeTree(tree: Tree)(parent: Tree, paramName: Option[TermName], qualifier: Tree)(using Context): Tree = tree match
case tree: QualifiedTypeTree if (parent eq tree.parent) && (paramName eq tree.paramName) && (qualifier eq tree.qualifier) => tree
case _ => finalize(tree, untpd.QualifiedTypeTree(parent, paramName, qualifier)(tree.source))

def TypedSplice(tree: Tree)(splice: tpd.Tree)(using Context): ProxyTree = tree match {
case tree: TypedSplice if splice `eq` tree.splice => tree
case _ => finalize(tree, untpd.TypedSplice(splice)(using ctx))
Expand Down Expand Up @@ -763,6 +774,8 @@ object untpd extends Trees.Instance[Untyped] with UntypedTreeInfo {
cpy.MacroTree(tree)(transform(expr))
case CapturesAndResult(refs, parent) =>
cpy.CapturesAndResult(tree)(transform(refs), transform(parent))
case QualifiedTypeTree(parent, paramName, qualifier) =>
cpy.QualifiedTypeTree(tree)(transform(parent), paramName, transform(qualifier))
case _ =>
super.transformMoreCases(tree)
}
Expand Down Expand Up @@ -822,6 +835,8 @@ object untpd extends Trees.Instance[Untyped] with UntypedTreeInfo {
this(x, expr)
case CapturesAndResult(refs, parent) =>
this(this(x, refs), parent)
case QualifiedTypeTree(parent, paramName, qualifier) =>
this(this(x, parent), qualifier)
case _ =>
super.foldMoreCases(x, tree)
}
Expand Down
6 changes: 6 additions & 0 deletions compiler/src/dotty/tools/dotc/config/Feature.scala
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ object Feature:
val saferExceptions = experimental("saferExceptions")
val pureFunctions = experimental("pureFunctions")
val captureChecking = experimental("captureChecking")
val qualifiedTypes = experimental("qualifiedTypes")
val into = experimental("into")
val modularity = experimental("modularity")
val quotedPatternsWithPolymorphicFunctions = experimental("quotedPatternsWithPolymorphicFunctions")
Expand Down Expand Up @@ -60,6 +61,7 @@ object Feature:
(saferExceptions, "Enable safer exceptions"),
(pureFunctions, "Enable pure functions for capture checking"),
(captureChecking, "Enable experimental capture checking"),
(qualifiedTypes, "Enable experimental qualified types"),
(into, "Allow into modifier on parameter types"),
(modularity, "Enable experimental modularity features"),
(packageObjectValues, "Enable experimental package objects as values"),
Expand Down Expand Up @@ -146,6 +148,10 @@ object Feature:
if ctx.run != null then ctx.run.nn.ccEnabledSomewhere
else enabledBySetting(captureChecking)

/** Is qualifiedTypes enabled for this compilation unit? */
def qualifiedTypesEnabled(using Context) =
enabledBySetting(qualifiedTypes)

def sourceVersionSetting(using Context): SourceVersion =
SourceVersion.valueOf(ctx.settings.source.value)

Expand Down
1 change: 1 addition & 0 deletions compiler/src/dotty/tools/dotc/config/Printers.scala
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ object Printers {
val overload = noPrinter
val patmatch = noPrinter
val pickling = noPrinter
val qualifiedTypes = noPrinter
val quotePickling = noPrinter
val plugins = noPrinter
val recheckr = noPrinter
Expand Down
1 change: 1 addition & 0 deletions compiler/src/dotty/tools/dotc/core/Definitions.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1036,6 +1036,7 @@ class Definitions {
@tu lazy val DeprecatedAnnot: ClassSymbol = requiredClass("scala.deprecated")
@tu lazy val DeprecatedOverridingAnnot: ClassSymbol = requiredClass("scala.deprecatedOverriding")
@tu lazy val DeprecatedInheritanceAnnot: ClassSymbol = requiredClass("scala.deprecatedInheritance")
@tu lazy val QualifiedAnnot: ClassSymbol = requiredClass("scala.annotation.qualified")
@tu lazy val ImplicitAmbiguousAnnot: ClassSymbol = requiredClass("scala.annotation.implicitAmbiguous")
@tu lazy val ImplicitNotFoundAnnot: ClassSymbol = requiredClass("scala.annotation.implicitNotFound")
@tu lazy val InferredDepFunAnnot: ClassSymbol = requiredClass("scala.caps.internal.inferredDepFun")
Expand Down
1 change: 1 addition & 0 deletions compiler/src/dotty/tools/dotc/core/StdNames.scala
Original file line number Diff line number Diff line change
Expand Up @@ -587,6 +587,7 @@ object StdNames {
val productElementName: N = "productElementName"
val productIterator: N = "productIterator"
val productPrefix: N = "productPrefix"
val qualified : N = "qualified"
val quotes : N = "quotes"
val raw_ : N = "raw"
val rd: N = "rd"
Expand Down
5 changes: 4 additions & 1 deletion compiler/src/dotty/tools/dotc/core/TypeComparer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import Capabilities.Capability
import NameKinds.WildcardParamName
import MatchTypes.isConcrete
import scala.util.boundary, boundary.break
import qualified_types.{QualifiedType, QualifiedTypes}

/** Provides methods to compare types.
*/
Expand Down Expand Up @@ -884,6 +885,8 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling
println(i"assertion failed while compare captured $tp1 <:< $tp2")
throw ex
compareCapturing || fourthTry
case QualifiedType(parent2, qualifier2) =>
QualifiedTypes.typeImplies(tp1, qualifier2) && recur(tp1, parent2)
case tp2: AnnotatedType if tp2.isRefining =>
(tp1.derivesAnnotWith(tp2.annot.sameAnnotation) || tp1.isBottomType) &&
recur(tp1, tp2.parent)
Expand Down Expand Up @@ -2110,7 +2113,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling
* since `T >: Int` is subsumed by both alternatives in the first match clause.
*
* However, the following should not:
*
*
* def foo[T](e: Expr[T]): T = e match
* case I1(_) | B(_) => 42
*
Expand Down
18 changes: 10 additions & 8 deletions compiler/src/dotty/tools/dotc/core/Types.scala
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ import compiletime.uninitialized
import cc.*
import CaptureSet.IdentityCaptRefMap
import Capabilities.*

import qualified_types.QualifiedType
import scala.annotation.internal.sharable
import scala.annotation.threadUnsafe

Expand All @@ -56,7 +56,7 @@ object Types extends TypeUtils {
* The principal subclasses and sub-objects are as follows:
*
* ```none
* Type -+- ProxyType --+- NamedType ----+--- TypeRef
* Type -+- TypeProxy --+- NamedType ----+--- TypeRef
* | | \
* | +- SingletonType-+-+- TermRef
* | | |
Expand Down Expand Up @@ -191,9 +191,10 @@ object Types extends TypeUtils {

/** Is this type a (possibly refined, applied, aliased or annotated) type reference
* to the given type symbol?
* @sym The symbol to compare to. It must be a class symbol or abstract type.
* @param sym The symbol to compare to. It must be a class symbol or abstract type.
* It makes no sense for it to be an alias type because isRef would always
* return false in that case.
* @param skipRefined If true, skip refinements, annotated types and applied types.
*/
def isRef(sym: Symbol, skipRefined: Boolean = true)(using Context): Boolean = this match {
case this1: TypeRef =>
Expand All @@ -211,7 +212,7 @@ object Types extends TypeUtils {
else this1.underlying.isRef(sym, skipRefined)
case this1: TypeVar =>
this1.instanceOpt.isRef(sym, skipRefined)
case this1: AnnotatedType =>
case this1: AnnotatedType if (!this1.isRefining || skipRefined) =>
this1.parent.isRef(sym, skipRefined)
case _ => false
}
Expand Down Expand Up @@ -1567,6 +1568,7 @@ object Types extends TypeUtils {
def apply(tp: Type) = /*trace(i"deskolemize($tp) at $variance", show = true)*/
tp match {
case tp: SkolemType => range(defn.NothingType, atVariance(1)(apply(tp.info)))
case QualifiedType(_, _) => tp
case _ => mapOver(tp)
}
}
Expand Down Expand Up @@ -2102,7 +2104,7 @@ object Types extends TypeUtils {
/** Is `this` isomorphic to `that`, assuming pairs of matching binders `bs`?
* It is assumed that `this.ne(that)`.
*/
protected def iso(that: Any, bs: BinderPairs): Boolean = this.equals(that)
def iso(that: Any, bs: BinderPairs): Boolean = this.equals(that)

/** Equality used for hash-consing; uses `eq` on all recursive invocations,
* except where a BindingType is involved. The latter demand a deep isomorphism check.
Expand Down Expand Up @@ -3486,7 +3488,7 @@ object Types extends TypeUtils {
case _ => false
}

override protected def iso(that: Any, bs: BinderPairs) = that match
override def iso(that: Any, bs: BinderPairs) = that match
case that: AndType => tp1.equals(that.tp1, bs) && tp2.equals(that.tp2, bs)
case _ => false
}
Expand Down Expand Up @@ -3640,7 +3642,7 @@ object Types extends TypeUtils {
case _ => false
}

override protected def iso(that: Any, bs: BinderPairs) = that match
override def iso(that: Any, bs: BinderPairs) = that match
case that: OrType => tp1.equals(that.tp1, bs) && tp2.equals(that.tp2, bs) && isSoft == that.isSoft
case _ => false
}
Expand Down Expand Up @@ -4966,7 +4968,7 @@ object Types extends TypeUtils {
* anymore, or NoType if the variable can still be further constrained or a provisional
* instance type in the constraint can be retracted.
*/
private[core] def permanentInst = inst
def permanentInst = inst
private[core] def setPermanentInst(tp: Type): Unit =
inst = tp
if tp.exists && owningState != null then
Expand Down
Loading
Loading