Skip to content

Conversation

fabianschuiki
Copy link
Contributor

The FIRRTL spec defines a formal construct to indicate that a module should be executed as a formal test. Currently, there is no way to emit this construct from Chisel.

This PR adds a user-facing FormalTest class that can be used to mark a module as to be executed as a formal test. This would commonly be used inside a test harness module to mark the surrounding module as a test. For example:

class TestHarness extends RawModule {
  new FormalTest(this)
}

The formal test can be given an optional name and a map of parameters. The parameters are distinct from blackbox modules in that they do not map to Verilog parameters, but instead allow for any recursive nesting of integers, doubles, strings, arrays, and maps, as prescribed by the FIRRTL spec. Multiple formal test markers may be added to a single module, which may be useful if a test should be run with different sets of user-defined parameters.

This PR also adds the necessary IR nodes to the FIRRTL and Chisel FIRRTL IRs.

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 (or new API)

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), clean up the commit message, and label with Please Merge.
    • Merge: Ensure that contributor has cleaned up their commit history, then merge with Create a merge commit.

Copy link
Contributor

@mikeurbach mikeurbach left a comment

Choose a reason for hiding this comment

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

The general boilerplate of adding a new construct to emit in FIRRTL looks good to me. Just a higher-level question about the user API.

} else {
module._proposedName
}
private[chisel3] override def generateComponent(): Option[Component] = None
Copy link
Contributor

Choose a reason for hiding this comment

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

If this is directly adding components to the builder in the constructor, and doesn't actually generateComponent as a BaseModule, maybe this should be more of a helper function rather than a BaseModule class that is instantiated? It seems like all we really need here is a public API to push a DefFormalTest, which can pass the name and parameters to FIRRTL. So maybe this could be more like:

object FormalTest {
  def apply(...) = {
    ...
  }
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hmmm yeah, that is a good point. I'll give that a shot!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks for the suggestion! That cleaned things up quite a bit 🙂. Not entirely sure where to get the source location from now, and the FIRRTL-side Component needs a def id: BaseModule field, but I can make both work by just using the test harness module for that. 👍

Copy link
Contributor

Choose a reason for hiding this comment

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

Cool, that does look cleaner to me. If you want to materialize a SourceInfo at the callsite, I think you can add an implicit argument to your apply method, like:

def apply(...)(implicit sourceInfo: SourceInfo)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah nice, I was wondering if that would just work, or if the plugin was hardwired to only touch certain constructs. I've added that and threaded it through to the FIRRTL emission. Works like a charm! Thanks for the pointer 🙂

@fabianschuiki fabianschuiki force-pushed the emit-firrtl-formal-test branch 2 times, most recently from d735f39 to d5f11cc Compare January 22, 2025 00:33
@fabianschuiki fabianschuiki added the Feature New feature, will be included in release notes label Jan 22, 2025
@fabianschuiki fabianschuiki force-pushed the emit-firrtl-formal-test branch 3 times, most recently from 40c7cc5 to 83f8031 Compare January 22, 2025 17:50
Copy link
Contributor

@mikeurbach mikeurbach left a comment

Choose a reason for hiding this comment

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

This is looking good to me. I think this is a reasonable API, but curious what others think.

}

generateFirrtlAndFileCheck(new Foo)(
"""|CHECK-LABEL: formal Foo of Foo_2 :
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: maybe add a check for module Foo_2 so we know that is indeed the module name? Could maybe use the capture / substitution functionality as well, so we could do something like CHECK: module [[NAME:Foo.+]] and later CHECK: formal Foo of [[NAME]]?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah yeah, nice catch. Forgot that this actually runs through FileCheck with all the bells and whistles.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done 👍

Comment on lines +234 to +254
FormalTest(this)
FormalTest(this, MapTestParam(Map("hello" -> StringTestParam("world"))))
FormalTest(
this,
MapTestParam(
Map(
"a_int" -> IntTestParam(42),
"b_double" -> DoubleTestParam(13.37),
"c_string" -> StringTestParam("hello"),
"d_array" -> ArrayTestParam(Seq(IntTestParam(42), StringTestParam("hello"))),
"e_map" -> MapTestParam(
Map(
"x" -> IntTestParam(42),
"y" -> StringTestParam("hello")
)
)
)
),
"thisBetterWork"
)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

It's fine that in this PR we're just testing for the basic FIRRTL emission, but how are Chisel users really intended to use 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.

I'd expect this to combine nicely with some of the work that @tmckay-sifive has been doing on defining unit tests and inline tests. There could be an API like formalTest { ... } that you get for your Testable or HasTest (don't remember what the proposed trait was exactly), which would mark the generate test harness as a formal test. We may also want to allow the user to define some of the parameters on the test, or at least supplement whatever Chisel does internally with their own stuff. There might also be cases where the user really wants to manually mark modules as formal tests through this API, if they need multiple test runs on the same harness with different parameters.

TL;DR, I'd imagine something like this as a the lowest barrier of entry for formally testing modules:

class MyModule extends RawModule with Testable {
  val a = IO(Input(UInt(8.W)))
  val b = IO(Input(UInt(8.W)))
  val z = IO(Output(UInt(8.W)))

  // ... module body ...

  formalTest "Complete" { dut =>
    AssertProperty(dut.a + dut.b === dut.z)
  }
  formalTest "NeutralA" { dut =>
    AssumeProperty(dut.a === 0.U)
    AssertProperty(dut.b === dut.z)
  }
  formalTest "NeutralB" { dut =>
    AssumeProperty(dut.b === 0.U)
    AssertProperty(dut.a === dut.z)
  }
}

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 wondering if we really want FormalTest and the Parameters classes to be part of the Chisel public API vs. a more sugary API like your sketch above. In any case, not gonna sandbag here, we should just make sure to massage the API before we really start advertising this to users

@fabianschuiki fabianschuiki force-pushed the emit-firrtl-formal-test branch from 83f8031 to 6ebe8fa Compare January 23, 2025 01:46
The FIRRTL spec defines a `formal` construct to indicate that a module
should be executed as a formal test. Currently, there is no way to emit
this construct from Chisel.

This PR adds a user-facing `FormalTest` class that can be used to mark
a module as to be executed as a formal test. This would commonly be used
inside a test harness module to mark the surrounding module as a test.
For example:

    class TestHarness extends RawModule {
      FormalTest(this)
    }

The formal test can be given an optional name and a map of parameters.
The parameters are distinct from blackbox modules in that they do not
map to Verilog parameters, but instead allow for any recursive nesting
of integers, doubles, strings, arrays, and maps, as prescribed by the
FIRRTL spec. Multiple formal test markers may be added to a single
module, which may be useful if a test should be run with different sets
of user-defined parameters.

This PR also adds the necessary IR nodes to the FIRRTL and Chisel FIRRTL
IRs.
@fabianschuiki fabianschuiki force-pushed the emit-firrtl-formal-test branch from 6ebe8fa to 597d577 Compare January 23, 2025 19:36
@fabianschuiki fabianschuiki enabled auto-merge (squash) January 23, 2025 19:42
@fabianschuiki fabianschuiki merged commit f497ceb into chipsalliance:main Jan 23, 2025
15 checks passed
@fabianschuiki fabianschuiki deleted the emit-firrtl-formal-test branch January 23, 2025 20:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Feature New feature, will be included in release notes
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants