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

Compiler requires non-dependency to be in classpath #4484

Closed
scabug opened this issue Apr 14, 2011 · 4 comments
Closed

Compiler requires non-dependency to be in classpath #4484

scabug opened this issue Apr 14, 2011 · 4 comments

Comments

@scabug
Copy link

scabug commented Apr 14, 2011

=== What steps will reproduce the problem ===
Three simple class definitions in three separate source files:

File project1/Project1.scala

class Project1

File project2/Project2.scala

class Project2(val project1: Project1)

File project3/Project3.scala

class Project3(val project2: Project2)  {
  def works = project2
  def fails(b: Boolean) = if (b) project2 else null
}

As seen from the source above:

  • Project2 depends on Project1
  • Project3 depends on Project2

Now, compile each project separately into separate output folders:

scalac -d bin/project1/ project1/Project1.scala
scalac -d bin/project2/ -cp bin/project1/ project2/Project2.scala
scalac -d bin/project3/ -cp bin/project2/ project3/Project3.scala

=== What is the expected behavior? ===
It is expected that the code compiles. :-)

=== What do you see instead? ===
Project1 and Project2 compiles without errors.

Compiling Project3 fails with this output:

exception when typing if (b)
  Project3.this.project2()
else
  null
exception when typing def fails(b: Boolean): Project2 = if (b)
  Project3.this.project2()
else
  null
exception when typing class Project3 extends java.lang.Object with ScalaObject {
  <paramaccessor> private[this] val project2: Project2 = _;
  <stable> <accessor> <paramaccessor> def project2(): Project2 = Project3.this.project2;
  def this(project2: Project2): Project3 = {
    Project3.super.this();
    ()
  };
  def works(): Project2 = Project3.this.project2();
  def fails(b: Boolean): Project2 = if (b)
    Project3.this.project2()
  else
    null
}
exception when typing package <empty> {
  class Project3 extends java.lang.Object with ScalaObject {
    <paramaccessor> private[this] val project2: Project2 = _;
    <stable> <accessor> <paramaccessor> def project2(): Project2 = Project3.this.project2;
    def this(project2: Project2): Project3 = {
      Project3.super.this();
      ()
    };
    def works(): Project2 = Project3.this.project2();
    def fails(b: Boolean): Project2 = if (b)
      Project3.this.project2()
    else
      null
  }
}
error: class file needed by Project2 is missing.
reference type Project1 of package <empty> refers to nonexisting symbol.
one error found

Commenting out function definition fails(b: Boolean) makes the code compile.

Also, adding Project1 to the classpath makes the code compile. We have decompiled the bytecode using javap and there are no references to Project1.

We have implemented the same code in Java, and Project1 is not required when compiling Project3. The bytecode produced is almost identical to the code produced by the Scala compiler.

=== What versions of the following are you using? ===

  • Scala: 2.9.0.r24728-b20110411140906
  • Java:
  • java version "1.6.0_24"
  • Java(TM) SE Runtime Environment (build 1.6.0_24-b07-334-10M3326)
  • Java HotSpot(TM) 64-Bit Server VM (build 19.1-b02-334, mixed mode)
  • Operating system: Mac OS X 10.6
@scabug
Copy link
Author

scabug commented Apr 14, 2011

Imported From: https://issues.scala-lang.org/browse/SI-4484?orig=1
Reporter: Martin Gamwell Dawids (mgd)
Attachments:

@scabug
Copy link
Author

scabug commented Apr 14, 2011

Martin Gamwell Dawids (mgd) said:
Reproduction scenario

@scabug
Copy link
Author

scabug commented May 3, 2011

@dragos said:
Thank you for the very detailed bug report, and nice script to reproduce it.

As you noticed, Java can get away with it because the language is a bit simpler. In this case, the type checker needs to compute the least-upper bound of the two branches in the if, for it to infer the return type of the method. Moreover, in Scala you have refinement types, and they can be inferred from such code. Therefore, the compiler needs access to the members of a type, and their types, and so on. If you simplify a bit the code, you can make it compile. For instance, this succeeds:

class Project3(val project2: Project2)  {
	def works = project2
	def fails(b: Boolean): Project2 = if (b) project2 else works
}

I doubt there is anything we can do about it, so I have to mark this as won't fix.

@scabug scabug closed this as completed May 18, 2011
@scabug
Copy link
Author

scabug commented Jun 3, 2011

Martin Gamwell Dawids (mgd) said:
I see the problem.

The original issue occurred when using simple case classes, like this:

File project1/Project1.scala

class Project1

File project2/Project2.scala

class Project2(val project1: Project1)

File project3/Project3.scala

case class Project3(val project2: Project2)

The reproduction scenario was done by decompiling the class file for Project3 and then writing the simplest class that would trigger the problem.

I think it is a pity that nothing can be done. It will tie projects closer together than seems to be necessary. Having a very simple case class in one project will expand this project's dependencies to the transitive closure of its direct dependencies (I guess). This is bad news for modularisation in OSGi (at least) as you will have to declare your ''all'' transitive dependencies. Also, generally it is a pity that all your transitive dependencies must be present at compile time.

Would it be possible to loosen up on type inference when a class's transitive dependencies are not present at compile time (like Project1 is not)? In this case the compiler could only infer that the least upper bound of 'project2' and 'null' is 'Any' as it has no access to Project1. Maybe a compiler option could allow this behaviour. I understand that it is problematic as the compiler will the infer different result type depending on this compiler option.

Alternatively, something could maybe be done regarding the code that is implicitly produced when compiling a case class. Something along the lines:

class Project3(val project2: Project2)  {
  def works = project2
  def fails(b: Boolean) : Project2 = {
    val nullProject2 : Project2 = null
    if (b) project2 else nullProject2
  }
}

(Unfortunately, this also does not compile, but I think it should.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant