Skip to content

Commit 82756b2

Browse files
committed
Add preserveTrailingComma flag and empty tuple (,) syntax
- Add preserveTrailingComma flag to Scanner Region class to prevent trailing comma from being silently consumed in tuple contexts - Update Parser to use the flag in expression, type, and pattern parsing - Implement empty tuple syntax (,) for expressions, types, and patterns - Add inParensWithTrailingComma helper for cleaner tuple parsing - Ensure (a,<newline>) and (a, b,<newline>) are correctly parsed as tuples - Add tests for single/multi-element tuples, empty tuples, and multiline variants
1 parent 2fc86aa commit 82756b2

File tree

3 files changed

+98
-19
lines changed

3 files changed

+98
-19
lines changed

compiler/src/dotty/tools/dotc/parsing/Parsers.scala

Lines changed: 50 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -641,6 +641,19 @@ object Parsers {
641641
def inParensWithCommas[T](body: => T): T = enclosedWithCommas(LPAREN, body)
642642
def inBracketsWithCommas[T](body: => T): T = enclosedWithCommas(LBRACKET, body)
643643

644+
/** Like inParensWithCommas but also preserves trailing comma for tuple detection. */
645+
def inParensWithTrailingComma[T](body: => T): T =
646+
accept(LPAREN)
647+
in.currentRegion.withPreserveTrailingComma:
648+
val closing = RPAREN
649+
val isEmpty = in.token == closing
650+
val ts = body
651+
if in.token != closing then
652+
val prefix = if !isEmpty && canStartExprTokens3.contains(in.token) then "',' or " else ""
653+
syntaxErrorOrIncomplete(ExpectedTokenButFound(closing, in.token, prefix))
654+
if in.token == closing then in.nextToken()
655+
ts
656+
644657
def inBracesOrIndented[T](body: => T, rewriteWithColon: Boolean = false): T =
645658
if in.token == INDENT then
646659
val rewriteToBraces = in.rewriteNoIndent
@@ -1817,28 +1830,40 @@ object Parsers {
18171830
if in.token == RPAREN then
18181831
in.nextToken()
18191832
functionRest(Nil)
1833+
else if in.token == COMMA then
1834+
// Empty tuple with comma: (,)
1835+
in.nextToken()
1836+
accept(RPAREN)
1837+
val tuple = atSpan(start)(makeTupleOrParens(Nil, trailingComma = true))
1838+
typeRest:
1839+
infixTypeRest(inContextBound):
1840+
refinedTypeRest:
1841+
withTypeRest:
1842+
annotTypeRest:
1843+
simpleTypeRest(tuple)
18201844
else
18211845
val paramStart = in.offset
18221846
def addErased() =
18231847
erasedArgs.addOne(isErased)
18241848
if isErased then in.skipToken()
18251849
addErased()
18261850
val (args, trailingComma) =
1827-
in.currentRegion.withCommasExpected:
1828-
funArgType() match
1829-
case Ident(name) if name != tpnme.WILDCARD && in.isColon =>
1830-
def funParam(start: Offset, mods: Modifiers) =
1831-
atSpan(start):
1832-
addErased()
1833-
typedFunParam(in.offset, ident(), imods)
1834-
commaSeparatedRestWithTrailingComma(
1835-
typedFunParam(paramStart, name.toTermName, imods),
1836-
() => funParam(in.offset, imods))
1837-
case t =>
1838-
def funArg() =
1839-
erasedArgs.addOne(false)
1840-
funArgType()
1841-
commaSeparatedRestWithTrailingComma(t, funArg)
1851+
in.currentRegion.withPreserveTrailingComma:
1852+
in.currentRegion.withCommasExpected:
1853+
funArgType() match
1854+
case Ident(name) if name != tpnme.WILDCARD && in.isColon =>
1855+
def funParam(start: Offset, mods: Modifiers) =
1856+
atSpan(start):
1857+
addErased()
1858+
typedFunParam(in.offset, ident(), imods)
1859+
commaSeparatedRestWithTrailingComma(
1860+
typedFunParam(paramStart, name.toTermName, imods),
1861+
() => funParam(in.offset, imods))
1862+
case t =>
1863+
def funArg() =
1864+
erasedArgs.addOne(false)
1865+
funArgType()
1866+
commaSeparatedRestWithTrailingComma(t, funArg)
18421867
accept(RPAREN)
18431868
if in.isArrow || isPureArrow || erasedArgs.contains(true) then
18441869
functionRest(args)
@@ -2885,7 +2910,7 @@ object Parsers {
28852910
atSpan(start) { Ident(pname) }
28862911
case LPAREN =>
28872912
atSpan(in.offset) {
2888-
val (ts, trailingComma) = inParensWithCommas(exprsInParensOrBindingsWithTrailingComma())
2913+
val (ts, trailingComma) = inParensWithTrailingComma(exprsInParensOrBindingsWithTrailingComma())
28892914
makeTupleOrParens(ts, trailingComma)
28902915
}
28912916
case LBRACE | INDENT =>
@@ -2995,6 +3020,9 @@ object Parsers {
29953020
*/
29963021
def exprsInParensOrBindingsWithTrailingComma(): (List[Tree], Boolean) =
29973022
if in.token == RPAREN then (Nil, false)
3023+
else if in.token == COMMA then
3024+
in.nextToken() // skip the comma
3025+
(Nil, true) // empty tuple with comma: (,)
29983026
else in.currentRegion.withCommasExpected {
29993027
var isFormalParams = false
30003028
def exprOrBinding() =
@@ -3417,7 +3445,7 @@ object Parsers {
34173445
wildcardIdent()
34183446
case LPAREN =>
34193447
atSpan(in.offset) {
3420-
val (ts, trailingComma) = inParensWithCommas(patternsOptWithTrailingComma())
3448+
val (ts, trailingComma) = inParensWithTrailingComma(patternsOptWithTrailingComma())
34213449
makeTupleOrParens(ts, trailingComma)
34223450
}
34233451
case QUOTE =>
@@ -3472,7 +3500,11 @@ object Parsers {
34723500
* Used for tuple syntax like (a,) to force tuple interpretation.
34733501
*/
34743502
def patternsOptWithTrailingComma(location: Location = Location.InPattern): (List[Tree], Boolean) =
3475-
if in.token == RPAREN then (Nil, false) else patternsWithTrailingComma(location)
3503+
if in.token == RPAREN then (Nil, false)
3504+
else if in.token == COMMA then
3505+
in.nextToken() // skip the comma
3506+
(Nil, true) // empty tuple with comma: (,)
3507+
else patternsWithTrailingComma(location)
34763508

34773509
/** ArgumentPatterns ::= ‘(’ [Patterns] ‘)’
34783510
* | ‘(’ [Patterns ‘,’] PatVar ‘*’ [‘,’ Patterns] ‘)’

compiler/src/dotty/tools/dotc/parsing/Scanners.scala

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -770,6 +770,7 @@ object Scanners {
770770
peekAhead()
771771
if isAfterLineEnd
772772
&& currentRegion.commasExpected
773+
&& !currentRegion.preserveTrailingComma
773774
&& (token == RPAREN || token == RBRACKET || token == RBRACE || token == OUTDENT)
774775
then
775776
// encountered a trailing comma
@@ -1661,6 +1662,17 @@ object Scanners {
16611662

16621663
def commasExpected = myCommasExpected
16631664

1665+
private var myPreserveTrailingComma: Boolean = false
1666+
1667+
inline def withPreserveTrailingComma[T](inline op: => T): T =
1668+
val saved = myPreserveTrailingComma
1669+
myPreserveTrailingComma = true
1670+
val res = op
1671+
myPreserveTrailingComma = saved
1672+
res
1673+
1674+
def preserveTrailingComma = myPreserveTrailingComma
1675+
16641676
def toList: List[Region] =
16651677
this :: (if outer == null then Nil else outer.toList)
16661678

tests/pos/tuple-trailing-comma.scala

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
// Test trailing comma syntax for tuples (B variant)
1+
// Test trailing comma syntax for tuples (full variant)
22
// This allows (A,) to be a single-element type tuple and (a,) to be a single-element value tuple
3+
// Also allows trailing comma for multi-element tuples like (a, b,)
34

45
object TupleTrailingComma:
56
// Type tuples with trailing comma
@@ -18,3 +19,37 @@ object TupleTrailingComma:
1819
case (a, b,) => println(s"pair: $a, $b")
1920
case (a, b) => println(s"pair no trailing: $a, $b")
2021
case _ => println("other")
22+
23+
// With newlines - trailing comma should still be recognized
24+
val v4: (Int,
25+
) = (1,
26+
)
27+
28+
val v5: (Int, String,
29+
) = (1, "hello",
30+
)
31+
32+
type T4 = (Int,
33+
)
34+
35+
type T5 = (Int, String,
36+
)
37+
38+
def test2(x: Any): Unit = x match
39+
case (a,
40+
) => println(s"single with newline: $a")
41+
case (a, b,
42+
) => println(s"pair with newline: $a, $b")
43+
case _ => println("other")
44+
45+
// Empty tuple syntax
46+
val empty1: (,) = (,)
47+
val empty2: (,
48+
) = (,
49+
)
50+
51+
type EmptyT = (,)
52+
53+
def testEmpty(x: Any): Unit = x match
54+
case (,) => println("empty tuple")
55+
case _ => println("other")

0 commit comments

Comments
 (0)