-
Notifications
You must be signed in to change notification settings - Fork 1.1k
REPL - invoke pprint reflectively #24119
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
base: main
Are you sure you want to change the base?
REPL - invoke pprint reflectively #24119
Conversation
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? |
ad52fbe
to
549dcbd
Compare
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 | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
with methodHandle?
There was a problem hiding this comment.
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
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") |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
good idea!
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
fails with 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 |
I guess we can check if the class exists in the classloader, and if not then fall back to the previous implementation? |
549dcbd
to
37a56a6
Compare
I pushed a change with the fallback option if the class loader doesn't have the class |
37a56a6
to
350733e
Compare
|
||
var myClassLoader: AbstractFileClassLoader = uninitialized | ||
|
||
private var pprintState: ((Any, Int, Int, Int) => String) | Null = null |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
350733e
to
bbbe580
Compare
now with no caching - meaning there is no complex state management anymore |
fixes #24111
reset the print function each time the classloader resets, if cant load reflectively, then fallback to calling directly.
before:
after: