Language Constructs

Packages

Packages group related top-level declarations under a common namespace.

package VGA {
    const SCREEN_WIDTH = 640
    const SCREEN_HEIGHT = 480

    module SyncPulses(
        clk: clock,
    ) -> (
        hsync: bool,
        vsync: bool,
        row: uint[clog2!(SCREEN_HEIGHT)],
        col: uint[clog2!(SCREEN_WIDTH)],
    ) {
        // ...
       hsync = 1'b0
       vsync = 1'b0
       row = 9'd0
       col = 10'd0
    }
}

module Top(
    clk: clock,
) -> (
    hsync: bool,
    vsync: bool,
    vga_color: (r: u8, g: u8, b: u8),
) {
    let pulses = VGA::SyncPulses(clk, hsync, vsync)
    vga_color = (
        r: pulses.row[7:0],
        g: pulses.col[7:0],
        b: 8'd127,
    )
}
  • Members declared within a package are accessed using the :: operator.

  • Imported files are implicitly wrapped in a package with the same name as the file.

Constant Declarations

Constants are named fixed values known at compile-time.

const BAUD_RATE = 115200
const CLOCK_FREQ = 100 * 1_000_000 // 100 MHz
const CYCLES_PER_BIT = CLOCK_FREQ / BAUD_RATE
 module Top() -> () {}

Type Alias Declarations

Type aliases are used to give a name to a commonly used type.

type U8 = u8
type RGB = (r: U8, g: U8, b: U8)
 module Top() -> () {}

Parameterised type aliases

Type aliases can be parameterised by providing a list of type parameters.

type Triplet[T] = (T, T, T)

module Top() -> () {
    let triplet: Triplet[bool] = (true, false, false)
}

Module Declarations

  • Modules enable hierarchical design by encapsulating reusable functionality.

  • A module is defined by a name, an optional list of parameters, and a list of input and output ports.

module FullAdder(
    a: bool,
    b: bool,
    carry_in: bool,
) -> (
    sum: bool,
    carry_out: bool,
) {
    let xor1 = a xor b
    sum = carry_in xor xor1
    carry_out = (carry_in and xor1) or (a and b)
}

 module Top() -> () {}

Parameterised Modules

Modules can be parameterised by specifying a list of parameters surrounded by [ and ] after the module name.

module Adder[N: Nat](
    a: uint[N],
    b: uint[N],
    carry_in: bool,
) -> (
    sum: uint[N],
    carry_out: bool,
) {
    let carry_chain: [N + 1]bool
    carry_chain[0] = carry_in
    let bits: [N]bool

    for i in 0..<N {
        FullAdder(
            a: a[i],
            b: b[i],
            carry_in: carry_chain[i],
            carry_out: carry_chain[i + 1],
            sum: bits[i],
        )
    }

    carry_out = carry_chain[N]
    sum = uint!(bits)
}

 module Top() -> () {}

Module instances

  • Module instances are first-class values that can be assigned to variables.

  • If a module is parameterised, all the parameters must be specified when creating an instance.

  • Parameter names can be omitted if the parameters are specified in the same order as the declaration.

module Adder[N: Nat](
    a: uint[N],
    b: uint[N],
    carry_in: bool,
) -> (
    sum: uint[N],
    carry_out: bool,
) {
    let total: uint[N + 1] = a + b + carry_in
    sum = total[N - 1 : 0]
    carry_out = total[N]
}

module Top(clk: clock) -> () {
    let counter = Reg[u32](clk)
    counter.d = counter.q + 1
    
    let adder = Adder[32]( // or Adder[N: 32](..)
        a: counter.q,
        b: 32'1,
        carry_in: 1'0,
    )
}
  • A module instance need not assign all ports as the module's ports are accessible as fields of the instance using the . operator.

  • Ports whose values are local variables with the same name can omit the right-hand side of the assignment.

All three Reg instances in the following example are equivalent.

module Top(clk: clock, rst: bool) -> () {
    let reg1 = Reg[u32](clk: clk, rst: rst)
    let reg2 = Reg[u32](clk, rst)
    let reg3 = Reg[u32]()

    reg3.clk = clk
    reg3.rst = rst
}

Let Bindings

  • A let binding introduces a named value in the current scope.
  • Let bindings must be defined inside a module declaration.
  • The type of the binding is inferred from the right-hand side of the assignment if present otherwise, the type must be explicitly specified.
 module Test() -> () {
    let message1: [3]u8
    message1 = "Yo!"
    let message2 = "Hi!"
    let message3: [3]u8 = "Hey"
 }

Assignments

If a binding is assigned multiple times, only the last assignment is used.

 module Test() -> () {
    let a = true // this first assignment is ignored
    a = false
 }

Block Expressions

Block expressions group multiple statements in a new lexical scope and optionally return a value:

 module Test(clk: clock) -> () {
    let result = {
        let a = 5
        let b = 10
        (a + b)  // Last expression becomes the block's value
    }

    assert!(result == 15)
 }