Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions build.mill
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,39 @@ trait Chisel extends CrossSbtModule with HasScala2MacroAnno with HasScalaPlugin
override def scalacOptions = Task { super.scalacOptions() :+ "-Wconf:cat=other-implicit-type:s" }

override def testForkGrouping = discoveredTestClasses().grouped(8).toSeq

// Compile shared libraries for the simulator link library tests.
def sharedTestLibs: T[Seq[os.Path]] = Task(persistent = true) {
val srcDir = this.moduleDir / "src" / "test" / "resources" / "chisel3" / "simulator"
val cc = sys.env.getOrElse("CC", "cc")
val shared = if (scala.util.Properties.isMac) "-dynamiclib" else "-shared"
val srcFiles = Seq("linkLibA.c", "linkLibB.c", "linkLibC.c")
val libFiles = srcFiles.map { src =>
val lib = Task.dest / (src.stripSuffix(".c") + ".so")
os.proc(cc, shared, "-fPIC", srcDir / src, "-o", lib).call()
lib
}
libFiles
}

// Pass some of the pre-compiled shared libraries to the simulator via
// environment variables.
override def forkEnv = Task {
val libs = sharedTestLibs()
super.forkEnv() ++ Map(
// CHISELSIM_LIBS lets ChiselSim resolve libraries by name:
"CHISELSIM_LIBS" -> s"linkLibA=${libs(0)}",
// Also pass in a full path to test the `libraryPaths` setting:
"LINKLIBC_FULL_PATH" -> libs(2).toString
)
}

// Pass one of the pre-compiled shared libraries to the simulator via a JVM
// property.
override def forkArgs = Task {
val libs = sharedTestLibs()
super.forkArgs() :+ s"-Dchiselsim.libraries=linkLibB=${libs(1)}"
}
Comment on lines 342 to 373
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 instead of applying this to all chisel tests, we shouldn't add a new test build unit for this. Obviously they aren't harmful, but it feels a bit incongruous

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah I was wondering the same thing. I then thought that separating this out into a different build would complicate the whole build for no good practical reason, just aesthetics.

}
Comment on lines +358 to 374
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 going to block but I don't love this meaning given to the various libs by index--I'd rather we used a case class (or even named tuple, <3 Scala 3) to give names to them, but I'll let it go.


