Skip to content

Design Hierarchy

DFHDL supports composable design hierarchies through design class instantiation and port connections.

Terminology

  • design - A Scala class extending XXDesign, where XX can be DF, RT, or ED, corresponding to the desired design domain.
  • design member - Any DFHDL object instantiated within a design (the design contains or owns all its members).
  • child design/component - A design instance that is owned by another design.
  • top design - The highest-level design in the hierarchy (not contained by any other design), also known as the top-level design.
  • top-app design - A @top annotated top design that generates a main entry point with the default application.
topconstchild A param1param2child B in portout port param

Design Declaration

Syntax

A DFHDL design declaration follows standard Scala class syntax, with specialized handling by the DFHDL Scala compiler plugin.

Design declaration syntax
/** _documentation_ */
@top(genMain) //required only for top-level designs
[_modifiers_] class _name_(_params_) extends XXDesign:
  _contents_
end _name_ //optional `end` marker
  • _name_ - The Scala class name for the design. The DFHDL compiler preserves this name and uses it in error messages and generated artifacts (e.g., Verilog modules or VHDL entities). See the naming section for details.

  • (_params_) - An optional parameter block. This can include either Scala parameters that are inlined during design elaboration, or DFHDL design parameters that are preserved through elaboration and compilation. If no parameters are needed, Scala syntax accepts either empty parentheses () or no parentheses. See Parameter Block Syntax for details.

  • _XXDesign_ - The base class to extend, where XX specifies the design domain: DF for dataflow, RT for register-transfer, or ED for event-driven.

  • _contents_ - The design interface (ports/interfaces/domains) and functionality (variables, functions, child designs, processes, etc.), based on the selected design domain's semantics.

  • @top(genMain) - A required annotation for top-level designs (designs not instantiated within another design). The annotation has an optional val genMain: Boolean = true parameter:

    • When genMain = true, the design becomes a top-app design where all parameters must have default values, and a main Scala entry point named top__name_ is generated
    • When genMain = false, the annotation only provides a default top-level context for the design
  • _documentation_ - Design documentation in Scaladoc format. This documentation is preserved throughout compilation and included in the generated backend code.

  • _modifiers_ - Optional Scala modifiers. See Design Class Modifier Rules for details.

LeftShift2 example

Basic top-app design example: a two-bits left shifter

The DFHDL code below implements a two-bit left shifter design named LeftShift2. The design:

  • Uses register-transfer (RT) domain semantics by extending RTDesign
  • Has an 8-bit input port and an 8-bit output port
  • Performs a fixed 2-bit left shift operation on the input
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
import dfhdl.*
//optionally set the default backend configuration option
//(can be overridden by the top-app CLI)
given options.CompilerOptions.Backend = backends.verilog

/** A two-bits left shifter */
@top class LeftShift2 extends RTDesign:
  /** bits input */
  val iBits = Bits(8) <> IN
  /** bits output */
  val oBits = Bits(8) <> OUT
  oBits := iBits << 2
LeftShift22<< iBitsoBits

Since this design is annotated with @top, it is a top-app design that generates an executable Scala program. This program compiles the design and generates backend code (Verilog or VHDL). The backend can be configured through:

  • Command-line arguments when running the program
  • Implicit backend settings in the code (as shown in this example)

The @top annotation captures any implicit/given options within its scope and provides them as defaults when no CLI arguments are specified.

Looking at the generated Verilog code, we can observe several key differences from the DFHDL source:

  1. Module Interface: DFHDL's Scala-style port declarations (<> IN/OUT) are translated to traditional Verilog port declarations (input wire/output logic)

  2. Documentation: Scaladoc comments are preserved and converted to Verilog-style comments (/* */)

  3. Default Settings: The compiler adds standard Verilog settings like `default_nettype none and `timescale

  4. Include Files: The compiler adds necessary include files for backend-specific definitions

  5. Assignment Syntax: DFHDL's := assignments are translated to Verilog's assign statements

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
/* A two-bits left shifter */
`default_nettype none
`timescale 1ns/1ps
`include "LeftShift2_defs.svh"

module LeftShift2(
  /* bits input */
  input  wire logic [7:0] iBits,
  /* bits output */
  output      logic [7:0] oBits
);
  `include "dfhdl_defs.svh"
  assign oBits = iBits << 2;
endmodule

The generated VHDL code shows similar transformations from the DFHDL source:

  1. Entity Interface: DFHDL's port declarations are translated to VHDL's in/out mode declarations with explicit signal types

  2. Documentation: Scaladoc comments are preserved as VHDL comments (--)

  3. Library/Package Usage: The compiler adds necessary library and package references (ieee, std_logic_1164, etc.)

  4. Signal Types: DFHDL's Bits type is translated to VHDL's std_logic_vector with appropriate widths

  5. Assignment Syntax: While both DFHDL and VHDL use :=, the semantics differ - DFHDL represents high-level connections while VHDL represents signal assignments

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
-- A two-bits left shifter 
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use work.dfhdl_pkg.all;
use work.LeftShift2_pkg.all;

entity LeftShift2 is
port (
  -- bits input 
  iBits : in  std_logic_vector(7 downto 0);
  -- bits output 
  oBits : out std_logic_vector(7 downto 0)
);
end LeftShift2;

architecture LeftShift2_arch of LeftShift2 is
begin
  oBits <= slv_sll(iBits, 2);
end LeftShift2_arch;
Runnable example
import dfhdl.*
//optionally set the default backend configuration option
//(can be overridden by the top-app CLI)
given options.CompilerOptions.Backend = backends.verilog

/** A two-bits left shifter */
@top class LeftShift2 extends RTDesign:
  /** bits input */
  val iBits = Bits(8) <> IN
  /** bits output */
  val oBits = Bits(8) <> OUT
  oBits := iBits << 2

Parameter Block Syntax

The DFHDL design parameter block follows standard Scala syntax, accepting a comma-separated list of parameter declarations:

