Skip to content

Commit d660f47

Browse files
Add AutoBlackbox (#4495)
* feat: add SlangUtils to extract metadata from Verilog * feat: add AutoBlackBox * feat: allow overriding source locator in module Signed-off-by: unlsycn <[email protected]> * feat: add AutoBlackBoxSpec Signed-off-by: unlsycn <[email protected]> * ci: build and cache slang in workflow Signed-off-by: unlsycn <[email protected]> --------- Signed-off-by: unlsycn <[email protected]> Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
1 parent 3a7ba9a commit d660f47

File tree

9 files changed

+239
-1
lines changed

9 files changed

+239
-1
lines changed
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
name: Build Slang
2+
3+
inputs:
4+
version:
5+
description: 'version to install'
6+
required: false
7+
default: '7.0'
8+
9+
runs:
10+
using: composite
11+
steps:
12+
- id: cache-slang
13+
uses: actions/cache@v4
14+
with:
15+
path: /usr/local/bin/slang
16+
key: slang-${{ runner.os }}-${{ inputs.version }}
17+
- name: Build from source
18+
shell: bash
19+
if: steps.cache-slang.outputs.cache-hit != 'true'
20+
run: |
21+
cd /tmp
22+
wget -q https://github.com/MikePopoloski/slang/archive/refs/tags/v${{ inputs.version }}.tar.gz -O slang.tar.gz
23+
tar xf slang.tar.gz
24+
cd slang-${{ inputs.version }}
25+
cmake -B build
26+
cmake --build build
27+
cd build && ctest --output-on-failure
28+
cp bin/slang /usr/local/bin/slang
29+
slang || true

.github/workflows/ci-circt-nightly.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ jobs:
5151
jvm: [8]
5252
scala: ["2.13.15"]
5353
espresso: ["2.4"]
54+
slang: ["7.0"]
5455
circt: ["nightly"]
5556
ref: ${{ fromJSON(needs.determine-branch.outputs.branches) }}
5657
uses: ./.github/workflows/test.yml
@@ -59,6 +60,7 @@ jobs:
5960
jvm: ${{ matrix.jvm }}
6061
scala: ${{ matrix.scala }}
6162
espresso: ${{ matrix.espresso }}
63+
slang: ${{ matrix.slang }}
6264
circt: ${{ matrix.circt }}
6365
ref: ${{ matrix.ref }}
6466

.github/workflows/ci.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,14 @@ jobs:
2121
jvm: [8]
2222
scala: ["2.13.15"]
2323
espresso: ["2.4"]
24+
slang: ["7.0"]
2425
uses: ./.github/workflows/test.yml
2526
with:
2627
system: ${{ matrix.system }}
2728
jvm: ${{ matrix.jvm }}
2829
scala: ${{ matrix.scala }}
2930
espresso: ${{ matrix.espresso }}
31+
slang: ${{ matrix.slang }}
3032

3133
# Sentinel job to simplify how we specify which checks need to pass in branch
3234
# protection and in Mergify. This job checks that all matrix jobs were

.github/workflows/test.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@ on:
2323
default: '2.4'
2424
required: true
2525
type: string
26+
slang:
27+
description: 'The slang version to use'
28+
default: '7.0'
29+
required: true
30+
type: string
2631
circt:
2732
description: 'The CIRCT version to use (must be a valid release tag, "nightly", or "version-file").
2833
Leave off (or empty) to let Chisel manage the CIRCT version'
@@ -49,6 +54,10 @@ jobs:
4954
uses: ./.github/workflows/install-espresso
5055
with:
5156
version: ${{ inputs.espresso }}
57+
- name: Install Slang
58+
uses: ./.github/workflows/build-slang
59+
with:
60+
version: ${{ inputs.slang }}
5261
- name: Setup Java
5362
uses: actions/setup-java@v4
5463
with:

plugin/src/main/scala/chisel3/internal/plugin/ChiselComponent.scala

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -244,7 +244,10 @@ class ChiselComponent(val global: Global, arguments: ChiselPluginArguments)
244244
super.transform(tree)
245245
}
246246
// Also look for Module class definitions for inserting source locators
247-
case module: ClassDef if isAModule(module.symbol) && !module.mods.hasFlag(Flag.ABSTRACT) =>
247+
case module: ClassDef
248+
if isAModule(module.symbol) && !module.mods.hasFlag(
249+
Flag.ABSTRACT
250+
) && !isOverriddenSourceLocator(module.impl) =>
248251
val path = SourceInfoFileResolver.resolve(module.pos.source)
249252
val info = localTyper.typed(q"chisel3.experimental.SourceLine($path, ${module.pos.line}, ${module.pos.column})")
250253

