Add SvGenerationUtils.

Useful functions for interfacing Chisel with SV.

Change-Id: I6f08996d4017f38864eada9a34f6169525a58ecc
diff --git a/hdl/chisel/src/common/BUILD b/hdl/chisel/src/common/BUILD
index df98801..40605d1 100644
--- a/hdl/chisel/src/common/BUILD
+++ b/hdl/chisel/src/common/BUILD
@@ -123,6 +123,15 @@
     ],
 )
 
+chisel_library(
+    name = "sv_generation_utils",
+    srcs = [
+        "SvGenerationUtils.scala",
+    ],
+    visibility = ["//visibility:public"],
+    deps = [],
+)
+
 chisel_test(
     name = "common_test",
     srcs = [
@@ -208,6 +217,17 @@
     ],
 )
 
+chisel_test(
+    name = "sv_generation_utils_test",
+    srcs = [
+        "SvGenerationUtilsTest.scala",
+    ],
+    visibility = ["//visibility:public"],
+    deps = [
+        ":sv_generation_utils",
+    ],
+)
+
 chisel_library(
     name = "testing",
     srcs = [
diff --git a/hdl/chisel/src/common/SvGenerationUtils.scala b/hdl/chisel/src/common/SvGenerationUtils.scala
new file mode 100644
index 0000000..9d304a7
--- /dev/null
+++ b/hdl/chisel/src/common/SvGenerationUtils.scala
@@ -0,0 +1,62 @@
+// Copyright 2025 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package common
+
+import chisel3._
+import chisel3.reflect.DataMirror
+
+object GenerateInterface {
+  def apply(interface: Data, baseName: String = ""): String = {
+    def getLeafPortNames(name: String, data: Data): Seq[(String, Data)] = {
+      data match {
+        case _: Element => Seq((name, data))
+        case r: Record => {
+          r.elements.toList.reverse.map({ case (n, d) => {
+            getLeafPortNames(s"${name}_${n}", d)
+          }}).reduce(_ ++ _)
+        }
+        case v: Vec[_] => v.zipWithIndex.flatMap { case (d, i) =>
+          getLeafPortNames(s"${name}_${i}", d)
+        }
+      }
+    }
+
+    val leafs = getLeafPortNames(baseName, interface)
+    var ios: Seq[String] = Seq()
+    for ((leafName, leafData) <- leafs) {
+      val direction = DataMirror.directionOf(leafData) match {
+        case chisel3.ActualDirection.Input => "input "
+        case chisel3.ActualDirection.Output => "output"
+        case _ => "unknown"
+      }
+
+      val ioLine = leafData match {
+        case b: Bool => Some(s"$direction logic $leafName")
+        case u: UInt => Some(s"$direction logic [${u.getWidth-1}:0] $leafName")
+        case s: SInt => Some(s"$direction logic [${s.getWidth-1}:0] $leafName")
+        case c: Clock => Some(s"$direction logic $leafName")
+        case c: Reset => Some(s"$direction logic $leafName")
+        // Assume remaining element is a ChiselEnum
+        case e: Element =>
+            Some(s"$direction logic [${e.getWidth-1}:0] $leafName")
+        case _ => None
+      }
+
+      ios = ios ++ ioLine
+    }
+
+    ios.map("  " ++ _).mkString(",\n")
+  }
+}
diff --git a/hdl/chisel/src/common/SvGenerationUtilsTest.scala b/hdl/chisel/src/common/SvGenerationUtilsTest.scala
new file mode 100644
index 0000000..30114b9
--- /dev/null
+++ b/hdl/chisel/src/common/SvGenerationUtilsTest.scala
@@ -0,0 +1,169 @@
+// Copyright 2025 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package common
+
+import chisel3._
+import chisel3.util._
+import chisel3.simulator.scalatest.ChiselSim
+import org.scalatest.freespec.AnyFreeSpec
+
+class BasicModule extends Module {
+  val io = IO(new Bundle {
+    val in  = Input(UInt(32.W))
+    val out = Output(UInt(32.W))
+  })
+
+  io.out := io.in + 1.U
+}
+
+class ValidModule extends Module {
+  val io = IO(new Bundle {
+    val in  = Input(Valid(UInt(32.W)))
+    val out = Output(Valid(UInt(32.W)))
+  })
+
+  io.out := io.in.map(_ + 1.U)
+}
+
+object TrafficLight extends ChiselEnum {
+  val RED     = Value
+  val YELLOW  = Value
+  val GREEN   = Value
+}
+
+class ChiselEnumModule extends Module {
+  val io = IO(new Bundle {
+    val in  = Input(TrafficLight())
+    val out = Output(TrafficLight())
+  })
+  io.out := MuxLookup(io.in, TrafficLight.RED)(Seq(
+    TrafficLight.RED -> TrafficLight.GREEN,
+    TrafficLight.GREEN -> TrafficLight.YELLOW,
+    TrafficLight.YELLOW -> TrafficLight.RED,
+  ))
+}
+
+class VectorModule extends Module {
+  val io = IO(new Bundle {
+    val in  = Input(Vec(4, UInt(8.W)))
+    val out = Output(Vec(4, UInt(8.W)))
+  })
+  io.out := io.in
+}
+
+class KitchenSync extends Bundle {
+  val b = Bool()
+  val u = UInt(12.W)
+  val s = SInt(7.W)
+  val e = TrafficLight()
+  val v = Vec(3, new Bundle {
+    val nu = UInt(3.W)
+    val ns = SInt(2.W)
+  })
+}
+
+class KitchenSyncModule extends Module {
+  val io = IO(new Bundle {
+    val in  = Flipped(Decoupled(new KitchenSync()))
+    val out = Decoupled(new KitchenSync())
+  })
+  io.out <> Queue(io.in, 2)
+}
+
+class GenerateInterfaceSpec extends AnyFreeSpec with ChiselSim {
+    "BasicModule" in {
+        val expectedInterface =
+            """  input  logic [31:0] io_in,
+              |  output logic [31:0] io_out""".stripMargin
+
+        simulate(new BasicModule()) { dut =>
+            val interface = GenerateInterface(dut.io, "io")
+            assert(interface === expectedInterface)
+        }
+    }
+
+    "ValidModule" in {
+        val expectedInterface =
+            """  input  logic io_in_valid,
+              |  input  logic [31:0] io_in_bits,
+              |  output logic io_out_valid,
+              |  output logic [31:0] io_out_bits""".stripMargin
+
+        simulate(new ValidModule()) { dut =>
+            val interface = GenerateInterface(dut.io, "io")
+            assert(interface === expectedInterface)
+        }
+    }
+
+    "ChiselEnumModule" in {
+        val expectedInterface =
+            """  input  logic [1:0] io_in,
+              |  output logic [1:0] io_out""".stripMargin
+        simulate(new ChiselEnumModule()) { dut =>
+            val interface = GenerateInterface(dut.io, "io")
+            assert(interface === expectedInterface)
+        }
+    }
+
+    "VectorModule" in {
+        val expectedInterface =
+            """  input  logic [7:0] io_in_0,
+              |  input  logic [7:0] io_in_1,
+              |  input  logic [7:0] io_in_2,
+              |  input  logic [7:0] io_in_3,
+              |  output logic [7:0] io_out_0,
+              |  output logic [7:0] io_out_1,
+              |  output logic [7:0] io_out_2,
+              |  output logic [7:0] io_out_3""".stripMargin
+
+        simulate(new VectorModule()) { dut =>
+            val interface = GenerateInterface(dut.io, "io")
+            assert(interface === expectedInterface)
+        }
+    }
+
+    "KitchenSyncModule" in {
+        val expectedInterface =
+            """  output logic io_in_ready,
+              |  input  logic io_in_valid,
+              |  input  logic io_in_bits_b,
+              |  input  logic [11:0] io_in_bits_u,
+              |  input  logic [6:0] io_in_bits_s,
+              |  input  logic [1:0] io_in_bits_e,
+              |  input  logic [2:0] io_in_bits_v_0_nu,
+              |  input  logic [1:0] io_in_bits_v_0_ns,
+              |  input  logic [2:0] io_in_bits_v_1_nu,
+              |  input  logic [1:0] io_in_bits_v_1_ns,
+              |  input  logic [2:0] io_in_bits_v_2_nu,
+              |  input  logic [1:0] io_in_bits_v_2_ns,
+              |  input  logic io_out_ready,
+              |  output logic io_out_valid,
+              |  output logic io_out_bits_b,
+              |  output logic [11:0] io_out_bits_u,
+              |  output logic [6:0] io_out_bits_s,
+              |  output logic [1:0] io_out_bits_e,
+              |  output logic [2:0] io_out_bits_v_0_nu,
+              |  output logic [1:0] io_out_bits_v_0_ns,
+              |  output logic [2:0] io_out_bits_v_1_nu,
+              |  output logic [1:0] io_out_bits_v_1_ns,
+              |  output logic [2:0] io_out_bits_v_2_nu,
+              |  output logic [1:0] io_out_bits_v_2_ns""".stripMargin
+
+        simulate(new KitchenSyncModule()) { dut =>
+            val interface = GenerateInterface(dut.io, "io")
+            assert(interface === expectedInterface)
+        }
+    }
+}
\ No newline at end of file