Skip to content

Add SplicePattern AST to parse and type quote pattern splices #17396

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

Merged
merged 1 commit into from
May 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 3 additions & 3 deletions compiler/src/dotty/tools/dotc/ast/Desugar.scala
Original file line number Diff line number Diff line change
Expand Up @@ -338,9 +338,9 @@ object desugar {
def quotedPattern(tree: untpd.Tree, expectedTpt: untpd.Tree)(using Context): untpd.Tree = {
def adaptToExpectedTpt(tree: untpd.Tree): untpd.Tree = tree match {
// Add the expected type as an ascription
case _: untpd.Splice =>
case _: untpd.SplicePattern =>
untpd.Typed(tree, expectedTpt).withSpan(tree.span)
case Typed(expr: untpd.Splice, tpt) =>
case Typed(expr: untpd.SplicePattern, tpt) =>
cpy.Typed(tree)(expr, untpd.makeAndType(tpt, expectedTpt).withSpan(tpt.span))

// Propagate down the expected type to the leafs of the expression
Expand Down Expand Up @@ -1979,7 +1979,7 @@ object desugar {
case Quote(body, _) =>
new UntypedTreeTraverser {
def traverse(tree: untpd.Tree)(using Context): Unit = tree match {
case Splice(expr) => collect(expr)
case SplicePattern(body, _) => collect(body)
case _ => traverseChildren(tree)
}
}.traverse(body)
Expand Down
25 changes: 25 additions & 0 deletions compiler/src/dotty/tools/dotc/ast/Trees.scala
Original file line number Diff line number Diff line change
Expand Up @@ -737,6 +737,22 @@ object Trees {
type ThisTree[+T <: Untyped] = Splice[T]
}

/** A tree representing a pattern splice `${ pattern }`, `$ident` or `$ident(args*)` in a quote pattern.
*
* Parser will only create `${ pattern }` and `$ident`, hence they will not have args.
* While typing, the `$ident(args*)` the args are identified and desugared into a `SplicePattern`
* containing them.
Comment on lines +742 to +744
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not have the parser take care of this since it's based purely on syntactic information?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the next PR I will introduce QuotePattern. At that point SplicePatterns will be kept after parser and until we encode the pattern into unapply and '{..}s.

*
* SplicePattern are removed after typing the pattern and are not present in TASTy.
*
* @param body The tree that was spliced
* @param args The arguments of the splice (the HOAS arguments)
*/
case class SplicePattern[+T <: Untyped] private[ast] (body: Tree[T], args: List[Tree[T]])(implicit @constructorOnly src: SourceFile)
extends TermTree[T] {
type ThisTree[+T <: Untyped] = SplicePattern[T]
}

/** A type tree that represents an existing or inferred type */
case class TypeTree[+T <: Untyped]()(implicit @constructorOnly src: SourceFile)
extends DenotingTree[T] with TypTree[T] {
Expand Down Expand Up @@ -1147,6 +1163,7 @@ object Trees {
type Inlined = Trees.Inlined[T]
type Quote = Trees.Quote[T]
type Splice = Trees.Splice[T]
type SplicePattern = Trees.SplicePattern[T]
type TypeTree = Trees.TypeTree[T]
type InferredTypeTree = Trees.InferredTypeTree[T]
type SingletonTypeTree = Trees.SingletonTypeTree[T]
Expand Down Expand Up @@ -1325,6 +1342,10 @@ object Trees {
case tree: Splice if (expr eq tree.expr) => tree
case _ => finalize(tree, untpd.Splice(expr)(sourceFile(tree)))
}
def SplicePattern(tree: Tree)(body: Tree, args: List[Tree])(using Context): SplicePattern = tree match {
case tree: SplicePattern if (body eq tree.body) && (args eq tree.args) => tree
case _ => finalize(tree, untpd.SplicePattern(body, args)(sourceFile(tree)))
}
def SingletonTypeTree(tree: Tree)(ref: Tree)(using Context): SingletonTypeTree = tree match {
case tree: SingletonTypeTree if (ref eq tree.ref) => tree
case _ => finalize(tree, untpd.SingletonTypeTree(ref)(sourceFile(tree)))
Expand Down Expand Up @@ -1566,6 +1587,8 @@ object Trees {
cpy.Quote(tree)(transform(body)(using quoteContext), transform(tags))
case tree @ Splice(expr) =>
cpy.Splice(tree)(transform(expr)(using spliceContext))
case tree @ SplicePattern(body, args) =>
cpy.SplicePattern(tree)(transform(body)(using spliceContext), transform(args))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why isn't spliceContext used for the arguments?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Arguments of the SplicePattern are defined and references within the quoted pattern. The definition and references are at the same level.

case '{ (x: T) => $f(x) } =>

In this example f is the body, which is at a lower staging level and usable in the RHS of the =>. On the other hand x is defined in the pattern (x: T) and then referenced in the pattern within the SplicePattern in (x).

case tree @ Hole(isTerm, idx, args, content) =>
cpy.Hole(tree)(isTerm, idx, transform(args), transform(content))
case _ =>
Expand Down Expand Up @@ -1711,6 +1734,8 @@ object Trees {
this(this(x, body)(using quoteContext), tags)
case Splice(expr) =>
this(x, expr)(using spliceContext)
case SplicePattern(body, args) =>
this(this(x, body)(using spliceContext), args)
case Hole(_, _, args, content) =>
this(this(x, args), content)
case _ =>
Expand Down
1 change: 1 addition & 0 deletions compiler/src/dotty/tools/dotc/ast/untpd.scala
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,7 @@ object untpd extends Trees.Instance[Untyped] with UntypedTreeInfo {
def Inlined(call: tpd.Tree, bindings: List[MemberDef], expansion: Tree)(implicit src: SourceFile): Inlined = new Inlined(call, bindings, expansion)
def Quote(body: Tree, tags: List[Tree])(implicit src: SourceFile): Quote = new Quote(body, tags)
def Splice(expr: Tree)(implicit src: SourceFile): Splice = new Splice(expr)
def SplicePattern(body: Tree, args: List[Tree])(implicit src: SourceFile): SplicePattern = new SplicePattern(body, args)
def TypeTree()(implicit src: SourceFile): TypeTree = new TypeTree()
def InferredTypeTree()(implicit src: SourceFile): TypeTree = new InferredTypeTree()
def SingletonTypeTree(ref: Tree)(implicit src: SourceFile): SingletonTypeTree = new SingletonTypeTree(ref)
Expand Down
4 changes: 3 additions & 1 deletion compiler/src/dotty/tools/dotc/parsing/Parsers.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1750,10 +1750,10 @@ object Parsers {
def splice(isType: Boolean): Tree =
val start = in.offset
atSpan(in.offset) {
val inPattern = (staged & StageKind.QuotedPattern) != 0
val expr =
if (in.name.length == 1) {
in.nextToken()
val inPattern = (staged & StageKind.QuotedPattern) != 0
withinStaged(StageKind.Spliced)(if (inPattern) inBraces(pattern()) else stagedBlock())
}
else atSpan(in.offset + 1) {
Expand All @@ -1769,6 +1769,8 @@ object Parsers {
else "To use a given Type[T] in a quote just write T directly"
syntaxError(em"$msg\n\nHint: $hint", Span(start, in.lastOffset))
Ident(nme.ERROR.toTypeName)
else if inPattern then
SplicePattern(expr, Nil)
else
Splice(expr)
}
Expand Down
6 changes: 6 additions & 0 deletions compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala
Original file line number Diff line number Diff line change
Expand Up @@ -735,6 +735,12 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) {
case Splice(expr) =>
val spliceTypeText = (keywordStr("[") ~ toTextGlobal(tree.typeOpt) ~ keywordStr("]")).provided(printDebug && tree.typeOpt.exists)
keywordStr("$") ~ spliceTypeText ~ keywordStr("{") ~ toTextGlobal(expr) ~ keywordStr("}")
case SplicePattern(pattern, args) =>
val spliceTypeText = (keywordStr("[") ~ toTextGlobal(tree.typeOpt) ~ keywordStr("]")).provided(printDebug && tree.typeOpt.exists)
keywordStr("$") ~ spliceTypeText ~ {
if args.isEmpty then keywordStr("{") ~ inPattern(toText(pattern)) ~ keywordStr("}")
else toText(pattern.symbol.name) ~ "(" ~ toTextGlobal(args, ", ") ~ ")"
}
case Hole(isTerm, idx, args, content) =>
val (prefix, postfix) = if isTerm then ("{{{", "}}}") else ("[[[", "]]]")
val argsText = toTextGlobal(args, ", ")
Expand Down
2 changes: 1 addition & 1 deletion compiler/src/dotty/tools/dotc/typer/Applications.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1097,7 +1097,7 @@ trait Applications extends Compatibility {
}
else {
val app = tree.fun match
case _: untpd.Splice if ctx.mode.is(Mode.QuotedPattern) => typedAppliedSplice(tree, pt)
case _: untpd.SplicePattern => typedAppliedSplice(tree, pt)
case _ => realApply
app match {
case Apply(fn @ Select(left, _), right :: Nil) if fn.hasType =>
Expand Down
130 changes: 58 additions & 72 deletions compiler/src/dotty/tools/dotc/typer/QuotesAndSplices.scala
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import dotty.tools.dotc.core.Types._
import dotty.tools.dotc.inlines.PrepareInlineable
import dotty.tools.dotc.staging.StagingLevel.*
import dotty.tools.dotc.transform.SymUtils._
import dotty.tools.dotc.typer.ErrorReporting.errorTree
import dotty.tools.dotc.typer.Implicits._
import dotty.tools.dotc.typer.Inferencing._
import dotty.tools.dotc.util.Spans._
Expand Down Expand Up @@ -74,45 +75,55 @@ trait QuotesAndSplices {
def typedSplice(tree: untpd.Splice, pt: Type)(using Context): Tree = {
record("typedSplice")
checkSpliceOutsideQuote(tree)
assert(!ctx.mode.is(Mode.QuotedPattern))
tree.expr match {
case untpd.Quote(innerExpr, Nil) if innerExpr.isTerm =>
report.warning("Canceled quote directly inside a splice. ${ '{ XYZ } } is equivalent to XYZ.", tree.srcPos)
return typed(innerExpr, pt)
case _ =>
}
if (ctx.mode.is(Mode.QuotedPattern))
if (isFullyDefined(pt, ForceDegree.flipBottom)) {
def spliceOwner(ctx: Context): Symbol =
if (ctx.mode.is(Mode.QuotedPattern)) spliceOwner(ctx.outer) else ctx.owner
val pat = typedPattern(tree.expr, defn.QuotedExprClass.typeRef.appliedTo(pt))(
using spliceContext.retractMode(Mode.QuotedPattern).addMode(Mode.Pattern).withOwner(spliceOwner(ctx)))
val baseType = pat.tpe.baseType(defn.QuotedExprClass)
val argType = if baseType != NoType then baseType.argTypesHi.head else defn.NothingType
Splice(pat, argType).withSpan(tree.span)
}
else {
report.error(em"Type must be fully defined.\nConsider annotating the splice using a type ascription:\n ($tree: XYZ).", tree.expr.srcPos)
tree.withType(UnspecifiedErrorType)
}
else {
if (level == 0) {
// Mark the first inline method from the context as a macro
def markAsMacro(c: Context): Unit =
if (c.owner eq c.outer.owner) markAsMacro(c.outer)
else if (c.owner.isInlineMethod) c.owner.setFlag(Macro)
else if (!c.outer.owner.is(Package)) markAsMacro(c.outer)
else assert(ctx.reporter.hasErrors) // Did not find inline def to mark as macro
markAsMacro(ctx)
}

// TODO typecheck directly (without `exprSplice`)
val internalSplice =
untpd.Apply(untpd.ref(defn.QuotedRuntime_exprSplice.termRef), tree.expr)
typedApply(internalSplice, pt)(using spliceContext).withSpan(tree.span) match
case tree @ Apply(TypeApply(_, tpt :: Nil), spliced :: Nil) if tree.symbol == defn.QuotedRuntime_exprSplice =>
cpy.Splice(tree)(spliced)
case tree => tree
if (level == 0) {
// Mark the first inline method from the context as a macro
def markAsMacro(c: Context): Unit =
if (c.owner eq c.outer.owner) markAsMacro(c.outer)
else if (c.owner.isInlineMethod) c.owner.setFlag(Macro)
else if (!c.outer.owner.is(Package)) markAsMacro(c.outer)
else assert(ctx.reporter.hasErrors) // Did not find inline def to mark as macro
markAsMacro(ctx)
}

// TODO typecheck directly (without `exprSplice`)
val internalSplice =
untpd.Apply(untpd.ref(defn.QuotedRuntime_exprSplice.termRef), tree.expr)
typedApply(internalSplice, pt)(using spliceContext).withSpan(tree.span) match
case tree @ Apply(TypeApply(_, tpt :: Nil), spliced :: Nil) if tree.symbol == defn.QuotedRuntime_exprSplice =>
cpy.Splice(tree)(spliced)
case tree => tree
}

def typedSplicePattern(tree: untpd.SplicePattern, pt: Type)(using Context): Tree = {
record("typedSplicePattern")
if isFullyDefined(pt, ForceDegree.flipBottom) then
def patternOuterContext(ctx: Context): Context =
if (ctx.mode.is(Mode.QuotedPattern)) patternOuterContext(ctx.outer) else ctx
val typedArgs = tree.args.map {
case arg: untpd.Ident =>
typedExpr(arg)
case arg =>
report.error("Open pattern expected an identifier", arg.srcPos)
EmptyTree
}
for arg <- typedArgs if arg.symbol.is(Mutable) do // TODO support these patterns. Possibly using scala.quoted.util.Var
report.error("References to `var`s cannot be used in higher-order pattern", arg.srcPos)
val argTypes = typedArgs.map(_.tpe.widenTermRefExpr)
val patType = if tree.args.isEmpty then pt else defn.FunctionOf(argTypes, pt)
val pat = typedPattern(tree.body, defn.QuotedExprClass.typeRef.appliedTo(patType))(
using spliceContext.retractMode(Mode.QuotedPattern).addMode(Mode.Pattern).withOwner(patternOuterContext(ctx).owner))
val baseType = pat.tpe.baseType(defn.QuotedExprClass)
val argType = if baseType.exists then baseType.argTypesHi.head else defn.NothingType
untpd.cpy.SplicePattern(tree)(pat, typedArgs).withType(pt)
else
errorTree(tree, em"Type must be fully defined.\nConsider annotating the splice using a type ascription:\n ($tree: XYZ).", tree.body.srcPos)
}

def typedHole(tree: untpd.Hole, pt: Type)(using Context): Tree =
Expand All @@ -127,29 +138,17 @@ trait QuotesAndSplices {
*/
def typedAppliedSplice(tree: untpd.Apply, pt: Type)(using Context): Tree = {
assert(ctx.mode.is(Mode.QuotedPattern))
val untpd.Apply(splice: untpd.Splice, args) = tree: @unchecked
def isInBraces: Boolean = splice.span.end != splice.expr.span.end
if !isFullyDefined(pt, ForceDegree.flipBottom) then
report.error(em"Type must be fully defined.", splice.srcPos)
tree.withType(UnspecifiedErrorType)
else if isInBraces then // ${x}(...) match an application
val untpd.Apply(splice: untpd.SplicePattern, args) = tree: @unchecked
def isInBraces: Boolean = splice.span.end != splice.body.span.end
if isInBraces then // ${x}(...) match an application
val typedArgs = args.map(arg => typedExpr(arg))
val argTypes = typedArgs.map(_.tpe.widenTermRefExpr)
val splice1 = typedSplice(splice, defn.FunctionOf(argTypes, pt))
Apply(splice1.select(nme.apply), typedArgs).withType(pt).withSpan(tree.span)
val splice1 = typedSplicePattern(splice, defn.FunctionOf(argTypes, pt))
untpd.cpy.Apply(tree)(splice1.select(nme.apply), typedArgs).withType(pt)
else // $x(...) higher-order quasipattern
val typedArgs = args.map {
case arg: untpd.Ident =>
typedExpr(arg)
case arg =>
report.error("Open pattern expected an identifier", arg.srcPos)
EmptyTree
}
if args.isEmpty then
report.error("Missing arguments for open pattern", tree.srcPos)
val argTypes = typedArgs.map(_.tpe.widenTermRefExpr)
val typedPat = typedSplice(splice, defn.FunctionOf(argTypes, pt))
ref(defn.QuotedRuntimePatterns_patternHigherOrderHole).appliedToType(pt).appliedTo(typedPat, SeqLiteral(typedArgs, TypeTree(defn.AnyType)))
report.error("Missing arguments for open pattern", tree.srcPos)
typedSplicePattern(untpd.cpy.SplicePattern(tree)(splice.body, args), pt)
}

/** Type a pattern variable name `t` in quote pattern as `${given t$giveni: Type[t @ _]}`.
Expand Down Expand Up @@ -227,29 +226,16 @@ trait QuotesAndSplices {
val freshTypeBindingsBuff = new mutable.ListBuffer[Tree]
val typePatBuf = new mutable.ListBuffer[Tree]
override def transform(tree: Tree)(using Context) = tree match {
case Typed(splice: Splice, tpt) if !tpt.tpe.derivesFrom(defn.RepeatedParamClass) =>
case Typed(splice @ SplicePattern(pat, Nil), tpt) if !tpt.tpe.derivesFrom(defn.RepeatedParamClass) =>
transform(tpt) // Collect type bindings
transform(splice)
case Apply(TypeApply(fn, targs), Splice(pat) :: args :: Nil) if fn.symbol == defn.QuotedRuntimePatterns_patternHigherOrderHole =>
args match // TODO support these patterns. Possibly using scala.quoted.util.Var
case SeqLiteral(args, _) =>
for arg <- args; if arg.symbol.is(Mutable) do
report.error("References to `var`s cannot be used in higher-order pattern", arg.srcPos)
try ref(defn.QuotedRuntimePatterns_higherOrderHole.termRef).appliedToTypeTrees(targs).appliedTo(args).withSpan(tree.span)
finally {
val patType = pat.tpe.widen
val patType1 = patType.translateFromRepeated(toArray = false)
val pat1 = if (patType eq patType1) pat else pat.withType(patType1)
patBuf += pat1
}
case Splice(pat) =>
try ref(defn.QuotedRuntimePatterns_patternHole.termRef).appliedToType(tree.tpe).withSpan(tree.span)
finally {
val patType = pat.tpe.widen
val patType1 = patType.translateFromRepeated(toArray = false)
val pat1 = if (patType eq patType1) pat else pat.withType(patType1)
patBuf += pat1
}
case SplicePattern(pat, args) =>
val patType = pat.tpe.widen
val patType1 = patType.translateFromRepeated(toArray = false)
val pat1 = if (patType eq patType1) pat else pat.withType(patType1)
patBuf += pat1
if args.isEmpty then ref(defn.QuotedRuntimePatterns_patternHole.termRef).appliedToType(tree.tpe).withSpan(tree.span)
else ref(defn.QuotedRuntimePatterns_higherOrderHole.termRef).appliedToType(tree.tpe).appliedTo(SeqLiteral(args, TypeTree(defn.AnyType))).withSpan(tree.span)
case Select(pat: Bind, _) if tree.symbol.isTypeSplice =>
val sym = tree.tpe.dealias.typeSymbol
if sym.exists then
Expand Down
1 change: 1 addition & 0 deletions compiler/src/dotty/tools/dotc/typer/Typer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3096,6 +3096,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
case untpd.EmptyTree => tpd.EmptyTree
case tree: untpd.Quote => typedQuote(tree, pt)
case tree: untpd.Splice => typedSplice(tree, pt)
case tree: untpd.SplicePattern => typedSplicePattern(tree, pt)
case tree: untpd.MacroTree => report.error("Unexpected macro", tree.srcPos); tpd.nullLiteral // ill-formed code may reach here
case tree: untpd.Hole => typedHole(tree, pt)
case _ => typedUnadapted(desugar(tree, pt), pt, locked)
Expand Down