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 :=
    ExprWhen | ExprMatch | ExprStruct | ExprBinOpLogical

ExprWhen         := "when" "{" ExprWhenArm* "}"
ExprWhenArm      := "case" Expr "=>" Expr
                    | "case" Expr ModDefStmtBlock
                    | "case" Expr ExprWhen
                    | "case" Expr ExprMatch
                    | "else" "=>" Expr
                    | "else" ModDefStmtBlock
                    | "else" ExprWhen
                    | "else" ExprMatch
ExprMatch        := "match" Expr "{" ExprMatchArm* "}"
ExprMatchArm     := "case" Pat "=>" Expr
                    | "case" Pat ModDefStmtBlock
                    | "case" Pat ExprWhen
                    | "case" Pat ExprMatch
                    | "else" "=>" Expr
                    | "else" ModDefStmtBlock
                    | "else" ExprWhen
                    | "else" ExprMatch
ExprStruct       := "$" "{" AssignList "}"
ExprBinOpLogical := ExprBinOpLogical ( "&&" | "||" | "^^" ) ExprBinOpCompare | ExprBinOpCompare
ExprBinOpCompare := ExprBinOpCompare ( "<" | "<=" | ">" | ">=" | "==" | "!=" ) ExprBinOpAdditive | ExprBinOpAdditive
ExprBinOpAdditive:= ExprBinOpAdditive ( "+" | "-" | "&" | "|" | "^" ) ExprUnOp | ExprUnOp
ExprUnOp         := ( "-" | "~" | "!" ) ExprUnOp | 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 ")"

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
memory.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, red = 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].

When Expressions

When expressions select between two or more branches based on conditions.

ExprWhen :=
    "when" "{" ExprWhenArm* "}"

ExprWhenArm :=
    "case" Expr "=>" Expr
    | "case" Expr ModDefStmtBlock
    | "case" Expr ExprWhen
    | "case" Expr ExprMatch
    | "else" "=>" Expr
    | "else" ModDefStmtBlock
    | "else" ExprWhen
    | "else" ExprMatch
wire max : Word[8]
max := when {
    case a > b => a
    else => b
}

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

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

Arm Syntax

Each arm may use one of four forms:

  1. Bare expression: case cond => expr

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

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

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

The same forms apply to the else arm.

Match Expressions

Match expressions perform pattern matching on a value.

ExprMatch :=
    "match" Expr "{" ExprMatchArm* "}"

ExprMatchArm :=
    "case" Pat "=>" Expr
    | "case" Pat ModDefStmtBlock
    | "case" Pat ExprWhen
    | "case" Pat ExprMatch
    | "else" "=>" Expr
    | "else" ModDefStmtBlock
    | "else" ExprWhen
    | "else" ExprMatch
wire decoded : Word[8]
decoded := match op {
    case #OP     => 1
    case #OP_IMM => 2
    case #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 arm may use one of four forms:

  1. Bare expression: case pat => expr

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

  3. Nested when: case pat when { ... }

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

The same forms apply to the else arm.

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 := when {
    case enable => data
    else => 0
}

The mux function is desugared to a when 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

For other builtin functions such as any, all, sext, zext, and trunc, see the language reference or examples.

Unary Operators

Operator

Description

- expr

Arithmetic negation (two’s complement)

~ expr

Bitwise NOT (invert all bits)

! expr

Logical NOT (for Bit values)

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

Binary Operators

Arithmetic Operators

Operator

Description

a + b

Addition (modular, wraps on overflow)

a - b

Subtraction (modular, wraps on underflow)

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

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 := ?