Skip to content

Make Named Tuples a stable feature in 3.7 #22753

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 6 commits into from
Mar 10, 2025
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
2 changes: 0 additions & 2 deletions compiler/src/dotty/tools/dotc/config/Feature.scala
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ object Feature:
val pureFunctions = experimental("pureFunctions")
val captureChecking = experimental("captureChecking")
val into = experimental("into")
val namedTuples = experimental("namedTuples")
val modularity = experimental("modularity")
val betterMatchTypeExtractors = experimental("betterMatchTypeExtractors")
val quotedPatternsWithPolymorphicFunctions = experimental("quotedPatternsWithPolymorphicFunctions")
Expand Down Expand Up @@ -64,7 +63,6 @@ object Feature:
(pureFunctions, "Enable pure functions for capture checking"),
(captureChecking, "Enable experimental capture checking"),
(into, "Allow into modifier on parameter types"),
(namedTuples, "Allow named tuples"),
(modularity, "Enable experimental modularity features"),
(betterMatchTypeExtractors, "Enable better match type extractors"),
(betterFors, "Enable improvements in `for` comprehensions")
Expand Down
11 changes: 9 additions & 2 deletions compiler/src/dotty/tools/dotc/parsing/Parsers.scala
Original file line number Diff line number Diff line change
Expand Up @@ -671,7 +671,7 @@ object Parsers {
else leading :: Nil

def maybeNamed(op: () => Tree): () => Tree = () =>
if isIdent && in.lookahead.token == EQUALS && in.featureEnabled(Feature.namedTuples) then
if isIdent && in.lookahead.token == EQUALS && sourceVersion.enablesNamedTuples then
atSpan(in.offset):
val name = ident()
in.nextToken()
Expand Down Expand Up @@ -1160,6 +1160,13 @@ object Parsers {
patch(source, infixOp.span, asApply.show(using ctx.withoutColors))
asApply // allow to use pre-3.6 syntax in migration mode
else infixOp
case Parens(assign @ Assign(ident, value)) if !isNamedTupleOperator =>
report.errorOrMigrationWarning(DeprecatedInfixNamedArgumentSyntax(), infixOp.right.srcPos, MigrationVersion.AmbiguousNamedTupleSyntax)
if MigrationVersion.AmbiguousNamedTupleSyntax.needsPatch then
val asApply = cpy.Apply(infixOp)(Select(opInfo.operand, opInfo.operator.name), assign :: Nil)
patch(source, infixOp.span, asApply.show(using ctx.withoutColors))
asApply // allow to use pre-3.6 syntax in migration mode
else infixOp
case _ => infixOp
}

Expand Down Expand Up @@ -2177,7 +2184,7 @@ object Parsers {

if namedOK && isIdent && in.lookahead.token == EQUALS then
commaSeparated(() => namedArgType())
else if tupleOK && isIdent && in.lookahead.isColon && in.featureEnabled(Feature.namedTuples) then
else if tupleOK && isIdent && in.lookahead.isColon && sourceVersion.enablesNamedTuples then
commaSeparated(() => namedElem())
else
commaSeparated(() => argType())
Expand Down
6 changes: 3 additions & 3 deletions compiler/src/dotty/tools/dotc/reporting/messages.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3362,7 +3362,7 @@ end QuotedTypeMissing

final class DeprecatedAssignmentSyntax(key: Name, value: untpd.Tree)(using Context) extends SyntaxMsg(DeprecatedAssignmentSyntaxID):
override protected def msg(using Context): String =
i"""Deprecated syntax: in the future it would be interpreted as a named tuple with one element,
i"""Deprecated syntax: since 3.7 this is interpreted as a named tuple with one element,
|not as an assignment.
|
|To assign a value, use curly braces: `{${key} = ${value}}`."""
Expand All @@ -3372,9 +3372,9 @@ final class DeprecatedAssignmentSyntax(key: Name, value: untpd.Tree)(using Conte

class DeprecatedInfixNamedArgumentSyntax()(using Context) extends SyntaxMsg(DeprecatedInfixNamedArgumentSyntaxID):
def msg(using Context) =
i"""Deprecated syntax: infix named arguments lists are deprecated; in the future it would be interpreted as a single name tuple argument.
i"""Deprecated syntax: infix named arguments lists are deprecated; since 3.7 it is interpreted as a single name tuple argument.
|To avoid this warning, either remove the argument names or use dotted selection."""
+ Message.rewriteNotice("This", version = SourceVersion.`3.6-migration`)
+ Message.rewriteNotice("This", version = SourceVersion.`3.7-migration`)

def explain(using Context) = ""

Expand Down
30 changes: 18 additions & 12 deletions compiler/src/dotty/tools/dotc/typer/Typer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -825,7 +825,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
def tryNamedTupleSelection() =
val namedTupleElems = qual.tpe.widenDealias.namedTupleElementTypes(true)
val nameIdx = namedTupleElems.indexWhere(_._1 == selName)
if nameIdx >= 0 && Feature.enabled(Feature.namedTuples) then
if nameIdx >= 0 && sourceVersion.enablesNamedTuples then
typed(
untpd.Apply(
untpd.Select(untpd.TypedSplice(qual), nme.apply),
Expand Down Expand Up @@ -3500,19 +3500,22 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
/** Checks if `tree` is a named tuple with one element that could be
* interpreted as an assignment, such as `(x = 1)`. If so, issues a warning.
*/
def checkDeprecatedAssignmentSyntax(tree: untpd.Tuple)(using Context): Unit =
tree.trees match
case List(NamedArg(name, value)) =>
def checkDeprecatedAssignmentSyntax(tree: untpd.Tuple | untpd.Parens)(using Context): Unit =
val assignmentArgs = tree match {
case untpd.Tuple(List(NamedArg(name, value))) =>
val tmpCtx = ctx.fresh.setNewTyperState()
typedAssign(untpd.Assign(untpd.Ident(name), value), WildcardType)(using tmpCtx)
if !tmpCtx.reporter.hasErrors then
// If there are no errors typing the above, then the named tuple is
// ambiguous and we issue a warning.
report.migrationWarning(DeprecatedAssignmentSyntax(name, value), tree.srcPos)
if MigrationVersion.AmbiguousNamedTupleSyntax.needsPatch then
patch(tree.source, Span(tree.span.start, tree.span.start + 1), "{")
patch(tree.source, Span(tree.span.end - 1, tree.span.end), "}")
case _ => ()
Option.unless(tmpCtx.reporter.hasErrors)(name -> value)
case untpd.Parens(Assign(ident: untpd.Ident, value)) => Some(ident.name -> value)
case _ => None
}
assignmentArgs.foreach: (name, value) =>
// If there are no errors typing the above, then the named tuple is
// ambiguous and we issue a warning.
report.migrationWarning(DeprecatedAssignmentSyntax(name, value), tree.srcPos)
if MigrationVersion.AmbiguousNamedTupleSyntax.needsPatch then
patch(tree.source, Span(tree.span.start, tree.span.start + 1), "{")
patch(tree.source, Span(tree.span.end - 1, tree.span.end), "}")

/** Retrieve symbol attached to given tree */
protected def retrieveSym(tree: untpd.Tree)(using Context): Symbol = tree.removeAttachment(SymOfTree) match {
Expand Down Expand Up @@ -3621,6 +3624,9 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
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 tree: untpd.Parens =>
checkDeprecatedAssignmentSyntax(tree)
typedUnadapted(desugar(tree, pt), pt, locked)
case _ => typedUnadapted(desugar(tree, pt), pt, locked)
}

Expand Down
2 changes: 1 addition & 1 deletion compiler/test/dotty/tools/dotc/CompilationTests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ class CompilationTests {
compileFile("tests/rewrites/i20002.scala", defaultOptions.and("-indent", "-rewrite")),
compileDir("tests/rewrites/annotation-named-pararamters", defaultOptions.and("-rewrite", "-source:3.6-migration")),
compileFile("tests/rewrites/i21418.scala", unindentOptions.and("-rewrite", "-source:3.5-migration")),
compileFile("tests/rewrites/infix-named-args.scala", defaultOptions.and("-rewrite", "-source:3.6-migration")),
compileFile("tests/rewrites/infix-named-args.scala", defaultOptions.and("-rewrite", "-source:3.7-migration")),
compileFile("tests/rewrites/ambiguous-named-tuple-assignment.scala", defaultOptions.and("-rewrite", "-source:3.6-migration")),
compileFile("tests/rewrites/i21382.scala", defaultOptions.and("-indent", "-rewrite")),
compileFile("tests/rewrites/unused.scala", defaultOptions.and("-rewrite", "-Wunused:all")),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
---
layout: doc-page
title: "Named Tuples"
nightlyOf: https://docs.scala-lang.org/scala3/reference/experimental/named-tuples.html
nightlyOf: https://docs.scala-lang.org/scala3/reference/other-new-features/named-tuples.html
---

The elements of a tuple can now be named. Example:
Starting in Scala 3.7, the elements of a tuple can be named.
Example:
```scala
type Person = (name: String, age: Int)
val Bob: Person = (name = "Bob", age = 33)
Expand Down
2 changes: 1 addition & 1 deletion docs/sidebar.yml
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ subsection:
- page: reference/other-new-features/export.md
- page: reference/other-new-features/opaques.md
- page: reference/other-new-features/opaques-details.md
- page: reference/other-new-features/named-tuples.md
- page: reference/other-new-features/open-classes.md
- page: reference/other-new-features/parameter-untupling.md
- page: reference/other-new-features/parameter-untupling-spec.md
Expand Down Expand Up @@ -158,7 +159,6 @@ subsection:
- page: reference/experimental/cc.md
- page: reference/experimental/purefuns.md
- page: reference/experimental/tupled-function.md
- page: reference/experimental/named-tuples.md
- page: reference/experimental/modularity.md
- page: reference/experimental/typeclasses.md
- page: reference/experimental/runtimeChecked.md
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1725,8 +1725,7 @@ class CompletionTest {
.completion(m6, Set())

@Test def namedTupleCompletion: Unit =
code"""|import scala.language.experimental.namedTuples
|
code"""|
|val person: (name: String, city: String) =
| (name = "Jamie", city = "Lausanne")
|
Expand Down
3 changes: 0 additions & 3 deletions library/src/scala/NamedTuple.scala
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
package scala
import annotation.experimental
import compiletime.ops.boolean.*

@experimental
object NamedTuple:

/** The type to which named tuples get mapped to. For instance,
Expand Down Expand Up @@ -133,7 +131,6 @@ object NamedTuple:
end NamedTuple

/** Separate from NamedTuple object so that we can match on the opaque type NamedTuple. */
@experimental
object NamedTupleDecomposition:
import NamedTuple.*
extension [N <: Tuple, V <: Tuple](x: NamedTuple[N, V])
Expand Down
1 change: 1 addition & 0 deletions library/src/scala/runtime/stdLibPatches/language.scala
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ object language:
* @see [[https://dotty.epfl.ch/docs/reference/experimental/named-tuples]]
*/
@compileTimeOnly("`namedTuples` can only be used at compile time in import statements")
@deprecated("The experimental.namedTuples language import is no longer needed since the feature is now standard", since = "3.7")
object namedTuples

/** Experimental support for new features for better modularity, including
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2032,8 +2032,7 @@ class CompletionSuite extends BaseCompletionSuite:

@Test def `namedTuple completions` =
check(
"""|import scala.language.experimental.namedTuples
|import scala.NamedTuple.*
"""|import scala.NamedTuple.*
|
|val person = (name = "Jamie", city = "Lausanne")
|
Expand All @@ -2044,8 +2043,7 @@ class CompletionSuite extends BaseCompletionSuite:

@Test def `Selectable with namedTuple Fields member` =
check(
"""|import scala.language.experimental.namedTuples
|import scala.NamedTuple.*
"""|import scala.NamedTuple.*
|
|class NamedTupleSelectable extends Selectable {
| type Fields <: AnyNamedTuple
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -507,8 +507,7 @@ class PcDefinitionSuite extends BasePcDefinitionSuite:

@Test def `named-tuples` =
check(
"""|import scala.language.experimental.namedTuples
|
"""|
|val <<foo>> = (name = "Bob", age = 42, height = 1.9d)
|val foo_name = foo.na@@me
|""".stripMargin
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -720,8 +720,7 @@ class HoverTermSuite extends BaseHoverSuite:

@Test def `named-tuples`: Unit =
check(
"""import scala.language.experimental.namedTuples
|
"""
|val foo = (name = "Bob", age = 42, height = 1.9d)
|val foo_name = foo.na@@me
|""".stripMargin,
Expand All @@ -730,9 +729,7 @@ class HoverTermSuite extends BaseHoverSuite:

@Test def `named-tuples2`: Unit =
check(
"""|import scala.language.experimental.namedTuples
|
|import NamedTuple.*
"""|import NamedTuple.*
|
|class NamedTupleSelectable extends Selectable {
| type Fields <: AnyNamedTuple
Expand Down
14 changes: 7 additions & 7 deletions tests/neg/i20517.check
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
-- [E007] Type Mismatch Error: tests/neg/i20517.scala:10:43 ------------------------------------------------------------
10 | def dep(foo: Foo[Any]): From[foo.type] = (elem = "") // error
| ^^^^^^^^^^^
| Found: (elem : String)
| Required: NamedTuple.From[(foo : Foo[Any])]
|
| longer explanation available when compiling with `-explain`
-- [E007] Type Mismatch Error: tests/neg/i20517.scala:9:43 -------------------------------------------------------------
9 | def dep(foo: Foo[Any]): From[foo.type] = (elem = "") // error
| ^^^^^^^^^^^
| Found: (elem : String)
| Required: NamedTuple.From[(foo : Foo[Any])]
|
| longer explanation available when compiling with `-explain`
1 change: 0 additions & 1 deletion tests/neg/i20517.scala
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import scala.language.experimental.namedTuples
import NamedTuple.From

case class Foo[+T](elem: T)
Expand Down
36 changes: 18 additions & 18 deletions tests/neg/i22192.check
Original file line number Diff line number Diff line change
@@ -1,30 +1,30 @@
-- Error: tests/neg/i22192.scala:6:12 ----------------------------------------------------------------------------------
6 | case City(iam = n, confused = p) => // error // error
-- Error: tests/neg/i22192.scala:4:12 ----------------------------------------------------------------------------------
4 | case City(iam = n, confused = p) => // error // error
| ^^^^^^^
| No element named `iam` is defined in selector type City
-- Error: tests/neg/i22192.scala:6:21 ----------------------------------------------------------------------------------
6 | case City(iam = n, confused = p) => // error // error
-- Error: tests/neg/i22192.scala:4:21 ----------------------------------------------------------------------------------
4 | case City(iam = n, confused = p) => // error // error
| ^^^^^^^^^^^^
| No element named `confused` is defined in selector type City
-- [E006] Not Found Error: tests/neg/i22192.scala:7:7 ------------------------------------------------------------------
7 | s"$n has a population of $p" // error // error
-- [E006] Not Found Error: tests/neg/i22192.scala:5:7 ------------------------------------------------------------------
5 | s"$n has a population of $p" // error // error
| ^
| Not found: n
|
| longer explanation available when compiling with `-explain`
-- [E006] Not Found Error: tests/neg/i22192.scala:7:30 -----------------------------------------------------------------
7 | s"$n has a population of $p" // error // error
-- [E006] Not Found Error: tests/neg/i22192.scala:5:30 -----------------------------------------------------------------
5 | s"$n has a population of $p" // error // error
| ^
| Not found: p
|
| longer explanation available when compiling with `-explain`
-- Error: tests/neg/i22192.scala:10:12 ---------------------------------------------------------------------------------
10 | case Some(iam = n) => // error
| ^^^^^^^
| No element named `iam` is defined in selector type City
-- [E006] Not Found Error: tests/neg/i22192.scala:11:4 -----------------------------------------------------------------
11 | n // error
| ^
| Not found: n
|
| longer explanation available when compiling with `-explain`
-- Error: tests/neg/i22192.scala:8:12 ----------------------------------------------------------------------------------
8 | case Some(iam = n) => // error
| ^^^^^^^
| No element named `iam` is defined in selector type City
-- [E006] Not Found Error: tests/neg/i22192.scala:9:4 ------------------------------------------------------------------
9 | n // error
| ^
| Not found: n
|
| longer explanation available when compiling with `-explain`
2 changes: 0 additions & 2 deletions tests/neg/i22192.scala
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import scala.language.experimental.namedTuples

case class City(name: String, population: Int)

def getCityInfo(city: City) = city match
Expand Down
20 changes: 10 additions & 10 deletions tests/neg/i22192a.check
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
-- Error: tests/neg/i22192a.scala:6:12 ---------------------------------------------------------------------------------
6 | case Some(iam = n) => // error
-- Error: tests/neg/i22192a.scala:4:12 ---------------------------------------------------------------------------------
4 | case Some(iam = n) => // error
| ^^^^^^^
| No element named `iam` is defined in selector type (name : String)
-- [E006] Not Found Error: tests/neg/i22192a.scala:7:4 -----------------------------------------------------------------
7 | n // error
-- [E006] Not Found Error: tests/neg/i22192a.scala:5:4 -----------------------------------------------------------------
5 | n // error
| ^
| Not found: n
|
| longer explanation available when compiling with `-explain`
-- Error: tests/neg/i22192a.scala:11:12 --------------------------------------------------------------------------------
11 | case Some(iam = n) => // error
| ^^^^^^^
| No element named `iam` is defined in selector type (name : String, population : Int)
-- [E006] Not Found Error: tests/neg/i22192a.scala:12:4 ----------------------------------------------------------------
12 | n // error
-- Error: tests/neg/i22192a.scala:9:12 ---------------------------------------------------------------------------------
9 | case Some(iam = n) => // error
| ^^^^^^^
| No element named `iam` is defined in selector type (name : String, population : Int)
-- [E006] Not Found Error: tests/neg/i22192a.scala:10:4 ----------------------------------------------------------------
10 | n // error
| ^
| Not found: n
|
Expand Down
2 changes: 0 additions & 2 deletions tests/neg/i22192a.scala
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import scala.language.experimental.namedTuples

type City = (name: String)

def getCityInfo(city: Option[City]) = city match
Expand Down
Loading
Loading