Variable Types

AVL provides a number of variable types more appropriate for HDL design than the standard Python types.

  • Python lacks 4-state supports. As such all cocotb and AVL variables are 2-state.

  • Python lacks width support for integers. AVL adds native width and sign support including half, single and double precision floating point numbers.

System Verilog Type

Python Type

AVL Variable

shortint

int

avl.Int16

int

int

avl.Int32

longint

int

avl.Int64

byte

int

avl.Byte / avl.Int8

bit

bool

avl.Logic / avl.Bool

logic

int

avl.Logic / avl.Unit8 / avl.Uint16 / avl.Uint32 / avl.Uint64

reg

int

integer

int

avl.Int / avl.Int32

time

int

avl.Int64

real

float

avl.Double / avl.Fp64

shortreal

float

avl.Half / avl.Fp32

float

avl.Float / avl.Fp16

string

str

str

enum

Enum

avl.Enum

Variable Usage

AVL attempts to make variable usage as close to Python and SystemVerilog as possible.

All AVL variable types inherit from the avl.Var base class which provides the magic methods to make the variables behave like their Python and SystemVerilog counterparts.

The only exception to this is when attempting to directly update the value of a variable, when the user must update the underlying value explicitly.

Unlike python ints avl_vars are mutable, so the user can update the value of the variable directly without creating a new handle. AVL variables can be used in the same way as Python variables, and can be used in expressions, assignments, and function calls. This is to allow variables saved for lambda functions to be preserved.

avl.Logic objects are unsigned and support constraints usings bitwise operations. However, due to this they can be slower to randomize than avl.Int and avl.Uint objects which only support arithmetic constraints.

avl.Logic and its children (i.e. avl.Uint, avl.Int and avl.Bool) all support bit slice access with the following constraints:

  • Single-index slices are supported, e.g. myvar[31] will return bit 31 of myvar.

  • Slice indexes follow the same rule as for strings and lists: myvar[0:8] will access bits 0 to 7 of myvar.

  • Slice indexes must always be positive.

  • SystemVerilog-like accesses where the greater index is first (e.g. myvar[8:0]), are not supported.

  • Steps (e.g. myvar[0:8:1]) are not supported.

Integer / Logic Example

# Copyright 2024 Apheleia
#
# Description:
# Apheleia attributes example


import avl
import cocotb
from z3 import And


