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

Properly display value of value class in repl #15545

Merged
merged 5 commits into from
Jul 1, 2022
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion compiler/src/dotty/tools/dotc/printing/ReplPrinter.scala
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ class ReplPrinter(_ctx: Context) extends RefinedPrinter(_ctx) {
if (sym.is(Method)) {
sym.info match {
case tp: ExprType => ":" ~~ toText(tp.resType)
case _ => toText(sym.info)
case info => toText(info)
}
}
else if (sym.isType && sym.info.isTypeAlias) toText(sym.info)
Expand Down
50 changes: 33 additions & 17 deletions compiler/src/dotty/tools/repl/Rendering.scala
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import dotc.reporting.Diagnostic
* `ReplDriver#resetToInitial` is called, the accompanying instance of
* `Rendering` is no longer valid.
*/
private[repl] class Rendering(parentClassLoader: Option[ClassLoader] = None) {
private[repl] class Rendering(parentClassLoader: Option[ClassLoader] = None):

import Rendering._

Expand Down Expand Up @@ -80,43 +80,62 @@ private[repl] class Rendering(parentClassLoader: Option[ClassLoader] = None) {
* then this bug will surface, so perhaps better not?
* https://github.com/scala/bug/issues/12337
*/
private[repl] def truncate(str: String): String = {
private[repl] def truncate(str: String): String =
val showTruncated = " ... large output truncated, print value to show all"
val ncp = str.codePointCount(0, str.length) // to not cut inside code point
if ncp <= MaxStringElements then str
else str.substring(0, str.offsetByCodePoints(0, MaxStringElements - 1)) + showTruncated
}

/** Return a String representation of a value we got from `classLoader()`. */
private[repl] def replStringOf(value: Object)(using Context): String = {
private[repl] def replStringOf(value: Object)(using Context): String =
assert(myReplStringOf != null,
"replStringOf should only be called on values creating using `classLoader()`, but `classLoader()` has not been called so far")
val res = myReplStringOf(value)
if res == null then "null // non-null reference has null-valued toString" else truncate(res)
}

/** Load the value of the symbol using reflection.
*
* Calling this method evaluates the expression using reflection
*/
private def valueOf(sym: Symbol)(using Context): Option[String] = {
private def valueOf(sym: Symbol)(using Context): Option[String] =
val objectName = sym.owner.fullName.encode.toString.stripSuffix("$")
val resObj: Class[?] = Class.forName(objectName, true, classLoader())
val value =
resObj
.getDeclaredMethods.find(_.getName == sym.name.encode.toString)
.map(_.invoke(null))
val string = value.map(replStringOf(_))
val value = resObj
.getDeclaredMethods.find(_.getName == sym.name.encode.toString)
.map(_.invoke(null))
.flatMap(rewrapValueClass(sym.info.classSymbol, _))

val valueString = value.map(replStringOf)

if (!sym.is(Flags.Method) && sym.info == defn.UnitType)
None
else
string.map { s =>
valueString.map { s =>
if (s.startsWith(REPL_WRAPPER_NAME_PREFIX))
s.drop(REPL_WRAPPER_NAME_PREFIX.length).dropWhile(c => c.isDigit || c == '$')
else
s
}
}

/** Rewrap value classes to their's Wrappers and evaluate their `toString` value.
*
* @param sym Value Class symbol
* @param value underlying value
*/
def rewrapValueClass(sym: Symbol, value: Object)(using Context): Option[Object] =
if (sym.isValueClass && !sym.isPrimitiveValueClass) then
rochala marked this conversation as resolved.
Show resolved Hide resolved
val valueClassName = sym.flatName.encode.toString
rochala marked this conversation as resolved.
Show resolved Hide resolved
val valueClass = Class.forName(valueClassName, true, classLoader())

for {
constructor <- valueClass.getConstructors.headOption
toStringMethod <- valueClass.getMethods.find(_.getName == nme.toString_.toString)
} yield {
val instance = constructor.newInstance(value)
toStringMethod.invoke(instance)
rochala marked this conversation as resolved.
Show resolved Hide resolved
}
else
Some(value)

def renderTypeDef(d: Denotation)(using Context): Diagnostic =
infoDiagnostic("// defined " ++ d.symbol.showUser, d)
Expand Down Expand Up @@ -171,9 +190,8 @@ private[repl] class Rendering(parentClassLoader: Option[ClassLoader] = None) {
private def infoDiagnostic(msg: String, d: Denotation)(using Context): Diagnostic =
new Diagnostic.Info(msg, d.symbol.sourcePos)

}

object Rendering {
object Rendering:
final val REPL_WRAPPER_NAME_PREFIX = str.REPL_SESSION_LINE

extension (s: Symbol)
Expand All @@ -182,5 +200,3 @@ object Rendering {
val text = printer.dclText(s)
text.mkString(ctx.settings.pageWidth.value, ctx.settings.printLines.value)
}

}
116 changes: 116 additions & 0 deletions compiler/test-resources/repl/i15493
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
scala> class NInt(val x: Int) extends AnyVal
// defined class NInt

scala> NInt(23)
val res0: NInt = NInt@17

scala> res0.toString
val res1: String = NInt@17

scala> 23
val res2: Int = 23

scala> class NBoolean(val x: Boolean) extends AnyVal
// defined class NBoolean

scala> NBoolean(true)
val res3: NBoolean = NBoolean@4cf

scala> res3.toString
val res4: String = NBoolean@4cf

scala> true
val res5: Boolean = true

scala> class NByte(val x: Byte) extends AnyVal
// defined class NByte

scala> NByte(1)
val res6: NByte = NByte@1

scala> res6.toString
val res7: String = NByte@1

scala> val res8: Byte = 1
val res8: Byte = 1

scala> class NShort(val x: Short) extends AnyVal
// defined class NShort

scala> NShort(1)
val res9: NShort = NShort@1

scala> res9.toString
val res10: String = NShort@1

scala> val res11: Short = 1
val res11: Short = 1

scala> class NLong(val x: Long) extends AnyVal
// defined class NLong

scala> NLong(1)
val res12: NLong = NLong@1

scala> res12.toString
val res13: String = NLong@1

scala> 1L
val res14: Long = 1

scala> class NFloat(val x: Float) extends AnyVal
// defined class NFloat

scala> NFloat(1L)
val res15: NFloat = NFloat@3f800000

scala> res15.toString
val res16: String = NFloat@3f800000

scala> 1.0F
val res17: Float = 1.0

scala> class NDouble(val x: Double) extends AnyVal
// defined class NDouble

scala> NDouble(1D)
val res18: NDouble = NDouble@3ff00000

scala> res18.toString
val res19: String = NDouble@3ff00000

scala> 1.0D
val res20: Double = 1.0

scala> class NChar(val x: Char) extends AnyVal
// defined class NChar

scala> NChar('a')
val res21: NChar = NChar@61

scala> res21.toString
val res22: String = NChar@61

scala> 'a'
val res23: Char = a

scala> class NString(val x: String) extends AnyVal
// defined class NString

scala> NString("test")
val res24: NString = NString@364492

scala> res24.toString
val res25: String = NString@364492

scala> "test"
val res26: String = test

scala> class CustomToString(val x: Int) extends AnyVal { override def toString(): String = s"Test$x" }
// defined class CustomToString

scala> CustomToString(23)
val res27: CustomToString = Test23

scala> res27.toString
val res28: String = Test23