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
14 changes: 14 additions & 0 deletions .claude/commands/bugfix.md
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,20 @@ width as ONE named operation whose body does the whole calculation in `compileti
Naming those operations (`CLog2P1`, `ArithMaxWidth`, `PartSelectHigh`, `RangeWidth`) is worth doing
for its own sake, and it makes the guard-once rule visible at each site.

Even the single named fold has one context it cannot survive: a `Check2.CheckNUB[HI, HighIdx[W,
L]]` bound routes the application through `UBound.Aux` resolution, where the guard's reduction is
CONTEXT-DEPENDENT — it can collapse to `Int` inside the implicit search while the same application
reduces to a literal at the summon site, so the found candidate's inferred type no longer conforms
to the required one and the user sees a raw given-mismatch dump (issue #488), or the op fails to
resolve at all. The tell: `Found: given_CheckNUB_Wide...[..., T2 = Int, ...]` against
`Required: ...CheckNUB[..., HighIdx[(8 : Int), (2 : Int)]]` — the fold collapsed on one side and
not the other. The robust spelling moves the literal-vs-wide decision from the guard to GIVEN
prioritization: a type class (`IntP.HighIdxOf`) whose high-priority instance computes the bound in
raw `compiletime.ops` over `Int & Singleton` args and whose low-priority instance answers `Int`,
summoned in a FIRST using group so the check in the second group receives a plain, already-decided
type parameter. This is also what let the three bits range-selection givens collapse into one:
the H-form existed only to dodge the fold inside `UBound`.

### Weakening a type does not break values, it deletes diagnostics

Making the type level say less is safe for the generated hardware, because the IR carries the real
Expand Down
46 changes: 44 additions & 2 deletions .claude/commands/verilog-to-dfhdl.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,15 @@ automatically (the emitted parent shows `child_i_clk = wb_clk`). Beyond the per-
Use the top's names as the global default; override the internals per-module.
- **A clock-only annotation removes the reset.** On an `RTDesign` *with* registers, annotating only
`@hw.constraints.timing.clock(portName = "...")` (no reset annotation) suppresses the default
reset entirely; the register `init`s emit as **power-up only** (`logic r = 1'b0;`, no `if(rst)`).
This is how you port a no-reset module (pipeline, RAM).
reset entirely; the register `init`s emit as **power-up only** (an `initial` block, no `rst_l` port
and no `if(rst)` arm). This is how you define a domain with an init and no reset signal, i.e. how
you port a no-reset module (pipeline, RAM). It is *not* a bug, and it is silent, so when you
rename a clock port on a module that **does** reset, restate `@..reset` alongside it or the flop
quietly becomes reset-less:
```scala
@timing.clock(portName = "rawclk") // alone: no reset
@timing.reset(mode = _.async, active = _.low, portName = "rst_l") // keep the reset
```
- **Annotation ⇒ auto-reset.** A `@..reset` annotation synchronously resets every register with a
real `init` to that init (`if (rst) r <= init;`). A Verilog `if(rst) r <= RESET_VAL` mux folds
straight into `init RESET_VAL` - drop the explicit mux.
Expand All @@ -113,6 +120,41 @@ automatically (the emitted parent shows `child_i_clk = wb_clk`). Beyond the per-
`clk.actual` reads it as a `Bit` (needed to drive a derived clock from the root clock, as an
ungated ICG does). What does collide is a *non-magnet* port whose name shadows the magnet's, so
keep a data port off the names `clk`/`rst` ("Unsupported read-to-read connection").
- **Renaming a child's clock port works, and the parent binds it correctly.**
`@timing.clock(portName = "rawclk")` on a child emits `input wire logic rawclk`, and the parent
connects **its own** clock to it (`assign f_rawclk = clk;`) because the magnet matches by domain,
not by name. This is how you port a cell whose clock input the baseline calls something else.
The domain propagates *down*, so a grandchild instantiated inside that cell is emitted with
`rawclk` too, while the same class instantiated from an ordinary `clk` design is emitted with
`clk`.
<!-- USER-GUIDE DOC GAP: the two sentences above are general language behaviour (portName
rename + magnet binds by domain, not name + downward domain propagation) and belong in
docs/user-guide/design-domains/index.md, linked from here. Kept inline for now because
they are inseparable from the three emitter traps below. -->
Two traps come with it, both found on VeeR's `rvdff_fpga`:
- **Keep a renamed-clock design childless.** If it instantiates anything, the emission is invalid
SystemVerilog: duplicated `rst_l_0`/`rst_l_1` ports against a `.rst_l` connection, plus
`assign <ModuleTypeName>.<child>.clk = clk;` - a hierarchical assign through the module *type*
name. It also surfaces at the parent as a bogus `Found multiple connections write to the same
variable/port <inst>_clk`, once the parent instantiates a same-domain sibling. Both faces
vanish when the cell has no children, so write the leaf logic directly instead of wrapping a
child. It elaborates clean either way, so **run the emitted files through slang** after
renaming a clock port.
- **`val clk = Clk <> IN` silently beats `portName`.** In a design annotated
`@timing.clock(portName = "rawclk")`, adding `val clk = Clk <> IN` makes *that* the domain
clock: `rawclk` disappears from the port list entirely and the registers clock on whatever the
parent wires to `clk`. No diagnostic. So a cell that takes two clock inputs and flops on the
*renamed* one must declare the other as a plain `Bit <> IN` - which is safe, and the magnet
does **not** claim it despite the name:
```scala
@timing.clock(portName = "rawclk") // rvdff_fpga, FPGA arm
@timing.reset(mode = _.async, active = _.low, portName = "rst_l")
class rvdff_fpga(val WIDTH: Int <> CONST = 1) extends RTDesign:
val clk = Bit <> IN // the baseline's dead clock input
val dout = Bits(WIDTH) <> OUT.REG init all(0)
if (clken) dout.din := din // gold: rvdffs (.clk(rawclk), .en(clken), .*)
// parent emits: assign f_rawclk = clk; (root clock) assign f_clk = <derived>; (dead input)
```

