diff --git a/.claude/commands/ir-reference.md b/.claude/commands/ir-reference.md
index 6ce0c636c..1812ae51d 100644
--- a/.claude/commands/ir-reference.md
+++ b/.claude/commands/ir-reference.md
@@ -922,7 +922,7 @@ All require `using MemberGetSet`.
```scala
dfVal.getReadDeps // Set[DFValReadDep] — things that read this value
dfVal.getPartialAliases // Set[DFVal.Alias.Partial] — aliases of this value
-dfVal.getConnectionTo // Option[DFNet] — single connection driving this value
+dfVal.getConnectionsTo // Set[DFNet] — connections driving this value (per-slice; a Dcl may have multiple, one per disjoint bit range)
dfVal.getConnectionsFrom // Set[DFNet] — connections driven from this value
dfVal.getAssignmentsTo // Set[DFVal] — values assigned to this
dfVal.getAssignmentsFrom // Set[DFVal] — values assigned from this
diff --git a/compiler/ir/src/main/scala/dfhdl/compiler/analysis/DFValAnalysis.scala b/compiler/ir/src/main/scala/dfhdl/compiler/analysis/DFValAnalysis.scala
index 1b44d4c55..18695f80b 100644
--- a/compiler/ir/src/main/scala/dfhdl/compiler/analysis/DFValAnalysis.scala
+++ b/compiler/ir/src/main/scala/dfhdl/compiler/analysis/DFValAnalysis.scala
@@ -224,8 +224,8 @@ extension (dfVal: DFVal)
// case _ => false
// }
// end hasPrevAlias
- def getConnectionTo(using MemberGetSet): Option[DFNet] =
- getSet.designDB.connectionTable.getNets(dfVal).headOption
+ def getConnectionsTo(using MemberGetSet): Set[DFNet] =
+ getSet.designDB.connectionTable.getNets(dfVal)
def getConnectionsFrom(using MemberGetSet): Set[DFNet] =
getSet.designDB.connectionTableInverted.getOrElse(dfVal, Set())
def getAssignmentsTo(using MemberGetSet): Set[DFVal] =
@@ -385,7 +385,7 @@ extension (dfVal: DFVal)
def isConstVAR(using MemberGetSet): Boolean =
dfVal match
case dcl @ DclVar() =>
- dcl.getAssignmentsTo.isEmpty && dcl.getConnectionTo.isEmpty
+ dcl.getAssignmentsTo.isEmpty && dcl.getConnectionsTo.isEmpty
case _ => false
def isAllowedMultipleReferences(using MemberGetSet): Boolean = dfVal match
case _ if !dfVal.isAnonymous => true // allow named
diff --git a/compiler/ir/src/main/scala/dfhdl/compiler/analysis/StateAnalysis.scala b/compiler/ir/src/main/scala/dfhdl/compiler/analysis/StateAnalysis.scala
index 13735ccaf..d503b7949 100644
--- a/compiler/ir/src/main/scala/dfhdl/compiler/analysis/StateAnalysis.scala
+++ b/compiler/ir/src/main/scala/dfhdl/compiler/analysis/StateAnalysis.scala
@@ -5,7 +5,7 @@ import dfhdl.internals.*
import scala.annotation.tailrec
object StateAnalysis:
- @tailrec private def consumeFrom(
+ private def consumeFrom(
value: DFVal,
slice: Slice,
assignMap: AssignMap,
@@ -54,31 +54,40 @@ object StateAnalysis:
val rvSet = consumeFrom(relValRef.get, assignMap, currentSet)
val idxSet = consumeFrom(idxRef.get, assignMap, currentSet)
(rvSet union idxSet)
- case DFVal.Alias.SelectField(
- dfType = dfType: DFStruct,
- relValRef = relValRef,
- fieldName = fieldName
- ) =>
- // var rbh = dfType.width - 1
- // dfType.fieldMap.foreach { case (n, t) =>
- // val rbl = rbh - t.width + 1
- // if (n == fieldName)
- // rbh = rbl - 1
- // }
- ???
+ case sf @ DFVal.Alias.SelectField(relValRef = relValRef, fieldName = fieldName) =>
+ // Re-seed the slice to the field's extent in the parent struct's coordinates,
+ // mirroring the ApplyRange case above.
+ val relVal = relValRef.get
+ relVal.dfType match
+ case structType: DFStruct =>
+ val low = structType.fieldRelBitLow(fieldName)
+ val newSlice: Slice = sf.dfType.widthIntOpt match
+ case Some(w) => Slice.Concrete(Range(low, low + w))
+ case None => Slice.Unknown
+ consumeFrom(relVal, newSlice, assignMap, currentSet)
+ case _ => consumeFrom(relVal, slice, assignMap, currentSet)
case IteratorDcl() => currentSet
// out ports of child designs are not consuming state within the current design
case dcl @ DclOut()
if dcl.getOwnerDesign.isOneLevelBelow(currentBlock.getThisOrOwnerDesign) => currentSet
case dcl: DFVal.Dcl if (dcl.isPortOut || dcl.isVar) && !dcl.isReg && !dcl.isInProcess =>
- value.getConnectionTo match
- case Some(DFNet.Connection(_, fromVal: DFVal, _)) =>
- consumeFrom(fromVal, slice, assignMap, currentSet)
- case _ =>
+ // A declaration may be driven by multiple connections, each covering a
+ // disjoint bit range. Consume state from every connecting producer, and
+ // for the portion of `slice` not driven by any connection fall back to the
+ // assignment-coverage check (an uncovered, unassigned slice still consumes
+ // its implicit previous value).
+ value.getConnectionsTo.toList match
+ case Nil =>
val scope = assignMap(value)
scope.contains(slice, value.dfType.widthIntOpt) match
case Tri.Yes => currentSet
case _ => currentSet + value
+ case conns =>
+ conns.foldLeft(currentSet) {
+ case (cs, DFNet.Connection(_, fromVal: DFVal, _)) =>
+ consumeFrom(fromVal, slice, assignMap, cs)
+ case (cs, _) => cs
+ }
case _ => currentSet
end match
end consumeFrom
@@ -112,12 +121,12 @@ object StateAnalysis:
case DFVal.Alias.ApplyIdx(relValRef = relValRef, relIdx = idxRef) =>
// for simplification, assigning the entirety of the array
assignTo(relValRef.get, assignMap)
- case DFVal.Alias.SelectField(
- dfType = dfType: DFStruct,
- relValRef = relValRef,
- fieldName = fieldName
- ) =>
- ???
+ case DFVal.Alias.SelectField(relValRef = relValRef, fieldName = fieldName) =>
+ // shift the field-relative slice into the parent struct's coordinates
+ relValRef.get.dfType match
+ case structType: DFStruct =>
+ assignTo(relValRef.get, slice.shift(structType.fieldRelBitLow(fieldName)), assignMap)
+ case _ => assignTo(relValRef.get, slice, assignMap)
case x => assignMap.assignTo(x, slice)
end match
end assignTo
diff --git a/compiler/ir/src/main/scala/dfhdl/compiler/printing/DFOwnerPrinter.scala b/compiler/ir/src/main/scala/dfhdl/compiler/printing/DFOwnerPrinter.scala
index 54017d969..44a601841 100644
--- a/compiler/ir/src/main/scala/dfhdl/compiler/printing/DFOwnerPrinter.scala
+++ b/compiler/ir/src/main/scala/dfhdl/compiler/printing/DFOwnerPrinter.scala
@@ -225,7 +225,10 @@ protected trait DFOwnerPrinter extends AbstractOwnerPrinter:
val design = inst.getDesignBlock
val ports = getSet.designDB.designInstPBNS(inst).view.collect {
case pbns if pbns.isIn =>
- val DFNet.Connection(_, from: DFVal, _) = pbns.getConnectionTo.get.runtimeChecked
+ // the positional def-instance form expects a single producer per input port;
+ // a piecewise-connected input port (multiple partial nets) cannot be rendered
+ // here, so we fall back to the first connection's producer.
+ val DFNet.Connection(_, from: DFVal, _) = pbns.getConnectionsTo.head.runtimeChecked
printer.csDFValRef(from, inst.getOwner)
}.mkString(", ")
val designParamList = csDesignParamList(inst.paramMap)
diff --git a/compiler/ir/src/main/scala/dfhdl/compiler/printing/Printer.scala b/compiler/ir/src/main/scala/dfhdl/compiler/printing/Printer.scala
index 62a332d3e..05b43d421 100644
--- a/compiler/ir/src/main/scala/dfhdl/compiler/printing/Printer.scala
+++ b/compiler/ir/src/main/scala/dfhdl/compiler/printing/Printer.scala
@@ -51,7 +51,7 @@ trait Printer
lhsOrig match
case dfIfc: DFInterfaceOwner => "<->"
case dfVal: DFVal =>
- if (dfVal.getConnectionTo.contains(net) ^ swapLR) "<--"
+ if (dfVal.getConnectionsTo.contains(net) ^ swapLR) "<--"
else "-->"
val (lhsRef, rhsRef) = if (swapLR) (net.rhsRef, net.lhsRef) else (net.lhsRef, net.rhsRef)
def csNode(ref: DFNet.Ref): String =
diff --git a/compiler/stages/src/main/scala/dfhdl/compiler/stages/ViaConnection.scala b/compiler/stages/src/main/scala/dfhdl/compiler/stages/ViaConnection.scala
index 119fb624c..4aeebfd57 100644
--- a/compiler/stages/src/main/scala/dfhdl/compiler/stages/ViaConnection.scala
+++ b/compiler/stages/src/main/scala/dfhdl/compiler/stages/ViaConnection.scala
@@ -17,40 +17,49 @@ case object ViaConnection extends HierarchyStage:
val pbnsSkip = mutable.Set.empty[DFVal.PortByNameSelect]
val pbnsGrps =
subDB.designInstPBNS.getOrElse(ib, Nil).groupBy(_.portNamePath).values.toList
- pbnsGrps.foreach {
- // a single PBNS is connected to no more than a single net
- case List(pbns) =>
- if (pbns.isOut)
- pbns.getConnectionsFrom.headOption match
- // already has via connections
- case Some(n) if n.isViaConnection => pbnsSkip += pbns
- // connected to OPEN, so we skip it from variable creation, but add the net to
- // the list of nets to be moved as via connections
- case Some(n @ DFNet.Connection(toVal = openVal: DFVal.Special))
- if openVal.isOpen =>
- pbnsSkip += pbns
- nets += n
- // already connected to a variable, but without via connection, so we add the net
- // to the list of nets to be moved as via connections
- case Some(n @ DFNet.Connection(toVal = DclVar())) =>
- pbnsSkip += pbns
- nets += n
- // not connected through a net
- case _ => // do nothing
- end match
- else // input port
- pbns.getConnectionTo match
- // already has via connections
- case Some(n) if n.isViaConnection => pbnsSkip += pbns
- // we have a single net that is assigned not more than once (otherwise, for RTL purposes we require another value so an internal multi-assignment rtl variable/reg can be assigned into a signal/wire)
- case Some(n @ DFNet.Connection(fromVal = v @ DclVar()))
- if v.getAssignmentsTo.isEmpty =>
- nets += n
- pbnsSkip += pbns
- // not connected through a net
- case _ => // do nothing
- // multiple PBNS (all belong to the same port)
- case _ => // do nothing
+ pbnsGrps.foreach { pbnss =>
+ // all connection nets driving (input) or driven by (output) this port group.
+ // a port may be wired by several nets, each covering a disjoint bit range.
+ val groupNets =
+ pbnss.flatMap(p => if (p.isOut) p.getConnectionsFrom else p.getConnectionsTo).toSet
+ // a port already wired through via-connections (possibly several partial ones)
+ // is left untouched
+ if (groupNets.nonEmpty && groupNets.forall(_.isViaConnection))
+ pbnss.foreach(pbnsSkip += _)
+ else
+ pbnss match
+ // a single PBNS that is not already a via-connection
+ case List(pbns) =>
+ if (pbns.isOut)
+ pbns.getConnectionsFrom.headOption match
+ // connected to OPEN, so we skip it from variable creation, but add the net to
+ // the list of nets to be moved as via connections
+ case Some(n @ DFNet.Connection(toVal = openVal: DFVal.Special))
+ if openVal.isOpen =>
+ pbnsSkip += pbns
+ nets += n
+ // already connected to a variable, but without via connection, so we add the
+ // net to the list of nets to be moved as via connections
+ case Some(n @ DFNet.Connection(toVal = DclVar())) =>
+ pbnsSkip += pbns
+ nets += n
+ // not connected through a net
+ case _ => // do nothing
+ end match
+ else // input port
+ pbns.getConnectionsTo.toList match
+ // we have a single net that is assigned not more than once (otherwise, for RTL purposes we require another value so an internal multi-assignment rtl variable/reg can be assigned into a signal/wire)
+ case (n @ DFNet.Connection(fromVal = v @ DclVar())) :: Nil
+ if v.getAssignmentsTo.isEmpty =>
+ nets += n
+ pbnsSkip += pbns
+ // not connected through a net
+ case _ => // do nothing
+ end match
+ // multiple PBNS (all belong to the same port) that are not via-connected are
+ // bridged through a synthesized variable
+ case _ => // do nothing
+ end match
}
// Meta design to construct the variables to be connected to the ports
diff --git a/compiler/stages/src/main/scala/dfhdl/compiler/stages/verilog/VerilogValPrinter.scala b/compiler/stages/src/main/scala/dfhdl/compiler/stages/verilog/VerilogValPrinter.scala
index 2c95d2edd..fdf5df845 100644
--- a/compiler/stages/src/main/scala/dfhdl/compiler/stages/verilog/VerilogValPrinter.scala
+++ b/compiler/stages/src/main/scala/dfhdl/compiler/stages/verilog/VerilogValPrinter.scala
@@ -67,7 +67,7 @@ protected trait VerilogValPrinter extends AbstractValPrinter:
def regOrWireRep = dfVal.modifier.dir match
case Modifier.IN => ""
case _ =>
- if (dfVal.getConnectionTo.nonEmpty) "wire "
+ if (dfVal.getConnectionsTo.nonEmpty) "wire "
else "reg "
val fixedDFTypeStr =
if (supportLogicType) dfTypeStr
diff --git a/compiler/stages/src/main/scala/dfhdl/compiler/stages/vhdl/VHDLValPrinter.scala b/compiler/stages/src/main/scala/dfhdl/compiler/stages/vhdl/VHDLValPrinter.scala
index f78046b35..b77d00693 100644
--- a/compiler/stages/src/main/scala/dfhdl/compiler/stages/vhdl/VHDLValPrinter.scala
+++ b/compiler/stages/src/main/scala/dfhdl/compiler/stages/vhdl/VHDLValPrinter.scala
@@ -233,7 +233,13 @@ protected trait VHDLValPrinter extends AbstractValPrinter:
def csDFValAliasApplyRange(dfVal: Alias.ApplyRange): String =
dfVal.dfType match
case DFBits(_) | DFUInt(_) | DFSInt(_) =>
- s"${dfVal.relValCodeString}(${dfVal.idxHighRef.refCodeString} downto ${dfVal.idxLowRef.refCodeString})"
+ val slice =
+ s"${dfVal.relValCodeString}(${dfVal.idxHighRef.refCodeString} downto ${dfVal.idxLowRef.refCodeString})"
+ // SInt slice now produces DFUInt; wrap with `unsigned(...)` since
+ // VHDL preserves the signed subtype across slicing.
+ (dfVal.relValRef.get.dfType, dfVal.dfType) match
+ case (DFSInt(_), DFUInt(_)) => s"unsigned($slice)"
+ case _ => slice
case _ =>
s"${dfVal.relValCodeString}(${dfVal.idxLowRef.refCodeString} to ${dfVal.idxHighRef.refCodeString})"
end match
diff --git a/compiler/stages/src/test/scala/StagesSpec/PrintCodeStringSpec.scala b/compiler/stages/src/test/scala/StagesSpec/PrintCodeStringSpec.scala
index 199bd7918..8269aba09 100644
--- a/compiler/stages/src/test/scala/StagesSpec/PrintCodeStringSpec.scala
+++ b/compiler/stages/src/test/scala/StagesSpec/PrintCodeStringSpec.scala
@@ -1943,6 +1943,66 @@ class PrintCodeStringSpec extends StageSpec(stageCreatesUnrefAnons = true):
)
}
+ test("partial bit and range connections to a single port") {
+ class Foo extends EDDesign:
+ val a = Bits(8) <> IN
+ val y = Bits(8) <> OUT
+ y(0) <> a(7)
+ y(7, 1) <> a(6, 0)
+ end Foo
+ val top = (new Foo).getCodeString
+ assertNoDiff(
+ top,
+ """|class Foo extends EDDesign:
+ | val a = Bits(8) <> IN
+ | val y = Bits(8) <> OUT
+ | y(0) <> a(7)
+ | y(7, 1) <> a(6, 0)
+ |end Foo""".stripMargin
+ )
+ }
+ test("struct field connections to a single port") {
+ case class AB(a: Bits[4] <> VAL, b: Bits[4] <> VAL) extends Struct
+ class Foo extends EDDesign:
+ val i = AB <> IN
+ val y = AB <> OUT
+ y.a <> i.b
+ y.b <> i.a
+ end Foo
+ val top = (new Foo).getCodeString
+ assertNoDiff(
+ top,
+ """|final case class AB(
+ | a: Bits[4] <> VAL
+ | b: Bits[4] <> VAL
+ |) extends Struct
+ |
+ |class Foo extends EDDesign:
+ | val i = AB <> IN
+ | val y = AB <> OUT
+ | y.a <> i.b
+ | y.b <> i.a
+ |end Foo""".stripMargin
+ )
+ }
+ test("partial connect and assign mix under RTDesign") {
+ class Foo extends RTDesign:
+ val a = Bits(8) <> IN
+ val y = Bits(8) <> OUT
+ y(3, 0) <> a(3, 0)
+ y(7, 4) := a(7, 4)
+ end Foo
+ val top = (new Foo).getCodeString
+ assertNoDiff(
+ top,
+ """|class Foo extends RTDesign:
+ | val a = Bits(8) <> IN
+ | val y = Bits(8) <> OUT
+ | y(3, 0) <> a(3, 0)
+ | y(7, 4) := a(7, 4)
+ |end Foo""".stripMargin
+ )
+ }
test("different width parameters in nested designs") {
class ID(val WIDTH: Int <> CONST = 10) extends EDDesign:
val x = Bits(WIDTH) <> IN
diff --git a/compiler/stages/src/test/scala/StagesSpec/PrintVHDLCodeSpec.scala b/compiler/stages/src/test/scala/StagesSpec/PrintVHDLCodeSpec.scala
index dc8397f53..8c46bdf2a 100644
--- a/compiler/stages/src/test/scala/StagesSpec/PrintVHDLCodeSpec.scala
+++ b/compiler/stages/src/test/scala/StagesSpec/PrintVHDLCodeSpec.scala
@@ -1577,4 +1577,101 @@ class PrintVHDLCodeSpec extends StageSpec:
|""".stripMargin
)
}
+ test("partial bit and range connections to a single port") {
+ class Foo extends EDDesign:
+ val a = Bits(8) <> IN
+ val y = Bits(8) <> OUT
+ y(0) <> a(7)
+ y(7, 1) <> a(6, 0)
+ end Foo
+ val top = (new Foo).getCompiledCodeString
+ assertNoDiff(
+ top,
+ """|library ieee;
+ |use ieee.std_logic_1164.all;
+ |use ieee.numeric_std.all;
+ |use work.dfhdl_pkg.all;
+ |
+ |entity Foo is
+ |port (
+ | a : in std_logic_vector(7 downto 0);
+ | y : out std_logic_vector(7 downto 0)
+ |);
+ |end Foo;
+ |
+ |architecture Foo_arch of Foo is
+ |begin
+ | y(0) <= a(7);
+ | y(7 downto 1) <= a(6 downto 0);
+ |end Foo_arch;""".stripMargin
+ )
+ }
+ test("struct field connections to a single port") {
+ case class AB(a: Bits[4] <> VAL, b: Bits[4] <> VAL) extends Struct
+ class Foo extends EDDesign:
+ val i = AB <> IN
+ val y = AB <> OUT
+ y.a <> i.b
+ y.b <> i.a
+ end Foo
+ val top = (new Foo).getCompiledCodeString
+ assertNoDiff(
+ top,
+ """|type t_struct_AB is record
+ | a : std_logic_vector(3 downto 0);
+ | b : std_logic_vector(3 downto 0);
+ |end record;
+ |
+ |library ieee;
+ |use ieee.std_logic_1164.all;
+ |use ieee.numeric_std.all;
+ |use work.dfhdl_pkg.all;
+ |use work.Foo_pkg.all;
+ |
+ |entity Foo is
+ |port (
+ | i : in t_struct_AB;
+ | y : out t_struct_AB
+ |);
+ |end Foo;
+ |
+ |architecture Foo_arch of Foo is
+ |begin
+ | y.a <= i.b;
+ | y.b <= i.a;
+ |end Foo_arch;""".stripMargin
+ )
+ }
+ test("partial connect and assign mix under RTDesign") {
+ class Foo extends RTDesign:
+ val a = Bits(8) <> IN
+ val y = Bits(8) <> OUT
+ y(3, 0) <> a(3, 0)
+ y(7, 4) := a(7, 4)
+ end Foo
+ val top = (new Foo).getCompiledCodeString
+ assertNoDiff(
+ top,
+ """|library ieee;
+ |use ieee.std_logic_1164.all;
+ |use ieee.numeric_std.all;
+ |use work.dfhdl_pkg.all;
+ |
+ |entity Foo is
+ |port (
+ | a : in std_logic_vector(7 downto 0);
+ | y : out std_logic_vector(7 downto 0)
+ |);
+ |end Foo;
+ |
+ |architecture Foo_arch of Foo is
+ |begin
+ | y(3 downto 0) <= a(3 downto 0);
+ | process (all)
+ | begin
+ | y(7 downto 4) <= a(7 downto 4);
+ | end process;
+ |end Foo_arch;""".stripMargin
+ )
+ }
end PrintVHDLCodeSpec
diff --git a/compiler/stages/src/test/scala/StagesSpec/PrintVerilogCodeSpec.scala b/compiler/stages/src/test/scala/StagesSpec/PrintVerilogCodeSpec.scala
index 6dacd6f18..544eccfe7 100644
--- a/compiler/stages/src/test/scala/StagesSpec/PrintVerilogCodeSpec.scala
+++ b/compiler/stages/src/test/scala/StagesSpec/PrintVerilogCodeSpec.scala
@@ -1616,4 +1616,83 @@ class PrintVerilogCodeSpec extends StageSpec:
|endmodule""".stripMargin
)
}
+ test("partial bit and range connections to a single port") {
+ class Foo extends EDDesign:
+ val a = Bits(8) <> IN
+ val y = Bits(8) <> OUT
+ y(0) <> a(7)
+ y(7, 1) <> a(6, 0)
+ end Foo
+ val top = (new Foo).getCompiledCodeString
+ assertNoDiff(
+ top,
+ """|`default_nettype none
+ |`timescale 1ns/1ps
+ |
+ |module Foo(
+ | input wire logic [7:0] a,
+ | output logic [7:0] y
+ |);
+ | `include "dfhdl_defs.svh"
+ | assign y[0] = a[7];
+ | assign y[7:1] = a[6:0];
+ |endmodule""".stripMargin
+ )
+ }
+ test("struct field connections to a single port") {
+ case class AB(a: Bits[4] <> VAL, b: Bits[4] <> VAL) extends Struct
+ class Foo extends EDDesign:
+ val i = AB <> IN
+ val y = AB <> OUT
+ y.a <> i.b
+ y.b <> i.a
+ end Foo
+ val top = (new Foo).getCompiledCodeString
+ assertNoDiff(
+ top,
+ """|typedef struct packed {
+ | logic [3:0] a;
+ | logic [3:0] b;
+ |} t_struct_AB;
+ |
+ |`default_nettype none
+ |`timescale 1ns/1ps
+ |`include "Foo_defs.svh"
+ |
+ |module Foo(
+ | input wire t_struct_AB i,
+ | output t_struct_AB y
+ |);
+ | `include "dfhdl_defs.svh"
+ | assign y.a = i.b;
+ | assign y.b = i.a;
+ |endmodule""".stripMargin
+ )
+ }
+ test("partial connect and assign mix under RTDesign") {
+ class Foo extends RTDesign:
+ val a = Bits(8) <> IN
+ val y = Bits(8) <> OUT
+ y(3, 0) <> a(3, 0)
+ y(7, 4) := a(7, 4)
+ end Foo
+ val top = (new Foo).getCompiledCodeString
+ assertNoDiff(
+ top,
+ """|`default_nettype none
+ |`timescale 1ns/1ps
+ |
+ |module Foo(
+ | input wire logic [7:0] a,
+ | output logic [7:0] y
+ |);
+ | `include "dfhdl_defs.svh"
+ | assign y[3:0] = a[3:0];
+ | always_comb
+ | begin
+ | y[7:4] = a[7:4];
+ | end
+ |endmodule""".stripMargin
+ )
+ }
end PrintVerilogCodeSpec
diff --git a/compiler/stages/src/test/scala/StagesSpec/ViaConnectionSpec.scala b/compiler/stages/src/test/scala/StagesSpec/ViaConnectionSpec.scala
index 5a51e8164..7c4310da0 100644
--- a/compiler/stages/src/test/scala/StagesSpec/ViaConnectionSpec.scala
+++ b/compiler/stages/src/test/scala/StagesSpec/ViaConnectionSpec.scala
@@ -302,6 +302,82 @@ class ViaConnectionSpec extends StageSpec(stageCreatesUnrefAnons = true):
)
}
+ test("Partial via-connections on an input port are preserved") {
+ class ID extends DFDesign:
+ val x = Bits(8) <> IN
+ val y = Bits(8) <> OUT
+ y := x
+
+ class IDTop extends DFDesign:
+ val a = Bits(8) <> IN
+ val y = Bits(8) <> OUT
+ val id = new ID:
+ this.x(3, 0) <> a(3, 0)
+ this.x(7, 4) <> a(7, 4)
+ id.y <> y
+
+ val top = (new IDTop).viaConnection
+ assertCodeString(
+ top,
+ """|class ID extends DFDesign:
+ | val x = Bits(8) <> IN
+ | val y = Bits(8) <> OUT
+ | y := x
+ |end ID
+ |
+ |class IDTop extends DFDesign:
+ | val a = Bits(8) <> IN
+ | val y = Bits(8) <> OUT
+ | val id_y = Bits(8) <> VAR
+ | val id = new ID():
+ | this.y <>/*-->*/ id_y
+ | this.id.x(3, 0) <>/*<--*/ a(3, 0)
+ | this.id.x(7, 4) <>/*<--*/ a(7, 4)
+ | end id
+ | y <> id_y
+ |end IDTop
+ |""".stripMargin
+ )
+ }
+ test("Partial connections to an instance input port are bridged through a variable") {
+ class ID extends DFDesign:
+ val x = Bits(8) <> IN
+ val y = Bits(8) <> OUT
+ y := x
+
+ class IDTop extends DFDesign:
+ val x = Bits(8) <> IN
+ val y = Bits(8) <> OUT
+ val id = new ID
+ id.x(3, 0) <> x(3, 0)
+ id.x(7, 4) <> x(7, 4)
+ id.y <> y
+
+ val top = (new IDTop).viaConnection
+ assertCodeString(
+ top,
+ """|class ID extends DFDesign:
+ | val x = Bits(8) <> IN
+ | val y = Bits(8) <> OUT
+ | y := x
+ |end ID
+ |
+ |class IDTop extends DFDesign:
+ | val x = Bits(8) <> IN
+ | val y = Bits(8) <> OUT
+ | val id_x = Bits(8) <> VAR
+ | val id_y = Bits(8) <> VAR
+ | val id = new ID():
+ | this.x <>/*<--*/ id_x
+ | this.y <>/*-->*/ id_y
+ | end id
+ | id_x(3, 0) <> x(3, 0)
+ | id_x(7, 4) <> x(7, 4)
+ | y <> id_y
+ |end IDTop
+ |""".stripMargin
+ )
+ }
test("Hierarchical design with parameters 1") {
class ID(
val width: Int <> CONST,
diff --git a/core/src/main/scala/dfhdl/core/DFBits.scala b/core/src/main/scala/dfhdl/core/DFBits.scala
index f7f7ac532..1b1d4be41 100644
--- a/core/src/main/scala/dfhdl/core/DFBits.scala
+++ b/core/src/main/scala/dfhdl/core/DFBits.scala
@@ -594,9 +594,9 @@ object DFBits:
R
](using
ub: DFUInt.Val.UBArg[W, R]
- ): ExactOp2Aux["apply", DFC, DFValAny, L, R, DFVal[DFBit, Modifier[A, Any, Any, P]]] =
+ ): ExactOp2Aux["apply", DFC, DFValAny, L, R, DFVal[DFBit, Modifier[A, C, Any, P]]] =
new ExactOp2["apply", DFC, DFValAny, L, R]:
- type Out = DFVal[DFBit, Modifier[A, Any, Any, P]]
+ type Out = DFVal[DFBit, Modifier[A, C, Any, P]]
def apply(lhs: L, idx: R)(using DFC): Out = trydf {
DFVal.Alias.ApplyIdx(DFBit, lhs, ub(lhs.widthIntParam, idx)(using dfc.anonymize))
}(using dfc, CTName("bit selection (apply)"))
@@ -616,10 +616,10 @@ object DFBits:
checkHiLo: BitsHiLo.CheckNUB[HI, LO]
): ExactOp3Aux["apply", DFC, DFValAny, L, HI, LO, DFVal[
DFBits[HI - LO + 1],
- Modifier[A, Any, Any, P]
+ Modifier[A, C, Any, P]
]] =
new ExactOp3["apply", DFC, DFValAny, L, HI, LO]:
- type Out = DFVal[DFBits[HI - LO + 1], Modifier[A, Any, Any, P]]
+ type Out = DFVal[DFBits[HI - LO + 1], Modifier[A, C, Any, P]]
def apply(lhs: L, idxHigh: HI, idxLow: LO)(using DFC): Out = trydf {
val idxHighParam = IntParam(idxHigh)
val idxLowParam = IntParam(idxLow)
@@ -812,12 +812,12 @@ object DFBits:
def unary_~(using DFCG): DFValTP[DFBits[W], P] = trydf {
DFVal.Func(lhs.dfType, FuncOp.unary_~, List(lhs))
}
- def msbit(using DFCG): DFVal[DFBit, Modifier[A, Any, Any, P]] =
+ def msbit(using DFCG): DFVal[DFBit, Modifier[A, C, Any, P]] =
import DFVal.Ops.apply as applyBits
- lhs.applyBits((lhs.widthIntParam - 1).toDFConst).asVal[DFBit, Modifier[A, Any, Any, P]]
- def lsbit(using DFCG): DFVal[DFBit, Modifier[A, Any, Any, P]] =
+ lhs.applyBits((lhs.widthIntParam - 1).toDFConst).asVal[DFBit, Modifier[A, C, Any, P]]
+ def lsbit(using DFCG): DFVal[DFBit, Modifier[A, C, Any, P]] =
import DFVal.Ops.apply as applyBits
- lhs.applyBits(0).asVal[DFBit, Modifier[A, Any, Any, P]]
+ lhs.applyBits(0).asVal[DFBit, Modifier[A, C, Any, P]]
def msbits[RW <: IntP](updatedWidth: IntParam[RW])(using
check: `LW >= RW`.CheckNUB[W, RW],
dfc: DFCG
diff --git a/core/src/main/scala/dfhdl/core/DFDecimal.scala b/core/src/main/scala/dfhdl/core/DFDecimal.scala
index fa086d46c..9e3348443 100644
--- a/core/src/main/scala/dfhdl/core/DFDecimal.scala
+++ b/core/src/main/scala/dfhdl/core/DFDecimal.scala
@@ -924,6 +924,27 @@ object DFXInt:
case None =>
DFXInt.Val.Ops.toDFXIntOf(dfValArg)(dfType).asValTP[DFXInt[LS, LW, LN], RP]
end conv
+ // Check Verilog-semantics mismatch for comparisons: same trigger as
+ // `/`, `%` -- a narrow non-carry chain mixed with an implicit Int on
+ // either side widens to 32-bit in Verilog but not in DFHDL.
+ override def apply[P](dfVal: DFValTP[DFXInt[LS, LW, LN], P], arg: R)(using
+ dfc: DFC,
+ opv: ValueOf[Op],
+ cv: ValueOf[C]
+ ): DFValTP[DFBool, P | RP] = trydf:
+ val dfValArg = conv(dfVal.dfType, arg)(using dfc.anonymize)
+ import dfc.getSet
+ val op = opv.value
+ op match
+ case FuncOp.=== | FuncOp.=!= | FuncOp.< | FuncOp.> | FuncOp.<= | FuncOp.>= =>
+ if DFXInt.Val.Ops.shouldWarnVerilogSemantics(dfVal.asIR, dfValArg.asIR)
+ then
+ dfc.logEvent(
+ DFWarning(op.toString, DFXInt.Val.Ops.verilogSemanticsWarnMsg)
+ )
+ case _ =>
+ func(dfVal, dfValArg)
+ end apply
end DFXIntCompare
end Compare
@@ -943,9 +964,9 @@ object DFXInt:
R
](using
ub: DFUInt.Val.UBArg[W, R]
- ): ExactOp2Aux["apply", DFC, DFValAny, L, R, DFValTP[DFBit, P]] =
+ ): ExactOp2Aux["apply", DFC, DFValAny, L, R, DFVal[DFBit, Modifier[A, C, Any, P]]] =
new ExactOp2["apply", DFC, DFValAny, L, R]:
- type Out = DFValTP[DFBit, P]
+ type Out = DFVal[DFBit, Modifier[A, C, Any, P]]
def apply(lhs: L, idx: R)(using DFC): Out = trydf {
DFVal.Alias.ApplyIdx(DFBit, lhs, ub(lhs.widthIntParam, idx)(using dfc.anonymize))
}(using dfc, CTName("bit selection (apply)"))
@@ -964,12 +985,12 @@ object DFXInt:
checkHigh: BitIndex.CheckNUB[HI, W],
checkLow: BitIndex.CheckNUB[LO, W],
checkHiLo: BitsHiLo.CheckNUB[HI, LO]
- ): ExactOp3Aux["apply", DFC, DFValAny, L, HI, LO, DFValTP[
- DFXInt[S, HI - LO + 1, BitAccurate],
- P
+ ): ExactOp3Aux["apply", DFC, DFValAny, L, HI, LO, DFVal[
+ DFUInt[HI - LO + 1],
+ Modifier[A, C, Any, P]
]] =
new ExactOp3["apply", DFC, DFValAny, L, HI, LO]:
- type Out = DFValTP[DFXInt[S, HI - LO + 1, BitAccurate], P]
+ type Out = DFVal[DFUInt[HI - LO + 1], Modifier[A, C, Any, P]]
def apply(lhs: L, idxHigh: HI, idxLow: LO)(using DFC): Out = trydf {
val idxHighParam = IntParam(idxHigh)
val idxLowParam = IntParam(idxLow)
@@ -1177,6 +1198,19 @@ object DFXInt:
)
case _ => false
+ // Unified Verilog-semantics warning trigger shared by `/`, `%` (arithOp)
+ // and comparison operations (DFXIntCompare). Warns when a narrow non-carry
+ // chain mixes with a tagged-from-Int operand on either side - directly OR
+ // nested inside the chain.
+ private[core] def shouldWarnVerilogSemantics(
+ lhs: ir.DFVal,
+ rhs: ir.DFVal
+ )(using ir.MemberGetSet): Boolean =
+ (hasImplicitlyFromIntTag(rhs) && containsNarrowNonCarryArith(lhs)) ||
+ (hasImplicitlyFromIntTag(lhs) && containsNarrowNonCarryArith(rhs)) ||
+ containsNarrowNonCarryArithWithTaggedOperand(lhs) ||
+ containsNarrowNonCarryArithWithTaggedOperand(rhs)
+
// Check that a wildcard `Int` value fits in the bit-accurate value's type.
// Produces an elaboration error if it doesn't.
private def checkWildcardFit(
@@ -1226,13 +1260,11 @@ object DFXInt:
)(using dfc: DFC): DFValTP[DFXInt[OS, OW, ON], LP | RP] =
val rhsFix = rhs.toDFXIntOf(lhs.dfType)(using dfc.anonymize)
import dfc.getSet
- // Check A: / and % — both operands are context-determined in Verilog
+ // Check A: / and % — both operands are context-determined in Verilog,
+ // so any narrow non-carry chain mixed with an implicit Int diverges.
val shouldWarn = op match
case FuncOp./ | FuncOp.% =>
- (hasImplicitlyFromIntTag(rhsFix.asIR) &&
- containsNarrowNonCarryArith(lhs.asIR)) ||
- (hasImplicitlyFromIntTag(lhs.asIR) &&
- containsNarrowNonCarryArith(rhsFix.asIR))
+ shouldWarnVerilogSemantics(lhs.asIR, rhsFix.asIR)
case _ => false
if shouldWarn then
dfc.logEvent(DFWarning(op.toString, verilogSemanticsWarnMsg))
@@ -1698,6 +1730,30 @@ object DFUInt:
lhs.widthIntOpt.foreach(check(_))
DFVal.Alias.AsIs(DFInt32, lhs.signed)
}
+ @targetName("msbitsDFUInt")
+ def msbits[RW <: IntP](updatedWidth: IntParam[RW])(using
+ check: `LW >= RW`.CheckNUB[W, RW],
+ dfc: DFCG
+ ): DFValTP[DFUInt[RW], P] = trydf {
+ (lhs.widthIntOpt, updatedWidth.toScalaIntOpt) match
+ case (Some(lhsWidthInt), Some(updatedWidthInt)) => check(lhsWidthInt, updatedWidthInt)
+ case _ =>
+ DFVal.Alias.ApplyRange
+ .applyDFXInt(lhs, lhs.widthIntParam - 1, lhs.widthIntParam - updatedWidth)
+ .asValTP[DFUInt[RW], P]
+ }
+ @targetName("lsbitsDFUInt")
+ def lsbits[RW <: IntP](updatedWidth: IntParam[RW])(using
+ check: `LW >= RW`.CheckNUB[W, RW],
+ dfc: DFCG
+ ): DFValTP[DFUInt[RW], P] = trydf {
+ (lhs.widthIntOpt, updatedWidth.toScalaIntOpt) match
+ case (Some(lhsWidthInt), Some(updatedWidthInt)) => check(lhsWidthInt, updatedWidthInt)
+ case _ =>
+ DFVal.Alias.ApplyRange
+ .applyDFXInt(lhs, updatedWidth - 1, 0)
+ .asValTP[DFUInt[RW], P]
+ }
end extension
end Ops
end Val
@@ -1742,6 +1798,30 @@ object DFSInt:
def unsigned(using DFCG): DFValTP[DFUInt[IntP.-[W, 1]], P] = trydf {
DFVal.Alias.AsIs(DFUInt(lhs.widthIntParam - 1), lhs)
}
+ @targetName("msbitsDFSInt")
+ def msbits[RW <: IntP](updatedWidth: IntParam[RW])(using
+ check: `LW >= RW`.CheckNUB[W, RW],
+ dfc: DFCG
+ ): DFValTP[DFUInt[RW], P] = trydf {
+ (lhs.widthIntOpt, updatedWidth.toScalaIntOpt) match
+ case (Some(lhsWidthInt), Some(updatedWidthInt)) => check(lhsWidthInt, updatedWidthInt)
+ case _ =>
+ DFVal.Alias.ApplyRange
+ .applyDFXInt(lhs, lhs.widthIntParam - 1, lhs.widthIntParam - updatedWidth)
+ .asValTP[DFUInt[RW], P]
+ }
+ @targetName("lsbitsDFSInt")
+ def lsbits[RW <: IntP](updatedWidth: IntParam[RW])(using
+ check: `LW >= RW`.CheckNUB[W, RW],
+ dfc: DFCG
+ ): DFValTP[DFUInt[RW], P] = trydf {
+ (lhs.widthIntOpt, updatedWidth.toScalaIntOpt) match
+ case (Some(lhsWidthInt), Some(updatedWidthInt)) => check(lhsWidthInt, updatedWidthInt)
+ case _ =>
+ DFVal.Alias.ApplyRange
+ .applyDFXInt(lhs, updatedWidth - 1, 0)
+ .asValTP[DFUInt[RW], P]
+ }
extension [P](lhs: DFValTP[DFInt32, P])
@targetName("negateDFInt32")
def unary_-(using DFCG): DFValTP[DFInt32, P] = trydf {
diff --git a/core/src/main/scala/dfhdl/core/DFVal.scala b/core/src/main/scala/dfhdl/core/DFVal.scala
index 5a6581304..101db690a 100644
--- a/core/src/main/scala/dfhdl/core/DFVal.scala
+++ b/core/src/main/scala/dfhdl/core/DFVal.scala
@@ -297,7 +297,7 @@ object DFVal extends DFValLP:
protected type FieldWithModifier[V, M <: ModifierAny] = V match
case DFVal[t, _] =>
M match
- case Modifier[a, Any, i, p] => DFVal[t, Modifier[a, Any, i, p]]
+ case Modifier[a, c, i, p] => DFVal[t, Modifier[a, c, i, p]]
protected type FieldsWithModifier[V <: NamedTuple.AnyNamedTuple, M <: ModifierAny] =
NamedTuple.Map[V, [t] =>> FieldWithModifier[t, M]]
protected[core] type Fields[T <: DFTypeAny, M <: ModifierAny] = T match
@@ -893,8 +893,8 @@ object DFVal extends DFValLP:
relVal: DFVal[DFXInt[S, W, NativeType.BitAccurate], M],
idxHigh: IntParam[H],
idxLow: IntParam[L]
- )(using DFC): DFVal[DFXInt[S, H - L + 1, NativeType.BitAccurate], M] =
- forced(relVal.asIR, idxHigh, idxLow).asVal[DFXInt[S, H - L + 1, NativeType.BitAccurate], M]
+ )(using DFC): DFVal[DFUInt[H - L + 1], M] =
+ forced(relVal.asIR, idxHigh, idxLow).asVal[DFUInt[H - L + 1], M]
def applyVector[T <: DFTypeAny, M <: ModifierAny, H <: IntP, L <: IntP](
relVal: DFVal[DFVector[T, Tuple1[?]], M],
idxHigh: IntParam[H],
@@ -909,8 +909,7 @@ object DFVal extends DFValLP:
val selLength = idxHigh - idxLow + 1
val dfType = relVal.dfType.runtimeChecked match
case ir.DFBits(_) => ir.DFBits(selLength.ref)
- case ir.DFUInt(_) => ir.DFUInt(selLength.ref)
- case ir.DFSInt(_) => ir.DFSInt(selLength.ref)
+ case ir.DFUInt(_) | ir.DFSInt(_) => ir.DFUInt(selLength.ref)
case ir.DFVector(cellType = cellType) =>
ir.DFVector(cellType, List(selLength.ref))
relVal match
diff --git a/core/src/test/scala/CoreSpec/DFDecimalSpec.scala b/core/src/test/scala/CoreSpec/DFDecimalSpec.scala
index f66b85546..f48e8cefb 100644
--- a/core/src/test/scala/CoreSpec/DFDecimalSpec.scala
+++ b/core/src/test/scala/CoreSpec/DFDecimalSpec.scala
@@ -77,6 +77,67 @@ class DFDecimalSpec extends DFSpec:
t2 := t1
}
}
+ test("DFVal Selection") {
+ val u8 = UInt(8) <> VAR
+ val s8 = SInt(8) <> VAR
+ assertCodeString {
+ """|val u_ms8 = u8(7, 0)
+ |val u_ms7 = u8(7, 1)
+ |val u_ms1 = u8(7, 7)
+ |val u_ls8 = u8(7, 0)
+ |val u_ls7 = u8(6, 0)
+ |val u_ls1 = u8(0, 0)
+ |val s_ms8 = s8(7, 0)
+ |val s_ms7 = s8(7, 1)
+ |val s_ms1 = s8(7, 7)
+ |val s_ls8 = s8(7, 0)
+ |val s_ls7 = s8(6, 0)
+ |val s_ls1 = s8(0, 0)
+ |""".stripMargin
+ } {
+ val u_ms8 = u8.msbits(8)
+ val u_ms7 = u8.msbits(7)
+ val u_ms1 = u8.msbits(1)
+ val u_ls8 = u8.lsbits(8)
+ val u_ls7 = u8.lsbits(7)
+ val u_ls1 = u8.lsbits(1)
+ val s_ms8 = s8.msbits(8)
+ val s_ms7 = s8.msbits(7)
+ val s_ms1 = s8.msbits(1)
+ val s_ls8 = s8.lsbits(8)
+ val s_ls7 = s8.lsbits(7)
+ val s_ls1 = s8.lsbits(1)
+ }
+ val nine = 9
+ assertDSLErrorLog(
+ """The applied RHS value width (9) is larger than the LHS variable width (8)."""
+ )(
+ """u8.msbits(9)"""
+ ) {
+ u8.msbits(nine)
+ }
+ assertDSLErrorLog(
+ """The applied RHS value width (9) is larger than the LHS variable width (8)."""
+ )(
+ """u8.lsbits(9)"""
+ ) {
+ u8.lsbits(nine)
+ }
+ assertDSLErrorLog(
+ """The applied RHS value width (9) is larger than the LHS variable width (8)."""
+ )(
+ """s8.msbits(9)"""
+ ) {
+ s8.msbits(nine)
+ }
+ assertDSLErrorLog(
+ """The applied RHS value width (9) is larger than the LHS variable width (8)."""
+ )(
+ """s8.lsbits(9)"""
+ ) {
+ s8.lsbits(nine)
+ }
+ }
test("Assignment") {
assertCodeString {
"""|val c: UInt[8] <> CONST = d"8'1"
@@ -126,7 +187,8 @@ class DFDecimalSpec extends DFSpec:
|val pow: Int <> CONST = 2 ** param
|val u4 = u6(3, 0)
|val ubit = u6(1)
- |val s4 = s6(3, 0)
+ |val s4u = s6(3, 0)
+ |val s4 = s6(3, 0).bits.sint
|val sbit = s6(1)
|val uint42: UInt[6] <> CONST = d"6'42"
|val sint42: SInt[7] <> CONST = sd"7'42"
@@ -272,7 +334,8 @@ class DFDecimalSpec extends DFSpec:
val pow = 2 ** param
val u4 = u6(3, 0)
val ubit = u6(1)
- val s4 = s6(3, 0)
+ val s4u = s6(3, 0)
+ val s4 = s6(3, 0).bits.sint
val sbit = s6(1)
val str42 = "42"
val strNeg42 = "-42"
@@ -930,6 +993,47 @@ class DFDecimalSpec extends DFSpec:
val w32b = UInt(32) <> VAR
val t7 = (w32 + w32b) / 4
+ // Generalized / and %: warn when the chain has an implicit Int inside,
+ // even when neither outer operand is itself an implicit Int.
+ assertRuntimeWarningLog(warnMsg) {
+ val t8 = (a + 1) / b
+ }
+ assertRuntimeWarningLog(warnMsg) {
+ val t9 = (a + 1) % b
+ }
+
+ // Comparisons: warn when a narrow non-carry chain mixes with an implicit
+ // Int on either side - directly OR nested inside the chain.
+ assertRuntimeWarningLog(warnMsg) {
+ val tc1 = (a + 1) == b
+ }
+ assertRuntimeWarningLog(warnMsg) {
+ val tc2 = (a + b) == 5
+ }
+ assertRuntimeWarningLog(warnMsg) {
+ val tc3 = (a + 1) == 5
+ }
+ assertRuntimeWarningLog(warnMsg) {
+ val tc4 = 5 < (a + 1)
+ }
+ assertRuntimeWarningLog(warnMsg) {
+ val tc5 = (a + b + 0) != c
+ }
+ assertRuntimeWarningLog(warnMsg) {
+ val tc6 = (a + 1) < b
+ }
+ assertRuntimeWarningLog(warnMsg) {
+ val tc7 = b == (a + 1)
+ }
+
+ // Comparisons - NO WARN: no chain, no implicit Int, or carry op chain
+ val nc1 = a == 5
+ val nc2 = a == b
+ val nc3 = (a + b) == c
+ val nc4 = (a +^ b) == 5
+ val nc5 = (a + d"1") == b
+ val nc6 = (a + b) == d"8'5"
+
assertNoWarnings()
}
test("Int32 algebraic simplifications") {
diff --git a/core/src/test/scala/CoreSpec/DFTypeSpec.scala b/core/src/test/scala/CoreSpec/DFTypeSpec.scala
index 4b8d284d7..b868273d4 100644
--- a/core/src/test/scala/CoreSpec/DFTypeSpec.scala
+++ b/core/src/test/scala/CoreSpec/DFTypeSpec.scala
@@ -88,17 +88,12 @@ class DFTypeSpec extends DFSpec:
)(
"""z(3, 0).prev"""
)
- assertCompileError(
- "At least one of the connection arguments must be a connectable DFHDL value (var/port)."
- )(
- """z(3, 0) <> all(0)"""
- )
+ // a partial selection of a connectable (var/port) is itself connectable
+ z(3, 0) <> all(0)
val tplx = tpl <> VAR
tplx._1 := 1
- assertCompileError(
- "At least one of the connection arguments must be a connectable DFHDL value (var/port)."
- )(
- """tplx._1 <> 1"""
- )
+ // struct/tuple field selection of a connectable is also connectable
+ val tplc = tpl <> VAR
+ tplc._1 <> 1
}
end DFTypeSpec
diff --git a/docs/include/hdelk_hook.py b/docs/include/hdelk_hook.py
index 58eaf475f..510ad6df3 100644
--- a/docs/include/hdelk_hook.py
+++ b/docs/include/hdelk_hook.py
@@ -1,16 +1,28 @@
import re
import json
+import hashlib
import subprocess
import tempfile
import os
from pyhocon import ConfigFactory
CACHE_DIR = os.path.join('.cache', 'plugin', 'hdelk')
+GENERATOR_PATH = os.path.join('docs', 'javascripts', 'hdelk.js')
+
+# The cache key incorporates the generator source so that any change to hdelk.js
+# automatically invalidates previously cached SVGs. A deterministic hash (md5) is used
+# so the cache is also reused across separate builds (unlike the randomized builtin hash).
+try:
+ with open(GENERATOR_PATH, 'r', encoding='utf-8') as _gen_file:
+ GENERATOR_SRC = _gen_file.read()
+except OSError:
+ GENERATOR_SRC = ''
def replace_hdelk(match):
width = match.group(1) or "100%" # Default to 100% if width is not specified
diagram_json = json.dumps(ConfigFactory.parse_string(match.group(2)), indent=2)
- diagram_id = f"hdelk-diagram-{hash(diagram_json)}" # Generate a unique ID
+ cache_key = hashlib.md5((diagram_json + GENERATOR_SRC).encode('utf-8')).hexdigest()
+ diagram_id = f"hdelk-diagram-{cache_key}" # Generate a unique ID
svg_file_path = os.path.join(CACHE_DIR, f"{diagram_id}.svg")
# Ensure the cache directory exists
diff --git a/docs/javascripts/hdelk.js b/docs/javascripts/hdelk.js
index 5734b6da2..cf8a8b0eb 100644
--- a/docs/javascripts/hdelk.js
+++ b/docs/javascripts/hdelk.js
@@ -90,6 +90,27 @@ var hdelk = (function() {
}
};
+ /**
+ * Returns the subset of a node's edges that are "feed-throughs": edges whose source and
+ * target are both ports of this same node (e.g. connecting an input port directly to an
+ * output port). These represent a direct internal wire and are rendered as a straight
+ * line across the node rather than routed by ELK (which would loop them around the
+ * outside, since boundary ports face outward).
+ * Must be called after ports and edges have been normalized.
+ * @param {object} child node under consideration
+ * @returns {Array} the feed-through edges
+ */
+ var feedThroughEdges = function( child ) {
+ if ( !child.edges || !child.ports ) return [];
+ var ownPortIds = {};
+ child.ports.forEach( function( p ) { if ( p && p.id ) ownPortIds[ p.id ] = true; } );
+ return child.edges.filter( function( e ) {
+ var s = e.sources && e.sources[ 0 ];
+ var t = e.targets && e.targets[ 0 ];
+ return s && t && ownPortIds[ s ] && ownPortIds[ t ];
+ } );
+ };
+
/**
* Takes the child object and recursively transforms sub-objects into a form that Elk.JS can use
* @param {object} child present child node under consideration
@@ -435,6 +456,21 @@ var hdelk = (function() {
} );
}
+ // Nodes with internal feed-through wires (an input port wired straight to an
+ // output port) place their label at the top and reserve vertical space between
+ // the top border and the ports. This keeps the straight wire(s), which are drawn
+ // at the port positions, below the label text so they never cross it. The reserved
+ // space is counted into the node size (NODE_LABELS PORTS), so the box grows to fit.
+ var ftCount = feedThroughEdges( child ).length;
+ if ( ftCount > 0 ) {
+ // Place the label at the top and pack the ports (where the feed-through wires
+ // attach) toward the bottom. The node is grown to fit a top label band plus one
+ // wire row per feed-through, so the wires stay clear below the label text.
+ child.layoutOptions[ 'elk.nodeLabels.placement' ] = 'V_TOP H_CENTER INSIDE';
+ child.layoutOptions[ 'elk.portAlignment.default' ] = 'END';
+ child.layoutOptions[ 'elk.nodeSize.minimum' ] = '(0, ' + ( 30 + ftCount * 22 ) + ')';
+ }
+
var children = child.children;
if ( children ) {
children.forEach( function( item, index ) {
@@ -506,8 +542,18 @@ var hdelk = (function() {
var edges = child.edges;
if ( edges ) {
+ // build a lookup of this node's own ports to detect feed-through edges
+ var ownPorts = {};
+ if ( child.ports )
+ child.ports.forEach( function( p ) { if ( p && p.id ) ownPorts[ p.id ] = p; } );
edges.forEach( function( item, index ) {
- edge( group, item, offsetX + child.x, offsetY + child.y );
+ var s = item.sources && item.sources[ 0 ];
+ var t = item.targets && item.targets[ 0 ];
+ if ( s && t && ownPorts[ s ] && ownPorts[ t ] )
+ // feed-through wire: draw a direct line between the two own ports
+ feedThroughWire( group, ownPorts[ s ], ownPorts[ t ], offsetX + child.x, offsetY + child.y );
+ else
+ edge( group, item, offsetX + child.x, offsetY + child.y );
} );
}
@@ -585,6 +631,23 @@ var hdelk = (function() {
return group;
}
+ /**
+ * Draws a direct internal wire between two ports of the same node (a feed-through),
+ * as a straight line from the source port center to the target port center, with a
+ * terminator at the target end to indicate direction.
+ */
+ var feedThroughWire = function( draw, srcPort, tgtPort, offsetX, offsetY ) {
+ var group = draw.group();
+ var x1 = offsetX + srcPort.x + srcPort.width / 2;
+ var y1 = offsetY + srcPort.y + srcPort.height / 2;
+ var x2 = offsetX + tgtPort.x + tgtPort.width / 2;
+ var y2 = offsetY + tgtPort.y + tgtPort.height / 2;
+ group.line( x1, y1, x2, y2 ).stroke( { color: edge_color, width: edge_width } );
+ var terminator = ( edge_width < 3 ) ? 3 : edge_width;
+ group.rect( terminator * 2, terminator * 2 ).attr( { fill: edge_color } ).move( x2 - terminator, y2 - terminator );
+ return group;
+ }
+
var edge = function( draw, edge, offsetX, offsetY ) {
var group = draw.group();
diff --git a/docs/transitioning/from-verilog/index.md b/docs/transitioning/from-verilog/index.md
index 4f012353f..3b191d7c5 100644
--- a/docs/transitioning/from-verilog/index.md
+++ b/docs/transitioning/from-verilog/index.md
@@ -520,9 +520,9 @@ There is no `>>>` operator in DFHDL. The type of the LHS determines the shift se
///
-/// admonition | Bit/Boolean Operators: `|`/`&` and `||`/`&&`
+/// admonition | Bit/Boolean Operators: `|`/`&`/`~` and `||`/`&&`/`!`
type: verilog
-In DFHDL, `||`/`&&` and `|`/`&` are interchangeable on `Bit` and `Boolean` types. The generated Verilog operator depends on the LHS type: `Bit` produces bitwise `|`/`&`, `Boolean` produces logical `||`/`&&`.
+In DFHDL, `||`/`&&`/`!` and `|`/`&`/`~` are interchangeable on `Bit` and `Boolean` types. The generated Verilog operator depends on the LHS type: `Bit` produces bitwise `|`/`&`/`~`, `Boolean` produces logical `||`/`&&`/`!`.
```sv linenums="0" title="Verilog"
@@ -669,21 +669,23 @@ val bit5 = data(5) // single bit
-Bit-slicing and single-bit access work on `Bits`, `UInt`, and `SInt` values with the same syntax. Slicing preserves the source type:
+Bit-slicing and single-bit access work on `Bits`, `UInt`, and `SInt` values with the same syntax, including the `.msbits(W)` and `.lsbits(W)` convenience methods. As in Verilog, a slice is a bit-level operation and yields an unsigned result — `SInt` slices return `UInt`, not `SInt`:
| Source type | Slice result |
|---|---|
| `Bits[N]` | `Bits` |
| `UInt[N]` | `UInt` |
-| `SInt[N]` | `SInt` |
-
-You do **not** need to convert `SInt` to `Bits` before slicing — the result is already `SInt`:
+| `SInt[N]` | `UInt` (chain `.bits.sint` to re-interpret as signed) |
```scala
-val prod = SInt(16) <> VAR
-val top8 = prod(15, 8) // 8-bit SInt slice
-val sign = prod(15) // single bit access
+val prod = SInt(16) <> VAR
+val top8U = prod(15, 8) // UInt[8]: raw upper byte
+val top8M = prod.msbits(8) // UInt[8]: same as prod(15, 8)
+val top8S = prod(15, 8).bits.sint // SInt[8]: sign-preserving truncation
+val sign = prod(15) // single-bit access (Bit)
```
+
+To recover signed semantics on a slice, chain `.bits.sint` (re-interpret the bits as signed, same width). Do **not** use `.signed` for this — `.signed` is a numeric conversion that widens by one zero-extension bit, which is not what slice migration wants.
///
/// admonition | Arithmetic with Signed Values and Constants
@@ -793,7 +795,7 @@ See [Arithmetic Operations][arithmetic-ops] and [Carry Arithmetic][carry-ops] fo
type: verilog
In Verilog, unsized integer literals are 32-bit. When combined with narrower signals, the wider literal causes the entire expression to evaluate at 32-bit width via context-dependent propagation. This prevents intermediate overflow in expressions like `(a + b + c + d) / 4`.
-In DFHDL, Scala `Int` literals are implicitly converted to minimum-width bit-accurate types (e.g., `4` becomes `UInt[3]`). Each arithmetic operation independently uses the LHS width, so intermediate results can overflow before reaching a division or shift.
+In DFHDL, Scala `Int` literals are implicitly converted to minimum-width bit-accurate types (e.g., `4` becomes `UInt[3]`). Each arithmetic operation independently uses the LHS width, so intermediate results can overflow before reaching a division, shift, comparison, or wider-target assignment.
DFHDL detects this pattern at elaboration and issues a warning. See [Implicit Scala `Int` and Verilog-semantics mismatch][arithmetic-ops] for the full list of warning triggers.
diff --git a/docs/user-guide/connectivity/index.md b/docs/user-guide/connectivity/index.md
index bb969be08..8da704c1b 100755
--- a/docs/user-guide/connectivity/index.md
+++ b/docs/user-guide/connectivity/index.md
@@ -4,19 +4,26 @@ typora-copy-images-to: ./
[](){#connectivity}
# Connectivity
+DFHDL wires designs together with two kinds of operators:
+
+* The connection operator `<>`.
+* The assignment operators `:=` (blocking) and `:==` (non-blocking).
+
+This section focuses on the `<>` connection operator and how it relates to the assignment operators.
+
## Key Differences Between `<>` and `:=`/`:==`
-| Criteria | `<>` Connection | `:=`/`:==` Assignment |
-| ----------------------------------- | :----------------------------------------------------------- | :----------------------------------------------------------- |
-| Directionality &
Commutativity | The operator is commutative, meaning `a <> b` is equivalent to b `b <> a`. One argument is the *producer*, while the other *consumer*. The dataflow direction is sensitive to the context in which the operator is applied. | The operator is non-commutative, meaning `a := b` determines that `b` is the *producer*, transferring data to the *consumer* `a`. |
-| Mutation | A consumer can only be connected once. | Consumer assignments are unlimited. |
-| Statement Order | Connections statements can be placed in any order. | Assignment statements |
+| Criteria | `<>` Connection | `:=`/`:==` Assignment |
+| --- | :--- | :--- |
+| Directionality &
Commutativity | Commutative: `a <> b` is equivalent to `b <> a`. One side is the *producer* and the other the *consumer*; the dataflow direction is inferred from the operands and the context in which the operator is applied. | Non-commutative: `a := b` makes `b` the *producer*, transferring data to the *consumer* `a`. |
+| Mutation | Each consumer bit can be connected at most once. A consumer may still receive several connections, as long as they target disjoint bit ranges (e.g. `y(3, 0) <> a` and `y(7, 4) <> b`). | A consumer bit can be assigned any number of times; the last assignment in program order wins. |
+| Statement Order | Connection statements can be placed in any order. | Assignment statements are order-sensitive. |
-##Connection `<>` Rules
+## Connection `<>` Rules
-###Port Direct Connections
+### Port Direct Connections
-The onnection operator `<>` is generally used to connect parent designs to their child designs (components) and connect between sibling designs (children of the same parent). Opposed to VHDL/Verilog, there is no need to go through 'signals' to connect sibling design ports, e.g.:
+The connection operator `<>` is generally used to connect parent designs to their child designs (components) and to connect between sibling designs (children of the same parent). Unlike VHDL/Verilog, there is no need to go through intermediate 'signals' to connect sibling design ports, e.g.:
@@ -67,7 +74,7 @@ children = [
-###Port Via Connections
+### Port Via Connections
```scala
class Plus1 extends DFDesign:
@@ -88,9 +95,9 @@ class Plus2 extends EDDesign:
val p1B = new Plus1():
this.x <> p1B_x
this.y <> p1B_y
- p1A_x <> x
- p1B_x <> p1A_y
- y <> p1B_y
+ p1A_x <> x
+ p1B_x <> p1A_y
+ y <> p1B_y
end Plus2
```
@@ -98,7 +105,7 @@ end Plus2
type: dfhdl
```scastie main="top_Plus2"
-import dfhdl.*
+import dfhdl.*
class Plus1 extends DFDesign:
val x = UInt(8) <> IN
@@ -120,186 +127,152 @@ given options.CompilerOptions.PrintDFHDLCode = true
///
+### Connectable Value Connections
-
-###Dataflow Value Connections
-
-At least one of the connected sides must be a dataflow port (cannot connect two dataflow values together), e.g.:
+At least one side of a connection must be a *connectable* DFHDL value — a variable (`VAR`) or a port (`IN`/`OUT`/`INOUT`). Connecting two immutable values (e.g. two constants or two read-only aliases) is not allowed, e.g.:
```scala
-trait Conn1 {
- val port = DFUInt(8) <> OUT
- val temp1 = DFUInt(8)
- val temp2 = DFUInt(8)
- port <> temp1 //OK!
- temp1 <> temp2 //Bad connection! At least one connection side must be a port
-}
+class Conn1 extends DFDesign:
+ val port = UInt(8) <> OUT
+ val temp1 = UInt(8) <> VAR
+ val temp2 = UInt(8) <> VAR
+ port <> temp1 // OK: a port connected to a variable
+ temp1 <> temp2 // OK: both sides are connectable variables
```
+### Partial Selection Connections
-
-### Dataflow Input Port Assignment `:=` Rule
-
-An input port cannot be assigned to. A connection must be used to transfer data to an input port, e.g.:
+A partial selection of a connectable value is itself connectable. This includes bit selection and bit-range selection of `Bits`/`UInt`/`SInt` values, as well as struct field and tuple element selection. This lets you drive a single port or variable from several sources, as long as each bit is driven exactly once (see [Multiple Connections](#multiple-connections)):
```scala
-trait IO extends DFDesign {
- val in = DFUInt(8) <> IN
- val out = DFUInt(8) <> OUT
- out := in //OK! Can assign internally to an output port
-}
-trait Assign1 extends DFDesign {
- val io = new IO{}
- io.in := 1 //Bad assignment! Must use a connection annotation
- io.in <> 1 //OK!
- io.out := 1 //Bad assignment! Output ports can only be assigned internally
-}
+class Split extends EDDesign:
+ val hi = UInt(4) <> IN
+ val lo = UInt(4) <> IN
+ val y = Bits(8) <> OUT
+ y(7, 4) <> hi.bits
+ y(3, 0) <> lo.bits
```
+### Input Port Assignment `:=` Rule
-
-###Immutable Value Connections
-
-When connecting a port to an immutable value, the port must be a consumer, meaning the connection is done internally to an output port or externally to an input port, e.g.:
+An input port cannot be assigned to. A connection must be used to drive data into an input port, e.g.:
```scala
-trait IO extends DFDesign {
- val i = DFUInt(8) <> IN
- val o = DFUInt(8) <> OUT
- //For brevity, we consider every connection/assignment in this example separately.
- //We ignore multiple connection issues that should arise.
- o <> 1 //OK!
- i <> 1 //Bad connection! 1 is immutable (constant)
- i <> o.prev //Bad connection! o.prev is immutable
- i.prev <> o //OK!
-}
-trait IOUser extends DFDesign {
- val io = new IO {}
- io.i <> 1 //OK!
- io.o <> 1 //Bad connection! 1 is immutable
-}
+class IO extends DFDesign:
+ val in = UInt(8) <> IN
+ val out = UInt(8) <> OUT
+ out := in // OK: an output port can be assigned internally
+
+class Assign1 extends DFDesign:
+ val io = IO()
+ // io.in := 1 // Bad assignment! An input port cannot be assigned to
+ io.in <> 1 // OK: use a connection instead
+ // io.out := 1 // Bad assignment! An output port can only be assigned internally
```
-
-### Different Type Connections
+### Immutable Value Connections
-Connecting between different types is possible, but depends on the specific type: if it enables automatic conversion for the connection to succeed. Different port widths are considered different types and casting is required. An alias/casted/converted dataflow value is considered immutable for the connection (see above). Here are some examples:
+When connecting a port to an immutable value (such as a constant), the port must be the consumer. This means the connection is done internally to an output port or externally to an input port, e.g.:
```scala
-trait DifferentTypesConn extends DFDesign {
- val i = DFUInt(8) <> IN
- val o = DFUInt(8) <> OUT
- val ob9 = DFBits(9) <> OUT
-
- val u7 = DFUInt(7)
- val u9 = DFUInt(9)
- val b8 = DFBits(8)
-
- //For brevity, we consider every connection/assignment in this example separately.
- //We ignore multiple connection issues that should arise.
- u7 <> o //OK! u7 is automatically extended to connect to
- u7 <> i //Bad connection! u7 is considered immutable when extended to 8 bits
- o <> b8 //Bad connection! There is not automatic casting between bits and uint
- o <> b8.uint //OK!
- o.bits <> b8 //Bad connection! An alias of output port cannot be connected to
- //This may change in the future.
- o.bits := b8 //OK!
- u9 <> i //OK! In this example u9 is the consumer
- ob9 <> b8 //Bad connection! Bit vectors are NOT automatically extended.
- ob9 := b8 //Bad assignment! Bit vectors are NOT automatically extended.
-}
+class IO extends DFDesign:
+ val i = UInt(8) <> IN
+ val o = UInt(8) <> OUT
+ o <> 1 // OK: `o` (output) is the consumer of the constant `1`
+ // i <> 1 // Bad connection! `i` is a producer internally; a constant cannot drive into it
+
+class IOUser extends DFDesign:
+ val io = IO()
+ io.i <> 1 // OK: `io.i` is a consumer externally
+ // io.o <> 1 // Bad connection! `io.o` is a producer externally
```
-
-### Multiple Connections
+### Different Type Connections
-Two or more dataflow producers cannot be connected to the same consumer (a single producer can be connected to more than one consumer), e.g.:
+Connecting between different types requires the types to match, possibly through an explicit cast. Different widths are considered different types and require an explicit cast/resize. A casted/converted dataflow value is immutable for the purposes of the connection (see [above](#immutable-value-connections)), so it can only be used as a producer. Here are some examples:
```scala
-trait Gen extends DFDesign {
- val out1 = DFUInt(8) <> OUT init 1
- val out2 = DFUInt(8) <> OUT init 2
-}
-trait Conn2 extends DFDesign {
- val in1 = DFUInt(8) <> IN
- val in2 = DFUInt(8) <> IN
- val out = DFUInt(8) <> OUT
- val temp1 = DFUInt(8)
- temp1 <> in1 //OK!
- out <> in1 //Also OK! (Same producer can connect to more than one cosumer)
- temp1 <> in2 //Bad connection! Second producer connection to temp1
-
- val gen = new Gen {}
- val temp2 = DFUInt(8)
- val temp3 = DFUInt(8)
- gen.out1 <> temp2 //OK!
- gen.out1 <> temp3 //Also OK! (Same producer can connect to more than one cosumer)
- gen.out2 <> temp2 //Bad connection! Second producer connection to temp2
-}
+class DifferentTypesConn extends DFDesign:
+ val o = UInt(8) <> OUT
+ val ob9 = Bits(9) <> OUT
+ val b8 = Bits(8) <> VAR
+ val b9 = Bits(9) <> VAR
+ o <> b8.uint // OK: an explicit Bits-to-UInt cast is applied
+ ob9 <> b9 // OK: matching 9-bit Bits widths
+ // o <> b8 // Bad connection! There is no automatic casting between Bits and UInt
+ // ob9 <> b8 // Bad connection! Bit vectors are NOT automatically extended (8 vs 9)
+ // o.bits <> b8 // Bad connection! `.bits` is a type-cast alias (immutable) of the output port
```
+In contrast to type casts, *bit and bit-range selections* of a port (e.g. `o(3, 0)`) are connectable, as shown in [Partial Selection Connections](#partial-selection-connections).
+### Multiple Connections {#multiple-connections}
-###Mixing Assignments and Connections
-
-The same consumer cannot be both assigned to and connected to as the consumer, e.g.:
+A given consumer bit can be connected at most once. A single producer, however, can be connected to more than one consumer. Connecting two producers to the *same* consumer bit is an error:
```scala
-trait Conn3 extends DFDesign {
- val out1 = DFUInt(8) <> OUT
- val out2 = DFUInt(8) <> OUT
- val out3 = DFUInt(8) <> OUT
- out1 <> 1 //OK!
- out1 := 1 //Bad assignment! Cannot assign to a connected dataflow variable
-
- out2 := 2 //OK!
- out2 <> 2 //Bad connection! Cannot connect to an assigned dataflow variable
-
- out3 := 1 //OK!
- out3 := 2 //Also OK! (Multiple assignments are accepted)
-}
+class Conn2 extends DFDesign:
+ val in1 = UInt(8) <> IN
+ val in2 = UInt(8) <> IN
+ val out = UInt(8) <> OUT
+ val temp1 = UInt(8) <> VAR
+ temp1 <> in1 // OK
+ out <> in1 // Also OK! The same producer can connect to more than one consumer
+ // temp1 <> in2 // Bad connection! A second producer drives the same bits of `temp1`
```
+Because a partial selection is connectable, the same consumer may be driven by several connections that cover *disjoint* bit ranges:
+```scala
+class Conn3 extends EDDesign:
+ val a = UInt(4) <> IN
+ val b = UInt(4) <> IN
+ val out = Bits(8) <> OUT
+ out(3, 0) <> a.bits // OK: drives bits 3..0
+ out(7, 4) <> b.bits // OK: drives bits 7..4 (disjoint from the above)
+ // out(0) <> a(0) // Bad connection! bit 0 is already driven above
+```
-### Connection Statement Order
+### Mixing Assignments and Connections
-The connection `<>` statement ordering does not matter.
+The same consumer bit cannot be both assigned to (`:=`/`:==`) and connected to (`<>`), e.g.:
-
-### Connection and Initial Conditions
+```scala
+class Conn4 extends RTDesign:
+ val out1 = UInt(8) <> OUT
+ val out2 = UInt(8) <> OUT
+ val out3 = UInt(8) <> OUT
+ out1 <> 1 // OK
+ // out1 := 1 // Bad assignment! Cannot assign to a connected value
+
+ out2 := 2 // OK
+ // out2 <> 2 // Bad connection! Cannot connect to an assigned value
+
+ out3 := 1 // OK
+ out3 := 2 // Also OK! Multiple assignments to the same bits are accepted
+```
-A connection `<>` transfers initial conditions to the consumer, but if the consumer is already initialized then the consumer keeps its existing initial conditions. Here is an example:
+Different bits of the same value may be split between an assignment and a connection, as long as the bit ranges are disjoint:
```scala
-trait IOInit extends DFDesign {
- val i = DFUInt(8) //init = (11, 12) Overriden from TopInit connection
- val o = DFUInt(8) init 5 //init = (5) Not overridden due to assignment
- val ip = i.prev //init = (12) Prev moves down the init queue
- o := ip
-}
-trait TopInit extends DFDesign {
- val i = DFUInt(8) <> IN.init(1, 2) //init = (1, 2) The top-level initial conditions
- val o = DFUInt(8) <> OUT init 1 //init = (1) Keeps its initializaion
- val iPlus10 = in + 10 //init = (11, 12) Arithmetics affect init
- val io = new IOInit {}
- io.i <> inPlus10
- o <> io.o
-}
+class Conn5 extends RTDesign:
+ val a = Bits(8) <> IN
+ val y = Bits(8) <> OUT
+ y(3, 0) <> a(3, 0) // OK: connection drives bits 3..0
+ y(7, 4) := a(7, 4) // OK: assignment drives bits 7..4 (disjoint from the above)
```
-
+### Connection Statement Order
-We learn from the above that port initial conditions are often overridden due to connections. So why should we apply initial conditions to a port? Answer: If we want to define what happens when a port is open (unconnected). Read the next two sections for more information.
+The order of `<>` connection statements does not matter.
-
-###Open (Unconnected) Ports {#connectivity-open-ports}
+### Open (Unconnected) Ports {#connectivity-open-ports}
-Ports have two connection sides: a consumer side and a producer side. Typically ports have both sides connected, except for top-level ports. When either port side is unconnected, we refer to it as *open*, and expect the following behavior:
+Ports have two connection sides: a consumer side and a producer side. Typically both sides are connected, except for top-level ports. When either side is unconnected, the port is *open*, with the following behavior:
-* When the port consumer side is open, the port produces tokens according to its initial condition. Uninitialized open-consumer ports generate bubble tokens.
+* When the port consumer side is open, the port produces its initial value. An uninitialized open-consumer port produces a bubble (undefined) value.
-* When the port producer side is open (unless it is a top-level output port), the port is considered as not used, and is pruned during compilation. All dataflow streams that are only used by this port will be pruned as well.
+* When the port producer side is open (unless it is a top-level output port), the port is considered unused and is pruned during compilation, along with any logic used only to drive it.
To explicitly mark a port as unconnected, use the `OPEN` keyword with the `<>` connection operator:
@@ -317,191 +290,250 @@ class Top extends EDDesign:
val sensor_inst = Sensor()
sensor_inst.din <> din
sensor_inst.dout <> dout
- sensor_inst.debug <> OPEN // explicitly unconnected
+ sensor_inst.debug <> OPEN // explicitly unconnected
```
-**Note**: `OPEN` can only be used with the `<>` connection operator. Using it with `:=` assignment will result in a compile error.
+/// admonition
+ type: note
+`OPEN` can only be used with the `<>` connection operator. Using it with `:=` assignment results in a compile error.
+///
-
-### Initial Condition Cyclic Loop Errors
+## Valid Connection and Assignment Examples
-Connections enable dataflow feedbacks and even dataflow dependency loops. There is no problem in dependency loops, other than pipelining limitations (see chapter TBD for more information). However, if we only apply connections and references that transfer initial conditions, we end up with a cyclic dependency for initial condition which is illegal. Therefore to enable dependency loops, at least one link in the loop must be an assignment, which has an implicit state and does not affect initial conditions. Consider the following examples:
+
```scala
-trait IO1 extends DFDesign {
- val i = DFUInt(8) <> IN
- val o = DFUInt(8) <> OUT
- o <> i //Connection transfers initial conditions from i to o
-}
-trait BadConnLoop1 extends DFDesign {
- val o = DFUInt(8) <> OUT
- val io = new IO1 {}
- io.i <> io.o //Bad connection! An initial conditions cyclic loop
- o <> io.o
-}
-trait IO2 extends DFDesign {
- val i = DFUInt(8) <> IN
- val o = DFUInt(8) <> OUT
- o <> i.prev //prev transfers initial conditions
-}
-trait BadConnLoop2 extends DFDesign {
- val o = DFUInt(8) <> OUT
- val io = new IO2 {}
- io.i <> io.o //Bad connection! An initial conditions cyclic loop
- o <> io.o
-}
-trait IO3 extends DFDesign {
- val i = DFUInt(8) <> IN
- val o = DFUInt(8) <> OUT
- o := i //Assignment does not affect initial conditions and therefore breaks the loop
-}
-trait OKConnLoop extends DFDesign {
- val o = DFUInt(8) <> OUT
- val io = new IO3 {}
- io.i <> io.o //OK!
- o <> io.o
-}
+class IODesign extends DFDesign:
+ val i = UInt(8) <> IN
+ val o = UInt(8) <> OUT
+ o <> i
```
-
-
-**Note**: when following the drawing convention within this document, we want to avoid a double-lined loop in order to avoid a cyclic initial conditions dependency.
-
-
-##Valid Connection and Assignment Examples
-
-```scala
-trait IODesign extends DFDesign {
- val i = DFUInt(8) <> IN
- val o = DFUInt(8) <> OUT
- o <> i
-}
+```hdelk width=100%
+stroke-width = 0
+children = [
+ {
+ id = IODesign
+ inPorts = [i]
+ outPorts = [o]
+ edges = [[IODesign.i, IODesign.o]]
+ }
+]
```
-
+
---
+
+
```scala
-trait IODesign1 extends DFDesign {
- val i = DFUInt(8) <> IN
- val o = DFUInt(8) <> OUT
- val tmp = DFUInt(8)
+class IODesign1 extends DFDesign:
+ val i = UInt(8) <> IN
+ val o = UInt(8) <> OUT
+ val tmp = UInt(8) <> VAR
tmp <> i
- o <> tmp
-}
+ o <> tmp
+```
+
+```hdelk width=100%
+stroke-width = 0
+children = [
+ {
+ id = IODesign1
+ inPorts = [i]
+ outPorts = [o]
+ children = [{ id = tmp }]
+ edges = [
+ [IODesign1.i, tmp]
+ [tmp, IODesign1.o]
+ ]
+ }
+]
```
-
+
---
+
+
```scala
-trait IODesign2 extends DFDesign {
- val i1 = DFUInt(8) <> IN
- val o1 = DFUInt(8) <> OUT
- val i2 = DFUInt(8) <> IN
- val o2 = DFUInt(8) <> OUT
+class IODesign2 extends DFDesign:
+ val i1 = UInt(8) <> IN
+ val o1 = UInt(8) <> OUT
+ val i2 = UInt(8) <> IN
+ val o2 = UInt(8) <> OUT
o1 <> i1
o2 <> i2
-}
```
-
+```hdelk width=100%
+stroke-width = 0
+children = [
+ {
+ id = IODesign2
+ inPorts = [i1, i2]
+ outPorts = [o1, o2]
+ edges = [
+ [IODesign2.i1, IODesign2.o1]
+ [IODesign2.i2, IODesign2.o2]
+ ]
+ }
+]
+```
+
+
---
+
+
```scala
-trait Container extends DFDesign {
- val i = DFUInt(8) <> IN
- val o = DFUInt(8) <> OUT
- val io = new IODesign {}
- i <> io.i //Connecting between owner input and child input
- io.o <> o //Connecting between child output and owner output
-}
+class Container extends DFDesign:
+ val i = UInt(8) <> IN
+ val o = UInt(8) <> OUT
+ val io = IODesign()
+ i <> io.i // connecting owner input to child input
+ io.o <> o // connecting child output to owner output
```
-
+```hdelk width=100%
+stroke-width = 0
+children = [
+ {
+ id = Container
+ inPorts = [i]
+ outPorts = [o]
+ children = [
+ { id = io, type = IODesign, highlight = 1, inPorts = [i], outPorts = [o] }
+ ]
+ edges = [
+ [Container.i, io.i]
+ [io.o, Container.o]
+ ]
+ }
+]
+```
+
+
---
+
+
```scala
-trait Container2 extends DFDesign {
- val i = DFUInt(8) <> IN
- val o = DFUInt(8) <> OUT
- val io1 = new IODesign {}
- val io2 = new IODesign {}
- i <> io1.i //Connecting between owner input and child input
- io1.o <> io2.i //Connecting between siblings (output <> input)
- io2.o <> o //Connecting between child output and owner output
-}
+class Container2 extends DFDesign:
+ val i = UInt(8) <> IN
+ val o = UInt(8) <> OUT
+ val io1 = IODesign()
+ val io2 = IODesign()
+ i <> io1.i // connecting owner input to child input
+ io1.o <> io2.i // connecting between siblings (output <> input)
+ io2.o <> o // connecting child output to owner output
```
-
+```hdelk width=100%
+stroke-width = 0
+children = [
+ {
+ id = Container2
+ inPorts = [i]
+ outPorts = [o]
+ children = [
+ { id = io1, type = IODesign, highlight = 1, inPorts = [i], outPorts = [o] }
+ { id = io2, type = IODesign, highlight = 1, inPorts = [i], outPorts = [o] }
+ ]
+ edges = [
+ [Container2.i, io1.i]
+ [io1.o, io2.i]
+ [io2.o, Container2.o]
+ ]
+ }
+]
+```
+
+
---
+
+
```scala
-trait Container3 extends DFDesign {
- val i = DFUInt(8) <> IN
- val o = DFUInt(8) <> OUT
- val io = new IODesign2 {}
- i <> io.i1 //Connecting between owner input and child input
- i <> io.i2 //Connecting between owner input and child input
+class Container3 extends DFDesign:
+ val i = UInt(8) <> IN
+ val o = UInt(8) <> OUT
+ val io = IODesign2()
+ i <> io.i1 // connecting owner input to child input
+ i <> io.i2 // connecting owner input to child input
o <> (io.o1 + io.o2)
-}
```
-
-
----
-
-```scala
-trait Container4 extends DFDesign {
- val i = DFUInt(8) <> IN
- val o = DFUInt(8) <> OUT
- val io = new IODesign2 {}
- i <> io.i1 //Connecting between owner input and child input
- io.i2 <> 5 //Connecting between constant value and child input
- o <> io.o2
-}
+```hdelk width=100%
+stroke-width = 0
+children = [
+ {
+ id = Container3
+ inPorts = [i]
+ outPorts = [o]
+ children = [
+ { id = io, type = IODesign2, highlight = 1, inPorts = [i1, i2], outPorts = [o1, o2] }
+ { id = op, label = "+" }
+ ]
+ edges = [
+ [Container3.i, io.i1]
+ [Container3.i, io.i2]
+ [io.o1, op]
+ [io.o2, op]
+ [op, Container3.o]
+ ]
+ }
+]
```
-
+
---
+
+
```scala
-trait Blank2 extends DFDesign {
- val i1 = DFUInt(8) <> IN
- val o1 = DFUInt(8) <> OUT
- val i2 = DFUInt(8) <> IN
- val o2 = DFUInt(8) <> OUT
-}
-trait Container5 extends DFDesign {
- val i = DFUInt(8) <> IN
- val o = DFUInt(8) <> OUT
- val io = new Blank2 {
- o1 <> i1 //Assignment
- o2 <> i2 //Internal connection
- }
- i <> io.i1 //Connecting between owner input and child input
- io.i2 <> io.o1 //External connection between child input/output creates a feeback
+class Container4 extends DFDesign:
+ val i = UInt(8) <> IN
+ val o = UInt(8) <> OUT
+ val io = IODesign2()
+ i <> io.i1 // connecting owner input to child input
+ io.i2 <> 5 // connecting a constant value to a child input
o <> io.o2
-}
```
-
+```hdelk width=100%
+stroke-width = 0
+children = [
+ {
+ id = Container4
+ inPorts = [i]
+ outPorts = [o]
+ children = [
+ { id = const, label = 5, constant = 1 }
+ { id = io, type = IODesign2, highlight = 1, inPorts = [i1, i2], outPorts = [o1, o2] }
+ ]
+ edges = [
+ [Container4.i, io.i1]
+ [const, io.i2]
+ [io.o2, Container4.o]
+ ]
+ }
+]
+```
-Note: although there is a feedback in this design, there is no circular initial conditions dependency.
+
---
-
## Magnet Port Connections
## Future Work
* In the future `<>` will be used to connect multi-port interfaces.
-* We will add support to treat an alias of a port as a port when connection `<>` rules are enforced.
-* Connecting between any ancestor which is not a parent and child. Currently not supported fully.
\ No newline at end of file
+* Connecting between any ancestor which is not a direct parent and child. Currently not fully supported.
diff --git a/docs/user-guide/naming/index.md b/docs/user-guide/naming/index.md
index fd6825238..4d7e166aa 100644
--- a/docs/user-guide/naming/index.md
+++ b/docs/user-guide/naming/index.md
@@ -1,12 +1,37 @@
# Naming
-## Reserved Names
+## Name Collisions & Shadowing
-When translating from Verilog, signal and module names may conflict with names already in scope from Scala or DFHDL. There are two categories of conflicts:
+When translating from Verilog/VHDL, signal, port, and module names may collide with names already in scope — Scala keywords, DFHDL built-ins, or your own design classes. This section is the single place that covers how to detect and resolve all such collisions.
-### Scala Reserved Keywords
+### General recommendation: Capitalize design-class names
-Scala keywords cannot be used directly as identifiers. Use backtick escaping:
+The simplest way to avoid the entire class of collisions between a **design class** and a **value** (port/variable) is a naming convention:
+
+- Name **design classes** with a `Capitalized` (PascalCase) name.
+- Name **ports and variables** with `camelCase` names.
+
+Because the two casings can never be identical, a design class and a value will never shadow each other. This is the **preferred convention** for new designs.
+
+```scala
+class Adder(val WIDTH: Int <> CONST = 8) extends EDDesign:
+ val a = UInt(WIDTH) <> IN
+ val b = UInt(WIDTH) <> IN
+ val y = UInt(WIDTH) <> OUT
+ // ...
+
+// In a parent design, the Capitalized class name never collides with camelCase values:
+val adder = Adder(WIDTH = 16)
+```
+
+/// admonition | Caveat: direct Verilog/VHDL translation that preserves original names
+ type: warning
+When you translate an existing Verilog/VHDL design and deliberately **preserve the original names** (so the generated HDL matches the source), you cannot always apply the Capitalized convention — the original names may already collide. These cases need the targeted resolutions below.
+///
+
+### Scala reserved keywords
+
+Scala keywords cannot be used directly as identifiers. Escape them with backticks:
`val`, `var`, `def`, `type`, `class`, `object`, `trait`, `enum`, `match`, `case`, `if`, `else`, `for`, `while`, `do`, `return`, `throw`, `try`, `catch`, `finally`, `yield`, `import`, `export`, `new`, `this`, `super`, `true`, `false`, `null`, `then`, `end`, `given`, `using`, `extension`, `with`, `abstract`, `final`, `override`, `sealed`, `lazy`, `private`, `protected`
@@ -16,9 +41,9 @@ val `val` = SInt(16) <> OUT
`val` <> 42
```
-### DFHDL Built-in Names
+### DFHDL built-in names
-`import dfhdl.*` brings DFHDL built-in functions and types into scope. If a user-defined class has the same name as a built-in, the built-in shadows the class. Known built-ins that commonly conflict with Verilog module names:
+`import dfhdl.*` brings DFHDL built-in functions and types into scope. If a user-defined class has the same name as a built-in, the built-in shadows the class. Built-ins that commonly collide with Verilog module names:
`abs`, `clog2`, `max`, `min`, `all`, `Bit`, `Bits`, `UInt`, `SInt`
@@ -27,15 +52,39 @@ val `val` = SInt(16) <> OUT
class abs(val DATA_WIDTH: Int <> CONST = 8) extends EDDesign:
// ...
-// In the parent design, `abs(...)` resolves to the built-in function.
+// In the parent design, `abs(...)` resolves to the built-in function, not the class.
// Fix: create a type alias before instantiation
type AbsModule = abs
val u_abs = AbsModule(DATA_WIDTH = 16)
```
+### Design-class name colliding with a value name
+
+A common translation collision is a **design class** whose name is the same as a port or variable in scope (or a built-in). When the bare name `design_class_name(...)` resolves to the value instead of the class, use one of these two fixes:
+
+**1. Explicitly instantiate with `new`.** The `new` keyword forces resolution to the class constructor, sidestepping the shadowing value:
+
+```scala
+// `design_class_name` the value is in scope and shadows the class
+val u = new design_class_name(DATA_WIDTH = 16)
+```
+
+**2. Use a Capitalized class name plus `@targetName` to preserve the emitted name.** Rename the Scala class to a Capitalized identifier (which can no longer collide with a camelCase value) and pin the original lowercase name onto the generated HDL with `@targetName`:
+
+```scala
+import scala.annotation.targetName
+
+@targetName("design_class_name")
+class DesignClassName(val DATA_WIDTH: Int <> CONST = 8) extends EDDesign:
+ // ...
+
+// No collision with values; generated HDL module is still named "design_class_name"
+val u = DesignClassName(DATA_WIDTH = 16)
+```
+
## Resolution Patterns
-### Backtick Escaping
+### Backtick escaping
For Scala keywords used as signal names:
@@ -44,12 +93,12 @@ val `type` = UInt(8) <> IN
val `match` = Bit <> OUT
```
-### `@targetName` Annotation
+### `@targetName` annotation
When a Scala-side name must differ from the generated HDL name, use `@targetName` to set the hardware name explicitly. This is useful when:
-- A port name conflicts with a sub-module class name in the same design
-- You want to rename a Scala identifier but preserve the original Verilog port name
+- A port name conflicts with a sub-module class name in the same design.
+- You want to rename a Scala identifier but preserve the original Verilog port/module name (see [Design-class name colliding with a value name](#design-class-name-colliding-with-a-value-name)).
```scala
import scala.annotation.targetName
@@ -63,7 +112,7 @@ val kernel_out = Bits(WIDTH) <> OUT
val u_kernel = kernel()
```
-### Type Alias for Class Name Conflicts
+### Type alias for class-name conflicts
When a class name conflicts with a DFHDL built-in function:
diff --git a/docs/user-guide/processes/index.md b/docs/user-guide/processes/index.md
index 0ed094bf5..e5355904a 100644
--- a/docs/user-guide/processes/index.md
+++ b/docs/user-guide/processes/index.md
@@ -129,6 +129,22 @@ class CombLogic extends EDDesign:
y := a + b
```
+/// admonition | The inline single-line `process(all): stmt` form does not parse
+ type: warning
+A process body must be a **block**, not an inline statement on the same line as the `process(...)` colon. Writing the body inline like `process(all): y := a + b` does **not** parse. Use one of these two accepted forms instead:
+
+```scala
+// 1. Braces around the body (body may be on the same line)
+process(all) { y := a + b }
+
+// 2. Colon with the body on the next, indented line
+process(all):
+ y := a + b
+```
+
+This applies to every process form (`process(sig)`, `process(all)`, `process(clk)`, etc.).
+///
+
### Forever process: `process.forever` / `process`
A process with no sensitivity list runs continuously. It is allowed in RT and ED, but **not** in DF. The shorthand `process:` (no arguments) is rewritten by the compiler to `process.forever`.
@@ -173,6 +189,54 @@ process(clk, rst):
out :== nextVal
```
+For the **Verilog-style async reset** pattern, put the edges in the sensitivity list and branch on reset only:
+
+```scala
+process(clk.rising, rst.rising):
+ if (rst)
+ out :== 0
+ else
+ out :== nextVal
+```
+
+/// admonition | ED is a faithful mirror of Verilog/VHDL — write synthesizable patterns
+ type: warning
+The ED domain is intentionally a low-level, faithful mapping to Verilog `always` blocks / VHDL `process` blocks. **DFHDL does not enforce synthesizability in the ED domain.** If you describe a non-synthesizable process pattern, the generated Verilog/VHDL will faithfully reflect that pattern — and downstream synthesis (or even a parser like Yosys) may reject it.
+
+It is your responsibility to write process bodies that match a synthesizable template. In particular, when an edge is **already in the sensitivity list**, do not re-check that edge inside the body — the body has already been triggered by it.
+
+**Non-synthesizable** (clock edge appears both in sensitivity list and as a nested `else if`):
+
+```scala
+// BAD: emits `else if (posedge clk)` inside an always_ff — not valid Verilog
+process(clk.rising, rst.rising):
+ if (rst)
+ x :== 0
+ else if (clk.rising) // redundant — body already runs on rising clk
+ x :== x + 1
+```
+
+**Synthesizable** equivalents — pick the style that matches the intent:
+
+```scala
+// Verilog-style async reset: edges in the sensitivity list, branch on reset only
+process(clk.rising, rst.rising):
+ if (rst)
+ x :== 0
+ else
+ x :== x + 1
+
+// VHDL-style: list the signals, branch on reset, then on the clock edge
+process(clk, rst):
+ if (rst)
+ x :== 0
+ else if (clk.rising)
+ x :== x + 1
+```
+
+The rule of thumb: an edge qualifier (`.rising` / `.falling`) belongs in **either** the sensitivity list **or** an `if` inside the body — not both for the same signal.
+///
+
## Assignments inside processes
### Blocking assignment `:=`
diff --git a/docs/user-guide/type-system/index.md b/docs/user-guide/type-system/index.md
index 43cec862f..231ce1ab7 100755
--- a/docs/user-guide/type-system/index.md
+++ b/docs/user-guide/type-system/index.md
@@ -1617,7 +1617,8 @@ val u = e.uint // Enum -> UInt (encoding-dependent width)
Applies to: `Bits`, `UInt`, `SInt`
-- **Range slice**: `value(hi, lo)` extracts bits `hi` down to `lo`, returning a narrower value of the **same type** (`Bits` → `Bits`, `UInt` → `UInt`, `SInt` → `SInt`).
+- **Range slice**: `value(hi, lo)` extracts bits `hi` down to `lo`. A slice is a bit-level operation and produces an unsigned result: `Bits` → `Bits`, `UInt` → `UInt`, `SInt` → `UInt`. This matches Verilog's "slices are unsigned" convention. To recover signed bit-semantics on an `SInt` slice, chain `.bits.sint` to re-interpret the slice as signed (same width). Do **not** use `.signed` for this — `.signed` is a numeric conversion that adds a zero-extension sign bit, widening by 1.
+- **Top/bottom slice**: `value.msbits(W)` returns the top `W` bits and `value.lsbits(W)` returns the bottom `W` bits — same unsigned-result rule as range slicing (`Bits` → `Bits`, `UInt` → `UInt`, `SInt` → `UInt`). Equivalent to `value(N-1, N-W)` and `value(W-1, 0)` respectively, but without needing to spell out the indices.
- **Single-bit access**: `value(idx)` returns the bit at position `idx` (as `Bit`). The index can be a static integer or a dynamic `UInt` variable.
```scala
@@ -1625,10 +1626,16 @@ val b8 = Bits(8) <> VAR
val u8 = UInt(8) <> VAR
val s8 = SInt(8) <> VAR
-// Range slicing — preserves the original type
-val b4 = b8(7, 4) // Bits[4]: upper nibble
-val u4 = u8(3, 0) // UInt[4]: lower nibble
-val s4 = s8(3, 0) // SInt[4]: lower nibble
+// Range slicing — always produces an unsigned result
+val b4 = b8(7, 4) // Bits[4]: upper nibble
+val u4 = u8(3, 0) // UInt[4]: lower nibble
+val u4FromS = s8(3, 0) // UInt[4]: SInt slice is unsigned
+val s4 = s8(7, 4).bits.sint // SInt[4]: sign-preserving truncation via re-interpret
+
+// Top/bottom slicing — convenience for `(N-1, N-W)` / `(W-1, 0)`
+val bTop4 = b8.msbits(4) // Bits[4]: top 4 bits, same as b8(7, 4)
+val uBot4 = u8.lsbits(4) // UInt[4]: bottom 4 bits, same as u8(3, 0)
+val sTop4 = s8.msbits(4) // UInt[4]: top 4 bits of SInt, still unsigned
// Single-bit access
val msb = b8(7) // Bit
@@ -1661,6 +1668,15 @@ val data = Bits(8) <> VAR init all(0)
val pos = UInt(4) <> VAR init 0 // 4-bit, but Bits(8) needs UInt(3)
val bit_out = data(pos.resize) // .resize adjusts to UInt(3) automatically
```
+
+The same `.resize` trick applies to **any** dynamic index, including writes into a memory/vector when the index comes from a wider source such as a slice of a larger `UInt`. The index width is checked against `clog2` of the indexed size, so let `.resize` reconcile it:
+```scala
+val mem = Bits(8) X 16 <> VAR // 16-deep memory, needs a UInt(4) index
+val idx = UInt(8) <> IN // wider index source (e.g. a sliced address)
+process(clk):
+ if (clk.rising)
+ mem(idx.resize) :== din // .resize adjusts idx to UInt(4) for the write
+```
///
### Width Adjustment {#width-adjustment}
@@ -1717,7 +1733,8 @@ These operations propagate constant modifiers, meaning that if all arguments are
| `lhs & rhs` | Logical AND | The LHS argument must be a `Bit`/`Boolean` DFHDL value. The RHS must be a `Bit`/`Boolean` candidate. | LHS-Type DFHDL value |
| `lhs | rhs` | Logical OR | The LHS argument must be a `Bit`/`Boolean` DFHDL value. The RHS must be a `Bit`/`Boolean` candidate. | LHS-Type DFHDL value |
| `lhs ^ rhs` | Logical XOR | The LHS argument must be a `Bit`/`Boolean` DFHDL value. The RHS must be a `Bit`/`Boolean` candidate. | LHS-Type DFHDL value |
-| `!lhs` | Logical NOT | The argument must be a `Bit`/`Boolean` DFHDL value. | LHS-Type DFHDL value |
+| `!lhs` | Logical NOT | The argument must be a `Bit`/`Boolean` DFHDL value. | LHS-Type DFHDL value |
+| `~lhs` | Logical NOT | The argument must be a `Bit`/`Boolean` DFHDL value. | LHS-Type DFHDL value |
///
```scala
@@ -1748,9 +1765,9 @@ val e3 = 0 ^ true
val sc: Boolean = true && true
```
-/// admonition | Logical `||`/`&&` and bitwise `|`/`&` on Bit and Boolean values
+/// admonition | Logical `||`/`&&`/`!` and bitwise `|`/`&`/`~` on Bit and Boolean values
type: tip
-In DFHDL, the operators `||` and `&&` are equivalent to `|` and `&`, respectively, when applied on either DFHDL `Bit` or `Boolean` types. In Verilog, the actual operator printed depends on the LHS argument of the operation: if it's `Bit`, the operator will be `|`/`&`; if it's `Boolean`, the operator will be `||`/`&&`.
+In DFHDL, the operators `||`, `&&`, and `!` are equivalent to `|`, `&`, `~`, respectively, when applied on either DFHDL `Bit` or `Boolean` types. In Verilog, the actual operator printed depends on the LHS argument of the operation: if it's `Bit`, the operator will be `|`/`&`/`~`; if it's `Boolean`, the operator will be `||`/`&&`/`!`.
///
/// details | Transitioning from Verilog
@@ -1764,7 +1781,8 @@ Under the ED domain, the following operations are equivalent:
| `lhs & rhs` | `lhs & rhs` | `lhs && rhs` |
| `lhs | rhs` | `lhs | rhs` | `lhs || rhs` |
| `lhs ^ rhs` | `lhs ^ rhs` | `lhs ^ rhs` |
-| `!lhs` | `!lhs` | `!lhs` |
+| `!lhs` | `~lhs` | `!lhs` |
+| `~lhs` | `~lhs` | `!lhs` |
///
/// details | Transitioning from VHDL
@@ -2025,36 +2043,75 @@ u9 := sum // resized from 8 to 9, no carry promotion
/// admonition | Implicit Scala `Int` and Verilog-semantics mismatch
type: warning
-In Verilog, unsized integer literals are 32-bit. When such a literal appears in an expression like `(a + b + c + d) / 4`, Verilog's context-dependent width propagation widens the entire expression to 32 bits, preventing intermediate overflow.
+Verilog and DFHDL disagree on the width of an unsized integer literal, and that disagreement determines whether intermediate `+`/`-`/`*` operations should be treated as carry (widening) or non-carry (modular) operations:
+
+- In **Verilog**, an unsized integer literal is 32-bit, and context-dependent width propagation widens narrower operands up to the literal's width. Intermediate `+`/`-`/`*` therefore effectively act as carry operations and cannot overflow into the surrounding `/`, `%`, shift, comparison, or wider-target assignment.
+- In **DFHDL**, a Scala `Int` literal (or DFHDL `Int` parameter) adapts to the minimum bit-accurate width of the surrounding bit-accurate value. Intermediate `+`/`-`/`*` stay at that narrow width and are modular, so the chain can overflow before the surrounding non-modular operation is applied.
+
+When DFHDL detects an arithmetic chain whose Verilog vs DFHDL result may diverge because of an implicit `Int`, it issues an **elaboration warning** so you can pick the intent you actually want:
-In DFHDL, a Scala `Int` literal like `4` is implicitly converted to the minimum bit-accurate width (`UInt[3]` for value 4). Each `+` independently uses the LHS width, so intermediate additions stay at the LHS width (e.g., 8 bits) and can overflow before the `/` is applied. Similarly, the Verilog pattern of "forcing larger evaluation" by adding a zero constant (e.g., `(a + b + 0) >> 1`) does not widen the expression in DFHDL.
+- **Accept overflow** (DFHDL modular semantics): replace the implicit `Int` operand(s) with explicit bit-accurate literals (`d"..."`/`sd"..."`). The warning is silenced because the operand is no longer treated as a widening `Int`.
+- **Prevent overflow** (Verilog widening semantics): use carry operations (`+^`, `-^`, `*^`) to widen the chain so it cannot overflow at the operand widths involved. The warning is silenced because the chain is no longer a narrow non-carry chain.
-DFHDL issues an **elaboration warning** when it detects these patterns:
+The patterns DFHDL flags are:
-**1. Non-modular operation with implicit `Int` and overflowing chain:**
-A `/` or `%` operation has an implicit Scala `Int` (or DFHDL `Int`) operand, and the other operand contains anonymous sub-32-bit `+`/`-`/`*` operations.
+**1. Non-modular operation with implicit `Int`:**
+A `/` or `%` operation where (a) one operand is an implicit Scala `Int` (or DFHDL `Int`) and the other contains anonymous sub-32-bit `+`/`-`/`*` operations, OR (b) one operand is itself an anonymous narrow `+`/`-`/`*` whose direct arguments include an implicit `Int`.
```scala
val a, b = UInt(8) <> VAR
-val t1 = (a + b) / 4 // WARNING: a + b can overflow at 8-bit
-val t2 = (a * 3 + b) % 3 // WARNING: a * 3 + b can overflow
-val t3 = a / 4 // OK: no intermediate overflow possible
-```
-
-**2. Shift with implicit `Int` inside the expression chain ("forcing larger evaluation"):**
+val t1 = (a + b) / 4 // WARNING: a + b can overflow at 8-bit
+val t2 = (a * 3 + b) % 3 // WARNING: a * 3 + b can overflow
+val t3 = (a + 1) / b // WARNING: 1 is implicit Int inside the chain
+val t4 = a / 4 // OK: no intermediate overflow possible
+// Accept overflow: replace implicit Ints with bit-accurate literals
+val t1a = (a + b) / d"3'4" // OK: 4 is explicit, modular semantics accepted
+val t2a = (a * d"3" + b) % d"2'3" // OK: all literals are bit-accurate
+val t3a = (a + d"1") / b // OK: 1 is explicit
+// Prevent overflow: use carry operations to widen the chain (matches Verilog)
+val t1c = (a +^ b) / 4 // OK: carry add -> UInt[9], cannot overflow
+val t2c = (a *^ 3 +^ b) % 3 // OK: carry mul + carry add
+val t3c = (a +^ 1) / b // OK: carry add widens chain before divide
+```
+
+**2. Comparison with implicit `Int` and narrow chain:**
+A comparison (`==`, `!=`, `<`, `>`, `<=`, `>=`) where (a) one operand is an implicit `Int` and the other contains an anonymous narrow `+`/`-`/`*` chain, OR (b) one operand is itself an anonymous narrow `+`/`-`/`*` with an implicit `Int` directly inside it.
+```scala
+val t5 = (a + b) == 5 // WARNING: chain wraps to 5 only in DFHDL when a + b = 261
+val t6 = (a + 1) == b // WARNING: a + 1 wraps to 0 only in DFHDL when a = 255
+val t7 = (a + 1) == 5 // WARNING: chain has implicit Int inside
+val t8 = a == 5 // OK: no chain
+val t9 = (a + b) == b // OK: no implicit Int involved
+// Accept overflow: replace implicit Ints with bit-accurate literals
+val t5a = (a + b) == d"8'5" // OK: 5 is explicit, modular compare accepted
+val t6a = (a + d"1") == b // OK: 1 is explicit
+val t7a = (a + d"1") == d"8'5" // OK: both literals are bit-accurate
+// Prevent overflow: widen the chain with carry so it sees the true value
+val t5c = (a +^ b) == 5 // OK: carry add -> UInt[9], 5 adapts to 9 bits
+val t7c = (a +^ 1) == 5 // OK: carry add prevents wrap
+```
+
+**3. Shift with implicit `Int` inside the expression chain ("forcing larger evaluation"):**
A `>>` or `<<` operation whose LHS expression contains both an implicit `Int` operand and sub-32-bit `+`/`-`/`*` operations.
```scala
-val t4 = (a + b + 0) >> 1 // WARNING: + 0 forces 32-bit in Verilog, not in DFHDL
-val t5 = (a + b) >> 2 // OK: no implicit Int in the + chain, Verilog also loses carry
+val t10 = (a + b + 0) >> 1 // WARNING: + 0 forces 32-bit in Verilog, not in DFHDL
+val t11 = (a + b) >> 2 // OK: no implicit Int in the + chain, Verilog also loses carry
+// Accept overflow: replace the implicit Int with a bit-accurate literal
+val t10a = (a + b + d"1'0") >> 1 // OK: 0 is explicit
+// Prevent overflow: widen the chain with carry before shifting
+val t10c = (a +^ b +^ 0) >> 1 // OK: carry chain cannot overflow
```
-**3. Assignment to wider target with implicit `Int` in the chain:**
+**4. Assignment to wider target with implicit `Int` in the chain:**
An anonymous expression assigned to a wider target contains both an implicit `Int` and sub-32-bit `+`/`-`/`*` operations.
```scala
val sum = UInt(10) <> VAR
-sum := a + b + 1 // WARNING: + 1 widens to 32-bit in Verilog, not in DFHDL
-sum := a + b + d"1" // OK: explicit literal, no implicit Int
+sum := a + b + 1 // WARNING: + 1 widens to 32-bit in Verilog, not in DFHDL
val cnt = UInt(8) <> VAR
-cnt := cnt + 1 // OK: same-width target, modular truncation matches
+cnt := cnt + 1 // OK: same-width target, modular truncation matches
+// Accept overflow: replace the implicit Int with a bit-accurate literal
+sum := a + b + d"1" // OK: 1 is explicit
+// Prevent overflow: widen the chain with carry to match the wider target
+sum := a +^ b +^ 1 // OK: carry chain widens result before assignment
```
**No warning** is issued when:
@@ -2063,22 +2120,6 @@ cnt := cnt + 1 // OK: same-width target, modular truncation matc
- The integer constant is an explicit bit-accurate literal (e.g., `d"3'4"`).
- The bit-accurate expression width is already 32 bits or wider.
- The implicit `Int` is only used in modular operations (`+`, `-`, `*`) assigned to a same-width target.
-
-**Mitigation strategies:**
-```scala
-val a, b, c, d = UInt(8) <> IN
-val result = UInt(8) <> OUT
-
-// WARNING: implicit Int with non-carry chain before division
-result <> (a + b + c + d) / 4
-
-// Fix 1: use carry addition to prevent intermediate overflow
-result <> ((a +^ b +^ c +^ d) / 4).resize
-
-// Fix 2: use an explicit bit-accurate literal to accept DFHDL
-// overflow semantics and silence the warning
-result <> (a + b + c + d) / d"3'4"
-```
///
### Carry Arithmetic (`+^`, `-^`, `*^`) {#carry-ops}
diff --git a/lib/src/test/scala/ElaborationChecksSpec.scala b/lib/src/test/scala/ElaborationChecksSpec.scala
index d8706f4dc..8e7bf1cb1 100644
--- a/lib/src/test/scala/ElaborationChecksSpec.scala
+++ b/lib/src/test/scala/ElaborationChecksSpec.scala
@@ -591,4 +591,42 @@ class ElaborationChecksSpec extends DesignSpec:
|Message: Cannot apply this operation between a value of WIDTH1 bits width (LHS) and a value of WIDTH2 bits width (RHS).
|An explicit conversion must be applied.""".stripMargin
)
+
+ test("the same bit connected more than once check"):
+ object Test:
+ @top(false) class MultiConn extends EDDesign:
+ val y = Bits(4) <> OUT
+ y(0) <> 1
+ y(0) <> 0
+ end MultiConn
+ import Test.*
+ assertElaborationErrors(MultiConn())(
+ s"""|Elaboration errors found!
+ |DFiant HDL connectivity error!
+ |Position: ${currentFilePos}ElaborationChecksSpec.scala:600:9 - 600:18
+ |Hierarchy: MultiConn
+ |LHS: y(0)
+ |RHS: 0
+ |Message: Found multiple connections write to the same variable/port `MultiConn.y`.
+ |The previous write occurred at ${currentFilePos}ElaborationChecksSpec.scala:599:9 - 599:18""".stripMargin
+ )
+
+ test("the same bit assigned and connected check"):
+ object Test:
+ @top(false) class AssignConn extends RTDesign:
+ val y = Bits(4) <> OUT
+ y(1, 0) := b"00"
+ y(0) <> 1
+ end AssignConn
+ import Test.*
+ assertElaborationErrors(AssignConn())(
+ s"""|Elaboration errors found!
+ |DFiant HDL connectivity error!
+ |Position: ${currentFilePos}ElaborationChecksSpec.scala:619:9 - 619:18
+ |Hierarchy: AssignConn
+ |LHS: y(0)
+ |RHS: 1
+ |Message: Found multiple connections write to the same variable/port `AssignConn.y`.
+ |The previous write occurred at ${currentFilePos}ElaborationChecksSpec.scala:618:9 - 618:25""".stripMargin
+ )
end ElaborationChecksSpec
diff --git a/mkdocs.yml b/mkdocs.yml
index 7a2926768..dfc54774c 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -140,13 +140,12 @@ markdown_extensions:
extra_javascript:
- javascripts/nav-workaround.js
- - javascripts/config.js
- - https://polyfill.io/v3/polyfill.min.js?features=es6
+ - javascripts/config.js
- https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-chtml.js
- https://scastie.scala-lang.org/embedded.js
- javascripts/scastie.js
- javascripts/d3.js
- - https://cdn.jsdelivr.net/gh/kieler/elkjs@0.9.3/lib/elk.bundled.js
+ - https://cdn.jsdelivr.net/gh/kieler/elkjs@0.11.1/lib/elk.bundled.js
- javascripts/d3-hwschematic.js
- javascripts/d3-hwschematic-embed.js
diff --git a/platforms b/platforms
index 20e026de9..575c74316 160000
--- a/platforms
+++ b/platforms
@@ -1 +1 @@
-Subproject commit 20e026de925a355c32fd424dc0bd29df5fbd714d
+Subproject commit 575c743165b1e580844d555b6bb3fd763de6098f