Design declaration parameter block syntax
([_access_] _name_: _type_ [= _default_], ...)
  • _type_ - Either a pure Scala type or a DFHDL parameter type (DFType <> CONST):

    • Pure Scala parameters are inlined during elaboration
    • DFHDL parameters are preserved in the generated backend code
    • See Design Parameter Type Rules for details
  • _name_ - The parameter name. For DFHDL parameters, this name is:

    • Preserved throughout compilation
    • Used in the generated backend code
    • Available through the CLI for top-app designs
  • _default_ - Optional default value. Required for all parameters in top-app designs. See Design Parameter Default Value Rules for details.

  • _access_ - Optional Scala access modifier. Usually val to make the parameter public. See Design Parameter Access Rules for details.

LeftShiftBasic example

Scala-parameterized top-app design example: a basic left shifter

The DFHDL code below implements a basic left shifter design named LeftShiftBasic. This design is similar to the earlier example of LeftShift2 except here the design has the shift value as an input, and its input and output port widths are set according to the Scala parameter width.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
/** A basic left shifter */
@top class LeftShiftBasic(
    val width: Int = 8
) extends RTDesign:
  /** bits input */
  val iBits = Bits(width) <> IN
  /** requested shift */
  val shift = UInt.until(width) <> IN
  /** bits output */
  val oBits = Bits(width) <> OUT
  oBits := iBits << shift
LeftShiftBasic<< shiftiBitsoBitswidth
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
/* A basic left shifter */
`default_nettype none
`timescale 1ns/1ps
`include "LeftShiftBasic_defs.svh"

module LeftShiftBasic(
  /* bits input */
  input  wire logic [7:0] iBits,
  /* requested shift */
  input  wire logic [2:0] shift,
  /* bits output */
  output      logic [7:0] oBits
);
  `include "dfhdl_defs.svh"
  assign oBits = iBits << shift;
endmodule
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
-- A basic left shifter 
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use work.dfhdl_pkg.all;
use work.LeftShiftBasic_pkg.all;

entity LeftShiftBasic is
port (
  -- bits input 
  iBits : in  std_logic_vector(7 downto 0);
  -- requested shift 
  shift : in  unsigned(2 downto 0);
  -- bits output 
  oBits : out std_logic_vector(7 downto 0)
);
end LeftShiftBasic;

architecture LeftShiftBasic_arch of LeftShiftBasic is
begin
  oBits <= slv_sll(iBits, to_integer(shift));
end LeftShiftBasic_arch;
Runnable example
import dfhdl.*
given options.CompilerOptions.Backend = backends.verilog

/** A basic left shifter */
@top class LeftShiftBasic(
    val width: Int = 8
) extends RTDesign:
  /** bits input */
  val iBits = Bits(width) <> IN
  /** requested shift */
  val shift = UInt.until(width) <> IN
  /** bits output */
  val oBits = Bits(width) <> OUT
  oBits := iBits << shift

The basic code shifter above did not generate the width parameter in the Verilog and VHDL backend code. The following example shows how to preserve the width parameter:

LeftShiftGen example

DFHDL-parameterized top-app design example: a generic left shifter

The DFHDL code below implements a generic left shifter design named LeftShiftGen. This design is similar to the earlier example of LeftShiftBasic, except here the width parameter is now a DFHDL parameter, as indicated by its Int <> CONST type. This enables the DFHDL compiler to preserve the parameter name and directly use it in the generated backend code where applicable.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
/** A generic left shifter 
  *   
  * @param width
  *   the width of the input and output bits
  */
@top class LeftShiftGen(
    val width: Int <> CONST = 8,
) extends RTDesign:
  /** bits input */
  val iBits = Bits(width)       <> IN
  /** requested shift */
  val shift = UInt.until(width) <> IN
  /** bits output */
  val oBits = Bits(width)       <> OUT
  oBits := iBits << shift
LeftShiftGen<< shiftiBitsoBitswidth
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
/* A generic left shifter 

   @param width
     the width of the input and output bits
  */
`default_nettype none
`timescale 1ns/1ps
`include "LeftShiftGen_defs.svh"

module LeftShiftGen#(parameter int width = 8)(
  /* bits input */
  input  wire logic [width - 1:0]         iBits,
  /* requested shift */
  input  wire logic [$clog2(width) - 1:0] shift,
  /* bits output */
  output      logic [width - 1:0]         oBits
);
  `include "dfhdl_defs.svh"
  assign oBits = iBits << shift;
endmodule
 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
-- A generic left shifter 
--   
-- @param width
--   the width of the input and output bits
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use work.dfhdl_pkg.all;
use work.LeftShiftGen_pkg.all;

entity LeftShiftGen is
generic (
  width : integer := 8
);
port (
  -- bits input 
  iBits : in  std_logic_vector(width - 1 downto 0);
  -- requested shift 
  shift : in  unsigned(clog2(width) - 1 downto 0);
  -- bits output 
  oBits : out std_logic_vector(width - 1 downto 0)
);
end LeftShiftGen;

architecture LeftShiftGen_arch of LeftShiftGen is
begin
  oBits <= slv_sll(iBits, to_integer(shift));
end LeftShiftGen_arch;
Runnable example
import dfhdl.*
given options.CompilerOptions.Backend = backends.verilog

/** A generic left shifter 
  *   
  * @param width
  *   the width of the input and output bits
  */
@top class LeftShiftGen(
    val width: Int <> CONST = 8,
) extends RTDesign:
  /** bits input */
  val iBits = Bits(width)       <> IN
  /** requested shift */
  val shift = UInt.until(width) <> IN
  /** bits output */
  val oBits = Bits(width)       <> OUT
  oBits := iBits << shift

Design Parameter Type Rules

  • Any pure Scala parameter or DFHDL parameter types are acceptable.
  • Top-app design parameters that can be modified from the CLI must be one of:
    • Pure Scala Types: String, Boolean, Int, and Double
    • DFHDL Types: Int <> CONST, Bit <> CONST, and Boolean <> CONST

Top-app design with accepted and ignored CLI arguments example

In this example, the top-app supported parameters pureIntArg and dfhdlIntArg are preserved to be modifiable from the CLI, while ignored and dfhdlIgnored keep their default values.

