Skip to content

Type System

DFHDL is a Scala library and thus inherently supports type-safe and modern language constructs. This chapter covers the rules and API of this type system.

Check out the benefits of the DFHDL type system
  • 🦾 Strongly-typed


    Most type checks are performed statically, enforcing strict rules that help avoid ambiguity.

    //8-bit unsigned input
    val u8 = UInt(8) <> IN 
    //2-bit unsigned input
    val u2 = UInt(2) <> IN 
    val y1 = u8 - u2 //ok
    // Error prevents ambiguous behavior 
    // when a wider num is subtracted from 
    // a narrow num.
    val y2 = u2 - u8 //error
    
    strongly-typed-example

  • Bit-accurate


    Each DFHDL value has a defined bit-width, which is used to enforce rules that prevent data loss.

    //8-bit unsigned input
    val u8 = UInt(8) <> IN  
    //8-bit signed output
    val s8 = SInt(8) <> OUT 
    // Error prevents data loss when u8 is 
    // converted to a 9-bit signed to be 
    // assigned to s8, which is only 8-bits 
    // wide.
    s8 := u8 //error
    
    bit-accurate-example

  • Composable


    Types can be composed through structs or tuples to form new, combined types.

    //new Pixel type as a structure
    //of two unsigned 8-bit numbers
    case class Pixel(
      x: UInt[8] <> VAL,
      y: UInt[8] <> VAL
    ) extends Struct
    
    val pixel = Pixel <> VAR
    //select and assign fields
    pixel.x := pixel.y
    
  • Expandable


    New types can be defined, and methods can be added for entirely new or existing types.

    //new AESByte type of unsigned 8-bit num
    case class AESByte() 
      extends Opaque(UInt(8))
    //define addition between two AESByte
    //values as a xor operation
    extension (lhs: AESByte <> VAL)
      def +(rhs: AESByte <> VAL): AESByte <> DFRET =
        (lhs.actual ^ rhs.actual).as(AESByte)
    val x, y = AESByte <> VAR
    val z = x + y //actually XOR
    

DFHDL Values

Each DFHDL value is simply a Scala object that has two critical fields:

Internal Type-System Hierarchy (For Advanced Users)

DFHDL brings type-driven development concepts to hardware design, by creating an extensible type class hierarchy. Any DFHDL value is a Scala object instance of the class DFVal[T <: DFTypeAny, M <: ModifierAny], where T is the type (shape) of value and M is a modifier that sets additional characteristics of the DFHDL value, like if it's assignable, connectable, initializable, etc.

type-system type-system

For example, the Scala value x which references a port declared like val x = Boolean <> IN has the type DFVal[DFBool, Modifier.Dcl].

Variable and Port Declarations

Ports are DFHDL values that define the inputs and outputs of a design. Variables are DFHDL values that represent internal design wiring, logic, or state.

Syntax

Port/Variable declaration syntax
val _name_ = _dftype_ <> _modifier_ [init _const_]
  • _name_ is the Scala value name reference for the DFHDL port/variable you constructed. The DFHDL compiler preserves this name and uses it in error messages and the final generated artifacts (e.g., Verilog module or VHDL entity port names). _name_ can also be a series of names separated by commas to declare several equivalent ports/variables. More information is available under the naming section.
  • _dftype_ is set according to the shape type (DFType) of the DFHDL value. Each of the supported DFTypes have their own constructors. See relevant sections for the DFHDL DFType you wish to construct.
  • <> is the operator applied between a _dftype_ and a _modifier_ to construct the Scala value that represents a DFHDL variable or port accordingly. Note: the same <> operator is used as a language construct for declaring connections. Thanks to Scala method overloading, <> can be shared for both use-cases with no issues (due to the Scala argument type difference).
  • _modifier_ is set with one of the following:
    • VAR - to construct a variable
    • IN - to construct an input port
    • OUT - to construct an output port
    • INOUT - to construct a bidirectional input-output port
    • VAR.REG / OUT.REG - to construct a registered variable or output port (available only in RT domains)
    • VAR.SHARED - to construct a shared variable that can be assigned in more than one domain (this feature is to be used scarcely, to model unique designs like True Dual-Port RAM). Directly under an ED domain, a shared variable is assigned within a process only with the non-blocking :== operator, so its writes commit at the end of the process step (the standard RAM inference behavior; in the VHDL backend such writes still render with the variable assignment :=).
  • init is an optional construct to initialize the DFHDL variable/port declaration history with the applied _const_ value.
  • _const_ is the state history initialization value which must be a constant that is supported by the DFType _dftype_. Under DF domain only, _const_ can also be represented by a Scala Tuple sequence of constant initialization values that are supported by the DFType _dftype_.
Port/Variable declaration examples
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
class Foo extends DFDesign:
  //8-bit unsigned integer input port named 'i', 
  //initialized with the value 27
  val i = UInt(8)    <> IN  init 27

  //single bit output port named 'o' 
  //with a sequence history (0, 1, 0) init
  //(possible under DF domain only)
  val o = Bit        <> OUT init (0, 1, 0)

  //5 element vector of 8-bit vector cells 
  //variable named 'v' with no init
  val v = Bits(8) X 5 <> VAR

  //multiple equivalent single bit input port 
  //declarations named 'a', 'b', and 'c'
  val a, b, c = Bit   <> IN
Transitioning from Verilog

TODO

Transitioning from VHDL

TODO

Rules

Scope

  • Variables can be declared in any DFHDL scope, except global scope, meaning within DFHDL designs, domains, interfaces, methods, processes, and conditional blocks.

    1
    2
    3
    4
    //error: Port/Variable declarations cannot be global
    val x = Bit <> VAR 
    class Foo extends DFDesign:
      val o = Bit <> OUT
    

  • Ports can only be declared at the scopes of DFHDL designs, domains, and interfaces. Other scopes are not allowed.

    1
    2
    3
    4
    5
    6
    class Foo extends DFDesign:
      val i = Boolean <> IN
      if (i)
        //error: Ports can only be directly owned by a design, a domain or an interface.
        val o = Bit <> OUT 
        o := 0
    

  • Named DFTypes, such as an enum, a struct, or an opaque type, may be declared at global scope (shared across designs) or inside a design class body (private to that design). Unlike ports and variables, they are not restricted to non-global scopes. See Declaration Scope for which to choose and what it means for the generated HDL.

Naming

Ports and variables must always be named, and cannot be anonymous.

Anonymous declaration elaboration error example
1
2
3
class Foo extends DFDesign:
  //elaboration error: Unable to determine names for the members declared at the following positions
  Bit <> OUT 

As you'll read later on, constants and other values can be anonymous.

Connectable

Ports and variables are connectable, meaning they can be the receiving (drain/consumer) end of a connection <> operation. For input ports this occurs outside their design scope, while connecting to an external value. For output ports and variables this occurs only within their design scope, while connecting to an internal value.

1
2
3
4
class ID extends DFDesign:
  val x = Bit <> IN
  val y = Bit <> OUT
  y <> x //connecting x to y

Assignable (Mutable)

Output ports, input-output ports, and variables are assignable (mutable), when they can be the receiving (drain/consumer) end of an assignment :=/:== operation, which occurs only within their design scope. Input ports can never be assigned (are immutable). Registered ports and variables are assignable only when referencing their registers' input via .din selection (referencing a register without .din is always considered to be its output, which is immutable). A .din selection can also be read, yielding the register's pending value for the next clock edge; see reading the register input.

Assignment semantics are a key difference between the different design domains DFHDL has to offer. Here are some basic examples:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
class Foo1 extends DFDesign:
  val x = Bit <> IN
  val y = Bit <> OUT
  //dataflow assignment of x to y
  y := x

class Foo2 extends RTDesign:
  val x  = Bit <> IN
  val y1 = Bit <> OUT
  val y2 = Bit <> OUT.REG
  //wire assignment of x to y1
  y1     := x 
  //registered assignment of x to y2
  y2.din := x 

class Foo3 extends EDDesign:
  val clk = Bit <> IN
  val x   = Bit <> IN
  val y1  = Bit <> OUT
  val y2  = Bit <> OUT
  process(all):
    //blocking assignment of x to y1
    y1 := x 
  process(clk):
    if (clk.rising)
      //non-blocking assignment of x to y2
      y2 :== x 

class Errors1 extends RTDesign:
  val x  = Bit <> IN
  val y1 = Bit <> OUT.REG
  val y2 = Bit <> OUT
  //error: Cannot assign to an immutable value.
  x  := 1
  //error: Cannot assign to a register output; it is immutable.
  //To assign to the register's input, apply `.din` on the LHS argument of the assignment.
  y1 := x
  //error: Non-blocking assignments `:==` are allowed only inside an event-driven (ED) domain.
  //Change the assignment to a regular assignment `:=` or the logic domain to ED.
  y2 :== x

class Errors2 extends EDDesign:
  val x = Bit <> IN
  val y = Bit <> OUT
  //error: Blocking assignments `:=` are only allowed inside a process under an event-driven (ED) domain.
  //Change the assignment to a connection `<>` or place it in a process.
  y := x
  //error: Non-blocking assignments `:==` are only allowed inside a process under an event-driven (ED) domain.
  //Change the assignment to a connection `<>` or place it in a process.
  y :== x
Be sure to read more on assignment rules and semantics in the assignment section.

Variability (Not Constant)

DFHDL ports and variables are never considered to be constant (even when connected/assigned only once and to a constant value) for elaboration. Later compilation stages can apply further constant propagation steps that reduce logic utilization.

1
2
3
4
5
6
class Errors extends DFDesign:
  val x  = Bit <> VAR
  x := 1
  val c: Bit <> CONST = 1
  // error: Not a constant
  val e: Bit <> CONST = x

INOUT Port Limitation

INOUT (bidirectional) ports are generally used to define IO pins of top-level device connectivity (e.g., protocols like I2C benefit from such ability). They are not meant for inter-device wiring reduction, and thus should be used scarcely within their intended purpose. Throughout the years they were also used to workaround HDL limitations like reading from output ports in VHDL'93, or lack of interfaces. Since DFHDL has none of these limitations, we encourage you to use INOUT for their intended purpose only, as synthesis tools for FPGAs and even ASICs will not cooperate. Although, theoretically, in DF domain we can enable bidirectional communication that can later be compiled into two separate ports, there is no real value behind this.

1
2
3
class I2CCore extends EDDesign:
  val scl = Bit <> INOUT
  val sda = Bit <> INOUT

Grouping

Ports can be grouped together in dedicated interfaces.

Transitioning

Transitioning from Verilog

TODO

Transitioning from VHDL

TODO

Differences from Scala parameters/fields

TODO: Data validity, Number of outputs

Constant/Literal Values

In DFHDL there are three methods to construct constant DFHDL values:

  1. Literal value generators: These language constructs directly generate constant DFHDL values. Currently, these are:
  2. Constant candidates: Various Scala values can become DFHDL values, as.
    Constant declaration syntax
    val _name_: _dftype_ <> CONST = _value_
    
  3. Constant value propagation: Cleaners

Syntax

Rules

Unconnectable

Constant values are not connectable, meaning they can never be the receiving (drain/consumer) end of a connection <> operation.

Unassignable (Immutable)

Constant values are immutable and cannot be assigned, meaning they can never be the receiving (drain/consumer) end of an assignment :=/:== operation.

DFHDL Value Statement Order & Referencing

Any DFHDL value must be declared before it can be referenced in code. Other than this (pretty intuitive) limitation, no other limitations exist and ports, variables, constants, and other values may be freely distributed within their approved scope space. During the compilation process, you can notice that the compiler reorders the port declarations so that they always come second to constant declarations, and variables right after.

The rule constrains references, not statements. The order of the connection and assignment statements themselves carries no meaning, and reordering them never changes the generated hardware. Each statement may only mention values that are already declared above it.

It also constrains val declarations only. Scala def definitions, which is what DFHDL methods and process steps are, may be referenced from anywhere in the body, including above their own definition. See Methods and steps are exempt.

Forward References

A design body is an ordinary Scala class body, and Scala permits such a body to reference a val that is defined further down. This is a forward reference, and Scala neither rejects it nor warns about it: because the value has not been constructed yet, the reference silently evaluates to null. DFHDL detects the missing value and reports an elaboration error.

Forward reference (error)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class Top extends RTDesign:
  val i = Bit <> IN
  val o = Bit <> OUT
  val ctrl = new Ctrl()
  // `active` is declared further down, so it is still `null` here
  ctrl.enable <> active
  val gen = new Gen()
  val active = gen.active
  gen.i <> i
  o <> ctrl.o
Elaboration error
1
2
3
4
5
6
7
8
DFiant HDL elaboration error!
Position:  Top.scala:6:3 - 6:24
Hierarchy: Top
Operation: `<>`
Message:   Found a reference to an uninitialized DFHDL value.
This is caused by a forward reference: the value is declared later in the class body.
To Fix:
Move the declaration before its first use.

The resolution is always the same: move the declaration above its first use. Here it is enough to instantiate gen before ctrl.

Declaration before use (OK)
1
2
3
4
5
6
7
8
9
class Top extends RTDesign:
  val i = Bit <> IN
  val o = Bit <> OUT
  val gen = new Gen()
  val active = gen.active
  gen.i <> i
  val ctrl = new Ctrl()
  ctrl.enable <> active
  o <> ctrl.o

A named DFType follows the same rule, and a forward reference to one is reported as an uninitialized DFHDL type:

Forward-referenced DFType (error)
1
2
3
class Top extends RTDesign:
  val o = Word <> OUT // error: `Word` is declared below
  val Word = Bits(8)

Forward reference to a design instance

When the forward reference is to the design instance itself rather than to one of its ports, Scala selects the member off a null instance before any DFHDL code runs. Scala therefore raises the failure itself, as a NullPointerException that names the culprit, and DFHDL never gets the chance to turn it into an elaboration error:

1
2
3
4
5
6
class Top extends RTDesign:
  val i = Bit <> IN
  val o = Bit <> OUT
  gen.i <> i // `gen` is declared below
  val gen = new Gen()
  o <> gen.active
