Modules

Modules are the primary building blocks of Virdant designs. A module defines a hardware block with ports, internal components, and behavioral statements.

Module Body

The body of a module definition is a sequence of statements that declare ports, components, instances, and control flow.

ModDefStmt :=
    ModDefStmtComponent
    | ModDefStmtDriver
    | ModDefStmtInstance
    | ModDefStmtSocket
    | ModDefStmtMatch
    | ModDefStmtUnused
    | ModDefStmtDependsOn

The order of statements within a module body does not matter. Virdant processes the module body as a set of declarations, not as a sequence of imperative statements.

Ports

Ports are the interface between a module and the outside world. They are declared with a direction, a name, and a type.

ModDefStmtComponent :=
    DocString Annotations "incoming" Ident ":" Type OnClause? ItBlock?
    | DocString Annotations "outgoing" Ident ":" Type OnClause? ItBlock?
    | DocString Annotations "outgoing" "wire" Ident ":" Type OnClause? ItBlock?
    | DocString Annotations "outgoing" "reg" Ident ":" Type OnClause? ItBlock?
    | DocString Annotations "wire" Ident ":" Type OnClause? ItBlock?
    | DocString Annotations "reg" Ident ":" Type OnClause? ItBlock?
mod Passthrough {
    incoming inp : Word[8]
    outgoing out : Word[8]

    out := inp
}

incoming ports receive values from outside the module. outgoing ports send values from the module to the outside.

Wires

A wire declaration creates an internal combinational signal.

wire sum : Word[8]
sum := a + b

A wire may optionally have an on clause and a block, making it a clocked wire. Wires may also be given an initial value through an it block.

wire w : Word[8] {
    w := a + b
}

Registers

A reg declaration creates a sequential (stateful) element. Every register must have a clock associated with it via the on keyword.

reg counter : Word[8] on clock
counter <= counter + 1

Registers update their value on the rising edge of the associated clock. The value assigned to a register uses the <= assignment operator, indicating a non-blocking (clocked) assignment.

A register may have an associated block for additional statements:

reg counter : Word[8] on clock {
    it <= when {
        case reset => 0
        else => it + 1
    }
}

Inside the register’s block, the special identifier it refers to the register itself.

Assignments

Virdant provides three assignment operators for driving signals.

:= (combinational assignment)

Drives a combinational signal. The target is continuously driven by the expression.

out := a + b          // combinational: out always equals a + b
<= (sequential assignment)

Drives a register. The target updates on the next clock edge.

reg r : Word[8] on clock
r <= a + b            // sequential: r updates on next clock edge
:=: (bidirectional connection)

Connects two signals bidirectionally. Both sides drive each other.

memory.mem :=: core.mem

Module Instances

Module instances allow you to compose larger designs from smaller modules.

ModDefStmtInstance :=
    DocString Annotations "mod" Ident "of" Ofness ItBlock?
mod ha1 of HalfAdder
ha1.a := a
ha1.b := b
sum := ha1.sum
carry := ha1.carry

The mod keyword introduces an instance, followed by a name for the instance, the of keyword, and the type (module name) being instantiated.

Ports on the instance are accessed using dot notation: <instance>.<port>.

An instance may optionally have an it block for grouping connections:

mod ha1 of HalfAdder {
    it.a := a
    it.b := b
}
sum := ha1.sum
carry := ha1.carry

Socket Instances

Sockets provide a way to group related ports and connect them as a unit.

ModDefStmtSocket :=
    DocString Annotations "client" "socket" Ident "of" Ofness ItBlock?
    | DocString Annotations "server" "socket" Ident "of" Ofness ItBlock?
client socket mem of Mem {
    it.addr := 0
    unused it.data
}

A socket instance must specify its role: either client or server. The role determines the direction of the individual ports within the socket.

For more details on sockets, see Sockets.

Match Statements

Match statements enable pattern-based conditional logic in module bodies.

ModDefStmtMatch :=
    "match" Expr "{" ModDefStmtMatchArm* "}"

ModDefStmtMatchArm :=
    "case" Pat "=>" ModDefStmtBlock
    | "else" "=>" ModDefStmtBlock