## Memories and `initFile`

Expand Down
2 changes: 1 addition & 1 deletion benchmarks
Submodule benchmarks updated 63 files
+16 −0 README.md
+1 −0 common/BenchRun.scala
+1 −0 common/BenchTable.scala
+1 −0 common/BenchUtil.scala
+1 −0 common/Verilator.scala
+1 −0 common/VerilatorThreadSweep.scala
+1 −0 protocol_engine/ProtocolEngine.scala
+1 −0 protocol_engine/ProtocolEngineBench.scala
+1 −0 protocol_engine/verilator/bench_protocol_engine.cpp
+1 −1 serv/ServBench.scala
+1 −1 serv/ServantHello.scala
+1 −1 serv/ServantHelloMini.scala
+1 −1 serv/ServantPhil.scala
+1 −1 serv/config.scala
+1 −0 serv/serv_alu.scala
+1 −0 serv/serv_bufreg.scala
+1 −0 serv/serv_bufreg2.scala
+1 −0 serv/serv_csr.scala
+1 −0 serv/serv_ctrl.scala
+1 −0 serv/serv_decode.scala
+1 −0 serv/serv_immdec.scala
+1 −0 serv/serv_mem_if.scala
+1 −0 serv/serv_rf_if.scala
+1 −0 serv/serv_rf_ram.scala
+1 −0 serv/serv_rf_ram_if.scala
+1 −0 serv/serv_state.scala
+1 −0 serv/serv_top.scala
+1 −0 serv/servant.scala
+1 −0 serv/servant_gpio.scala
+1 −0 serv/servant_mux.scala
+1 −0 serv/servant_ram.scala
+1 −1 serv/servant_sim.scala
+1 −0 serv/servant_timer.scala
+1 −0 serv/servile.scala
+1 −0 serv/servile_arbiter.scala
+1 −0 serv/servile_mux.scala
+1 −1 serv/uart_decoder.scala
+1 −1 serv/verilator/bench_serv.cpp
+1 −0 sha_farm/SHAFarm.scala
+1 −0 sha_farm/ShaFarmBench.scala
+2 −0 sha_farm/verilator/bench_sha.cpp
+16 −0 veer_eh1/LICENSE-VeeR-EH1
+13 −1 veer_eh1/README.md
+2 −1 veer_eh1/beh_lib.scala
+20 −1 veer_eh1/clk_domains.scala
+2 −1 veer_eh1/config.scala
+2 −1 veer_eh1/dec_gpr_ctl.scala
+11 −3 veer_eh1/defines.scala
+16 −4 veer_eh1/globals.scala
+2 −1 veer_eh1/lsu_clkdomain.scala
+213 −0 veer_eh1/lsu_dccm_ctl.scala
+2 −1 veer_eh1/lsu_trigger.scala
+2 −1 veer_eh1/rvbradder.scala
+2 −1 veer_eh1/rvecc_decode.scala
+2 −1 veer_eh1/rvecc_encode.scala
+2 −1 veer_eh1/rveven_paritycheck.scala
+2 −1 veer_eh1/rveven_paritygen.scala
+2 −1 veer_eh1/rvlsadder.scala
+2 −1 veer_eh1/rvmaskandmatch.scala
+2 −1 veer_eh1/rvrangecheck.scala
+2 −1 veer_eh1/rvsyncss.scala
+2 −1 veer_eh1/rvtwoscomp.scala
+2 −1 veer_eh1/veer_types.scala
2 changes: 1 addition & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ val oslibVersion = "0.11.8"
val scallopVersion = "6.0.0"
val upickleVersion = "4.4.3"
val scalapptainerVersion = "0.5.4"
val factumVersion = "0.2.0"
val factumVersion = "0.3.0"

