Skip to content

Commit 4e028bf

Browse files
committed
Support extension methods imported from different objects
Add a special case to name resolution so that when expanding an extension method from `e.m` to `m(e)` and `m` is imported by several imports on the same level, we try to typecheck under every such import and pick the successful alternative if it exists and is unambiguous. Fixes #16920
1 parent 8b635cc commit 4e028bf

File tree

9 files changed

+324
-43
lines changed

9 files changed

+324
-43
lines changed

compiler/src/dotty/tools/dotc/reporting/ErrorMessageID.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,7 @@ enum ErrorMessageID(val isActive: Boolean = true) extends java.lang.Enum[ErrorMe
193193
case ConstrProxyShadowsID // errorNumber 177
194194
case MissingArgumentListID // errorNumber: 178
195195
case MatchTypeScrutineeCannotBeHigherKindedID // errorNumber: 179
196+
case AmbiguousExtensionMethodID // errorNumber 180
196197

197198
def errorNumber = ordinal - 1
198199

compiler/src/dotty/tools/dotc/reporting/messages.scala

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1434,6 +1434,15 @@ extends ReferenceMsg(AmbiguousOverloadID), NoDisambiguation {
14341434
|"""
14351435
}
14361436

1437+
class AmbiguousExtensionMethod(tree: untpd.Tree, expansion1: tpd.Tree, expansion2: tpd.Tree)(using Context)
1438+
extends ReferenceMsg(AmbiguousExtensionMethodID), NoDisambiguation:
1439+
def msg(using Context) =
1440+
i"""Ambiguous extension methods:
1441+
|both $expansion1
1442+
|and $expansion2
1443+
|are possible expansions of $tree"""
1444+
def explain(using Context) = ""
1445+
14371446
class ReassignmentToVal(name: Name)(using Context)
14381447
extends TypeMsg(ReassignmentToValID) {
14391448
def msg(using Context) = i"""Reassignment to val $name"""

compiler/src/dotty/tools/dotc/typer/Typer.scala

Lines changed: 81 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -159,8 +159,13 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
159159
* @param required flags the result's symbol must have
160160
* @param excluded flags the result's symbol must not have
161161
* @param pos indicates position to use for error reporting
162+
* @param altImports a ListBuffer in which alternative imported references are
163+
* collected in case `findRef` is called from an expansion of
164+
* an extension method, i.e. when `e.m` is expanded to `m(e)` and
165+
* a reference for `m` is searched. `null` in all other situations.
162166
*/
163-
def findRef(name: Name, pt: Type, required: FlagSet, excluded: FlagSet, pos: SrcPos)(using Context): Type = {
167+
def findRef(name: Name, pt: Type, required: FlagSet, excluded: FlagSet, pos: SrcPos,
168+
altImports: mutable.ListBuffer[TermRef] | Null = null)(using Context): Type = {
164169
val refctx = ctx
165170
val noImports = ctx.mode.is(Mode.InPackageClauseName)
166171
def suppressErrors = excluded.is(ConstructorProxy)
@@ -231,15 +236,52 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
231236
fail(AmbiguousReference(name, newPrec, prevPrec, prevCtx))
232237
previous
233238

234-
/** Recurse in outer context. If final result is same as `previous`, check that it
235-
* is new or shadowed. This order of checking is necessary since an
236-
* outer package-level definition might trump two conflicting inner
237-
* imports, so no error should be issued in that case. See i7876.scala.
239+
/** Assemble and check alternatives to an imported reference. This implies:
240+
* - If we expand an extension method (i.e. altImports != null),
241+
* search imports on the same level for other possible resolutions of `name`.
242+
* The result and altImports together then contain all possible imported
243+
* references of the highest possible precedence, where `NamedImport` beats
244+
* `WildImport`.
245+
* - Find a posssibly shadowing reference in an outer context.
246+
* If the result is the same as `previous`, check that it is new or
247+
* shadowed. This order of checking is necessary since an outer package-level
248+
* definition might trump two conflicting inner imports, so no error should be
249+
* issued in that case. See i7876.scala.
250+
* @param previous the previously found reference (which is an import)
251+
* @param prevPrec the precedence of the reference (either NamedImport or WildImport)
252+
* @param prevCtx the context in which the reference was found
253+
* @param using_Context the outer context of `precCtx`
238254
*/
239-
def recurAndCheckNewOrShadowed(previous: Type, prevPrec: BindingPrec, prevCtx: Context)(using Context): Type =
240-
val found = findRefRecur(previous, prevPrec, prevCtx)
241-
if found eq previous then checkNewOrShadowed(found, prevPrec)(using prevCtx)
242-
else found
255+
def checkImportAlternatives(previous: Type, prevPrec: BindingPrec, prevCtx: Context)(using Context): Type =
256+
257+
def addAltImport(altImp: TermRef) =
258+
if !TypeComparer.isSameRef(previous, altImp)
259+
&& !altImports.uncheckedNN.exists(TypeComparer.isSameRef(_, altImp))
260+
then
261+
altImports.uncheckedNN += altImp
262+
263+
if altImports != null && ctx.isImportContext then
264+
val curImport = ctx.importInfo.uncheckedNN
265+
namedImportRef(curImport) match
266+
case altImp: TermRef =>
267+
if prevPrec == WildImport then
268+
// Discard all previously found references and continue with `altImp`
269+
altImports.clear()
270+
checkImportAlternatives(altImp, NamedImport, ctx)(using ctx.outer)
271+
else
272+
addAltImport(altImp)
273+
checkImportAlternatives(previous, prevPrec, prevCtx)(using ctx.outer)
274+
case _ =>
275+
if prevPrec == WildImport then
276+
wildImportRef(curImport) match
277+
case altImp: TermRef => addAltImport(altImp)
278+
case _ =>
279+
checkImportAlternatives(previous, prevPrec, prevCtx)(using ctx.outer)
280+
else
281+
val found = findRefRecur(previous, prevPrec, prevCtx)
282+
if found eq previous then checkNewOrShadowed(found, prevPrec)(using prevCtx)
283+
else found
284+
end checkImportAlternatives
243285

244286
def selection(imp: ImportInfo, name: Name, checkBounds: Boolean): Type =
245287
imp.importSym.info match
@@ -329,7 +371,6 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
329371
if (ctx.scope eq EmptyScope) previous
330372
else {
331373
var result: Type = NoType
332-
333374
val curOwner = ctx.owner
334375

335376
/** Is curOwner a package object that should be skipped?
@@ -450,11 +491,11 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
450491
else if (isPossibleImport(NamedImport) && (curImport nen outer.importInfo)) {
451492
val namedImp = namedImportRef(curImport.uncheckedNN)
452493
if (namedImp.exists)
453-
recurAndCheckNewOrShadowed(namedImp, NamedImport, ctx)(using outer)
494+
checkImportAlternatives(namedImp, NamedImport, ctx)(using outer)
454495
else if (isPossibleImport(WildImport) && !curImport.nn.importSym.isCompleting) {
455496
val wildImp = wildImportRef(curImport.uncheckedNN)
456497
if (wildImp.exists)
457-
recurAndCheckNewOrShadowed(wildImp, WildImport, ctx)(using outer)
498+
checkImportAlternatives(wildImp, WildImport, ctx)(using outer)
458499
else {
459500
updateUnimported()
460501
loop(ctx)(using outer)
@@ -3412,11 +3453,37 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
34123453
def selectionProto = SelectionProto(tree.name, mbrProto, compat, privateOK = inSelect)
34133454

34143455
def tryExtension(using Context): Tree =
3415-
findRef(tree.name, WildcardType, ExtensionMethod, EmptyFlags, qual.srcPos) match
3456+
val altImports = new mutable.ListBuffer[TermRef]()
3457+
findRef(tree.name, WildcardType, ExtensionMethod, EmptyFlags, qual.srcPos, altImports) match
34163458
case ref: TermRef =>
3417-
extMethodApply(untpd.TypedSplice(tpd.ref(ref).withSpan(tree.nameSpan)), qual, pt)
3459+
def tryExtMethod(ref: TermRef)(using Context) =
3460+
extMethodApply(untpd.TypedSplice(tpd.ref(ref).withSpan(tree.nameSpan)), qual, pt)
3461+
if altImports.isEmpty then
3462+
tryExtMethod(ref)
3463+
else
3464+
// Try all possible imports and collect successes and failures
3465+
val successes, failures = new mutable.ListBuffer[(Tree, TyperState)]
3466+
for alt <- ref :: altImports.toList do
3467+
val nestedCtx = ctx.fresh.setNewTyperState()
3468+
val app = tryExtMethod(alt)(using nestedCtx)
3469+
(if nestedCtx.reporter.hasErrors then failures else successes)
3470+
+= ((app, nestedCtx.typerState))
3471+
typr.println(i"multiple extensioin methods, success: ${successes.toList}, failure: ${failures.toList}")
3472+
3473+
def pick(alt: (Tree, TyperState)): Tree =
3474+
val (app, ts) = alt
3475+
ts.commit()
3476+
app
3477+
3478+
successes.toList match
3479+
case Nil => pick(failures.head)
3480+
case success :: Nil => pick(success)
3481+
case (expansion1, _) :: (expansion2, _) :: _ =>
3482+
report.error(AmbiguousExtensionMethod(tree, expansion1, expansion2), tree.srcPos)
3483+
expansion1
34183484
case _ =>
34193485
EmptyTree
3486+
end tryExtension
34203487

34213488
def nestedFailure(ex: TypeError) =
34223489
rememberSearchFailure(qual,

docs/_docs/reference/contextual/extension-methods.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -244,7 +244,16 @@ The precise rules for resolving a selection to an extension method are as follow
244244
Assume a selection `e.m[Ts]` where `m` is not a member of `e`, where the type arguments `[Ts]` are optional, and where `T` is the expected type.
245245
The following two rewritings are tried in order:
246246

247-
1. The selection is rewritten to `m[Ts](e)`.
247+
1. The selection is rewritten to `m[Ts](e)` and typechecked, using the following
248+
slight modification of the name resolution rules:
249+
250+
- If `m` is imported by several imports which are all on the nesting level,
251+
try each import as an extension method instead of failing with an ambiguity.
252+
If only one import leads to an expansion that typechecks without errors, pick
253+
that expansion. If there are several such imports, but only one import which is
254+
not a wildcard import, pick the expansion from that import. Otherwise, report
255+
an ambiguous reference error.
256+
248257
2. If the first rewriting does not typecheck with expected type `T`,
249258
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
250259

tests/neg/i13558.check

Lines changed: 0 additions & 26 deletions
This file was deleted.

tests/neg/i16920.check

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
-- [E008] Not Found Error: tests/neg/i16920.scala:18:11 ----------------------------------------------------------------
2+
18 | "five".wow // error
3+
| ^^^^^^^^^^
4+
| value wow is not a member of String.
5+
| An extension method was tried, but could not be fully constructed:
6+
|
7+
| Two.wow("five")
8+
|
9+
| failed with:
10+
|
11+
| Found: ("five" : String)
12+
| Required: Int
13+
-- [E008] Not Found Error: tests/neg/i16920.scala:26:6 -----------------------------------------------------------------
14+
26 | 5.wow // error
15+
| ^^^^^
16+
| value wow is not a member of Int.
17+
| An extension method was tried, but could not be fully constructed:
18+
|
19+
| AlsoFails.wow(5)
20+
|
21+
| failed with:
22+
|
23+
| Found: (5 : Int)
24+
| Required: Boolean
25+
-- [E008] Not Found Error: tests/neg/i16920.scala:27:11 ----------------------------------------------------------------
26+
27 | "five".wow // error
27+
| ^^^^^^^^^^
28+
| value wow is not a member of String.
29+
| An extension method was tried, but could not be fully constructed:
30+
|
31+
| AlsoFails.wow("five")
32+
|
33+
| failed with:
34+
|
35+
| Found: ("five" : String)
36+
| Required: Boolean
37+
-- [E008] Not Found Error: tests/neg/i16920.scala:34:6 -----------------------------------------------------------------
38+
34 | 5.wow // error
39+
| ^^^^^
40+
| value wow is not a member of Int.
41+
| An extension method was tried, but could not be fully constructed:
42+
|
43+
| Three.wow(5)
44+
|
45+
| failed with:
46+
|
47+
| Ambiguous extension methods:
48+
| both Three.wow(5)
49+
| and Two.wow(5)
50+
| are possible expansions of 5.wow
51+
-- [E008] Not Found Error: tests/neg/i16920.scala:42:11 ----------------------------------------------------------------
52+
42 | "five".wow // error
53+
| ^^^^^^^^^^
54+
| value wow is not a member of String.
55+
| An extension method was tried, but could not be fully constructed:
56+
|
57+
| Two.wow("five")
58+
|
59+
| failed with:
60+
|
61+
| Found: ("five" : String)
62+
| Required: Int
63+
-- [E008] Not Found Error: tests/neg/i16920.scala:49:11 ----------------------------------------------------------------
64+
49 | "five".wow // error
65+
| ^^^^^^^^^^
66+
| value wow is not a member of String.
67+
| An extension method was tried, but could not be fully constructed:
68+
|
69+
| Two.wow("five")
70+
|
71+
| failed with:
72+
|
73+
| Found: ("five" : String)
74+
| Required: Int
75+
-- [E008] Not Found Error: tests/neg/i16920.scala:56:6 -----------------------------------------------------------------
76+
56 | 5.wow // error
77+
| ^^^^^
78+
| value wow is not a member of Int.
79+
| An extension method was tried, but could not be fully constructed:
80+
|
81+
| Three.wow(5)
82+
|
83+
| failed with:
84+
|
85+
| Ambiguous extension methods:
86+
| both Three.wow(5)
87+
| and Two.wow(5)
88+
| are possible expansions of 5.wow

tests/neg/i16920.scala

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
object One:
2+
extension (s: String)
3+
def wow: Unit = println(s)
4+
5+
object Two:
6+
extension (i: Int)
7+
def wow: Unit = println(i)
8+
9+
object Three:
10+
extension (i: Int)
11+
def wow: Unit = println(i)
12+
13+
object Fails:
14+
import One._
15+
def test: Unit =
16+
import Two._
17+
5.wow
18+
"five".wow // error
19+
20+
object AlsoFails:
21+
extension (s: Boolean)
22+
def wow = println(s)
23+
import One._
24+
import Two._
25+
def test: Unit =
26+
5.wow // error
27+
"five".wow // error
28+
29+
object Fails2:
30+
import One._
31+
import Two._
32+
import Three._
33+
def test: Unit =
34+
5.wow // error
35+
"five".wow // ok
36+
37+
object Fails3:
38+
import One._
39+
import Two.wow
40+
def test: Unit =
41+
5.wow // ok
42+
"five".wow // error
43+
44+
object Fails4:
45+
import Two.wow
46+
import One._
47+
def test: Unit =
48+
5.wow // ok
49+
"five".wow // error
50+
51+
object Fails5:
52+
import One.wow
53+
import Two.wow
54+
import Three.wow
55+
def test: Unit =
56+
5.wow // error
57+
"five".wow // ok

tests/neg/i13558.scala renamed to tests/pos/i13558.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,12 @@ object Main {
2020
import ExtensionB._
2121
import ExtensionA._
2222
val a = A()
23-
println(a.id) // error
23+
println(a.id) // now ok
2424
}
2525
def main2(args: Array[String]): Unit = {
2626
import ExtensionA._
2727
import ExtensionB._
2828
val a = A()
29-
println(a.id) // error
29+
println(a.id) // now ok
3030
}
3131
}

0 commit comments

Comments
 (0)