Skip to content

Conversation

bishabosha
Copy link
Member

@bishabosha bishabosha commented Oct 1, 2025

fixes #24111

reset the print function each time the classloader resets, if cant load reflectively, then fallback to calling directly.

before:

scala -S 3.nightly
Downloading Scala 3.8.0-RC1-bin-20251003-172687a-NIGHTLY compiler
Downloading Scala 3.8.0-RC1-bin-20251003-172687a-NIGHTLY bridge
Welcome to Scala 3.8.0-RC1-bin-20251003-172687a-NIGHTLY-git-172687a (17.0.14, Java OpenJDK 64-Bit Server VM).
Type in expressions for evaluation. Or try :help.
                                                                                                                                 
scala> (1, 2L, 3.0F, "hello", 'a')
val res0: (Int, Long, Float, String, Char) = (1,2,3.0,hello,a)
                                                                                                                                 
scala> 

after:

Welcome to Scala 3.8.0-RC1-bin-SNAPSHOT-git-ad52fbe (17.0.14, Java OpenJDK 64-Bit Server VM).
Type in expressions for evaluation. Or try :help.
                                                                                                                                 
scala> (1, 2L, 3.0F, "hello", 'a')
val res0: (Int, Long, Float, String, Char) = (1, 2L, 3.0F, "hello", 'a')
                                                                                                                                 
scala> 

@bishabosha
Copy link
Member Author

i think breakage in the interfaces would become clear as repl tests would fail.

But to isolate the classpath issue in a test, perhaps sbt scripted test is appropriate?

@bishabosha bishabosha force-pushed the repl-use-pprint-via-reflection branch from ad52fbe to 549dcbd Compare October 1, 2025 16:08
@bishabosha
Copy link
Member Author

bishabosha commented Oct 1, 2025

another way i guess is compiling a header script with a wrapper print method and invoking that reflectively,

or perhaps there is some way to manipulate the classloaders to avoid reflection?

classOf[Int], // initialOffset
classOf[Boolean], // escape Unicode
classOf[Boolean], // show field names
)
Copy link
Contributor

Choose a reason for hiding this comment

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

with methodHandle?

Copy link
Member Author

Choose a reason for hiding this comment

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

this isn't well suited because method handle is only optimised if its a static final field, but this is based on a dynamic class loader

Comment on lines 38 to 48
BlackWhite_apply = pprintCls.getMethod("apply",
classOf[Any], // value
classOf[Int], // width
classOf[Int], // height
classOf[Int], // indentation
classOf[Int], // initialOffset
classOf[Boolean], // escape Unicode
classOf[Boolean], // show field names
)
val fansiStrCls = Class.forName("dotty.shaded.fansi.Str", false, cl)
FansiStr_plainText = fansiStrCls.getMethod("plainText")
Copy link
Contributor

@lihaoyi lihaoyi Oct 1, 2025

Choose a reason for hiding this comment

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

Could this be more cleanly written using reflective structural types?

BlackWhite
  .asInstanceOf[{
    def apply(
      value: Any, 
      width: Int, 
      height: Int, 
      indentation: Int, 
      initialOffset: Int, 
      escapeUnicode: Boolean, 
      showFieldNames: Boolean): { def plainText: String }
  }]
  .apply(value, width, height, 2, initialOffset, false, true)
  .plainText

Copy link
Member Author

Choose a reason for hiding this comment

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

good idea!

Copy link
Member Author

Choose a reason for hiding this comment

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

although this still doesn't statically protect against the actual api changing

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah it would be just as safe/unsafe as it is now, just a bit easier to read

@bishabosha
Copy link
Member Author

bishabosha commented Oct 2, 2025

fails with java.lang.ClassNotFoundException: dotty.shaded.pprint.PPrinter$BlackWhite$ in test-scala3-compiler-bootstrapped and test-scala3-compiler-nonbootstrapped for ReplCompilerTests

