-
Notifications
You must be signed in to change notification settings - Fork 651
[debug] Add circt_debug_* intrinsics for Chisel type metadata #5276
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?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 | ||
| } |
| 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 | ||
| } | ||
| } | ||
| 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Is this not the same as the macro-derived serializer?
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No -- // 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.") | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
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 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.
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.
Agreed -- the multi-ctor case here are exactly the kind of thing typeclass derivation would solve cleanly