From 24e43b9c0f6da3060c734bea0803af42563f9990 Mon Sep 17 00:00:00 2001 From: TheDrawingCoding-Gamer Date: Mon, 28 Jul 2025 10:51:12 -0400 Subject: [PATCH 1/4] try to fix i21808 (doesnt work) --- .../dotty/tools/dotc/typer/Synthesizer.scala | 82 ++++++++++++++++--- tests/neg/i21808a.check | 4 + tests/neg/i21808a.scala | 10 +++ tests/neg/i21808b.check | 4 + tests/neg/i21808b.scala | 10 +++ tests/pos/i21808.scala | 10 +++ 6 files changed, 108 insertions(+), 12 deletions(-) create mode 100644 tests/neg/i21808a.check create mode 100644 tests/neg/i21808a.scala create mode 100644 tests/neg/i21808b.check create mode 100644 tests/neg/i21808b.scala create mode 100644 tests/pos/i21808.scala diff --git a/compiler/src/dotty/tools/dotc/typer/Synthesizer.scala b/compiler/src/dotty/tools/dotc/typer/Synthesizer.scala index 8df2fc1ed4b7..ff0970d16044 100644 --- a/compiler/src/dotty/tools/dotc/typer/Synthesizer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Synthesizer.scala @@ -20,6 +20,7 @@ import ast.tpd.* import Synthesizer.* import sbt.ExtractDependencies.* import xsbti.api.DependencyContext.* +import dotty.tools.dotc.core.Definitions.MaxTupleArity /** Synthesize terms for special classes */ class Synthesizer(typer: Typer)(using @constructorOnly c: Context): @@ -105,27 +106,84 @@ class Synthesizer(typer: Typer)(using @constructorOnly c: Context): val synthesizedTupleFunction: SpecialHandler = (formal, span) => formal match case AppliedType(_, funArgs @ fun :: tupled :: Nil) => + def doesFunctionTupleInto(baseFun: Type, actualArgs: List[Type], + actualRet: Type, tupled: Type) = + tupled.dealias =:= constructDependentTupleType(actualArgs, actualRet, defn.isContextFunctionType(baseFun)) + def doesFunctionUntupleTo(baseFun: Type, actualArgs: List[Type], + actualRet: Type, untupled: Type) = + untupled.dealias =:= untupleDependentTupleType(actualArgs, actualRet, defn.isContextFunctionType(baseFun)) + def functionTypeEqual(baseFun: Type, actualArgs: List[Type], actualRet: Type, expected: Type) = - expected =:= defn.FunctionNOf(actualArgs, actualRet, - defn.isContextFunctionType(baseFun)) + expected.dealias =:= defn.FunctionNOf(actualArgs, actualRet, defn.isContextFunctionType(baseFun)) + def untupleDependentTupleType(args: List[Type], ret: Type, contextual: Boolean): Type = + val methodKind = if contextual then ContextualMethodType else MethodType + + + val p = methodKind(args.indices.map(nme.syntheticParamName).toList)( + mt => args, + mt => + val tpeMap = new TypeMap: + def apply(tp: Type): Type = + tp match + case TermRef(TermParamRef(_, paramNum), name) => + name match + case x: TermName => + x.toString match + // hack upon hack upon hack + // please make there be a reverse map so i dont have to do this : ( + case s"_$id" => + mt.paramRefs(id.toInt) + case _ => ??? + case _ => ??? + case _ => mapOver(tp) + tpeMap(ret) + ) + p + + def constructDependentTupleType(args: List[Type], ret: Type, contextual: Boolean): Type = + val methodKind = if contextual then ContextualMethodType else MethodType + + val p = methodKind(List(nme.syntheticParamName(0)))( + mt => List(defn.tupleType(args)), + mt => + val tpeMap = new TypeMap: + def apply(tp: Type): Type = + tp match + case TermParamRef(binder, paramNum) => + mt.paramRefs(0).select(nme.selectorName(paramNum)) + case _ => + mapOver(tp) + tpeMap(ret) + ).toFunctionType() + p + val dealiasedFun = fun.dealias + val dealiasedTupled = tupled.dealias val arity: Int = if defn.isFunctionNType(fun) then // TupledFunction[(...) => R, ?] - fun.functionArgInfos match - case funArgs :+ funRet - if functionTypeEqual(fun, defn.tupleType(funArgs) :: Nil, funRet, tupled) => - // TupledFunction[(...funArgs...) => funRet, ?] - funArgs.size - case _ => -1 + // dont use functionArgInfos it dealiases and drops dependents + + dealiasedFun match + case defn.RefinedFunctionOf(method: MethodType) if doesFunctionTupleInto(dealiasedFun, method.paramInfos, method.resType, dealiasedTupled) => + method.paramInfos.size + // poly types are unsupported + case defn.RefinedFunctionOf(_) => -1 + case _ => + fun.functionArgInfos match + case funArgs :+ funRet + if functionTypeEqual(dealiasedFun, defn.tupleType(funArgs) :: Nil, funRet, dealiasedTupled) => + // TupledFunction[(...funArgs...) => funRet, ?] + funArgs.size + case _ => -1 else if defn.isFunctionNType(tupled) then // TupledFunction[?, (...) => R] - tupled.functionArgInfos match + dealiasedTupled.argInfos match case tupledArgs :: funRet :: Nil => + // TupledFunction[?, ((...)) => R] tupledArgs.tupleElementTypes match - case Some(funArgs) if functionTypeEqual(tupled, funArgs, funRet, fun) => - // TupledFunction[?, ((...funArgs...)) => funRet] - funArgs.size + case Some(args) if doesFunctionUntupleTo(dealiasedTupled, args, funRet, dealiasedFun) => + args.size case _ => -1 case _ => -1 else diff --git a/tests/neg/i21808a.check b/tests/neg/i21808a.check new file mode 100644 index 000000000000..89dd43c92752 --- /dev/null +++ b/tests/neg/i21808a.check @@ -0,0 +1,4 @@ +-- [E172] Type Error: tests/neg/i21808a.scala:9:63 --------------------------------------------------------------------- +9 | summon[TupledFunction[(x: T, y: T) => x.type, ((T, T)) => T]] // error + | ^ + | (x: Test.T, y: Test.T) => x.type cannot be tupled as ((Test.T, Test.T)) => Test.T diff --git a/tests/neg/i21808a.scala b/tests/neg/i21808a.scala new file mode 100644 index 000000000000..df4cf3d5cb90 --- /dev/null +++ b/tests/neg/i21808a.scala @@ -0,0 +1,10 @@ +//> using options -experimental + +import scala.util.TupledFunction +import scala.util.NotGiven + +object Test { + type T + + summon[TupledFunction[(x: T, y: T) => x.type, ((T, T)) => T]] // error +} \ No newline at end of file diff --git a/tests/neg/i21808b.check b/tests/neg/i21808b.check new file mode 100644 index 000000000000..02d04b43b977 --- /dev/null +++ b/tests/neg/i21808b.check @@ -0,0 +1,4 @@ +-- [E172] Type Error: tests/neg/i21808b.scala:9:63 --------------------------------------------------------------------- +9 | summon[TupledFunction[(T, T) => T, (x: (T, T)) => x._1.type]] // error + | ^ + | (Test.T, Test.T) => Test.T cannot be tupled as (x: (Test.T, Test.T)) => (x._1 : Test.T) diff --git a/tests/neg/i21808b.scala b/tests/neg/i21808b.scala new file mode 100644 index 000000000000..3042ab16abf0 --- /dev/null +++ b/tests/neg/i21808b.scala @@ -0,0 +1,10 @@ +//> using options -experimental + +import scala.util.TupledFunction +import scala.util.NotGiven + +object Test { + type T + + summon[TupledFunction[(T, T) => T, (x: (T, T)) => x._1.type]] // error +} \ No newline at end of file diff --git a/tests/pos/i21808.scala b/tests/pos/i21808.scala new file mode 100644 index 000000000000..fde33c858b82 --- /dev/null +++ b/tests/pos/i21808.scala @@ -0,0 +1,10 @@ +//> using options -experimental + +import scala.util.TupledFunction +import scala.util.NotGiven + +object Test { + type T + + summon[TupledFunction[(x: T, y: T) => x.type, (x: (T, T)) => x._1.type]] +} \ No newline at end of file From 783189df178629578aeb2ef1774465b40851a135 Mon Sep 17 00:00:00 2001 From: TheDrawingCoding-Gamer Date: Mon, 28 Jul 2025 11:33:32 -0400 Subject: [PATCH 2/4] Fix test, make new test that breaks The case where the left side isn't a function type/ is a type param now works for non dependent type cases. HOWEVER, now the compiler rejects constructions like `untupled` for dependent types, when it should accept them. [skip community_build] [skip docs] [skip test_windows_fast] [skip mima] --- .../dotty/tools/dotc/typer/Synthesizer.scala | 25 +++++++++++++------ tests/run/i21808.scala | 23 +++++++++++++++++ 2 files changed, 41 insertions(+), 7 deletions(-) create mode 100644 tests/run/i21808.scala diff --git a/compiler/src/dotty/tools/dotc/typer/Synthesizer.scala b/compiler/src/dotty/tools/dotc/typer/Synthesizer.scala index ff0970d16044..48ad2cf3ecd1 100644 --- a/compiler/src/dotty/tools/dotc/typer/Synthesizer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Synthesizer.scala @@ -178,14 +178,25 @@ class Synthesizer(typer: Typer)(using @constructorOnly c: Context): case _ => -1 else if defn.isFunctionNType(tupled) then // TupledFunction[?, (...) => R] - dealiasedTupled.argInfos match - case tupledArgs :: funRet :: Nil => - // TupledFunction[?, ((...)) => R] - tupledArgs.tupleElementTypes match - case Some(args) if doesFunctionUntupleTo(dealiasedTupled, args, funRet, dealiasedFun) => - args.size + dealiasedTupled match + case defn.RefinedFunctionOf(method: MethodType) => + method.argInfos match + case tupledArgs :: funRet :: Nil => + // TupledFunction[?, ((...)) => R] + tupledArgs.tupleElementTypes match + case Some(args) if doesFunctionUntupleTo(dealiasedTupled, args, funRet, dealiasedFun) => + args.size + case _ => -1 + case _ => -1 + case _ => + tupled.functionArgInfos match + case tupledArgs :: funRet :: Nil => + // TupledFunction[?, ((...)) => R] + tupledArgs.tupleElementTypes match + case Some(args) if functionTypeEqual(dealiasedTupled, args, funRet, dealiasedFun) => + args.size + case _ => -1 case _ => -1 - case _ => -1 else // TupledFunction[?, ?] -1 diff --git a/tests/run/i21808.scala b/tests/run/i21808.scala new file mode 100644 index 000000000000..13888c874286 --- /dev/null +++ b/tests/run/i21808.scala @@ -0,0 +1,23 @@ +//> using options -experimental + +import scala.util.TupledFunction +object Test { + def main(args: Array[String]): Unit = { + + val f2: (x: (Int, Long)) => x._1.type = (args: (Int, Long)) => args._1 + val g2: (x: Int, y: Long) => x.type = f2.untupled + println(g2(1, 3L)) + + } + + /** Creates an untupled version of this function: instead of a single argument of type [[scala.Tuple]] with N elements, + * it accepts N arguments. + * + * This is a generalization of [[scala.Function.untupled]] that work on functions of any arity + * + * @tparam F the function type + * @tparam Args the tuple type with the same types as the function arguments of F + * @tparam R the return type of F + */ + extension [F, Args <: Tuple, R](f: Args => R) def untupled(using tf: TupledFunction[F, Args => R]): F = tf.untupled(f) +} From ea71a5ff826094f03557f4717df94663b6ab3b0a Mon Sep 17 00:00:00 2001 From: TheDrawingCoding-Gamer Date: Mon, 28 Jul 2025 12:58:41 -0400 Subject: [PATCH 3/4] clean up code --- .../dotty/tools/dotc/core/Definitions.scala | 29 +++++++++++- .../dotty/tools/dotc/typer/Synthesizer.scala | 44 ++++++++----------- 2 files changed, 46 insertions(+), 27 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index a81f4be9068a..8aacbee09bc0 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -958,6 +958,7 @@ class Definitions { def TupleClass(using Context): ClassSymbol = TupleTypeRef.symbol.asClass @tu lazy val Tuple_cons: Symbol = TupleClass.requiredMethod("*:") @tu lazy val TupleModule: Symbol = requiredModule("scala.Tuple") + @tu lazy val Tuple_Elem: Symbol = TupleModule.requiredType("Elem") @tu lazy val EmptyTupleClass: Symbol = requiredClass("scala.EmptyTuple") @tu lazy val EmptyTupleModule: Symbol = requiredModule("scala.EmptyTuple") @tu lazy val NonEmptyTupleTypeRef: TypeRef = requiredClassRef("scala.NonEmptyTuple") @@ -1270,6 +1271,32 @@ class Definitions { else None } + // Pattern matcher of tuple selectors at the type level + // Matches Tuple.Elem[x, constant] and x._1.type + object TupleSelectorOf: + def unapply(tp: Type)(using Context): Option[(Type, Int)] = + if tp.isRef(Tuple_Elem) then + // get the arg infos for tuple elem + tp.argInfos match + case tupleType :: ConstantType(c) :: Nil + if c.isIntRange && (isTupleNType(tupleType) || tupleType.isRef(PairClass)) => + Some((tupleType, c.intValue)) + // we should probably report an error here, but its probably handled elsewhere + case _ => None + else + tp.dealias match + // very explicitly ONLY check for isTupleNType + // pair class doesn't have selector fields + case TermRef(tupleType, field) if isTupleNType(tupleType) => + field match + case name: SimpleName => + name.toString match + case s"_$id" => + id.toIntOption.map(it => (tupleType, it - 1)) + case _ => None + case _ => None + case _ => None + object ArrayOf { def apply(elem: Type)(using Context): Type = if (ctx.erasedTypes) JavaArrayType(elem) @@ -1473,7 +1500,7 @@ class Definitions { def patchStdLibClass(denot: ClassDenotation)(using Context): Unit = // Do not patch the stdlib files if we explicitly disable it // This is only to be used during the migration of the stdlib - if ctx.settings.YnoStdlibPatches.value then + if ctx.settings.YnoStdlibPatches.value then return def patch2(denot: ClassDenotation, patchCls: Symbol): Unit = diff --git a/compiler/src/dotty/tools/dotc/typer/Synthesizer.scala b/compiler/src/dotty/tools/dotc/typer/Synthesizer.scala index 48ad2cf3ecd1..36fe7c0c3a78 100644 --- a/compiler/src/dotty/tools/dotc/typer/Synthesizer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Synthesizer.scala @@ -108,43 +108,37 @@ class Synthesizer(typer: Typer)(using @constructorOnly c: Context): case AppliedType(_, funArgs @ fun :: tupled :: Nil) => def doesFunctionTupleInto(baseFun: Type, actualArgs: List[Type], actualRet: Type, tupled: Type) = - tupled.dealias =:= constructDependentTupleType(actualArgs, actualRet, defn.isContextFunctionType(baseFun)) + tupled =:= constructDependentTupleType(actualArgs, actualRet, defn.isContextFunctionType(baseFun)) def doesFunctionUntupleTo(baseFun: Type, actualArgs: List[Type], actualRet: Type, untupled: Type) = - untupled.dealias =:= untupleDependentTupleType(actualArgs, actualRet, defn.isContextFunctionType(baseFun)) + untupled =:= untupleDependentTupleType(actualArgs, actualRet, defn.isContextFunctionType(baseFun)) def functionTypeEqual(baseFun: Type, actualArgs: List[Type], actualRet: Type, expected: Type) = - expected.dealias =:= defn.FunctionNOf(actualArgs, actualRet, defn.isContextFunctionType(baseFun)) + expected =:= defn.FunctionNOf(actualArgs, actualRet, defn.isContextFunctionType(baseFun)) def untupleDependentTupleType(args: List[Type], ret: Type, contextual: Boolean): Type = val methodKind = if contextual then ContextualMethodType else MethodType - - val p = methodKind(args.indices.map(nme.syntheticParamName).toList)( + val arity = args.length + methodKind(args.indices.map(nme.syntheticParamName).toList)( mt => args, mt => val tpeMap = new TypeMap: def apply(tp: Type): Type = tp match - case TermRef(TermParamRef(_, paramNum), name) => - name match - case x: TermName => - x.toString match - // hack upon hack upon hack - // please make there be a reverse map so i dont have to do this : ( - case s"_$id" => - mt.paramRefs(id.toInt) - case _ => ??? - case _ => ??? + case defn.TupleSelectorOf(TermParamRef(_, paramNum), fieldNum) => + if fieldNum >= arity then + NoType + else + mt.paramRefs(fieldNum) case _ => mapOver(tp) tpeMap(ret) ) - p def constructDependentTupleType(args: List[Type], ret: Type, contextual: Boolean): Type = val methodKind = if contextual then ContextualMethodType else MethodType - val p = methodKind(List(nme.syntheticParamName(0)))( + methodKind(List(nme.syntheticParamName(0)))( mt => List(defn.tupleType(args)), mt => val tpeMap = new TypeMap: @@ -156,35 +150,33 @@ class Synthesizer(typer: Typer)(using @constructorOnly c: Context): mapOver(tp) tpeMap(ret) ).toFunctionType() - p - val dealiasedFun = fun.dealias - val dealiasedTupled = tupled.dealias + val arity: Int = if defn.isFunctionNType(fun) then // TupledFunction[(...) => R, ?] // dont use functionArgInfos it dealiases and drops dependents - dealiasedFun match - case defn.RefinedFunctionOf(method: MethodType) if doesFunctionTupleInto(dealiasedFun, method.paramInfos, method.resType, dealiasedTupled) => + fun.dealias match + case defn.RefinedFunctionOf(method: MethodType) if doesFunctionTupleInto(fun, method.paramInfos, method.resType, tupled) => method.paramInfos.size // poly types are unsupported case defn.RefinedFunctionOf(_) => -1 case _ => fun.functionArgInfos match case funArgs :+ funRet - if functionTypeEqual(dealiasedFun, defn.tupleType(funArgs) :: Nil, funRet, dealiasedTupled) => + if functionTypeEqual(fun, defn.tupleType(funArgs) :: Nil, funRet, tupled) => // TupledFunction[(...funArgs...) => funRet, ?] funArgs.size case _ => -1 else if defn.isFunctionNType(tupled) then // TupledFunction[?, (...) => R] - dealiasedTupled match + tupled.dealias match case defn.RefinedFunctionOf(method: MethodType) => method.argInfos match case tupledArgs :: funRet :: Nil => // TupledFunction[?, ((...)) => R] tupledArgs.tupleElementTypes match - case Some(args) if doesFunctionUntupleTo(dealiasedTupled, args, funRet, dealiasedFun) => + case Some(args) if doesFunctionUntupleTo(tupled, args, funRet, fun) => args.size case _ => -1 case _ => -1 @@ -193,7 +185,7 @@ class Synthesizer(typer: Typer)(using @constructorOnly c: Context): case tupledArgs :: funRet :: Nil => // TupledFunction[?, ((...)) => R] tupledArgs.tupleElementTypes match - case Some(args) if functionTypeEqual(dealiasedTupled, args, funRet, dealiasedFun) => + case Some(args) if functionTypeEqual(tupled, args, funRet, fun) => args.size case _ => -1 case _ => -1 From cf4329c1fcdbefa30c6f6b3a32e34866cb317acb Mon Sep 17 00:00:00 2001 From: TheDrawingCoding-Gamer Date: Mon, 28 Jul 2025 12:59:09 -0400 Subject: [PATCH 4/4] =?UTF-8?q?If=20the=20new=20test=20broke=20don't=20run?= =?UTF-8?q?=20it=20=F0=9F=98=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove the new test as it seems to be a different, unrelated issue [skip community_build] [skip docs] [skip test_windows_fast] [skip mima] --- tests/run/i21808.scala | 23 ----------------------- 1 file changed, 23 deletions(-) delete mode 100644 tests/run/i21808.scala diff --git a/tests/run/i21808.scala b/tests/run/i21808.scala deleted file mode 100644 index 13888c874286..000000000000 --- a/tests/run/i21808.scala +++ /dev/null @@ -1,23 +0,0 @@ -//> using options -experimental - -import scala.util.TupledFunction -object Test { - def main(args: Array[String]): Unit = { - - val f2: (x: (Int, Long)) => x._1.type = (args: (Int, Long)) => args._1 - val g2: (x: Int, y: Long) => x.type = f2.untupled - println(g2(1, 3L)) - - } - - /** Creates an untupled version of this function: instead of a single argument of type [[scala.Tuple]] with N elements, - * it accepts N arguments. - * - * This is a generalization of [[scala.Function.untupled]] that work on functions of any arity - * - * @tparam F the function type - * @tparam Args the tuple type with the same types as the function arguments of F - * @tparam R the return type of F - */ - extension [F, Args <: Tuple, R](f: Args => R) def untupled(using tf: TupledFunction[F, Args => R]): F = tf.untupled(f) -}