match state {
    case #Idle => {
        r <= 0
        out := ready
    }
    case #Active => {
        r <= r + 1
        out := busy
    }
    else => {
        r <= r
        out := idle
    }
}

The else arm catches any value not matched by the preceding case arms. If no else arm is present, the match must be exhaustive.

Unused Declarations

The unused keyword indicates that a signal is intentionally not used. This suppresses compiler warnings about unused signals.

ModDefStmtUnused :=
    "unused" Path
unused mem.data

unused takes a path that may refer to a component, a port, or a field of an instance.

The it Identifier

Inside certain blocks (such as register blocks and instance blocks), the special identifier it refers to the enclosing declaration.

For a register it refers to the register itself. For an instance it refers to the instance.

reg counter : Word[8] on clock {
    it <= when {
        case reset => 0
        else => it + 1
    }
}

mod fifo of Fifo {
    it.clock := clock
    it.data := data
}

The it identifier is only available within the block of a declaration that provides it.

On Clause

The on clause associates a register with a clock signal.

reg r : Word[8] on clock

Every register must have exactly one on clause. The clock signal referenced in the on clause must be of type Clock.

The register latches its input value on the rising edge of the named clock. All registers driven by the same clock form a single clock domain.

Driver Blocks

Driver blocks are groups additional module body statements under a declaration. The keyword it inside the block refers to the enclosing declaration.

reg r : Word[8] on clock {
    it <= 0
    out := it
}

Blocks are optional. When present, they provide a scoped grouping for related assignments and connections.

dependson Statements

The dependson statement declares a combinational dependency between two signals that the compiler cannot infer on its own.

ModDefStmtDependsOn :=
    Path "dependson" Path

The first path is the dependent signal and the second is the dependee: changes to the dependee combinationally affect the dependent. Virdant adds a combinational edge from the dependent to the dependee in the module’s dependency graph.

ext mod PLL {
    incoming clk_in  : Clock
    outgoing clk_out : Clock {
        it dependson clk_in
    }
}

In this example the PLL is an ext mod: its body is opaque to Virdant, so the compiler cannot see that clk_out is derived from clk_in. The dependson statement tells Virdant about that dependency.

A standalone dependson (outside an it block) is also allowed:

ext mod ExternalMux {
    incoming sel    : Bit
    incoming a      : Bit
    incoming b      : Bit
    outgoing result : Bit

    result dependson sel
    result dependson a
    result dependson b
}

For internal modules, dependson documents a dependency chain without requiring the full driver expression:

mod Internal {
    incoming clk : Clock
    incoming inp : Word[8] on clk

    reg stored : Word[8] on clk
    stored <= inp

    outgoing out_val : Word[8] on clk {
        it dependson stored
        it := stored
    }
}

Use dependson sparingly. It only adds edges to the dependency graph; it does not create a driver. The dependent signal still needs a real driver (:=, <=, or :=:).

Combinational Cycle Detection

Virdant builds a dependency graph for every module and checks it for combinational cycles. Each := and :=: driver adds combinational edges from its target to every signal read on the right-hand side. Each dependson statement adds a combinational edge from its dependent to its dependee.

The checker stitches the module’s own graph together with the already built graphs of its immediate submodule instances, prefixing each submodule path with the instance name. A cycle is reported at the module that closes it — the lowest module whose own local edge completes a loop — so each loop is reported exactly once.

mod Loop {
    wire a : Word[8] on clk
    wire b : Word[8] on clk
    wire c : Word[8] on clk

    a := 1
    b := 2
    c := 3

    a dependson b
    b dependson c
    c dependson a
}

The three dependson edges form a cycle a -> b -> c -> a. Although each wire also has a literal driver (a self-loop, which is skipped), the dependson edges alone close a combinational loop and the compiler reports an error.

Cycles that cross module boundaries are detected the same way:

mod Loop2 {
    mod loop_a of LoopA
    mod loop_b of LoopB

    loop_a.inp := loop_b.out
    loop_b.inp := loop_a.out
}

Here loop_a.inp depends on loop_b.out, which depends on loop_b.w, which depends on loop_b.inp, which depends on loop_a.out — a stitched combinational loop reported at Loop2.