Skip to content
Draft
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
10 changes: 6 additions & 4 deletions compiler/src/dotty/tools/dotc/ast/untpd.scala
Original file line number Diff line number Diff line change
Expand Up @@ -576,10 +576,12 @@ object untpd extends Trees.Instance[Untyped] with UntypedTreeInfo {
def makeSelfDef(name: TermName, tpt: Tree)(using Context): ValDef =
ValDef(name, tpt, EmptyTree).withFlags(PrivateLocal)

def makeTupleOrParens(ts: List[Tree])(using Context): Tree = ts match
case (t: NamedArg) :: Nil => Tuple(t :: Nil)
case t :: Nil => Parens(t)
case _ => Tuple(ts)
def makeTupleOrParens(ts: List[Tree], trailingComma: Boolean = false)(using Context): Tree =
if trailingComma then Tuple(ts)
else ts match
case (t: NamedArg) :: Nil => Tuple(t :: Nil)
case t :: Nil => Parens(t)
case _ => Tuple(ts)

def makeTuple(ts: List[Tree])(using Context): Tree = ts match
case (t: NamedArg) :: Nil => Tuple(t :: Nil)
Expand Down
142 changes: 114 additions & 28 deletions compiler/src/dotty/tools/dotc/parsing/Parsers.scala
Original file line number Diff line number Diff line change
Expand Up @@ -641,6 +641,19 @@ object Parsers {
def inParensWithCommas[T](body: => T): T = enclosedWithCommas(LPAREN, body)
def inBracketsWithCommas[T](body: => T): T = enclosedWithCommas(LBRACKET, body)

/** Like inParensWithCommas but also preserves trailing comma for tuple detection. */
def inParensWithTrailingComma[T](body: => T): T =
accept(LPAREN)
in.currentRegion.withPreserveTrailingComma:
val closing = RPAREN
val isEmpty = in.token == closing
val ts = body
if in.token != closing then
val prefix = if !isEmpty && canStartExprTokens3.contains(in.token) then "',' or " else ""
syntaxErrorOrIncomplete(ExpectedTokenButFound(closing, in.token, prefix))
if in.token == closing then in.nextToken()
ts

def inBracesOrIndented[T](body: => T, rewriteWithColon: Boolean = false): T =
if in.token == INDENT then
val rewriteToBraces = in.rewriteNoIndent
Expand All @@ -660,6 +673,15 @@ object Parsers {
commaSeparatedRest(part(), part)
}

/** Like commaSeparated but also detects trailing comma.
* Returns (list, trailingComma) where trailingComma is true if the list
* ended with a comma followed by a closing token.
*/
def commaSeparatedWithTrailingComma[T](part: () => T): (List[T], Boolean) =
in.currentRegion.withCommasExpected {
commaSeparatedRestWithTrailingComma(part(), part)
}

/** {`,` <part>}
*
* currentRegion.commasExpected has to be set separately.
Expand All @@ -673,6 +695,22 @@ object Parsers {
ts.toList
else leading :: Nil

/** Like commaSeparatedRest but also detects trailing comma.
* Returns (list, trailingComma) where trailingComma is true if the list
* ended with a comma followed by a closing token (RPAREN, RBRACKET, RBRACE).
* Used for tuple syntax like (A,) or (a,).
*/
def commaSeparatedRestWithTrailingComma[T](leading: T, part: () => T): (List[T], Boolean) =
if in.token == COMMA then
val ts = new ListBuffer[T] += leading
while in.token == COMMA do
in.nextToken()
if in.token == RPAREN || in.token == RBRACKET || in.token == RBRACE then
return (ts.toList, true)
ts += part()
(ts.toList, false)
else (leading :: Nil, false)

def maybeNamed(op: () => Tree): () => Tree = () =>
if isIdent && in.lookahead.token == EQUALS && sourceVersion.enablesNamedTuples then
atSpan(in.offset):
Expand Down Expand Up @@ -1792,34 +1830,46 @@ object Parsers {
if in.token == RPAREN then
in.nextToken()
functionRest(Nil)
else if in.token == COMMA then
// Empty tuple with comma: (,)
in.nextToken()
accept(RPAREN)
val tuple = atSpan(start)(makeTupleOrParens(Nil, trailingComma = true))
typeRest:
infixTypeRest(inContextBound):
refinedTypeRest:
withTypeRest:
annotTypeRest:
simpleTypeRest(tuple)
else
val paramStart = in.offset
def addErased() =
erasedArgs.addOne(isErased)
if isErased then in.skipToken()
addErased()
val args =
in.currentRegion.withCommasExpected:
funArgType() match
case Ident(name) if name != tpnme.WILDCARD && in.isColon =>
def funParam(start: Offset, mods: Modifiers) =
atSpan(start):
addErased()
typedFunParam(in.offset, ident(), imods)
commaSeparatedRest(
typedFunParam(paramStart, name.toTermName, imods),
() => funParam(in.offset, imods))
case t =>
def funArg() =
erasedArgs.addOne(false)
funArgType()
commaSeparatedRest(t, funArg)
val (args, trailingComma) =
in.currentRegion.withPreserveTrailingComma:
in.currentRegion.withCommasExpected:
funArgType() match
case Ident(name) if name != tpnme.WILDCARD && in.isColon =>
def funParam(start: Offset, mods: Modifiers) =
atSpan(start):
addErased()
typedFunParam(in.offset, ident(), imods)
commaSeparatedRestWithTrailingComma(
typedFunParam(paramStart, name.toTermName, imods),
() => funParam(in.offset, imods))
case t =>
def funArg() =
erasedArgs.addOne(false)
funArgType()
commaSeparatedRestWithTrailingComma(t, funArg)
accept(RPAREN)
if in.isArrow || isPureArrow || erasedArgs.contains(true) then
functionRest(args)
else
val tuple = atSpan(start):
makeTupleOrParens(args.mapConserve(convertToElem))
makeTupleOrParens(args.mapConserve(convertToElem), trailingComma)
typeRest:
infixTypeRest(inContextBound):
refinedTypeRest:
Expand Down Expand Up @@ -2120,7 +2170,8 @@ object Parsers {
def simpleType1() = simpleTypeRest {
if in.token == LPAREN then
atSpan(in.offset) {
makeTupleOrParens(inParensWithCommas(argTypes(namedOK = false, wildOK = true, tupleOK = true)))
val (ts, trailingComma) = inParensWithCommas(argTypesWithTrailingComma(namedOK = false, wildOK = true, tupleOK = true))
makeTupleOrParens(ts, trailingComma)
}
else if in.token == LBRACE then
atSpan(in.offset) { RefinedTypeTree(EmptyTree, refinement(indentOK = false)) }
Expand Down Expand Up @@ -2209,6 +2260,12 @@ object Parsers {
* NameAndType ::= id ‘:’ Type
*/
def argTypes(namedOK: Boolean, wildOK: Boolean, tupleOK: Boolean): List[Tree] =
argTypesWithTrailingComma(namedOK, wildOK, tupleOK)._1

/** Like argTypes but also returns whether a trailing comma was present.
* Used for tuple syntax like (A,) to force tuple interpretation.
*/
def argTypesWithTrailingComma(namedOK: Boolean, wildOK: Boolean, tupleOK: Boolean): (List[Tree], Boolean) =
def wildCardCheck(gen: Tree): Tree =
val t = gen
if wildOK then t else rejectWildcardType(t)
Expand All @@ -2234,12 +2291,12 @@ object Parsers {
NamedArg(name, argType())

if namedOK && (isIdent && in.lookahead.token == EQUALS) then
commaSeparated(() => namedTypeArg())
commaSeparatedWithTrailingComma(() => namedTypeArg())
else if tupleOK && isIdent && in.lookahead.isColon && sourceVersion.enablesNamedTuples then
commaSeparated(() => nameAndType())
commaSeparatedWithTrailingComma(() => nameAndType())
else
commaSeparated(() => typeArg())
end argTypes
commaSeparatedWithTrailingComma(() => typeArg())
end argTypesWithTrailingComma

def paramTypeOf(core: () => Tree): Tree =
if in.token == ARROW || isPureArrow(nme.PUREARROW) then
Expand Down Expand Up @@ -2852,7 +2909,10 @@ object Parsers {
placeholderParams = param :: placeholderParams
atSpan(start) { Ident(pname) }
case LPAREN =>
atSpan(in.offset) { makeTupleOrParens(inParensWithCommas(exprsInParensOrBindings())) }
atSpan(in.offset) {
val (ts, trailingComma) = inParensWithTrailingComma(exprsInParensOrBindingsWithTrailingComma())
makeTupleOrParens(ts, trailingComma)
}
case LBRACE | INDENT =>
canApply = false
blockExpr()
Expand Down Expand Up @@ -2948,12 +3008,21 @@ object Parsers {
end newExpr

/** ExprsInParens ::= ExprInParens {`,' ExprInParens}
* | NamedExprInParens {‘,’ NamedExprInParens}
* | NamedExprInParens {',' NamedExprInParens}
* Bindings ::= Binding {`,' Binding}
* NamedExprInParens ::= id '=' ExprInParens
*/
def exprsInParensOrBindings(): List[Tree] =
if in.token == RPAREN then Nil
exprsInParensOrBindingsWithTrailingComma()._1

/** Like exprsInParensOrBindings but also returns whether a trailing comma was present.
* Used for tuple syntax like (a,) to force tuple interpretation.
*/
def exprsInParensOrBindingsWithTrailingComma(): (List[Tree], Boolean) =
if in.token == RPAREN then (Nil, false)
else if in.token == COMMA then
in.nextToken() // skip the comma
(Nil, true) // empty tuple with comma: (,)
else in.currentRegion.withCommasExpected {
var isFormalParams = false
def exprOrBinding() =
Expand All @@ -2963,7 +3032,7 @@ object Parsers {
val t = maybeNamed(exprInParens)()
if t.isInstanceOf[ValDef] then isFormalParams = true
t
commaSeparatedRest(exprOrBinding(), exprOrBinding)
commaSeparatedRestWithTrailingComma(exprOrBinding(), exprOrBinding)
}

/** ParArgumentExprs ::= `(' [‘using’] [ExprsInParens] `)'
Expand Down Expand Up @@ -3375,7 +3444,10 @@ object Parsers {
case USCORE =>
wildcardIdent()
case LPAREN =>
atSpan(in.offset) { makeTupleOrParens(inParensWithCommas(patternsOpt())) }
atSpan(in.offset) {
val (ts, trailingComma) = inParensWithTrailingComma(patternsOptWithTrailingComma())
makeTupleOrParens(ts, trailingComma)
}
case QUOTE =>
simpleExpr(Location.InPattern)
case XMLSTART =>
Expand Down Expand Up @@ -3410,16 +3482,30 @@ object Parsers {
p

/** Patterns ::= Pattern [`,' Pattern]
* | NamedPattern {‘,’ NamedPattern}
* | NamedPattern {',' NamedPattern}
* NamedPattern ::= id '=' Pattern
*/
def patterns(location: Location = Location.InPattern): List[Tree] =
commaSeparated(maybeNamed(() => pattern(location)))
// check that patterns are all named or all unnamed is done at desugaring

/** Like patterns but also returns whether a trailing comma was present. */
def patternsWithTrailingComma(location: Location = Location.InPattern): (List[Tree], Boolean) =
commaSeparatedWithTrailingComma(maybeNamed(() => pattern(location)))

def patternsOpt(location: Location = Location.InPattern): List[Tree] =
if (in.token == RPAREN) Nil else patterns(location)

/** Like patternsOpt but also returns whether a trailing comma was present.
* Used for tuple syntax like (a,) to force tuple interpretation.
*/
def patternsOptWithTrailingComma(location: Location = Location.InPattern): (List[Tree], Boolean) =
if in.token == RPAREN then (Nil, false)
else if in.token == COMMA then
in.nextToken() // skip the comma
(Nil, true) // empty tuple with comma: (,)
else patternsWithTrailingComma(location)

/** ArgumentPatterns ::= ‘(’ [Patterns] ‘)’
* | ‘(’ [Patterns ‘,’] PatVar ‘*’ [‘,’ Patterns] ‘)’
*
Expand Down
12 changes: 12 additions & 0 deletions compiler/src/dotty/tools/dotc/parsing/Scanners.scala
Original file line number Diff line number Diff line change
Expand Up @@ -770,6 +770,7 @@ object Scanners {
peekAhead()
if isAfterLineEnd
&& currentRegion.commasExpected
&& !currentRegion.preserveTrailingComma
&& (token == RPAREN || token == RBRACKET || token == RBRACE || token == OUTDENT)
then
// encountered a trailing comma
Expand Down Expand Up @@ -1661,6 +1662,17 @@ object Scanners {

def commasExpected = myCommasExpected

private var myPreserveTrailingComma: Boolean = false

inline def withPreserveTrailingComma[T](inline op: => T): T =
val saved = myPreserveTrailingComma
myPreserveTrailingComma = true
val res = op
myPreserveTrailingComma = saved
res

def preserveTrailingComma = myPreserveTrailingComma

def toList: List[Region] =
this :: (if outer == null then Nil else outer.toList)

Expand Down
55 changes: 55 additions & 0 deletions tests/pos/tuple-trailing-comma.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// Test trailing comma syntax for tuples (full variant)
// This allows (A,) to be a single-element type tuple and (a,) to be a single-element value tuple
// Also allows trailing comma for multi-element tuples like (a, b,)

object TupleTrailingComma:
// Type tuples with trailing comma
type T1 = (Int,) // single-element type tuple
type T2 = (Int, String,) // trailing comma with multiple elements
type T3 = (Int, String) // regular tuple (should still work)

// Value tuples with trailing comma
val v1: (Int,) = (1,) // single-element value tuple
val v2 = (1, 2,) // trailing comma with multiple elements
val v3 = (1, 2) // regular tuple (should still work)

// Pattern matching with trailing comma
def test(x: Any): Unit = x match
case (a,) => println(s"single: $a")
case (a, b,) => println(s"pair: $a, $b")
case (a, b) => println(s"pair no trailing: $a, $b")
case _ => println("other")

// With newlines - trailing comma should still be recognized
val v4: (Int,
) = (1,
)

val v5: (Int, String,
) = (1, "hello",
)

type T4 = (Int,
)

type T5 = (Int, String,
)

def test2(x: Any): Unit = x match
case (a,
) => println(s"single with newline: $a")
case (a, b,
) => println(s"pair with newline: $a, $b")
case _ => println("other")

// Empty tuple syntax
val empty1: (,) = (,)
val empty2: (,
) = (,
)

type EmptyT = (,)

def testEmpty(x: Any): Unit = x match
case (,) => println("empty tuple")
case _ => println("other")
Loading