class example_env(avl.Env):

    def test_unsigned(self):

        self.info("Testing unsigned logic and arithmetic operations...")

        a = avl.Logic(5, width=4)     # 0b0101
        b = avl.Logic(13, width=4)    # 0b1101

        c = 1 + a
        assert c == 6              # (1 + 5) % 16 = 6
        assert type(c) is avl.Logic  # Ensure c is still a Logic object

        # Constants
        mod = 1 << 4  # 16
        assert mod == 16

        # Arithmetic
        assert a + b == 2          # (5 + 13) % 16 = 18 % 16 = 2
        assert a - b == 8          # (5 - 13) % 16 = -8 % 16 = 8
        assert b - a == 8          # (13 - 5) % 16 = 8
        assert -a == 11            # (-5) % 16 = 11
        assert abs(b) == 13


        # Overflow / wraparound
        max_val = avl.Logic(15, width=4)
        assert max_val + 1 == 0    # 15 + 1 = 16 % 16 = 0

        min_val = avl.Logic(0, width=4)
        assert min_val - 1 == 15   # 0 - 1 = -1 % 16 = 15

        # Multiplication / division
        assert a * 3 == 15         # (5 * 3) % 16 = 15
        assert b // 2 == 6         # 13 // 2 = 6
        assert b % 4 == 1          # 13 % 4 = 1

        # Comparisons
        assert (a < b) is True     # 5 < 13
        assert (a > b) is False
        assert (a == 5) is True
        assert (b != 5) is True
        assert (a <= 5) is True
        assert (b >= 13) is True

        # Bitwise operations
        assert a & b == 5          # 0b0101 & 0b1101 = 0b0101 = 5
        assert a | b == 13         # 0b0101 | 0b1101 = 0b1101 = 13
        assert a ^ b == 8          # 0b0101 ^ 0b1101 = 0b1000 = 8
        assert ~a == 10            # ~5 = -6 % 16 = 10

        # Shifts
        assert a << 1 == 10        # (5 << 1) % 16 = 10
        assert b >> 1 == 6         # 13 >> 1 = 6

        # Oversized int operands
        assert a + 1000 == 13      # (5 + 1000) % 16 = 1005 % 16 = 13
        assert a - 300 == 9        # (5 - 300) % 16 = -295 % 16 = 9
        assert a * 123 == 7        # (5 * 123) % 16 = 615 % 16 = 7
        assert b // 999 == 0       # 13 // 999 = 0
        assert b % 99 == 13        # 13 % 99 = 13

        # Comparisons with large ints
        assert (a < 1000) is True
        assert (b < 1000) is True
        assert (a > -1000) is True
        assert (b != 999) is True

        # Bitwise operations with oversized ints
        assert a & 0b10011010010 == 0   # 5 & (1234 % 16 = 2) = 0b0101 & 0b0010 = 0b0000 = 0
        assert a | 0b10011010010 == 7   # 5 | 2 = 0b0101 | 0b0010 = 0b0111 = 7
        assert a ^ 0b10011010010 == 7   # 5 ^ 2 = 0b0101 ^ 0b0010 = 0b0111 = 7

        # Immutability check
        a_id = id(a)
        c = a + 1
        assert c == 6
        assert id(a) == a_id
        assert id(c) != a_id

        a += 1
        assert a == 6
        assert id(a) == a_id  # a should still point to the same object
        assert id(c) != a_id  # c should still point to a different object

        # slices
        c = avl.Logic(0xdeadbeef, width=32)
        assert c[0:8] == 0xef
        assert c[24:32] == 0xde
        assert c[8:24] == 0xadbe
        assert c[19] == 1
        assert c[14] == 0

        c[8:16] = 0xab
        assert c == 0xdeadabef
        c[3] = 0
        assert c == 0xdeadabe7

        self.info("Unsigned logic and arithmetic operations passed successfully.")

    def test_randomization(self):

        self.info("Testing randomization...")

        # Create a random Logic variable
        rand_var = avl.Logic(0, auto_random=True, width=4)

        # Check if the value is within the expected range
        assert 0 <= rand_var < (1 << 4)

        # Simple randomization
        for _ in range(10):
            rand_var.randomize(hard=[lambda x: And(x >=1, x <= 2)])
            assert  rand_var >= 1 and rand_var <= 2

        # Bitwise
        for _ in range(10):
            rand_var.randomize(hard=[lambda x: And(x & 0b0001 == 0, x & 0b0010 == 0)])
            assert rand_var.value % 4 == 0

            rand_var.randomize(hard=[lambda x: (x ^ 0b0011) & 0b0011 == 0])
            assert rand_var.value % 4 == 3

        self.info("Randomization test passed successfully.")

    def __init__(self, name, parent):
        super().__init__(name, parent)

        # Test unsigned arithmetic operations
        self.test_unsigned()

        # Test randomization
        self.test_randomization()


@cocotb.test
async def test(dut):
    _ = example_env("env", None)

Float / Real Example

# Copyright 2024 Apheleia
#
# Description:
# Apheleia attributes example


import avl
import cocotb
import numpy as np
from cocotb.triggers import Timer