DFHDL code
1
2
3
4
5
6
7
8
import dfhdl.*
class CustomArg
@top class Foo(
    val pureIntArg:   Int              = 5,
    val dfhdlIntArg:  Int <> CONST     = 7,
    val ignored:      CustomArg        = CustomArg(),
    val dfhdlIgnored: Bits[8] <> CONST = all(0)
) extends DFDesign
CLI help mode output, when running via sbt (truncated)
1
2
3
4
5
Design Name: Foo
Usage: sbt runMain "top_Foo [design-args] <mode> [options]"
 Design arguments:
      --pureIntArg  <Int>    (default = 5)
      --dfhdlIntArg  <Int>   (default = 7)
Runnable example
import dfhdl.*
//this option forces the top-app
//to run help mode by default
given options.AppOptions.DefaultMode = 
  options.AppOptions.DefaultMode.help
class CustomArg
@top class Foo(
    val pureIntArg:   Int              = 5,
    val dfhdlIntArg:  Int <> CONST     = 7,
    val ignored:      CustomArg        = CustomArg(),
    val dfhdlIgnored: Bits[8] <> CONST = all(0)
) extends DFDesign

Design Parameter Default Value Rules

For top-app designs, all parameters must have default values.

@top annotation and default parameter value requirement example

This example shows FooErr is missing a default value and throws an error. There are three ways to resolve this, shown in FooOK1, FooOK2, and FooOK3.

 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
//Error: Missing argument's default value 
//for top-level design with a default app 
//entry point. Either add a default value 
//or disable the app entry point generation 
//with `@top(false)`.
@top class FooErr(
    val arg1: Int = 5,
    val arg2: Boolean <> CONST
) extends DFDesign

//OK: all parameters have default values
//Top-app capability: YES
//Top-level design capability: YES
@top class FooOK1(
    val arg1: Int = 5,
    val arg2: Boolean <> CONST = true
) extends DFDesign

//OK: the `genMain` argument in the `@top` 
//annotation is set to `false`
//Top-app capability: NO
//Top-level design capability: YES
@top(false) class FooOK2(
    val arg1: Int = 5,
    val arg2: Boolean <> CONST
) extends DFDesign

//OK: no top annotation
//Top-app capability: NO
//Top-level design capability: NO
class FooOK3(
    val arg1: Int = 5,
    val arg2: Boolean <> CONST
) extends DFDesign

Good design practice - avoid default parameter value overuse

Overusing default parameter values is considered bad design practice. In general, default values should be used sparingly and only to define "sensible defaults" for parameters that are rarely changed. A good rule of thumb is to avoid default values that affect a design's interface (e.g., the width of a port).

Design Parameter Access Rules

Without any Scala access modifier, a Scala class parameter access is declared as private val. This default access leads to an error if that parameter affects the type of non-private class member (e.g., a width parameter affecting the bits width of a port). To resolve this error, the parameter can be declared as public val, as protected val, or even private[scope] val with a scope access qualifier.

Parameter access example

This example shows an access error in FooErr, where a private parameter width affects the type of a non-private member v. There are four ways to resolve this error, shown in FooOK1, FooOK2, FooOK3, and FooOK4. FooOK5 demonstrates that private parameters can be accessed by public members as long as they don't affect the type.

 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
//Error: non-private value v in class FooErr 
//refers to private value width ... (lengthy 
//type description)
class FooErr(
    width: Int <> CONST
) extends DFDesign:
  val v = UInt(width) <> VAR

//OK: both parameter and its dependent design 
//members are public
class FooOK1(
    val width: Int <> CONST
) extends DFDesign:
  val v = UInt(width) <> VAR

//OK: both parameter and its dependent design 
//members are private (note that ports should
//never be private, and therefore parameters
//that affect their types should never be 
//private)
class FooOK2(
    width: Int <> CONST
) extends DFDesign:
  private val v = UInt(width) <> VAR

//OK: the parameter is protected
class FooOK3(
    protected val width: Int <> CONST
) extends DFDesign:
  val v = UInt(width) <> VAR

package Bar:
  //OK: the parameter is private but only 
  //outside the scope of `Bar`.
  class FooOK4(
      private[Bar] val width: Int <> CONST
  ) extends DFDesign:
    val v = UInt(width) <> VAR

//OK: the parameter `v0` is private, but it
//does not affect the type of the public
//dfhdl variable `v`. Only `width` affects
//the type of `v` and it's public as well.
class FooOK5(
    v0: Int <> CONST,
    val width: Int <> CONST
) extends DFDesign:
  val v = UInt(width) <> VAR init v0

Good design practice - how to choose the right parameter/member access?

  • For Simple Development - During initial development, you can declare all parameters and named design members as public val for simplicity.
  • Protection for Shared Code - When sharing your design with others in DFHDL (Scala) form, follow good design practices to maintain source and binary compatibility of your Scala library artifacts. Remember: You can always remove protection without breaking code, but adding protection later will cause breakage.
    • Public Interface, Protected Implementation - Keep the design interface (ports, domains, interfaces, and their type-affecting parameters) public. All other design class members should have private or protected access modifiers.
    • Private vs. Protected Access - Use private access for members that should only be accessible within the design class itself. Use protected access for members that should also be accessible by subclasses.
    • Scoped Protection for Testing - For verification code that needs access to internal design members, use package-private access with a scope qualifier (e.g. private[mylib]) instead of making members fully public. This restricts access to just your library's test code.

In this example, the Foo class demonstrates good design practices for parameter and member access.

Good design practice example
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
package mylib
import dfhdl.*

class Foo(
    val width: Int <> CONST,
    val boolParam: Boolean <> CONST,
    protected[mylib] val someValue: Int <> CONST
) extends DFDesign:
  //Public interface
  val i = UInt(width) <> IN
  val o = UInt(width) <> OUT
  //Protected implementation
  protected[mylib] val v = UInt(width) <> VAR

Design Class Modifier Rules

A DFHDL design class cannot be declared as final class or case class. Attempting to do so produces an error:

DFHDL design class modifier limitation example
1
2
3
4
//error: DFHDL classes cannot be final classes.
final class Foo extends DFDesign
//error: DFHDL classes cannot be case classes.
case class Bar() extends DFDesign

All other Scala class modifiers have no special effect or limitation from a DFHDL compiler perspective. Nonetheless, these modifiers can be relevant when defining a more complex design API, as part of the DFHDL meta-programming capabilities through the Scala language (e.g., changing class access to protected).

LRShiftFlat example

Generic left-right shifter, flat design example

