Top-level methods with the same name in the same package overwrite each other
Closed this issue · 12 comments
Compiler version
3.3.5, 3.4.3, 3.5.2, 3.6.3
Minimized code
// test1.scala
package example
private def foobar: String = "foo"
object test1 { val x = foobar }
// test2.scala
package example
private def foobar: Int = 1
object test2 { val x = foobar }Output
In the console:
scala> example.test1.x
val res0: String = foo
scala> example.test2.x
val res1: String = fooExpectation
Ideally the calls to foobar would be scoped properly in each file but I'm guessing that's hard, so at least a compiler warning or error would be nice.
For a moment, I believed.
scala> example.test1.x
val res0: String = foo
scala> example.test2.x
val res1: Int = 1
scala>
That is separate compilation and on the REPL classpath of 3.7.
I do see the same issue with latest nightly and with 3.6.4-RC2.
@tgodzik How to reproduce? I get the same result with scala-cli repl --server=false -S 3.6.3 -Vprint:typer -cp ~/sandbox.
@tgodzik Thanks! I don't normally use scala-cli that way. I get the problem now.
(Same result with regular joint compilation. The private within is not a repl artifact.)
TIL it's intended that top-level defs from different files can be overloaded.
This is not an overload: <SingleDenotation of type ExprType(AndType(TypeRef(TermRef(ThisType(TypeRef(NoPrefix,module class scala)),object Predef),type String),TypeRef(TermRef(ThisType(TypeRef(NoPrefix,module class <root>)),object scala),class Int)))>
I remember previous discussion on a ticket about the semantic of top-level private, but I incorrectly assumed I correctly intuited what was intended. Now I think explicit private[p] should be required for current behavior.
FWIW, lookup in typer has an exclusion for opaque types (where opaque scope really is the package object). I would expect lookup to prefer my local package object, but in the presence of overloads, what I really mean is that if there is a competing symbol, prefer my local package object as a tie-breaker. (That does not seem stranger as a special rule than relaxed imports that make extension methods more useful or tractable.)
When compiling test1.scala and test2.scala separately the issue does not appear, so it seems like it's only a problem with choosing the correct reference or creating the denotation (and not with tasty or anything more serious).
Oh, but even if we choose to use the more-local reference, the problem will still happen for other files which do not have their own local ref:
package example
private def foobar: String = "foo"
object test1 { val x = foobar }package example
private def foobar: Int = 0
object test2 { val x = foobar }package example:
object test3:
def x = foobar // which one do we choose?
@main def main() =
println(example.test1.x)
println(example.test2.x)
println(example.test3.x)We could scope the private for file (instead of package), but that's a little arbitrary, especially that private objects in packages are scoped across different files (and having multiple of those in the same package throws an error). Also, vals have the same problems as defs here. What I'm trying to say here is that I think we should error this out.
Ok, via reference (https://docs.scala-lang.org/scala3/reference/dropped-features/package-objects.html):
A private top-level definition is always visible from everywhere in the enclosing package. If several top-level definitions are overloaded variants with the same name, they must all come from the same source file.
So it should definitely error out, like it already does when two of the same names with private[package] are used.
Oh good. I didn't check the spec, I only misread a code comment in Typer.
This remains true if the 2 first files are compiled separately, regardless of the access modifier used, unfortunately
Because we are now "aware" of the package object due to opaque types, I might have preferred a spec change, with private meaning private to the package object, and qualified private for current behavior. (Note to self when I read this later.)
