Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support extension methods imported from different objects #17050

Merged
merged 3 commits into from
Apr 22, 2023

Conversation

odersky
Copy link
Contributor

@odersky odersky commented Mar 6, 2023

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

@odersky
Copy link
Contributor Author

odersky commented Mar 6, 2023

test performance please

1 similar comment
@odersky
Copy link
Contributor Author

odersky commented Mar 6, 2023

test performance please

@odersky
Copy link
Contributor Author

odersky commented Mar 6, 2023

@mbovel it seems the benchmark server is down?

@mbovel
Copy link
Member

mbovel commented Mar 6, 2023

test performance please

1 similar comment
@mbovel
Copy link
Member

mbovel commented Mar 6, 2023

test performance please

@dottybot
Copy link
Member

dottybot commented Mar 6, 2023

performance test scheduled: 1 job(s) in queue, 0 running.

@mbovel
Copy link
Member

mbovel commented Mar 6, 2023

it seems the benchmark server is down?

Fixed.

@mbovel mbovel removed their assignment Mar 6, 2023
@dottybot
Copy link
Member

dottybot commented Mar 6, 2023

Performance test finished successfully:

Visit https://dotty-bench.epfl.ch/17050/ to see the changes.

Benchmarks is based on merging with main (424da9f)

@odersky
Copy link
Contributor Author

odersky commented Mar 6, 2023

test performance please

@dottybot
Copy link
Member

dottybot commented Mar 6, 2023

performance test scheduled: 1 job(s) in queue, 0 running.

@dottybot
Copy link
Member

dottybot commented Mar 6, 2023

Performance test finished successfully:

Visit https://dotty-bench.epfl.ch/17050/ to see the changes.

Benchmarks is based on merging with main (424da9f)

@Ichoran
Copy link

Ichoran commented Mar 6, 2023

This looks great! I just wonder if it's necessary to add a test where an import is re-imported at a deeper level to make sure the search for alternatives is maintained. Something like

object ReimportSucceeds:
  import One._
  def test: Unit =
    import Two._
    import One._
    "five".wow
    5.wow

The reason this is important is that people may want only some extensions in a broader context, but when they switch to a narrower context with more extensions, they may still wish to have the broader extensions available.

For instance, one might want + to add Durations everywhere in a file, but to import a file-handling library that uses + on Path only in one method. But one might want to do temporal math in that same method.

A bulk reimport is probably a reasonable balance between actual accidental collisions and requiring a bunch of extra boilerplate in order to get the desired functionality. Does it already work this way by default? Because I don't know how duplicate but more deeply-nested import statements are handled, I don't know whether this is actually a separate case that needs to be tested.

@odersky
Copy link
Contributor Author

odersky commented Mar 6, 2023

@Ichoran Nested imports take precedence over outer ones, so the example you show should not be a problem.

@bishabosha
Copy link
Member

bishabosha commented Mar 7, 2023

doesnt this require a SIP? or does this come under "inference", I notice this changes the reference "Translation of Calls to Extension Methods", so it is touching spec

Copy link
Member

@bishabosha bishabosha left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should be under experimental import until SIP approval

@bishabosha bishabosha added the needs-minor-release This PR cannot be merged until the next minor release label Mar 7, 2023
@sjrd
Copy link
Member

sjrd commented Mar 7, 2023

Yes, indeed. As much as I think this is a clear win, there is a reason we have a process: to allow eyes with other kinds of expertise than compiler engineering to see it first, to catch things we might miss.

@odersky
Copy link
Contributor Author

odersky commented Mar 7, 2023

I agree that this could be a SIP, but I won't have the time to submit and defend it. Would one of you want to take that over?

@sjrd
Copy link
Member

sjrd commented Mar 7, 2023

I can write the SIP, yes.

@odersky
Copy link
Contributor Author

odersky commented Mar 7, 2023

For now this requires a language import

import language.experimental.relaxedExtensionImports

We will drop that requirement once the SIP committee has approved the change.

@Ichoran
Copy link

Ichoran commented Mar 7, 2023

@sjrd - I'm glad you're able to write the SIP! If it would help to have more motivating examples, I have plenty!

Copy link
Member

@bishabosha bishabosha left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One of the other problems from the Contributor's thread is that this example still does not work:

import language.experimental.relaxedExtensionImports
import scala.annotation.targetName

trait Foo[+T]

object Ops:
  extension (foo : Foo[Int])
    @targetName("bazInt")
    def baz : Unit = {}

import Ops.*

extension (foo : Foo[Double])
  @targetName("bazDouble")
  def baz : Unit = {}

@main def Test =
  val f = new Foo[Int] {}
  f.baz //error

In this case the bazDouble is always chosen because it is not imported, but again surely from the user's point of view it makes sense to also try the valid Ops.bazInt that was imported. To compare with the working Implicit Classes version:

import scala.annotation.targetName

trait Foo[+T]

object Ops:
  implicit class FooOpsInt(foo : Foo[Int]):
    @targetName("bazInt")
    def baz : Unit = {}

import Ops.*

implicit class FooOpsDouble(foo : Foo[Double]):
  @targetName("bazDouble")
  def baz : Unit = {}

@main def Test =
  val f = new Foo[Int] {}
  f.baz //ok

@odersky
Copy link
Contributor Author

odersky commented Mar 8, 2023

In this case the bazDouble is always chosen because it is not imported, but again surely from the user's point of view it makes sense to also try the valid Ops.bazInt that was imported.

Yes, but that's just wishful thinking. And catering to that would simply produce a huge ball of mud. The current change is already highly problematic for it non-orthogonality. But the argument was that this is an important use case and there was no way to re-arrange things to make it work without the change. By comparison, the latest example is a fringe use case, not worth complicating the rules even further.

@bishabosha bishabosha dismissed their stale review March 8, 2023 13:51

it won't be worked on

Copy link
Member

@bishabosha bishabosha left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is a great improvement over the status quo - and the local scope version always being picked is something that can be overcome by application developers (move the extension to a separate object)

@bishabosha bishabosha removed the needs-minor-release This PR cannot be merged until the next minor release label Mar 8, 2023
@sjrd
Copy link
Member

sjrd commented Mar 10, 2023

SIP: scala/improvement-proposals#60

@odersky
Copy link
Contributor Author

odersky commented Apr 22, 2023

Since SIP 54 just got accepted, I'll remove the experimental language import.

@sjrd
Copy link
Member

sjrd commented Apr 22, 2023

It's been accepted to be shipped as experimental, at this stage. ;)

@odersky
Copy link
Contributor Author

odersky commented Apr 22, 2023

Ah indeed, it was labelled stage:design, not stage:implementation.

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 scala#16920
As long as this is not SIP approved, we need to put this under experimental.
@odersky odersky merged commit 49879ac into scala:main Apr 22, 2023
@odersky odersky deleted the fix-16920 branch April 22, 2023 19:40
@Kordyjan Kordyjan added this to the 3.3.1 milestone Aug 1, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Separate compilation breaks extension methods with the same name
8 participants