The DFHDL code below implements a generic left-right shifter flat design named LRShiftFlat. This design expands on LeftShiftGen by adding a dir enum port value that specifies the shift direction and a shift operation multiplexer through a match statement.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
enum ShiftDir extends Encode:
  case Left, Right

/** A left-right bits shifter (flat version)
  *
  * @param width
  *   the width of the input and output bits
  */
@top class LRShiftFlat(
    val width: Int <> CONST = 8
) extends RTDesign:
  /** bits input */
  val iBits = Bits(width)       <> IN
  /** requested shift */
  val shift = UInt.until(width) <> IN
  /** direction of shift */
  val dir   = ShiftDir          <> IN
  /** bits output */
  val oBits = Bits(width)       <> OUT
  oBits := dir match
    case ShiftDir.Left  => iBits << shift
    case ShiftDir.Right => iBits >> shift
LRShiftFlat<< >> mux dirshiftiBitsoBitswidth
 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
/* A left-right bits shifter (flat version)

   @param width
     the width of the input and output bits
  */
`default_nettype none
`timescale 1ns/1ps
`include "LRShiftFlat_defs.svh"

module LRShiftFlat#(parameter int width = 8)(
  /* bits input */
  input  wire logic [width - 1:0]         iBits,
  /* requested shift */
  input  wire logic [$clog2(width) - 1:0] shift,
  /* direction of shift */
  input  wire t_enum_ShiftDir             dir,
  /* bits output */
  output      logic [width - 1:0]         oBits
);
  `include "dfhdl_defs.svh"
  always_comb
  begin
    case (dir)
      ShiftDir_Left:  oBits = iBits << shift;
      ShiftDir_Right: oBits = iBits >> shift;
    endcase
  end
endmodule
1
2
3
4
5
6
7
8
`ifndef LRSHIFTFLAT_DEFS
`define LRSHIFTFLAT_DEFS
typedef enum logic [0:0] {
  ShiftDir_Left  = 0,
  ShiftDir_Right = 1
} t_enum_ShiftDir;

