diff --git a/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala b/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala index 8ede7ba831f5..3c8574076f8f 100644 --- a/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala +++ b/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala @@ -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. diff --git a/library/src/scala/Tuple.scala b/library/src/scala/Tuple.scala index afba20a33c24..6460d05d9da6 100644 --- a/library/src/scala/Tuple.scala +++ b/library/src/scala/Tuple.scala @@ -1,7 +1,7 @@ package scala import language.experimental.captureChecking -import annotation.showAsInfix +import annotation.{showAsInfix, targetName} import compiletime.* import compiletime.ops.int.* @@ -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 { diff --git a/library/src/scala/TupleMacros.scala b/library/src/scala/TupleMacros.scala new file mode 100644 index 000000000000..2ed1b5f54594 --- /dev/null +++ b/library/src/scala/TupleMacros.scala @@ -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] } + } + } + } +} diff --git a/project/MiMaFilters.scala b/project/MiMaFilters.scala index 75f4c9e86465..8c5471124e62 100644 --- a/project/MiMaFilters.scala +++ b/project/MiMaFilters.scala @@ -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$"), ), ) diff --git a/repl/src/dotty/tools/repl/Rendering.scala b/repl/src/dotty/tools/repl/Rendering.scala index 3af59e547fbd..83c7e80fac19 100644 --- a/repl/src/dotty/tools/repl/Rendering.scala +++ b/repl/src/dotty/tools/repl/Rendering.scala @@ -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 } diff --git a/tests/run/tuple-apply-unapply.check b/tests/run/tuple-apply-unapply.check new file mode 100644 index 000000000000..a15a5d38d608 --- /dev/null +++ b/tests/run/tuple-apply-unapply.check @@ -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 === diff --git a/tests/run/tuple-apply-unapply.scala b/tests/run/tuple-apply-unapply.scala new file mode 100644 index 000000000000..1b4277b6574c --- /dev/null +++ b/tests/run/tuple-apply-unapply.scala @@ -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 ===") +}