class example_env(avl.Env):

    def test_avl_fp16(self):
        a = avl.Fp16(2.0)
        b = avl.Fp16(3.0)

        # Arithmetic
        assert a + b == 5.0            # 2.0 + 3.0 = 5.0
        c = avl.Fp16(a)
        c += b
        assert c == 5.0                # 2.0 + 3.0 = 5.0

        assert a - b == -1.0           # 2.0 - 3.0 = -1.0
        c = avl.Fp16(a)
        c -= b
        assert c == -1.0               # 2.0 - 3.0 = -1.0

        assert a * b == 6.0            # 2.0 * 3.0 = 6.0
        c = avl.Fp16(a)
        c *= b
        assert c == 6.0                # 2.0 * 3.0 = 6.0

        assert b / a == 1.5            # 3.0 / 2.0 = 1.5
        c = avl.Fp16(b)
        c /= a
        assert c == 1.5                # 3.0 / 2.0 = 1.5

        # Power
        assert a ** 2 == 4.0           # 2.0 ** 2 = 4.0
        c = avl.Fp16(a)
        c **= 2
        assert c == 4.0                # 2.0 ** 2 = 4.0

        # Comparisons
        assert a < b                   # 2.0 < 3.0
        assert not (a > b)
        assert a <= b
        assert not (a >= b)
        assert a != b
        assert a == avl.Fp16(2.0)

        # String
        assert str(a) == "2.0"         # str of float16
        assert isinstance(repr(a), str)

        # === Special values ===

        # Positive infinity
        pos_inf = avl.Fp16(np.inf)
        assert pos_inf > b             # inf > 3.0
        assert str(pos_inf) == "inf"

        # Negative infinity
        neg_inf = avl.Fp16(-np.inf)
        assert neg_inf < b             # -inf < 3.0
        assert str(neg_inf) == "-inf"

        # NaN
        nan = avl.Fp16(np.nan)
        assert nan != nan              # NaN is not equal to anything
        assert not (nan < b)           # Comparisons with NaN are always False
        assert not (nan > b)
        assert not (nan == b)
        assert str(nan) == "nan"

        # Overflow to inf
        large = avl.Fp16(1e5)
        assert large.value == np.inf    # np.float16 overflows at ~65504

        # Underflow to 0
        tiny = avl.Fp16(1e-10)
        assert tiny.value == 0.0        # underflow to zero in float16

    def test_avl_fp32(self):
        a = avl.Fp32(2.0)
        b = avl.Fp32(3.0)

        # Arithmetic
        assert a + b == 5.0            # 2.0 + 3.0 = 5.0
        c = avl.Fp32(a)
        c += b
        assert c == 5.0                # 2.0 + 3.0 = 5.0

        assert a - b == -1.0           # 2.0 - 3.0 = -1.0
        c = avl.Fp32(a)
        c -= b
        assert c == -1.0               # 2.0 - 3.0 = -1.0

        assert a * b == 6.0            # 2.0 * 3.0 = 6.0
        c = avl.Fp32(a)
        c *= b
        assert c == 6.0                # 2.0 * 3.0 = 6.0

        assert b / a == 1.5            # 3.0 / 2.0 = 1.5
        c = avl.Fp32(b)
        c /= a
        assert c == 1.5                # 3.0 / 2.0 = 1.5

        # Power
        assert a ** 2 == 4.0           # 2.0 ** 2 = 4.0
        c = avl.Fp32(a)
        c **= 2
        assert c == 4.0                # 2.0 ** 2 = 4.0

        # Comparisons
        assert a < b
        assert not (a > b)
        assert a <= b
        assert not (a >= b)
        assert a != b
        assert a == avl.Fp32(2.0)

        # String
        assert str(a) == "2.0"
        assert isinstance(repr(a), str)

        # Special values
        pos_inf = avl.Fp32(np.inf)
        assert pos_inf > b
        assert str(pos_inf) == "inf"

        neg_inf = avl.Fp32(-np.inf)
        assert neg_inf < b
        assert str(neg_inf) == "-inf"

        nan = avl.Fp32(np.nan)
        assert nan != nan
        assert not (nan < b)
        assert not (nan > b)
        assert not (nan == b)
        assert str(nan) == "nan"

        large = avl.Fp32(1e40)
        assert large.value == np.inf

        tiny = avl.Fp32(1e-50)
        assert tiny.value == 0.0

    def test_avl_fp64(self):
        a = avl.Fp64(2.0)
        b = avl.Fp64(3.0)

        # Arithmetic
        assert a + b == 5.0
        c = avl.Fp64(a)
        c += b
        assert c == 5.0

        assert a - b == -1.0
        c = avl.Fp64(a)
        c -= b
        assert c == -1.0

        assert a * b == 6.0
        c = avl.Fp64(a)
        c *= b
        assert c == 6.0

        assert b / a == 1.5
        c = avl.Fp64(b)
        c /= a
        assert c == 1.5

        # Power
        assert a ** 2 == 4.0
        c = avl.Fp64(a)
        c **= 2
        assert c == 4.0

        # Comparisons
        assert a < b
        assert not (a > b)
        assert a <= b
        assert not (a >= b)
        assert a != b
        assert a == avl.Fp64(2.0)

        # String
        assert str(a) == "2.0"
        assert isinstance(repr(a), str)

        # Special values
        pos_inf = avl.Fp64(np.inf)
        assert pos_inf > b
        assert str(pos_inf) == "inf"

        neg_inf = avl.Fp64(-np.inf)
        assert neg_inf < b
        assert str(neg_inf) == "-inf"

        nan = avl.Fp64(np.nan)
        assert nan != nan
        assert not (nan < b)
        assert not (nan > b)
        assert not (nan == b)
        assert str(nan) == "nan"

        large = avl.Fp64(1e310)
        assert large.value == np.inf

        tiny = avl.Fp64(1e-400)
        assert tiny.value == 0.0

    def __init__(self, name, parent):
        super().__init__(name, parent)

        self.test_avl_fp16()

        self.test_avl_fp32()

        self.test_avl_fp64()

        # Variables for testing HDL interaction
        self.fp16 = avl.Fp16(0.0)
        self.fp32 = avl.Fp32(0.0)
        self.fp64 = avl.Fp64(0.0)


