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 {
Comment on lines +7 to +18
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think for Scala 3 we'll eventually want to do this with static reflection (aka macros) rather than runtime reflection which, as you note, can't get the information needed.

Future work.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Agreed -- the multi-ctor case here are exactly the kind of thing typeclass derivation would solve cleanly

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
}
}
167 changes: 167 additions & 0 deletions core/src/main/scala/chisel3/debug/DebugMeta.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
// 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 {
// Manual writer keeps `value` flat (`null` / `v`); `json.macroRW` would array-wrap it (`[]` / `[v]`).
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)
)
)
Comment on lines +24 to +39
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
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)
)
)
implicit val rw: json.ReadWriter[ClassParam] = json.macroRW

Is this not the same as the macro-derived serializer?

Copy link
Copy Markdown
Author

@fkhaidari fkhaidari May 8, 2026

Choose a reason for hiding this comment

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

No -- macroRW array-wraps Option[T], which is okay in general, but feels less semantically clean here: downstream consumers would have to unwrap a one-element array to get the value. The manual bimap produces a flatter value

// macroRW
{"name":"x","typeName":"String","value":["hi"]}
{"name":"y","typeName":"Long","value":[42]}

// manual bimap (this PR)
{"name":"x","typeName":"String","value":"hi"}
{"name":"y","typeName":"Long","value":42}

}

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