Statements

Statements are the declarations that appear inside a module body. Unlike expressions, which compute values, statements declare the structure and behavior of a module.

The full set of module body statements includes:

  • Component declarations (ports, wires, registers)

  • Memory declarations (memory)

  • Driver statements (assignments, including indexed memory writes)

  • Instance declarations

  • Socket instance declarations

  • Conditional statements (if)

  • Match statements

  • Let bindings

  • Unused declarations

  • Initial declarations

Statements are order-independent within a module body. Virdant processes them as a set of declarations, not as a sequence of imperative steps.

Component Declarations

Component declarations introduce named signals into a module.

ModDefStmtComponent :=
    DocString? Annotations ("incoming" | "outgoing" | "bus" | "net"
                            | "outgoing" "wire" | "outgoing" "reg"
                            | "wire" | "reg") Ident ":" Type
                           DependsonList? DefaultClause? OnClause? ItBlock?

DefaultClause := "default" Expr
mod Example {
    // Port declarations
    incoming clock : Clock
    incoming inp   : Word[8]
    outgoing out   : Word[8]

    // Internal wire
    wire sum : Word[8]

    // Register
    reg counter : Word[8] on clock

    // Outgoing register port
    outgoing reg status : Word[8] on clock
}

For a detailed description of each component type, see Modules. Tristate components (bus and net) are described in Modules.

Driver Statements

Driver statements assign values to components.

ModDefStmtDriver :=
    Path ":=" Expr
    | Path "<=" Expr
    | Path "#=" Expr
    | Path "#-" Expr
    | Path "#=#" Path
    | Path ":=:" Path

ModDefStmtIndexedDriver :=
    Path "[" "dyn" Expr "]" "<=" Expr
    | Path "[" "dyn" Expr "]" ":=" Expr
    | Path "[" Nat "]" "<=" Expr
    | Path "[" Nat "]" ":=" Expr

Combinational assignment :=

The := operator drives a combinational signal continuously.

out := a + b
sum := ha1.sum

The target of := must be a combinational signal (wire, port, or combinational output of a submodule).

Sequential assignment <=

The <= operator drives a register. The value is latched on the next rising edge of the register’s clock.

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

The target of <= must be a reg declaration.

Indexed memory writes

The <= operator also writes one word of a memory component — the only indexed left-hand side in the language. The index is dynamic (dyn) or a constant, and the write commits on the rising edge of the memory’s clock:

memory mem[16] : Word[8]
if {
    when write_enable {
        mem[dyn addr] <= write_data
    }
}

A dynamic index must be exactly Word[k] where the depth is 2^k; a constant index must be in range. Inside a memory body the write site may use it (it[dyn addr] <= write_data). A memory has exactly one write site, and sibling when clauses with non-exclusive guards may not both write it. := is accepted in place of <= with a warning: a memory write always commits on the clock edge.

Bidirectional connection :=:

The :=: operator connects two signals bidirectionally.

memsrv.mem :=: core.mem

Both sides of :=: must be socket instances of compatible socket types. The operator connects every port in the socket, respecting the direction of each port relative to each instance.

Tristate Drivers

Three driver operators work on tristate components (bus and net, see Modules). Unlike := and <=, they carry a strength, and a tristate component may have any number of drivers.

Tristate drive #=

The #= operator drives a bus or net at full strength. If the driver is guarded by a when or with arm that does not fire, the component is undriven in that case and floats (high impedance):

bus sda : Bit {
    if {
        when pull {
            it #= false      // drive low strongly
        }
        // no `else`: when not pulling, the line floats
    }
}

Weak drive #-

The #- operator drives a bus or net weakly, like a pull resistor. Any strong driver (#=) overrides a weak one on the same net. In generated Verilog, a weak drive is emitted as assign (weak1, weak0) ...:

net sda : Bit
sda #- true          // weak pull-up: idle-high

Net attachment #=#

The #=# operator attaches a submodule’s bus port to a bus or net in the current module, joining them into one electrically shared line. Both operands must be paths to tristate components:

net sda : Bit
mod master of Participant
master.sda #=# sda

