diff --git a/core/src/main/scala-2/chisel3/debug/CtorParamsPlatform.scala b/core/src/main/scala-2/chisel3/debug/CtorParamsPlatform.scala new file mode 100644 index 00000000000..6af77e8057f --- /dev/null +++ b/core/src/main/scala-2/chisel3/debug/CtorParamsPlatform.scala @@ -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 +} diff --git a/core/src/main/scala-3/chisel3/debug/CtorParamsPlatform.scala b/core/src/main/scala-3/chisel3/debug/CtorParamsPlatform.scala new file mode 100644 index 00000000000..4727dff77e9 --- /dev/null +++ b/core/src/main/scala-3/chisel3/debug/CtorParamsPlatform.scala @@ -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 + } +} diff --git a/core/src/main/scala/chisel3/debug/DebugMeta.scala b/core/src/main/scala/chisel3/debug/DebugMeta.scala new file mode 100644 index 00000000000..27a8b541f9b --- /dev/null +++ b/core/src/main/scala/chisel3/debug/DebugMeta.scala @@ -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.") + } +} diff --git a/core/src/main/scala/chisel3/debug/DebugMetaEmitter.scala b/core/src/main/scala/chisel3/debug/DebugMetaEmitter.scala new file mode 100644 index 00000000000..86007867575 --- /dev/null +++ b/core/src/main/scala/chisel3/debug/DebugMetaEmitter.scala @@ -0,0 +1,197 @@ +// SPDX-License-Identifier: Apache-2.0 +package chisel3.debug + +import logger.LazyLogging + +import chisel3._ +import chisel3.internal._ +import chisel3.internal.binding._ +import chisel3.internal.firrtl.ir._ +import chisel3.debug.CtorParamExtractor.dataToTypeName +import chisel3.experimental.{BaseModule, SourceInfo} + +import scala.collection.mutable +import upickle.{default => json} + +/** Injects `circt_debug_*` secret-command intrinsics into an elaborated + * [[Circuit]]. Driven by [[chisel3.stage.phases.AddDebugIntrinsics]]. + */ +private[chisel3] object DebugIntrinsics { + + def generate(circuit: Circuit): Unit = { + val emitter = new ComponentDebugEmitter + circuit.components.foreach(emitter.generate) + } + + private class ComponentDebugEmitter extends LazyLogging { + private val emittedEnums = mutable.HashSet.empty[String] + private val emittedIds = mutable.HashSet.empty[HasId] + + private val ctorExtractor = new CtorParamExtractor + + def generate(component: Component): Unit = component match { + case ctx @ DefModule(id, _, _, _, ports, block) => + processModule(id, ports ++ ctx.secretPorts, block) + case ctx @ DefClass(id, _, ports, block) => + processModule(id, ports ++ ctx.secretPorts, block) + case _: DefBlackBox => () + case _: DefIntrinsicModule => () + case ctx => + throw new InternalErrorException(s"generate: unknown Component type: $ctx") + } + + private def processModule(id: BaseModule, allPorts: Seq[Port], block: Block): Unit = { + emittedIds.clear() + createIntrinsic(id, id._getSourceLocator).foreach(block.addSecretCommand) + allPorts.foreach { p => createIntrinsic(p.id, None, p.sourceInfo).foreach(block.addSecretCommand) } + processBlock(block) + } + + // `When` is special-cased so that we read `elseRegion` only via `hasElse`; + // the getter would otherwise materialise an empty Block on first call. + private def processBlock(block: Block): Unit = block.getCommands().foreach { c => + generate(c).foreach(block.addSecretCommand) + c match { + case w: When => + processBlock(w.ifRegion) + if (w.hasElse) processBlock(w.elseRegion) + case lb: LayerBlock => processBlock(lb.region) + case dc: DefContract => processBlock(dc.region) + case _ => () + } + } + + private def generate(cmd: Command): Seq[Command] = cmd match { + case e: DefPrim[_] => createIntrinsic(e.id, None, e.sourceInfo) + case DefWire(si, id) => createIntrinsic(id, None, si) + case DefReg(si, id, _) => createIntrinsic(id, None, si) + case DefRegInit(si, id, _, _, _) => createIntrinsic(id, None, si) + case DefMemory(si, id, t, size) => createIntrinsicMem(id, t, size, si) + case DefSeqMemory(si, id, t, size, _) => createIntrinsicMem(id, t, size, si) + case FirrtlMemory(si, id, t, size, _, _, _, _, _) => createIntrinsicMem(id, t, size, si) + case _ => Seq.empty + } + + private def hasOpaqueCtor(target: Any): Boolean = target match { + case _: chisel3.Bits | _: chisel3.Clock | _: chisel3.Reset => true + case _: chisel3.experimental.Analog => true + case _: chisel3.Vec[_] | _: chisel3.EnumType => true + case _ => target.getClass.getName == "chisel3.util.MixedVec" + } + + private def extractParams(target: Any): Seq[ClassParam] = + if (hasOpaqueCtor(target)) Nil else ctorExtractor.getCtorParams(target) + + private def paramsAttr(params: Seq[ClassParam]): Seq[(String, Param)] = + if (params.isEmpty) Nil else Seq("params" -> StringParam(json.write(params))) + + private def createIntrinsicMem(target: HasId, innerType: Data, size: BigInt, si: SourceInfo): Seq[Command] = { + val typeName = s"${target.getClass.getSimpleName}[${dataToTypeName(innerType)}[$size]]" + val name = target.getOptionRef + .map(_.localName) + .getOrElse(target match { + case m: MemBase[_] => m.instanceName + case _ => "" + }) + if (name.isEmpty) { + logger.warn(s"createIntrinsicMem: skipping memory with empty name: $target") + return Seq.empty + } + Seq( + DefIntrinsic( + si, + "circt_debug_var", + Nil, + Seq("typeName" -> StringParam(typeName), "name" -> StringParam(name)) ++ paramsAttr(extractParams(target)) + ) + ) + } + + private def createIntrinsic(target: Data, parent: Option[String], si: SourceInfo): Seq[Command] = { + if (!emittedIds.add(target)) return Seq.empty + val typeName = dataToTypeName(target) + val subCmds: Seq[Command] = target match { + case e: EnumType => createEnumDefIntrinsic(e, si).toSeq + case record: Record => + val pn = Some(signalRef(target)) + record.elements.values.flatMap(createIntrinsic(_, pn, si)).toSeq + case vecLike: VecLike[_] => + val pn = Some(signalRef(target)) + vecLike.toSeq.flatMap(e => createIntrinsic(e.asInstanceOf[Data], pn, si)) + case _ => Nil + } + subCmds ++ createDebugIntrinsic(target, typeName, parent, extractParams(target), si).toSeq + } + + private case class EnumVariant(name: String, value: String) + private implicit val enumVariantRW: json.ReadWriter[EnumVariant] = json.macroRW + + private def enumNames(e: EnumType): (String, String) = { + val fqn = e.factory.enumTypeName.stripSuffix("$") + (fqn, fqn.split("\\.").last) + } + + private def createEnumDefIntrinsic(e: EnumType, si: SourceInfo): Option[Command] = { + val (fqn, simple) = enumNames(e) + if (!emittedEnums.add(fqn)) return None + val variants = e.factory.allWithNames.map { case (v, name) => EnumVariant(name, v.litValue.toString) } + Some( + DefIntrinsic( + si, + "circt_debug_enumdef", + Nil, + Seq( + "typeName" -> StringParam(simple), + "fqn" -> StringParam(fqn), + "variants" -> StringParam(json.write(variants)) + ) + ) + ) + } + + private def createIntrinsic(target: BaseModule, si: SourceInfo): Seq[Command] = Seq( + DefIntrinsic( + si, + "circt_debug_moduleinfo", + Nil, + Seq("typeName" -> StringParam(target.desiredName)) ++ paramsAttr(ctorExtractor.getCtorParams(target)) + ) + ) + + private def createDebugIntrinsic( + target: Data, + typeName: String, + parent: Option[String], + params: Seq[ClassParam], + si: SourceInfo + ): Option[Command] = { + val name = signalRef(target) + if (name.isEmpty) return None + // Synthetic `_`-prefixed names don't survive to final FIRRTL. + if (parent.isEmpty && name.startsWith("_")) return None + // firrtl.int.generic operands must be passive. + val ssaOperands: Seq[Arg] = + if (target.direction.isInstanceOf[ActualDirection.Bidirectional]) Nil else Seq(Node(target)) + val intrinsicName = if (parent.isDefined) "circt_debug_subfield" else "circt_debug_var" + val enumParam: Seq[(String, Param)] = target match { + case e: EnumType => + val (fqn, simple) = enumNames(e) + Seq("enumTypeName" -> StringParam(simple), "enumFqn" -> StringParam(fqn)) + case _ => Nil + } + val parentParam = parent.map("parent" -> StringParam(_)).toSeq + Some( + DefIntrinsic( + si, + intrinsicName, + ssaOperands, + Seq("typeName" -> StringParam(typeName), "name" -> StringParam(name)) + ++ parentParam ++ enumParam ++ paramsAttr(params) + ) + ) + } + } + + private def signalRef(d: Data): String = + d.getOptionRef.map(_.localName).getOrElse("") +} diff --git a/core/src/main/scala/chisel3/debug/EmitDebugIntrinsicsAnnotation.scala b/core/src/main/scala/chisel3/debug/EmitDebugIntrinsicsAnnotation.scala new file mode 100644 index 00000000000..cf41a4585bc --- /dev/null +++ b/core/src/main/scala/chisel3/debug/EmitDebugIntrinsicsAnnotation.scala @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: Apache-2.0 + +package chisel3.debug + +import firrtl.seqToAnnoSeq +import firrtl.annotations.NoTargetAnnotation +import firrtl.options.{HasShellOptions, ShellOption, Unserializable} + +/** Opt-in toggle for [[chisel3.stage.phases.AddDebugIntrinsics]]. + * Activate with `--with-debug-intrinsics` or by adding this annotation. + */ +case object EmitDebugIntrinsicsAnnotation extends NoTargetAnnotation with Unserializable with HasShellOptions { + + override val options: Seq[ShellOption[Unit]] = Seq( + new ShellOption[Unit]( + longOption = "with-debug-intrinsics", + toAnnotationSeq = _ => Seq(EmitDebugIntrinsicsAnnotation), + helpText = "Emit circt_debug_* intrinsics carrying Chisel type metadata", + helpValueName = None + ) + ) +} diff --git a/docs/src/explanations/intrinsics.md b/docs/src/explanations/intrinsics.md index 8b59aa08d51..9c85ba7fc6f 100644 --- a/docs/src/explanations/intrinsics.md +++ b/docs/src/explanations/intrinsics.md @@ -36,3 +36,82 @@ class Foo extends RawModule { val myresult = IntrinsicExpr("MyIntrinsic", UInt(32.W), "STRING" -> "test")(3.U, 5.U) } ``` + +## Debug type-info intrinsics + +Chisel can optionally emit a family of `circt_debug_*` intrinsics that carry +Chisel-level type and constructor-parameter metadata through to FIRRTL, so that +downstream tooling (waveform viewers, debuggers) can reconstruct Bundle, Vec +and `ChiselEnum` type names as well as module parameters that are lost during +conversion to plain FIRRTL. + +The following intrinsics are emitted: + +- `circt_debug_moduleinfo` - one per module, with `typeName` and a JSON-encoded + `params` string describing primary constructor arguments. +- `circt_debug_var` - one per top-level signal, port or memory, with `typeName` + and `name`. +- `circt_debug_subfield` - one per Bundle/Vec element, linked to its parent via + a `parent` attribute. +- `circt_debug_enumdef` - one per `ChiselEnum` type (emitted once per circuit), + listing all variants and their literal values. + +Emission is opt-in and off by default. Enable it by passing +`--with-debug-intrinsics` on the command line, or by adding +`EmitDebugIntrinsicsAnnotation` to the annotation seq programmatically: + +```scala mdoc:compile-only +import circt.stage.ChiselStage + +class MyBundle extends Bundle { + val a = UInt(8.W) + val b = Bool() +} +class MyModule(val width: Int) extends Module { + val io = IO(new Bundle { + val in = Input(new MyBundle) + val out = Output(new MyBundle) + }) + io.out := io.in +} + +// Emit CHIRRTL with circt_debug_* intrinsics interleaved. +val chirrtl = ChiselStage.emitCHIRRTL( + new MyModule(8), + args = Array("--with-debug-intrinsics") +) +``` + +Under the hood this activates the `AddDebugIntrinsics` stage phase, which walks +the elaborated circuit and appends intrinsics as secret commands - user code is +not altered. Without the annotation/flag the phase is a no-op. + +### Notes on reflection + +Constructor-parameter extraction uses Java/Scala reflection on user module and +Bundle classes. Two consequences worth knowing about: + +- **JDK 17+** restricts reflective access to non-public members. The pass calls + `setAccessible(true)` on `val`-accessor methods to read constructor argument + values. On a locked-down JVM this may throw `InaccessibleObjectException`; + the pass catches it and emits parameters without values rather than crashing + the build. To get values back, run with + `--add-opens java.base/=ALL-UNNAMED` for the affected packages, or + use `--with-debug-intrinsics` only in development builds. +- **Scala 3 ctor parameter names**: on Scala 3 the pass reads constructor + parameter names via Java reflection (`Parameter.getName`). Scala 3 does not + emit the JVM `MethodParameters` attribute by default, so `getName` returns + synthetic `arg0`, `arg1`, ... names -- the pass logs a one-shot warning per + affected class. There is currently no built-in scalac flag to fix this; the + warning is informational, and intrinsics still emit parameter types and + values, just without source-level names. Java sources can recover names by + compiling with `javac -parameters`. +- **Nested constructor parameters are extracted recursively** up to a small + depth limit. `toString` is invoked on user objects whose runtime class has + no extractable accessors; arbitrary `toString` implementations therefore + show up in the emitted metadata. The 256-byte cap bounds the *output size* + of `toString`, not its execution cost -- a slow or allocation-heavy + `toString` will slow or OOM the compile. Avoid expensive `toString`s in + classes you expect to feed into debug metadata. Cycles in the object graph + are detected via identity tracking and printed as `toString` rather than + recursed into. diff --git a/src/main/scala/chisel3/stage/phases/AddDebugIntrinsics.scala b/src/main/scala/chisel3/stage/phases/AddDebugIntrinsics.scala new file mode 100644 index 00000000000..36f864f7f9b --- /dev/null +++ b/src/main/scala/chisel3/stage/phases/AddDebugIntrinsics.scala @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: Apache-2.0 +package chisel3.stage.phases + +import chisel3.debug.{DebugIntrinsics, EmitDebugIntrinsicsAnnotation} +import chisel3.stage.ChiselCircuitAnnotation + +import firrtl.options.{Dependency, Phase} +import firrtl.{annoSeqToSeq, seqToAnnoSeq, AnnotationSeq} + +/** No-op unless [[chisel3.debug.EmitDebugIntrinsicsAnnotation]] is present. */ +class AddDebugIntrinsics extends Phase { + override def prerequisites = Seq(Dependency[Elaborate]) + override def optionalPrerequisites = Seq.empty + override def optionalPrerequisiteOf = Seq(Dependency[Convert], Dependency[AddDedupGroupAnnotations]) + override def invalidates(a: Phase) = false + + def transform(annotations: AnnotationSeq): AnnotationSeq = + if (!annotations.contains(EmitDebugIntrinsicsAnnotation)) annotations + else + // Drop the annotation after consumption so a second pass is a no-op + // (the circuit is already mutated; re-running would duplicate intrinsics). + annotations.flatMap { + case EmitDebugIntrinsicsAnnotation => Nil + case a: ChiselCircuitAnnotation => + DebugIntrinsics.generate(a.elaboratedCircuit._circuit) + Seq(a) + case a => Seq(a) + } +} diff --git a/src/main/scala/circt/stage/ChiselStage.scala b/src/main/scala/circt/stage/ChiselStage.scala index ab9309ab668..05fb6a68d3e 100644 --- a/src/main/scala/circt/stage/ChiselStage.scala +++ b/src/main/scala/circt/stage/ChiselStage.scala @@ -34,6 +34,7 @@ class ChiselStage extends Stage { Dependency[chisel3.stage.phases.AddImplicitOutputFile], Dependency[chisel3.stage.phases.AddImplicitOutputAnnotationFile], Dependency[chisel3.stage.phases.AddSerializationAnnotations], + Dependency[chisel3.stage.phases.AddDebugIntrinsics], Dependency[chisel3.stage.phases.Convert], Dependency[chisel3.stage.phases.AddDedupGroupAnnotations], Dependency[circt.stage.phases.AddImplicitOutputFile], @@ -56,6 +57,7 @@ object ChiselStage { private def phase = new PhaseManager( Seq( Dependency[chisel3.stage.phases.Elaborate], + Dependency[chisel3.stage.phases.AddDebugIntrinsics], Dependency[chisel3.stage.phases.Convert], Dependency[chisel3.stage.phases.AddDedupGroupAnnotations], Dependency[circt.stage.phases.AddImplicitOutputFile], diff --git a/src/main/scala/circt/stage/Shell.scala b/src/main/scala/circt/stage/Shell.scala index a858df1c7b0..3f3c3c3186c 100644 --- a/src/main/scala/circt/stage/Shell.scala +++ b/src/main/scala/circt/stage/Shell.scala @@ -2,6 +2,7 @@ package circt.stage +import chisel3.debug.EmitDebugIntrinsicsAnnotation import chisel3.stage.{ ChiselCircuitAnnotation, ChiselGeneratorAnnotation, @@ -56,7 +57,8 @@ trait CLI extends BareShell { this: BareShell => UseSRAMBlackbox, IncludeInlineTestsForModule, IncludeInlineTestsWithName, - SuppressSourceInfoAnnotation + SuppressSourceInfoAnnotation, + EmitDebugIntrinsicsAnnotation ).foreach(_.addOptions(parser)) parser.note("CIRCT (MLIR FIRRTL Compiler) options") diff --git a/src/test/scala/chisel3/debug/DebugCtorParamExtractorSpec.scala b/src/test/scala/chisel3/debug/DebugCtorParamExtractorSpec.scala new file mode 100644 index 00000000000..ddf81c702ab --- /dev/null +++ b/src/test/scala/chisel3/debug/DebugCtorParamExtractorSpec.scala @@ -0,0 +1,210 @@ +// SPDX-License-Identifier: Apache-2.0 +package chisel3.debug + +import org.scalatest.funspec.AnyFunSpec +import org.scalatest.matchers.should.Matchers +import chisel3._ + +// Method-local classes become `$anon$1`; staticClass lookup needs object scope. +object DebugCtorParamExtractorSpec { + class Foo(val width: Int) + class Bar(val n: Int, val label: String, val flag: Boolean) + class Baz(x: Int) + class Mixed(val a: Int, b: String) + case class MyCaseClass(n: Int, name: String) + class Inner(val x: Int) + class Outer(val inner: Inner) + class A(val v: Int) + class B(val a: A) + class C(val b: B) + class Empty + class WithProtected(protected val n: Int) + class WithPrivate(private val n: Int) + class WithData(val gen: UInt) + class WithSecondary(val a: Int, val b: Int) { + def this(a: Int, b: Int, c: Int, d: Int) = this(a, b) + } + class WithGeneric(val xs: Seq[Int]) + class WithMap(val m: Map[String, Int]) + class WithTypeParam[T](val gen: T) + class WithDoubles(val nan: Double, val pos: Double, val neg: Double, val regular: Double) + class WithFloats(val nan: Float, val pos: Float, val neg: Float) + class WithBigLong(val small: Long, val big: Long) + class SharedSibling(val left: Inner, val right: Inner) + // Non-val ctor: reflection finds no accessor -> falls back to `toString`. + class HugeToString(x: Int) { override def toString: String = "x" * 100000 } + class WithHuge(val payload: HugeToString) +} + +class DebugCtorParamExtractorSpec extends AnyFunSpec with Matchers { + import DebugCtorParamExtractorSpec._ + + private def s(v: String): Option[ujson.Value] = Some(ujson.Str(v)) + private def n(v: Any): Option[ujson.Value] = Some(ujson.Str(v.toString)) + private def b(v: Boolean): Option[ujson.Value] = Some(ujson.Bool(v)) + + describe("val parameters") { + it("extracts name, typeName and value for a single val param") { + CtorParamExtractor.getCtorParams(new Foo(8)) shouldEqual Seq( + ClassParam("width", "Int", n(8)) + ) + } + + it("extracts multiple val params of different types") { + CtorParamExtractor.getCtorParams(new Bar(4, "hello", true)) shouldEqual Seq( + ClassParam("n", "Int", n(4)), + ClassParam("label", "String", s("hello")), + ClassParam("flag", "Boolean", b(true)) + ) + } + } + + describe("non-val parameters") { + it("returns None for value when param has no val") { + CtorParamExtractor.getCtorParams(new Baz(42)) shouldEqual Seq( + ClassParam("x", "Int", None) + ) + } + + it("mixes val and non-val in same constructor") { + CtorParamExtractor.getCtorParams(new Mixed(1, "ignored")) shouldEqual Seq( + ClassParam("a", "Int", n(1)), + ClassParam("b", "String", None) + ) + } + } + + describe("case class") { + it("extracts all fields with values") { + CtorParamExtractor.getCtorParams(MyCaseClass(7, "world")) shouldEqual Seq( + ClassParam("n", "Int", n(7)), + ClassParam("name", "String", s("world")) + ) + } + } + + describe("nested class parameters") { + it("recursively serializes one level of nesting") { + CtorParamExtractor.getCtorParams(new Outer(new Inner(3))) shouldEqual Seq( + ClassParam("inner", "Inner", s("Inner(x: 3)")) + ) + } + + it("handles two levels of nesting") { + val params = CtorParamExtractor.getCtorParams(new C(new B(new A(5)))) + params.head.value shouldEqual s("B(a: A(v: 5))") + } + } + + describe("empty constructor") { + it("returns empty Seq") { + CtorParamExtractor.getCtorParams(new Empty) shouldBe empty + } + } + + describe("protected and private val parameters") { + it("extracts protected val") { + CtorParamExtractor.getCtorParams(new WithProtected(9)) shouldEqual Seq( + ClassParam("n", "Int", n(9)) + ) + } + + it("extracts private val") { + CtorParamExtractor.getCtorParams(new WithPrivate(9)) shouldEqual Seq( + ClassParam("n", "Int", n(9)) + ) + } + } + + describe("Data parameters") { + it("does not crash and returns non-empty value for unbound Data param") { + val params = CtorParamExtractor.getCtorParams(new WithData(UInt(8.W))) + (params should have).length(1) + params.head.name shouldEqual "gen" + params.head.typeName shouldEqual "UInt" + params.head.value should not be empty + } + } + + describe("type names") { + it("strips generic arguments from a Seq type") { + val params = CtorParamExtractor.getCtorParams(new WithGeneric(Seq(1, 2, 3))) + params.head.typeName shouldEqual "Seq" + } + + it("strips generic arguments from a Map type") { + val params = CtorParamExtractor.getCtorParams(new WithMap(Map("a" -> 1))) + params.head.typeName shouldEqual "Map" + } + + it("returns the type-parameter name for an unbound type ref") { + val params = CtorParamExtractor.getCtorParams(new WithTypeParam[Int](42)) + params.head.typeName shouldEqual "T" + } + } + + describe("numeric values") { + // Numbers serialize as strings: JSON has no NaN/Infinity, Double loses + // precision on Long > 2^53. + it("emits Double NaN/Infinity and finite values uniformly as strings") { + val params = CtorParamExtractor.getCtorParams( + new WithDoubles(Double.NaN, Double.PositiveInfinity, Double.NegativeInfinity, 1.5) + ) + val byName = params.map(p => p.name -> p.value).toMap + byName("nan") shouldEqual Some(ujson.Str("NaN")) + byName("pos") shouldEqual Some(ujson.Str("Infinity")) + byName("neg") shouldEqual Some(ujson.Str("-Infinity")) + byName("regular") shouldEqual Some(ujson.Str("1.5")) + } + + it("emits Float NaN/Infinity as strings") { + val params = CtorParamExtractor.getCtorParams( + new WithFloats(Float.NaN, Float.PositiveInfinity, Float.NegativeInfinity) + ) + val byName = params.map(p => p.name -> p.value).toMap + byName("nan") shouldEqual Some(ujson.Str("NaN")) + byName("pos") shouldEqual Some(ujson.Str("Infinity")) + byName("neg") shouldEqual Some(ujson.Str("-Infinity")) + } + + it("emits Long values as strings preserving exact representation") { + val big = (1L << 53) + 1L + val params = CtorParamExtractor.getCtorParams(new WithBigLong(42L, big)) + val byName = params.map(p => p.name -> p.value).toMap + byName("small") shouldEqual Some(ujson.Str("42")) + byName("big") shouldEqual Some(ujson.Str(big.toString)) + } + } + + describe("shared sibling object (not a cycle)") { + // `visited` is path-scoped: a shared sibling must recurse on both visits. + it("recurses into both occurrences of a shared object") { + val cfg = new Inner(7) + val params = CtorParamExtractor.getCtorParams(new SharedSibling(cfg, cfg)) + params.map(p => p.name -> p.value) shouldEqual Seq( + "left" -> s("Inner(x: 7)"), + "right" -> s("Inner(x: 7)") + ) + } + } + + describe("oversized toString") { + it("truncates pathologically long toString output") { + val params = CtorParamExtractor.getCtorParams(new WithHuge(new HugeToString(1))) + val rendered = params.head.value.collect { case ujson.Str(v) => v }.getOrElse("") + rendered.length should be <= CtorParamExtractor.MaxRenderedLen + CtorParamExtractor.TruncatedSuffix.length + rendered should endWith(CtorParamExtractor.TruncatedSuffix) + } + } + + describe("primary vs. secondary constructors") { + // Scala 2 yields ("a","b"); Scala 3 returns empty. Neither must leak secondary params. + it("never reports parameter names that don't correspond to instance accessors") { + val params = CtorParamExtractor.getCtorParams(new WithSecondary(1, 2)) + val names = params.map(_.name) + names should (equal(Seq("a", "b")).or(be(empty))) + names should not contain "c" + names should not contain "d" + } + } +} diff --git a/src/test/scala/chisel3/debug/DebugDataTypesSpec.scala b/src/test/scala/chisel3/debug/DebugDataTypesSpec.scala new file mode 100644 index 00000000000..2b7f1d7adab --- /dev/null +++ b/src/test/scala/chisel3/debug/DebugDataTypesSpec.scala @@ -0,0 +1,178 @@ +// SPDX-License-Identifier: Apache-2.0 +package chisel3.debug + +import chisel3._ +import org.scalatest.funspec.AnyFunSpec +import org.scalatest.matchers.should.Matchers + +class DebugDataTypesSpec extends AnyFunSpec with Matchers { + + import DebugTestCircuits.DataTypesCircuits._ + import DebugTestCircuits.{BindingChoice, PortBinding, RegBinding, WireBinding} + import DebugTestUtils._ + + // By-name `chirrtl` so a shared `lazy val` only forces inside `it`. + private def emitAndCheck(label: String, gen: => RawModule)(patterns: Seq[String]): Unit = + it(label)(checkIntrinsics(patterns.map(_ -> 1), emit(gen))) + + private def checkAt(chirrtl: => String, label: String)(patterns: Seq[String]): Unit = + it(label)(checkIntrinsics(patterns.map(_ -> 1), chirrtl)) + + def wrap(b: BindingChoice, t: String): String = b match { + case PortBinding => s"IO[$t]" + case WireBinding => s"Wire[$t]" + case RegBinding => s"Reg[$t]" + } + + def clockResetFor(b: BindingChoice): Seq[String] = + if (b == RegBinding) Seq(varPattern("IO[Clock]", "clock"), varPattern("IO[Reset]", "reset")) + else Seq.empty + + def typeTests(b: BindingChoice): Unit = { + def vp(typ: String, name: String, et: Option[String] = None) = varPattern(wrap(b, typ), name, et) + def sp(typ: String, name: String, parent: String, et: Option[String] = None) = + subfieldPattern(wrap(b, typ), name, parent, et) + + emitAndCheck(s"[$b] should annotate ground types", new TopCircuitGroundTypes(b))( + Seq(vp("UInt<8>", "uint"), vp("SInt<8>", "sint"), vp("Bool", "bool"), vp("UInt<8>", "bits")) + ++ clockResetFor(b) + ++ (if (b != RegBinding) Seq(vp("Analog<1>", "analog")) else Nil) + ) + + emitAndCheck(s"[$b] should annotate bundles", new TopCircuitBundles(b))( + Seq( + vp("AnonymousBundle", "a"), + vp("MyEmptyBundle", "bnd"), + vp("MyBundle", "c"), + sp("UInt<8>", "c.a", "c"), + sp("SInt<8>", "c.b", "c"), + sp("Bool", "c.c", "c") + ) ++ clockResetFor(b) + ) + + emitAndCheck(s"[$b] should annotate nested bundles", new TopCircuitBundlesNested(b))( + Seq( + vp("MyNestedBundle", "a"), + sp("Bool", "a.a", "a"), + sp("MyBundle", "a.b", "a"), + sp("UInt<8>", "a.b.a", "a.b"), + sp("SInt<8>", "a.b.b", "a.b"), + sp("Bool", "a.b.c", "a.b"), + sp("MyBundle", "a.c", "a"), + sp("UInt<8>", "a.c.a", "a.c"), + sp("SInt<8>", "a.c.b", "a.c"), + sp("Bool", "a.c.c", "a.c") + ) ++ clockResetFor(b) + ) + + emitAndCheck(s"[$b] should annotate vecs", new TopCircuitVecs(b))( + Seq( + vp("SInt<23>[5]", "a"), + sp("SInt<23>", "a[0]", "a"), + vp("SInt<23>[3][5]", "bv"), + sp("SInt<23>[3]", "bv[0]", "bv"), + sp("SInt<23>", "bv[0][0]", "bv[0]"), + vp("AnonymousBundle[5]", "c"), + sp("AnonymousBundle", "c[0]", "c"), + sp("UInt<8>", "c[0].x", "c[0]"), + vp("MixedVec", "d"), + sp("UInt<3>", "d.0", "d"), + sp("SInt<10>", "d.1", "d") + ) ++ clockResetFor(b) + ) + + emitAndCheck(s"[$b] should annotate bundle with vec", new TopCircuitBundleWithVec(b))( + Seq( + vp("AnonymousBundle", "a"), + sp("UInt<8>[5]", "a.vec", "a"), + sp("UInt<8>", "a.vec[0]", "a.vec") + ) ++ clockResetFor(b) + ) + } + + describe("Clock and Reset annotations") { + emitAndCheck("should annotate explicit clock and reset types", new TopCircuitClockReset)( + Seq( + varPattern("IO[Clock]", "clock"), + varPattern("IO[Bool]", "syncReset"), + varPattern("IO[Reset]", "reset"), + varPattern("IO[AsyncReset]", "asyncReset") + ) + ) + emitAndCheck("should annotate implicit clock and reset", new TopCircuitImplicitClockReset)( + Seq(varPattern("IO[Clock]", "clock"), varPattern("IO[Bool]", "reset")) + ) + } + + describe("Port (IO) annotations") { typeTests(PortBinding) } + describe("Wire annotations") { typeTests(WireBinding) } + describe("Reg annotations") { typeTests(RegBinding) } + + describe("ChiselEnum annotations") { + lazy val chirrtl = emit(new TopCircuitEnumSimple) + val DTE = Some("DebugTestEnum") + val DTE2 = Some("DebugTestEnum2") + + checkAt(chirrtl, "should emit enumdef once per enum type")( + Seq(enumDefPattern("DebugTestEnum"), enumDefPattern("DebugTestEnum2")) + ) + checkAt(chirrtl, "should annotate enum port with enumTypeName")( + Seq(varPattern("IO[DebugTestEnum]", "e", DTE), varPattern("IO[DebugTestEnum2]", "e2", DTE2)) + ) + checkAt(chirrtl, "should annotate enum subfield with enumTypeName")( + Seq( + subfieldPattern("IO[DebugTestEnum]", "bnd.en", "bnd", DTE), + subfieldPattern("IO[DebugTestEnum2]", "bnd.en2", "bnd", DTE2), + subfieldPattern("IO[UInt<8>]", "bnd.x", "bnd", None) + ) + ) + checkAt(chirrtl, "should annotate enum in vec with enumTypeName")( + Seq( + varPattern("IO[DebugTestEnum[3]]", "v"), + subfieldPattern("IO[DebugTestEnum]", "v[0]", "v", DTE) + ) + ) + } + + describe("Memory annotations") { + import DebugTestCircuits.MemCircuits._ + + val memVar = """intrinsic\(circt_debug_var<[^)]*name\s*=\s*"mem"""" + + it("should annotate Mem as var without parent") { + val chirrtl = emit(new TopCircuitMem(UInt(8.W))) + countOccurrences(chirrtl, memVar) should be(1) + countOccurrences(chirrtl, """circt_debug_subfield""") should be(0) + } + + it("should annotate SyncReadMem as var without parent") { + val chirrtl = emit(new TopCircuitSyncMem(UInt(8.W))) + countOccurrences(chirrtl, memVar) should be(1) + } + } + + describe("MixedVec opaque-ctor handling") { + // Per-element subfield emission already covers MixedVec's `Seq[Data]` ctor arg. + it("does not attach a `params=` attribute to the MixedVec circt_debug_var") { + import DebugTestCircuits.DataTypesCircuits._ + val chirrtl = emit(new TopCircuitVecs(DebugTestCircuits.PortBinding)) + val mixedVecVar = """intrinsic\(circt_debug_var<[^>]*name\s*=\s*"d"[^>]*>""".r + val matched = mixedVecVar + .findFirstIn(chirrtl) + .getOrElse( + fail("no circt_debug_var for MixedVec port `d`") + ) + (matched should not).include("params") + } + } + + describe("Tmp values in when/else") { + emitAndCheck("should annotate named vals inside when blocks", new TopCircuitWhenElse)( + Seq( + varPattern("OpResult[UInt<8>]", "evenSel"), + varPattern("OpResult[UInt<8>]", "selIsOne"), + varPattern("OpResult[UInt<8>]", "oddSel") + ) + ) + } +} diff --git a/src/test/scala/chisel3/debug/DebugIntrinsicsSpec.scala b/src/test/scala/chisel3/debug/DebugIntrinsicsSpec.scala new file mode 100644 index 00000000000..16329f19dd7 --- /dev/null +++ b/src/test/scala/chisel3/debug/DebugIntrinsicsSpec.scala @@ -0,0 +1,184 @@ +// SPDX-License-Identifier: Apache-2.0 +package chisel3.debug + +import chisel3._ +import chisel3.experimental.IntrinsicModule +import chisel3.experimental.hierarchy.Definition +import chisel3.internal.firrtl.ir +import chisel3.properties.{Class => PropertiesClass} +import chisel3.stage.{ChiselCircuitAnnotation, ChiselGeneratorAnnotation} +import chisel3.stage.phases.{AddDebugIntrinsics, Elaborate} +import circt.stage.ChiselStage +import firrtl.{annoSeqToSeq, seqToAnnoSeq, AnnotationSeq} +import org.scalatest.funspec.AnyFunSpec +import org.scalatest.matchers.should.Matchers + +private class DebugSimpleModule extends Module { + val in = IO(Input(UInt(8.W))) + val out = IO(Output(UInt(8.W))) + out := in +} + +private class DebugParamModule(val width: Int, val hasReset: Boolean) extends Module { + override def desiredName = s"DebugParamModule_w${width}_r$hasReset" + val in = IO(Input(UInt(width.W))) + val out = IO(Output(UInt(width.W))) + if (hasReset) { val reg = RegInit(0.U(width.W)); reg := in; out := reg } + else out := in +} + +private class DebugSubModule extends Module { + val a = IO(Input(UInt(4.W))) + val b = IO(Output(UInt(4.W))) + b := a + 1.U +} + +private class DebugTopModule extends Module { + val in = IO(Input(UInt(4.W))) + val out = IO(Output(UInt(4.W))) + val sub = Module(new DebugSubModule) + sub.a := in + out := sub.b +} + +private class DebugParamBundle(val n: Int) extends Bundle { + val data = UInt(n.W) +} + +private class DebugBundleModule(val width: Int) extends Module { + val io = IO(new DebugParamBundle(width)) +} + +private class DebugMyIntrinsicMod extends IntrinsicModule("my_test_intr") { + val o = IO(Output(Bool())) +} + +private class DebugWithIntrinsic extends RawModule { + val intr = Module(new DebugMyIntrinsicMod) +} + +private class DebugMyClass extends PropertiesClass + +private class DebugWithClass extends RawModule { + val descDef = Definition(new DebugMyClass) +} + +class DebugIntrinsicsSpec extends AnyFunSpec with Matchers { + import DebugTestUtils._ + + describe("circt_debug_moduleinfo") { + + it("emits exactly one moduleinfo per module") { + val chirrtl = emit(new DebugSimpleModule) + countOccurrences(chirrtl, moduleInfoPattern("DebugSimpleModule")) should be(1) + } + + it("emits moduleinfo with typeName for a parametrized module") { + val chirrtl = emit(new DebugParamModule(8, false)) + countOccurrences(chirrtl, moduleInfoPattern("DebugParamModule_w8_rfalse")) should be(1) + } + + it("emits moduleinfo for every module in a hierarchy") { + val chirrtl = emit(new DebugTopModule) + countOccurrences(chirrtl, moduleInfoPattern("DebugTopModule")) should be(1) + countOccurrences(chirrtl, moduleInfoPattern("DebugSubModule")) should be(1) + } + + it("emits moduleinfo with constructor params serialized in params field") { + val chirrtl = emit(new DebugParamModule(5, true)) + val name = "DebugParamModule_w5_rtrue" + countOccurrences(chirrtl, moduleInfoPattern(name)) should be(1) + val params = requireParams(chirrtl, name) + assertParam(params, "width", "Int", 5) // numbers serialize as JSON strings + assertParam(params, "hasReset", "Boolean", true) // Booleans stay as native JSON bool + } + + it("does not emit moduleinfo for blackboxes") { + import DebugTestCircuits.ModuleCircuits._ + val chirrtl = emit(new TopCircuitBlackBox) + countOccurrences(chirrtl, moduleInfoPattern("TopCircuitBlackBox")) should be(1) + countOccurrences(chirrtl, moduleInfoPattern("MyBlackBox")) should be(0) + } + + it("does not emit moduleinfo for IntrinsicModule") { + val chirrtl = emit(new DebugWithIntrinsic) + countOccurrences(chirrtl, moduleInfoPattern("DebugWithIntrinsic")) should be(1) + countOccurrences(chirrtl, moduleInfoPattern("DebugMyIntrinsicMod")) should be(0) + } + + it("emits moduleinfo for chisel3.properties.Class (DefClass)") { + val chirrtl = emit(new DebugWithClass) + countOccurrences(chirrtl, moduleInfoPattern("DebugWithClass")) should be(1) + countOccurrences(chirrtl, moduleInfoPattern("DebugMyClass")) should be(1) + } + + it("emits moduleinfo with params for a module whose port is a parametrized bundle") { + val chirrtl = emit(new DebugBundleModule(16)) + countOccurrences(chirrtl, moduleInfoPattern("DebugBundleModule")) should be(1) + val params = requireParams(chirrtl, "DebugBundleModule") + assertParam(params, "width", "Int", 16) + } + } + + describe("EmitDebugIntrinsicsAnnotation toggle") { + it("emits no circt_debug_* intrinsics by default") { + val chirrtl = ChiselStage.emitCHIRRTL(new DebugSimpleModule) + (chirrtl should not).include("circt_debug_") + } + + it("emits circt_debug_* intrinsics with --with-debug-intrinsics") { + val chirrtl = ChiselStage.emitCHIRRTL(new DebugSimpleModule, args = Array("--with-debug-intrinsics")) + chirrtl should include("circt_debug_moduleinfo") + chirrtl should include("circt_debug_var") + } + } + + describe("AddDebugIntrinsics phase idempotency") { + // Walk the in-memory circuit (rather than re-emitting CHIRRTL) so we count + // exactly the secret commands the phase added -- this is what would double + // if the consume-the-annotation guard regressed. + def countDebugIntrinsics(circuit: ir.Circuit): Int = { + def inBlock(block: ir.Block): Int = { + val direct = (block.getCommands() ++ block.getSecretCommands()).count { + case d: ir.DefIntrinsic => d.intrinsic.startsWith("circt_debug_") + case _ => false + } + val nested = block + .getCommands() + .map { + case w: ir.When => inBlock(w.ifRegion) + (if (w.hasElse) inBlock(w.elseRegion) else 0) + case lb: ir.LayerBlock => inBlock(lb.region) + case dc: ir.DefContract => inBlock(dc.region) + case _ => 0 + } + .sum + direct + nested + } + circuit.components.map { + case dm: ir.DefModule => inBlock(dm.block) + case dc: ir.DefClass => inBlock(dc.block) + case _ => 0 + }.sum + } + + it("consumes EmitDebugIntrinsicsAnnotation so a second pass does not double-emit") { + val elaborated: AnnotationSeq = (new Elaborate) + .transform(Seq(ChiselGeneratorAnnotation(() => new DebugTopModule))) + val withFlag = elaborated :+ EmitDebugIntrinsicsAnnotation + val pass = new AddDebugIntrinsics + + val afterFirst = pass.transform(withFlag) + afterFirst.exists(_ == EmitDebugIntrinsicsAnnotation) shouldBe false + + val cca1 = afterFirst.collectFirst { case a: ChiselCircuitAnnotation => a } + .getOrElse(fail("no ChiselCircuitAnnotation after first pass")) + val countOnce = countDebugIntrinsics(cca1.elaboratedCircuit._circuit) + countOnce should be > 0 + + val afterSecond = pass.transform(afterFirst) + val cca2 = afterSecond.collectFirst { case a: ChiselCircuitAnnotation => a } + .getOrElse(fail("no ChiselCircuitAnnotation after second pass")) + countDebugIntrinsics(cca2.elaboratedCircuit._circuit) shouldEqual countOnce + } + } +} diff --git a/src/test/scala/chisel3/debug/DebugTestCircuits.scala b/src/test/scala/chisel3/debug/DebugTestCircuits.scala new file mode 100644 index 00000000000..d16e5960080 --- /dev/null +++ b/src/test/scala/chisel3/debug/DebugTestCircuits.scala @@ -0,0 +1,226 @@ +// SPDX-License-Identifier: Apache-2.0 + +package chisel3.debug + +import chisel3._ +import chisel3.experimental.{fromStringToStringParam, Analog} +import chisel3.util.MixedVec +import circt.stage.ChiselStage +import org.scalatest.matchers.should.Matchers + +object DebugTestEnum extends ChiselEnum { val EA, EB, EC = Value } +object DebugTestEnum2 extends ChiselEnum { val ED, EE, EF = Value } + +object DebugTestUtils extends Matchers { + + private def q(s: String): String = java.util.regex.Pattern.quote(s) + + def countOccurrences(chirrtl: String, pattern: String): Int = + pattern.r.findAllMatchIn(chirrtl).length + + def getMissingOccurrences(chirrtl: String, pattern: String, expectedLines: Seq[String]): String = { + val lines = chirrtl.split("\n").toSeq + val anyR = (".*" + pattern + ".*").r + val idxs = lines.indices.filter(i => anyR.findFirstIn(lines(i)).isDefined) + val withNext = + idxs.flatMap(i => if (i < lines.length - 1) List(lines(i), lines(i + 1)) else List(lines(i))).distinct + val expRe = expectedLines.map(_.r) + withNext.filterNot(line => expRe.exists(_.findFirstIn(line).isDefined)).mkString("\n") + } + + private def attrPart(name: String, value: String): String = s"""[^)]*$name\\s*=\\s*"${q(value)}"""" + private def enumPart(et: Option[String]): String = et.fold("")(attrPart("enumTypeName", _)) + + private def intrinsic(kind: String, attrs: String*): String = + s"""intrinsic\\(circt_debug_$kind<${attrs.mkString}""" + + def varPattern(typeName: String, name: String, enumTypeName: Option[String] = None): String = + intrinsic("var", attrPart("typeName", typeName), attrPart("name", name), enumPart(enumTypeName)) + + def subfieldPattern(typeName: String, name: String, parent: String, enumTypeName: Option[String] = None): String = + intrinsic( + "subfield", + attrPart("typeName", typeName), + attrPart("name", name), + attrPart("parent", parent), + enumPart(enumTypeName) + ) + + def enumDefPattern(typeName: String): String = intrinsic("enumdef", attrPart("typeName", typeName)) + def moduleInfoPattern(typeName: String): String = intrinsic("moduleinfo", attrPart("typeName", typeName)) + + // Collapse whitespace so regex helpers ignore line wrapping. + def emit(gen: => RawModule): String = + ChiselStage.emitCHIRRTL(gen, args = Array("--with-debug-intrinsics")).replaceAll("\\s+", " ") + + private val moduleInfoBodyRe = """intrinsic\(circt_debug_moduleinfo<(.*?)>\)""".r + private val paramsBodyRe = """params\s*=\s*"((?:[^"\\]|\\.)*)"""".r + + def requireParams(chirrtl: String, typeName: String): String = { + val tnPat = s"""typeName\\s*=\\s*"${q(typeName)}"""".r + moduleInfoBodyRe + .findAllMatchIn(chirrtl) + .map(_.group(1)) + .find(body => tnPat.findFirstIn(body).isDefined) + .flatMap(body => paramsBodyRe.findFirstMatchIn(body).map(_.group(1))) + .getOrElse(fail(s"no circt_debug_moduleinfo for $typeName")) + } + + def assertParam(params: String, name: String, typeName: String, value: Any): Unit = { + val bq = "\\\"" + val valueJson = value match { + case b: Boolean => b.toString + case other => bq + other.toString + bq + } + params should include(s"${bq}name$bq:$bq$name$bq") + params should include(s"${bq}typeName$bq:$bq$typeName$bq") + params should include(s"${bq}value$bq:$valueJson") + } + + def checkIntrinsics(expected: Seq[(String, Int)], chirrtl: String): Unit = { + val names = expected.map(_._1) + expected.foreach { case (pattern, count) => + withClue(s"Pattern '$pattern':\n${getMissingOccurrences(chirrtl, pattern, names)}\n") { + countOccurrences(chirrtl, pattern) should be(count) + } + } + } +} + +object DebugTestCircuits { + + sealed abstract class BindingChoice(label: String) { + def apply[T <: Data](data: T): T + override def toString = label + } + case object PortBinding extends BindingChoice("IO") { def apply[T <: Data](d: T): T = IO(d) } + case object WireBinding extends BindingChoice("Wire") { def apply[T <: Data](d: T): T = Wire(d) } + case object RegBinding extends BindingChoice("Reg") { def apply[T <: Data](d: T): T = Reg(d) } + + abstract class DebugTestModule(bindingChoice: BindingChoice) extends RawModule { + def body: Unit + bindingChoice match { + case RegBinding => + val clock = IO(Input(Clock())) + val reset = IO(Input(Reset())) + withClockAndReset(clock, reset) { body } + case _ => body + } + } + + object ModuleCircuits { + + class TopCircuitBlackBox extends RawModule { + class MyBlackBox extends ExtModule(Map("PARAM1" -> "TRUE", "PARAM2" -> "DEFAULT")) { + val io = IO(new Bundle {}) + } + val myBlackBox1: MyBlackBox = Module(new MyBlackBox) + val myBlackBox2: MyBlackBox = Module(new MyBlackBox) + val myBlackBoxes: Seq[MyBlackBox] = Seq.fill(2)(Module(new MyBlackBox)) + } + } + + object DataTypesCircuits { + + class TopCircuitClockReset extends RawModule { + val clock: Clock = IO(Input(Clock())) + val syncReset: Bool = IO(Input(Bool())) + val reset: Reset = IO(Input(Reset())) + val asyncReset: AsyncReset = IO(Input(AsyncReset())) + } + + class TopCircuitImplicitClockReset extends Module + + class TopCircuitGroundTypes(b: BindingChoice) extends DebugTestModule(b) { + override def body: Unit = { + val uint: UInt = b(UInt(8.W)) + val sint: SInt = b(SInt(8.W)) + val bool: Bool = b(Bool()) + if (b != RegBinding) { + val analog: Analog = b(Analog(1.W)) + } + val bits: UInt = b(Bits(8.W)) + } + } + + class MyEmptyBundle extends Bundle + + class MyBundle extends Bundle { + val a: UInt = UInt(8.W) + val b: SInt = SInt(8.W) + val c: Bool = Bool() + } + + class TopCircuitBundles(b: BindingChoice) extends DebugTestModule(b) { + override def body: Unit = { + val a: Bundle = b(new Bundle {}) + val bnd: MyEmptyBundle = b(new MyEmptyBundle) + val c: MyBundle = b(new MyBundle) + } + } + + class MyNestedBundle extends Bundle { + val a: Bool = Bool() + val b: MyBundle = new MyBundle + val c: MyBundle = Flipped(new MyBundle) + } + + class TopCircuitBundlesNested(b: BindingChoice) extends DebugTestModule(b) { + override def body: Unit = { + val a: MyNestedBundle = b(new MyNestedBundle) + } + } + + class TopCircuitVecs(b: BindingChoice) extends DebugTestModule(b) { + override def body: Unit = { + val a: Vec[SInt] = b(Vec(5, SInt(23.W))) + val bv: Vec[Vec[SInt]] = b(Vec(5, Vec(3, SInt(23.W)))) + val c = b(Vec(5, new Bundle { val x: UInt = UInt(8.W) })) + val d = b(MixedVec(UInt(3.W), SInt(10.W))) + } + } + + class TopCircuitBundleWithVec(b: BindingChoice) extends DebugTestModule(b) { + override def body: Unit = { + val a = b(new Bundle { val vec = Vec(5, UInt(8.W)) }) + } + } + + class TopCircuitWhenElse extends RawModule { + val inSeq = IO(Input(Vec(8, UInt(8.W)))) + val out = IO(Output(UInt(8.W))) + val sel = IO(Input(UInt(math.sqrt(8).ceil.toInt.W))) + val tmp = sel + 1.U + when(sel % 2.U === 0.U) { + val outTmp = inSeq(sel) + val evenSel = outTmp + 1.U + out := evenSel + }.elsewhen(sel === 1.U) { + val outTmp = inSeq(sel) + val selIsOne = outTmp + 1.U + out := selIsOne + }.otherwise { + val outTmp = inSeq(sel) + val oddSel = outTmp + 1.U + out := oddSel + } + } + + class TopCircuitEnumSimple extends RawModule { + val e = IO(Input(DebugTestEnum())) + val e2 = IO(Input(DebugTestEnum2())) + val bnd = IO(Input(new Bundle { + val en = DebugTestEnum() + val en2 = DebugTestEnum2() + val x = UInt(8.W) + })) + val v = IO(Input(Vec(3, DebugTestEnum()))) + } + + } + + object MemCircuits { + class TopCircuitMem[T <: Data](gen: T) extends Module { val mem = Mem(4, gen) } + class TopCircuitSyncMem[T <: Data](gen: T) extends Module { val mem = SyncReadMem(4, gen) } + } +}