Skip to content

Commit 02e60f8

Browse files
authored
Merge pull request #9576 from dotty-staging/fix-#9562
Fix #9562: Fix handling of identifiers in extension methods
2 parents c908b13 + ec964a9 commit 02e60f8

File tree

16 files changed

+89
-60
lines changed

16 files changed

+89
-60
lines changed

compiler/src/dotty/tools/dotc/ast/Desugar.scala

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -899,7 +899,7 @@ object desugar {
899899
name = mdef.name.toExtensionName,
900900
tparams = ext.tparams ++ mdef.tparams,
901901
vparamss = mdef.vparamss match
902-
case vparams1 :: vparamss1 if !isLeftAssoc(mdef.name) =>
902+
case vparams1 :: vparamss1 if mdef.name.isRightAssocOperatorName =>
903903
vparams1 :: ext.vparamss ::: vparamss1
904904
case _ =>
905905
ext.vparamss ++ mdef.vparamss
@@ -1204,10 +1204,10 @@ object desugar {
12041204
case _ =>
12051205
Apply(sel, arg :: Nil)
12061206

1207-
if (isLeftAssoc(op.name))
1208-
makeOp(left, right, Span(left.span.start, op.span.end, op.span.start))
1209-
else
1207+
if op.name.isRightAssocOperatorName then
12101208
makeOp(right, left, Span(op.span.start, right.span.end))
1209+
else
1210+
makeOp(left, right, Span(left.span.start, op.span.end, op.span.start))
12111211
}
12121212

12131213
/** Translate tuple expressions of arity <= 22

compiler/src/dotty/tools/dotc/ast/Positioned.scala

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import util.Spans._
66
import util.{SourceFile, NoSource, SourcePosition, SrcPos}
77
import core.Contexts._
88
import core.Decorators._
9+
import core.NameOps._
910
import core.Flags.{JavaDefined, ExtensionMethod}
1011
import core.StdNames.nme
1112
import ast.Trees.mods
@@ -208,7 +209,7 @@ abstract class Positioned(implicit @constructorOnly src: SourceFile) extends Src
208209
check(tree.vparamss)
209210
case tree: DefDef if tree.mods.is(ExtensionMethod) =>
210211
tree.vparamss match {
211-
case vparams1 :: vparams2 :: rest if !isLeftAssoc(tree.name) =>
212+
case vparams1 :: vparams2 :: rest if tree.name.isRightAssocOperatorName =>
212213
check(tree.tparams)
213214
check(vparams2)
214215
check(vparams1)

compiler/src/dotty/tools/dotc/ast/TreeInfo.scala

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -185,9 +185,6 @@ trait TreeInfo[T >: Untyped <: Type] { self: Trees.Instance[T] =>
185185
case _ => false
186186
}
187187

188-
/** Is name a left-associative operator? */
189-
def isLeftAssoc(operator: Name): Boolean = !operator.isEmpty && (operator.toSimpleName.last != ':')
190-
191188
/** Is this argument node of the form <expr> : _*, or is it a reference to
192189
* such an argument ? The latter case can happen when an argument is lifted.
193190
*/

compiler/src/dotty/tools/dotc/core/ContextOps.scala

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -19,48 +19,48 @@ object ContextOps:
1919
def enter(sym: Symbol): Symbol = inContext(ctx) {
2020
ctx.owner match
2121
case cls: ClassSymbol => cls.classDenot.enter(sym)
22-
case _ => scope.openForMutations.enter(sym)
22+
case _ => ctx.scope.openForMutations.enter(sym)
2323
sym
2424
}
2525

2626
/** The denotation with the given `name` and all `required` flags in current context
2727
*/
2828
def denotNamed(name: Name, required: FlagSet = EmptyFlags): Denotation = inContext(ctx) {
29-
if (owner.isClass)
30-
if (outer.owner == owner) { // inner class scope; check whether we are referring to self
31-
if (scope.size == 1) {
32-
val elem = scope.lastEntry
29+
if (ctx.owner.isClass)
30+
if (ctx.outer.owner == ctx.owner) { // inner class scope; check whether we are referring to self
31+
if (ctx.scope.size == 1) {
32+
val elem = ctx.scope.lastEntry
3333
if (elem.name == name) return elem.sym.denot // return self
3434
}
35-
val pre = owner.thisType
35+
val pre = ctx.owner.thisType
3636
pre.findMember(name, pre, required, EmptyFlags)
3737
}
3838
else // we are in the outermost context belonging to a class; self is invisible here. See inClassContext.
39-
owner.findMember(name, owner.thisType, required, EmptyFlags)
39+
ctx.owner.findMember(name, ctx.owner.thisType, required, EmptyFlags)
4040
else
41-
scope.denotsNamed(name).filterWithFlags(required, EmptyFlags).toDenot(NoPrefix)
41+
ctx.scope.denotsNamed(name).filterWithFlags(required, EmptyFlags).toDenot(NoPrefix)
4242
}
4343

4444
/** A fresh local context with given tree and owner.
4545
* Owner might not exist (can happen for self valdefs), in which case
4646
* no owner is set in result context
4747
*/
4848
def localContext(tree: untpd.Tree, owner: Symbol): FreshContext = inContext(ctx) {
49-
val freshCtx = fresh.setTree(tree)
50-
if (owner.exists) freshCtx.setOwner(owner) else freshCtx
49+
val freshCtx = ctx.fresh.setTree(tree)
50+
if owner.exists then freshCtx.setOwner(owner) else freshCtx
5151
}
5252

5353
/** Context where `sym` is defined, assuming we are in a nested context. */
5454
def defContext(sym: Symbol): Context = inContext(ctx) {
55-
outersIterator
55+
ctx.outersIterator
5656
.dropWhile(_.owner != sym)
5757
.dropWhile(_.owner == sym)
5858
.next()
5959
}
6060

6161
/** A new context for the interior of a class */
6262
def inClassContext(selfInfo: TypeOrSymbol): Context = inContext(ctx) {
63-
val localCtx: Context = fresh.setNewScope
63+
val localCtx: Context = ctx.fresh.setNewScope
6464
selfInfo match {
6565
case sym: Symbol if sym.exists && sym.name != nme.WILDCARD => localCtx.scope.openForMutations.enter(sym)
6666
case _ =>
@@ -69,7 +69,7 @@ object ContextOps:
6969
}
7070

7171
def packageContext(tree: untpd.PackageDef, pkg: Symbol): Context = inContext(ctx) {
72-
if (pkg.is(Package)) fresh.setOwner(pkg.moduleClass).setTree(tree)
72+
if (pkg.is(Package)) ctx.fresh.setOwner(pkg.moduleClass).setTree(tree)
7373
else ctx
7474
}
7575
end ContextOps

compiler/src/dotty/tools/dotc/core/NameOps.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ object NameOps {
7272
def isAnonymousClassName: Boolean = name.startsWith(str.ANON_CLASS)
7373
def isAnonymousFunctionName: Boolean = name.startsWith(str.ANON_FUN)
7474
def isUnapplyName: Boolean = name == nme.unapply || name == nme.unapplySeq
75+
def isRightAssocOperatorName: Boolean = name.lastPart.last == ':'
7576

7677
def isOperatorName: Boolean = name match
7778
case name: SimpleName => name.exists(isOperatorPart)

compiler/src/dotty/tools/dotc/core/SymDenotations.scala

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,18 @@ object SymDenotations {
325325
else recurWithParamss(info, rawParamss)
326326
end paramSymss
327327

328+
/** The extension parameter of this extension method
329+
* @pre this symbol is an extension method
330+
*/
331+
final def extensionParam(using Context): Symbol =
332+
def leadParam(paramss: List[List[Symbol]]): Symbol = paramss match
333+
case (param :: _) :: paramss1 if param.isType => leadParam(paramss1)
334+
case _ :: (snd :: Nil) :: _ if name.isRightAssocOperatorName => snd
335+
case (fst :: Nil) :: _ => fst
336+
case _ => NoSymbol
337+
assert(isAllOf(ExtensionMethod))
338+
leadParam(rawParamss)
339+
328340
/** The denotation is completed: info is not a lazy type and attributes have defined values */
329341
final def isCompleted: Boolean = !myInfo.isInstanceOf[LazyType]
330342

compiler/src/dotty/tools/dotc/parsing/Parsers.scala

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -927,7 +927,7 @@ object Parsers {
927927
var opStack: List[OpInfo] = Nil
928928

929929
def checkAssoc(offset: Token, op1: Name, op2: Name, op2LeftAssoc: Boolean): Unit =
930-
if (isLeftAssoc(op1) != op2LeftAssoc)
930+
if (op1.isRightAssocOperatorName == op2LeftAssoc)
931931
syntaxError(MixedLeftAndRightAssociativeOps(op1, op2, op2LeftAssoc), offset)
932932

933933
def reduceStack(base: List[OpInfo], top: Tree, prec: Int, leftAssoc: Boolean, op2: Name, isType: Boolean): Tree = {
@@ -967,7 +967,7 @@ object Parsers {
967967
def recur(top: Tree): Tree =
968968
if (isIdent && isOperator) {
969969
val op = if (isType) typeIdent() else termIdent()
970-
val top1 = reduceStack(base, top, precedence(op.name), isLeftAssoc(op.name), op.name, isType)
970+
val top1 = reduceStack(base, top, precedence(op.name), !op.name.isRightAssocOperatorName, op.name, isType)
971971
opStack = OpInfo(top1, op, in.offset) :: opStack
972972
colonAtEOLOpt()
973973
newLineOptWhenFollowing(canStartOperand)
@@ -3316,7 +3316,7 @@ object Parsers {
33163316
typeParamClause(ParamOwner.Def)
33173317
else leadingTparams
33183318
val vparamss = paramClauses() match
3319-
case rparams :: rparamss if leadingVparamss.nonEmpty && !isLeftAssoc(ident.name) =>
3319+
case rparams :: rparamss if leadingVparamss.nonEmpty && ident.name.isRightAssocOperatorName =>
33203320
rparams :: leadingVparamss ::: rparamss
33213321
case rparamss =>
33223322
leadingVparamss ::: rparamss

compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -795,7 +795,7 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) {
795795
val (prefix, vparamss) =
796796
if isExtension then
797797
val (leadingParams, otherParamss) = (tree.vparamss: @unchecked) match
798-
case vparams1 :: vparams2 :: rest if !isLeftAssoc(tree.name) =>
798+
case vparams1 :: vparams2 :: rest if tree.name.isRightAssocOperatorName =>
799799
(vparams2, vparams1 :: rest)
800800
case vparams1 :: rest =>
801801
(vparams1, rest)

compiler/src/dotty/tools/dotc/transform/SyntheticMembers.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -409,7 +409,7 @@ class SyntheticMembers(thisPhase: DenotTransformer) {
409409

410410
/** Is this an anonymous class deriving from an enum definition? */
411411
extension (cls: ClassSymbol) private def isEnumValueImplementation(using Context): Boolean =
412-
isAnonymousClass && classParents.head.typeSymbol.is(Enum) // asserted in Typer
412+
cls.isAnonymousClass && cls.classParents.head.typeSymbol.is(Enum) // asserted in Typer
413413

414414
/** If this is the class backing a serializable singleton enum value with base class `MyEnum`,
415415
* and not deriving from `java.lang.Enum` add the method:

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -199,9 +199,10 @@ object Applications {
199199
* I.e., if the expected type is a PolyProto, then `app` will be a `TypeApply(_, args)` where
200200
* `args` are the type arguments of the expected type.
201201
*/
202-
class IntegratedTypeArgs(val app: Tree)(implicit @constructorOnly src: SourceFile) extends tpd.Tree {
202+
class IntegratedTypeArgs(val app: Tree)(implicit @constructorOnly src: SourceFile) extends ProxyTree {
203203
override def span = app.span
204204

205+
def forwardTo = app
205206
def canEqual(that: Any): Boolean = app.canEqual(that)
206207
def productArity: Int = app.productArity
207208
def productElement(n: Int): Any = app.productElement(n)

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1110,7 +1110,7 @@ trait Checking {
11101110
def checkEnumParent(cls: Symbol, firstParent: Symbol)(using Context): Unit =
11111111

11121112
extension (sym: Symbol) def typeRefApplied(using Context): Type =
1113-
typeRef.appliedTo(typeParams.map(_.info.loBound))
1113+
sym.typeRef.appliedTo(sym.typeParams.map(_.info.loBound))
11141114

11151115
def ensureParentDerivesFrom(enumCase: Symbol)(using Context) =
11161116
val enumCls = enumCase.owner.linkedClass

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1620,7 +1620,7 @@ class RefChecks extends MiniPhase { thisPhase =>
16201620
private def checkByNameRightAssociativeDef(tree: DefDef) {
16211621
tree match {
16221622
case DefDef(_, name, _, params :: _, _, _) =>
1623-
if (settings.lint && !treeInfo.isLeftAssoc(name.decodedName) && params.exists(p => isByName(p.symbol)))
1623+
if (settings.lint && name.decodedName.isRightAssocOperatorName && params.exists(p => isByName(p.symbol)))
16241624
unit.warning(tree.pos,
16251625
"by-name parameters will be evaluated eagerly when called as a right-associative infix operator. For more details, see SI-1980.")
16261626
case _ =>

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

Lines changed: 17 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -447,25 +447,6 @@ class Typer extends Namer
447447
if (name == nme.ROOTPKG)
448448
return tree.withType(defn.RootPackage.termRef)
449449

450-
/** Convert a reference `f` to an extension method select `p.f`, where
451-
* `p` is the closest enclosing extension parameter, or else `this`.
452-
*/
453-
def extensionMethodSelect: untpd.Tree =
454-
val xmethod = ctx.owner.enclosingExtensionMethod
455-
val qualifier =
456-
if xmethod.exists then // TODO: see whether we can use paramss for that
457-
val leadParamName = xmethod.info.paramNamess.head.head
458-
def isLeadParam(sym: Symbol) =
459-
sym.is(Param) && sym.owner.owner == xmethod.owner && sym.name == leadParamName
460-
def leadParam(ctx: Context): Symbol =
461-
ctx.scope.lookupAll(leadParamName).find(isLeadParam) match
462-
case Some(param) => param
463-
case None => leadParam(ctx.outersIterator.dropWhile(_.scope eq ctx.scope).next)
464-
untpd.ref(leadParam(ctx).termRef)
465-
else
466-
untpd.This(untpd.EmptyTypeIdent)
467-
untpd.cpy.Select(tree)(qualifier, name)
468-
469450
val rawType = {
470451
val saved1 = unimported
471452
val saved2 = foundUnderScala2
@@ -516,7 +497,20 @@ class Typer extends Namer
516497
else if name.toTermName == nme.ERROR then
517498
setType(UnspecifiedErrorType)
518499
else if name.isTermName then
519-
tryEither(typed(extensionMethodSelect, pt))((_, _) => fail)
500+
// Convert a reference `f` to an extension method select `p.f`, where
501+
// `p` is the closest enclosing extension parameter, or else convert to `this.f`.
502+
val xmethod = ctx.owner.enclosingExtensionMethod
503+
val qualifier =
504+
if xmethod.exists then untpd.ref(xmethod.extensionParam.termRef)
505+
else untpd.This(untpd.EmptyTypeIdent)
506+
val selection = untpd.cpy.Select(tree)(qualifier, name)
507+
val result = tryEither(typed(selection, pt))((_, _) => fail)
508+
def canAccessUnqualified(sym: Symbol) =
509+
sym.is(ExtensionMethod) && (sym.extensionParam.span == xmethod.extensionParam.span)
510+
if !xmethod.exists || result.tpe.isError || canAccessUnqualified(result.symbol) then
511+
result
512+
else
513+
fail
520514
else
521515
fail
522516
end typedIdent
@@ -2348,20 +2342,18 @@ class Typer extends Namer
23482342
typedUnApply(cpy.Apply(tree)(op, l :: r :: Nil), pt)
23492343
else {
23502344
val app = typedApply(desugar.binop(l, op, r), pt)
2351-
if (untpd.isLeftAssoc(op.name)) app
2352-
else {
2345+
if op.name.isRightAssocOperatorName then
23532346
val defs = new mutable.ListBuffer[Tree]
2354-
def lift(app: Tree): Tree = (app: @unchecked) match {
2347+
def lift(app: Tree): Tree = (app: @unchecked) match
23552348
case Apply(fn, args) =>
23562349
if (app.tpe.isError) app
23572350
else tpd.cpy.Apply(app)(fn, LiftImpure.liftArgs(defs, fn.tpe, args))
23582351
case Assign(lhs, rhs) =>
23592352
tpd.cpy.Assign(app)(lhs, lift(rhs))
23602353
case Block(stats, expr) =>
23612354
tpd.cpy.Block(app)(stats, lift(expr))
2362-
}
23632355
wrapDefs(defs, lift(app))
2364-
}
2356+
else app
23652357
}
23662358
checkValidInfix(tree, result.symbol)
23672359
result

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

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ extension [T: Numeric](x: T)
7676
```
7777

7878
If an extension method has type parameters, they come immediately after `extension` and are followed by the extended parameter.
79-
When calling a generic extension method, any explicitly given type arguments follow the method name.
79+
When calling a generic extension method, any explicitly given type arguments follow the method name.
8080
So the `second` method could be instantiated as follows:
8181

8282
```scala
@@ -92,8 +92,8 @@ extension [T](x: T)(using n: Numeric[T])
9292
def + (y: T): T = n.plus(x, y)
9393
```
9494

95-
**Note**: Type parameters have to be given after the `extension` keyword; they cannot be given after the `def`.
96-
This restriction might be lifted in the future once we support multiple type parameter clauses in a method.
95+
**Note**: Type parameters have to be given after the `extension` keyword; they cannot be given after the `def`.
96+
This restriction might be lifted in the future once we support multiple type parameter clauses in a method.
9797
By contrast, using clauses can be defined for the `extension` as well as per `def`.
9898

9999
### Collective Extensions
@@ -215,7 +215,7 @@ List(1, 2) < List(3)
215215

216216
The precise rules for resolving a selection to an extension method are as follows.
217217

218-
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.
218+
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.
219219
The following two rewritings are tried in order:
220220

221221
1. The selection is rewritten to `extension_m[Ts](e)`.
@@ -233,9 +233,11 @@ An extension method can also be used as an identifier by itself. If an identifie
233233
resolve, the identifier is rewritten to:
234234

235235
- `x.m` if the identifier appears in an extension with parameter `x`
236+
and the method `m` resolves to an extension method in
237+
a (possibly collective) extension that also contains the call,
236238
- `this.m` otherwise
237239

238-
and the rewritten term is again tried as an application of an extension method. Example:
240+
and the rewritten term is again tried as an application of an extension method. In
239241

240242
```scala
241243
extension (s: String)
@@ -264,7 +266,7 @@ def extension_position(s: String)(ch: Char, n: Int): Int =
264266
extension (x: Double) def ** (exponent: Int): Double =
265267
require(exponent >= 0)
266268
if exponent == 0 then 1 else x * (x ** (exponent - 1))
267-
269+
268270
import DoubleOps.{**, extension_**}
269271
assert(2.0 ** 3 == extension_**(2.0)(3))
270272
```

tests/neg/i9562.scala

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
class Foo:
2+
def foo = 23
3+
4+
object Unrelated:
5+
extension (f: Foo)
6+
def g = f.foo // OK
7+
8+
extension (f: Foo)
9+
def h1: Int = foo // error
10+
def h2: Int = h1 + 1 // OK
11+
def h3: Int = g // error
12+
def ++: (x: Int): Int = h1 + x // OK

tests/pos/i9562.scala

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
class Foo:
2+
def foo = 23
3+
4+
object Unrelated:
5+
extension (f: Foo)
6+
def g = f.foo // OK
7+
8+
extension (f: Foo)
9+
def h1: Int = 0
10+
def h2: Int = h1 + 1 // OK
11+
def ++: (x: Int): Int = h2 + x // OK

0 commit comments

Comments
 (0)