Scala runtime error
1
2
java.lang.NullPointerException: Cannot invoke "Gen.i()" because
the return value of "Top.gen()" is null

The resolution is the same: declare the instance before referencing it.

Methods and Steps Are Exempt

Everything above concerns val declarations. A Scala def is a method rather than a stored field, so it exists for the whole class body and may be called from anywhere in it, including above its own definition. Two DFHDL constructs are defs and therefore exempt from the declaration order rule:

  • Methods, in every form: DF, ED, and static methods (<> DFRET, <> EDRET, <> CONSTRET), as well as inline method generators.

  • Step blocks in an RT process, which are def Name: Step definitions. A step may therefore jump to a step defined further down, which is what lets an FSM with both forward and backward transitions be written in one readable order.

Forward call to a method (OK)
1
2
3
4
5
6
class Top extends EDDesign:
  val a = UInt(8) <> IN
  val b = UInt(8) <> IN
  val y = UInt(8) <> OUT
  y <> add(a, b) // OK: `add` is a `def`, defined below
  def add(l: UInt[8] <> VAL, r: UInt[8] <> VAL): UInt[8] <> EDRET = l + r
Forward jump to a later step (OK)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
class Top extends RTDesign:
  val x = Bit <> IN
  val y = Bit <> OUT.REG init 0
  process:
    def S0: Step =
      y.din := 0
      if (x) S2 else S0 // OK: `S2` is a step defined below
    def S1: Step =
      y.din := 1
      FirstStep
    def S2: Step =
      y.din := 0
      if (x) S1 else FirstStep

Note

The exemption is about where the def is written, not about what its body may reference. A method body still reads the enclosing design's values, so any value it captures must be declared before the call site that reaches it.

DFHDL Value Connections

After ([or during][via-connections]) a design instantiation, its ports need to be connected to other ports or values of the same DFType by applying the <> operator. Variables can also be connected and used as intermediate wiring between ports. Output ports can be directly referenced (read) without being connected to an intermediate variable. For more rules about design and port connectivity, see the relevant section.

Successful port/variable connection example
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
class ID extends DFDesign:
  val x = UInt(8) <> IN
  val y = UInt(8) <> OUT
  //internal connection between ports
  y <> x 

class IDTop extends DFDesign:
  val x  = UInt(8) <> IN
  val y  = UInt(8) <> OUT
  val yv = UInt(8) <> VAR
  val id = new ID()
  //direct connection between
  //parent and child design ports
  id.x <> x 
  //connecting through an intermediate 
  //variable
  id.y <> yv
  y <> yv

Failed port/variable connection example
1
2
3
4
5
6
7
8
9
class Foo extends DFDesign:
  val x  = UInt(8) <> IN
  val y1 = Bit     <> OUT
  val y2 = UInt(8) <> OUT
  y1 <> x //DFType mismatch error
  y2 <> x
  //connection error (cannot connect 
  //to the same port more than once)
  y2 <> x 

DFHDL Value Assignment (Mutation)

Both output ports and variables are [mutable][mutability] and can be assigned with values of the same DFType and only within the scope of the design they belong to. Input ports cannot be directly assigned, and require an intermediate variable connected to them to modify their value. Generally assignments to DFHDL values are applied through the := operator. In processes under ED domains there are two kind of assignments: blocking assignments via :=, and non-blocking assignments via :==. Other domains support only blocking assignments via :=. Read more on domain semantics in the [next section][domain-semantics]. See the connectivity section for more rules about mixing connections and assignments.

Successful port/variable connection example
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
class Shift extends DFDesign:
  val x = Bits(8) <> IN
  val y = Bits(8) <> OUT
  //assigning `x` left-shifted by 1 
  //to `y`
  y := x << 1

class IDTop extends DFDesign:
  val x  = UInt(8) <> IN
  val y  = UInt(8) <> OUT
  val yv = UInt(8) <> VAR
  val id = new ID()
  //direct connection between
  //parent and child design ports
  id.x <> x 
  //connecting through an intermediate 
  //variable
  id.y <> yv
  y <> yv

Scala var with DFHDL Values

A Scala var is rebound during elaboration, while a DFHDL variable is assigned at runtime with :=. The two look alike and mean different things, so a var holding a DFHDL value is accepted only in the positions where it cannot express something the elaboration is unable to honour. Everywhere else it is a compile error, and no flag relaxes it.

What a var is for is accumulating during elaboration, in an event-driven (ED) design or domain body. The design below packs four input lanes into one word, one lane per iteration:

Elaboration-time accumulation
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
/** Packs four 8-bit lanes into one 32-bit word */
class LaneConcat extends EDDesign:
  /** the four input lanes */
  val lanes = Bits(8) X 4 <> IN
  /** the packed word */
  val word  = Bits(32)    <> OUT
  //the accumulator, ascribed `<> VAL` so its width
  //is not fixed by the first lane
  private var acc: Bits[Int] <> VAL = lanes(0)
  //a Scala range in a concurrent scope, so this
  //loop runs during elaboration and unrolls
  for (i <- 1 until 4) acc = acc ++ lanes(i)
  //freeze the accumulator before a process reads it
  val allLanes = acc
  process(all):
    word := allLanes
end LaneConcat

