Skip to content

Commit

Permalink
Make '--module' support arguments (#4358)
Browse files Browse the repository at this point in the history
Dramatically increase the power of the '--module' argument to
'circt.stage.ChiselMain'.  This adds support for constructing modules with
parameters using reflection as demonstrated elsewhere [[1]].

This is long missing support that I intended to add when both the original
reflective construction was added [[2]] and turned on [[3]].  At the time,
I didn't know how to do reflective construction of arbitrary modules and
left that feature unfinished for the next 5 years.

This is done to move 'ChiselMain' more in the direction of being a command
line utility for running Chisel generators.  This is related to the
discussions of a hypothetical 'chisel-cli' that has been discussed on

This commit changes the API such that any class constructed this way must
take a trailing '()'.  Anything without this will be interpreted as a
string.  This is a change to how '--module' used to work where the
argument was assumed to be a class.

[1]: https://github.com/seldridge/reflective-builder
[2]: a423db5
[3]: 4eff16b

Signed-off-by: Schuyler Eldridge <[email protected]>
  • Loading branch information
seldridge authored Aug 18, 2024
1 parent 56897b5 commit da4cb2d
Show file tree
Hide file tree
Showing 3 changed files with 213 additions and 14 deletions.
39 changes: 35 additions & 4 deletions src/main/scala/chisel3/stage/ChiselAnnotations.scala
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,28 @@ case class ChiselGeneratorAnnotation(gen: () => RawModule) extends NoTargetAnnot

object ChiselGeneratorAnnotation extends HasShellOptions {

/** Try to convert a string to a Scala type. */
private def stringToAny(str: String): Any = {

/* Something that looks like object creation, e.g., "Foo(42)" */
val classPattern = "([a-zA-Z0-9_$.]+)\\((.*)\\)".r

str match {
case boolean if boolean.toBooleanOption.isDefined => boolean.toBoolean
case integer if integer.toIntOption.isDefined => integer.toInt
case float if float.toDoubleOption.isDefined => float.toDouble
case classPattern(a, b) =>
val constructor = Class.forName(a).getConstructors()(0)
if (b.isEmpty) {
constructor.newInstance()
} else {
val arguments = b.split(',').map(stringToAny).toSeq
constructor.newInstance(arguments: _*)
}
case string => str
}
}

/** Construct a [[ChiselGeneratorAnnotation]] with a generator function that will try to construct a Chisel Module
* from using that Module's name. The Module must both exist in the class path and not take parameters.
* @param name a module name
Expand All @@ -235,7 +257,7 @@ object ChiselGeneratorAnnotation extends HasShellOptions {
def apply(name: String): ChiselGeneratorAnnotation = {
val gen = () =>
try {
Class.forName(name).asInstanceOf[Class[_ <: RawModule]].getDeclaredConstructor().newInstance()
stringToAny(name).asInstanceOf[RawModule]
} catch {
// The reflective instantiation will box any exceptions thrown, unbox them here.
// Note that this does *not* need to chain with the catches below which are triggered by an
Expand All @@ -244,12 +266,21 @@ object ChiselGeneratorAnnotation extends HasShellOptions {
case e: InvocationTargetException =>
throw e.getCause
case e: ClassNotFoundException =>
throw new OptionsException(s"Unable to locate module '$name'! (Did you misspell it?)", e)
case e: NoSuchMethodException =>
throw new OptionsException(
s"Unable to create instance of module '$name'! (Does this class take parameters?)",
s"Unable to run module generator '$name' because it or one of its arguments could not be found. (Did you misspell it or them?)",
e
)
case e: IllegalArgumentException =>
throw new OptionsException(
s"Unable to run module generator '$name' because the arguments are invalid. (Did you pass the correct number and type of arguments?)",
e
)
case e: ClassCastException =>
throw new OptionsException(
s"Unable to run module generator '$name' because this is not a 'RawModule'. (Did you try to construct something that is not a 'RawModule' or did you forget to append '()' to indicate that this is not a string?)",
e
)

}
ChiselGeneratorAnnotation(gen)
}
Expand Down
21 changes: 11 additions & 10 deletions src/test/scala/chiselTests/stage/ChiselAnnotationsSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -48,24 +48,25 @@ class ChiselAnnotationsSpec extends AnyFlatSpec with Matchers {

behavior.of("ChiselGeneratorAnnotation when stringly constructing from Module names")

it should "elaborate from a String" in {
val annotation = ChiselGeneratorAnnotation("chiselTests.stage.ChiselAnnotationsSpecFoo")
it should "elaborate a module without parameters" in {
val annotation = ChiselGeneratorAnnotation("chiselTests.stage.ChiselAnnotationsSpecFoo()")
val res = annotation.elaborate
res(0) shouldBe a[ChiselCircuitAnnotation]
res(1) shouldBe a[DesignAnnotation[_]]
}

it should "throw an exception if elaboration from a String refers to nonexistant class" in {
val bar = "chiselTests.stage.ChiselAnnotationsSpecBar"
val annotation = ChiselGeneratorAnnotation(bar)
intercept[OptionsException] { annotation.elaborate }.getMessage should startWith(s"Unable to locate module '$bar'")
it should "elaborate a module with parameters" in {
val annotation = ChiselGeneratorAnnotation("""chiselTests.stage.ChiselAnnotationsSpecBaz("hello")""")
val res = annotation.elaborate
res(0) shouldBe a[ChiselCircuitAnnotation]
res(1) shouldBe a[DesignAnnotation[_]]
}

it should "throw an exception if elaboration from a String refers to an anonymous class" in {
val baz = "chiselTests.stage.ChiselAnnotationsSpecBaz"
val annotation = ChiselGeneratorAnnotation(baz)
it should "throw an exception if elaboration from a String refers to nonexistant class" in {
val bar = "chiselTests.stage.ChiselAnnotationsSpecBar()"
val annotation = ChiselGeneratorAnnotation(bar)
intercept[OptionsException] { annotation.elaborate }.getMessage should startWith(
s"Unable to create instance of module '$baz'"
s"Unable to run module generator '$bar' because it or one of its arguments could not be found"
)
}

Expand Down
167 changes: 167 additions & 0 deletions src/test/scala/circtTests/stage/ChiselMainSpec.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
// SPDX-License-Identifier: Apache-2.0

package circtTests.stage

import chisel3._
import circt.stage.ChiselMain
import java.io.File
import org.scalatest.funspec.AnyFunSpec
import org.scalatest.matchers.should.Matchers
import scala.io.Source

object ChiselMainSpec {

class ParameterlessModule extends RawModule {
val a = IO(Input(Bool()))
val b = IO(Output(Bool()))

b :<= a
}

class BooleanModule(param: Boolean) extends RawModule {
override final def desiredName = s"${this.getClass().getSimpleName()}_$param"
}

class IntegerModule(param: Int) extends RawModule {
override final def desiredName = s"${this.getClass().getSimpleName()}_$param"
}

class DoubleModule(param: Double) extends RawModule {
override final def desiredName = s"${this.getClass().getSimpleName()}_$param"
}

object Enum {
sealed trait Type
class A extends Type {
override def toString = "A"
}
class B extends Type {
override def toString = "B"
}
}
class ObjectModule(param: Enum.Type) extends RawModule {
override final def desiredName = s"${this.getClass().getSimpleName()}_$param"
}

class StringModule(param: String) extends RawModule {
override final def desiredName = s"${this.getClass().getSimpleName()}_$param"
}

class MultipleParameters(bool: Boolean, int: Int) extends RawModule {
override final def desiredName = s"${this.getClass().getSimpleName()}_${bool}_$int"
}

}

class ChiselMainSpec extends AnyFunSpec with Matchers with chiselTests.Utils {
import ChiselMainSpec._

val testDir = new File("test_run_dir/ChiselMainSpec")
case class Test(module: String, filename: String) {
def test() = {
val outFile = new File(testDir, filename)
outFile.delete()
outFile shouldNot exist

info(module)
ChiselMain.main(
Array(
"--module",
module,
"--target",
"chirrtl",
"--target-dir",
testDir.toString
)
)

outFile should exist
}
}

describe("ChiselMain") {

describe("support for modules without parameters") {

it("should elaborate a parameterless module") {

Test(
"circtTests.stage.ChiselMainSpec$ParameterlessModule()",
"ParameterlessModule.fir"
).test()

}

}

describe("support for modules with parameters") {

it("should elaborate a module with a Boolean parameter") {

Test(
"circtTests.stage.ChiselMainSpec$BooleanModule(true)",
"BooleanModule_true.fir"
).test()

Test(
"circtTests.stage.ChiselMainSpec$BooleanModule(false)",
"BooleanModule_false.fir"
).test()

}

it("should elaborate a module with an Integer parameter") {

Test(
"circtTests.stage.ChiselMainSpec$IntegerModule(42)",
"IntegerModule_42.fir"
).test()

}

it("should elaborate a module with a Double parameter") {

Test(
"circtTests.stage.ChiselMainSpec$DoubleModule(3.141592654)",
"DoubleModule_3141592654.fir"
).test()

}

it("should elaborate a module with an object parameter") {

Test(
"circtTests.stage.ChiselMainSpec$ObjectModule(circtTests.stage.ChiselMainSpec$Enum$A())",
"ObjectModule_A.fir"
).test()

Test(
"circtTests.stage.ChiselMainSpec$ObjectModule(circtTests.stage.ChiselMainSpec$Enum$B())",
"ObjectModule_B.fir"
).test()

}

it("should elaborate a module with a string parameter") {

Test(
"""circtTests.stage.ChiselMainSpec$StringModule("hello")""",
"StringModule_hello.fir"
).test()

}

it("should elaborate a module that takes multiple parameters") {

Test(
"circtTests.stage.ChiselMainSpec$MultipleParameters(true,42)",
"MultipleParameters_true_42.fir"
).test()

}

}

}

}

0 comments on commit da4cb2d

Please sign in to comment.