[debug] Add circt_debug_* intrinsics for Chisel type metadata#5276
[debug] Add circt_debug_* intrinsics for Chisel type metadata#5276fkhaidari wants to merge 1 commit intochipsalliance:mainfrom
Conversation
|
|
a6036bf to
d8ed90a
Compare
d8ed90a to
61414ba
Compare
jackkoenig
left a comment
There was a problem hiding this comment.
Thank you for this contribution.
A few comments, I'm okay moving forward using runtime reflection but I think we should discuss how we might do this in a much more robust way using static reflection (aka macros) in Scala 3.
| // 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 { |
There was a problem hiding this comment.
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.
Agreed -- the multi-ctor case here are exactly the kind of thing typeclass derivation would solve cleanly
| 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) | ||
| ) | ||
| ) |
There was a problem hiding this comment.
| 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?
There was a problem hiding this comment.
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}| 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" |
There was a problem hiding this comment.
This feels a bit sketchy, what is this trying to accomplish? Is the problem that MixedVec has it's argument as private?
If that's the case, I think we have a really tricky usability issue since may arbitrarily make their arguments private or constructors private...
There was a problem hiding this comment.
The string-name check is a build-graph constraint: chisel3.util.MixedVec lives in the chisel module, which dependsOn(coreModule) -- and chisel3.debug.* is in core/. Importing it from there cycles the graph (verified, doesn't compile)
I could add a marker trait in core (e.g. private[chisel3] trait OmitDebugCtorParams) that MixedVec extends, so the matcher becomes case _: OmitDebugCtorParams => true. Not sure that's actually less sketchy though -- happy to go either way.
| `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. |
There was a problem hiding this comment.
Thank you for describing these limitations. I must confess these make me pretty nervous about the stability of this feature. I'm not opposed to moving forward with the limitations in mind, but I think we should mark this experimental (not using the experimental package, that was a bit of a failed experiment). I'll add some suggestions of how to warn users a bit.
I think looking into how to do this with static reflection (aka macros, especially typeclass derivation in Scala 3) is the more robust way forward.
There was a problem hiding this comment.
Agreed on both fronts. I'll apply your experimental warnings/scaladoc suggestions in the next push so users see the caveat right at the entry points. And yes, moving the runtime reflection to typeclass derivation (Scala 3 macros) is the right long-term direction
| ``` | ||
|
|
||
| ## Debug type-info intrinsics | ||
|
|
There was a problem hiding this comment.
| :::warning | |
| This feature is experimental and subject to change. | |
| See [Notes on reflection](#notes-on-reflection) below for limitations. | |
| ::: |
|
|
||
| override val options: Seq[ShellOption[Unit]] = Seq( | ||
| new ShellOption[Unit]( | ||
| longOption = "with-debug-intrinsics", |
There was a problem hiding this comment.
| longOption = "with-debug-intrinsics", | |
| longOption = "with-experimental-debug-intrinsics", |
| import firrtl.options.{HasShellOptions, ShellOption, Unserializable} | ||
|
|
||
| /** Opt-in toggle for [[chisel3.stage.phases.AddDebugIntrinsics]]. | ||
| * Activate with `--with-debug-intrinsics` or by adding this annotation. |
There was a problem hiding this comment.
| * Activate with `--with-debug-intrinsics` or by adding this annotation. | |
| * Activate with `--with-debug-intrinsics` or by adding this annotation. | |
| * | |
| * @note This API is experimental and subject to change. |
| import firrtl.options.{Dependency, Phase} | ||
| import firrtl.{annoSeqToSeq, seqToAnnoSeq, AnnotationSeq} | ||
|
|
||
| /** No-op unless [[chisel3.debug.EmitDebugIntrinsicsAnnotation]] is present. */ |
There was a problem hiding this comment.
| /** No-op unless [[chisel3.debug.EmitDebugIntrinsicsAnnotation]] is present. */ | |
| /** No-op unless [[chisel3.debug.EmitDebugIntrinsicsAnnotation]] is present. | |
| * | |
| * @note This API is experimental and subject to change. | |
| */ |
61414ba to
8b4eadc
Compare
Introduce an opt-in pass that injects CIRCT debug intrinsics carrying Chisel-level type and constructor-parameter metadata through to FIRRTL, so downstream tooling (waveform viewers, debuggers) can recover Bundle field names, Vec shapes, ChiselEnum types and module parameters that are otherwise lost during conversion. The DebugIntrinsics emitter walks the elaborated circuit and appends four kinds of secret intrinsics: - circt_debug_moduleinfo per module (typeName + ctor params) - circt_debug_var per top-level signal, port or memory - circt_debug_subfield per Bundle/Vec element, linked by parent - circt_debug_enumdef per ChiselEnum type (emitted once) Constructor parameters are extracted via runtime reflection and serialised to JSON. Booleans round-trip as native JSON bools; all numeric types (Byte / Short / Int / Long / Float / Double) serialise uniformly as JSON strings -- JSON has no NaN/Infinity literals and Double-backed parsers lose precision on Long > 2^53, so a single string shape avoids both pitfalls and gives downstream sinks one attribute mapping to write. JDK and Scala stdlib classes are not recursed into during nested ctor extraction; their constructors are implementation details and produce no user-meaningful parameters. User toString output embedded in the params payload is capped at 4 KiB to bound emission size against pathological implementations. Emission is disabled by default and activated by adding EmitDebugIntrinsicsAnnotation to the annotation seq programmatically, or by passing `--with-debug-intrinsics` on the CLI to any circt.stage.ChiselStage entry point. Either form schedules the new AddDebugIntrinsics phase between Elaborate and Convert; the phase is a no-op without the annotation. After consuming it, the phase strips EmitDebugIntrinsicsAnnotation from the seq so a second pass over the same annotations does not double-emit intrinsics. Includes unit tests for moduleinfo / var / subfield / enumdef emission across scalar, Bundle, Vec, ChiselEnum, memory and hierarchy cases, plus a docs section in docs/src/explanations/intrinsics.md. Based on work of @rameloni Signed-off-by: fkhaidari <khaidari.fg@gmail.com>
8b4eadc to
2aa674c
Compare
|
@jackkoenig, thank you for your review! |
Hi! This PR adds source-level debug info collection for Chisel
Based on @rameloni's Tywaves work (#4015, #4224), but using intrinsics
instead of annotations as @fabianschuiki suggested in that thread.
Emits four intrinsics during elaboration:
circt_debug_var-- type + ctor params per signalcirct_debug_subfield-- aggregate hierarchycirct_debug_moduleinfo-- module-level metadata (class, ctor params)circt_debug_enumdef-- ChiselEnum definitionsFully opt-in via
--emit-debug-type-info. Without the flag, FIRRTLoutput is bit-identical to main.
Companion CIRCT PR (intrinsic lowering + debug dialect extensions)
is in progress at [fkhaidari/circt#1] and will be submitted upstream
later
cc @fabianschuiki @rameloni @jackkoenig @seldridge
Contributor Checklist
docs/src?Type of Improvement
Desired Merge Strategy
Release Notes
Add opt-in
ChiselStageflagwithDebug = truethat emitscirct_debug_*intrinsics preserving Chisel-level type and parameter metadata in FIRRTL.
Reviewer Checklist (only modified by reviewer)
3.6.x,5.x, or6.xdepending on impact, API modification or big change:7.0)?Enable auto-merge (squash)and clean up the commit message.Create a merge commit.