`endif
 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
-- A left-right bits shifter (flat version)
--
-- @param width
--   the width of the input and output bits
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use work.dfhdl_pkg.all;
use work.LRShiftFlat_pkg.all;

entity LRShiftFlat is
generic (
  width : integer := 8
);
port (
  -- bits input 
  iBits : in  std_logic_vector(width - 1 downto 0);
  -- requested shift 
  shift : in  unsigned(clog2(width) - 1 downto 0);
  -- direction of shift 
  dir   : in  t_enum_ShiftDir;
  -- bits output 
  oBits : out std_logic_vector(width - 1 downto 0)
);
end LRShiftFlat;

architecture LRShiftFlat_arch of LRShiftFlat is
begin
  process (all)
  begin
    case dir is
      when ShiftDir_Left  => oBits <= slv_sll(iBits, to_integer(shift));
      when ShiftDir_Right => oBits <= slv_srl(iBits, to_integer(shift));
    end case;
  end process;
end LRShiftFlat_arch;
 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
51
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use work.dfhdl_pkg.all;

package LRShiftFlat_pkg is
type t_enum_ShiftDir is (
  ShiftDir_Left, ShiftDir_Right
);
function bitWidth(A: t_enum_ShiftDir) return integer;
function to_slv(A: t_enum_ShiftDir) return std_logic_vector;
function to_t_enum_ShiftDir(A: std_logic_vector) return t_enum_ShiftDir;
function bool_sel(C : boolean; T : t_enum_ShiftDir; F : t_enum_ShiftDir) return t_enum_ShiftDir;


end package LRShiftFlat_pkg;

package body LRShiftFlat_pkg is
function bitWidth(A : t_enum_ShiftDir) return integer is
begin
  return 1;
end;
function to_slv(A : t_enum_ShiftDir) return std_logic_vector is
  variable int_val : integer;
begin
  case A is
    when ShiftDir_Left  => int_val := 0;
    when ShiftDir_Right => int_val := 1;
  end case;
  return resize(to_slv(int_val), 1);
end;
function to_t_enum_ShiftDir(A : std_logic_vector) return t_enum_ShiftDir is
begin
  case to_integer(unsigned(A)) is
    when 0              => return ShiftDir_Left;
    when 1              => return ShiftDir_Right;
    when others         => 
      assert false report "Unknown state detected!" severity error;
      return ShiftDir_Left;
  end case;
end;
function bool_sel(C : boolean; T : t_enum_ShiftDir; F : t_enum_ShiftDir) return t_enum_ShiftDir is
begin
  if C then
    return T;
  else
    return F;
  end if;
end;

end package body LRShiftFlat_pkg;
Runnable example
import dfhdl.*
given options.CompilerOptions.Backend = backends.verilog

enum ShiftDir extends Encode:
  case Left, Right

/** A left-right bits shifter (flat version)
  *
  * @param width
  *   the width of the input and output bits
  */
@top class LRShiftFlat(
    val width: Int <> CONST = 8
) extends RTDesign:
  /** bits input */
  val iBits = Bits(width)       <> IN
  /** requested shift */
  val shift = UInt.until(width) <> IN
  /** direction of shift */
  val dir   = ShiftDir          <> IN
  /** bits output */
  val oBits = Bits(width)       <> OUT
  oBits := dir match
    case ShiftDir.Left  => iBits << shift
    case ShiftDir.Right => iBits >> shift

Design Class Inheritance

DFHDL leverages Scala inheritance to enable sharing functionality between design classes.

ShiftGen example

Generic left and right shifters, design class inheritance example

The DFHDL code below demonstrates how to implement both left and right generic shifters efficiently by using a common abstract class named ShiftGen. The width parameter is declared as an abstract class field (without an assigned value) inside the ShiftGen class body. By extending ShiftGen, both LeftShiftGen and RightShiftGen can utilize the IOs already declared in ShiftGen. They only need to explicitly declare the width parameter and implement the shift functionality in their respective class bodies.

 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
/** A generic abstract shifter with only IOs */
abstract class ShiftGen extends RTDesign:
  /** the width of the input and output bits */
  val width: Int <> CONST //abstract
  /** bits input */
  val iBits = Bits(width) <> IN
  /** requested shift */
  val shift = UInt.until(width) <> IN
  /** bits output */
  val oBits = Bits(width) <> OUT

/** A generic left shifter 
  *   
  * @param width
  *   the width of the input and output bits
  */
class LeftShiftGen(
    val width: Int <> CONST
) extends ShiftGen:
  oBits := iBits << shift

/** A generic right shifter 
  *   
  * @param width
  *   the width of the input and output bits
  */
class RightShiftGen(
    val width: Int <> CONST
) extends ShiftGen:
  oBits := iBits >> shift
LeftShiftGen<< shiftiBitsoBitswidthRightShiftGen>> shiftiBitsoBitswidth

Design Composition & Instantiation

DFHDL supports three mechanisms to form a design hierarchy through design instantiation and composition:

  • Direct Connection Composition - The recommended mechanism for complex design hierarchies with multiple inputs and outputs. Design instantiation and port connection can be done separately, allowing child design ports to be referenced without intermediate variables.

  • Via Connection Composition - A legacy mechanism that connects ports only within a design instantiation. This exists for compatibility with Verilog module instancing and VHDL component instancing. The DFHDL compiler automatically transforms direct connections into via connections.

  • Functional Composition - A method-call mechanism for dataflow designs, primarily used for arithmetic/logic functionality with a single output port. The DFHDL compiler automatically transforms functional composition into direct design composition.

The following sections explore these composition mechanisms using our running example of a bit shifter:

Direct Connection Composition

Direct connection composition is the recommended approach for building hierarchical designs in DFHDL. It offers several key advantages:

  1. Separate instantiation and connection - Create child design instances first, then connect their ports later
  2. Direct port references - Access child design ports without intermediate variables
  3. Flexible connectivity - Connect ports in any order and across multiple statements

Syntax

The syntax for direct composition follows standard Scala class instantiation, with port connections made via the <> operator:

Direct composition syntax
//instantiate a child design
val _childDesignName_ = _designClass_(_params_)
//port connection (repeat for each child port)
_childDesignName_._childPort_ <> _connectedValue_ 

Where:

  • _childDesignName_ - The instance name for the child design. This name is preserved by the DFHDL compiler and used in error messages and generated artifacts. See the naming section for details.

  • _designClass_ - The design class to instantiate.

  • _params_ - Parameters for the child design. Empty parentheses () are required even if no parameters are needed. Parameters can be specified:

    • As ordered values (e.g., Counter(8, Up))
    • As named parameters (e.g., Counter(width = 8, dir = Up))
    • Parameters with default values can be omitted
  • _childPort_ - The port of the child design to connect.

  • _connectedValue_ - The value to connect to the child port. Can be:

    • A constant
    • A variable
    • A port of the parent design
    • A port of another child design instance

The <> connection operator has no explicit directionality - it automatically infers producer/consumer relationships based on the connected value types and scope. See the connectivity section for details.

LRShiftDirect example

Generic left-right shifter using direct connection composition

The code below implements a generic left-right shifter named LRShiftDirect. This design provides the same functionality as LRShiftFlat, but uses composition to split left and right shift operations into separate designs. The implementation leverages the design class inheritance shown in the ShiftGen example.

Note

While this example demonstrates direct composition, a flat approach is often preferable for simpler designs. For complex designs, however, splitting functionality into sub-components promotes code reuse, simplifies verification, and follows good design practices.

 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
51
52
53
54
/** A generic abstract shifter with only IOs */
abstract class ShiftGen extends RTDesign:
  /** the width of the input and output bits */
  val width: Int <> CONST //abstract
  /** bits input */
  val iBits = Bits(width) <> IN
  /** requested shift */
  val shift = UInt.until(width) <> IN
  /** bits output */
  val oBits = Bits(width) <> OUT

/** A generic left shifter 
  *   
  * @param width
  *   the width of the input and output bits
  */
class LeftShiftGen(
    val width: Int <> CONST
) extends ShiftGen:
  oBits := iBits << shift

/** A generic right shifter 
  *   
  * @param width
  *   the width of the input and output bits
  */
class RightShiftGen(
    val width: Int <> CONST
) extends ShiftGen:
  oBits := iBits >> shift

enum ShiftDir extends Encode:
  case Left, Right

/** A left-right bits shifter, direct composition
  *
  * @param width
  *   the width of the input and output bits
  */
@top class LRShiftDirect(
    val width: Int <> CONST = 8
) extends ShiftGen:
  /** direction of shift */
  val dir   = ShiftDir <> IN
  val lshifter = LeftShiftGen(width)
  val rshifter = RightShiftGen(width)
  lshifter.iBits <> iBits
  lshifter.shift <> shift
  rshifter.iBits <> iBits
  rshifter.shift <> shift
  oBits := dir match
    case ShiftDir.Left  => lshifter.oBits
    case ShiftDir.Right => rshifter.oBits
end LRShiftDirect
LeftShiftGen<< shiftiBitsoBitswidthRightShiftGen>> shiftiBitsoBitswidthLRShiftDirectlshifterLeftShiftGenshiftiBitsoBitswidthrshifterRightShiftGenshiftiBitsoBitswidthmux dirshiftiBitsoBitswidth
 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
51
52
/* A left-right bits shifter, direct composition

   @param width
     the width of the input and output bits
  */
`default_nettype none
`timescale 1ns/1ps
`include "LRShiftDirect_defs.svh"

module LRShiftDirect#(parameter int width = 8)(
  /* bits input */
  input  wire logic [width - 1:0]         iBits,
  /* requested shift */
  input  wire logic [$clog2(width) - 1:0] shift,
  /* bits output */
  output      logic [width - 1:0]         oBits,
  /* direction of shift */
  input  wire t_enum_ShiftDir             dir
);
  `include "dfhdl_defs.svh"
  logic [width - 1:0] lshifter_iBits;
  logic [$clog2(width) - 1:0] lshifter_shift;
  logic [width - 1:0] lshifter_oBits;
  logic [width - 1:0] rshifter_iBits;
  logic [$clog2(width) - 1:0] rshifter_shift;
  logic [width - 1:0] rshifter_oBits;
  LeftShiftGen #(
    .width (width)
  ) lshifter(
    .iBits /*<--*/ (lshifter_iBits),
    .shift /*<--*/ (lshifter_shift),
    .oBits /*-->*/ (lshifter_oBits)
  );
  RightShiftGen #(
    .width (width)
  ) rshifter(
    .iBits /*<--*/ (rshifter_iBits),
    .shift /*<--*/ (rshifter_shift),
    .oBits /*-->*/ (rshifter_oBits)
  );
  assign lshifter_iBits = iBits;
  assign lshifter_shift = shift;
  assign rshifter_iBits = iBits;
  assign rshifter_shift = shift;
  always_comb
  begin
    case (dir)
      ShiftDir_Left:  oBits = lshifter_oBits;
      ShiftDir_Right: oBits = rshifter_oBits;
    endcase
  end
endmodule
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
/* A generic left shifter 

   @param width
     the width of the input and output bits
  */
`default_nettype none
`timescale 1ns/1ps
`include "LRShiftDirect_defs.svh"

