Expressions

Expressions in Virdant represent combinational logic. They compute values from inputs without side effects or state.

The expression language includes literals, references, arithmetic, bitwise operations, comparisons, conditionals, pattern matching, struct construction, function calls, indexing, and projection.

Grammar

The full expression grammar, from highest to lowest precedence:

Expr := If | Match | Let | ExprStruct | ExprBinOpLogical

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
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
Let          := "let" LetBindingList ArmBlock
                | "let" LetBindingList "=>" Expr
                | "let" LetBindingList If
                | "let" LetBindingList Match
LetBindingList   := LetBinding | LetBindingList "," LetBinding
LetBinding       := Ident ":" Type "=" Expr | Ident "=" Expr
ExprStruct       := "$" "{" AssignList "}"
ExprBinOpLogical := ExprBinOpLogical ( "&&" | "||" | "^^" ) ExprBinOpCompare | ExprBinOpCompare
ExprBinOpCompare := ExprBinOpCompare ( "<" | "<=" | ">" | ">=" | "==" | "!=" ) ExprBinOpAdditive | ExprBinOpAdditive
ExprBinOpAdditive:= ExprBinOpAdditive ( "+" | "-" | "&" | "|" | "^" ) ExprUnOp | ExprUnOp
ExprUnOp         := ( "-" | "~" | "!" ) ExprUnOp | "tap" Path | ExprAscription
ExprAscription   := ExprPrimary ":" Type | ExprPrimary
ExprPrimary      := Ofness "(" ArgList? ")"
                   | ExprPrimary "->" Ident
                   | ExprPrimary "[" Index "]"
                   | ExprPrimary "[" Index ".." Index "]"
                   | ExprPrimary "[" "dyn" Expr "]"
                   | "@" Ident "(" ArgList? ")"
                   | ExprAtom
ExprAtom         := Path | WordLit | BitLit | "Str" | "#" Ident | "@" Ident | "?" | "dontcare" | "(" Expr ")"
Index            := "Nat"

Precedence and Associativity

Expression operators are listed below in order of decreasing precedence, from tightest to loosest binding.

Category

Operators

Associativity

Unary

- ~ !

Right-to-left

Multiplicative / Additive / Bitwise

+ +. - -. & | ^

Left-to-right

Comparison

< <= > >= == !=

Left-to-right

Logical

&& || ^^

Left-to-right

Postfix

-> [] [..], function call

Left-to-right

Literals

Literals represent constant values directly in the expression syntax.

// Bit literals
true
false

// Word literals with explicit width
42w8
0xffw16
0b1010w4

// Word literals with inferred width (context required)
42
0xff
0b1010

// Enum tag reference
#Idle
#OP

For more details on literals, see Grammar.

Paths

A path refers to a named component, port, or instance field.

Path :=
    Ident ("." Ident)*
    | "it" ("." Ident)*
counter        // a local register or wire
ha1.sum        // port on a submodule instance
memsrv.mem.addr // port on a socket instance

The special path it refers to the enclosing declaration’s target (available inside declaration blocks).

Struct Construction

Struct values are constructed using the $ syntax.

ExprStruct :=
    "$" "{" AssignList "}"

AssignList :=
    (Assign ",")* Assign ","?
    | Expr ","?
    |

Assign :=
    Ident "=" Expr
wire color : Color
color := ${ red = 0, green = 0, blue = 0 }

Fields may be assigned in any order. All fields must be assigned.

If a struct type has many fields, you may also use a single expression as the struct value if the types are compatible (positional construction).

Field Projection

Fields of struct values are accessed with the -> operator.

ExprPrimary :=
    ExprPrimary "->" Ident
wire r : Word[8]
r := color->red

Projection is left-associative, so nested projections like outer->inner->field are parsed as (outer->inner)->field.

Indexing and Slicing

Individual bits and ranges of bits may be extracted from Word[n] values.

ExprPrimary :=
    ExprPrimary "[" Index "]"
    | ExprPrimary "[" Index ".." Index "]"
    | ExprPrimary "[" "dyn" Expr "]"
// Bit selection (constant index)
bit := data[3]

// Range selection (inclusive of both bounds)
nibble := data[7..4]

// Dynamic bit selection (index by expression, prefixed with `dyn`)
bit := data[dyn idx]

