-
Notifications
You must be signed in to change notification settings - Fork 623
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
base: main
Are you sure you want to change the base?
Conversation
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.
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?
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) |
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.
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.
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 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.
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'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
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.
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.
Hi @jackkoenig, |
The other issue is the ability to extend for the multi-threaded version of |
All those |
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." | ||
} | ||
) |
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'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?
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.
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.
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] |
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.
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.
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.
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 |
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.
@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() |
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.
And here
be more flexible with the provided lit Vec
more concise, and matches the previous message tested in ChiselSimSpec
lengths are checked to match, elements are themselves checked when being individually poked
+ Refactor PeekPokeTestModule to its own file
- Use implicit functions inside the trait - Better error messages for expect on Vec
This PR changes the "testable" implicit classes in
PeekPokeAPI
to allow for a unifiedpeek()
,poke()
, andexpect
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 (!=
).For the implemented
Element
values, the observed value is tuned into a Chisel object using theencode()
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 previousexpect
+encode
:Contributor Checklist
docs/src
?Type of Improvement
Feature
Desired Merge Strategy
Release Notes
Reviewer Checklist (only modified by reviewer)
3.6.x
,5.x
, or6.x
depending on impact, API modification or big change:7.0
)?Enable auto-merge (squash)
and clean up the commit message.Create a merge commit
.