module LeftShiftGen#(parameter int width)(
  /* bits input */
  input  wire logic [width - 1:0]         iBits,
  /* requested shift */
  input  wire logic [$clog2(width) - 1:0] shift,
  /* bits output */
  output      logic [width - 1:0]         oBits
);
  `include "dfhdl_defs.svh"
  assign oBits = iBits << shift;
endmodule
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
/* A generic right shifter 

   @param width
     the width of the input and output bits
  */
`default_nettype none
`timescale 1ns/1ps
`include "LRShiftDirect_defs.svh"

module RightShiftGen#(parameter int width)(
  /* bits input */
  input  wire logic [width - 1:0]         iBits,
  /* requested shift */
  input  wire logic [$clog2(width) - 1:0] shift,
  /* bits output */
  output      logic [width - 1:0]         oBits
);
  `include "dfhdl_defs.svh"
  assign oBits = iBits >> shift;
endmodule
1
2
3
4
5
6
7
8
`ifndef LRSHIFTDIRECT_DEFS
`define LRSHIFTDIRECT_DEFS
typedef enum logic [0:0] {
  ShiftDir_Left  = 0,
  ShiftDir_Right = 1
} t_enum_ShiftDir;

`endif
 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
51
52
53
54
55
56
57
58
59
60
-- A left-right bits shifter, direct composition
--
-- @param width
--   the width of the input and output bits
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use work.dfhdl_pkg.all;
use work.LRShiftDirect_pkg.all;

entity LRShiftDirect is
generic (
  width : integer := 8
);
port (
  -- bits input 
  iBits : in  std_logic_vector(width - 1 downto 0);
  -- requested shift 
  shift : in  unsigned(clog2(width) - 1 downto 0);
  -- bits output 
  oBits : out std_logic_vector(width - 1 downto 0);
  -- direction of shift 
  dir   : in  t_enum_ShiftDir
);
end LRShiftDirect;

architecture LRShiftDirect_arch of LRShiftDirect is
  signal lshifter_iBits : std_logic_vector(width - 1 downto 0);
  signal lshifter_shift : unsigned(clog2(width) - 1 downto 0);
  signal lshifter_oBits : std_logic_vector(width - 1 downto 0);
  signal rshifter_iBits : std_logic_vector(width - 1 downto 0);
  signal rshifter_shift : unsigned(clog2(width) - 1 downto 0);
  signal rshifter_oBits : std_logic_vector(width - 1 downto 0);
begin
  lshifter : entity work.LeftShiftGen(LeftShiftGen_arch) generic map (
    width        => width
  ) port map (
    iBits        => lshifter_iBits,
    shift        => lshifter_shift,
    oBits        => lshifter_oBits
  );
  rshifter : entity work.RightShiftGen(RightShiftGen_arch) generic map (
    width        => width
  ) port map (
    iBits        => rshifter_iBits,
    shift        => rshifter_shift,
    oBits        => rshifter_oBits
  );
  lshifter_iBits <= iBits;
  lshifter_shift <= shift;
  rshifter_iBits <= iBits;
  rshifter_shift <= shift;
  process (all)
  begin
    case dir is
      when ShiftDir_Left  => oBits <= lshifter_oBits;
      when ShiftDir_Right => oBits <= rshifter_oBits;
    end case;
  end process;
end LRShiftDirect_arch;
 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
-- A generic left shifter 
--   
-- @param width
--   the width of the input and output bits
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use work.dfhdl_pkg.all;
use work.LRShiftDirect_pkg.all;

entity LeftShiftGen is
generic (
  width : integer
);
port (
  -- bits input 
  iBits : in  std_logic_vector(width - 1 downto 0);
  -- requested shift 
  shift : in  unsigned(clog2(width) - 1 downto 0);
  -- bits output 
  oBits : out std_logic_vector(width - 1 downto 0)
);
end LeftShiftGen;

architecture LeftShiftGen_arch of LeftShiftGen is
begin
  oBits <= slv_sll(iBits, to_integer(shift));
end LeftShiftGen_arch;
 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
-- A generic right shifter 
--   
-- @param width
--   the width of the input and output bits
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use work.dfhdl_pkg.all;
use work.LRShiftDirect_pkg.all;

entity RightShiftGen is
generic (
  width : integer
);
port (
  -- bits input 
  iBits : in  std_logic_vector(width - 1 downto 0);
  -- requested shift 
  shift : in  unsigned(clog2(width) - 1 downto 0);
  -- bits output 
  oBits : out std_logic_vector(width - 1 downto 0)
);
end RightShiftGen;

architecture RightShiftGen_arch of RightShiftGen is
begin
  oBits <= slv_srl(iBits, to_integer(shift));
end RightShiftGen_arch;
 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
51
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use work.dfhdl_pkg.all;

package LRShiftDirect_pkg is
type t_enum_ShiftDir is (
  ShiftDir_Left, ShiftDir_Right
);
function bitWidth(A: t_enum_ShiftDir) return integer;
function to_slv(A: t_enum_ShiftDir) return std_logic_vector;
function to_t_enum_ShiftDir(A: std_logic_vector) return t_enum_ShiftDir;
function bool_sel(C : boolean; T : t_enum_ShiftDir; F : t_enum_ShiftDir) return t_enum_ShiftDir;


end package LRShiftDirect_pkg;

