Skip to content

Commit

Permalink
Add syntax for qualified types
Browse files Browse the repository at this point in the history
Co-Authored-By: Quentin Bernet <[email protected]>
  • Loading branch information
mbovel and Sporarum committed Sep 15, 2024
1 parent c9b9aea commit 1c74ac1
Show file tree
Hide file tree
Showing 12 changed files with 303 additions and 5 deletions.
28 changes: 26 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, betterForsEnabled}
Expand Down Expand Up @@ -199,9 +199,10 @@ object desugar {
def valDef(vdef0: ValDef)(using Context): Tree =
val vdef @ ValDef(_, tpt, rhs) = vdef0
val valName = normalizeName(vdef, tpt).asTermName
val tpt1 = qualifiedType(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 Down Expand Up @@ -2145,6 +2146,10 @@ 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, None, qualifier) =>
ErrorReporting.errorTree(parent, em"missing parameter name in qualified type", tree.srcPos)
case QualifiedTypeTree(parent, Some(paramName), qualifier) =>
qualifiedType(parent, paramName, 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 @@ -2323,4 +2328,23 @@ object desugar {
collect(tree)
buf.toList
}

/** If `tree` is a `QualifiedTypeTree`, then desugars it using `paramName` as
* the qualified paramater name. Otherwise, returns `tree` unchanged.
*/
def qualifiedType(tree: Tree, paramName: TermName)(using Context): Tree = tree match
case QualifiedTypeTree(parent, None, qualifier) => qualifiedType(parent, paramName, qualifier, tree.span)
case _ => 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)

}
11 changes: 11 additions & 0 deletions compiler/src/dotty/tools/dotc/ast/untpd.scala
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,9 @@ object untpd extends Trees.Instance[Untyped] with UntypedTreeInfo {
*/
case class CapturesAndResult(refs: List[Tree], parent: Tree)(implicit @constructorOnly src: SourceFile) extends TypTree

/** { x: T with p }  (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 @@ -704,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 @@ -767,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 @@ -826,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 @@ -33,6 +33,7 @@ object Feature:
val clauseInterleaving = experimental("clauseInterleaving")
val pureFunctions = experimental("pureFunctions")
val captureChecking = experimental("captureChecking")
val qualifiedTypes = experimental("qualifiedTypes")
val into = experimental("into")
val namedTuples = experimental("namedTuples")
val modularity = experimental("modularity")
Expand Down Expand Up @@ -65,6 +66,7 @@ object Feature:
(clauseInterleaving, "Enable clause interleaving"),
(pureFunctions, "Enable pure functions for capture checking"),
(captureChecking, "Enable experimental capture checking"),
(qualifiedTypes, "Enable experimental qualified types"),
(into, "Allow into modifier on parameter types"),
(namedTuples, "Allow named tuples"),
(modularity, "Enable experimental modularity features"),
Expand Down Expand Up @@ -158,6 +160,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/core/StdNames.scala
Original file line number Diff line number Diff line change
Expand Up @@ -585,6 +585,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 refl: N = "refl"
Expand Down
58 changes: 55 additions & 3 deletions compiler/src/dotty/tools/dotc/parsing/Parsers.scala
Original file line number Diff line number Diff line change
Expand Up @@ -445,6 +445,13 @@ object Parsers {
finally inMatchPattern = saved
}

private var inQualifiedType = false
private def fromWithinQualifiedType[T](body: => T): T =
val saved = inQualifiedType
inQualifiedType = true
try body
finally inQualifiedType = saved

private var staged = StageKind.None
def withinStaged[T](kind: StageKind)(op: => T): T = {
val saved = staged
Expand Down Expand Up @@ -1085,6 +1092,25 @@ object Parsers {
|| in.lookahead.token == EOF // important for REPL completions
|| ctx.mode.is(Mode.Interactive) // in interactive mode the next tokens might be missing

/** Under `qualifiedTypes` language import: is the token sequence following
* the current `{` classified as a qualified type? This is the case if the
* next token is an `IDENT`, followed by `:`.
*/
def followingIsQualifiedType(): Boolean =
in.featureEnabled(Feature.qualifiedTypes) && {
val lookahead = in.LookaheadScanner(allowIndent = true)

if in.token == INDENT then
() // The LookaheadScanner doesn't see previous indents, so no need to skip it
else
lookahead.nextToken() // skips the opening brace

lookahead.token == IDENTIFIER && {
lookahead.nextToken()
lookahead.token == COLONfollow
}
}

/* --------- OPERAND/OPERATOR STACK --------------------------------------- */

var opStack: List[OpInfo] = Nil
Expand Down Expand Up @@ -1872,12 +1898,22 @@ object Parsers {
t
}

/** WithType ::= AnnotType {`with' AnnotType} (deprecated)
*/
/** With qualifiedTypes enabled:
* WithType ::= AnnotType [`with' PostfixExpr]
*
* Otherwise:
* WithType ::= AnnotType {`with' AnnotType} (deprecated)
*/
def withType(): Tree = withTypeRest(annotType())

def withTypeRest(t: Tree): Tree =
if in.token == WITH then
if Feature.qualifiedTypesEnabled && in.token == WITH then
if inQualifiedType then t
else
in.nextToken()
val qualifier = postfixExpr()
QualifiedTypeTree(t, None, qualifier).withSpan(Span(t.span.start, qualifier.span.end))
else if in.token == WITH then
val withOffset = in.offset
in.nextToken()
if in.token == LBRACE || in.token == INDENT then
Expand Down Expand Up @@ -2025,6 +2061,7 @@ object Parsers {
* | ‘(’ ArgTypes ‘)’
* | ‘(’ NamesAndTypes ‘)’
* | Refinement
* | QualifiedType -- under qualifiedTypes
* | TypeSplice -- deprecated syntax (since 3.0.0)
* | SimpleType1 TypeArgs
* | SimpleType1 `#' id
Expand All @@ -2034,6 +2071,8 @@ object Parsers {
atSpan(in.offset) {
makeTupleOrParens(inParensWithCommas(argTypes(namedOK = false, wildOK = true, tupleOK = true)))
}
else if in.token == LBRACE && followingIsQualifiedType() then
qualifiedType()
else if in.token == LBRACE then
atSpan(in.offset) { RefinedTypeTree(EmptyTree, refinement(indentOK = false)) }
else if (isSplice)
Expand Down Expand Up @@ -2198,6 +2237,19 @@ object Parsers {
else
inBraces(refineStatSeq())

/** QualifiedType ::= `{` Ident `:` Type `with` Block `}`
*/
def qualifiedType(): Tree =
val startOffset = in.offset
accept(LBRACE)
val id = ident()
accept(COLONfollow)
val tp = fromWithinQualifiedType(typ())
accept(WITH)
val qualifier = block(simplify = true)
accept(RBRACE)
QualifiedTypeTree(tp, Some(id), qualifier).withSpan(Span(startOffset, qualifier.span.end))

/** TypeBounds ::= [`>:' Type] [`<:' Type]
* | `^` -- under captureChecking
*/
Expand Down
4 changes: 4 additions & 0 deletions compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala
Original file line number Diff line number Diff line change
Expand Up @@ -808,6 +808,10 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) {
prefix ~~ idx.toString ~~ "|" ~~ tpeText ~~ "|" ~~ argsText ~~ "|" ~~ contentText ~~ postfix
case CapturesAndResult(refs, parent) =>
changePrec(GlobalPrec)("^{" ~ Text(refs.map(toText), ", ") ~ "}" ~ toText(parent))
case QualifiedTypeTree(parent, paramName, predicate) =>
paramName match
case Some(name) => "{" ~ toText(name) ~ ": " ~ toText(parent) ~ " with " ~ toText(predicate) ~ "}"
case None => toText(parent) ~ " with " ~ toText(predicate)
case ContextBoundTypeTree(tycon, pname, ownName) =>
toText(pname) ~ " : " ~ toText(tycon) ~ (" as " ~ toText(ownName) `provided` !ownName.isEmpty)
case _ =>
Expand Down
4 changes: 4 additions & 0 deletions library/src/scala/annotation/qualified.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package scala.annotation

/** Annotation for qualified types. */
@experimental class qualified[T](predicate: T => Boolean) extends StaticAnnotation
4 changes: 4 additions & 0 deletions library/src/scala/runtime/stdLibPatches/language.scala
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,10 @@ object language:
@compileTimeOnly("`captureChecking` can only be used at compile time in import statements")
object captureChecking

/** Experimental support for qualified types */
@compileTimeOnly("`qualifiedTypes` can only be used at compile time in import statements")
object qualifiedTypes

/** Experimental support for automatic conversions of arguments, without requiring
* a language import `import scala.language.implicitConversions`.
*
Expand Down
Loading

0 comments on commit 1c74ac1

Please sign in to comment.