Skip to content

Commit d6e835d

Browse files
jackkoenigmwachs5
authored andcommitted
Add support for printing hierarchical names in Verilog (%m) (chipsalliance#4820)
Co-authored-by: Megan Wachs <[email protected]>
1 parent a27fa12 commit d6e835d

File tree

7 files changed

+96
-31
lines changed

7 files changed

+96
-31
lines changed

core/src/main/scala/chisel3/Printable.scala

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -64,16 +64,21 @@ sealed abstract class Printable {
6464
}
6565
object Printable {
6666

67+
private[chisel3] def isNoArgSpecifier(c: Char): Boolean = c == '%' || c == 'm'
68+
6769
/** Pack standard printf fmt, args* style into Printable
6870
*/
6971
def pack(fmt: String, data: Data*): Printable = {
7072
val args = data.iterator
7173
// Error handling
7274
def carrotAt(index: Int) = (" " * index) + "^"
73-
def errorMsg(index: Int) =
74-
s"""| fmt = "$fmt"
75+
def errorMsg(index: Int) = {
76+
// Escape newlines because they mess up the error message
77+
val fmtEsc = fmt.replaceAll("\n", "\\\\n")
78+
s"""| fmt = "$fmtEsc"
7579
| ${carrotAt(index)}
7680
| data = ${data.mkString(", ")}""".stripMargin
81+
}
7782

7883
def checkArg(i: Int): Unit = {
7984
if (!args.hasNext) {
@@ -89,11 +94,11 @@ object Printable {
8994
while (iter < fmt.size) {
9095
// Encountered % which is either
9196
// 1. Describing a format specifier.
92-
// 2. Literal Percent
97+
// 2. %% or %m
9398
// 3. Dangling percent - most likely due to a typo - intended literal percent or forgot the specifier.
9499
// Try to give meaningful error reports
95100
if (fmt(iter) == '%') {
96-
if (iter != fmt.size - 1 && (fmt(iter + 1) != '%' && !fmt(iter + 1).isWhitespace)) {
101+
if (iter != fmt.size - 1 && (!isNoArgSpecifier(fmt(iter + 1)) && !fmt(iter + 1).isWhitespace)) {
97102
checkArg(iter)
98103
buf += fmt.substring(curr_start, iter)
99104
curr_start = iter
@@ -112,7 +117,7 @@ object Printable {
112117
throw new UnknownFormatConversionException(msg)
113118
}
114119

115-
// A literal percent - hence increment by 2.
120+
// %% or %m - hence increment by 2.
116121
else {
117122
iter += 2
118123
}
@@ -235,3 +240,9 @@ case object Percent extends Printable {
235240
final def unpack(ctx: Component)(implicit info: SourceInfo): (String, Iterable[String]) = ("%%", List.empty)
236241
final def unpackArgs: Seq[Bits] = List.empty
237242
}
243+
244+
/** Represents the hierarchical name in the Verilog (`%m`) */
245+
case object HierarchicalName extends Printable {
246+
final def unpack(ctx: Component)(implicit info: SourceInfo): (String, Iterable[String]) = ("%m", List.empty)
247+
final def unpackArgs: Seq[Bits] = List.empty
248+
}

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,10 @@ private[chisel3] object Converter {
2424
case PString(str) => (str.replaceAll("%", "%%"), List.empty)
2525
case format: FirrtlFormat =>
2626
("%" + format.specifier, List(format.bits.ref))
27-
case Name(data) => (data.ref.name, List.empty)
28-
case FullName(data) => (data.ref.fullName(ctx), List.empty)
29-
case Percent => ("%%", List.empty)
27+
case Name(data) => (data.ref.name, List.empty)
28+
case FullName(data) => (data.ref.fullName(ctx), List.empty)
29+
case Percent => ("%%", List.empty)
30+
case HierarchicalName => ("%m", List.empty)
3031
}
3132
}
3233

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -84,9 +84,10 @@ private[chisel3] object Serializer {
8484
case PString(str) => (str.replaceAll("%", "%%"), List.empty)
8585
case format: FirrtlFormat =>
8686
("%" + format.specifier, List(format.bits.ref))
87-
case Name(data) => (data.ref.name, List.empty)
88-
case FullName(data) => (data.ref.fullName(ctx), List.empty)
89-
case Percent => ("%%", List.empty)
87+
case Name(data) => (data.ref.name, List.empty)
88+
case FullName(data) => (data.ref.fullName(ctx), List.empty)
89+
case Percent => ("%%", List.empty)
90+
case HierarchicalName => ("%m", List.empty)
9091
}
9192
}
9293

core/src/main/scala/chisel3/package.scala

Lines changed: 28 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -229,31 +229,41 @@ package object chisel3 {
229229
@nowarn("msg=checkLengths in class StringContext is deprecated")
230230
def cf(args: Any*): Printable = {
231231

232-
// Handle literal %
233-
// Takes the part string -
234-
// - this is assumed to not have any format specifiers - already handled / removed before calling this function.
235-
// Only thing present is literal % if any which should ideally be with %%.
236-
// If not - then flag an error.
237-
// Return seq of Printables (either PString or Percent or both - nothing else
238-
def percentSplitter(s: String): Seq[Printable] = {
239-
if (s.isEmpty) Seq(PString(""))
240-
else {
241-
val pieces = s.split("%%").toList.flatMap { p =>
242-
if (p.contains('%')) throw new UnknownFormatConversionException("Un-escaped % found")
243-
// Wrap in PString and intersperse the escaped percentages
244-
Seq(Percent, PString(p))
232+
// Handle special escapes like %% and %m
233+
def escapeHandler(s: String): Seq[Printable] = {
234+
val pieces = mutable.ListBuffer.empty[Printable]
235+
var start = 0
236+
var end = 0
237+
while (end < s.length) {
238+
if (s(end) == '%') {
239+
val piece =
240+
if (end == s.length - 1) {
241+
throw new UnknownFormatConversionException("Trailing %")
242+
} else if (s(end + 1) == '%') {
243+
Percent
244+
} else if (s(end + 1) == 'm') {
245+
HierarchicalName
246+
} else {
247+
throw new UnknownFormatConversionException("Un-escaped %")
248+
}
249+
pieces += PString(s.substring(start, end))
250+
pieces += piece
251+
start = end + 2
252+
end = start
253+
} else {
254+
end += 1
245255
}
246-
if (pieces.isEmpty) Seq(Percent)
247-
else pieces.tail // Don't forget to drop the extra percent we put at the beginning
248256
}
257+
pieces += PString(s.substring(start, end))
258+
pieces.toList
249259
}
250260

251261
def extractFormatSpecifier(part: String): (Option[String], String) = {
252262
// Check if part starts with a format specifier (with % - disambiguate with literal % checking the next character if needed to be %)
253263
// In the case of %f specifier there is a chance that we need more information - so capture till the 1st letter (a-zA-Z).
254264
// Example cf"This is $val%2.2f here" - parts - Seq("This is ","%2.2f here") - the format specifier here is %2.2f.
255265
val endFmtIdx =
256-
if (part.length > 1 && part(0) == '%' && part(1) != '%') part.indexWhere(_.isLetter)
266+
if (part.length > 1 && part(0) == '%' && !Printable.isNoArgSpecifier(part(1))) part.indexWhere(_.isLetter)
257267
else -1
258268
val (fmt, rest) = part.splitAt(endFmtIdx + 1)
259269

@@ -301,10 +311,10 @@ package object chisel3 {
301311
case t => PString(fmt.getOrElse("%s").format(t))
302312

303313
}
304-
Seq(fmtArg) ++ percentSplitter(modP)
314+
Seq(fmtArg) ++ escapeHandler(modP)
305315
}
306316
}
307-
Printables(percentSplitter(parts.head) ++ pables)
317+
Printables(escapeHandler(parts.head) ++ pables)
308318
}
309319
}
310320

docs/src/explanations/printing.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,22 @@ printf(cf"myUInt = $myUInt%b") // myUInt = 100001
5757
printf(cf"myUInt = $myUInt%c") // myUInt = !
5858
```
5959

60+
#### Special values
61+
62+
There are special values you can include in your `cf` interpolated string:
63+
64+
* `HierarchicalName` (`%m`): The hierarchical name of the signal
65+
* `Percent` (`%%`): A literal `%`
66+
67+
```scala mdoc:compile-only
68+
printf(cf"hierarchical path = $HierarchicalName\n") // hierarchical path = <verilog.module.path>
69+
printf(cf"hierarchical path = %m\n") // equivalent to the above
70+
71+
printf(cf"100$Percent\n") // 100%
72+
printf(cf"100%%\n") // equivalent to the above
73+
```
74+
75+
6076
#### Aggregate data-types
6177

6278
Chisel provides default custom "pretty-printing" for Vecs and Bundles. The default printing of a Vec is similar to printing a Seq or List in Scala while printing a Bundle is similar to printing a Scala Map.
@@ -126,6 +142,7 @@ Chisel provides `printf` in a similar style to its C namesake. It accepts a doub
126142
| `%b` | binary number |
127143
| `%c` | 8-bit ASCII character |
128144
| `%%` | literal percent |
145+
| `%m` | hierarchical name |
129146

130147
It also supports a small set of escape characters:
131148

src/test/scala-2/chiselTests/PrintableSpec.scala

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,13 @@ package chiselTests
44

55
import chisel3._
66
import circt.stage.ChiselStage
7+
import chisel3.testing.scalatest.FileCheck
78
import org.scalactic.source.Position
89
import org.scalatest.flatspec.AnyFlatSpec
910
import org.scalatest.matchers.should.Matchers
1011

1112
/* Printable Tests */
12-
class PrintableSpec extends AnyFlatSpec with Matchers {
13+
class PrintableSpec extends AnyFlatSpec with Matchers with FileCheck {
1314
// This regex is brittle, it specifically finds the clock and enable signals followed by commas
1415
private val PrintfRegex = """\s*printf\(\w+, [^,]+,(.*)\).*""".r
1516
private val StringRegex = """([^"]*)"(.*?)"(.*)""".r
@@ -310,4 +311,16 @@ class PrintableSpec extends AnyFlatSpec with Matchers {
310311
generateAndCheck(new MyModule) { case Seq(Printf("\\\\ \\\\]", Seq())) =>
311312
}
312313
}
314+
315+
it should "support all legal format specifiers" in {
316+
class MyModule extends Module {
317+
val in = IO(Input(UInt(8.W)))
318+
printf(cf"$HierarchicalName $in%d $in%x $in%b $in%c %%\n")
319+
}
320+
ChiselStage
321+
.emitCHIRRTL(new MyModule)
322+
.fileCheck()(
323+
"""CHECK: printf(clock, UInt<1>(0h1), "%m %d %x %b %c %%\n", in, in, in, in)"""
324+
)
325+
}
313326
}

src/test/scala-2/chiselTests/Printf.scala

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ package chiselTests
44

55
import chisel3._
66
import circt.stage.ChiselStage
7+
import chisel3.testing.scalatest.FileCheck
78
import org.scalatest.flatspec.AnyFlatSpec
89
import org.scalatest.matchers.should.Matchers
910

@@ -38,7 +39,7 @@ class ScopeTesterModule extends Module {
3839
val wp = cf"$w"
3940
}
4041

41-
class PrintfSpec extends AnyFlatSpec with Matchers {
42+
class PrintfSpec extends AnyFlatSpec with Matchers with FileCheck {
4243
"A printf with a single argument" should "elaborate" in {
4344
val chirrtl = ChiselStage.emitCHIRRTL(new SinglePrintfTester)
4445

@@ -72,4 +73,15 @@ class PrintfSpec extends AnyFlatSpec with Matchers {
7273
}
7374
}
7475
}
76+
"printf" should "support all legal format specifiers" in {
77+
class MyModule extends Module {
78+
val in = IO(Input(UInt(8.W)))
79+
printf("%m %d %x %b %c %%\n", in, in, in, in)
80+
}
81+
ChiselStage
82+
.emitCHIRRTL(new MyModule)
83+
.fileCheck()(
84+
"""CHECK: printf(clock, UInt<1>(0h1), "%m %d %x %b %c %%\n", in, in, in, in)"""
85+
)
86+
}
7587
}

0 commit comments

Comments
 (0)