package body LRShiftDirect_pkg is
function bitWidth(A : t_enum_ShiftDir) return integer is
begin
  return 1;
end;
function to_slv(A : t_enum_ShiftDir) return std_logic_vector is
  variable int_val : integer;
begin
  case A is
    when ShiftDir_Left  => int_val := 0;
    when ShiftDir_Right => int_val := 1;
  end case;
  return resize(to_slv(int_val), 1);
end;
function to_t_enum_ShiftDir(A : std_logic_vector) return t_enum_ShiftDir is
begin
  case to_integer(unsigned(A)) is
    when 0              => return ShiftDir_Left;
    when 1              => return ShiftDir_Right;
    when others         => 
      assert false report "Unknown state detected!" severity error;
      return ShiftDir_Left;
  end case;
end;
function bool_sel(C : boolean; T : t_enum_ShiftDir; F : t_enum_ShiftDir) return t_enum_ShiftDir is
begin
  if C then
    return T;
  else
    return F;
  end if;
end;

end package body LRShiftDirect_pkg;
Runnable example
import dfhdl.*
given options.CompilerOptions.Backend = backends.verilog

/** A generic abstract shifter with only IOs */
abstract class ShiftGen extends RTDesign:
  /** the width of the input and output bits */
  val width: Int <> CONST //abstract
  /** bits input */
  val iBits = Bits(width) <> IN
  /** requested shift */
  val shift = UInt.until(width) <> IN
  /** bits output */
  val oBits = Bits(width) <> OUT

/** A generic left shifter 
  *   
  * @param width
  *   the width of the input and output bits
  */
class LeftShiftGen(
    val width: Int <> CONST
) extends ShiftGen:
  oBits := iBits << shift

/** A generic right shifter 
  *   
  * @param width
  *   the width of the input and output bits
  */
class RightShiftGen(
    val width: Int <> CONST
) extends ShiftGen:
  oBits := iBits >> shift

enum ShiftDir extends Encode:
  case Left, Right

/** A left-right bits shifter, direct composition
  *
  * @param width
  *   the width of the input and output bits
  */
@top class LRShiftDirect(
    val width: Int <> CONST = 8
) extends ShiftGen:
  /** direction of shift */
  val dir   = ShiftDir <> IN
  val lshifter = LeftShiftGen(width)
  val rshifter = RightShiftGen(width)
  lshifter.iBits <> iBits
  lshifter.shift <> shift
  rshifter.iBits <> iBits
  rshifter.shift <> shift
  oBits := dir match
    case ShiftDir.Left  => lshifter.oBits
    case ShiftDir.Right => rshifter.oBits
end LRShiftDirect

Via Connection Composition

Via connection composition is a legacy mechanism that connects child design ports within the child design instantiation. It exists for compatibility with Verilog module instantiation and VHDL component instantiation. The DFHDL compiler automatically transforms direct connections into via connections.

Syntax

The syntax for via composition uses Scala anonymous class instantiation, with port connections made inside the instantiation block:

Via composition syntax
//instantiate a child design
val _childDesignName_ = new _designClass_(_params_):
    //port connection (repeat for each child port)
    _childPort_ <> _connectedValue_

The new keyword and colon : syntax creates an anonymous class instance. Port connections must be made within this instantiation block, similar to Verilog module and VHDL component instantiation. This means connected values must be declared before they are used in the connection operation.

Handling port name collisions between parent and child designs

When connecting ports with the same name in parent and child designs, Scala's name shadowing rules will favor the child port name. For example, when connecting the iBits port of LeftShiftGen to the iBits port of LRShiftVia, we need a way to reference the parent's iBits port from within the child design.

To solve this, we use Scala's class self reference feature and name it parent, as shown in the LRShiftVia example below.

LRShiftVia example

Generic left-right shifter, via composition example

The DFHDL code below implements the same generic left-right shifter composition seen in the LRShiftDirect example, but uses via composition instead of direct composition. We define a parent self reference for the LRShiftVia design to refer to the LRShiftVia design within the lshifter: LeftShiftGen and rshifter: RightShiftGen child designs. We also use intermediate variables for the oBits ports of the lshifter and rshifter child designs and apply the multiplexer logic to select between them.

 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
/** A left-right bits shifter, via composition
  *
  * @param width
  *   the width of the input and output bits
  */
@top class LRShiftVia(
    val width: Int <> CONST = 8
) extends ShiftGen:
  parent => //parent design reference
  /** direction of shift */
  val dir = ShiftDir <> IN
  val lshifter_oBits = Bits(width) <> VAR
  val lshifter = new LeftShiftGen(width):
    iBits <> parent.iBits
    shift <> parent.shift
    oBits <> lshifter_oBits
  val rshifter_oBits = Bits(width) <> VAR
  val rshifter = new RightShiftGen(width):
    iBits <> parent.iBits
    shift <> parent.shift
    oBits <> rshifter_oBits
  oBits := dir match
    case ShiftDir.Left  => lshifter_oBits
    case ShiftDir.Right => rshifter_oBits
end LRShiftVia

Another interesting example is the automatic transformation of a direct composition design into a via composition design:

Direct-to-via compilation transformation example

The code below shows how the DFHDL compiler transforms the LRShiftDirect design into via composition form. For each child design port, the compiler:

  1. Creates an intermediate variable with the same type
  2. Connects the child port to this variable inside the child's instantiation block
  3. Connects the variable to the appropriate value in the parent design

 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
class LRShiftDirect(val width: Int <> CONST = 8) extends EDDesign:
  /** bits input */
  val iBits          = Bits(width)        <> IN
  /** requested shift */
  val shift          = UInt(clog2(width)) <> IN
  /** bits output */
  val oBits          = Bits(width)        <> OUT
  /** direction of shift */
  val dir            = ShiftDir           <> IN
  val lshifter_iBits = Bits(width)        <> VAR
  val lshifter_shift = UInt(clog2(width)) <> VAR
  val lshifter_oBits = Bits(width)        <> VAR
  val lshifter = new LeftShiftGen(width = width):
    this.iBits   <>/*<--*/ lshifter_iBits
    this.shift   <>/*<--*/ lshifter_shift
    this.oBits   <>/*-->*/ lshifter_oBits
  val rshifter_iBits = Bits(width)        <> VAR
  val rshifter_shift = UInt(clog2(width)) <> VAR
  val rshifter_oBits = Bits(width)        <> VAR
  val rshifter = new RightShiftGen(width = width):
    this.iBits   <>/*<--*/ rshifter_iBits
    this.shift   <>/*<--*/ rshifter_shift
    this.oBits   <>/*-->*/ rshifter_oBits
  lshifter_iBits <> iBits
  lshifter_shift <> shift
  rshifter_iBits <> iBits
  rshifter_shift <> shift
  process(all):
    dir match
      case ShiftDir.Left  => oBits := lshifter_oBits
      case ShiftDir.Right => oBits := rshifter_oBits
    end match
