diff --git a/compiler/src/dotty/tools/dotc/ast/Desugar.scala b/compiler/src/dotty/tools/dotc/ast/Desugar.scala index e66c71731b4f..1ec7e2c5aa9d 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)) @@ -2158,6 +2159,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) @@ -2336,4 +2341,23 @@ object desugar { collect(tree) buf.toList } + + /** If `tree` is a `QualifiedTypeTree`, then desugars it using `paramName` as + * the qualified parameter 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 e8e3646bd087..9cec1182b05a 100644 --- a/compiler/src/dotty/tools/dotc/ast/untpd.scala +++ b/compiler/src/dotty/tools/dotc/ast/untpd.scala @@ -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`. @@ -703,6 +710,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)) @@ -766,6 +777,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) } @@ -825,6 +838,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 ad20bab46c1e..630315e2d290 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 modularity = experimental("modularity") val betterMatchTypeExtractors = experimental("betterMatchTypeExtractors") @@ -64,6 +65,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"), (modularity, "Enable experimental modularity features"), (betterMatchTypeExtractors, "Enable better match type extractors"), @@ -156,6 +158,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 cd727ba8ac72..d3e8a77c1bf3 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 @@ -1879,12 +1886,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 in.featureEnabled(Feature.qualifiedTypes) && 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 @@ -2032,6 +2049,7 @@ object Parsers { * | ‘(’ ArgTypes ‘)’ * | ‘(’ NamesAndTypes ‘)’ * | Refinement + * | QualifiedType -- under qualifiedTypes * | TypeSplice -- deprecated syntax (since 3.0.0) * | SimpleType1 TypeArgs * | SimpleType1 `#' id @@ -2042,7 +2060,10 @@ object Parsers { makeTupleOrParens(inParensWithCommas(argTypes(namedOK = false, wildOK = true, tupleOK = true))) } else if in.token == LBRACE then - atSpan(in.offset) { RefinedTypeTree(EmptyTree, refinement(indentOK = false)) } + if in.featureEnabled(Feature.qualifiedTypes) && in.lookahead.token == IDENTIFIER then + qualifiedType() + else + atSpan(in.offset) { RefinedTypeTree(EmptyTree, refinement(indentOK = false)) } else if (isSplice) splice(isType = true) else @@ -2205,6 +2226,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 3caba59a091f..40f9ce854497 100644 --- a/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala @@ -814,6 +814,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/compiler/src/dotty/tools/dotc/typer/ImportInfo.scala b/compiler/src/dotty/tools/dotc/typer/ImportInfo.scala index 78cba674bfff..16f690790f80 100644 --- a/compiler/src/dotty/tools/dotc/typer/ImportInfo.scala +++ b/compiler/src/dotty/tools/dotc/typer/ImportInfo.scala @@ -206,7 +206,7 @@ class ImportInfo(symf: Context ?=> Symbol, /** Does this import clause or a preceding import clause enable `feature`? * - * @param feature a possibly quailified name, e.g. + * @param feature a possibly qualified name, e.g. * strictEquality * experimental.genericNumberLiterals * 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 b8d990cf56f5..c98952451ba8 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/project/MiMaFilters.scala b/project/MiMaFilters.scala index 00e7153bcb83..1c9e274640fa 100644 --- a/project/MiMaFilters.scala +++ b/project/MiMaFilters.scala @@ -12,6 +12,8 @@ object MiMaFilters { ProblemFilters.exclude[MissingClassProblem]("scala.runtime.stdLibPatches.language$experimental$betterFors$"), ProblemFilters.exclude[MissingFieldProblem]("scala.runtime.stdLibPatches.language#experimental.quotedPatternsWithPolymorphicFunctions"), ProblemFilters.exclude[MissingClassProblem]("scala.runtime.stdLibPatches.language$experimental$quotedPatternsWithPolymorphicFunctions$"), + ProblemFilters.exclude[MissingFieldProblem]("scala.runtime.stdLibPatches.language#experimental.qualifiedTypes"), + ProblemFilters.exclude[MissingClassProblem]("scala.runtime.stdLibPatches.language$experimental$qualifiedTypes$"), ProblemFilters.exclude[DirectMissingMethodProblem]("scala.quoted.runtime.Patterns.higherOrderHoleWithTypes"), ), @@ -72,6 +74,8 @@ object MiMaFilters { ProblemFilters.exclude[MissingClassProblem]("scala.runtime.stdLibPatches.language$experimental$betterMatchTypeExtractors$"), ProblemFilters.exclude[MissingClassProblem]("scala.runtime.stdLibPatches.language$experimental$modularity$"), ProblemFilters.exclude[MissingClassProblem]("scala.runtime.stdLibPatches.language$experimental$namedTuples$"), + ProblemFilters.exclude[MissingFieldProblem]("scala.runtime.stdLibPatches.language#experimental.qualifiedTypes"), + ProblemFilters.exclude[MissingClassProblem]("scala.runtime.stdLibPatches.language$experimental$qualifiedTypes$"), ProblemFilters.exclude[MissingFieldProblem]("scala.runtime.stdLibPatches.language.3.7-migration"), ProblemFilters.exclude[MissingFieldProblem]("scala.runtime.stdLibPatches.language.3.7"), ProblemFilters.exclude[MissingClassProblem]("scala.runtime.stdLibPatches.language$3$u002E7$"), diff --git a/tests/printing/qualifiers.check b/tests/printing/qualifiers.check new file mode 100644 index 000000000000..6d021fc599ce --- /dev/null +++ b/tests/printing/qualifiers.check @@ -0,0 +1,219 @@ +[[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 Neg = + Int @qualified[Int]( + { + def $anonfun(x: Int): Boolean = x.<(0) + closure($anonfun) + } + ) + type Pos = + Int @qualified[Int]( + { + def $anonfun(x: Int): Boolean = x.>(0) + closure($anonfun) + } + ) + type Pos2 = + Int @qualified[Int]( + { + def $anonfun(x: Int): Boolean = x.>(0) + closure($anonfun) + } + ) + type Pos3 = + Int @qualified[Int]( + { + def $anonfun(x: Int): Boolean = x.>(0) + closure($anonfun) + } + ) + type Pos4 = + Int @qualified[Int]( + { + def $anonfun(x: Int): Boolean = x.>(0) + closure($anonfun) + } + ) + type Pos5 = + Int @qualified[Int]( + { + def $anonfun(x: Int): Boolean = + { + val res: Boolean = x.>(0) + res:Boolean + } + closure($anonfun) + } + ) + type Nested = + 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 Intersection = + 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 x: 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(x: Int): Boolean = x.>(0) + closure($anonfun) + } + ) + = 1 + val x4: + Int @qualified[Int]( + { + def $anonfun(x: Int): Boolean = x.>(0) + closure($anonfun) + } + ) + = 1 + val x5: + Int @qualified[Int]( + { + def $anonfun(x5: Int): Boolean = x.>(0) + closure($anonfun) + } + ) + = 1 + val x6: 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 T1 = + Object + { + val x: Int + } + type T2 = + Object + { + val x: Int + } + type T3 = + Object + { + type T = Int + } + type T4 = + Object + { + def x: Int + } + type T5 = + Object + { + def x: Int + def x_=(x$1: Int): _root_.scala.Unit + } + type T6 = + Object + { + val x: Int + } + type T7 = + 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..f09f260d4962 --- /dev/null +++ b/tests/printing/qualifiers.scala @@ -0,0 +1,59 @@ +package example + +type Neg = {x: Int with x < 0} +type Pos = {x: Int with x > 0} +type Pos2 = {x: Int + with x > 0 +} +type Pos3 = {x: Int with + x > 0 +} +type Pos4 = + {x: Int with x > 0} +type Pos5 = {x: Int with + val res = x > 0 + res +} + +type Nested = {x: Int with { val y: {z: Int with z > 0} = ??? ; x > y }} +type Intersection = Int & {x: Int with x > 0} +type ValRefinement = {val x: Int with x > 0} + +def id[T](x: T): T = x + +def test() = + val x: Pos = 1 + val x2: {x: Int with x > 0} = 1 + val x3: { + x: Int with x > 0 + } = 1 + val x4: {x: Int with + x > 0 + } = 1 + val x5: Int with x > 0 = 1 + val x6: 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 T1 = {val x: Int} +type T2 = { + val x: Int +} +type T3 = {type T = Int} +type T4 = {def x: Int} +type T5 = {var x: Int} +type T6 = Object {val x: Int} +type T7 = Object: + val x: Int diff --git a/tests/run-tasty-inspector/stdlibExperimentalDefinitions.scala b/tests/run-tasty-inspector/stdlibExperimentalDefinitions.scala index 7a8dcb9bd2df..f04f51dfe701 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",