2025-10-01T17:11:28.8699192Z [info] Test run dotty.tools.dotc.core.tasty.TastyHeaderUnpicklerTest finished: 0 failed, 0 ignored, 20 total, 0.014s
2025-10-01T17:11:28.8713906Z [info] Test run dotty.tools.repl.ReplCompilerTests started
2025-10-01T17:11:28.8719700Z [info] Test dotty.tools.repl.ReplCompilerTests.testSingletonPrint started
2025-10-01T17:11:29.0459423Z [error] Test dotty.tools.repl.ReplCompilerTests.testSingletonPrint failed: org.junit.ComparisonFailure: expected:<[val a: String = "hello"
2025-10-01T17:11:29.0487191Z [error] val x: a.type = "hello"]> but was:<[java.lang.ClassNotFoundException: dotty.shaded.pprint.PPrinter$BlackWhite$
2025-10-01T17:11:29.0488430Z [error]   at dotty.tools.repl.AbstractFileClassLoader.findClass$$anonfun$1(AbstractFileClassLoader.scala:49)
2025-10-01T17:11:29.0489551Z [error]   at scala.runtime.function.JProcedure1.apply(JProcedure1.java:15)
2025-10-01T17:11:29.0490372Z [error]   at scala.runtime.function.JProcedure1.apply(JProcedure1.java:10)
2025-10-01T17:11:29.0491175Z [error]   at scala.collection.immutable.List.foreach(List.scala:327)
2025-10-01T17:11:29.0492109Z [error]   at dotty.tools.repl.AbstractFileClassLoader.findClass(AbstractFileClassLoader.scala:46)
2025-10-01T17:11:29.0493100Z [error]   at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:592)
2025-10-01T17:11:29.0493908Z [error]   at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:525)
2025-10-01T17:11:29.0495180Z [error]   at dotty.tools.repl.AbstractFileClassLoader.loadClass(AbstractFileClassLoader.scala:60)
2025-10-01T17:11:29.0496144Z [error]   at java.base/java.lang.Class.forName0(Native Method)
2025-10-01T17:11:29.0496825Z [error]   at java.base/java.lang.Class.forName(Class.java:467)
2025-10-01T17:11:29.0497564Z [error]   at dotty.tools.repl.Rendering.pprintRender(Rendering.scala:36)
2025-10-01T17:11:29.0498344Z [error]   at dotty.tools.repl.Rendering.replStringOf(Rendering.scala:86)
2025-10-01T17:11:29.0499132Z [error]   at dotty.tools.repl.Rendering.valueOf$$anonfun$2(Rendering.scala:102)

and again class not found exception in dotty.tools.languageserver.WorksheetTest in test_windows_fast

@lihaoyi
Copy link
Contributor

lihaoyi commented Oct 2, 2025

I guess we can check if the class exists in the classloader, and if not then fall back to the previous implementation?

@bishabosha bishabosha force-pushed the repl-use-pprint-via-reflection branch from 549dcbd to 37a56a6 Compare October 2, 2025 16:45
@bishabosha
Copy link
Member Author

I pushed a change with the fallback option if the class loader doesn't have the class

@bishabosha bishabosha force-pushed the repl-use-pprint-via-reflection branch from 37a56a6 to 350733e Compare October 3, 2025 09:58
@SethTisue SethTisue added this to the 3.8.0 milestone Oct 3, 2025

var myClassLoader: AbstractFileClassLoader = uninitialized

private var pprintState: ((Any, Int, Int, Int) => String) | Null = null
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we need to cache this? It seems like it would be simpler to reflectively invoke it every time, since it's going to be run once every few seconds/minutes at most performance wouldn't be a problem

Copy link
Member Author

@bishabosha bishabosha Oct 6, 2025

Choose a reason for hiding this comment

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

i guess i was mostly concerned about catching exceptions every single time in the bad case, maybe it isn't too concerning

Copy link
Contributor

Choose a reason for hiding this comment

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

Even the overhead of exceptions should be negligible. We're talking a few microseconds for an code path that runs every few seconds, or a 0.0001% slowdown. Definitely not worth the increase in code complexity

Copy link
Member Author

@bishabosha bishabosha Oct 6, 2025

Choose a reason for hiding this comment

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

I have removed the caching. I was able to inspect the tests e.g. dotty.tools.repl.ReplCompilerTests and those explicitly only have the standard library on the repl's own classpath, so it makes sense for reflection not to find the class. Perhaps a clean solution would be a settings flag to disable pprint functionality (as the fallback is not perfect), or move the pprint stuff to scala.runtime

Copy link
Contributor

Choose a reason for hiding this comment

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

I guess the question is how is the REPL expected to be used: with the compiler on the classpath, or without? If it's meant to be without, then the only way we can make it work reliably would be to put it in scala-library.jar instead of scala-compiler.jar

Copy link
Member Author

@bishabosha bishabosha Oct 6, 2025

Choose a reason for hiding this comment

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

its definitely by standard with compiler classpath included, both for 2.13.x and 3.x - so actually its really just these repl specific tests that are restricting the classpath

fixes scala#24111

reflectively load the function each time, (i.e. don't cache). As this is a rare event (i.e user interaction),
any cost is less of a concern.
if cant load reflectively, then fallback to calling directly.
@bishabosha bishabosha force-pushed the repl-use-pprint-via-reflection branch from 350733e to bbbe580 Compare October 6, 2025 15:31
@bishabosha
Copy link
Member Author

now with no caching - meaning there is no complex state management anymore

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.

pprint not destructuring results in REPL
5 participants