end LRShiftDirect
Note how the compiler adds comments (/*<--*/ and /*-->*/) to indicate the direction of data flow in the child design port connections.

The following runnable example is the same as the LRShiftDirect example, except for the default compiler options, which we altered to print the compiled design in DFHDL code format rather than the backend code format.

Runnable example
import dfhdl.*
given options.CompilerOptions.Backend = backends.verilog
//disable the default backend code print (in scastie)
given options.CompilerOptions.PrintBackendCode = false
//enable the DFHDL code print after compilation
given options.CompilerOptions.PrintDFHDLCode = true

/** A generic abstract shifter with only IOs */
abstract class ShiftGen extends RTDesign:
  /** the width of the input and output bits */
  val width: Int <> CONST //abstract
  /** bits input */
  val iBits = Bits(width) <> IN
  /** requested shift */
  val shift = UInt.until(width) <> IN
  /** bits output */
  val oBits = Bits(width) <> OUT

/** A generic left shifter 
  *   
  * @param width
  *   the width of the input and output bits
  */
class LeftShiftGen(
    val width: Int <> CONST
) extends ShiftGen:
  oBits := iBits << shift

/** A generic right shifter 
  *   
  * @param width
  *   the width of the input and output bits
  */
class RightShiftGen(
    val width: Int <> CONST
) extends ShiftGen:
  oBits := iBits >> shift

enum ShiftDir extends Encode:
  case Left, Right

/** A left-right bits shifter, direct composition
  *
  * @param width
  *   the width of the input and output bits
  */
@top class LRShiftDirect(
    val width: Int <> CONST = 8
) extends ShiftGen:
  /** direction of shift */
  val dir   = ShiftDir <> IN
  val lshifter = LeftShiftGen(width)
  val rshifter = RightShiftGen(width)
  lshifter.iBits <> iBits
  lshifter.shift <> shift
  rshifter.iBits <> iBits
  rshifter.shift <> shift
  oBits := dir match
    case ShiftDir.Left  => lshifter.oBits
    case ShiftDir.Right => rshifter.oBits
end LRShiftDirect

Functional Composition

Functional composition is a method-call based mechanism primarily used for dataflow designs with a single output. It's particularly useful for arithmetic and logic operations. The DFHDL compiler automatically transforms functional composition into direct design composition.

Syntax

The syntax for functional composition follows standard Scala method declaration and invocation to declare and invoke design definitions (aka design methods or design functions):

Functional composition syntax
//declare a design definition
def _designName_(
    _param1_: _type1_ <> VAL,
    _param2_: _type2_ <> VAL,
    ...,
    _paramN_: _typeN_ <> VAL
): _returnType_ <> DFRET = _expression_

//invoke the design definition
_designName_(_param1_, _param2_, ..., _paramN_)

Where:

  • _designName_ - The name of the design definition
  • _paramN_ - Input parameters that act as design ports
  • _typeN_ - DFHDL types for the parameters
  • _returnType_ - DFHDL type for the output port
  • <> VAL - Marks parameters as design input ports
  • <> DFRET - Marks the return type as a design output port
  • _expression_ - The design functionality

LRShiftFunc example

Generic left-right shifter using functional composition

The code below implements the same generic left-right shifter functionality seen in previous examples, but uses functional composition. The implementation is split into three function designs:

  1. LeftShiftGen - Performs left shift operation
  2. RightShiftGen - Performs right shift operation
  3. LRShiftFunc - Top-level function that selects between left/right shift

Since functions can't be top-level designs in DFHDL, we wrap LRShiftFunc in a top-level design class LRShiftFuncWrapper.

 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
/** A generic left shifter */
def LeftShiftGen(
  iBits: Bits[Int] <> VAL,
  shift: UInt[Int] <> VAL
): Bits[Int] <> DFRET = iBits << shift

/** A generic right shifter */
def RightShiftGen(
  iBits: Bits[Int] <> VAL,
  shift: UInt[Int] <> VAL
): Bits[Int] <> DFRET = iBits >> shift

enum ShiftDir extends Encode:
  case Left, Right

/** A left-right bits shifter, functional composition */
def LRShiftFunc(
  iBits: Bits[Int] <> VAL,
  shift: UInt[Int] <> VAL,
  dir: ShiftDir <> VAL
): Bits[Int] <> DFRET =
  dir match
    case ShiftDir.Left  => LeftShiftGen(iBits, shift)
    case ShiftDir.Right => RightShiftGen(iBits, shift)

/** A left-right bits shifter wrapper
  * (required as top-level design for functional composition)
  *
  * @param width
  *   the width of the input and output bits
  */
@top class LRShiftFuncWrapper(
    val width: Int <> CONST = 8
) extends RTDesign:
  /** bits input */
  val iBits = Bits(width) <> IN
  /** requested shift */
  val shift = UInt.until(width) <> IN
  /** bits output */
  val oBits = Bits(width) <> OUT
  /** direction of shift */
  val dir   = ShiftDir <> IN

  oBits <> LRShiftFunc(iBits, shift, dir)
end LRShiftFuncWrapper
LRShiftFuncWrapperLRShiftFuncLeftShiftGen<< shiftiBitsoBitsRightShiftGen>> shiftiBitsoBitsmux dirshiftiBitsoBitsdirshiftiBitsoBitswidth

Advantages of functional composition

Functional composition offers several benefits:

  1. Concise syntax - No need for explicit port declarations and connections
  2. Natural expression - Operations can be written in a more natural mathematical style
  3. Easy reuse - Functions can be composed and reused easily
  4. Type safety - Scala's type system ensures correct port connections