Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

[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
mbovel wants to merge 14 commits into scala:main
base: main
Choose a base branch
Loading
from mbovel:qualifiers-ci
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
14 commits
Select commit Hold shift + click to select a range
e2a56b5
Add syntax for qualified types
mbovel Jun 4, 2025
3cb4ae5
Allow nested qualified types with implicit argument name
mbovel Jun 4, 2025
95f6655
Allow qualified types without argument name
mbovel Jun 4, 2025
36cd600
Allow qualified types with implicit argument name in patterns
mbovel Jun 4, 2025
a3f5d5c
Lower precedence of qualified types' `with`
mbovel Jun 4, 2025
be10905
Basic subtyping and solver for qualified types
mbovel Jun 11, 2025
7d8ecd0
Runtime checks for qualified types
mbovel Jun 11, 2025
f4aac86
Add E-Graph-based rewriting to the QualifierSolver
mbovel Jun 12, 2025
d4ea201
Normalize TermRefs in QualifierComparer
mbovel Jun 12, 2025
845d560
Move normalization and comparison logic to the E-Graph
mbovel Jun 25, 2025
38351ff
Implement lambdas to E-Node conversion
mbovel Jul 3, 2025
dacc557
Add support for field accesses and constructors
mbovel Jul 3, 2025
09dd330
Encode qualifier arguments as E-Nodes
mbovel Aug 21, 2025
77fd08b
Re-add `qualifiedTypes` in `object language`
mbovel Sep 30, 2025
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
62 changes: 60 additions & 2 deletions compiler/src/dotty/tools/dotc/ast/Desugar.scala
View file Open in desktop
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 @@ -213,9 +213,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 @@ -232,6 +233,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 @@ -2347,6 +2356,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 @@ -2525,4 +2536,51 @@ 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
trace(i"desugar qualified types in pattern: $tpt", Printers.qualifiedTypes):
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
View file Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -1439,6 +1439,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
17 changes: 16 additions & 1 deletion compiler/src/dotty/tools/dotc/ast/untpd.scala
View file Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,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 @@ -466,7 +473,7 @@ object untpd extends Trees.Instance[Untyped] with UntypedTreeInfo {
def New(tpt: Tree, argss: List[List[Tree]])(using Context): Tree =
ensureApplied(argss.foldLeft(makeNew(tpt))(Apply(_, _)))

/** A new expression with constrictor and possibly type arguments. See
/** A new expression with constructor and possibly type arguments. See
* `New(tpt, argss)` for details.
*/
def makeNew(tpt: Tree)(using Context): Tree = {
Expand Down Expand Up @@ -732,6 +739,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)(using 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 @@ -795,6 +806,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 @@ -854,6 +867,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
View file Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ object Feature:
val pureFunctions = experimental("pureFunctions")
val captureChecking = experimental("captureChecking")
val separationChecking = experimental("separationChecking")
val qualifiedTypes = experimental("qualifiedTypes")
val into = experimental("into")
val modularity = experimental("modularity")
val quotedPatternsWithPolymorphicFunctions = experimental("quotedPatternsWithPolymorphicFunctions")
Expand Down Expand Up @@ -64,6 +65,7 @@ object Feature:
(pureFunctions, "Enable pure functions for capture checking"),
(captureChecking, "Enable experimental capture checking"),
(separationChecking, "Enable experimental separation checking (requires captureChecking)"),
(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 @@ -150,6 +152,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
View file Open in desktop
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
4 changes: 4 additions & 0 deletions compiler/src/dotty/tools/dotc/core/Definitions.scala
View file Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -625,9 +625,11 @@ class Definitions {
@tu lazy val Int_/ : Symbol = IntClass.requiredMethod(nme.DIV, List(IntType))
@tu lazy val Int_* : Symbol = IntClass.requiredMethod(nme.MUL, List(IntType))
@tu lazy val Int_== : Symbol = IntClass.requiredMethod(nme.EQ, List(IntType))
@tu lazy val Int_!= : Symbol = IntClass.requiredMethod(nme.NE, List(IntType))
@tu lazy val Int_>= : Symbol = IntClass.requiredMethod(nme.GE, List(IntType))
@tu lazy val Int_<= : Symbol = IntClass.requiredMethod(nme.LE, List(IntType))
@tu lazy val Int_> : Symbol = IntClass.requiredMethod(nme.GT, List(IntType))
@tu lazy val Int_< : Symbol = IntClass.requiredMethod(nme.LT, List(IntType))
@tu lazy val LongType: TypeRef = valueTypeRef("scala.Long", java.lang.Long.TYPE, LongEnc, nme.specializedTypeNames.Long)
def LongClass(using Context): ClassSymbol = LongType.symbol.asClass
@tu lazy val Long_+ : Symbol = LongClass.requiredMethod(nme.PLUS, List(LongType))
Expand Down Expand Up @@ -670,6 +672,7 @@ class Definitions {
@tu lazy val StringClass: ClassSymbol = requiredClass("java.lang.String")
def StringType: Type = StringClass.typeRef
@tu lazy val StringModule: Symbol = StringClass.linkedClass
@tu lazy val String_== : TermSymbol = enterMethod(StringClass, nme.EQ, methOfAnyRef(BooleanType), Final)
@tu lazy val String_+ : TermSymbol = enterMethod(StringClass, nme.raw.PLUS, methOfAny(StringType), Final)
@tu lazy val String_valueOf_Object: Symbol = StringModule.info.member(nme.valueOf).suchThat(_.info.firstParamTypes match {
case List(pt) => pt.isAny || pt.stripNull().isAnyRef
Expand Down Expand Up @@ -1048,6 +1051,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
View file Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -589,6 +589,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
10 changes: 9 additions & 1 deletion compiler/src/dotty/tools/dotc/core/TypeComparer.scala
View file Open in desktop
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 @@ -886,6 +887,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) =>
recur(tp1, parent2) && QualifiedTypes.typeImplies(tp1, qualifier2, qualifierSolver())
case tp2: AnnotatedType if tp2.isRefining =>
(tp1.derivesAnnotWith(tp2.annot.sameAnnotation) || tp1.isBottomType) &&
recur(tp1, tp2.parent)
Expand Down Expand Up @@ -3306,6 +3309,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling

protected def explainingTypeComparer(short: Boolean) = ExplainingTypeComparer(comparerContext, short)
protected def matchReducer = MatchReducer(comparerContext)
protected def qualifierSolver() = qualified_types.QualifierSolver(using comparerContext)

private def inSubComparer[T, Cmp <: TypeComparer](comparer: Cmp)(op: Cmp => T): T =
val saved = myInstance
Expand Down Expand Up @@ -3971,7 +3975,7 @@ class ExplainingTypeComparer(initctx: Context, short: Boolean) extends TypeCompa
lastForwardGoal = null

override def traceIndented[T](str: String)(op: => T): T =
val str1 = str.replace('\n', ' ')
val str1 = str
if short && str1 == lastForwardGoal then
op // repeated goal, skip for clarity
else
Expand Down Expand Up @@ -4040,5 +4044,9 @@ class ExplainingTypeComparer(initctx: Context, short: Boolean) extends TypeCompa
super.subCaptures(refs1, refs2, vs)
}

override def qualifierSolver() =
val traceIndented0 = [T] => (message: String) => traceIndented[T](message)
qualified_types.ExplainingQualifierSolver(traceIndented0)(using comparerContext)

def lastTrace(header: String): String = header + { try b.toString finally b.clear() }
}
23 changes: 15 additions & 8 deletions compiler/src/dotty/tools/dotc/core/Types.scala
View file Open in desktop
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, QualifiedAnnotation}
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 @@ -1615,6 +1616,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 @@ -2150,7 +2152,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 @@ -3547,7 +3549,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 @@ -3701,7 +3703,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 @@ -5033,7 +5035,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 Expand Up @@ -6269,6 +6271,8 @@ object Types extends TypeUtils {
tp.derivedAnnotatedType(underlying, annot)
protected def derivedCapturingType(tp: Type, parent: Type, refs: CaptureSet): Type =
tp.derivedCapturingType(parent, refs)
protected def derivedENodeParamRef(tp: qualified_types.ENodeParamRef, index: Int, underlying: Type): Type =
tp.derivedENodeParamRef(index, underlying)
protected def derivedWildcardType(tp: WildcardType, bounds: Type): Type =
tp.derivedWildcardType(bounds)
protected def derivedSkolemType(tp: SkolemType, info: Type): Type =
Expand Down Expand Up @@ -6481,6 +6485,9 @@ object Types extends TypeUtils {
case tp: JavaArrayType =>
derivedJavaArrayType(tp, this(tp.elemType))

case tp: qualified_types.ENodeParamRef =>
derivedENodeParamRef(tp, tp.index, this(tp.underlying))

case _ =>
tp
}
Expand Down
Loading
Loading

AltStyle によって変換されたページ (->オリジナル) /