From 1c74ac1ef4d0d77742bbc561b46e9f7b46c7252e Mon Sep 17 00:00:00 2001 From: Matt Bovel Date: Fri, 13 Sep 2024 18:32:39 +0200 Subject: [PATCH] Add syntax for qualified types Co-Authored-By: Quentin Bernet <28290641+Sporarum@users.noreply.github.com> --- .../src/dotty/tools/dotc/ast/Desugar.scala | 28 +++- compiler/src/dotty/tools/dotc/ast/untpd.scala | 11 ++ .../src/dotty/tools/dotc/config/Feature.scala | 6 + .../src/dotty/tools/dotc/core/StdNames.scala | 1 + .../dotty/tools/dotc/parsing/Parsers.scala | 58 ++++++- .../tools/dotc/printing/RefinedPrinter.scala | 4 + library/src/scala/annotation/qualified.scala | 4 + .../runtime/stdLibPatches/language.scala | 4 + tests/printing/qualifiers.check | 152 ++++++++++++++++++ tests/printing/qualifiers.flags | 1 + tests/printing/qualifiers.scala | 36 +++++ .../stdlibExperimentalDefinitions.scala | 3 + 12 files changed, 303 insertions(+), 5 deletions(-) create mode 100644 library/src/scala/annotation/qualified.scala create mode 100644 tests/printing/qualifiers.check create mode 100644 tests/printing/qualifiers.flags create mode 100644 tests/printing/qualifiers.scala diff --git a/compiler/src/dotty/tools/dotc/ast/Desugar.scala b/compiler/src/dotty/tools/dotc/ast/Desugar.scala index 659701b02371..60e5517a17f5 100644 --- a/compiler/src/dotty/tools/dotc/ast/Desugar.scala +++ b/compiler/src/dotty/tools/dotc/ast/Desugar.scala @@ -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} @@ -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)) @@ -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) @@ -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) + } diff --git a/compiler/src/dotty/tools/dotc/ast/untpd.scala b/compiler/src/dotty/tools/dotc/ast/untpd.scala index 60309d4d83bd..c4db1305ba63 100644 --- a/compiler/src/dotty/tools/dotc/ast/untpd.scala +++ b/compiler/src/dotty/tools/dotc/ast/untpd.scala @@ -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`. @@ -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)) @@ -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) } @@ -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) } diff --git a/compiler/src/dotty/tools/dotc/config/Feature.scala b/compiler/src/dotty/tools/dotc/config/Feature.scala index 8b9a64924ace..9d57cb19ff10 100644 --- a/compiler/src/dotty/tools/dotc/config/Feature.scala +++ b/compiler/src/dotty/tools/dotc/config/Feature.scala @@ -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") @@ -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"), @@ -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) diff --git a/compiler/src/dotty/tools/dotc/core/StdNames.scala b/compiler/src/dotty/tools/dotc/core/StdNames.scala index d3e198a7e7a7..87deb66a35d8 100644 --- a/compiler/src/dotty/tools/dotc/core/StdNames.scala +++ b/compiler/src/dotty/tools/dotc/core/StdNames.scala @@ -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" diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index 8a173faa3cec..0fe970f1fc8a 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -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 @@ -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 @@ -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 @@ -2025,6 +2061,7 @@ object Parsers { * | ‘(’ ArgTypes ‘)’ * | ‘(’ NamesAndTypes ‘)’ * | Refinement + * | QualifiedType -- under qualifiedTypes * | TypeSplice -- deprecated syntax (since 3.0.0) * | SimpleType1 TypeArgs * | SimpleType1 `#' id @@ -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) @@ -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 */ diff --git a/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala index ea729e9549d5..4578acacb843 100644 --- a/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala @@ -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 _ => diff --git a/library/src/scala/annotation/qualified.scala b/library/src/scala/annotation/qualified.scala new file mode 100644 index 000000000000..2fae020be762 --- /dev/null +++ b/library/src/scala/annotation/qualified.scala @@ -0,0 +1,4 @@ +package scala.annotation + +/** Annotation for qualified types. */ +@experimental class qualified[T](predicate: T => Boolean) extends StaticAnnotation diff --git a/library/src/scala/runtime/stdLibPatches/language.scala b/library/src/scala/runtime/stdLibPatches/language.scala index 547710d55293..7004ac313602 100644 --- a/library/src/scala/runtime/stdLibPatches/language.scala +++ b/library/src/scala/runtime/stdLibPatches/language.scala @@ -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`. * diff --git a/tests/printing/qualifiers.check b/tests/printing/qualifiers.check new file mode 100644 index 000000000000..2a7912741238 --- /dev/null +++ b/tests/printing/qualifiers.check @@ -0,0 +1,152 @@ +[[syntax trees at end of typer]] // tests/printing/qualifiers.scala +package example { + class Foo() extends Object() { + val x: + Int @qualified[Int]( + { + def $anonfun(x: Int): Boolean = x.>(0) + closure($anonfun) + } + ) + = 1 + } + trait A() extends Object {} + final lazy module val qualifiers$package: example.qualifiers$package = + new example.qualifiers$package() + final module class qualifiers$package() extends Object() { + this: example.qualifiers$package.type => + type Useless = + Int @qualified[Int]( + { + def $anonfun(x: Int): Boolean = true + closure($anonfun) + } + ) + type Pos = + Int @qualified[Int]( + { + def $anonfun(x: Int): Boolean = x.>(0) + closure($anonfun) + } + ) + type Neg = + Int @qualified[Int]( + { + def $anonfun(x: Int): Boolean = x.<(0) + closure($anonfun) + } + ) + type Nesting = + Int @qualified[Int]( + { + def $anonfun(x: Int): Boolean = + { + val y: + Int @qualified[Int]( + { + def $anonfun(z: Int): Boolean = z.>(0) + closure($anonfun) + } + ) + = ??? + x.>(y) + } + closure($anonfun) + } + ) + type Pos2 = + Int & + Int @qualified[Int]( + { + def $anonfun(x: Int): Boolean = x.>(0) + closure($anonfun) + } + ) + type ValRefinement = + Object + { + val x: + Int @qualified[Int]( + { + def $anonfun(x: Int): Boolean = x.>(0) + closure($anonfun) + } + ) + } + def id[T >: Nothing <: Any](x: T): T = x + def test(): Unit = + { + val x1: example.Pos = 1 + val x2: + Int @qualified[Int]( + { + def $anonfun(x: Int): Boolean = x.>(0) + closure($anonfun) + } + ) + = 1 + val x3: + Int @qualified[Int]( + { + def $anonfun(x3: Int): Boolean = x3.>(0) + closure($anonfun) + } + ) + = 1 + val x4: Int = + example.id[ + Int @qualified[Int]( + { + def $anonfun(x: Int): Boolean = x.<(0) + closure($anonfun) + } + ) + ](1).+(example.id[example.Neg](-1)) + () + } + def bar( + x: + Int @qualified[Int]( + { + def $anonfun(x: Int): Boolean = x.>(0) + closure($anonfun) + } + ) + ): Nothing = ??? + def secondGreater1(x: Int, y: Int)( + z: + Int @qualified[Int]( + { + def $anonfun(w: Int): Boolean = x.>(y) + closure($anonfun) + } + ) + ): Nothing = ??? + def secondGreater2(x: Int, y: Int)( + z: + Int @qualified[Int]( + { + def $anonfun(z: Int): Boolean = x.>(y) + closure($anonfun) + } + ) + ): Nothing = ??? + final lazy module given val given_A: example.given_A = new example.given_A() + final module class given_A() extends Object(), example.A { + this: example.given_A.type => + val b: Boolean = false + example.id[Boolean](true) + } + type B = + Object + { + val x: Int + } + type C = + Object + { + val x: Int + } + } +} + diff --git a/tests/printing/qualifiers.flags b/tests/printing/qualifiers.flags new file mode 100644 index 000000000000..0a6f384436cd --- /dev/null +++ b/tests/printing/qualifiers.flags @@ -0,0 +1 @@ +-language:experimental.qualifiedTypes diff --git a/tests/printing/qualifiers.scala b/tests/printing/qualifiers.scala new file mode 100644 index 000000000000..15ed8d0da6e8 --- /dev/null +++ b/tests/printing/qualifiers.scala @@ -0,0 +1,36 @@ +package example + +type Useless = {x: Int with true} +type Pos = + {x: Int with x > 0} +type Neg = {x: Int with + x < 0 +} +type Nesting = {x: Int with { val y: {z: Int with z > 0} = ??? ; x > y }} +type Pos2 = Int & {x: Int with x > 0} +type ValRefinement = {val x: Int with x > 0} + +def id[T](x: T): T = x + +def test() = + val x1: Pos = 1 + val x2: {x: Int with x > 0} = 1 + val x3: Int with x3 > 0 = 1 + val x4: Int = id[{x: Int with x < 0}](1) + id[Neg](-1) + +def bar(x: Int with x > 0) = ??? +def secondGreater1(x: Int, y: Int)(z: {w: Int with x > y}) = ??? +def secondGreater2(x: Int, y: Int)(z: Int with x > y) = ??? + +class Foo: + val x: Int with x > 0 = 1 + +trait A +// Not a qualified type: +given A with + val b = false + id(true) + +// Also not qualified types: +type B = {val x: Int} +type C = Object {val x: Int} diff --git a/tests/run-tasty-inspector/stdlibExperimentalDefinitions.scala b/tests/run-tasty-inspector/stdlibExperimentalDefinitions.scala index 15ccd38f860c..8909c0b58d79 100644 --- a/tests/run-tasty-inspector/stdlibExperimentalDefinitions.scala +++ b/tests/run-tasty-inspector/stdlibExperimentalDefinitions.scala @@ -35,6 +35,9 @@ val experimentalDefinitionInLibrary = Set( "scala.caps", "scala.caps$", + // Experimental feature: qualified types + "scala.annotation.qualified", + //// New feature: into "scala.annotation.into", "scala.annotation.internal.$into",