inThisBuild(
List(
Expand Down
69 changes: 69 additions & 0 deletions compiler/ir/src/main/scala/dfhdl/compiler/ir/DB.scala
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,62 @@ final case class DB private (
List(top -> List())
).reverse

/** The base name (no extension) of the HDL file each design is emitted into.
*
* A file is a DECLARATION's HDL output, not a design's: designs group by their `dclMeta`
* namespace and position, so one `class Bar` that elaboration specialized into several distinct
* designs (`Bar_0`, `Bar_1`, ...) emits ONE `Bar` file holding all of them, while same-named
* declarations from elsewhere keep files of their own. The declaration's name is recovered from
* the emitted names of the designs sharing it: their longest common prefix, cut back to its last
* `_`, which is the separator every duplication suffix is appended behind (`Bar_0`/`Bar_1` ->
* `Bar`, `Adder_8_0`/`Adder_8_1` -> `Adder_8`, and the instance-named clones `ReduplicateDesign`
* makes, `Foo_a`/`Foo_b` -> `Foo`). Reading the suffix off the GROUP rather than off one name is
* what tells an enumerated `Bar_0` apart from a declared `Adder_8`: a design that is the only
* one of its declaration was never suffixed, and keeps its whole name.
*
* Names are then made unique CASE-INSENSITIVELY (these are file names, and the output must be
* the same on every operating system), earlier designs winning: a lone design keeps its emitted
* name, which `UniqueDesigns` already made case-insensitively unique, so the fallback is what a
* recovered declaration name colliding with one of those takes (a `cipher` sub-design under a
* `Cipher` top stays in `cipher_0`, alongside `Cipher`).
*
* Shared by the emission (`Printer.designFileGroups`) and by anything that has to name the file
* a design ended up in, such as Verilator's per-file lint waivers.
*/
lazy val designFileNameMap: Map[DFDesignBlock, String] =
val byDeclaration = designMemberList.map(_._1).groupByOrdered { d =>
// an UNKNOWN position (a synthesized design, declared in no Scala source) identifies no
// declaration, so such a design groups only with itself
if (d.dclMeta.position.isUnknown) Left(d)
else Right((d.dclMeta.namespace, d.dclMeta.position))
}
val taken = mutable.Set.empty[String]
byDeclaration.flatMap { (_, designs) =>
val preferred =
if (designs.sizeIs > 1) commonDclNameStem(designs) else designs.head.dclName
var fileName = preferred
var idx = 0
while (!taken.add(fileName.toLowerCase))
fileName = s"${preferred}_$idx"
idx += 1
designs.map(_ -> fileName)
}.toMap
end designFileNameMap

// The stem the emitted names of `designs` were all suffixed from: their longest common prefix,
// cut back to its last `_` (the separator a duplication suffix is appended behind) and stripped
// of it. Falls back to the first name whole where there is no such prefix, i.e. where the names
// do not actually share a suffixed stem.
private def commonDclNameStem(designs: List[DFDesignBlock]): String =
val first = designs.head.dclName
val prefixLen = designs.view.map(_.dclName).foldLeft(first.length) { (len, name) =>
var i = 0
while (i < len && i < name.length && first(i) == name(i)) i += 1
i
}
val sepIdx = first.take(prefixLen).lastIndexOf('_')
if (sepIdx > 0) first.take(sepIdx) else first

// holds a hash table that lists members of each owner block. The member list order is maintained.
lazy val designMemberTable: Map[DFDesignBlock, List[DFMember]] =
Map(designMemberList*)
Expand Down Expand Up @@ -2515,6 +2571,19 @@ final case class DB private (
if (newMembers == members) this else this.update(members = newMembers)
end canonicalForm

// Whether every elaboration-loaded external init file (`SourceType.InitFile`, recorded with its
// loaded contents by `initFile`), here and in the sub-DBs, still reads back the contents this DB
// was elaborated with. The re-read goes through the same resolution elaboration used (classpath
// resource first, filesystem path second), and a missing or unreadable file counts as changed.
// Elaboration caches consult this to reject a stale entry: the design load gate before adopting
// a sub-design entry, and the DFApp elaborate step before accepting a whole-design cache hit.
def initFilesUnchanged: Boolean =
(srcFiles.view ++ subDBs.valuesIterator.flatMap(_.srcFiles)).forall {
case SourceFile(SourceOrigin.External, SourceType.InitFile, path, contents) =>
InitFileFormat.readInitFileContentsOpt(path).contains(contents)
case _ => true
}

end DB

object DB:
Expand Down
50 changes: 41 additions & 9 deletions compiler/ir/src/main/scala/dfhdl/compiler/ir/InitFileFormat.scala
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,13 @@ enum InitFileUndefinedValue derives CanEqual, ReadWriter:

object InitFileFormat:
import InitFileFormat.*
def readInitFile(
fileName: String,
fileFormat: InitFileFormat,
arrLen: Int,
dataWidth: Int,
undefinedValue: InitFileUndefinedValue
): Vector[(BitVector, BitVector)] =

/** Reads the raw contents of an init file, resolved as a classpath resource first and a
* filesystem path second (line endings normalized to `\n`). This resolution is the identity of
* the file: cache validation re-reads through it and compares contents (see
* `readInitFileContentsOpt`).
*/
def readInitFileContents(fileName: String): String =
val source =
try Source.fromResource(fileName)
catch
Expand All @@ -34,8 +34,29 @@ object InitFileFormat:
throw new IllegalArgumentException(
s"Init file not found: $fileName\nmake sure either to place the file in your Scala project resource folder or provide a proper relative/absolute path."
)
try source.getLines().mkString("\n")
finally source.close()
end readInitFileContents

/** As `readInitFileContents`, but None for a file that cannot be found or read. Serves cache
* validation, where an unreadable file means a stale entry (a miss) rather than an error: the
* live elaboration that follows raises the proper user-facing error.
*/
def readInitFileContentsOpt(fileName: String): Option[String] =
try Some(readInitFileContents(fileName))
catch case scala.util.control.NonFatal(_) => None

val fileContents = source.getLines().mkString("\n")
/** Parses already-read init file contents (see `readInitFileContents`; `fileName` is for error
* reporting and `Auto` format detection only).
*/
def parseInitFile(
fileName: String,
fileContents: String,
fileFormat: InitFileFormat,
arrLen: Int,
dataWidth: Int,
undefinedValue: InitFileUndefinedValue
): Vector[(BitVector, BitVector)] =
val detectedFormat = fileFormat match
case Auto => detectAutoFormat(fileName, fileContents, dataWidth)
case _ => fileFormat
Expand All @@ -55,7 +76,18 @@ object InitFileFormat:
s"Init file error detected in $detectedFormat formatted ${fileName}:$lineNum\n$msg"
)
end try
end readInitFile
end parseInitFile

def readInitFile(
fileName: String,
fileFormat: InitFileFormat,
arrLen: Int,
dataWidth: Int,
undefinedValue: InitFileUndefinedValue
): Vector[(BitVector, BitVector)] =
parseInitFile(
fileName, readInitFileContents(fileName), fileFormat, arrLen, dataWidth, undefinedValue
)

private val verilogCommentPattern = """//.*|/\*.*?\*/""".r
private val validBinPattern =
Expand Down
4 changes: 4 additions & 0 deletions compiler/ir/src/main/scala/dfhdl/compiler/ir/SourceFile.scala
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ enum SourceType derives CanEqual, ReadWriter:
case BlackBox
case GlobalDef
case DFHDLDef
// An external data file loaded during elaboration (`initFile` memory contents), recorded with
// the loaded contents under `SourceOrigin.External`. Elaboration caches re-read the file and
// reject an entry whose file has since changed (see `DB.initFilesUnchanged`).
case InitFile
case Tool(toolName: String, srcType: String)

enum SourceOrigin derives CanEqual, ReadWriter:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,11 @@ final case class SubDesignEntry(
db.members.map(memberMap),
newRefTable.view.mapValues(t => memberMap.getOrElse(t, t)).toMap,
db.globalTags,
Nil
// the entry's recorded source files (external init files) stay with the adopted design:
// the final assembly emits them as the design's sub-DB content, so a whole-design cache
// (the DFApp elaborate step) can re-validate them even when this design was never
// elaborated live in the storing run of THAT cache
db.srcFiles
)
end cloneForAdoption
end SubDesignEntry
Expand Down
72 changes: 59 additions & 13 deletions compiler/ir/src/main/scala/dfhdl/compiler/printing/Printer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -661,22 +661,22 @@ trait Printer
dfhdlSourceFile,
packageSourceFiles,
globalSourceFile,
designPrinters.view
designFileGroups(
// A foreign IP supplies its own HDL wrapper as a bundled resource (copied into the project
// at commit), so DFHDL must not generate an HDL file for it (that would duplicate the
// wrapper module/entity).
.filterNot { case (block, _) => block.isForeignIPBlackbox }
.map { case (block, p) =>
val sourceType = block.instMode match
case _: DFDesignBlock.InstMode.BlackBox => SourceType.BlackBox
case _ => SourceType.Design
SourceFile(
SourceOrigin.Compiled,
sourceType,
hdlFolderName + separatorChar + designFileName(block.dclName),
formatCode(p.csFile(block), withColor = false)
)
}
designPrinters.filterNot { case (block, _) => block.isForeignIPBlackbox }
).map { case (fileName, group) =>
val sourceType = group.head._1.instMode match
case _: DFDesignBlock.InstMode.BlackBox => SourceType.BlackBox
case _ => SourceType.Design
SourceFile(
SourceOrigin.Compiled,
sourceType,
hdlFolderName + separatorChar + designFileName(fileName),
group.map((block, p) => formatCode(p.csFile(block), withColor = false)).mkString("\n")
)
}
).flatten
// removing existing compiled/committed files and adding the newly compiled files
val srcFiles = designDB.srcFiles.filter {
Expand All @@ -686,6 +686,52 @@ trait Printer
designDB.update(srcFiles = srcFiles)
end printedDB

/** The design FILES to emit, as `(file base name, the designs it holds)`.
*
* A file is a DECLARATION's HDL output, not a design's: see `DB.designFileNameMap`, which
* decides the grouping and the names.
*
* The groups come out in an order where a design's file precedes every file instantiating it,
* which VHDL analysis and Verilog compilation order both need. `designPrinters` already
* satisfies this design-by-design (post-order), but grouping merges positions, so the order is
* re-derived as a stable topological sort over the groups. A reference cycle (mutually recursive
* declarations) is broken at its first back edge, keeping the input order there.
*/
protected final def designFileGroups(
entries: List[(DFDesignBlock, TPrinter)]
): List[(String, List[(DFDesignBlock, TPrinter)])] =
val designDB = getSet.designDB
val fileNameOf = designDB.designFileNameMap
val groupList =
entries.groupByOrdered((block, _) => fileNameOf.getOrElse(block, block.dclName)).toVector
val groupIdxOf: Map[DFDesignBlock, Int] =
groupList.iterator.zipWithIndex.flatMap { (group, idx) =>
group._2.iterator.map(_._1 -> idx)
}.toMap
// group -> the groups holding the designs its designs instantiate
val prereqs = Array.fill(groupList.length)(mutable.LinkedHashSet.empty[Int])
designDB.designBlockOwnershipMap.foreach { (child, owners) =>
groupIdxOf.get(child).foreach { childIdx =>
owners.foreach { owner =>
groupIdxOf.get(owner).foreach(ownerIdx =>
if (ownerIdx != childIdx) prereqs(ownerIdx) += childIdx
)
}
}
}
val ordered = mutable.ListBuffer.empty[Int]
// 0 = unvisited, 1 = on the current path (a back edge to it is the cycle break), 2 = emitted
val state = Array.fill(groupList.length)(0)
def visit(idx: Int): Unit =
if (state(idx) == 0)
state(idx) = 1
prereqs(idx).foreach(visit)
state(idx) = 2
ordered.addOne(idx)
groupList.indices.foreach(visit)
ordered.toList.map(groupList(_))
end designFileGroups

val printVendorIPBlackbox: Boolean = false

// The (design block, printer-bound-to-its-getSet) pairs to render, in order.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -293,8 +293,7 @@ class NameVarVersionsSpec extends StageSpec:
| val v = Bits(8) <> VAR
| v := x
| c := x(0)
| val a: Bits[8] <> CONST = h"00"
| y2 := a
| y2 := h"00"
| if (c) y2 := x
| else if (x(1)) q.din := v
| v := v | h"0f"
Expand Down Expand Up @@ -331,9 +330,8 @@ class NameVarVersionsSpec extends StageSpec:
| v := x
| v_ver1 := v
| report(s"v is ${v_ver1}", Severity.Warning)
| val a: Bits[8] <> CONST = h"05"
| v_ver2 := v
| assert(v_ver2 == a, s"bad ${v_ver2}")
| assert(v_ver2 == h"05", s"bad ${v_ver2}")
| v := v | h"0f"
| y := v
| end a
Expand Down
Loading
Loading