Skip to content

Commit 49879ac

Browse files
authored
Support extension methods imported from different objects (#17050)
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
2 parents 7f0a902 + 81e9e1b commit 49879ac

File tree

12 files changed

+371
-41
lines changed

12 files changed

+371
-41
lines changed

compiler/src/dotty/tools/dotc/config/Feature.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ object Feature:
2929
val fewerBraces = experimental("fewerBraces")
3030
val saferExceptions = experimental("saferExceptions")
3131
val clauseInterleaving = experimental("clauseInterleaving")
32+
val relaxedExtensionImports = experimental("relaxedExtensionImports")
3233
val pureFunctions = experimental("pureFunctions")
3334
val captureChecking = experimental("captureChecking")
3435
val into = experimental("into")

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 Feature.enabled(Feature.relaxedExtensionImports) && 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: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -244,7 +244,18 @@ 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+
257+
**Note**: This relaxation is currently enabled only under the `experimental.relaxedExtensionImports` language import.
258+
248259
2. If the first rewriting does not typecheck with expected type `T`,
249260
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
250261

library/src/scala/runtime/stdLibPatches/language.scala

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,14 @@ object language:
6969
@compileTimeOnly("`clauseInterleaving` can only be used at compile time in import statements")
7070
object clauseInterleaving
7171

72+
/** Adds support for relaxed imports of extension methods.
73+
* Extension methods with the same name can be imported from several places.
74+
*
75+
* @see [[http://dotty.epfl.ch/docs/reference/contextual/extension-methods]]
76+
*/
77+
@compileTimeOnly("`relaxedExtensionImports` can only be used at compile time in import statements")
78+
object relaxedExtensionImports
79+
7280
/** Experimental support for pure function type syntax
7381
*
7482
* @see [[https://dotty.epfl.ch/docs/reference/experimental/purefuns]]

project/MiMaFilters.scala

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ object MiMaFilters {
2626
ProblemFilters.exclude[MissingClassProblem]("scala.runtime.stdLibPatches.language$experimental$clauseInterleaving$"),
2727
ProblemFilters.exclude[MissingFieldProblem]("scala.runtime.stdLibPatches.language#experimental.into"),
2828
ProblemFilters.exclude[MissingClassProblem]("scala.runtime.stdLibPatches.language$experimental$into$"),
29+
ProblemFilters.exclude[MissingFieldProblem]("scala.runtime.stdLibPatches.language#experimental.relaxedExtensionImports"),
30+
ProblemFilters.exclude[MissingClassProblem]("scala.runtime.stdLibPatches.language$experimental$relaxedExtensionImports$"),
2931
// end of New experimental features in 3.3.X
3032

3133
// Added java.io.Serializable as LazyValControlState supertype

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

0 commit comments

Comments
 (0)