Skip to content
Open
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
5 changes: 4 additions & 1 deletion compiler/src/dotty/tools/dotc/transform/patmat/Space.scala
Original file line number Diff line number Diff line change
Expand Up @@ -362,7 +362,10 @@ object SpaceEngine {
val funRef = fun1.tpe.asInstanceOf[TermRef]
if (fun.symbol.name == nme.unapplySeq)
val (arity, elemTp, resultTp) = unapplySeqInfo(fun.tpe.widen.finalResultType, fun.srcPos)
if fun.symbol.owner == defn.SeqFactoryClass && toUnderlying(pat.tpe).dealias.derivesFrom(defn.ListClass) then
// Special case: Tuple.unapplySeq with empty patterns covers EmptyTuple
if pats.isEmpty && fun.symbol.owner == defn.TupleModule.moduleClass then
Typ(defn.EmptyTupleModule.termRef, decomposed = false)
else if fun.symbol.owner == defn.SeqFactoryClass && toUnderlying(pat.tpe).dealias.derivesFrom(defn.ListClass) then
// The exhaustivity and reachability logic already handles decomposing sum types (into its subclasses)
// and product types (into its components). To get better counter-examples for patterns that are of type
// List (or a super-type of list, like LinearSeq) we project them into spaces that use `::` and Nil.
Expand Down
107 changes: 106 additions & 1 deletion library/src/scala/Tuple.scala
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package scala

import language.experimental.captureChecking
import annotation.showAsInfix
import annotation.{showAsInfix, targetName}
import compiletime.*
import compiletime.ops.int.*

Expand Down Expand Up @@ -288,9 +288,114 @@ object Tuple {
/** Tuple with one element. */
def apply[T](x: T): T *: EmptyTuple = Tuple1(x)

/** Uniform tuple construction for any arity with type preservation.
* For arities 0 and 1, the overloads above are preferred (binary compatibility).
* For arities >= 2, this varargs version is used.
*
* Examples:
* {{{
* Tuple() // EmptyTuple
* Tuple(1) // Tuple1[Int]
* Tuple(1, "a") // (Int, String)
* Tuple(1, "a", 2.0) // (Int, String, Double)
* }}}
*/
transparent inline def apply(inline args: Any*): Tuple = ${ TupleMacros.applyImpl('args) }

/** Matches an empty tuple. */
def unapply(x: EmptyTuple): true = true

/** Matches a 1-tuple, extracting its element. */
@targetName("unapply1")
inline def unapply[A](x: Tuple1[A]): Some[A] = Some(x._1)

/** Matches a 2-tuple, extracting its elements. */
@targetName("unapply2")
inline def unapply[A, B](x: (A, B)): (A, B) = x

/** Matches a 3-tuple, extracting its elements. */
@targetName("unapply3")
inline def unapply[A, B, C](x: (A, B, C)): (A, B, C) = x

/** Matches a 4-tuple, extracting its elements. */
@targetName("unapply4")
inline def unapply[A, B, C, D](x: (A, B, C, D)): (A, B, C, D) = x

/** Matches a 5-tuple, extracting its elements. */
@targetName("unapply5")
inline def unapply[A, B, C, D, E](x: (A, B, C, D, E)): (A, B, C, D, E) = x

/** Matches a 6-tuple, extracting its elements. */
@targetName("unapply6")
inline def unapply[A, B, C, D, E, F](x: (A, B, C, D, E, F)): (A, B, C, D, E, F) = x

/** Matches a 7-tuple, extracting its elements. */
@targetName("unapply7")
inline def unapply[A, B, C, D, E, F, G](x: (A, B, C, D, E, F, G)): (A, B, C, D, E, F, G) = x

/** Matches a 8-tuple, extracting its elements. */
@targetName("unapply8")
inline def unapply[A, B, C, D, E, F, G, H](x: (A, B, C, D, E, F, G, H)): (A, B, C, D, E, F, G, H) = x

/** Matches a 9-tuple, extracting its elements. */
@targetName("unapply9")
inline def unapply[A, B, C, D, E, F, G, H, I](x: (A, B, C, D, E, F, G, H, I)): (A, B, C, D, E, F, G, H, I) = x

/** Matches a 10-tuple, extracting its elements. */
@targetName("unapply10")
inline def unapply[A, B, C, D, E, F, G, H, I, J](x: (A, B, C, D, E, F, G, H, I, J)): (A, B, C, D, E, F, G, H, I, J) = x

/** Matches a 11-tuple, extracting its elements. */
@targetName("unapply11")
inline def unapply[A, B, C, D, E, F, G, H, I, J, K](x: (A, B, C, D, E, F, G, H, I, J, K)): (A, B, C, D, E, F, G, H, I, J, K) = x

/** Matches a 12-tuple, extracting its elements. */
@targetName("unapply12")
inline def unapply[A, B, C, D, E, F, G, H, I, J, K, L](x: (A, B, C, D, E, F, G, H, I, J, K, L)): (A, B, C, D, E, F, G, H, I, J, K, L) = x

/** Matches a 13-tuple, extracting its elements. */
@targetName("unapply13")
inline def unapply[A, B, C, D, E, F, G, H, I, J, K, L, M](x: (A, B, C, D, E, F, G, H, I, J, K, L, M)): (A, B, C, D, E, F, G, H, I, J, K, L, M) = x

/** Matches a 14-tuple, extracting its elements. */
@targetName("unapply14")
inline def unapply[A, B, C, D, E, F, G, H, I, J, K, L, M, N](x: (A, B, C, D, E, F, G, H, I, J, K, L, M, N)): (A, B, C, D, E, F, G, H, I, J, K, L, M, N) = x

/** Matches a 15-tuple, extracting its elements. */
@targetName("unapply15")
inline def unapply[A, B, C, D, E, F, G, H, I, J, K, L, M, N, O](x: (A, B, C, D, E, F, G, H, I, J, K, L, M, N, O)): (A, B, C, D, E, F, G, H, I, J, K, L, M, N, O) = x

/** Matches a 16-tuple, extracting its elements. */
@targetName("unapply16")
inline def unapply[A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P](x: (A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P)): (A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P) = x

/** Matches a 17-tuple, extracting its elements. */
@targetName("unapply17")
inline def unapply[A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q](x: (A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q)): (A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q) = x

/** Matches a 18-tuple, extracting its elements. */
@targetName("unapply18")
inline def unapply[A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R](x: (A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R)): (A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R) = x

/** Matches a 19-tuple, extracting its elements. */
@targetName("unapply19")
inline def unapply[A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S](x: (A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S)): (A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S) = x

/** Matches a 20-tuple, extracting its elements. */
@targetName("unapply20")
inline def unapply[A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T](x: (A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T)): (A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T) = x

/** Matches a 21-tuple, extracting its elements. */
@targetName("unapply21")
inline def unapply[A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U](x: (A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U)): (A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U) = x

/** Matches a 22-tuple, extracting its elements. */
@targetName("unapply22")
inline def unapply[A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V](x: (A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V)): (A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V) = x

/** Fallback for abstract Tuple types - provides runtime arity matching. */
def unapplySeq(x: Tuple): Option[Seq[Any]] = Some(x.productIterator.toSeq)

/** Converts an array into a tuple of unknown arity and types. */
def fromArray[T](xs: Array[T]): Tuple = {
val xs2 = xs match {
Expand Down
66 changes: 66 additions & 0 deletions library/src/scala/TupleMacros.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package scala

import scala.quoted.*

object TupleMacros {

/** Macro implementation for type-preserving Tuple construction.
* Extracts the precise types of each argument and builds a properly typed TupleN.
*/
def applyImpl(args: Expr[Seq[Any]])(using Quotes): Expr[Tuple] = {
import quotes.reflect.*

def extractRepeated(term: Term): List[Term] = term match {
case Repeated(elems, _) => elems
case Inlined(_, _, inner) => extractRepeated(inner)
case Typed(inner, _) => extractRepeated(inner)
case Block(_, inner) => extractRepeated(inner)
case _ =>
report.error(s"Expected literal varargs, got: ${term.show}")
Nil
}

val argTerms = extractRepeated(args.asTerm)
val n = argTerms.length

n match {
case 0 => '{ EmptyTuple }
case _ if n <= scala.runtime.Tuples.MaxSpecialized =>
val tupleClass = Symbol.requiredClass(s"scala.Tuple$n")
val tupleType = tupleClass.typeRef.appliedTo(argTerms.map(_.tpe.widen))
val tupleCompanion = tupleClass.companionModule

val applyMethod = tupleCompanion.methodMember("apply").head
val call = Apply(
TypeApply(
Select(Ref(tupleCompanion), applyMethod),
argTerms.map(t => TypeTree.of(using t.tpe.widen.asType.asInstanceOf[Type[Any]]))
),
argTerms
)

tupleType.asType match {
case '[t] => call.asExprOf[t & Tuple]
}

case _ =>
val consType = argTerms.foldRight(TypeRepr.of[EmptyTuple]) { (term, acc) =>
TypeRepr.of[*:].appliedTo(List(term.tpe.widen, acc))
}

val arrayElements = argTerms.map { t =>
t.tpe.widen.asType match {
case '[elem] =>
val termExpr = t.asExprOf[elem]
'{ $termExpr.asInstanceOf[Object] }
}
}
val arrayExpr = Expr.ofSeq(arrayElements)

consType.asType match {
case '[t] =>
'{ scala.runtime.Tuples.fromArray($arrayExpr.toArray[Object]).asInstanceOf[t & Tuple] }
}
}
}
}
4 changes: 4 additions & 0 deletions project/MiMaFilters.scala
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ object MiMaFilters {
ProblemFilters.exclude[DirectMissingMethodProblem]("scala.caps.package#package.freeze"),
// scala/scala3#24545 / scala/scala3#24788
ProblemFilters.exclude[MissingClassProblem]("scala.annotation.unchecked.uncheckedOverride"),
// scala/scala3#24874 - Uniform Tuple.apply and Tuple.unapply for SIP-NN
ProblemFilters.exclude[DirectMissingMethodProblem]("scala.Tuple.unapply*"),
ProblemFilters.exclude[MissingClassProblem]("scala.TupleMacros"),
ProblemFilters.exclude[MissingClassProblem]("scala.TupleMacros$"),
),

)
Expand Down
13 changes: 6 additions & 7 deletions repl/src/dotty/tools/repl/Rendering.scala
Original file line number Diff line number Diff line change
Expand Up @@ -98,13 +98,12 @@ private[repl] class Rendering(parentClassLoader: Option[ClassLoader] = None):

/** Return a colored fansi.Str representation of a value we got from `classLoader()`. */
private[repl] def replStringOf(value: Object, prefixLength: Int)(using Context): fansi.Str = {
// pretty-print things with 100 cols 50 rows by default,
val res = pprintRender(
value,
width = 100,
height = 50,
initialOffset = prefixLength
)
// Use Tuple1(x) format to avoid pprint's "(x,)" format
val res = value match {
case t: Tuple1[?] => s"Tuple1(${t._1})"
case _ =>
pprintRender(value, width = 100, height = 50, initialOffset = prefixLength)
}
if (ctx.settings.color.value == "never") fansi.Str(res).plainText else res
}

Expand Down
18 changes: 18 additions & 0 deletions tests/run/tuple-apply-unapply.check
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
=== Tuple.apply tests ===
Tuple() = ()
Tuple(42) = (42)
Tuple(1, hello) = (1,hello)
Tuple(1, a, 3.14) = (1,a,3.14)
Tuple(1..22) arity = 22
Tuple(1..23) arity = 23
=== Type preservation tests ===
Type preservation: OK
=== Tuple.unapply tests ===
EmptyTuple: OK
Tuple1: 42 (typed Int)
Tuple2: 1, hello (typed)
Tuple3: 1, a, 3.14 (typed)
=== Abstract Tuple (unapplySeq) ===
Abstract 3-tuple: 1, hello, 3.14
Correct arity matched
=== All tests passed ===
93 changes: 93 additions & 0 deletions tests/run/tuple-apply-unapply.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/** Test for uniform Tuple.apply
*
* Tuple.apply: uniform construction for any arity with type preservation
*/
object Test extends App {

// === APPLY TESTS ===
println("=== Tuple.apply tests ===")

// Arity 0
val t0 = Tuple()
assert(t0 == EmptyTuple)
println(s"Tuple() = $t0")

// Arity 1
val t1 = Tuple(42)
assert(t1 == Tuple1(42))
println(s"Tuple(42) = $t1")

// Arity 2
val t2 = Tuple(1, "hello")
assert(t2 == (1, "hello"))
println(s"Tuple(1, hello) = $t2")

// Arity 3
val t3 = Tuple(1, "a", 3.14)
assert(t3 == (1, "a", 3.14))
println(s"Tuple(1, a, 3.14) = $t3")

// Arity 22 (max specialized)
val t22 = Tuple(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22)
assert(t22.productArity == 22)
println(s"Tuple(1..22) arity = ${t22.productArity}")

// Arity 23 (TupleXXL)
val t23 = Tuple(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23)
assert(t23.productArity == 23)
println(s"Tuple(1..23) arity = ${t23.productArity}")

// === TYPE PRESERVATION TESTS ===
println("=== Type preservation tests ===")

// Verify types are preserved (these would fail to compile if types were wrong)
val check0: EmptyTuple = t0
val check1: Tuple1[Int] = t1
val check2: (Int, String) = t2
val check3: (Int, String, Double) = t3

println("Type preservation: OK")

// === UNAPPLY TESTS ===
println("=== Tuple.unapply tests ===")

// EmptyTuple
EmptyTuple match
case Tuple() => println("EmptyTuple: OK")

// Tuple1 - typed extraction
Tuple1(42) match
case Tuple(x) =>
val check: Int = x
println(s"Tuple1: $x (typed Int)")

// Tuple2 - typed extraction
(1, "hello") match
case Tuple(a, b) =>
val checkA: Int = a
val checkB: String = b
println(s"Tuple2: $a, $b (typed)")

// Tuple3 - typed extraction
(1, "a", 3.14) match
case Tuple(a, b, c) =>
val checkA: Int = a
val checkB: String = b
val checkC: Double = c
println(s"Tuple3: $a, $b, $c (typed)")

// Abstract Tuple - unapplySeq fallback (elements are Any)
println("=== Abstract Tuple (unapplySeq) ===")
val abstractTuple: Tuple = (1, "hello", 3.14)
abstractTuple match
case Tuple(a, b, c) => println(s"Abstract 3-tuple: $a, $b, $c")
case _ => assert(false, "Should have matched")

// Arity mismatch - should not match
abstractTuple match
case Tuple(a, b) => assert(false, "Should not match wrong arity")
case Tuple(a, b, c) => println("Correct arity matched")
case _ => assert(false, "Should have matched")

println("=== All tests passed ===")
}
Loading