Virdant is a strongly-typed hardware description language.
A package is the unit of compilation for Virdant.
It consists of a single .vir
file.
A package consists of a number of top-level declarations called items. These include:
- module definitions
- type definitions
pub mod Foo {
incoming clk : Clock;
incoming in : Word[8];
outgoing out : Word[8];
reg buffer : Word[8] on clk;
buffer <= in->add(1);
out := buffer;
}
A module definition consists of a list of statements. The order of statements is not significant.
The following statements declare a component. A component represents a simple piece of hardware that exists in a module.
incoming
outgoing
node
reg
mod
The incoming
and outgoing
components represent ports in towards and out from the module respectively.
The node
component represents a named value, used for clarity or to create an alias to a complex expression.
The reg
component represents a stateful value.
The mod
component represents a submodule instance.
Of these, incoming
outgoing
node
and reg
are called simple components.
Each simple component has a type.
At simulation time, each one will have a signal associated with it.
A mod
component is where one module is nested inside of another.
A submodule is declared with a name and a module definition.
In addition to declarations, there are wire statements, which supply a value to a simple component.
These are written as TARGET := EXPR
or TARGET <= EXPR
.
The left hand side of a wire statement is the target.
Inside of a module definition, each outgoing
, node
, and reg
component introduces a local target,
and each incoming
component of a submodule, as defined by its module definition, introduces a non-local target.
The right hand side of a wire statement is an expression. This expression will determine the values the target receives at runtime.
There are two kinds of wires:
:=
(continuous)<=
(latched)
Continuous wires are used to connect to incoming
outgoing
and node
components.
They continuously supply a value to the component.
This means that during simulation, the value of the target changes as soon as the value of the expression changes.
Latched wires are used to connect to reg
components.
They supply a new value to the component on each tick of that register's associated clock.
You can think of them as being a wire to the register's data pin ("D" pin) in the underlying hardware.
This is in contrast to when a reg
is used as an expression, which evaluates to the the register's output pin ("Q" pin).
Continuous connects, written :=
, are always in effect.
This is used with incoming
, outgoing
, and wire
components.
Latched connects, written <=
, are only used for reg
components, and take effect every clock cycle.
A module definition must supply exactly one connect statement for each target.
For any natural number n
, Word[n]
is an n
-bit integer.
It is nominally unsigned.
For any type T
and natural number n
, Vec[T, n]
is a vector of elements of type T
with length n
.
Clock
is the type of clock signals.
Any expression may reference any incoming
, wire
, reg
of the defining module or any outgoing
of a submodule.
For clarity, when referencing a reg
, you read the current value (the value that was latched on the previous cycle)
rather than the value which is about to be latched.
Submodules are not expressions, and so they do not have a type.
Constant values, such as 0
, 1
, 2
, etc. may be used as expressions.
Their bitwidth will be inferred whenever possible.
To give their bitwidth explicitly, use the notation 0w8
(read "0 with width 8"), etc.
You may also specify integers using binary or hexadecimal: 0b1011w4
, 0xffw8
, etc.
You may also use underscores to break up numbers however you like: 0b000_11
, etc.
Vectors may be constructed with the syntax [0, 1, 2]
.
For expressions which are well-typed, but whose type can't be inferred, you can use a type ascription: x->as(Word[8])
.
Types may have methods defined on them. These vary according to the type.
a->add(b)
- Addb
toa
. Both must have the same bitwidth.a->sub(b)
- Subtractb
froma
. Both must have the same bitwidth.a->neg()
- Negation ofa
. Result is the same type asa
.
a->eq(b)
- Comparea
tob
. Both must have the same bitwidth. Result is aWord[1]
.a->lt(b)
- Less than. Comparea
tob
. Both must have the same bitwidth. Result is aWord[1]
.a->lte(b)
- Less than or equal. Comparea
tob
. Both must have the same bitwidth. Result is aWord[1]
.a->gt(b)
- Greater than. Comparea
tob
. Both must have the same bitwidth. Result is aWord[1]
.a->gte(b)
- Greater than or equal. Comparea
tob
. Both must have the same bitwidth. Result is aWord[1]
.
a->and(b)
- Logical ANDa
andb
. Both must have the same bitwidth. Result is the same bitwidth.a->or(b)
- Logical ORa
andb
. Both must have the same bitwidth. Result is the same bitwidth.a->not()
- Logical NOTa
. Result is the same bitwidth.
a->sll(b)
- Shift left logical. The valuesa
andb
may have different bitwidths. Result is the same type asa
.a->srl(b)
- Shift left logical. The valuesa
andb
may have different bitwidths. Result is the same type asa
.
a->get(i)
- Indexes intoa
fetching the bit in positioni
. Wheni
is 0, this is the least significant bit. The bitwidth ofa
must be a power of 2 and the bitwidth ofi
must be the same as that power.
You concatenate words together using cat(x, y)
.
The results of each operand must be inferrable.
You index into words with x[0]
, x[1]
, x[2]
, etc.
The index must be a constant literal.
x[0]
is the least significant bit.
You slice into words with x[1..0]
, x[2..0]
, x[2..1]
, etc.
The indexes must be a constant literals.
For words, the higher index goes on the left.
The upper index is non-inclusive.
For example, if x : Word[8]
, then x
is the same as x[8..0]
.