diff --git a/compiler/src/dotty/tools/dotc/transform/Bridges.scala b/compiler/src/dotty/tools/dotc/transform/Bridges.scala index 9a76a5a6be9e..a89dd1d94327 100644 --- a/compiler/src/dotty/tools/dotc/transform/Bridges.scala +++ b/compiler/src/dotty/tools/dotc/transform/Bridges.scala @@ -12,7 +12,8 @@ import ContextFunctionResults.{contextResultCount, contextFunctionResultTypeAfte import StdNames.nme import Constants.Constant import TypeErasure.transformInfo -import Erasure.Boxing.adaptClosure +import Erasure.{Boxing, partialApply, PostErasureArgs} +import Boxing.adaptClosure /** A helper class for generating bridge methods in class `root`. */ class Bridges(root: ClassSymbol, thisPhase: DenotTransformer)(using Context) { @@ -168,11 +169,43 @@ class Bridges(root: ClassSymbol, thisPhase: DenotTransformer)(using Context) { expand(args, start, memberCount - otherCount)(using ctx.withOwner(bridge)) end etaExpand + /** Apply `ref` to the flatArgs, in the way that matches pre-eraure signature. + * + * For example: with pre-erasure signature `(using erased Ev)(a: A): String` + * and post-erasure args `flatArgs = [a]`, this method produces + * `Apply(Apply(ref, []), [a])` instead of `Apply(ref, [a])`. + * One `Apply` per pre-erasure list; the first is empty because its only + * parameter is erased. + */ + def applyMemberArgs(ref: Tree, flatArgs: List[Tree])(using Context): Tree = + val preInfo = inContext(preErasureCtx)(member.info) // pre-erasure type info + + def loop(fn: Tree, tp: Type, args: List[Tree]): Tree = tp match + case pt: PolyType => + loop(fn, pt.resultType, args) + + case mt: MethodType if fn.tpe.widen.isInstanceOf[MethodType] => + // `args` contains only non-erased params. Split by pre-erasure list boundary: + // `here` is this list's non-erased args, `rest` goes to later lists. + // e.g. for the 1st args-list of `(using erased Ev)(a: A)`, it's n=0 + // if the `args` are `[a]`, here=[], and rest=[a]. + val n = inContext(preErasureCtx)(mt.nonErasedParamCount) + val (here, rest) = args.splitAt(n) + + // `partialApply` (vs `tpd.Apply`) skips the arity check, which would + // reject the 0-arg intermediate Apply produced by an all-erased list. + // `PostErasureArgs` tells `typedApply` not to re-filter these args. + val next = partialApply(fn, here).withAttachment(PostErasureArgs, ()) + loop(next, mt.resultType, rest) + case _ => + fn + loop(ref, preInfo, flatArgs) + def bridgeRhs(argss: List[List[Tree]]) = assert(argss.tail.isEmpty) val ref = This(root).select(member) if member.info.isParameterless then ref // can happen if `member` is a module - else if memberCount == 0 then ref.appliedToTermArgs(argss.head) + else if memberCount == 0 then applyMemberArgs(ref, argss.head) else etaExpand(ref, argss.head) bridges += DefDef(bridge, bridgeRhs(_).withSpan(bridge.span)) diff --git a/compiler/src/dotty/tools/dotc/transform/Erasure.scala b/compiler/src/dotty/tools/dotc/transform/Erasure.scala index ed2a88519ca0..a1ec10ab7345 100644 --- a/compiler/src/dotty/tools/dotc/transform/Erasure.scala +++ b/compiler/src/dotty/tools/dotc/transform/Erasure.scala @@ -214,6 +214,12 @@ object Erasure { */ private val BunchedArgs = new Property.Key[Unit] + /** An attachment on an Apply node whose arguments are already in + * post-erasure form i.e. all arguments are non-erased parameters. + * Generate by `Bridges` so that `typedApply` skips its erased-argument filter. + */ + private[transform] val PostErasureArgs = new Property.Key[Unit] + /** An Apply node which might still be missing some arguments */ def partialApply(fn: Tree, args: List[Tree])(using Context): Tree = untpd.Apply(fn, args.toList) @@ -805,7 +811,8 @@ object Erasure { val origFun = fun.asInstanceOf[tpd.Tree] val origFunType = origFun.tpe.widen(using preErasureCtx) val ownArgs = origFunType match - case mt: MethodType if mt.hasErasedParams => + case mt: MethodType if mt.hasErasedParams + && !tree.hasAttachment(PostErasureArgs) => args.lazyZip(mt.paramErasureStatuses).flatMap: (arg, isErased) => if isErased then checkPureErased(arg, isArgument = true, diff --git a/tests/run/i25726.scala b/tests/run/i25726.scala new file mode 100644 index 000000000000..21d0185a52e4 --- /dev/null +++ b/tests/run/i25726.scala @@ -0,0 +1,55 @@ +// Regression test for https://github.com/scala/scala3/issues/25726 +import scala.language.experimental.erasedDefinitions + +class Ev + +// i25726 +trait Svc[A]: + def run(using erased Ev)(a: A): String +class ISvc extends Svc[Int]: + def run(using erased Ev)(a: Int): String = a.toString + +// more args-lists +class Token +trait Store[A]: + def put(using erased Token)(key: String)(value: A): String +class IntStore extends Store[Int]: + def put(using erased Token)(key: String)(value: Int): String = s"$key=$value" + +// trailing all-erased list +trait Trail[A]: + def id(a: A)(using erased Ev): A +class IntTrail extends Trail[Int]: + def id(a: Int)(using erased Ev): Int = a + +// Erased-first inside a single parameter list. +trait Single[A]: + def run(erased a: Ev, b: A): String +class IntSingle extends Single[Int]: + def run(erased a: Ev, b: Int): String = b.toString + +// Erased-first within a non-trailing parameter list. +trait Mixed[A]: + def run(erased e: Ev, a: A)(b: Int): String +class IntMixed extends Mixed[Int]: + def run(erased e: Ev, a: Int)(b: Int): String = s"$a+$b" + +object Test: + def main(args: Array[String]): Unit = + erased given Ev = new Ev + erased given Token = new Token + + val isvc: Svc[Int] = new ISvc + assert(isvc.run(42) == "42", isvc.run(42)) + + val store: Store[Int] = new IntStore + assert(store.put("answer")(42) == "answer=42", store.put("answer")(42)) + + val trail: Trail[Int] = new IntTrail + assert(trail.id(7) == 7) + + val single: Single[Int] = new IntSingle + assert(single.run(new Ev, 9) == "9", single.run(new Ev, 9)) + + val mixed: Mixed[Int] = new IntMixed + assert(mixed.run(new Ev, 1)(2) == "1+2", mixed.run(new Ev, 1)(2))