plugin/src/main/scala/chisel3/internal/plugin/ChiselUtils.scala

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,5 +61,8 @@ private[plugin] trait ChiselOuterUtils { outerSelf: TypingTransformers =>
6161
def isNullaryMethodNamed(name: String, defdef: DefDef): Boolean =
6262
defdef.name.decodedName.toString == name && defdef.tparams.isEmpty && defdef.vparamss.isEmpty
6363

64+
def isOverriddenSourceLocator(impl: Template) = impl.body
65+
.exists(m => m.symbol != null && m.symbol.name.toString == "_sourceInfo")
66+
6467
}
6568
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
3+
package chisel3.util.experimental
4+
5+
import chisel3._
6+
7+
import scala.collection.immutable.SeqMap
8+
import chisel3.experimental.SourceInfo
9+
10+
class AutoBlackBox(
11+
val verilog: String,
12+
val signalFilter: String => Boolean = _ => true
13+
)(
14+
implicit val __sourceInfo: SourceInfo)
15+
extends FixedIOExtModule(
16+
ioGenerator = new AutoBundleFromVerilog(
17+
SeqMap.from(SlangUtils.verilogModuleIO(SlangUtils.getVerilogAst(verilog)))
18+
)(signalFilter)
19+
) {
20+
override def desiredName = SlangUtils.verilogModuleName(SlangUtils.getVerilogAst(verilog))
21+
override def _sourceInfo = __sourceInfo
22+
}
23+
24+
class AutoBundleFromVerilog(
25+
allElements: SeqMap[String, Data]
26+
)(signalFilter: String => Boolean)
27+
extends Record {
28+
val elements: SeqMap[String, Data] = allElements.filter(n => signalFilter(n._1)).map {
29+
case (k, v) =>
30+
k -> chisel3.reflect.DataMirror.internal.chiselTypeClone(v)
31+
}
32+
def apply(data: String) = elements.getOrElse(
33+
data,
34+
throw new ChiselException(
35+
s"$data not found in Verilog IO: \n ${allElements.keys.mkString("\n")}"
36+
)
37+
)
38+
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
3+
package chisel3.util.experimental
4+
5+
import chisel3._
6+
import chisel3.experimental.{Analog, DoubleParam, IntParam, Param, StringParam}
7+
import ujson.Value.Value
8+
9+
import scala.collection.immutable.SeqMap
10+
11+
object SlangUtils {
12+
13+
/** Use Slang to parse Verilog into Json AST. */
14+
def getVerilogAst(verilog: String): Value = {
15+
val astFile = os.temp()
16+
os.proc(
17+
"slang",
18+
os.temp(verilog),
19+
"--single-unit",
20+
"--ignore-unknown-modules",
21+
"--compat",
22+
"vcs",
23+
"--ast-json",
24+
astFile
25+
).call()
26+
ujson.read(os.read(astFile))
27+
}
28+
29+
/** Extract IO from Verilog AST. */
30+
def verilogModuleIO(verilogAst: Value): SeqMap[String, Data] = {
31+
verilogAst.obj("design").obj("members").arr.flatMap {
32+
case value: ujson.Obj if value.value("kind").strOpt.contains("Instance") =>
33+
value.value("body").obj.value("members").arr.flatMap {
34+
case value: ujson.Obj if value.value("kind").strOpt.contains("Port") =>
35+
Some(
36+
value.value("name").str -> {
37+
val width: Int =
38+
"""logic\[(?<MSB>\d+):(?<LSB>\d+)\]""".r
39+
.findFirstMatchIn(value.value("type").str) match {
40+
case Some(m) =>
41+
m.group("MSB").toInt - m.group("LSB").toInt + 1
42+
case None =>
43+
value.value("type").str match {
44+
case "logic" => 1
45+
case _ =>
46+
throw new ChiselException(
47+
s"Unhandled type ${value.value("type").str}"
48+
)
49+
}
50+
}
51+
52+
value.value("direction").str match {
53+
case "In" => Input(UInt(width.W))
54+
case "Out" => Output(UInt(width.W))
55+
case "InOut" => Analog(width.W)
56+
}
57+
}
58+
)
59+
case _ => None
60+
}
61+
case _ => None
62+
}
63+
}.to(collection.immutable.SeqMap)
64+
65+
/** Extract module name from Verilog AST. */
66+
def verilogModuleName(verilogAst: Value): String = {
67+
val names = verilogAst.obj("design").obj("members").arr.flatMap {
68+
case value: ujson.Obj if value.value("kind").strOpt.contains("Instance") =>
69+
Some(value.value("name").str)
70+
case _ => None
71+
}
72+
require(names.length == 1, "only support one verilog module currently")
73+
names.head
74+
}
75+
76+
/** Extract module parameter from Verilog AST. */
77+
def verilogParameter(verilogAst: Value): Seq[(String, Param)] = {
78+
verilogAst.obj("design").obj("members").arr.flatMap {
79+
case value: ujson.Obj if value.value("kind").strOpt.contains("Instance") =>
80+
value.value("body").obj.value("members").arr.flatMap {
81+
case value: ujson.Obj if value.value("kind").strOpt.contains("Parameter") =>
82+
Some(value.value("name").str -> (value.value("type").str match {
83+
case "real" => DoubleParam(value.value("value").str.toDouble)
84+
case "string" =>
85+
StringParam(
86+
value.value("value").str.stripPrefix("\"").stripSuffix("\"")
87+
)
88+
case "int" => IntParam(BigInt(value.value("value").str.toInt))
89+
case lit =>
90+
throw new ChiselException(
91+
s"unsupported literal: $lit in\n ${ujson.reformat(value, 2)}"
92+
)
93+
}))
94+
case _ => None
95+
}
96+
case _ => None
97+
}
98+
}.toSeq
99+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
3+
package chiselTests.experimental
4+
5+
import chisel3.util.experimental.AutoBlackBox
6+
import chiselTests.ChiselFlatSpec
7+
import circt.stage.ChiselStage
8+
import chisel3.experimental.hierarchy.Instantiate
9+
import chisel3.experimental.SourceLine
10+
11+
class AutoBlackBoxSpec extends ChiselFlatSpec {
12+
"AutoBlackBox" should "generate IO, module name and parameter" in {
13+
assert(
14+
ChiselStage
15+
.emitCHIRRTL(
16+
new chisel3.Module {
17+
implicit val info = SourceLine("Foo.scala", 1, 2)
18+
Instantiate(
19+
new AutoBlackBox(
20+
"""module BlackBoxPassthrough
21+
|#(
22+
|parameter int width = 2,
23+
|parameter real pi = 3.14 + 1,
24+
|parameter string str = "abc"
25+
|)
26+
|(
27+
| input [width-1:0] in,
28+
| output [width-1:0] out,
29+
| inout VDD,
30+
| inout VSS
31+
|);
32+
| assign out = in;
33+
|endmodule
34+
|""".stripMargin,
35+
{
36+
case "VDD" => false
37+
case "VSS" => false
38+
case _ => true
39+
}
40+
)
41+
)
42+
}
43+
)
44+
.contains(
45+
"""extmodule BlackBoxPassthrough : @[Foo.scala 1:2]
46+
| output out : UInt<2>
47+
| input in : UInt<2>
48+
| defname = BlackBoxPassthrough
49+
|""".stripMargin
50+
)
51+
)
52+
}
53+
}

0 commit comments

Comments
 (0)