Indices are zero-based. data[0] is the least significant bit. data[7..4] extracts bits 7, 6, 5, and 4.

Dynamic Indexing

When the index is an expression rather than a constant literal, the dyn keyword prefixes the index expression.

The array must be of type Word[n] and the index of type Word[k], and the constraint n == 2^k must hold. The result type is Bit.

// n = 8, k = 3, 2^3 = 8 -- valid
out := arr[dyn idx]

// n = 1, k = 0, 2^0 = 1 -- valid
out := single_bit_arr[dyn zero_idx]

If the constraint is violated, the compiler reports a type error.

// Error: n = 8, k = 2, 2^2 = 4 != 8
out := arr[dyn wrong_width]

Union Construction

Union values are constructed using the @ syntax with the variant name.

ExprPrimary :=
    "@" Ident "(" ArgList? ")"

For a variant with no associated data, use empty parentheses:

wire u : MyUnion
u := @Foo()

For a variant with associated data:

wire u : MyUnion
u := @Bar(#Baz)

If the union type has multiple variants with the same name, the type context disambiguates which union type is intended.

The builtin Valid type also uses the @ constructor syntax:

wire v : Valid[Word[8]]
v := @Valid(0xff)

wire invalid : Valid[Word[8]]
invalid := @Invalid()

For more details, see the Types chapter on Valid[T].

If Expressions

If expressions select between two or more branches based 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
wire max : Word[8]
max := if {
    when a > b => a
    else => b
}

// Multi-branch if
wire sel : Word[8]
sel := if {
    when op == 0 => a & b
    when op == 1 => a | b
    when op == 2 => a + b
    else => a - b
}

All branches must have the same type. Each condition must be of type Bit. The if expression requires an else arm.

Arm Syntax

Each when clause may use one of five forms:

  1. Bare expression: when cond => expr

  2. Block: when cond { stmts } (blocks must contain a single expression to be valid in expression context)

  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.

Let Expressions

A let expression binds immutable names to values for use within a scope. In expression position, the body must produce a value: either with =>, or by chaining directly into an if or match whose arms produce values. Only the { ... } block form is a style error here, since a block performs rather than produces:

Let :=
    "let" LetBindingList ArmBlock
    | "let" LetBindingList "=>" Expr
    | "let" LetBindingList If
    | "let" LetBindingList Match
out := let scaled = a + a => scaled + b

// Chained form: also legal in expression position.
out := let done = a == 0w8 if {
    when done => 0w8
    else => 0w8
}

Bindings may be untyped (the type is inferred) or carry an explicit type, and several bindings may be given in a comma-separated list:

out := let lo = a, hi : Word[8] = b => hi + lo

Bindings are ordered: each binding may reference the bindings before it. Within the body, a bound name shadows any component of the same name, and 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.

For untyped bindings, the binding’s type is inferred from its first use in the body: a widthless literal RHS must be anchored by a use site that fixes its width.

The terminator-style rule is the same as for statement let`s(see :doc:`statements): => means the scope produces a value, { means the scope performs statements. In expression position, the => form is the idiomatic one.

Match Expressions

Match expressions perform pattern matching on a value.

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
wire decoded : Word[8]
decoded := match op {
    with #OP     => 1
    with #OP_IMM => 2
    with #LOAD   => 3
    else         => 0
}

The match expression evaluates the scrutinee, compares it against each pattern in order, and returns the expression from the first matching arm.

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

Arm Syntax

Each with arm may use one of five forms:

  1. Bare expression: with pat => expr

  2. Block: with pat { stmts } (blocks must contain a single expression to be valid in expression context)

  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.

For more details on patterns, see Patterns.

Function Calls

Functions (both user-defined and builtin) are called with the standard function call syntax.

ExprPrimary :=
    Ofness "(" ArgList? ")"
    | "@" Ident "(" ArgList? ")"
result := add(x, y)
zero := !any(result)

The function name may be a simple identifier or a fully-qualified name. The arguments must match the function’s parameter list in number and type.

Builtin Functions

Virdant provides several builtin functions, including type conversion and reduction operators.

mux(cond, a, b) - Multiplexer

Selects between two values based on a condition. Returns a if cond is true, otherwise returns b.

// Basic usage
out := mux(enable, data, 0)

// Equivalent to:
out := if {
    when enable => data
    else => 0
}

The mux function is desugared to an if expression during compilation. All three arguments must be provided:

  • cond must be of type Bit

  • a and b must have the same type

  • The result type matches the type of a and b

asclock(x) - Convert a Bit to a Clock

Reinterprets a Bit signal as a Clock. The subject x must be of type Bit, and the result is of type Clock.

wire bit_sig : Bit
wire clk : Clock
clk := asclock(bit_sig)

Since Clock is opaque — clocks may only be passed through ports or used in a register’s on clause — asclock is the only way to obtain a Clock from an ordinary signal. Its main use is clocking a register on a Bit signal, because the expression after on must be of type Clock:

mod ClockFromBit {
    incoming clk_bit : Bit

    wire clk : Clock
    clk := asclock(clk_bit)

    reg counter : Word[8] on clk {
        it <= it + 1w8
    }

    outgoing count : Word[8] {
        it := counter
    }
}

It can also be applied inline in the on clause itself:

reg r : Bit on asclock(clk_bit) {
    it <= !it
}

The remaining builtin functions are:

  • any(w) / all(w) — unary reductions over a Word, yielding a Bit that is set if any (respectively all) bits are set.

  • sext(w) / zext(w) / trunc(w) — change the width of a Word, sign-extending, zero-extending, or truncating it; the target width is checked against the expected type.

  • cast(x) — reinterpret the bit pattern of a Word[n] as an enum type, or vice versa.

Unary Operators

Operator

Description

- expr

Arithmetic negation (two’s complement)

~ expr

Bitwise NOT (invert all bits)

! expr

Logical NOT (for Bit values)

tap path

Read a tristate bus or net, asserting it is driven

Unary operators have the highest precedence and are right-associative.

wire neg : Word[8]
neg := -value

wire inverted : Word[8]
inverted := ~value

wire not_bit : Bit
not_bit := !ready

The tap Operator

tap reads a tristate component (bus or net, see Modules). Because a tristate net may be floating, a plain reference to one is a compile error (NetReadWithoutTap); tap asserts that the net is driven at the point of the read and evaluates to the net’s value:

net sda : Bit
sda #- true

seen := tap sda

Applying tap to anything other than a bus or net is an error (TapNotOnNet). Like the other unary operators, tap has the highest precedence and is right-associative.

Binary Operators

Arithmetic Operators

Operator

Description

a + b

Addition (modular, wraps on overflow)

a - b

Subtraction (modular, wraps on underflow)

a +. b

Addition with carry: like a + b, but the result is Word[n+1] and its most-significant bit is the carry-out

a -. b

Subtraction with borrow: like a - b, but the result is Word[n+1] and its most-significant bit is the borrow-out

Operands must be of type Word[n] with the same n. The + and - operators wrap their results to n bits; +. and -. instead widen the result to n+1 bits, so no information is lost:

// 0xf + 0x1 wraps to 0x0 ...
wire wrapped : Word[4] {
    it := 0xfw4 + 0x1w4
}

// ... while `+.` keeps the carry out in the top bit.
wire carried : Word[5] {
    it := 0xfw4 +. 0x1w4   // 0x10, carry bit set
}

Bitwise Operators

Operator

Description

a & b

Bitwise AND

a | b

Bitwise OR

a ^ b

Bitwise XOR

Operands must be of type Word[n] with the same n.

Comparison Operators

Operator

Description

a < b

Less than

a <= b

Less than or equal

a > b

Greater than

a >= b

Greater than or equal

a == b

Equal

a != b

Not equal

Comparison operators return Bit. Operands must have the same type.

Logical Operators

Operator

Description

a && b

Logical AND (for Bit values)

a || b

Logical OR (for Bit values)

a ^^ b

Logical XOR (for Bit values)

Logical operators require Bit operands and return Bit.

Type Ascriptions

An expression may be annotated with an explicit type using the : operator.

ExprAscription :=
    ExprPrimary ":" Type
value : Word[16]

Type ascriptions are useful for disambiguating overloaded expressions or providing type hints to the compiler.

Dontcare

The dontcare expression represents an unknown or unused value.

wire x : Word[8]
x := dontcare

The ? Expression

The ? token may be used in certain contexts where the compiler can infer the intended value.

// Used to ask the compiler to fill in a value
wire x : Word[8]
x := ?