Skip to content

Commit f497ceb

Browse files
Add FormalTest marker (#4635)
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.
1 parent b1ccb4f commit f497ceb

File tree

6 files changed

+144
-0
lines changed

6 files changed

+144
-0
lines changed
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
3+
package chisel3
4+
5+
import chisel3.experimental.{BaseModule, Param}
6+
import chisel3.internal.Builder
7+
import chisel3.internal.firrtl.ir._
8+
import chisel3.internal.throwException
9+
import chisel3.experimental.{SourceInfo, UnlocatableSourceInfo}
10+
11+
object FormalTest {
12+
def apply(
13+
module: BaseModule,
14+
params: MapTestParam = MapTestParam(Map.empty),
15+
name: String = ""
16+
)(implicit sourceInfo: SourceInfo): Unit = {
17+
val proposedName = if (name != "") {
18+
name
19+
} else {
20+
module._proposedName
21+
}
22+
val sanitizedName = Builder.globalNamespace.name(proposedName)
23+
Builder.components += DefFormalTest(sanitizedName, module, params, sourceInfo)
24+
}
25+
}
26+
27+
/** Parameters for test declarations. */
28+
sealed abstract class TestParam
29+
case class IntTestParam(value: BigInt) extends TestParam
30+
case class DoubleTestParam(value: Double) extends TestParam
31+
case class StringTestParam(value: String) extends TestParam
32+
case class ArrayTestParam(value: Seq[TestParam]) extends TestParam
33+
case class MapTestParam(value: Map[String, TestParam]) extends TestParam

core/src/main/scala/chisel3/internal/firrtl/Converter.scala

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -397,6 +397,14 @@ private[chisel3] object Converter {
397397
case RawParam(value) => fir.RawStringParam(name, value)
398398
}
399399

400+
def convert(param: TestParam): fir.TestParam = param match {
401+
case IntTestParam(value) => fir.IntTestParam(value)
402+
case DoubleTestParam(value) => fir.DoubleTestParam(value)
403+
case StringTestParam(value) => fir.StringTestParam(value)
404+
case ArrayTestParam(value) => fir.ArrayTestParam(value.map(convert))
405+
case MapTestParam(value) => fir.MapTestParam(value.map { case (name, value) => (name, convert(value)) })
406+
}
407+
400408
// TODO: Modify Panama CIRCT to account for type aliasing information. This is a temporary hack to
401409
// allow Panama CIRCT to compile
402410
def convert(
@@ -455,6 +463,13 @@ private[chisel3] object Converter {
455463
(ports ++ ctx.secretPorts).map(p => convert(p, typeAliases)),
456464
convert(block, ctx, typeAliases)
457465
)
466+
case ctx @ DefFormalTest(name, module, params, sourceInfo) =>
467+
fir.FormalTest(
468+
convert(sourceInfo),
469+
name,
470+
module.name,
471+
convert(params).asInstanceOf[fir.MapTestParam]
472+
)
458473
}
459474

460475
def convertLayer(layer: Layer): fir.Layer = {

core/src/main/scala/chisel3/internal/firrtl/IR.scala

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -494,6 +494,17 @@ private[chisel3] object ir {
494494
params: Map[String, Param]
495495
) extends Component
496496

497+
case class DefFormalTest(
498+
name: String,
499+
module: BaseModule,
500+
params: MapTestParam,
501+
sourceInfo: SourceInfo
502+
) extends Component {
503+
def id = module
504+
val ports: Seq[Port] = Seq.empty
505+
override val secretPorts = mutable.ArrayBuffer[Port]()
506+
}
507+
497508
case class DefIntrinsicModule(
498509
id: BaseIntrinsicModule,
499510
name: String,

firrtl/src/main/scala/firrtl/ir/IR.scala

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -647,6 +647,23 @@ case class IntModule(info: Info, name: String, ports: Seq[Port], intrinsic: Stri
647647
*/
648648
case class DefClass(info: Info, name: String, ports: Seq[Port], body: Statement) extends DefModule with UseSerializer
649649

650+
/** Parameters for test declarations.
651+
*/
652+
sealed abstract class TestParam extends FirrtlNode
653+
case class IntTestParam(value: BigInt) extends TestParam with UseSerializer
654+
case class DoubleTestParam(value: Double) extends TestParam with UseSerializer
655+
case class StringTestParam(value: String) extends TestParam with UseSerializer
656+
case class ArrayTestParam(value: Seq[TestParam]) extends TestParam with UseSerializer
657+
case class MapTestParam(value: Map[String, TestParam]) extends TestParam with UseSerializer
658+
659+
/** Formal Test
660+
*/
661+
case class FormalTest(info: Info, name: String, moduleName: String, params: MapTestParam)
662+
extends DefModule
663+
with UseSerializer {
664+
val ports: Seq[Port] = Seq.empty
665+
}
666+
650667
case class Circuit(
651668
info: Info,
652669
modules: Seq[DefModule],

firrtl/src/main/scala/firrtl/ir/Serializer.scala

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -479,6 +479,27 @@ object Serializer {
479479
case other => b ++= other.serialize // Handle user-defined nodes
480480
}
481481

482+
private def s(node: TestParam)(implicit b: StringBuilder, indent: Int): Unit = node match {
483+
case IntTestParam(value) => b ++= value.toString
484+
case DoubleTestParam(value) => b ++= value.toString
485+
case StringTestParam(value) => b ++= StringLit(value).escape
486+
case ArrayTestParam(value) =>
487+
b ++= "[";
488+
value.zipWithIndex.foreach { case (value, i) =>
489+
if (i > 0) b ++= ", "
490+
s(value)
491+
}
492+
b ++= "]"
493+
case MapTestParam(value) =>
494+
b ++= "{"
495+
value.keys.toSeq.sorted.zipWithIndex.foreach { case (name, i) =>
496+
if (i > 0) b ++= ", "
497+
b ++= name; b ++= " = "; s(value(name))
498+
}
499+
b ++= "}"
500+
case other => b ++= other.serialize // Handle user-defined nodes
501+
}
502+
482503
private def sIt(node: DefModule)(implicit indent: Int): Iterator[String] = node match {
483504
case Module(info, name, public, layers, ports, body) =>
484505
val start = {
@@ -519,6 +540,13 @@ object Serializer {
519540
b.toString
520541
}
521542
Iterator(start) ++ sIt(body)(indent + 1)
543+
case FormalTest(info, name, moduleName, params) =>
544+
implicit val b = new StringBuilder
545+
doIndent(0); b ++= "formal "; b ++= legalize(name); b ++= " of "; b ++= legalize(moduleName); b ++= " :"; s(info)
546+
params.value.keys.toSeq.sorted.foreach { case name =>
547+
newLineAndIndent(1); b ++= name; b ++= " = "; s(params.value(name))
548+
}
549+
Iterator(b.toString)
522550
case other =>
523551
Iterator(Indent * indent, other.serialize) // Handle user-defined nodes
524552
}
@@ -549,6 +577,7 @@ object Serializer {
549577
if (layers.nonEmpty) {
550578
implicit val b = new StringBuilder
551579
layers.foreach(s)
580+
newLineNoIndent()
552581
Iterator(b.toString)
553582
} else Iterator.empty
554583
}

src/test/scala/chiselTests/RawModuleSpec.scala

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,4 +228,43 @@ class RawModuleSpec extends ChiselFlatSpec with Utils with FileCheck {
228228
|""".stripMargin
229229
)
230230
}
231+
232+
"RawModule marked as formal test" should "emit a formal test declaration" in {
233+
class Foo extends RawModule {
234+
FormalTest(this)
235+
FormalTest(this, MapTestParam(Map("hello" -> StringTestParam("world"))))
236+
FormalTest(
237+
this,
238+
MapTestParam(
239+
Map(
240+
"a_int" -> IntTestParam(42),
241+
"b_double" -> DoubleTestParam(13.37),
242+
"c_string" -> StringTestParam("hello"),
243+
"d_array" -> ArrayTestParam(Seq(IntTestParam(42), StringTestParam("hello"))),
244+
"e_map" -> MapTestParam(
245+
Map(
246+
"x" -> IntTestParam(42),
247+
"y" -> StringTestParam("hello")
248+
)
249+
)
250+
)
251+
),
252+
"thisBetterWork"
253+
)
254+
}
255+
256+
generateFirrtlAndFileCheck(new Foo)(
257+
"""|CHECK: formal Foo of [[FOO:Foo_.*]] :
258+
|CHECK: formal Foo_1 of [[FOO]] :
259+
|CHECK: hello = "world"
260+
|CHECK: formal thisBetterWork of [[FOO]] :
261+
|CHECK: a_int = 42
262+
|CHECK: b_double = 13.37
263+
|CHECK: c_string = "hello"
264+
|CHECK: d_array = [42, "hello"]
265+
|CHECK: e_map = {x = 42, y = "hello"}
266+
|CHECK: module [[FOO]] :
267+
|""".stripMargin
268+
)
269+
}
231270
}

0 commit comments

Comments
 (0)