Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
20 changes: 20 additions & 0 deletions core/src/main/scala-2/chisel3/debug/CtorParamsPlatform.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// SPDX-License-Identifier: Apache-2.0

package chisel3.debug

import scala.reflect.runtime.universe._

private[debug] object CtorParamsPlatform {

def ctorParams(obj: Any): Seq[(String, String)] = {
val ctor = typeOfInstance(obj).typeSymbol.asClass.primaryConstructor
if (ctor == NoSymbol) Seq.empty
else
ctor.asMethod.paramLists.flatten
.filter(!_.name.toString.contains("$outer"))
.map(a => (a.name.toString.trim, a.info.typeSymbol.name.decodedName.toString.trim))
}

private def typeOfInstance(obj: Any): Type =
runtimeMirror(obj.getClass.getClassLoader).classSymbol(obj.getClass).toType
}
40 changes: 40 additions & 0 deletions core/src/main/scala-3/chisel3/debug/CtorParamsPlatform.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// SPDX-License-Identifier: Apache-2.0

package chisel3.debug

import logger.LazyLogging

// Scala 3 has no runtime mirror to pick a primary out of multiple ctors,
// so emit params only for single-ctor classes.
private[debug] object CtorParamsPlatform extends LazyLogging {

def ctorParams(obj: Any): Seq[(String, String)] = {
val cls = obj.getClass
val ctors = cls.getDeclaredConstructors
if (ctors.length != 1) {
if (ctors.length > 1)
logger.warn(s"ctorParams: ${cls.getName} has ${ctors.length} constructors; omitting `params`")
Seq.empty
} else {
val rawParams = ctors.head.getParameters.toSeq.filter(!_.getName.contains("$outer"))
val namesSynthetic = rawParams.nonEmpty && rawParams.forall(!_.isNamePresent)
val params = rawParams.map(p => (p.getName, simpleTypeName(p.getParameterizedType.getTypeName)))
if (namesSynthetic)
logger.warn(
s"ctorParams: ${cls.getName} has only synthetic parameter names " +
s"(${params.map(_._1).mkString(", ")}); emitted debug metadata will be of limited use. " +
s"Compile user code with a flag that retains parameter names " +
s"(e.g. javac `-parameters`) to recover real names."
)
params
}
}

private def simpleTypeName(raw: String): String = {
val noGeneric = raw.takeWhile(_ != '<')
val afterDot = noGeneric.substring(noGeneric.lastIndexOf('.') + 1)
val lastDollar = afterDot.lastIndexOf('$')
val name = if (lastDollar >= 0) afterDot.substring(lastDollar + 1) else afterDot
name.capitalize
}
}
166 changes: 166 additions & 0 deletions core/src/main/scala/chisel3/debug/DebugMeta.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
// SPDX-License-Identifier: Apache-2.0

package chisel3.debug

import logger.LazyLogging

import chisel3._

import upickle.{default => json}

import scala.collection.mutable
import scala.language.existentials
import scala.util.Try
import scala.util.control.NonFatal

private[debug] case class ClassParam(
name: String,
typeName: String,
value: Option[ujson.Value] = None
)

private[debug] object ClassParam {
implicit val rw: json.ReadWriter[ClassParam] = json
.readwriter[ujson.Value]
.bimap(
p =>
ujson.Obj(
"name" -> p.name,
"typeName" -> p.typeName,
"value" -> p.value.getOrElse(ujson.Null)
),
j =>
ClassParam(
j("name").str,
j("typeName").str,
j.obj.get("value").filterNot(_ == ujson.Null)
)
)
}

private[debug] object CtorParamExtractor {
private[debug] val MaxParamDepth = 8
private[debug] val MaxRenderedLen = 256
private[debug] val TruncatedSuffix = "...[truncated]"

private[debug] def dataToTypeName(data: Data): String = sanitize(data match {
case t: Record =>
t.topBindingOpt match {
case Some(binding) => s"${t._bindingToString(binding)}[${t.className}]"
case None => t.className
}
case t => t.toString.split(" ").last
})

private def sanitize(s: String): String =
s.replaceAll("[\\p{Cntrl}\"\\\\]", "")

private[debug] def getCtorParams(target: Any): Seq[ClassParam] =
new CtorParamExtractor().getCtorParams(target)
}