def unitTest(
Expand Down
66 changes: 61 additions & 5 deletions src/main/scala/chisel3/simulator/Settings.scala
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,12 @@ object MacroText {
* @param enableWavesAtTimeZero enable waveform dumping at time zero. This
* requires a simulator capable of dumping waves.
* @param randomization random initialization settings to use
* @param libraries Names of libraries to include in simulation. Use this to
* provide implementations for DPI functions, for example. The simulator will
* resolve these libraries to concrete files using the `CHISELSIM_LIBS`
* environment variable and `chiselsim.libraries` Java property.
* @param libraryPaths Paths to libraries to include in simulation. Use this to
* provide implementations for DPI functions, for example.
*/
final class Settings[A <: RawModule] private[simulator] (
/** Layers to turn on/off during Verilog elaboration */
Expand All @@ -90,7 +96,9 @@ final class Settings[A <: RawModule] private[simulator] (
val stopCond: Option[MacroText.Type[A]],
val plusArgs: Seq[svsim.PlusArg],
val enableWavesAtTimeZero: Boolean,
val randomization: Randomization
val randomization: Randomization,
val libraries: Seq[String],
val libraryPaths: Seq[String]
) {

def copy(
Expand All @@ -102,7 +110,43 @@ final class Settings[A <: RawModule] private[simulator] (
enableWavesAtTimeZero: Boolean = enableWavesAtTimeZero,
randomization: Randomization = randomization
) =
new Settings(verilogLayers, assertVerboseCond, printfCond, stopCond, plusArgs, enableWavesAtTimeZero, randomization)
new Settings(
verilogLayers,
assertVerboseCond,
printfCond,
stopCond,
plusArgs,
enableWavesAtTimeZero,
randomization,
libraries,
libraryPaths
)

def withLibraries(libraries: Seq[String]): Settings[A] =
new Settings(
verilogLayers,
assertVerboseCond,
printfCond,
stopCond,
plusArgs,
enableWavesAtTimeZero,
randomization,
libraries,
libraryPaths
)

def withLibraryPaths(libraryPaths: Seq[String]): Settings[A] =
new Settings(
verilogLayers,
assertVerboseCond,
printfCond,
stopCond,
plusArgs,
enableWavesAtTimeZero,
randomization,
libraries,
libraryPaths
)

private[simulator] def preprocessorDefines(
elaboratedModule: ElaboratedModule[A]
Expand Down Expand Up @@ -150,7 +194,9 @@ object Settings {
stopCond = Some(MacroText.NotSignal(get = _.reset)),
plusArgs = Seq.empty,
enableWavesAtTimeZero = false,
randomization = Randomization.random
randomization = Randomization.random,
libraries = Seq.empty,
libraryPaths = Seq.empty
)

/** Retun a default [[Settings]] for a [[RawModule]].
Expand Down Expand Up @@ -179,7 +225,9 @@ object Settings {
stopCond = None,
plusArgs = Seq.empty,
enableWavesAtTimeZero = false,
randomization = Randomization.random
randomization = Randomization.random,
libraries = Seq.empty,
libraryPaths = Seq.empty
)

/** Simple factory for construcing a [[Settings]] from arguments.
Expand All @@ -193,6 +241,12 @@ object Settings {
* @param printfCond a condition that guards printing of [[chisel3.printf]]s
* @param stopCond a condition that guards terminating the simulation (via
* `$fatal`) for asserts created from `circt_chisel_ifelsefatal` intrinsics
* @param libraries Names of libraries to include in simulation. Use this to
* provide implementations for DPI functions, for example. The simulator will
* resolve these libraries to concrete files using the `CHISELSIM_LIBS`
* environment variable and `chiselsim.libraries` Java property.
* @param libraryPaths Paths to libraries to include in simulation. Use this
* to provide implementations for DPI functions, for example.
* @return a [[Settings]] with the provided parameters set
*/
def apply[A <: RawModule](
Expand All @@ -210,7 +264,9 @@ object Settings {
stopCond = stopCond,
plusArgs = plusArgs,
enableWavesAtTimeZero = enableWavesAtTimeZero,
randomization = randomization
randomization = randomization,
libraries = Seq.empty,
libraryPaths = Seq.empty
)

}
36 changes: 36 additions & 0 deletions src/main/scala/chisel3/simulator/Simulator.scala
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,41 @@ trait Simulator[T <: Backend] {
}
Files.walkFileTree(Paths.get(workspace.primarySourcesPath), new DirectoryFinder)

// Collect a list of library names and paths from the environment. These
// will be used to resolve library names provided by the user. Environment
// variables take precedence over Java properties.
def parseLibraryMap(value: Option[String]): Map[String, String] = {
value
.getOrElse("")
.split(":")
.filter(_.nonEmpty)
.map { entry =>
entry.split("=", 2) match {
case Array(name, path) => name -> path
case _ =>
throw new IllegalArgumentException(
s"Invalid link library mapping `$entry`; expected `<name>=<path>`"
)
Comment on lines +274 to +279
Copy link
Contributor

Choose a reason for hiding this comment

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

This is fine but bonus points if you give an error with the bad input and a carat, e.g.

val filters = value.split(",")
filters.toList
// Add accumulating index to each filter for error reporting
.mapAccumulate(0) { case (idx, s) => (idx + 1 + s.length, (idx, s)) } // + 1 for removed ','
._2 // Discard accumulator
.map { case (idx, s) =>
WarningFilter.parse(s) match {
case Right(wf) => wf
case Left((jdx, msg)) =>
val carat = (" " * (idx + jdx)) + "^"
// Note tab before value and carat
throw new Exception(s"Failed to parse configuration: $msg\n $value\n $carat")
}
}
}

}
}
.toMap
}
val libraryMapFromProp = parseLibraryMap(sys.props.get("chiselsim.libraries"))
val libraryMapFromEnv = parseLibraryMap(sys.env.get("CHISELSIM_LIBS"))
Comment on lines +284 to +285
Copy link
Contributor

Choose a reason for hiding this comment

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

Usually these things are either or (and also either-or with being passed as arguments).

For example, you can set the CLASSPATH environment variable, but if you set it via -cp <classpath> that wins and the environment variable is ignored.

I'm not sure if we should do that here, but maybe? What do you think?

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 didn't know about the classpath behavior. My gut feeling is that since these are lists, we might as well combine them instead of having a single library provided via one mechanism effectively remove the ones provided in the other. That feels somewhat unexpected from a user point of view.

Copy link
Contributor

Choose a reason for hiding this comment

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

My intuition remains that overriding is more likely to be expected by users, but I'm also not going to sandbag on that because I agree that concatenation also feels reasonable.

val libraryMap = libraryMapFromProp ++ libraryMapFromEnv
val resolvedLibraryPaths = settings.libraries.map { name =>
libraryMap.getOrElse(
name,
throw new NoSuchElementException(
s"Link library `$name` not found. " +
"Set the `CHISELSIM_LIBS` environment variable or the " +
"`chiselsim.libraries` Java property to provide a list of " +
"`<name>=<path>` mappings separated by `:`. Available libraries: " +
s"[${libraryMap.keys.mkString(", ")}]"
)
)
}

val commonCompilationSettingsUpdated = commonSettingsModifications(
commonCompilationSettings.copy(
// Append to the include directorires based on what the
Expand All @@ -275,6 +310,7 @@ trait Simulator[T <: Backend] {
directoryFilter = commonCompilationSettings.directoryFilter.orElse(
settings.verilogLayers.shouldIncludeDirectory(elaboratedModule, workspace.primarySourcesPath)
),
linkLibraryPaths = commonCompilationSettings.linkLibraryPaths ++ resolvedLibraryPaths ++ settings.libraryPaths,
simulationSettings = commonCompilationSettings.simulationSettings.copy(
plusArgs = commonCompilationSettings.simulationSettings.plusArgs ++ settings.plusArgs,
enableWavesAtTimeZero =
Expand Down
1 change: 1 addition & 0 deletions src/test/resources/chisel3/simulator/linkLibA.c
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
void magicFuncA(int *result) { *result = 42; }
1 change: 1 addition & 0 deletions src/test/resources/chisel3/simulator/linkLibB.c
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
void magicFuncB(int *result) { *result = 1337; }
1 change: 1 addition & 0 deletions src/test/resources/chisel3/simulator/linkLibC.c
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
void magicFuncC(int *result) { *result = 9001; }
32 changes: 32 additions & 0 deletions src/test/scala-2/chiselTests/simulator/SimulatorSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package chiselTests.simulator
import chisel3._
import chisel3.layer.{block, Convention, Layer, LayerConfig}
import chisel3.simulator._
import chisel3.util.circt.dpi._
import org.scalatest.funspec.AnyFunSpec
import org.scalatest.matchers.must.Matchers
import org.scalatest.matchers.should.Matchers.convertToAnyShouldWrapper
Expand All @@ -15,6 +16,21 @@ class VerilatorSimulator(val workspacePath: String) extends Simulator[verilator.
val backendSpecificCompilationSettings = verilator.Backend.CompilationSettings.default
}

object SimulatorSpec {
class LinkLibraryTest extends RawModule {
val a = IO(Output(UInt(32.W)))
val b = IO(Output(UInt(32.W)))
val c = IO(Output(UInt(32.W)))
// The following DPI functions are implemented in
// `src/test/resources/chisel3/simulator/linkLib*.c`. The build system
// compiles these into shared libraries and makes them available to be linked
// into the simulator above.
a := RawUnclockedNonVoidFunctionCall("magicFuncA", UInt(32.W))(true.B)
b := RawUnclockedNonVoidFunctionCall("magicFuncB", UInt(32.W))(true.B)
c := RawUnclockedNonVoidFunctionCall("magicFuncC", UInt(32.W))(true.B)
}
}

class SimulatorSpec extends AnyFunSpec with Matchers {
describe("Chisel Simulator") {
it("runs GCD correctly") {
Expand Down Expand Up @@ -333,5 +349,21 @@ class SimulatorSpec extends AnyFunSpec with Matchers {
.result

}

it("supports link libraries provided by the build system") {
val simulator = new VerilatorSimulator("test_run_dir/simulator/LinkLibrary")
val settings = Settings
.defaultRaw[SimulatorSpec.LinkLibraryTest]
.withLibraries(Seq("linkLibA", "linkLibB"))
.withLibraryPaths(Seq(sys.env("LINKLIBC_FULL_PATH")))
val result = simulator
.simulate(new SimulatorSpec.LinkLibraryTest(), settings = settings) { module =>
import PeekPokeAPI._
val dut = module.wrapped
(dut.a.peek().litValue, dut.b.peek().litValue, dut.c.peek().litValue)
}
.result
assert(result === (42, 1337, 9001))
}
}
}
4 changes: 4 additions & 0 deletions svsim/package.mill
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ trait Svsim extends ChiselCrossModule with HasCommonOptions with ScalafmtModule
)
}

def compileMvnDeps = Seq(mvn"com.lihaoyi::unroll-annotation:0.3.0")

def scalacPluginMvnDeps = Seq(mvn"com.lihaoyi:::unroll-plugin:0.3.0")

object test extends SbtTests with TestModule.ScalaTest with ScalafmtModule {
def mvnDeps = Seq(v.scalatest, v.scalacheck)
}
Expand Down
5 changes: 4 additions & 1 deletion svsim/src/main/scala/Backend.scala
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package svsim

import com.lihaoyi.unroll
import java.io.File
import scala.util.matching.Regex

Expand Down Expand Up @@ -68,7 +69,9 @@ case class CommonCompilationSettings(
includeDirs: Option[Seq[String]] = None,
fileFilter: PartialFunction[File, Boolean] = PartialFunction.empty,
directoryFilter: PartialFunction[File, Boolean] = PartialFunction.empty,
simulationSettings: CommonSimulationSettings = CommonSimulationSettings.default
simulationSettings: CommonSimulationSettings = CommonSimulationSettings.default,
// @unroll is required to maintain binary compatibility
@unroll linkLibraryPaths: Seq[String] = Seq.empty
Copy link
Contributor

Choose a reason for hiding this comment

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

This is the greatest discovery of my life.

)
object CommonCompilationSettings {
object VerilogPreprocessorDefine {
Expand Down
2 changes: 2 additions & 0 deletions svsim/src/main/scala/vcs/Backend.scala
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,8 @@ final class Backend(
case Some(paths) => paths.flatMap(Seq("-y", _))
},

commonSettings.linkLibraryPaths.flatMap(lib => Seq("-sv_lib", lib)),

commonSettings.includeDirs match {
case None => Seq()
case Some(dirs) => dirs.map(dir => s"+incdir+$dir")
Expand Down
2 changes: 2 additions & 0 deletions svsim/src/main/scala/verilator/Backend.scala
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,8 @@ final class Backend(executablePath: String) extends svsim.Backend {
paths.foreach(p => addArg(Seq("-y", p)))
}

commonSettings.linkLibraryPaths.foreach(lib => addArg(Seq(lib)))

commonSettings.includeDirs.foreach { dirs =>
addArg(dirs.map(dir => s"+incdir+$dir"))
}
Expand Down