The for runs over a Scala range in a concurrent scope, so it is an elaboration-time loop: it unrolls, and each iteration rebinds acc to a wider concatenation. Nothing of the loop reaches the backend.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
module LaneConcat(
  /* the four input lanes */
  input  wire logic [7:0] lanes [0:3],
  /* the packed word */
  output      logic [31:0] word
);
  `include "dfhdl_defs.svh"
  logic [7:0]  acc;
  logic [31:0] allLanes;
  assign acc      = lanes[0];
  assign allLanes = {acc, lanes[1], lanes[2], lanes[3]};
  always_comb
  begin
    word = allLanes;
  end
endmodule
Three things are worth noticing in the generated code:

  1. The loop is gone. Four rebindings of one Scala name collapsed into a single concatenation, {acc, lanes[1], lanes[2], lanes[3]}. There is no for and no unrolled sequence, because the loop never existed in hardware to begin with.

  2. Only two of the bindings kept a name. acc names the first binding (lanes[0]), which is where the Scala name was introduced; the intermediate concatenations are anonymous and were folded away. allLanes is the val that froze the result.

  3. The vector port survives as a vector. lanes is emitted as an unpacked array (input wire logic [7:0] lanes [0:3]) and indexed with constants, since every index was resolved during elaboration.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
entity LaneConcat is
port (
  -- the four input lanes
  lanes : in  t_arrX1_std_logic_vector(0 to 3)(7 downto 0);
  -- the packed word
  word  : out std_logic_vector(31 downto 0)
);
end LaneConcat;

architecture LaneConcat_arch of LaneConcat is
  signal acc      : std_logic_vector(7 downto 0);
  signal allLanes : std_logic_vector(31 downto 0);
begin
  acc      <= lanes(0);
  allLanes <= acc & lanes(1) & lanes(2) & lanes(3);
  process (all)
  begin
    word   <= allLanes;
  end process;
end LaneConcat_arch;
The VHDL output tells the same story with different spelling:

  1. The concatenation is one signal assignment, acc & lanes(1) & lanes(2) & lanes(3), using VHDL's & operator where Verilog uses {...}.

  2. acc and allLanes become architecture signals, declared before begin, since a DFHDL value that carries a name needs somewhere to live.

  3. The vector port becomes a generated array type, t_arrX1_std_logic_vector(0 to 3)(7 downto 0), declared in the design's companion package.

The rules the compiler enforces:

  • Ascribe the type as T <> VAL or T <> CONST. An inferred type comes from the initializer, so it fixes the width at the first assignment (var acc = lanes(0) infers Bits[8], and the next acc = acc ++ lanes(1) is then a width error) and it carries the initializer's scope, domain and assignability markers into every later use. An assignable ascription (a variable or a port) is rejected outright, since it would let one name be rebound with = and assigned with :=.
  • Declare it only in an ED design or domain body. Every other scope (a process, an initial block, a method body, and an RT or DF design or domain body) is sequential: it is elaborated once, not once per execution, so a var there cannot accumulate across a loop. Reassigning it only rebinds the Scala name to a value built inside the loop.
  • Access it only from where it is declared. A read or a reassignment from a sequential scope, or from inside a named method, is rejected: a method can be called from anywhere, including from inside a hardware loop. Freeze the accumulator into a val first, the way allLanes does above.
  • Keep it private. A public (or protected) var member stays reassignable from outside the design once elaboration is over, and it takes part in the design's selectable surface.
  • Never hold a design, domain, or interface instance. An instance is structural: it is created once and rebinding the Scala name neither removes the old one nor creates a new one.

To accumulate in hardware rather than during elaboration, declare a DFHDL variable and assign it with :=. See accumulating across a loop.

Bubble Values

  • RT and ED - Don't Care / Unknown
  • DF - Stall

DFHDL Value Candidates

TODO: requires explanation The candidate produces a constant DFHDL value if the candidate argument is a constant.

Operation supported values for an argument of DFType T

`T`Candidate`T`ValueAnyValue`T`OperationCandidate . is! .. is? .. is? .. is! .. is? .
Bits assignment and concatenation operation candidates example
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
val b8 = Bits(8) <> VAR //8-bits variable
val b9 = Bits(9) <> VAR //9-bits variable

//Assignment operations to b8 accept either
//Bits candidates that are 8-bit wide or
//a same-element-vector (SEV) of 
//0/1/true/false, via `all(elem)`.
b8 := h"FF"  //ok: 8-bits constant
b8 := all(0) //ok: SEV of 0
b8 := 5      //fails `Bits` candidate
b8 := b9     //fails `:=` candidate

//Bits `++` concatenation operation with b8
//only accepts Bits candidate, while SEV
//is not a Bits candidate.
val x = b8 ++ h"FF"  //ok
val y = b8 ++ all(0) //error

Type Signatures and Parameterization

Every DFHDL value has a type of the form T <> M, where T is the DFHDL type (shape) and M is the modifier that determines how the value can be used.

Modifier Categories

Modifiers fall into two groups:

Declaration modifiers: used in val declarations with the <> operator:

  • VAR, VAR.REG, VAR.SHARED: variables
  • IN, OUT, OUT.REG, INOUT: ports

Type signature modifiers: used in type annotations for parameters, struct fields, and method signatures:

  • CONST: compile-time or elaboration-time constant parameter
  • VAL: read-only value (struct fields, method parameters)
  • DFRET / RTRET / EDRET: method return types (DF, RT, or ED domain)

Design Parameters

Design classes accept parameters as constructor arguments using <> CONST:

1
2
class Counter(val width: Int <> CONST = 8) extends RTDesign:
  val cnt = UInt(width) <> OUT.REG init 0
  • Int <> CONST for integer parameters (used for widths, lengths, counts). Accepts any Scala Int value (-2^31^ to 2^31^-1).
  • Typed constants like Bits[8] <> CONST and UInt[8] <> CONST are also possible
  • Default values are optional

Reading a Constant into Scala

A DFHDL constant is not a Scala value, so it cannot be passed where Scala expects one: a List size, an index computation, an if condition in elaboration code, or an ordinary method argument. The toScala* family reads the value during elaboration and hands it back as a plain Scala value.

Every DFHDL constant qualifies: a constant design parameter, a literal, or a value derived from them, since arithmetic over constants is itself a constant. The accessor is chosen by the DFHDL type:

DFHDL type Accessor Scala result
Int, UInt[W], SInt[W] .toScalaInt / .toScalaBigInt Int / BigInt
Bit, Boolean .toScalaBoolean / .toScalaBitNum Boolean / 0 or 1
Double .toScalaDouble Double
String .toScalaString String

Bits has no accessor of its own; convert it first with .uint or .sint.

1
2
3
4
5
6
7
class Foo(val Arg: Int <> CONST = 8) extends EDDesign:
  val x = Bits(Arg) <> IN
  val y = Bits(Arg) <> OUT
  val scalaInt: Int = Arg.toScalaInt
  val derived:  Int = (Arg * 2).toScalaInt //a derived constant reads the same way
  for (i <- 0 until Arg) //concurrent-scope range: implicit `.toScalaInt`, none needed
    y(i) <> x(scalaInt - 1 - i)

See Loops for the elaboration-time loop semantics behind the implicit range conversion.

Reading a value that is not a constant is rejected at compile time:

Scala compilation error
1
Only a DFHDL constant is convertible to a Scala value, but this DFHDL value is not a constant.

Reading a parameter into Scala specializes the design

A parameter that is only ever used as a DFHDL value stays fully parametric, and every instantiation shares a single elaborated design. Reading it into Scala bakes its value into the elaborated body instead, so instances with different applied values no longer unify and elaborate into separate designs, each carrying its own folded constants.

Reach for toScala* when Scala genuinely needs the value. When a derived value is only used internally and never has to survive as a named HDL parameter, a plain Scala parameter says the same thing more directly:

1
2
class Bar(Arg: Int = 8) extends EDDesign:
  val doubled: Int = Arg * 2 //an ordinary Scala Int throughout

VAL Modifier

VAL marks a read-only value. It is used for:

  • Struct field declarations:
    1
    case class Point(x: UInt[8] <> VAL, y: UInt[8] <> VAL) extends Struct
    
  • Method parameters:
    1
    def increment(x: UInt[8] <> VAL): UInt[8] <> DFRET = x + 1
    

VAL values cannot be assigned or connected; they are inputs to the computation.

Methods and DFRET

Methods are functional helpers. Arguments use <> VAL, return types use <> DFRET (or RTRET/EDRET for domain-specific methods):

1
2
3
4
5
6
// DF domain method
def double(value: Bits[Int] <> VAL): Bits[Int] <> DFRET = (value, value)

// Opaque type extension method
extension (c: Counter <> VAL)
  def increment: Counter <> DFRET = (c.actual + 1).as(Counter)

Bounded and Unbounded Types

DFHDL types carry their size (width or length) as a Scala type parameter. There are three levels of size specificity:

Bounded: the size is a literal singleton known at compile time. All type checks happen statically:

1
2
3
val a: UInt[8] <> CONST = d"255"
val b: Bits[4] <> CONST = h"A"
val v: Bits[8] X 4 <> CONST = all(all(0))

Parameterized bounded: the size is the singleton type of a named parameter. The compiler can track the relationship, even though the concrete value isn't known until instantiation:

1
2
3
4
class Foo(val w: Int <> CONST) extends RTDesign:
  val x: Bits[w.type] <> CONST = all(0)     // width tied to parameter w
  val y = UInt[w.type] <> VAR init 0         // same
  val v: UInt[4] X w.type <> CONST = all(0)  // vector length tied to w

Unbounded: the size is bare Int, with no compile-time size information. The DFHDL compiler still has the required size information available during elaboration, where it is checked:

1
2
3
4
val cu: UInt[Int] <> VAL = 1
val cs: SInt[Int] <> VAL = -1
val bv: Bits[8] X Int <> CONST = Vector(h"12", h"34")
def twice(value: Bits[Int] <> VAL): Bits[Int] <> DFRET = (value, value)

Result Sizes of Operations

The reference tables throughout this guide give the size an operation produces as a formula over its operand sizes: Max[LW, RW] for commutative arithmetic, LW + RW for concatenation, CLog2[N] for a range-derived width, and so on. These are notation for the resulting size, not types you write yourself.

Such a formula is evaluated at the Scala type level only when every operand size is a literal. The result is then a literal too, and the value is bounded. If any operand is parameterized or unbounded, the result is unbounded Int: the type level stops tracking the size, and the size is computed and checked during elaboration instead.

1
2
3
4
5
6
7
8
class Foo(val w: Int <> CONST) extends RTDesign:
  val a = UInt(8) <> IN
  val b = UInt(8) <> IN
  val s = a + b // UInt[8], both operand widths are literals

  val c = UInt(w) <> IN
  val d = UInt(w) <> IN
  val t = c + d // UInt[Int], the width is `w` and is checked during elaboration

This is why size mismatches between literal-sized values are reported by the Scala compiler, while the same mistake between parameterized values is reported during elaboration. Both are caught; only the moment differs.

Struct fields must be bounded

Struct field types cannot be unbounded. Each field must have a concrete or parameterized-bounded type:

1
2
3
4
5
// CORRECT: bounded fields
case class Pkt(header: Bits[8] <> VAL, data: UInt[32] <> VAL) extends Struct

// ERROR: unbounded fields are not allowed
// case class Bad(data: Bits[Int] <> VAL) extends Struct

DFHDL Value Types

Bit/Boolean

Bit DFHDL values represent binary 1 or 0 values, whereas Boolean DFHDL values represent true or false values, respectively. The Bit and Boolean DFHDL values are generally interchangeable, and automatically converted between one and the other.

Should I use Bit or Boolean DFTypes?

Although they are interchangeable, it's generally recommended to use Boolean DFHDL values with conditional if statements, guards, or expressions, and Bit DFHDL values for everything else. There could be constant parameters that are better defined as a true or false Boolean values rather than 0 or 1 Bit values.

Why have both Bit and Boolean DFTypes?

The main reason to differentiate between Bit and Boolean is that VHDL has both std_logic and boolean types, respectively. Verilog has only a single logic or wire to represent both. Indeed VHDL'2008 has relaxed some of the type constraints, but not enough. And nevertheless, DFHDL aims to support various HDL dialects, and thus enables simple implicit or explicit conversion between these two DFType values.

DFType Constructors

Use the Bit or Boolean objects/types to construct Bit or Boolean DFHDL values, respectively.

1
2
3
4
val bit   = Bit     <> VAR
val bool  = Boolean <> VAR
val c_bit:  Bit     <> CONST = 1
val c_bool: Boolean <> CONST = false

Type Signatures

Bit and Boolean have no size parameter. Type signatures: Bit <> CONST, Bit <> VAL, Boolean <> VAL, etc.

Candidates

  • DFHDL Bit values.
  • DFHDL Boolean values.
  • Scala 1 or 0 literal values. A regular Scala Int is not accepted. This candidate always produces a constant DFHDL value.
  • Scala Boolean values. This candidate always produces a constant DFHDL value.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
val bit  = Bit     <> VAR
val bool = Boolean <> VAR
//`bool` is implicitly converted to a 
//Bit DFHDL value.
bit := bool 
//`1` is implicitly converted to a DFHDL
//Bit constant value.
bit := 1
//`false` is implicitly converted to a 
//DFHDL Boolean constant, and then
//converted to a Bit constant value.
bit := false
val one: Int = 1
//error (only 1/0 literals are ok)
bit := one 
//`bit` is implicitly converted to a
//DFHDL Boolean
bool := bit 
//`true` is implicitly converted to a 
//DFHDL Boolean constant value.
bool := true
//`0` is implicitly converted to a 
//DFHDL Bit constant, and then
//converted to a Boolean constant value.
bool := 0
val TrueVal: Boolean = 1
//`TrueVal` is implicitly converted to
//a DFHDL Boolean value.
bool := TrueVal 

Bit variables accept Boolean comparison values as condidates

All comparison operators (==, !=, <, >, <=, >=) return Boolean, and can be directly assigned to Bit variables:

1
2
3
4
5
class Foo extends RTDesign:
  val limit   = UInt(8) <> IN
  val counter = UInt(8) <> VAR.REG init 0
  val tick    = Bit     <> OUT
  tick := counter == limit  // Implicit Boolean -> Bit conversion

if and while conditionals accept both Boolean and Bit values

if and while conditional expression and statements accept both Boolean and Bit values (no conversion is taking place). In stricter backends like vhdl.v93, an automatic conversion is applied Boolean where needed.

1
2
3
4
5
class Foo extends RTDesign:
  val tick = Bit <> IN
  if (tick) // if condition accepts both Bit and Boolean values
    //do something
  end if

Operations on this type: Logical Operations (&, |, ^, ~, &&, ||, !), Comparison Operations (==/!= only, with a Bit/Boolean as an operand), Selection (as the condition, and as a selected argument), Edge Detection (.rising, .falling, Bit only), and conversions to and from Bits/Boolean.

Bits

Bits DFHDL values represent vectors of DFHDL Bit values as elements. The vector bits width (length) is a positive constant number (nilable [zero-width] vectors will be supported in the future).

Differences between DFHDL Bits and DFHDL Vector of Bit

In addition to Bits, DFHDL also supports generic vectors of any DFHDL values. One could therefore construct a generic vector with Bit as the element DFType. This vector has a different type than Bits, since Bits is a special case, both internally in their implementations and externally in their API. Where applicable, both Bits and generic vector of Bits have overlapping equivalent APIs.

DFType Constructors

Constructor Description Arg Constraints Returns
Bits(width) Construct a Bits DFType with the given width as number of bits. width is a positive Scala Int or constant DFHDL Int value. Bits[width.type] DFType
Bits.until(sup) Construct a Bits DFType with the given sup supremum number the vector is expected to reach. The number of bits is set as clog2(sup). sup is a Scala Int or constant DFHDL Int value larger than 1. Bits[CLog2[width.type]] DFType
Bits.to(max) Construct a Bits DFType with the given max maximum number the vector is expected to reach. The number of bits is set as clog2(max+1). max is a positive Scala Int or constant DFHDL Int value. Bits[CLog2[width.type+1]] DFType
Bits[W] Construct a Bits DFType with the given W width as Scala type argument (for advanced users). width is a positive Scala Int or constant DFHDL Int Singleton type. Bits[W] DFType
1
2
3
4
5
6
7
val b8 = Bits(8)       <> VAR
val b3 = Bits.until(8) <> VAR
val b4 = Bits.to(8)    <> VAR
val b9 = Bits[9]       <> VAR
val w: Int <> CONST = 7
val b7 = Bits(w)       <> VAR
val b6: Bits[6] <> CONST = all(0)
Transitioning from Verilog
  • Specifying a width instead of an index range: In Verilog bit vectors are declared with an index range that enables outliers like non-zero index start, negative indexing or changing bit order. These use-cases are rare and they are better covered using different language constructs. Therefore, DFHDL simplifies things by only requiring a single width/length argument which yields a [width-1:0] sized vector (for generic vectors the element order the opposite).
  • Additional constructors: DFHDL provides additional constructs to simplify some common Verilog bit vector declaration. For example, instead of declaring reg [$clog2(DEPTH)-1:0] addr in Verilog, in DFHDL simply declare val addr = Bits.until(DEPTH) <> VAR.
Transitioning from VHDL
  • Specifying a width instead of an index range: In VHDL bit vectors are declared with an index range that enables outliers like non-zero index start, negative indexing or changing bit order. These use-cases are rare and they are better covered using different language constructs. Therefore, DFHDL simplifies things by only requiring a single width/length argument which yields a (width-1 downto 0) sized vector (for generic vectors the element order the opposite).
  • Additional constructors: DFHDL provides additional constructs to simplify some common VHDL bit vector declaration. For example, instead of declaring signal addr: std_logic_vector(clog2(DEPTH)-1 downto 0) in VHDL, in DFHDL simply declare val addr = Bits.until(DEPTH) <> VAR.

Type Signatures

  • Bounded: Bits[8], Bits[4]
  • Parameterized bounded: Bits[w.type] (where w: Int <> CONST)
  • Unbounded: Bits[Int]

Literal (Constant) Value Generation

Literal (constant) DFHDL Bits value generation is carried out through binary and hexadecimal string interpolation, a core Scala feature that was customized for DFHDL's exact use-case. There are also bit-accurate decimal and signed decimal interpolations available that produce UInt and SInt DFHDL values. If needed, those values can be cast to Bits. No octal interpolation is currently available or planned.

Binary Bits String-Interpolator
Binary Bits string-interpolation syntax
b"width'bin"
  • bin is a sequence of 0, 1, and ? characters, each representing a single bit. ? indicates a bit bubble. The leftest (first) character is the most-significant bit (MSB), and the rightest (last) character is the least-significant bit (LSB).
  • Separators ' ' (space) or _ (underscore) within bin are ignored.
  • bin can also contain interpolated Scala String arguments through ${arg}.
  • width, followed by a ' (apostrophe), is optional and specifies the bit vector's width. If omitted, the minimal width is inferred from the sequence length. If specified, leading zeros are added at the left of the sequence or the sequence is truncated based on the width. Truncation only occurs if the MSBits being removed are zeros; otherwise, it triggers a compilation error.
  • width can be an interpolated argument of either Scala Int or a Constant DFHDL Int value.
  • Returns: A constant DFHDL Bits value with the inferred or set width.
Binary Bits string-interpolation examples
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
b"1"        // Value = 1
b"1000"     // Value = 1000
b"8'1000"   // Value = 00001000
b"3'0100"   // Value = 100
b"3'1100"   // Compilation error
b"1?11"     // Value = 1?11 (? indicates a bit bubble)
b"11_00"    // Value = 1100
val value = "100"
val width = 10
b"$width'1${value}1" //Value = 0000011001
val p: Int <> CONST = 10
b"$p'0" // Value = 0....0 (p-bits wide)
Transitioning from Verilog

This interpolation covers the Verilog binary literal use-cases, but also adds the ability for parametric width to be set. The high impedance (high-Z) use-cases will be supported in the future, likely using a different language construct.

Transitioning from VHDL

This interpolation covers the VHDL binary literal use-cases, but also adds the ability for parametric width to be set. The high impedance (high-Z) use-cases will be supported in the future, likely using a different language construct.

Hexadecimal Bits String-Interpolator
Hexadecimal Bits string-interpolation syntax
h"width'hex"
  • hex is a sequence of hexadecimal characters (0-9, A-F, a-f, and ?) where ? indicates a 4-bit bubble. Each character represents a 4-bit nibble, encoded such that the leftest bit is the most-significant bit.
    The leftest (first) character is the most-significant nibble, and the rightest (last) character is the least-significant nibble.
  • Separators ' ' (space) or _ (underscore) within hex are ignored.
  • hex can also contain interpolated Scala String arguments through ${arg}.
  • Binary sequences can be embedded within {bin} tags, allowing integration of binary bit sequences of any length, not necessarily divisible by 4, between hex nibbles.
  • width, followed by a ', is optional and specifies the bit vector's width. If omitted, the minimal width is inferred from the sequence length. If specified, leading zeros are added or the sequence is truncated based on the width. Truncation only occurs if the most significant bits being removed are zeros or bubbles; otherwise, it triggers a compilation error.
  • width can be an interpolated argument of either Scala Int or a Constant DFHDL Int value.
  • Returns: A constant DFHDL Bits value with the inferred or set width.
Hexadecimal Bits string-interpolation examples
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
h"1"        // Value = 0001
h"27"       // Value = 00100111
h"6'27"     // Value = 100111
h"5'27"     // Compilation error
h"2?"       // Value = 0010????
h"F{00}F"   // Value = 1111001111
h"3_3"      // Value = 00110011
val value = "FF"
val width = 10
h"$width'${value}" //Value = 0011111111
Transitioning from Verilog

This interpolation covers the Verilog hexadecimal literal use-cases, but also adds the ability for parametric width to be set. The high impedance (high-Z) use-cases will be supported in the future, likely using a different language construct.

Transitioning from VHDL

This interpolation covers the VHDL hexadecimal literal use-cases, but also adds the ability for parametric width to be set. The high impedance (high-Z) use-cases will be supported in the future, likely using a different language construct.

Candidates

  • DFHDL Bits values
  • DFHDL Bit or Boolean values. This candidate produces a single bit Bits[1] vector.
  • DFHDL UInt values
  • Scala Tuple combination of any DFHDL values and 1/0 literal values. This candidate performs bit concatenation of all values, according their order in the tuple, encoded from the most-significant value position down to the least-significant value position.
  • Application-only candidate - Same-Element Vector (all(elem)).
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
val b8   = Bits(8) <> VAR
val b1   = Bits(1) <> VAR
//`bit` is implicitly converted to a 
//Bits[1] DFHDL value.
val bit  = Bit     <> VAR
b1 := bit
//`bool` is implicitly converted to a 
//Bits[1] DFHDL value.
val bool = Boolean <> VAR
bool := bit
//`u8` is implicitly converted to a 
//Bits[8] DFHDL value.
val u8   = UInt(8) <> VAR
b8 := u8
val s4   = SInt(4) <> VAR
//the tuple is implicitly converted
//to a Bits[8] DFHDL value.
b8 := (1, s4, b1, b"10")

Bits does not accept plain integer candidates

Unlike UInt/SInt, Bits values cannot be initialized or assigned with plain integers. Use all(0) for zero initialization, or a sized literal:

1
2
3
4
5
6
7
8
// CORRECT
val b8  = Bits(8) <> VAR init all(0)    // zero via all(0)
val b4  = Bits(4) <> VAR init b"4'0"    // zero via binary literal
val b6  = Bits(6) <> VAR init h"6'00"   // zero via hex literal

// error: An integer value cannot be a candidate for a Bits type.
// Try explicitly using a decimal constant via the `d"<width>'<number>"` string interpolation.
val b16 = Bits(16) <> VAR init 0        // compile error

Concatenated Assignment

DFHDL supports a special-case assignment of concatenated DFHDL Bits variables, using a Scala Tuple syntax on LHS of the assignment operator. Both LHS and RHS bits width must be the same. This assignment is just syntactic sugar for multiple separate assignments and carried out during the design elaboration. The assignment ordering is from the first value at most-significant position down to the last value at least-significant position.

1
2
3
4
5
6
class Foo extends DFDesign:
  val i4 = Bits(4) <> IN
  val b2 = Bits(2) <> OUT
  val b3 = Bits(3) <> OUT
  val b5 = Bits(5) <> OUT
  (b2, b5, b3) := (b"101", i4, b"111")
1
2
3
4
5
6
7
8
class Foo extends DFDesign:
  val i4 = Bits(4) <> IN
  val b2 = Bits(2) <> OUT
  val b3 = Bits(3) <> OUT
  val b5 = Bits(5) <> OUT
  b2 := b"10"
  b5 := (b"1", i4).toBits
  b3 := b"111"
Runnable example
import dfhdl.*

//print the code after elaboration
given options.ElaborationOptions.PrintDFHDLCode = true
//set mode to elaborate only
given options.AppOptions.AppMode = options.AppOptions.AppMode.elaborate

class Foo extends DFDesign:
  val i4 = Bits(4) <> IN
  val b2 = Bits(2) <> OUT
  val b3 = Bits(3) <> OUT
  val b5 = Bits(5) <> OUT
  (b2, b5, b3) := (b"101", i4, b"111")

UInt/SInt/Int

DFHDL provides three decimal numeric types:

  • UInt - Unsigned bit-accurate integer values
  • SInt - Signed bit-accurate integer values
  • Int - 32-bit integer values (used mainly for parameters). In operations with UInt or SInt, both Scala Int and DFHDL Int act as wildcards that adapt to the bit-accurate value's sign and width.

DFType Constructors

Constructor Description Arg Constraints Returns
UInt(width) Construct an unsigned integer DFType with the given width as number of bits. width is a positive Scala Int or constant DFHDL Int value. UInt[width.type] DFType
UInt.until(sup) Construct an unsigned integer DFType with the given sup supremum number the value is expected to reach. The number of bits is set as clog2(sup). sup is a Scala Int or constant DFHDL Int value larger than 1. UInt.until(1) is invalid (would produce 0-bit width). UInt[CLog2[width.type]] DFType
UInt.to(max) Construct an unsigned integer DFType with the given max maximum number the value is expected to reach. The number of bits is set as clog2(max+1). max is a positive Scala Int or constant DFHDL Int value. UInt.to(1) is valid (produces 1-bit width). UInt[CLog2[width.type+1]] DFType
SInt(width) Construct a signed integer DFType with the given width as number of bits. width is a positive Scala Int or constant DFHDL Int value. SInt[width.type] DFType
Int Construct a constant integer DFType. Used mainly for parameters. None Int DFType

Type Signatures

  • Bounded: UInt[8], SInt[16]
  • Parameterized bounded: UInt[w.type], SInt[w.type] (where w: Int <> CONST)
  • Unbounded: UInt[Int], SInt[Int]
  • Int has no size parameter: Int <> CONST, Int <> VAL

Candidates

  • DFHDL decimal values of the same type
  • DFHDL Bits values (via .uint or .sint casting)
  • Scala numeric values (Int, Long, etc.) for constant values
  • Decimal literals (string interpolation values)

Constant Generation

Unsigned Decimal String-Interpolator

The unsigned decimal string interpolator d creates unsigned integer constants (UInt) from decimal values. For negative values, use the signed decimal string-interpolator.

Unsigned decimal string-interpolation syntax
d"width'dec"
  • dec is a sequence of decimal characters ('0'-'9'). Negative values are not allowed.
  • width followed by a ' is optional and specifies the exact width of the integer's bit representation
  • Separators _ (underscore) and , (comma) within dec are ignored
  • If width is omitted, it is inferred from the value's size
  • If specified, the output is padded with zeros
  • Returns UInt[W], where W is the width in bits
  • An error occurs if the specified width is less than required to represent the value
  • When used with a DFHDL Int parameter and an explicit or parametric width, the interpolation binds the parameter as an unsigned value of that width. Without a width, the parameter passes through unchanged and remains a wildcard Int value.
1
2
3
4
5
6
7
8
d"0"           // UInt[1], value = 0
d"255"         // UInt[8], value = 255
d"8'42"        // UInt[8], value = 42
d"1,023"       // UInt[10], value = 1023
d"1_000"       // UInt[10], value = 1000
d"$param"      // Int, parameter passes through as a wildcard value
d"8'$param"    // UInt[8], unsigned binding with explicit width
d"${w}'$param" // UInt[w.type], unsigned binding with parametric width
Signed Decimal String-Interpolator

The signed decimal string interpolator sd creates signed integer constants (SInt) from decimal values.

Signed decimal string-interpolation syntax
sd"width'dec"
  • dec is a sequence of decimal characters ('0'-'9') with an optional prefix - for negative values
  • width followed by a ' is optional and specifies the exact width of the integer's bit representation
  • Separators _ (underscore) and , (comma) within dec are ignored
  • Output is always a signed integer type SInt[W], regardless of whether the value is negative or natural
  • Width is always at least 2 bits to accommodate the sign bit
  • An error occurs if the specified width is less than required to represent the value including the sign bit
  • When used with a DFHDL Int parameter, the interpolation binds it as signed
1
2
3
4
5
6
7
8
sd"0"           // SInt[2], value = 0 (natural number represented as signed)
sd"-1"          // SInt[2], value = -1
sd"255"         // SInt[9], value = 255 (natural number represented as signed)
sd"8'42"        // SInt[8], value = 42
sd"8'255"       // Error: width too small to represent value with sign bit
sd"$param"      // SInt[Int], signed binding of Int parameter
sd"8'$param"    // SInt[8], signed binding with explicit width
sd"${w}'$param" // SInt[w.type], signed binding with parametric width

Examples

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// Basic declarations
val u8 = UInt(8) <> VAR      // 8-bit unsigned
val s8 = SInt(8) <> VAR      // 8-bit signed
val param: Int <> CONST = -3  // Constant parameter

// Arithmetic
val sum = u8 + s8.uint       // Addition with casting
val diff = s8 - 5            // Subtraction with constant
val prod = u8 * u8           // Multiplication

// Comparisons
val lt = u8 < 100
val eq = s8 == sd"8'0"

// Initialization
val u4 = UInt(4) <> VAR init d"4'10"
val s4 = SInt(4) <> VAR init sd"4'-2"

Enumeration

DFHDL supports enumerated types through Scala's enum feature with special encoding traits. Enums provide a type-safe way to represent a fixed set of values.

Enum Type Definition

1
2
enum MyEnum extends Encoded:
  case A, B, C, D

Type Signatures

MyEnum <> VAL, MyEnum <> CONST. The enum name itself is the type, with no size parameter.

Declaration Scope

An enum may be declared at top level, where it is shared by every design in the compilation unit, or inside a design class body, where it is private to that design. A per-design FSM state type belongs inside the class:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class Child extends EDDesign:
  enum State extends Encoded:
    case IDLE, DRAW
  val state = State <> VAR
  // ...

class Parent extends EDDesign:
  enum State extends Encoded:
    case IDLE, BUSY, HOLD
  val state = State <> VAR
  val c = new Child

Where the generated typedef lands is decided by usage, not by where the enum is declared in Scala. An enum used inside a single design is emitted module-scoped; an enum that has to be visible to more than one design (because it appears in a port type, for instance) is emitted into the design's global definitions file instead.

Because a single-design enum is module-scoped, two designs may nest identically named enums of different widths with no conflict:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
module Child(...);
  typedef enum logic [0:0] { State_IDLE = 0, State_DRAW = 1 } t_enum_State;
  t_enum_State state;
endmodule

module Parent(...);
  typedef enum logic [1:0] { State_IDLE = 0, State_BUSY = 1, State_HOLD = 2 } t_enum_State;
  t_enum_State state;
  Child c(...);
endmodule

Put the same enum in a port type and it moves out to the shared file, since both modules must name the same type. That is a property of how the type is used, so a top-level Scala declaration is not required to get it, and does not by itself cause it.

Two top-level enums of the same name

Declaring the same enum name at top level in two files of one compilation unit is a duplicate definition. The first error says so plainly, but the errors after it do not:

1
2
3
State is already defined as class State in ./src/ModA.scala
value IDLE is not a member of State$2
value BUSY is not a member of State, but could be made available as an extension method.

The mangled State$2 and the import suggestions that follow are dead ends. Read the first line, and prefer nesting the enum inside the design that uses it.

Encoding Types

DFHDL supports several encoding schemes for enums:

  1. Binary Encoded (default)

    1
    2
    enum MyEnum extends Encoded:
      case A, B, C, D  // Encoded as 00,01,10,11
    

  2. One-Hot Encoded

    1
    2
    enum MyEnum extends Encoded.OneHot:
      case A, B, C  // Encoded as 001,010,100
    

  3. Gray Encoded

    1
    2
    enum MyEnum extends Encoded.Gray:
      case A, B, C  // Encoded as 00,01,11
    

  4. Custom Start Value

    1
    2
    enum MyEnum extends Encoded.StartAt(4):
      case A, B, C  // Encoded as 100,101,110
    

  5. Manual Encoding

    1
    2
    3
    4
    enum MyEnum(val value: UInt[8] <> CONST) extends Encoded.Manual(8):
      case A extends MyEnum(200)
      case B extends MyEnum(100)
      case C extends MyEnum(50)
    

    Note: the Manual encoding enum class must declare a constructor parameter (val value: UInt[N] <> CONST) and the bit width N must match the argument to Encoded.Manual(N). Each case must explicitly extend the enum class and pass a constant value. Omitting the constructor parameter will cause a compile error.

Pattern Matching

Enums can be used in pattern matching expressions:

1
2
3
4
5
6
val state = MyEnum <> VAR

state match
  case MyEnum.A => // handle A
  case MyEnum.B => // handle B
  case MyEnum.C => // handle C

Listing every declared entry makes the match exhaustive, yet a wildcard case _ => branch is still permitted: it covers any encoding of the underlying register that no entry names. See Enum Matches and the Wildcard case _ for when to include it.

Examples

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// State machine enum
enum State extends Encoded.OneHot:
  case Idle, Fetch, Execute, Store

class CPU extends RTDesign:
  val state = State <> VAR.REG init State.Idle

  state match
    case State.Idle => 
      // Idle state logic
    case State.Fetch =>
      // Fetch state logic
    case State.Execute =>
      // Execute state logic
    case State.Store =>
      // Store state logic

Operations on this type: Comparison Operations (==/!= between values of the same enum type), Selection (enum values are valid .sel arguments), pattern matching, and Enum to UInt conversion.

Vector

DFHDL vectors allow creating arrays of any DFHDL type. Unlike Bits which is specialized for bit vectors, generic vectors can hold any DFHDL type and support multi-dimensional arrays.

Vector Type Construction

The vector type is constructed using the X operator between a base type and dimension:

1
val vec = BaseType X Dimension <> Modifier

Examples:

1
2
3
val vec1 = UInt(8) X 4 <> VAR        // 1D vector of 4 8-bit unsigned ints
val vec2 = Bit X 8 X 8 <> VAR        // 2D 8x8 vector of bits
val vec3 = MyEnum X 16 <> VAR        // Vector of 16 enum values

Type Signatures

  • Bounded: UInt[8] X 4, Bits[8] X 4 X 4
  • Parameterized bounded: UInt[4] X len.type (where len: Int <> CONST)
  • Unbounded: Bits[8] X Int
  • Both element type and dimensions can be parameterized independently

Initialization

Vectors can be initialized in several ways:

1
2
3
4
5
6
7
8
// Initialize all elements to same value
val vec1 = UInt(8) X 4 <> VAR init all(0)

// Initialize with specific values
val vec2 = UInt(8) X 4 <> VAR init Vector(1, 2, 3, 4)

// Initialize from file
val mem = UInt(32) X 1024 <> VAR initFile "mem.hex"

Multi-dimensional Vectors

Multi-dimensional vectors are created by chaining X operators:

1
2
3
4
5
6
7
8
9
// 2D 4x4 matrix of 8-bit values
val matrix = UInt(8) X 4 X 4 <> VAR

// Access elements
val elem = matrix(row)(col)
matrix(1)(2) := 42

// Initialize 2D array
matrix := all(all(0))  // All elements to 0

Memory/RAM Implementation

Vectors are commonly used to implement memories and RAMs. A shared vector variable, declared under an ED design and accessed from one or more RT domains (one per memory port), yields the standard synthesizable RAM template:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class RAM extends EDDesign:
  val mem = UInt(32) X 1024 <> VAR.SHARED  // 1K x 32-bit memory

  val port = new RTDomain:
    val addr = UInt(10) <> IN              // 10-bit address
    val data = UInt(32) <> IN              // 32-bit write data
    val q    = UInt(32) <> OUT.REG         // 32-bit registered read data
    val we   = Bit      <> IN              // Write enable

    q.din := mem(addr)                     // Read (before the write, see note)
    if (we) mem(addr) := data              // Write

Note: place shared-variable reads before writes within the same domain or process. The DFHDL semantics are read-first either way (writes commit at the end of the clock step), but the VHDL backend renders the statements in source order with the variable assignment :=, so only the read-before-write order makes the VHDL read-first as well, matching the Verilog and simulation behavior on a same-address write collision.

File Initialization

Vectors support initialization from files in various formats:

1
2
3
4
5
// Initialize from hex file
val rom = UInt(8) X 256 <> VAR initFile("rom.hex", InitFileFormat.VerilogHex)

// Initialize from binary file
val ram = UInt(32) X 1024 <> VAR initFile "ram.bin"

Struct

DFHDL structures allow creating composite types by combining multiple DFHDL values into a single type. Structs are defined using Scala case classes that extend the Struct trait.

Struct Type Definition

1
2
3
4
5
case class MyStruct(
  field1: UInt[8] <> VAL,
  field2: Bits[4] <> VAL,
  field3: Boolean <> VAL
) extends Struct

Type Signatures

MyStruct <> VAL, MyStruct <> CONST. The struct name is the type. Struct fields must use bounded types (no UInt[Int] in fields).

Field Access and Assignment

Fields are accessed using dot notation and can be assigned individually:

1
2
3
4
val s = MyStruct <> VAR
s.field1 := 42          // Assign to individual field
s.field2 := b"1010"     // Assign bits
s := MyStruct(1, b"0101", true)  // Assign whole struct

Nested Structs

Structs can be nested to create more complex data structures:

1
2
3
4
5
6
case class Point(x: UInt[8] <> VAL, y: UInt[8] <> VAL) extends Struct
case class Rectangle(topLeft: Point <> VAL, bottomRight: Point <> VAL) extends Struct

val rect = Rectangle <> VAR
rect.topLeft.x := 0
rect.bottomRight.y := 100

Pattern Matching

Structs support pattern matching for field extraction:

1
2
3
4
val point = Point <> VAR
point match
  case Point(x, y) if x > 10 => // Use x and y
  case Point(0, _) => // Match x=0, any y

Examples

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// AXI-like interface struct
case class AXILite(
  addr: UInt[32] <> VAL,
  data: Bits[64] <> VAL,
  valid: Bit <> VAL,
  ready: Bit <> VAL
) extends Struct

class MyDesign extends RTDesign:
  val axi = AXILite <> OUT.REG

  // Initialize struct
  axi := AXILite(0, all(0), 0, 1)

  // Access individual fields
  axi.valid := 1
  axi.data := h"DEADBEEF"

Tuple

DFHDL tuples provide a way to group multiple DFHDL values together without defining a named structure. They are similar to Scala tuples but operate on DFHDL values.

Tuple Type Construction

1
val tuple = (Type1, Type2, ..., TypeN) <> Modifier

Type Signatures

(UInt[8], Bit) <> VAL, (Bits[Int], Bit) <> CONST. Elements can be individually bounded or unbounded.

Examples

1
2
3
4
5
6
7
8
9
// Basic tuple declaration
val pair = (UInt(8), Bit) <> VAR

// Nested tuples
val complex = ((UInt(8), Bit), Bits(4)) <> VAR

// Assignment
pair := (42, 1)
complex := ((100, 0), b"1010")

Element Access

Tuple elements can be accessed using ._N notation or pattern matching:

1
2
3
4
5
val first = pair._1    // Access first element
val second = pair._2   // Access second element

// Pattern matching
val (x, y) = pair

Opaque

Opaque types allow creating new DFHDL types that wrap existing types while hiding their internal representation. This is useful for creating abstraction layers and type-safe interfaces.

Opaque Type Definition

1
2
3
4
5
6
7
8
// Define opaque type wrapping UInt(8)
case class MyOpaque() extends Opaque(UInt(8))

// Define opaque type with custom operations
case class Counter() extends Opaque(UInt(32)):
  extension (c: Counter <> VAL)
    def increment: Counter <> DFRET = 
      (c.actual + 1).as(Counter)

Type Signatures

MyOpaque <> VAL, MyOpaque <> CONST. The opaque name is the type.

Usage

1
2
3
val op = MyOpaque <> VAR
val wrapped: UInt[8] <> VAL = op.actual  // Access wrapped value
op := 42.as(MyOpaque)  // Assign using .as conversion

Examples

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// AES byte type with custom operations
case class AESByte() extends Opaque(UInt(8)):
  extension (lhs: AESByte <> VAL)
    def +(rhs: AESByte <> VAL): AESByte <> DFRET =
      (lhs.actual ^ rhs.actual).as(AESByte)

class AESCircuit extends DFDesign:
  val in1 = AESByte <> IN
  val in2 = AESByte <> IN
  val out = AESByte <> OUT

  out := in1 + in2  // Uses custom + operation

Double

DFHDL Double values represent IEEE-754 double-precision floating-point numbers.

Type Construction

1
val d = Double <> Modifier

Type Signatures

Double <> VAL, Double <> CONST. No size parameter (always 64 bits).

Time/Freq

DFHDL provides special types for representing time and frequency values in hardware designs through physical units. These types help ensure correct timing specifications and frequency calculations.

Time Values

Time values can be created using various unit suffixes:

1
2
3
4
5
6
7
8
9
// Time unit constructors
val t1 = 1.fs     // Femtoseconds
val t2 = 1.ps     // Picoseconds
val t3 = 1.ns     // Nanoseconds
val t4 = 1.us     // Microseconds
val t5 = 1.ms     // Milliseconds
val t6 = 1.sec    // Seconds
val t7 = 1.mn     // Minutes
val t8 = 1.hr     // Hours

Both integer and floating-point values can be used with time units:

1
2
val t9 = 1.5.ns   // 1.5 nanoseconds
val t10 = 10.ms   // 10 milliseconds

Frequency Values

Frequency values can be specified using standard frequency units:

1
2
3
4
5
// Frequency unit constructors
val f1 = 1.Hz     // Hertz
val f2 = 1.KHz    // Kilohertz
val f3 = 1.MHz    // Megahertz
val f4 = 1.GHz    // Gigahertz

Like time values, both integer and floating-point values are supported:

1
2
val f5 = 100.MHz  // 100 megahertz
val f6 = 2.5.GHz  // 2.5 gigahertz

Usage in RT Domains

Physical values are particularly useful when configuring RT domains and specifying clock frequencies:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class TimingExample extends RTDesign:
  // Clock configuration with 100MHz frequency
  val clkCfg = ClkCfg(
    edge = ClkCfg.Edge.Rising,
    rate = 100.MHz,
    portName = "clk"
  )

  // Timing calculations
  val period = 10.ns      // Clock period
  val setupTime = 1.ns    // Setup time requirement
  val clockFreq = 1.GHz   // Clock frequency

Cycles in RT Domain

In RT domains, you can also specify cycle counts using the .cy unit:

1
2
class RTExample extends RTDesign:
  val delay = 5.cy    // 5 clock cycles delay

Note: The .cy unit is only available within register-transfer (RT) domains.

Unit (Void)

The Unit type in DFHDL represents a void or no-value type, similar to Scala's Unit type. It's typically used when an operation doesn't need to return a meaningful value.

Usage

1
2
3
4
5
6
7
8
// Method returning Unit
def doSomething: Unit <> DFRET =
  // Perform operations without returning value
  ()

// Assignment that produces no value
val x = Bit <> VAR
val y: Unit <> VAL = x := 1

Common Use Cases

  1. Side-effect operations
  2. Void method returns
  3. Assignment results
  4. Process bodies in event-driven designs
1
2
3
4
5
6
class Example extends EDDesign:
  val clk = Bit <> IN

  process(clk.rising):
    // Process body returns Unit
    doSomething

Operations

Which operations apply to which types

Every section in this part opens with an Applies to: line naming the types the operation accepts. Those lines are the reference for "does this type support this operation": they are meant to be exhaustive, so a type absent from one is a type the operation does not accept.

Read that line before concluding an operation is unavailable. The operations reference sits below the whole type reference, so it is easy to work through the type sections and never reach the section that actually decides whether an expression is legal.

Constant Propagation

When all operands of an expression are constants (CONST), the result is also a constant. This includes Scala Int literals, DFHDL Int parameters, and bit-accurate constants created with d"" or sd"".

1
2
3
4
5
6
7
val param: Int <> CONST = 10
val c1: UInt[8] <> CONST = d"8'5" + 3       // constant + literal = constant
val c2: UInt[8] <> CONST = d"8'5" + param   // constant + param = constant

val u8 = UInt(8) <> VAR
val v1 = u8 + 3        // VAR + literal = not constant
val v2 = u8 + param    // VAR + param = not constant

Conversions and Casts

The diagram below shows the conversion/cast paths between DFHDL types. Solid arrows are simple casts that preserve width; dashed arrows involve width changes.

type-conversion type-conversion

From To Method From To Method
T Bits .bits Bit Boolean .bool
Bits T .as(T) Boolean Bit .bit
Bits(w) UInt(w) .uint Bit/Boolean Bits(1) .bits
Bits(w) SInt(w) .sint Bit/Boolean Bits(w) .toBits(w)
UInt(w) SInt(w+1) .signed Bit/Boolean UInt(w) .toUInt(w)
UInt/SInt Int .ToInt Bit/Boolean SInt(w) .toSInt(w)

Any Type to/from Bits: .bits and .as(T)

Every DFHDL type can be converted to its raw bit representation with .bits. The inverse operation, .as(T), reinterprets a Bits value as a target type T, provided the bit widths match exactly:

1
2
3
val u8 = UInt(8) <> VAR
val b8 = u8.bits          // UInt(8) -> Bits(8)
val back = b8.as(UInt(8)) // Bits(8) -> UInt(8)

This also works with composite types such as enums, structs, and opaques:

1
2
3
val e = MyEnum <> VAR
val eBits = e.bits           // Enum -> Bits
val eBack = eBits.as(MyEnum) // Bits -> Enum

Bits to UInt/SInt: .uint and .sint

These are shorthand conversions from Bits that preserve width. The same bits are simply reinterpreted as unsigned or signed:

1
2
3
val b8 = Bits(8) <> VAR
val u8 = b8.uint  // Bits(8) -> UInt(8), same bit pattern
val s8 = b8.sint  // Bits(8) -> SInt(8), same bit pattern

UInt to SInt: .signed

Converting an unsigned value to signed requires an extra bit for the sign, so .signed widens the result by one bit:

1
2
val u8 = UInt(8) <> VAR
val s9 = u8.signed  // UInt(8) -> SInt(9)

To get an SInt with the same width (reinterpreting the bit pattern without expanding), go through Bits:

1
val s8 = u8.bits.sint  // UInt(8) -> Bits(8) -> SInt(8)

Bit and Boolean Conversions

Bit is the hardware single-bit type and Boolean is the logical type. They are convertible to each other with .bit and .bool:

1
2
3
val myBit  = Bit <> VAR
val myBool = myBit.bool  // Bit -> Boolean
val back   = myBool.bit  // Boolean -> Bit

Both Bit and Boolean can be widened (zero-extended) into Bits, UInt, or SInt with an explicit target width:

1
2
3
4
val flag = Bit <> VAR
val b4 = flag.toBits(4)  // Bit -> Bits(4)
val u4 = flag.toUInt(4)  // Bit -> UInt(4)
val s4 = flag.toSInt(4)  // Bit -> SInt(4)

When the value is 1, these produce the value 1 at the given width (not sign-extended). The single-bit .bits conversion is also available, returning Bits(1).

Enum to UInt: .uint

Enum values can be converted to their underlying unsigned integer representation:

1
2
val e = MyEnum <> VAR
val u = e.uint  // Enum -> UInt (encoding-dependent width)

Bit Selection and Slicing

Applies to: Bits, UInt, 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, with the 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.
  • Part-select (anchored slice): value.lsbitsAt(baseIdx, selWidth) returns selWidth bits whose LSB is anchored at baseIdx, and value.msbitsAt(baseIdx, selWidth) returns selWidth bits whose MSB is anchored at baseIdx. These are the DFHDL equivalents of Verilog's ascending (value[baseIdx +: selWidth]) and descending (value[baseIdx -: selWidth]) part-selects, equivalent to value(baseIdx + selWidth - 1, baseIdx) and value(baseIdx, baseIdx - selWidth + 1) respectively, with the same unsigned-result rule. The generalization of the top/bottom slices: msbits(W) is msbitsAt(N-1, W) and lsbits(W) is lsbitsAt(0, W). The width must always be an elaboration-time constant (a Scala Int value or an Int parameter). The base must be too, with one exception: inside an ED process, the base may be an expression over a process-scope for iterator, which emits a variable-base part-select. See ED Domain Loops for what that iterator is and is not.
  • 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.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
val b8 = Bits(8) <> VAR
val u8 = UInt(8) <> VAR
val s8 = SInt(8) <> VAR

// 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

// Part-select: anchored slices, equivalent to Verilog's `+:`/`-:`
val psUp   = b8.lsbitsAt(2, 4) // Bits[4]: same as b8(5, 2), Verilog b8[2 +: 4]
val psDown = b8.msbitsAt(5, 4) // Bits[4]: same as b8(5, 2), Verilog b8[5 -: 4]
val psU    = u8.lsbitsAt(2, 4) // UInt[4]: same as u8(5, 2)
val psS    = s8.msbitsAt(5, 4) // UInt[4]: SInt part-select is unsigned

// Single-bit access
val msb = b8(7)       // Bit
val lsb = u8(0)       // Bit

// Dynamic bit access (index is a UInt variable)
val idx = UInt(3) <> VAR
val dynbit = b8(idx)  // Bit at position idx

Dynamic bit indexing

You can index into a bit-vector value using a UInt variable, not just integer literals. The index must be a UInt whose width equals clog2(bits_width). For example, indexing into Bits(8) requires a UInt(3) index. If the width does not match, the compiler will report an error and suggest using .resize to automatically adjust the width.

Dynamic indexing works for both reads and writes:

1
2
3
4
5
6
7
8
val data = Bits(8) <> VAR init all(0)
val pos  = UInt(3) <> VAR init 0
val din  = Bit     <> IN

val bit_out = data(pos)      // dynamic read
process(clk):
  if (clk.rising)
    data(pos) :== din        // dynamic write

When the index variable is wider or narrower than needed, use .resize to automatically adjust it to the required width:

1
2
3
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:

1
2
3
4
5
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

Applies to: Bits, UInt, SInt

  • .resize(N) sets the width to exactly N bits. For UInt and Bits, widening zero-extends; for SInt, widening sign-extends. Narrowing truncates the most-significant bits.
  • .resize (no argument) automatically adjusts the width to match the assignment or operation context; narrowing or widening as needed.
  • .eby(K) extends the width by K bits, relative to the current width; sugar for .resize(width + K). K must be positive, so .eby always widens (zero-extension for UInt and Bits, sign-extension for SInt). This is the canonical widening spelling: elaboration prints any widening whose delta is a known number of bits in this relative form (a user-written x.resize(9) over an 8-bit x prints back as x.eby(1) -- the two produce identical designs). It is also the form that scales to parametric widths, where the absolute spelling would repeat the symbolic expression: x.eby(1) instead of x.resize(W + 1). Absolute .resize remains the spelling for narrowing and for widths given by a named parameter.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
val b8 = Bits(8) <> VAR
val b4 = Bits(4) <> VAR
b4 := b8.resize(4)    // explicit narrow to 4 bits
b8 := b4.resize       // auto-widen to match b8's width

val u8 = UInt(8) <> VAR
val u6 = UInt(6) <> VAR
u6 := u8.resize       // auto-narrow to match u6's width
u8 := u6.resize(8)    // explicit zero-extend to 8 bits

val s8 = SInt(8) <> VAR
val s4 = SInt(4) <> VAR
s8 := s4.resize       // sign-extend to match s8's width
s4 := s8.resize(4)    // explicit narrow to 4 bits

// relative widening, most useful with parametric widths
val W: Int <> CONST = 8
val sW  = SInt(W) <> VAR
val sW2 = SInt(W + 2) <> VAR
sW2 := sW.eby(2)      // sign-extend by 2 bits (to W + 2)
b8 := b4.eby(4)       // zero-extend by 4 bits; same design as b4.resize(8)

Bit Concatenation

Applies to: Bits, UInt, SInt

Multiple bit-vector values can be concatenated using Scala tuple syntax with .toBits:

1
2
3
4
val concat = (b"100", b"1", b"0", b"11").toBits  // Bits[8]
val u8 = UInt(8) <> VAR
val u4 = UInt(4) <> VAR
val wide = (u8, u4).toBits                        // Bits[12]

Values are concatenated from the first (most-significant) to the last (least-significant) position.

Building a value from a Scala collection

To assemble a value from a collection of single-bit sources, connect each bit of the target individually in a loop:

1
2
3
4
5
6
class Foo extends EDDesign:
  val data  = Bits(9 * 8) <> IN
  val thr   = UInt(8)     <> IN
  val flags = Bits(9)     <> OUT
  for (i <- 0 until 9)
    flags(i) <> (data.lsbitsAt(i * 8, 8).uint >= thr)

For wider lanes, accumulate with a Scala var ascribed to an unbounded Bits[Int], as in the LaneConcat example. The ascription is what keeps the accumulator's width from being fixed by the first element.

Logical Operations

Applies to: Bit, Boolean. For the elementwise bitwise operations on Bits/UInt vectors (&, |, ^, ~), see Bitwise Operations.

Logical operations' return type always matches the LHS argument's type. These operations propagate constant modifiers, meaning that if all arguments are constant, the returned value is also a constant.

Operation Description LHS/RHS Constraints Returns
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 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
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
val bt = Bit     <> VAR
val bl = Boolean <> VAR
val t1 = bt && bl    //result type: Bit
val t2 = bt ^ 1      //result type: Bit
val t3 = bl || false //result type: Boolean
val t4 = bt && true  //result type: Bit
val t5 = bl || bt    //result type: Boolean
val t6 = bl ^ 0 || !bt
//`t7` after the candidate implicit
//conversions, looks like so:
//(bl && bt.bool) ^ (!(bt || bl.bit)).bool
val t7 = (bl && bt) ^ !(bt || bl)
//error: swap argument positions to have
//the DFHDL value on the LHS.
val e1 = 0 ^ bt      
//error: swap argument positions to have
//the DFHDL value on the LHS.
val e2 = false ^ bt
//not supported since both arguments
//are just candidates
val e3 = 0 ^ true
//This just yields a Scala Boolean, 
//as a basic operation between Scala
//Boolean values.
val sc: Boolean = true && true

Logical ||/&&/! and bitwise |/&/~ on Bit and Boolean values

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 ||/&&/!.

Transitioning from Verilog

Under the ED domain, the following operations are equivalent:

DFHDL Operation Verilog Operation (Bit LHS) Verilog Operation (Boolean LHS)
lhs && rhs lhs & rhs lhs && rhs
lhs || rhs lhs | rhs lhs || rhs
lhs & rhs lhs & rhs lhs && rhs
lhs | rhs lhs | rhs lhs || rhs
lhs ^ rhs lhs ^ rhs lhs ^ rhs
!lhs ~lhs !lhs
~lhs ~lhs !lhs
Transitioning from VHDL

Under the ED domain, the following operations are equivalent:

DFHDL Operation VHDL Operation
lhs && rhs lhs and rhs
lhs || rhs lhs or rhs
lhs ^ rhs lhs xor rhs
!lhs not lhs

Bitwise Operations

Applies to: Bits, UInt

Bitwise operations apply elementwise on their vector arguments' bits, and their return type always matches the LHS argument's type. These operations propagate constant modifiers, meaning that if all arguments are constant, the returned value is also a constant. Do not confuse the two-operand &/|/^ with the single-operand postfix reduction operators .&/.|/.^, which fold a vector into a single Bit.

Operation Description LHS/RHS Constraints Returns
lhs & rhs Bitwise AND The LHS argument must be a Bits/UInt DFHDL value. The RHS must match the LHS type and width (for Bits, any same-width Bits candidate). LHS-Type DFHDL value
lhs | rhs Bitwise OR The LHS argument must be a Bits/UInt DFHDL value. The RHS must match the LHS type and width (for Bits, any same-width Bits candidate). LHS-Type DFHDL value
lhs ^ rhs Bitwise XOR The LHS argument must be a Bits/UInt DFHDL value. The RHS must match the LHS type and width (for Bits, any same-width Bits candidate). LHS-Type DFHDL value
~lhs Bitwise NOT (invert all bits) The argument must be a Bits/UInt DFHDL value. LHS-Type DFHDL value
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
val v8 = Bits(8) <> VAR
val m8 = Bits(8) <> VAR
val u8 = UInt(8) <> VAR
//bitwise NOT inverts all bits and
//preserves the argument's type
val t1 = ~v8          //result type: Bits[8]
val t2 = ~u8          //result type: UInt[8]
//AND/OR/XOR apply elementwise between
//two same-width vectors and preserve
//the LHS type
val t3 = v8 | m8      //result type: Bits[8]
val t4 = v8 & h"F0"   //result type: Bits[8]
val t5 = u8 ^ d"8'85" //result type: UInt[8]
//error: an integer value cannot be a
//candidate for a Bits type
val e1 = v8 | 2
//error: the argument widths must match
val e2 = v8 ^ b"1010"
Transitioning from Verilog

lhs & rhs/lhs | rhs/lhs ^ rhs/~lhs on Bits/UInt vector values map directly to Verilog's elementwise bitwise &/|/^/~. Verilog's same-symbol unary reduction operators (&v, |v, ^v) map to the postfix reduction operators instead.

Transitioning from VHDL

lhs & rhs/lhs | rhs/lhs ^ rhs/~lhs on Bits/UInt vector values map to VHDL's and/or/xor/not.

Bit Reduction Operations (.&, .|, .^)

Applies to: Bits, UInt (via implicit conversion to Bits)

Reduction operators fold all bits of a Bits vector into a single Bit value. They are the DFHDL equivalents of Verilog's unary reduction operators (&v, |v, ^v); the infix two-operand &/|/^ between same-width vectors are separate elementwise operations, covered under Bitwise Operations:

Operation Description Returns
bits.& AND reduction: 1 if all bits are 1 Bit
bits.| OR reduction: 1 if any bit is 1 Bit
bits.^ XOR reduction: 1 if an odd number of bits are 1 (parity) Bit
1
2
3
4
val b8 = Bits(8) <> VAR
val allSet   = b8.&    // Bit: 1 when all bits are 1
val anySet   = b8.|    // Bit: 1 when at least one bit is 1
val parity   = b8.^    // Bit: 1 when odd number of bits are 1
Transitioning from Verilog
Verilog DFHDL Notes
&v (AND reduce) v.& All bits must be 1
|v (OR reduce) v.| At least one bit is 1
^v (XOR reduce) v.^ Parity (odd number of 1s)
~&v (NAND reduce) !v.& Not all bits are 1
~|v (NOR reduce) !v.| No bits are 1
~^v (XNOR reduce) !v.^ Even parity

Selection (.sel)

Applies to: any DFHDL type as the selected arguments; the condition itself is a Bit or Boolean.

The .sel operation is a conditional selection, equivalent to Verilog's ternary operator cond ? onTrue : onFalse. It selects between two values based on a Bit or Boolean condition:

Operation Description Returns
cond.sel(onTrue, onFalse) Select onTrue when cond is true/1, onFalse otherwise Same type as the arguments

The onTrue and onFalse arguments can be any DFHDL type: UInt, SInt, Bits, Enum, Struct, etc. They can also be Scala literals constant parameters. The result type is determined by whichever argument is a DFHDL value (the other is auto-converted via type conversion):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
val flag = Boolean <> VAR
val u8   = UInt(8) <> VAR

// Select between two literals
val r1 = flag.sel(11, d"4'12")   // UInt[4]: 11 if true, 12 if false

// Select between DFHDL values
val r2 = flag.sel(u8, d"8'0")    // UInt[8]: u8 if true, 0 if false

// Select with Int parameters
val c1: Int <> CONST = 1
val c2: Int <> CONST = 2
val r3 = flag.sel(c1, c2)        // Int: c1 if true, c2 if false

// Select with other types
val e = flag.sel(MyEnum.A, MyEnum.B)  // MyEnum

Prefer if/match for complex conditions

For simple one-level selections, .sel is concise and maps directly to Verilog's ternary. However, nesting or chaining .sel operations (e.g., a.sel(b.sel(x, y), z)) quickly becomes unreadable. For complex conditional logic, use if/else or match expressions instead; they are clearer and produce equivalent hardware.

Transitioning from Verilog

The .sel operation compiles to Verilog's ternary operator:

DFHDL Verilog
cond.sel(a, b) cond ? a : b
Transitioning from VHDL

VHDL has no equivalent to Verilog's ternary expression. The DFHDL-generated VHDL package includes bool_sel functions that implement this behavior, with dedicated overloads generated for each type as required.

DFHDL Generated VHDL
cond.sel(a, b) bool_sel(cond, a, b)

Arithmetic Operations (+, -, *, /, %)

Applies to: UInt, SInt, Bits (via implicit conversion to UInt), Int, Double (% not available for Double)

Operation Description Returns
lhs + rhs Addition Commutative: widest, most signed
lhs - rhs Subtraction Same type as LHS
lhs * rhs Multiplication Commutative: widest, most signed
lhs / rhs Division Same type as LHS
lhs % rhs Modulo Same type as LHS
lhs max rhs Maximum Commutative: widest, most signed
lhs min rhs Minimum Commutative: widest, most signed
-lhs Unary negation Always signed: see the negation rules below

Bit-Accurate Type Constraints (UInt, SInt)

Commutative operations (+, *, max, min) produce a result that is as wide and as signed as possible given both operands. The narrower operand is resized to match. Operand order does not affect the result type.

Non-commutative operations (-, /, %) use the LHS type as the result. The RHS is resized to match the LHS before the operation is applied. The LHS must be at least as wide and at least as signed as the RHS.

Commutative result type rules (+, *, max, min)

The result is signed if either operand is signed. When mixing signed and unsigned, the unsigned operand is implicitly sign-extended by 1 bit (gaining a 0 sign bit).

LHS Type RHS Type Result Type
UInt[LW] UInt[RW] UInt[Max[LW, RW]]
SInt[LW] SInt[RW] SInt[Max[LW, RW]]
SInt[LW] UInt[RW] SInt[Max[LW, RW + 1]]
UInt[LW] SInt[RW] SInt[Max[LW + 1, RW]]
Non-commutative result type rules (-, /, %)

Sign rule: the LHS sign must be greater than or equal to the RHS sign (signed >= unsigned):

LHS RHS Allowed Note
UInt[W1] UInt[W2] Yes W1 >= W2
SInt[W1] SInt[W2] Yes W1 >= W2
SInt[W1] UInt[W2] Yes W1 >= W2 + 1 (RHS is implicitly widened by 1 bit for the sign bit)
UInt[W1] SInt[W2] No Compile error: an explicit conversion is required

Width rule: the LHS width must be greater than or equal to the (effective) RHS width. When applying SInt op UInt, the effective RHS width is RHS width + 1 because the unsigned value gains an implicit sign bit.

Unary Negation (-)

Unary negation applies to all the decimal types (UInt, SInt, Int, and Double) and to Bits. The result is always signed. SInt, Int, and Double arguments preserve their type and width. UInt[W] and Bits[W] arguments are implicitly converted to SInt[W + 1] before the negation is applied, so the result preserves the exact negated value (e.g., negating d"8'255" yields sd"9'-255").

Argument Type Implicit Conversion Result Type
SInt[W] (none) SInt[W]
UInt[W] .signed to SInt[W + 1] SInt[W + 1]
Bits[W] .uint.signed to SInt[W + 1] SInt[W + 1]
Int (none) Int
Double (none) Double
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
val u8 = UInt(8) <> VAR
val s8 = SInt(8) <> VAR
val b8 = Bits(8) <> VAR
val n1 = -s8    // SInt[8]: same type as the argument
val n2 = -u8    // SInt[9]: equivalent to -u8.signed
val n3 = -b8    // SInt[9]: equivalent to -b8.uint.signed

val s9 = SInt(9) <> VAR
s9 := -u8       // ok: exact fit
s9 := -b8       // ok: exact fit
// error: The applied RHS value width (9) is larger than
// the LHS variable width (8).
s8 := -u8

Wildcard Int Values

Both Scala Int values and DFHDL Int parameters (Int <> CONST) act as wildcards when used in operations with bit-accurate UInt or SInt values. The wildcard Int value adapts to the bit-accurate value's sign and width. If the wildcard Int value does not fit in the bit-accurate value's range or has incompatible sign, an error is generated. One exception: in carry operations a Scala Int operand contributes its value's minimal width instead of adapting, while a DFHDL Int parameter adapts as usual.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
val u8 = UInt(8) <> VAR
val s8 = SInt(8) <> VAR
val param: Int <> CONST = 10

// Wildcard `Int` value adapts to bit-accurate value in commutative ops
u8 + 5              // UInt[8] (5 adapts to UInt[8])
u8 + param          // UInt[8] (param adapts to UInt[8])
s8 + param          // SInt[8] (param adapts to SInt[8])
param + u8          // UInt[8] (commutative, same result)

// Wildcard `Int` value adapts to bit-accurate value in non-commutative ops
u8 - 3              // UInt[8] (3 adapts to UInt[8])
u8 / param          // UInt[8] (param adapts to UInt[8])

// Wildcard `Int` value adapts in comparisons
u8 == 200           // OK (200 fits in UInt[8])
s8 < (-5)           // OK (-5 fits in SInt[8])

// ERROR: wildcard `Int` value does not fit bit-accurate value
u8 + 1000           // ERROR: 1000 exceeds UInt[8] range (0..255)
u8 + (-1)           // ERROR: -1 is negative for unsigned bit-accurate value
s8 + 1000           // ERROR: 1000 exceeds SInt[8] range (-128..127)

See Wildcard Arithmetic Value Checking for details on when these checks occur (compile-time, elaboration-time, or synthesis-time).

Bits values in arithmetic

Bits values are implicit UInt candidates, so they can participate in arithmetic directly. The compiler automatically inserts .uint conversions on the operands and .bits on the result:

1
2
3
val i = Bits(8) <> IN
val o = Bits(8) <> OUT
o := i + i
Elaborates to:
1
o := (i.uint + i.uint).bits
The implicit .bits result conversion requires an exact target width, so the automatic target-context widening described above does not apply to a wider Bits target (a Bits(9) target for i + i is a width-mismatch error). Bits operands widen fine when the target is UInt/SInt (via their implicit .uint conversion); for a genuinely wider Bits target, use an explicit carry operation, whose result width then fits exactly:
1
2
val o9 = Bits(9) <> OUT
o9 := i +^ i   // UInt[9] carry result converts to the exact-width Bits(9)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
val u8 = UInt(8) <> VAR
val u4 = UInt(4) <> VAR
val s8 = SInt(8) <> VAR

// Commutative: result is widest, most signed
val r1 = u8 + u8          // UInt[8]  (max(8,8) = 8)
val r2 = u8 + u4          // UInt[8]  (max(8,4) = 8)
val r3 = u4 + u8          // UInt[8]  (commutative, same as above)
val r4 = s8 + u4          // SInt[8]  (max(8, 4+1) = 8, signed)
val r5 = u8 + s8          // SInt[9]  (max(8+1, 8) = 9, signed)
val r6 = u8 + 200         // UInt[8]  (literal adapts)
val r7 = (-5) + u8        // SInt[8]  (negative literal, signed result)

// Non-commutative: LHS-dominant
val r8 = 200 - u8         // UInt[8]
// error: The applied RHS value width (8) is larger than
// the LHS variable width (4).
val e1 = u4 - u8
// error: Cannot apply this operation between an unsigned
// value (LHS) and a signed value (RHS).
val e2 = u8 - s8

// Int parameter as wildcard in commutative ops
val param: Int <> CONST = 10
val r9  = u8 + param      // UInt[8]  (param adapts to UInt[8])
val r10 = param + u8      // UInt[8]  (commutative, same result)
val r11 = s8 + param      // SInt[8]  (param adapts to SInt[8])
val r12 = param * 2       // Int <> CONST = 20

// Double arithmetic
val d1 = Double <> VAR
val d2 = Double <> VAR
val r13 = d1 + d2         // Double
val r14 = d1 / d2         // Double

Overflow and automatic target-context widening

Standard arithmetic operations wrap on overflow. For example, d"8'255" + d"8'1" produces d"8'0". Use the carry variants (+^, -^, *^) described below to get a wider result that preserves the full value.

However, an anonymous arithmetic expression (+, -, *, unary -) that is assigned or connected to a variable wider than the operation's result is re-evaluated at the target's width and sign, exactly like Verilog's assignment-context width propagation: every operand, recursively through the anonymous expression, is widened to the target type, and the operations stay modular at that width. The carry operators are themselves shorthand for exactly this operand-widened evaluation (x +^ y is x.eby(1) + y.eby(1) with the operands first aligned to a common width), so when a widening lands exactly on a carry shape it prints back as the carry operator.

The widening context also crosses an anonymous .sel (matching Verilog's ?:, whose branch operands are context-determined) and anonymous if/match expressions (matching the per-branch assignments they lower to): each branch re-evaluates at the target, while the selection condition or match selector is unaffected. A shift's left operand is likewise context-determined (matching Verilog; the shift amount is self-determined), as long as the target keeps the operand's signedness: a shift evaluates at its operand's own signedness (an arithmetic-vs-logical >> difference), so a sign-crossing shift context is a boundary and the shifted result converts as a plain value there. The context stops at exactly three kinds of boundaries: a named value (a val-bound expression evaluates at its own declared width and only its result extends), a carry operation (its widened result is already exact), and any other operation (bitwise logic, comparisons, rotations), whose result converts as a plain value.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
val u8  = UInt(8) <> VAR
val u9  = UInt(9) <> VAR
val u10 = UInt(10) <> VAR
val u12 = UInt(12) <> VAR
val u16 = UInt(16) <> VAR
val s9  = SInt(9) <> VAR
u9  := u8 + u8   // carry fit: elaborates to u8 +^ u8
u9  := u8 - u8   // carry fit: elaborates to u8 -^ u8
u16 := u8 * u8   // carry fit: elaborates to u8 *^ u8
u12 := u8 * u8   // beyond the carry fit: u8.eby(4) * u8.eby(4)
u10 := u8 + u8   // target beyond the carry width: u8.eby(2) + u8.eby(2)
s9  := u8 - u8   // unsigned to signed: operands convert, u8.signed - u8.signed

// The context crosses .sel branches (Verilog's ?:), condition untouched:
val c = Bit <> VAR
u9 := c.sel(u8 + u8, u8 - u8)   // elaborates to c.sel(u8 +^ u8, u8 -^ u8)
// ... and if/match EXPRESSION branches the same way:
u9 := (if (c) u8 + u8 else u8 - u8)   // each branch elaborates as a carry op

// A shift's LEFT operand is context-determined (the amount is self-determined),
// so the carry bit survives a >> into a wider target:
u10 := (u8 + u8) >> 1   // elaborates to (u8.eby(2) + u8.eby(2)) >> 1

// Implicit Int operands and whole chains evaluate at the target width:
u10 := u8 + u8 + 1   // elaborates to u10 := u8.eby(2) + u8.eby(2) + d"10'1"

// Named expressions are NOT widened:
val sum = u8 + u8  // UInt[8], named value
u9 := sum          // extended by 1: sum.eby(1)

// Parametric widths decide symbolically and print RELATIVE widenings via `.eby`:
// for a, b: SInt(W) the following hold
//   SInt(W + 1) target: sum := a +^ b
//   SInt(W + 2) target: acc := a.eby(2) + b.eby(2)
//   SInt(2 * W) target: prod := a *^ b
//   SInt(W + 1) target: dx := c.sel(b -^ a, a -^ b)

A parametric width relation is accepted when it holds for every valid parameter assignment, using the fact that widths are positive: SInt(2 * W) accepts a W-wide operation because 2 * W >= W for any valid W. A relation that a valid assignment can violate is definitively rejected (SInt(W) never fits a 2 * W-wide value), and an undecidable one (e.g. a literal target such as SInt(16) against a free W, which may exceed 16) is conservatively rejected as well; both still require an explicit carry op or .resize to state the intent.

Implicit Scala Int and Verilog-semantics mismatch

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:

  • 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.

The patterns DFHDL flags are:

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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
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 + 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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
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.

1
2
3
4
5
6
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

No warning is issued when:

  • The expression uses carry operations (+^, -^, *^), which widen the result.
  • 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 (+, -, *) that feed an assignment. A same-width target wraps identically in both languages, and a wider target re-evaluates the chain at the target width (see the automatic target-context widening above), matching the context Verilog's assignment provides; truncation to the target width commutes with +/-/*, so the two evaluations agree for every input.
    1
    2
    3
    4
    5
    val sum = UInt(10) <> VAR
    // OK: widened to the target, elaborates to sum := a.eby(2) + b.eby(2) + d"10'1"
    sum := a + b + 1
    val cnt = UInt(8) <> VAR
    cnt := cnt + 1                          // OK: same-width target, modular truncation matches
    

Carry Arithmetic (+^, -^, *^)

Applies to: UInt, SInt

Carry operations widen the result to prevent overflow. Mixed signedness is allowed, and the result is signed if either operand is signed. When mixing signs, the unsigned operand is sign-extended by 1 bit.

Carry addition and subtraction (+^, -^):

LHS Type RHS Type Result Type
UInt[LW] UInt[RW] UInt[Max[LW, RW] + 1]
SInt[LW] SInt[RW] SInt[Max[LW, RW] + 1]
SInt[LW] UInt[RW] SInt[Max[LW, RW + 1] + 1]
UInt[LW] SInt[RW] SInt[Max[LW + 1, RW] + 1]

Carry multiplication (*^):

LHS Type RHS Type Result Type
UInt[LW] UInt[RW] UInt[LW + RW]
SInt[LW] SInt[RW] SInt[LW + RW]
SInt[LW] UInt[RW] SInt[LW + RW + 1]
UInt[LW] SInt[RW] SInt[LW + 1 + RW]

Wildcard Int operands: at least one operand must be bit-accurate; a carry operation between two Int values is a compile-time error. A Scala Int operand contributes its value's minimal width to the tables above. A DFHDL Int parameter (Int <> CONST) instead adapts to the bit-accurate operand's sign and width, so +^/-^ widen that operand by one bit and *^ doubles its width.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
val u8 = UInt(8) <> VAR

// Carry addition: width = max(8, 8) + 1 = 9
val r1 = u8 +^ u8           // UInt[9]
// d"8'255" +^ d"8'1" == d"9'256" (no overflow)

// Carry subtraction: width = max(8, 8) + 1 = 9
val r2 = u8 -^ u8           // UInt[9]

// Carry multiplication: width = 8 + 8 = 16
val r3 = u8 *^ u8           // UInt[16]

// Scala Int literal: 100 needs 7 bits
// width = 7 + 8 = 15
val r4 = 100 *^ u8          // UInt[15]

val s8 = SInt(8) <> VAR
val r5 = s8 +^ s8           // SInt[9]
val r6 = s8 *^ s8           // SInt[16]

// DFHDL Int parameter adapts to the bit-accurate operand
val param: Int <> CONST = 3
val r7 = u8 +^ param        // UInt[9]  (param adapts to UInt[8], carry widens to 9)
val r8 = u8 *^ param        // UInt[16] (param adapts to UInt[8], product doubles to 16)
val r9 = s8 +^ param        // SInt[9]  (param adapts to SInt[8])

// error: Carry operations require at least one bit-accurate
// operand (`UInt`/`SInt`), but both operands are `Int` values.
val e1 = param +^ 1

Comparison Operations (==, !=, <, >, <=, >=)

Applies to: UInt, SInt, Int, Double (all comparisons); Bits, Bit, Boolean, Enum, Struct, Tuple (==/!= only)

Decimal Comparisons

Comparison operations on UInt/SInt return a Boolean DFHDL value and have stricter constraints than arithmetic:

Operation Description Returns
lhs == rhs Equal Boolean
lhs != rhs Not equal Boolean
lhs < rhs Less than Boolean
lhs > rhs Greater than Boolean
lhs <= rhs Less than or equal Boolean
lhs >= rhs Greater than or equal Boolean

Unlike arithmetic operations which use relaxed rules (LHS sign >= RHS sign, LHS width >= RHS width), comparisons require exact matching:

  • Sign: Must match exactly (UInt with UInt, SInt with SInt).
  • Width: Must match exactly (both operands must have the same bit width).
  • Scala Int literals: The literal's bit width (adjusted +1 if the DFHDL value is signed and the literal is positive) must fit within the DFHDL value's width.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
val u8 = UInt(8) <> VAR
val u4 = UInt(4) <> VAR
val s8 = SInt(8) <> VAR

val c1 = u8 == u8           // Boolean: same sign, same width
val c2 = u8 < 200           // Boolean: 200 fits in UInt[8]
val c3 = 0 < u8             // Boolean: Scala Int on LHS
val c4 = s8 >= 1            // Boolean: 1 is promoted to SInt (width 2 fits in 8)

// error: Cannot apply this operation between an unsigned
// value (LHS) and a signed value (RHS).
// An explicit conversion must be applied.
val e1 = u8 == s8
// error: Cannot apply this operation between a value of
// 8 bits width (LHS) and a value of 4 bits width (RHS).
// An explicit conversion must be applied.
val e2 = u8 == u4
// error: Cannot compare a DFHDL value (width = 8) with a
// Scala `Int` argument that is wider (width = 10).
// An explicit conversion must be applied.
val e3 = u8 > 1000
Scala Int constants auto-lift in comparisons

Plain Scala Int values can be used directly in comparisons and arithmetic with DFHDL typed variables. No explicit coercion is needed:

1
2
3
4
val LIMIT: Int <> CONST = 5208
val counter = UInt.until(LIMIT) <> VAR
if (counter == LIMIT - 1)  // Int <> CONST compared with UInt: works directly
  counter := 0

Bits Comparisons

Bits values support == and != with other Bits values of the same width, with all(0), all(1), or with sized literals (d"...", h"...", b"..."). Plain Scala Int literals cannot be compared directly with Bits. Use a sized literal or convert to .uint first:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
val b8 = Bits(8) <> VAR
val isAllOnes  = b8 == all(1)      // Boolean: all bits are 1
val isAllZeros = b8 == all(0)      // Boolean: all bits are 0
val isMatch    = b8 == h"B0"       // Boolean: exact match with hex literal
val isDec      = b8 == d"8'12"     // Boolean: match with sized decimal

// ERROR: An integer value cannot be a candidate for a Bits type.
// val bad = b8 == 0
// FIX: use all(0), a sized literal, or convert to UInt first:
// b8 == all(0)  OR  b8 == d"8'0"  OR  b8.uint == 0

Bit/Boolean Comparisons

A Bit or Boolean is a valid operand of ==/!=, against another Bit/Boolean or against the 0/1 and true/false literals. The result is a Boolean, like every other comparison:

1
2
3
4
5
6
7
val b1, b2 = Bit <> VAR
val bl     = Boolean <> VAR

val c1 = b1 == 0      // Boolean
val c2 = b1 != 1      // Boolean
val c3 = b1 == b2     // Boolean
val c4 = bl == true   // Boolean

b == 0 and !b describe the same hardware. A Verilog translation produces the former (Verilog spells the test b == 1'b0), while idiomatic DFHDL tends toward the latter; use whichever keeps the source recognizable.

Ordering comparisons (<, >, <=, >=) do not apply to Bit/Boolean. They are rejected at compile time:

1
2
// error: Cannot compare DFHDL value of type `Bit` with value of type `1`.
val e1 = b1 < 1

Enum, Struct, and Tuple Comparisons

Enums, structs, and tuples support equality comparisons (== and !=) between values of the same type:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
val e1 = MyEnum <> VAR
val e2 = MyEnum <> VAR
val eq = e1 == e2   // Boolean

val s1 = MyStruct <> VAR
val s2 = MyStruct <> VAR
val eq2 = s1 == s2  // Boolean

val t1 = (UInt(8), Bit) <> VAR
val t2 = (UInt(8), Bit) <> VAR
val eq3 = t1 == t2  // Boolean

Shift Operations (<<, >>)

Applies to: Bits, UInt, SInt

Operation Description LHS/RHS Constraints Returns
lhs << rhs Left shift LHS: Bits/UInt/SInt, RHS: unsigned or Int Same type as LHS
lhs >> rhs Right shift (logical for Bits/UInt, arithmetic for SInt) LHS: Bits/UInt/SInt, RHS: unsigned or Int Same type as LHS

The >> operator is type-aware: on UInt and Bits it performs a logical (zero-filling) right shift, and on SInt it performs an arithmetic (sign-extending) right shift. There is no separate >>> operator in DFHDL; the operand type determines the behavior.

1
2
3
4
5
6
7
val b = Bits(8) <> VAR
val u = UInt(8) <> VAR
val s = SInt(8) <> VAR

val b_shifted = b << 2  // logical left shift
val u_shifted = u >> 2  // logical right shift (zero-fills MSBs)
val s_shifted = s >> 2  // arithmetic right shift (sign-extends MSBs)

Max/Min Operations (max, min)

Applies to: Int, Double

Operation Description Returns
lhs max rhs Maximum of two values Same type
lhs min rhs Minimum of two values Same type
1
2
3
4
5
6
7
8
val param: Int <> CONST = 2
val t1 = 1 max param    // Int <> CONST = 2
val t2 = 1 min param    // Int <> CONST = 1

val d1 = Double <> VAR
val d2 = Double <> VAR
val t3 = d1 max d2      // Double
val t4 = d1 min d2      // Double

Physical Arithmetic (Time, Freq)

Applies to: Time, Freq

Physical types follow dimensional analysis rules. Operations between Time and Freq produce dimensionally correct results.

Operation LHS RHS Returns
lhs + rhs Time Time Time
lhs - rhs Time Time Time
lhs * rhs Time Number Time
lhs * rhs Freq Number Freq
lhs * rhs Time Freq Number
lhs * rhs Freq Time Number
lhs / rhs Time Number Time
lhs / rhs Freq Number Freq
lhs / rhs Time Time Number
lhs / rhs Freq Freq Number
lhs / rhs Number Time Freq
lhs / rhs Number Freq Time
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
val period = 10.ns
val freq   = 100.MHz

// Scaling
val half_period = period / 2       // Time: 5 ns
val double_freq = freq * 2         // Freq: 200 MHz

// Dimensional conversions
val cycles = period * freq         // Number: 1.0
val calc_freq = 1 / period         // Freq: 100 MHz
val calc_period = 1 / freq         // Time: 10 ns

Cycle-based waits in RT domains

The .cy unit creates cycle-count values for use with .wait in register-transfer domains:

1
2
class Example extends RTDesign:
  5.cy.wait        // wait 5 clock cycles

Int Parameter Operations (**, clog2)

Applies to: Int (constant parameters)

These operations are available for Scala Int or DFHDL Int <> CONST values and are primarily used for compile-time calculations such as computing bit widths.

Operation Description Returns
lhs ** rhs Power (exponentiation) Int
clog2(value) Ceiling of log base 2 Int
1
2
3
4
val param: Int <> CONST = 2
val t1 = 3 ** param     // Int <> CONST = 9
val t2 = 2 ** param     // Int <> CONST = 4
val w  = clog2(256)     // Int = 8 (bits needed to represent 0..255)

Avoid using clog2 directly for widths

A common anti-pattern is using clog2 to declare the width of bit-accurate values:

1
2
3
// DON'T do this:
val addr = UInt(clog2(DEPTH)) <> VAR
val mask = Bits(clog2(SIZE)) <> VAR
Instead, use the .until or .to constructors which handle this automatically and are more readable:
1
2
3
// DO this instead:
val addr = UInt.until(DEPTH) <> VAR   // width = clog2(DEPTH)
val mask = Bits.until(SIZE) <> VAR    // width = clog2(SIZE)
See the DFType Constructors and Bits constructors sections for details on .until and .to.

When the computed width itself is needed, for example to pass it on to a child design or to size a related field, recover it from the constructed type with .width instead of calling clog2 yourself:

1
val ADDR_WIDTH = UInt.until(DEPTH).width   // Int <> CONST = clog2(DEPTH)
See Width and Length Queries for details.

Non-constant DFHDL Int values

Non-constant DFHDL Int values (e.g., Int <> VAR) are possible and support the same arithmetic operations (+, -, *, /, %). However, they are discouraged for synthesizable designs because they map to a fixed 32-bit signed representation; use SInt[32] instead for explicit control over the hardware. For simulation purposes, non-constant Int values are acceptable as long as the 32-bit width limitation is understood.

Slicing bits from a DFHDL Int

To extract a partial bit range from a DFHDL Int value, first convert it to Bits using .bits, then apply the slice: myInt.bits(hi, lo). This is a .bits conversion followed by (hi, lo) slicing. The .bits conversion is a DFHDL extension method available on DFHDL Int <> CONST values, not on plain Scala Int.

Width and Length Queries (.width, .length)

Applies to: .width: any DFType and any DFHDL value; .length: Bits/UInt/SInt DFTypes and values, and Vector DFTypes and values

Both queries return a constant DFHDL Int value (Int <> CONST) rather than a plain Scala Int, so they compose with design parameters: querying a parametric type keeps the result symbolic, and the generated code carries the width expression (clog2(DEPTH), LANE * LANES, and so on) instead of a folded number. A query over a DFHDL VALUE is spelled natively in the generated code where the target language has a width query: $bits(x)/$size(x) in SystemVerilog and x'length/bitWidth(x) in VHDL (dialects without one, such as Verilog-2001, inline the width expression instead). See Inter-Dependent Design Parameters for the canonical use.

Operation Description Returns
x.width The total bit width of x, a DFType or a DFHDL value Int <> CONST
x.length For Bits/UInt/SInt: the number of bits, identical to .width. For Vector: the number of elements Int <> CONST

Applying .width directly on a DFType is the DFHDL counterpart of Verilog's $clog2 width derivation: construct the type with UInt.until/UInt.to (or their Bits counterparts) and recover the width the constructor computed:

1
2
3
4
5
class Foo(val DEPTH: Int <> CONST = 854) extends RTDesign:
  // like Verilog's `$clog2(DEPTH)`, and stays parametric: the generated code keeps the
  // named constant `ADDR_WIDTH = clog2(DEPTH)`
  val ADDR_WIDTH = UInt.until(DEPTH).width
  val addr = UInt(ADDR_WIDTH) <> VAR

For vectors the two queries answer different questions: .length counts elements, while .width is the total bit width (the element count times the element width).

1
2
3
4
5
6
val w8    = Bits(8).width      // Int <> CONST = 8
val vec   = Bits(8) X 4 <> VAR
val elems = vec.length         // Int <> CONST = 4  (elements)
val bits  = vec.width          // Int <> CONST = 32 (total bits: 4 * 8)
val o     = UInt(8) <> OUT
val ol    = o.length           // Int <> CONST = 8, same as `o.width`

Declare with .until/.to, recover with .width

Prefer declaring range-derived values directly with the .until/.to constructors and reach for .width only where the width itself is the value you need. The declaration then remains the single source of the width relationship, and every derived width follows it.

History Operations

Applies to: Bit (.rising, .falling)

These operations are supported under both RT and ED domains. Under RT domain, these operations are synthesizable expressions.

Operation Description LHS Constraints Returns
lhs.rising True when a value changes from 0 to 1 Bit DFHDL value Boolean DFHDL value
lhs.falling True when a value changes from 1 to 0 Bit DFHDL value Boolean DFHDL value
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class Foo extends EDDesign:
  val clk  = Bit <> IN

  /* VHDL-style */
  process(clk):
    if (clk.rising) 
      //some sequential logic

  /* Verilog-style */
  process(clk.rising):
    //some sequential logic
Transitioning from Verilog

Under the ED domain, the x.rising and x.falling operations are equivalent to the Verilog posedge x and negedge x, respectively.

Transitioning from VHDL

Under the ED domain, the x.rising and x.falling operations are equivalent to the VHDL rising_edge(x) and falling_edge(x), respectively.

The following RT domain edge detection design:

1
2
3
4
class Detector extends RTDesign:
  val i = Bit <> IN
  val o = Bit <> OUT
  o := i.rising
is compiled down to the following ED design (depending on the clock and reset configurations):
1
2
3
4
5
6
7
8
class Detector extends EDDesign:
  val clk   = Bit <> IN
  val i     = Bit <> IN
  val o     = Bit <> OUT
  val i_reg = Bit <> VAR init 1
  process(clk.rising):
    i_reg :== i
  o <> !i_reg && i
The initial (reset) register value is 1/0 for rising/falling operations, respectively. This inherently prevents triggering immediately after reset, without sampling at least two input clock cycles.

Both Verilog and VHDL have no equivalent synthesizable shorthand syntax.

For more information see either the design domains or processes sections.

Constant Meta Operations

Applies to: constant Bit/Boolean values

These operations are activated during the elaboration stage of the DFHDL compilation, and are only available for constant Bit/Boolean DFHDL values. Their use case is for meta-programming purposes, to control the generated code without the knowledge of the DFHDL compiler (could be considered as pre-processing steps).

Operation Description LHS Constraints Returns
lhs.toScalaBitNum Extracts the known elaboration Scala BitNum(1 | 0) value from a constant DFHDL Bit/Boolean value Constant Bit/Boolean DFHDL value Scala BitNum value
lhs.toScalaBoolean Extracts the known elaboration Scala Boolean value from a constant DFHDL Bit/Boolean value Constant Bit/Boolean DFHDL value Scala Boolean value

The following runnable example demonstrates how such meta operation affect the elaborated design. The Boolean argument arg of a design Foo is used twice within the design: first, in an if condition directly; and second, in an if condition after a Scala value extraction. When referenced directly, the if is elaborated as-is, but when the if is applied on the extracted Scala value, the if is completely removed and either the block inside the if is elaborated when the argument is true or completely removed if false.

1
2
3
4
5
6
class Foo(
    val arg: Boolean <> CONST
) extends DFDesign:
    val o = Bit <> OUT
    if (!arg) o := 1 
    if (arg.toScalaBoolean) o := 0
1
2
3
4
5
6
class Foo(
    val arg: Boolean <> CONST
) extends DFDesign:
    val o = Bit <> OUT
    if (!arg) o := 1 
    o := 0
1
2
3
4
5
class Foo(
    val arg: Boolean <> CONST
) extends DFDesign:
    val o = Bit <> OUT
    if (!arg) o := 1 
Runnable example
import dfhdl.*

class Foo(
    val arg: Boolean <> CONST
) extends DFDesign:
  val o = Bit <> OUT
  if (!arg) o := 1 
  if (arg.toScalaBoolean) o := 0

@main def main = 
  println("Foo(true) Elaboration:")
  Foo(true).printCodeString
  println("Foo(false) Elaboration:")
  Foo(false).printCodeString

Vector Element Access

Applies to: Vector

1
2
val elem = vec(idx)     // Read element at index
vec(idx) := newValue    // Write element at index
Operation Description Returns
vec(idx) Access element at index Element type
vec.elements Get all elements as Scala sequence Seq[BaseType]
vec.length Get the number of elements Int <> CONST
vec.width Get the total bit width (see Width and Length Queries) Int <> CONST