Skip to content

ChiselSim peek/poke/expect for Enums, Records, Vecs, and generic Data #4824

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

Open
wants to merge 18 commits into
base: main
Choose a base branch
from

Conversation

kammoh
Copy link
Contributor

@kammoh kammoh commented Mar 23, 2025

This PR changes the "testable" implicit classes in PeekPokeAPI to allow for a unified peek(), poke(), and expect API for all data types, including Records, Vecs, Enums, and nested aggregates.
This unified API would be very helpful when developing higher-level test abstractions, such as enqueue/deque over clocked Decoupled interfaces.

This is a step towards adding some useful features and ergonomic APIs similar to those provided by chiseltest (#4209 included a rough PoC for integrating some of these features).

The code does not look very pretty, but I could not figure out a cleaner way to make it work. Also, there is some repeated code.

I'd greatly appreciate comments and ideas for an alternative approach or other improvements.

Note: The previous (existing) implementation of expect[T]() seems incorrect. In addition to mistakingly using the same type name, T for the different method type parameter, the comparison uses object equality (!=).

def expect[T](
      expected:     T,
      encode:       (Simulation.Value) => T,
      buildMessage: (T, T) => String,
      sourceInfo:   SourceInfo
    ): Unit = {
...
        if (observed != expected) {

For the implemented Element values, the observed value is tuned into a Chisel object using the encode() method. Since the objects are not the same, the comparison would always fail, even when the value is expected. Seems the previous tests did not cover this, but using the tests in this PR, I get false failures with the previous expect + encode:

Observed value: 'UInt<33>(3521158774)'
Expected value: 'UInt<33>(3521158774)'

Contributor Checklist

  • Did you add Scaladoc to every public function/method?
  • Did you add at least one test demonstrating the PR?
  • Did you delete any extraneous printlns/debugging code?
  • Did you specify the type of improvement?
  • Did you add appropriate documentation in docs/src?
  • Did you request a desired merge strategy?
  • Did you add text to be included in the Release Notes for this change?

Type of Improvement

Feature

Desired Merge Strategy

  • Squash: The PR will be squashed and merged (choose this if you have no preference).

Release Notes

Reviewer Checklist (only modified by reviewer)

  • Did you add the appropriate labels? (Select the most appropriate one based on the "Type of Improvement")
  • Did you mark the proper milestone (Bug fix: 3.6.x, 5.x, or 6.x depending on impact, API modification or big change: 7.0)?
  • Did you review?
  • Did you check whether all relevant Contributor checkboxes have been checked?
  • Did you do one of the following when ready to merge:
    • Squash: You/ the contributor Enable auto-merge (squash) and clean up the commit message.
    • Merge: Ensure that contributor has cleaned up their commit history, then merge with Create a merge commit.

@kammoh kammoh changed the title peek/poke/expect for all Data types, include Records and Vecs [chiselsim] Unified peek/poke/expect API for all Data types, include Records and Vecs Mar 24, 2025
Copy link
Contributor

@jackkoenig jackkoenig left a comment

Choose a reason for hiding this comment

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

Overall I think this looks really good. Sorry for the delay on reviewing, I'm not super familiar with the PeekPokeAPI so had to look over it again so I could review 😅

I don't want to sandbag so feel free to push back, but my only real complaint is a nit that the tests are a big big rather than being split into small pieces for like each type being tested. Would it be too much trouble to split those up?

Comment on lines 82 to 126
it("should peek and poke various data types correctly") {
val numTests = 100
simulate(new TestPeekPokeEnum(w)) { dut =>
assert(w == dut.io.in.bits.a.getWidth)
val vecDim = dut.vecDim
val truncationMask = (BigInt(1) << w) - 1
for {
_ <- 0 until numTests
a = BigInt(w, rand)
b = BigInt(w, rand)
v1 = Seq.fill(vecDim)(BigInt(w, rand))
v2 = Seq.fill(vecDim)(BigInt(w, rand))
op <- TestOp.all
} {

dut.io.in.bits.poke(
chiselTypeOf(dut.io.in.bits).Lit(
_.a -> a.U,
_.b -> b.U,
_.v1 -> Vec.Lit(v1.map(_.U(w.W)): _*),
_.v2 -> Vec.Lit(v2.map(_.U(w.W)): _*)
)
)
dut.io.in.valid.poke(true)
dut.io.op.poke(op)
dut.clock.step()
dut.io.in.valid.poke(false)

val peekedOp = dut.io.op.peek()
assert(peekedOp.litValue == op.litValue)
assert(peekedOp.toString.contains(TestOp.getClass.getSimpleName.stripSuffix("$")))

val expected = op match {
case TestOp.Add => a + b
case TestOp.Sub => a - b
case TestOp.Mul => a * b
case _ => throw new Exception("Invalid operation")
}
val expectedCmp = a.compare(b) match {
case -1 => dut.CmpResult.LT
case 0 => dut.CmpResult.EQ
case 1 => dut.CmpResult.GT
}

dut.io.out.valid.expect(true.B)
Copy link
Contributor

Choose a reason for hiding this comment

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

Would it be possible to split this into a bunch of smaller tests of just individual types? Having the peeks and pokes of all types together kind of muddles what's being tested a bit.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think the most reliable way to test peeks and pokes is to apply poke()s to the inputs, let the circuit do its thing, and then peek() at or expect() the outputs. I really think we need even more testing/coverage, not less.

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not saying have less testing or coverage, I'm just saying to do it in smaller chunks. For example, one could organize this by type and have basically a type parametered passthrough module (or have a register in there to delay a cycle), and then for each type and peek and poke method have a test that pokes the input and then checks the output.

My only real qualm is the "various" in it("should peek and poke various data types correctly"), I'd rather see:

it("should peek and poke Bool correctly")
it("should peek and poke UInt correctly")
it("should peek and poke ChiselEnum correctly")
... etc.

I'm not going to sandbag over this, I just think that would be better

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oh I see what you mean and do agree with you. I cleaned up the tests as much as I could. Please let me know if you think it still needs more cleanup.

@kammoh
Copy link
Contributor Author

kammoh commented Apr 23, 2025

Hi @jackkoenig,
Thank you for the review and your feedback!
I'll try to cleanup and reorganize the tests, maybe break it up into multiple files.
I also have some doubts about what needs to be exposed as the public API at this point and also what needs to be supported for all types of Data. I'm also thinking about the usability and extensibility of the API for downstream libraries (e.g. FixedPoint and FloatingPoint data).
Also please let me know if you have any thoughts on the the expect functions that have a message generator argument.
I'm also testing this with my own libraries and implementations to make sure everything turns out consistent and works as expected.

@kammoh
Copy link
Contributor Author

kammoh commented Apr 23, 2025

The other issue is the ability to extend for the multi-threaded version of PeekPokeAPI. It seems to me that the current structure makes code reuse difficult.

@kammoh
Copy link
Contributor Author

kammoh commented Apr 24, 2025

All those .asInstanceOf[T] is scary and ugly, but I don't know how else we can do this.
Any thoughts @jackkoenig @seldridge ?

Comment on lines 484 to 493
case (dat: Record, exp: Record) =>
new TestableRecord(dat).expect(exp, buildMsgFn _)
case (dat: Vec[_], exp: Vec[_]) =>
new TestableVec(dat).expectVec(
exp,
(obs: Data, _: Data, idx: Int) => {
require(obs.getClass == exp.getClass, s"Type mismatch: ${obs.getClass} != ${exp.getClass}")
buildMessage(obs.asInstanceOf[T], expected.asInstanceOf[T]) + s". First mismatch at index $idx."
}
)
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm a little confused why there is special logic for Vec here but not for Record? I see Records are checked in the methods on TestableRecord so I think it's fine there's no extra checks, but Vecs are tested in TestableVec so why are there additional checks here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's probably not needed, but I wasn't 100% sure. The type generics for the extension classes differ between Vec and Record:

class TestableRecord[T <: Record](val data: T)  // type parameter is the type itself, so methods accepting T are type-checked

vs

class TestableVec[T <: Data](val data: Vec[T]) extends PeekPokable[Vec[T]]  // type parameter is the element type and is gone after type erasure

That's the only way I could get Vec[T] working. Please let me know if you think there's a cleaner way.

I cleaned up the code a little bit (using a weird workaround for Vec in _expect and _poke), so the special cases are now tucked away.

Comment on lines 455 to 488
case x: Bool => new TestableBool(x).peek().asInstanceOf[T]
case x: UInt => new TestableUInt(x).peek().asInstanceOf[T]
case x: SInt => new TestableSInt(x).peek().asInstanceOf[T]
case x: EnumType => new TestableEnum(x).peek().asInstanceOf[T]
case x: Record => new TestableRecord(x).peek().asInstanceOf[T]
case x: Vec[_] => new TestableVec(x).peek().asInstanceOf[T]
Copy link
Contributor

Choose a reason for hiding this comment

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

Are these the asInstanceOfs you're concerned about? I actually think there are fine--we know the type is correct because we have matched on the concrete runtime type, so barring a bug in TestableBool (or whatever for the given type), the cast will always work. The issue is just convincing the compiler that the type is correct--which it is.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, my primary concern was really Vec, but I agree that all should be fine.

val peekedValue = peek()
// Not peeking the value beforehand or using `def` or `lazy val` results in a mysterious infinite recursion and StackOverflowError
// The value is not used if the expected value matches and incurs an overhead
// TODO: dig deeper amd see if we can avoid this
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@jackkoenig Any thoughts on why this happens?


// Not peeking the value beforehand or using `def` or `lazy val` results in a mysterious infinite recursion and StackOverflowError
// The value is not used if the expected value matches and incurs an overhead
// TODO: dig deeper amd see if we can avoid this
val peekedValue = data.peek()
Copy link
Contributor Author

Choose a reason for hiding this comment

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

And here

@kammoh kammoh changed the title [chiselsim] Unified peek/poke/expect API for all Data types, include Records and Vecs ChiselSim peek/poke/expect for Enums, Records, Vecs, and generic Data Apr 27, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants