Extension Method Resolution: Is this Expected Behaviour?

I’m building a computer algebra library for Scala 3, and am using typeclasses to model algebraic structures, and extension methods to define algebraic operations on typeclass carriers. It is common that different algebraic operations on different algebraic structures share the same name, and when I try to model this in Scala 3, I run into two issues regarding resolution of extension methods.

Here is a minified example of the first issue:

trait A[TA]:
  extension (a1: TA) def *(a2: TA): TA

trait B[TA: A, TB]:
  extension (a: TA) def *(b: TB): TB

def breaks[TA: A, TB: [TB] =>>B[TA, TB]](a: TA): TA = 
  // Error: Found:    (a : TA)
  //        Required: ?{ * : ? }
  //        Note that implicit extension methods cannot be applied because 
  //        they are ambiguous;
  //        both parameter evidence$2 and parameter evidence$1 provide an
  //        extension method `*` on (a : TA)
  a * a

I’m having a hard time understanding the documentation, but it seems to me that this shouldn’t be ambiguous?

  1. The selection is rewritten to m[Ts](e) and typechecked, using the following slight modification of the name resolution rules:
  • If m is imported by several imports which are all on the nesting level, try each import as an extension method instead of failing with an ambiguity. If only one import leads to an expansion that typechecks without errors, pick that expansion. If there are several such imports, but only one import which is not a wildcard import, pick the expansion from that import. Otherwise, report an ambiguous reference error.

    Note: This relaxation of the import rules applies only if the method m is used as an extension method. If it is used as a normal method in prefix form, the usual import rules apply, which means that importing m from multiple places can lead to an ambiguity error.

    1. If the first rewriting does not typecheck with expected type T, and there is an extension method m in some eligible object o, the selection is rewritten to o.m[Ts](e). An object o is eligible if
    • 2.1) o forms part of the implicit scope of T, or

    • 2.2) o is a given instance that is visible at the point of the application, or

    • 2.3) o is a given instance in the implicit scope of T.

    This second rewriting is attempted at the time where the compiler also tries an implicit conversion from T to a type containing m. If there is more than one way of rewriting, an ambiguity error results.

Both extensions are available via rule 2.2), and there is only one expansion which typechecks, so it shouldn’t be ambiguous? Or am I misunderstanding something?

The second issue is similar to the first, but instead of an ambiguity error, the Scala compiler doesn’t even try the first extension:

trait A[TA]:
  extension (a1: TA) def *(a2: TA): TA

trait B[TA: A, TB]:
  extension (a: TA) def *(b: TB): TB

  def breaks(a: TA): TA = a * a // Error: Found: (a: TA) required: TB

Is this expected behaviour? Again, as the second extension fails to typecheck, shouldn’t Scala continue resolution with the first extension method via rule 2.2)?

Rereading the documentation, I realise now that it only mentions the disambiguation via typechecking for rule 1), not rule 2). However, shouldn’t the following two extension methods fall under rewrite rule 1), and make the code compile?

trait A[TA]:
  def mul(a1: TA, a2: TA): TA
end A
object A:
  extension [TA: A](a1: TA) def *(a2: TA): TA = summon[A[TA]].mul(a1, a2)
end A

trait B[TA: A, TB]:
  def mul(a: TA, B: TB): TB
end B
object B:
  extension [TA: A, TB: [TB] =>> B[TA, TB]](a: TA)
    def *(b: TB): TB = summon[B[TA, TB]].mul(a, b)
end B

import A._
import B._
def breaks[TA: A](a: TA): TA = a * a // Breaks by ambiguity again

When the B._ import is removed, it does work

trait A[TA]:
  def mul(a1: TA, a2: TA): TA
end A
object A:
  extension [TA: A](a1: TA) def *(a2: TA): TA = summon[A[TA]].mul(a1, a2)
end A

trait B[TA: A, TB]:
  def mul(a: TA, B: TB): TB
end B
object B:
  extension [TA: A, TB: [TB] =>> B[TA, TB]](a: TA)
    def *(b: TB): TB = summon[B[TA, TB]].mul(a, b)
end B

import A._
def doesntBreak[TA: A](a: TA): TA = a * a

I’m not very well-versed with this kind of stuff, and would be very grateful for any help in understanding what’s going on here, and how to potentially fix/work around it if possible.

The error shows that it’s failing on the first parameter list. “An expansion that typechecks” does not mean the whole expression.

There was a recent discussion on the forum or a ticket about how the context bounds are expanded. Normally they are inserted at the end of the signature. You can write them explicitly as follows:

trait A[TA]:
  def mul(a1: TA, a2: TA): TA
end A
object A:
  extension [TA](a1: TA)(using A[TA]) def *(a2: TA): TA = summon[A[TA]].mul(a1, a2)
end A

trait B[TA: A, TB]:
  def mul(a: TA, B: TB): TB
end B
object B:
  extension [TA, TB](a: TA)(using A[TA], B[TA, TB])
    def *(b: TB): TB = summon[B[TA, TB]].mul(a, b)
end B

import A.*
import B.*
def breaks[TA: A](a: TA): TA = a * a