When several drivers share a net, a strong driver wins over weak ones; if no driver is active, the net floats. An if or match guarding only tristate drivers does not need to be exhaustive: an undriven arm leaves the net floating rather than holding its previous value.

Instance Declarations

Instance declarations instantiate a submodule inside the current module.

ModDefStmtInstance :=
    DocString? Annotations "mod" Ident "of" Ofness GenericsParams? ItBlock?

The instance name must be unique within the enclosing module. Ports on the instance are accessed via dot notation.

mod ha1 of HalfAdder
ha1.a := a
ha1.b := b

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

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

The type (after of) must be a module definition visible in the current scope, either defined in the same package or imported from another package.

Socket Instance Declarations

Socket instances declare either a client or server role for a socket type.

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

For details on sockets, see Sockets.

Conditional Statements

Conditional statements allow a module body to contain different sets of statements depending on conditions.

If := "if" "{" IfArm* "}"

IfArm :=
    "when" Expr "=>" Expr
    | "when" Expr ArmBlock
    | "when" Expr If
    | "when" Expr Match
    | "when" Expr Let
    | "else" "=>" Expr
    | "else" ArmBlock
    | "else" If
    | "else" Match
    | "else" Let

ArmBlock :=
    "{" ModDefStmt* "}"
    | "{" ArmBlockExpr "}"
if {
    when reset {
        r <= 0
        state := #Idle
    }
    when enable {
        r <= r + 1
        state := #Active
    }
    else {
        state := #Idle
    }
}

Each condition must be of type Bit. The else arm is optional in statement context, though it is recommended for completeness.

Arm Syntax

Each when clause may use one of five forms:

  1. Bare expression: when cond => expr (rarely used in statement context)

  2. Block: when cond { stmts }

  3. Nested if: when cond if { ... }

  4. Nested match: when cond match expr { ... }

  5. Let binding: when cond let x = expr { ... }

The same forms apply to the else arm. A let arm body follows the same terminator rules as any other let.

Conditional statements are different from if expressions: statements can contain declarations, assignments, instances, and other module body constructs, while if expressions only compute values.

Match Statements

Match statements perform pattern-based dispatch in the module body.

Match := "match" Expr "{" MatchArm* "}"

MatchArm :=
    "with" Pat "=>" Expr
    | "with" Pat ArmBlock
    | "with" Pat If
    | "with" Pat Match
    | "with" Pat Let
    | "else" "=>" Expr
    | "else" ArmBlock
    | "else" If
    | "else" Match
    | "else" Let

ArmBlock :=
    "{" ModDefStmt* "}"
    | "{" ArmBlockExpr "}"
enum type StateE {
    Idle
    Run
    Halt
}

reg state : StateE on clock
reg r : Word[8] on clock
outgoing out : Word[8]

match state {
    with #Idle {
        r <= 0w8
        out := 0w8
        state <= #Run
    }
    with #Run {
        r <= r + 1w8
        out := r
    }
    else {
        out := 0w8
    }
}

Arm Syntax

Each with arm may use one of five forms:

  1. Bare expression: with pat => expr (rarely used in statement context)

  2. Block: with pat { stmts }

  3. Nested if: with pat if { ... }

  4. Nested match: with pat match expr { ... }

  5. Let binding: with pat let x = expr { ... }

The same forms apply to the else arm. A let arm body follows the same terminator rules as any other let.

The else arm is optional but recommended for catch-all behavior. If no else arm is present and the match is not exhaustive, the compiler will report an error.

For more details on patterns, see Patterns.

Let Bindings

A let statement binds names to values for use within a scope. Bindings may be untyped (the type is inferred) or carry an explicit type, and several bindings may be given in a comma-separated list:

Let :=
    "let" LetBindingList ArmBlock
    | "let" LetBindingList "=>" Expr
    | "let" LetBindingList If
    | "let" LetBindingList Match

LetBindingList :=
    LetBinding
    | LetBindingList "," LetBinding

LetBinding :=
    Ident ":" Type "=" Expr
    | Ident "=" Expr
let x = a {
    out := x + x
}

let y : Word[8] = a + b {
    out := y
}

let lo = a, hi = b {
    out := hi + lo
}