@cocotb.test
async def test(dut):
    e = example_env("env", None)

    await Timer(100, unit="ns")

    e.fp16 += -0.3
    assert e.fp16 == -0.3
    dut.fp16.value = e.fp16.to_bits()

    e.fp32 += 11.4
    assert e.fp32 == 11.4
    dut.fp32.value = e.fp32.to_bits()

    e.fp64 += 110202.0821
    assert e.fp64 == 110202.0821
    dut.fp64.value = e.fp64.to_bits()

    await Timer(100, unit="ns")

    # Read back and check
    fp16 = avl.Fp16(0.0)
    fp16.from_bits(dut.fp16.value)
    assert fp16 == e.fp16

    fp32 = avl.Fp32(0.0)
    fp32.from_bits(dut.fp32.value)
    assert fp32 == e.fp32

    fp64 = avl.Fp64(0.0)
    fp64.from_bits(dut.fp64.value)
    assert fp64 == e.fp64

Enum Example

# Copyright 2024 Apheleia
#
# Description:
# Apheleia attributes example


import avl
import cocotb


class example_env(avl.Env):
    def __init__(self, name, parent):
        super().__init__(name, parent)

        self.e = avl.Enum("A", {"A": 0, "B": 1, "C": 2})

        # Add a constraint
        self.e.add_constraint("c", lambda x: x != self.e.A)

@cocotb.test
async def test(dut):
    e = example_env("env", None)

    # Direct allocation - must use .value to avoid trampling the class
    e.e.value = e.e.B
    assert e.e == e.e.B

    # Increment
    e.e += 1
    assert e.e == e.e.C

    # Random
    for _ in range(10):
        e.e.randomize()
        assert e.e in e.e.values.values()