private[debug] final class CtorParamExtractor extends LazyLogging {
import CtorParamExtractor.{dataToTypeName, MaxParamDepth, MaxRenderedLen, TruncatedSuffix}

private case class ClassDescriptor(
params: Seq[(String, String)],
accessors: Map[String, java.lang.reflect.Method]
)

private val descriptorCache = mutable.HashMap.empty[Class[_], ClassDescriptor]
// Identity-keyed; depth bounded by MaxParamDepth so linear scan beats hashing.
// Reset at every getCtorParams entry; safe because elaboration is single-threaded.
private val visited = mutable.ArrayBuffer.empty[AnyRef]

private def descriptor(target: Any): ClassDescriptor = {
val cls = target.getClass
descriptorCache.getOrElseUpdate(cls, buildDescriptor(target, cls))
}

private def buildDescriptor(target: Any, cls: Class[_]): ClassDescriptor = {
val params =
try CtorParamsPlatform.ctorParams(target)
catch {
case NonFatal(e) =>
logger.debug(s"ctorParams failed on ${cls.getName}: ${e.getMessage}")
Seq.empty[(String, String)]
}
val accessors = params.iterator.flatMap { case (name, _) =>
try {
val m = cls.getDeclaredMethod(name)
m.setAccessible(true)
Some(name -> m)
} catch { case NonFatal(_) => None }
}.toMap
ClassDescriptor(params, accessors)
}

private[debug] def getCtorParams(target: Any): Seq[ClassParam] = {
visited.clear()
target match { case ref: AnyRef => visited += ref; case _ => }
getCtorParamsImpl(target, 0)
}

private def getCtorParamsImpl(target: Any, depth: Int): Seq[ClassParam] = {
val d = descriptor(target)
d.params.map { case (name, typeName) =>
ClassParam(name, typeName, paramValue(target, d, name, typeName, depth))
}
}

private def paramValue(
obj: Any,
desc: ClassDescriptor,
name: String,
typeName: String,
depth: Int
): Option[ujson.Value] =
desc.accessors.get(name).flatMap { method =>
Try(method.invoke(obj.asInstanceOf[AnyRef])).fold(
e => { logger.debug(s"paramValue: cannot reflect $name: ${e.getMessage}"); None },
v => Some(renderValue(v, typeName, depth))
)
}

private def renderValue(v: Any, typeName: String, depth: Int): ujson.Value = v match {
case s: scala.collection.Seq[_] if s.exists(_.isInstanceOf[Data]) =>
ujson.Str(s.collect { case d: Data => dataToTypeName(d) }.mkString("[", ", ", "]"))
case d: Data => ujson.Str(dataToTypeName(d))
case b: Boolean => ujson.Bool(b)
case _: Byte | _: Short | _: Int | _: Long | _: Float | _: Double => ujson.Str(v.toString)
case null => ujson.Str("null")
case ref: AnyRef =>
if (depth >= MaxParamDepth || visited.exists(_ eq ref) || isOpaqueStdlibClass(ref.getClass))
ujson.Str(capped(ref.toString))
else {
visited += ref
val nested =
try getCtorParamsImpl(ref, depth + 1)
finally visited.dropRightInPlace(1)
if (nested.exists(_.value.isDefined))
ujson.Str(
capped(
s"$typeName(${nested.map(p => p.value.fold(p.name)(vv => s"${p.name}: ${renderJson(vv)}")).mkString(", ")})"
)
)
else ujson.Str(capped(ref.toString))
}
case other => ujson.Str(capped(other.toString))
}

private def renderJson(v: ujson.Value): String = v match {
case ujson.Str(s) => s
case ujson.Bool(b) => b.toString
case ujson.Num(n) => if (n.isWhole && !n.isInfinity) n.toLong.toString else n.toString
case other => other.toString
}

private def capped(s: String): String =
if (s.length <= MaxRenderedLen) s else s.substring(0, MaxRenderedLen) + TruncatedSuffix

private def isOpaqueStdlibClass(cls: Class[_]): Boolean = {
val name = cls.getName
name.startsWith("java.") || name.startsWith("javax.") ||
name.startsWith("sun.") || name.startsWith("scala.")
}
}
Loading