Bindings are ordered: each binding may reference the bindings before it, and a later binding shadows an earlier one of the same name.

let lo = a, hi = b + lo {
    out := hi
}

Within its body, a bound name shadows any component of the same name. The innermost binding wins: a match arm’s pattern variables shadow let bindings from outside the match, while a let written inside the arm shadows the arm’s pattern variables. Nothing outside a let’s body can see its bindings.

For untyped bindings, the binding’s type is inferred from its first use in the body. A widthless literal RHS must therefore be anchored by a use site that fixes its width: let expire = it == 0w11 infers Word[11] from the comparison, while an unanchored let x = 0 has no width to infer from and reports CantInfer.

A let may be chained directly into an if or match, in which case the bindings are in scope throughout the chained arms:

let x = a match op {
    with #Add {
        out := x + b
    }
    else {
        out := x
    }
}

A let may also appear as the body of a when or with arm (when cond let x = expr { ... }).

Terminator Style

A let body may be either a block ({ ... }) or a value (=> expr). The two spellings carry intent, and the compiler enforces the pairing:

  • { means this scope performs: it runs driver statements.

  • => means this scope produces: it evaluates to a value.

A let used as a statement must perform: its body is a block, or a chained if/match whose arms perform. A let used as an expression must produce: its body is a => value, or a chained if/match whose arms produce. A statement let whose block contains nothing but a single bare expression is also flagged, since the scope produces a value that is then discarded:

// Error: a statement `let` must use `{ ... }` instead of `=>`
let x = a => x

// OK
let x = a {
    out := x
}

The chained forms (let ... if { ... } and let ... match { ... }) are legal in both statement and expression position and are never flagged by the style check.

The Unused Statement

The unused statement declares that a signal is intentionally not used. This suppresses compiler warnings about unused components or ports.

ModDefStmtUnused :=
    "unused" Path
mod Core {
    client socket mem of Mem
    unused mem.data           // data output is intentionally unused
    mem.addr := 0
}

The path must refer to an existing signal, port, or instance field.

The Initial Statement

The initial statement declares the value a register holds when the design starts up (for example, after an FPGA is configured).

ModDefStmtInitial :=
    "initial" Path "<=" Expr
mod Top {
    incoming clock : Clock

    reg counter : Word[3] on clock {
        initial it <= 7

        if {
            when it > 0 {
                it <= it - 1
            }
        }
    }
}

The target must be a reg or outgoing reg, and the value should be a constant expression of the register’s type. A register without an initial statement starts at an unspecified value. The Virdant simulator chooses a pseudorandom bit pattern for it, so do not rely on the power-on value of a register without an initial statement.

An initial statement is not a driver: a register with an initial statement still needs its usual <= driver. Each register may have at most one initial statement, and initial may not appear inside an if or match block.

on Clause

The on clause associates a register with a clock signal.

OnClause :=
    "on" Expr
reg r : Word[8] on clock

The expression after on must evaluate to a value of type Clock.

Every reg declaration must have an on clause, unless the module declares exactly one component of type Clock. In that case the on clause is optional on every reg declaration, and an omitted on clause is equivalent to writing on with that component:

mod Counter {
    incoming clock : Clock

    // No `on` clause needed: `clock` is the module's only
    // Clock-typed component.
    reg count : Word[8]
    count <= count + 1
}

If the module declares zero or more than one component of type Clock, every reg declaration needs an explicit on clause. When the module does declare exactly one component of type Clock, an explicit on clause that names that clock is redundant: it is equivalent to omitting it, and the compiler emits a warning encouraging the implicit form. An on clause that references any other clock (for example, a submodule’s clock output) is not redundant and never warns.

it Block

An it block groups statements under a declaration. Inside the block, the keyword it refers to the enclosing declaration.

ItBlock :=
    ModDefStmtBlock

ModDefStmtBlock :=
    "{" ModDefStmt* "}"
reg r : Word[8] on clock {
    it <= inp
    out := it
}

The it block is syntactic sugar that avoids repeating the declaration name. It is purely a matter of style — the above is equivalent to:

reg r : Word[8] on clock
r <= inp
out := r