diff --git a/compiler/src/dotty/tools/dotc/ast/Desugar.scala b/compiler/src/dotty/tools/dotc/ast/Desugar.scala index 1e09183573b3..e884e7282d1b 100644 --- a/compiler/src/dotty/tools/dotc/ast/Desugar.scala +++ b/compiler/src/dotty/tools/dotc/ast/Desugar.scala @@ -366,7 +366,7 @@ object desugar { /** Split out the quoted pattern type variable definition from the pattern. * * Type variable definitions are all the `type t` defined at the start of a quoted pattern. - * Were name `t` is a pattern type variable name (i.e. lower case letters). + * Where name `t` is a pattern type variable name (i.e. lower case letters). * * ``` * type t1; ...; type tn; @@ -379,16 +379,12 @@ object desugar { def quotedPatternTypeVariables(tree: untpd.Tree)(using Context): (List[untpd.TypeDef], untpd.Tree) = tree match case untpd.Block(stats, expr) => - val untpdTypeVariables = stats.takeWhile { - case tdef @ untpd.TypeDef(name, _) => name.isVarPattern - case _ => false - }.asInstanceOf[List[untpd.TypeDef]] - val otherStats = stats.dropWhile { + val (untpdTypeVariables, otherStats) = stats.span { case tdef @ untpd.TypeDef(name, _) => name.isVarPattern case _ => false } val pattern = if otherStats.isEmpty then expr else untpd.cpy.Block(tree)(otherStats, expr) - (untpdTypeVariables, pattern) + (untpdTypeVariables.asInstanceOf[List[untpd.TypeDef]], pattern) case _ => (Nil, tree) diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index 3079b26df6cd..335ece005b72 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -1740,6 +1740,27 @@ object Parsers { /** The block in a quote or splice */ def stagedBlock() = inBraces(block(simplify = true)) + /** TypeBlock ::= {TypeBlockStat semi} Type + */ + def typeBlock(): Tree = + typeBlockStats() match + case Nil => typ() + case tdefs => Block(tdefs, typ()) + + def typeBlockStats(): List[Tree] = + val tdefs = new ListBuffer[Tree] + while in.token == TYPE do tdefs += typeBlockStat() + tdefs.toList + + /** TypeBlockStat ::= ‘type’ {nl} TypeDcl + */ + def typeBlockStat(): Tree = + val mods = defAnnotsMods(BitSet()) + val tdef = typeDefOrDcl(in.offset, in.skipToken(mods)) + if in.token == SEMI then in.nextToken() + if in.isNewLine then in.nextToken() + tdef + /** ExprSplice ::= ‘$’ spliceId -- if inside quoted block * | ‘$’ ‘{’ Block ‘}’ -- unless inside quoted pattern * | ‘$’ ‘{’ Pattern ‘}’ -- when inside quoted pattern @@ -2480,7 +2501,7 @@ object Parsers { atSpan(in.skipToken()) { withinStaged(StageKind.Quoted | (if (location.inPattern) StageKind.QuotedPattern else 0)) { val body = - if (in.token == LBRACKET) inBrackets(typ()) + if (in.token == LBRACKET) inBrackets(typeBlock()) else stagedBlock() Quote(body, Nil) } @@ -3758,6 +3779,8 @@ object Parsers { else makeTypeDef(bounds) case SEMI | NEWLINE | NEWLINES | COMMA | RBRACE | OUTDENT | EOF => makeTypeDef(typeBounds()) + case _ if (staged & StageKind.QuotedPattern) != 0 => + makeTypeDef(typeBounds()) case _ => syntaxErrorOrIncomplete(ExpectedTypeBoundOrEquals(in.token)) return EmptyTree // return to avoid setting the span to EmptyTree diff --git a/compiler/src/dotty/tools/dotc/typer/QuotesAndSplices.scala b/compiler/src/dotty/tools/dotc/typer/QuotesAndSplices.scala index 342b07c6026f..d3becfd6e9b7 100644 --- a/compiler/src/dotty/tools/dotc/typer/QuotesAndSplices.scala +++ b/compiler/src/dotty/tools/dotc/typer/QuotesAndSplices.scala @@ -166,7 +166,7 @@ trait QuotesAndSplices { tree.srcPos, "\n\nSIP-53: https://docs.scala-lang.org/sips/quote-pattern-type-variable-syntax.html") if !(typeSymInfo =:= TypeBounds.empty) && !(typeSym.info <:< typeSymInfo) then - report.warning(em"Ignored bound$typeSymInfo\n\nConsider defining bounds explicitly `'{ $typeSym$typeSymInfo; ... }`", tree.srcPos) + report.warning(em"Ignored bound$typeSymInfo\n\nConsider defining bounds explicitly `'{ $typeSym${typeSym.info & typeSymInfo}; ... }`", tree.srcPos) ref(typeSym) case None => def spliceOwner(ctx: Context): Symbol = @@ -399,6 +399,17 @@ trait QuotesAndSplices { } val (untpdTypeVariables, quoted0) = desugar.quotedPatternTypeVariables(desugar.quotedPattern(quoted, untpd.TypedSplice(TypeTree(quotedPt)))) + for tdef @ untpd.TypeDef(_, rhs) <- untpdTypeVariables do rhs match + case _: TypeBoundsTree => // ok + case LambdaTypeTree(_, body: TypeBoundsTree) => // ok + case _ => report.error("Quote type variable definition cannot be an alias", tdef.srcPos) + + if quoted.isType && untpdTypeVariables.nonEmpty then + checkExperimentalFeature( + "explicit type variable declarations quoted type patterns (SIP-53)", + untpdTypeVariables.head.srcPos, + "\n\nSIP-53: https://docs.scala-lang.org/sips/quote-pattern-type-variable-syntax.html") + val (typeTypeVariables, patternCtx) = val quoteCtx = quotePatternContext() if untpdTypeVariables.isEmpty then (Nil, quoteCtx) @@ -409,7 +420,7 @@ trait QuotesAndSplices { addQuotedPatternTypeVariable(typeVariable.symbol) val pattern = - if quoted.isType then typedType(quoted0, WildcardType) + if quoted.isType then typedType(quoted0, WildcardType)(using patternCtx) else typedExpr(quoted0, WildcardType) if untpdTypeVariables.isEmpty then pattern diff --git a/docs/_docs/reference/metaprogramming/macros.md b/docs/_docs/reference/metaprogramming/macros.md index 1ed89422eee5..87ab7d66d67d 100644 --- a/docs/_docs/reference/metaprogramming/macros.md +++ b/docs/_docs/reference/metaprogramming/macros.md @@ -530,15 +530,24 @@ It works the same way as a quoted pattern but is restricted to contain a type. Type variables can be used in quoted type patterns to extract a type. ```scala -def empty[T: Type]: Expr[T] = +def empty[T: Type](using Quotes): Expr[T] = Type.of[T] match case '[String] => '{ "" } case '[List[t]] => '{ List.empty[t] } + case '[type t <: Option[Int]; List[t]] => '{ List.empty[t] } ... ``` - `Type.of[T]` is used to summon the given instance of `Type[T]` in scope, it is equivalent to `summon[Type[T]]`. +It is possible to match against a higher-kinded type using appropriate type bounds on type variables. +```scala +def empty[K <: AnyKind : Type](using Quotes): Type[?] = + Type.of[K] match + case '[type f[X]; f] => Type.of[f] + case '[type f[X <: Int, Y]; f] => Type.of[f] + case '[type k <: AnyKind; k ] => Type.of[k] +``` + #### Type testing and casting It is important to note that instance checks and casts on `Expr`, such as `isInstanceOf[Expr[T]]` and `asInstanceOf[Expr[T]]`, will only check if the instance is of the class `Expr` but will not be able to check the `T` argument. These cases will issue a warning at compile-time, but if they are ignored, they can result in unexpected behavior. diff --git a/docs/_docs/reference/syntax.md b/docs/_docs/reference/syntax.md index a705c5a3fd79..b4b3aab6b02f 100644 --- a/docs/_docs/reference/syntax.md +++ b/docs/_docs/reference/syntax.md @@ -274,7 +274,7 @@ ColonArgument ::= colon [LambdaStart] LambdaStart ::= FunParams (‘=>’ | ‘?=>’) | HkTypeParamClause ‘=>’ Quoted ::= ‘'’ ‘{’ Block ‘}’ - | ‘'’ ‘[’ Type ‘]’ + | ‘'’ ‘[’ TypeBlock ‘]’ ExprSplice ::= spliceId -- if inside quoted block | ‘$’ ‘{’ Block ‘}’ -- unless inside quoted pattern | ‘$’ ‘{’ Pattern ‘}’ -- when inside quoted pattern @@ -293,6 +293,8 @@ BlockStat ::= Import | Extension | Expr1 | EndMarker +TypeBlock ::= {TypeBlockStat semi} Type +TypeBlockStat ::= ‘type’ {nl} TypeDcl ForExpr ::= ‘for’ ‘(’ Enumerators0 ‘)’ {nl} [‘do‘ | ‘yield’] Expr | ‘for’ ‘{’ Enumerators0 ‘}’ {nl} [‘do‘ | ‘yield’] Expr diff --git a/tests/neg-custom-args/no-experimental/sip-53-exprimental-b.scala b/tests/neg-custom-args/no-experimental/sip-53-exprimental-b.scala new file mode 100644 index 000000000000..2ec597995bd1 --- /dev/null +++ b/tests/neg-custom-args/no-experimental/sip-53-exprimental-b.scala @@ -0,0 +1,8 @@ +import scala.quoted.* + +def empty[K <: AnyKind : Type](using Quotes): Type[?] = + Type.of[K] match + case '[type t; `t`] => Type.of[t] // error + case '[type f[X]; `f`] => Type.of[f] // error + case '[type f[X <: Int, Y]; `f`] => Type.of[f] // error + case '[type k <: AnyKind; `k` ] => Type.of[k] // error diff --git a/tests/neg-macros/quote-pattern-type-var-bounds.scala b/tests/neg-macros/quote-pattern-type-var-bounds.scala new file mode 100644 index 000000000000..b97b21552a1e --- /dev/null +++ b/tests/neg-macros/quote-pattern-type-var-bounds.scala @@ -0,0 +1,22 @@ +import scala.quoted.* +def types(t: Type[?])(using Quotes) = t match { + case '[ type t; Int ] => + case '[ type t <: Int; Int ] => + case '[ type t >: 1 <: Int; Int ] => + case '[ type t = Int; Int ] => // error + case '[ type t = scala.Int; Int ] => // error + case '[ type f[t] <: List[Any]; Int ] => + case '[ type f[t <: Int] <: List[Any]; Int ] => + case '[ type f[t] = List[Any]; Int ] => // error +} + +def expressions(x: Expr[Any])(using Quotes) = x match { + case '{ type t; () } => + case '{ type t <: Int; () } => + case '{ type t >: 1 <: Int; () } => + case '{ type t = Int; () } => // error + case '{ type t = scala.Int; () } => // error + case '{ type f[t] <: List[Any]; () } => + case '{ type f[t <: Int] <: List[Any]; () } => + case '{ type f[t] = List[Any]; () } => // error +} diff --git a/tests/neg-macros/quote-type-variable-no-inference.check b/tests/neg-macros/quote-type-variable-no-inference.check new file mode 100644 index 000000000000..7e425e932117 --- /dev/null +++ b/tests/neg-macros/quote-type-variable-no-inference.check @@ -0,0 +1,12 @@ +-- Warning: tests/neg-macros/quote-type-variable-no-inference.scala:5:17 ----------------------------------------------- +5 | case '[ F[t, t] ] => // warn // error + | ^ + | Ignored bound <: Double + | + | Consider defining bounds explicitly `'{ type t <: Int & Double; ... }` +-- [E057] Type Mismatch Error: tests/neg-macros/quote-type-variable-no-inference.scala:5:15 ---------------------------- +5 | case '[ F[t, t] ] => // warn // error + | ^ + | Type argument t does not conform to upper bound Double + | + | longer explanation available when compiling with `-explain` diff --git a/tests/neg-macros/quote-type-variable-no-inference.scala b/tests/neg-macros/quote-type-variable-no-inference.scala new file mode 100644 index 000000000000..de03f4445302 --- /dev/null +++ b/tests/neg-macros/quote-type-variable-no-inference.scala @@ -0,0 +1,8 @@ +import scala.quoted.* + +def test(x: Type[?])(using Quotes) = + x match + case '[ F[t, t] ] => // warn // error + case '[ type u <: Int & Double; F[u, u] ] => + +type F[x <: Int, y <: Double] diff --git a/tests/pos-macros/i10864/Macro_1.scala b/tests/pos-macros/i10864/Macro_1.scala new file mode 100644 index 000000000000..7cf1e1850a76 --- /dev/null +++ b/tests/pos-macros/i10864/Macro_1.scala @@ -0,0 +1,15 @@ +import scala.quoted._ + +case class T(t: Type[_]) + +object T { + def impl[T <: AnyKind](using tt: Type[T])(using Quotes): Expr[Unit] = { + val t = T(tt) + t.t match + case '[type x <: AnyKind; x] => // ok + case _ => quotes.reflect.report.error("not ok :(") + '{} + } + + inline def run[T <: AnyKind] = ${ impl[T] } +} diff --git a/tests/pos-macros/i10864/Test_2.scala b/tests/pos-macros/i10864/Test_2.scala new file mode 100644 index 000000000000..e93fa1302221 --- /dev/null +++ b/tests/pos-macros/i10864/Test_2.scala @@ -0,0 +1,4 @@ +def test = + T.run[List] + T.run[Map] + T.run[Tuple22] diff --git a/tests/pos-macros/i10864a/Macro_1.scala b/tests/pos-macros/i10864a/Macro_1.scala new file mode 100644 index 000000000000..67cac5f85abd --- /dev/null +++ b/tests/pos-macros/i10864a/Macro_1.scala @@ -0,0 +1,21 @@ +import scala.quoted._ + +case class T(t: Type[_]) + +object T { + def impl[T <: AnyKind](using tt: Type[T])(using Quotes): Expr[Unit] = { + val t = T(tt) + t.t match + case '[type x; x] => + assert(Type.show[x] == "scala.Int", Type.show[x]) + case '[type f[X]; f] => + assert(Type.show[f] == "[A >: scala.Nothing <: scala.Any] => scala.collection.immutable.List[A]", Type.show[f]) + case '[type f[X <: Int]; f] => + assert(Type.show[f] == "[T >: scala.Nothing <: scala.Int] => C[T]", Type.show[f]) + case '[type f <: AnyKind; f] => + assert(Type.show[f] == "[K >: scala.Nothing <: scala.Any, V >: scala.Nothing <: scala.Any] => scala.collection.immutable.Map[K, V]", Type.show[f]) + '{} + } + + inline def run[T <: AnyKind] = ${ impl[T] } +} diff --git a/tests/pos-macros/i10864a/Test_2.scala b/tests/pos-macros/i10864a/Test_2.scala new file mode 100644 index 000000000000..7a1596d0fa41 --- /dev/null +++ b/tests/pos-macros/i10864a/Test_2.scala @@ -0,0 +1,8 @@ +@main +def run = + T.run[Int] + T.run[C] + T.run[List] + T.run[Map] + +class C[T <: Int] diff --git a/tests/pos-macros/i11738.scala b/tests/pos-macros/i11738.scala new file mode 100644 index 000000000000..e1213a5dee6d --- /dev/null +++ b/tests/pos-macros/i11738.scala @@ -0,0 +1,8 @@ +import scala.quoted.* + +def blah[A](using Quotes, Type[A]): Expr[Unit] = + Type.of[A] match + case '[h *: t] => println(s"h = ${Type.show[h]}, t = ${Type.show[t]}") // ok + case '[type f[X]; f[a]] => println(s"f = ${Type.show[f]}, a = ${Type.show[a]}") // error + case _ => + '{()} diff --git a/tests/pos-macros/i7264.scala b/tests/pos-macros/i7264.scala index c87409561bee..82264402c768 100644 --- a/tests/pos-macros/i7264.scala +++ b/tests/pos-macros/i7264.scala @@ -3,5 +3,6 @@ class Foo { def f[T2](t: Type[T2])(using Quotes) = t match { case '[ *:[Int, t2] ] => Type.of[ *:[Int, t2] ] + case '[ type t <: Tuple; *:[t, t] ] => } } diff --git a/tests/pos-macros/multiline-quote-patterns.scala b/tests/pos-macros/multiline-quote-patterns.scala new file mode 100644 index 000000000000..a1f1649b6059 --- /dev/null +++ b/tests/pos-macros/multiline-quote-patterns.scala @@ -0,0 +1,62 @@ +import scala.quoted.* +def types(t: Type[?])(using Quotes) = t match { + case '[ + type t; + t + ] => + + case '[ + type t + t + ] => + + case '[ + type t + List[t] + ] => + + case '[ + type t; + type u; + Map[t, u] + ] => + + case '[ + type t + type u + Map[t, u] + ] => + + case '[ + type t; type u + t => u + ] => +} + +def expressions(x: Expr[Any])(using Quotes) = x match { + case '{ + type t; + $x: t + } => + + case '{ + type t + $x: t + } => + + case '{ + type t; + List() + } => + + case '{ + type t + List() + } => + + case '{ + type t + type u + Map.empty[t, u] + } => +}