Arithmetic pitfalls

MyHDL arithmetics, in particular addition/subtraction of intbv() signals does not account for bit widths within a chain of additions/subtractions. Therefore it is possible to create scenarios where certain values that never occur in the MyHDL model (due to intbv() min max restrictions) are left unconvered (such as truncated results) in the resulting HDL.

The IRL kernel however does account for bit widths and is stricter with respect to truncation, plus it allows expressions that are not valid using MyHDL intbvs, as elaborated below. However, the bit width accounted for is always the hard amount of bits used for a binary value, not a logical limit as applied to an intbv().

In [1]:
import sys
sys.path.insert(0, "../..")
In [2]:
from myhdl import *

@block
def calc(a, b):
    @always_comb
    def worker():
        b.next = a + a - 8
        
    return instances()

a = Signal(intbv(min=0, max=9))
b = Signal(intbv(min=-8, max=9))
inst = calc(a, b)
inst.convert("VHDL")
Out[2]:
<myhdl._block._Block at 0x7fb9fc2bf220>
In [3]:
! grep -A 4 resize calc.vhd
b <= signed((resize(a, 5) + a) - 8);

end architecture MyHDL;

Let's recapitulate a few intbv properties:

In [4]:
a = intbv(8)[4:]
assert int(a.signed()) == -8
a, bin(a), a.signed()
Out[4]:
(intbv(8), '1000', intbv(-8))

We observe this being bit accurate: since the MSB is set, casting it to a Signed type will yield its negated value.

Implicit truncation

This case may appear constructed, but is an example of 'boundaries gone wrong' or 'testing with insufficient values'. Fortunately, we get a GHDL warning on the truncated vectors, but due to lack of static bit width accounting, it will be left unnoticed in the translation stage.

In [5]:
from myirl.test.common_test import run_ghdl

@block
def test_arith1():
    c = Signal(intbv(15)[4:])
    a = Signal(intbv(0, min=0, max=9))
    b = Signal(intbv(min=-8, max=9))

    @always_comb
    def worker():
        b.next = a + a + c + c - 36
        
    @instance
    def feed():
        a.next = 7
        c.next = 15
        yield delay(1)
        print(b)
        assert b == 8
        # These values will also yield the same result in the HDL transfer,
        # however, MyHDL simulation will notice
        a.next = 6
        c.next = 0
        yield delay(1)
        print(b)
        assert b == 8
    
    return instances()

def run():
    import os
    pwd = os.getcwd()

    inst = test_arith1()
    # Simulation would detect the above overflow in this case:
    try:
        inst.run_sim(10)
    except ValueError as e:
        print("ERROR DETECTED", e)
        
    inst = test_arith1()
    inst.convert("VHDL")

    run_ghdl([pwd + "/test_arith1.vhd", pwd + "/pck_myhdl_011.vhd"], inst, debug = True)
    
run()
08
ERROR DETECTED intbv value -24 < minimum -8
==== COSIM stdout ====

==== COSIM stderr ====

==== COSIM stdout ====
analyze /home/testing/src/myhdl2/myhdl.v2we/examples/pck_myhdl_011.vhd
analyze /home/testing/src/myhdl2/myhdl.v2we/examples/test_arith1.vhd
elaborate test_arith1

==== COSIM stderr ====

==== COSIM stdout ====
../../src/ieee2008/numeric_std-body.vhdl:3089:7:@0ms:(assertion warning): NUMERIC_STD.TO_UNSIGNED: vector truncated
../../src/ieee2008/numeric_std-body.vhdl:3089:7:@0ms:(assertion warning): NUMERIC_STD.TO_UNSIGNED: vector truncated
08
../../src/ieee2008/numeric_std-body.vhdl:3089:7:@1ns:(assertion warning): NUMERIC_STD.TO_UNSIGNED: vector truncated
08

==== COSIM stderr ====

In [6]:
! grep resize test_arith1.vhd
b <= signed((((resize(a, 5) + a) + c) + c) - 36);

intbv behaviour

Important to keep in mind: an addition or subtraction involving an intbv will no longer be an intbv:

In [7]:
t = a + a
type(t)
Out[7]:
int

So this will not work:

In [8]:
try:
    t = (a + 1).signed()
    assert False # Never hit
except AttributeError as e:
    print(e)
'int' object has no attribute 'signed'

In fact this is not a deficiency of the intbv concept, rather, this property elegantly offloads the boundary checks to the simulation. However, apart from non-supported constructs as the above, it does not support static checking or bit accounting for pipelines from the HLS library.

MyIRL / emulation variant

The IRL kernel does not implicitely truncate, unless the bit size of the result is one more than the signal it is assigned to. In this case, a warning is emitted. If the bit size is larger, a size mismatch error will be thrown.

When the result is signed, the arguments however are unsigned, the IRL requires a more explicit specification which part is to be assumed 'signed', otherwise, an exception is thrown:

In [9]:
from myirl.emulation.myhdl import *
from myirl.kernel.components import DesignModule, DummyVHDLModule

a = Signal(intbv()[4:])
sa = Signal(intbv(0, min=-16, max=17))
ctx = DummyVHDLModule()
op = sa.set(a)
try:
    op.emit(ctx)
    assert False # Should never get here
except TypeError as e:
    print("EXPECTED ERROR", e)
EXPECTED ERROR <s_4248> <= <s_c209> (<class 'myirl.emulation.signals.Signal'>): requires explicit casting

Since it is size-sensitive, a .signed() cast will result in a negative number if the MSB of the signal wire is set. This may result in a number of pitfalls, see MODEs below. One of them produces the wrong result. What we're trying to achieve, is the bit-correct operation for

In [10]:
result = a + a - 8

where result is obviously signed and a is unsigned. Some of the following logic constructs are incorrect. Which MODEs would that be?

In [11]:
@block
def calc(a, b, MODE = 0):
    
    if MODE == 0:
        @always_comb
        def worker():
            b.next = a.signed() + a - 8
    elif MODE == 1:
        @always_comb
        def worker():
            b.next = a + (a - 8).signed()
    elif MODE == 2:
        a1 = a.resize(a.size() + 1)
        @always_comb
        def worker():
            b.next = a1.signed() + a1.signed() - 8
    elif MODE == 3:
        @always_comb
        def worker():
            b.next = (a + a - 8).signed()            
    return instances()

def test():
    a = Signal(intbv(min=0, max=9))
    b = Signal(intbv(min=-2*8, max=2*8+1))
    print("Size a =", len(a), "Signed:", a.is_signed(), ", Size b =", len(b), "Signed:", b.is_signed())
    for mode in [0, 1, 2, 3]:
        inst = calc(a, b, MODE = mode)
        f = inst.elab(targets.VHDL)
    
test()
Size a = 4 Signed: False , Size b = 6 Signed: True
 Writing 'calc' to file /tmp/myirl_top_calc_njjbr593/calc.vhdl 
 Module top_calc: Existing instance calc, rename to calc_1 
 Writing 'calc_1' to file /tmp/myirl_top_calc_njjbr593/calc_1.vhdl 
 Module top_calc: Existing instance calc, rename to calc_2 
 Writing 'calc_2' to file /tmp/myirl_top_calc_njjbr593/calc_2.vhdl 
Warning: Implicit truncation of SUB(ADD(SGN(<myirl.kernel.sig.Resize object at 0x7fb9f4a6a610>), SGN(<myirl.kernel.sig.Resize object at 0x7fb9f4a6a610>)), C:8) result
 Module top_calc: Existing instance calc, rename to calc_3 
 Writing 'calc_3' to file /tmp/myirl_top_calc_njjbr593/calc_3.vhdl 
../../myirl/kernel/components.py:231: UserWarning: Unspecified port I/O `a` => IN
  base.warnings.warn("Unspecified port I/O `%s` => IN" % n)
../../myirl/kernel/components.py:228: UserWarning: Unspecified port I/O `b` => OUT
  base.warnings.warn("Unspecified port I/O `%s` => OUT" % n)

Simulation of VHDL transfer

To verify our assumption on incorrect implementations, we run the simulation for all four modes:

In [12]:
ctx = DesignModule("test_addsub", debug = True)

@block
def testbench_sum(mode):
    a = Signal(intbv(min=0, max=9))
    b = Signal(intbv(min=-2*8, max=2*8+1))
    uut = calc(a, b, mode)
    
    @instance
    def stim():
        for it in [ (8, 8), (0, -8), (12, 16)]:
            a.next = it[0]
            yield delay(1)
            print(b)
            assert b == it[1]
            yield delay(10)

    return instances()

from myirl.test import ghdl

def test_tb(mode):
    tb = testbench_sum(mode)
    f = tb.elab(targets.VHDL, elab_all = True)
    run_ghdl(f, tb, debug = True)
    return f


for mode in range(4):
    ctx.log("=========== TESTING MODE %d ===========" % mode, annotation = 'info')

    try:
        f = test_tb(mode)
        ctx.log("TEST PASS")
    except (ghdl.RuntimeError, ghdl.AnalysisError):
        ctx.log("TEST FAIL", annotation = 'warn')
 =========== TESTING MODE 0 =========== 
 Module top_testbench_sum: Existing instance calc, rename to calc_4 
Creating sequential 'testbench_sum/stim' 
 Elaborating component calc_s4_s6_0 
 Writing 'calc_4' to file /tmp/myirl_top_testbench_sum_pny4lvs2/calc_4.vhdl 
 Elaborating component testbench_sum_0 
 Writing 'testbench_sum' to file /tmp/myirl_top_testbench_sum_pny4lvs2/testbench_sum.vhdl 
 Creating library file /tmp/myirl_module_defs_v8x750mq/module_defs.vhdl 
==== COSIM stdout ====

==== COSIM stderr ====

==== COSIM stdout ====
analyze /home/testing/src/myhdl2/myirl/targets/../test/vhdl/txt_util.vhdl
analyze /home/testing/src/myhdl2/myirl/targets/libmyirl.vhdl
analyze /tmp/myirl_top_testbench_sum_pny4lvs2/calc_4.vhdl
analyze /tmp/myirl_top_testbench_sum_pny4lvs2/testbench_sum.vhdl
elaborate testbench_sum

==== COSIM stderr ====

==== COSIM stdout ====
0x38
/tmp/myirl_top_testbench_sum_pny4lvs2/testbench_sum.vhdl:40:13:@1ns:(assertion failure): Failed in /tmp/ipykernel_7560/2637647312.py:testbench_sum():15
/tmp/testbench_sum:error: assertion failed
in process .testbench_sum(myirl).stim
/tmp/testbench_sum:error: simulation failed

==== COSIM stderr ====

 TEST FAIL 
 =========== TESTING MODE 1 =========== 
 Module top_testbench_sum: Existing instance testbench_sum, rename to testbench_sum_1 
 Module top_testbench_sum: Existing instance calc, rename to calc_5 
Creating sequential 'testbench_sum/stim' 
 Elaborating component calc_s4_s6_1 
 Writing 'calc_5' to file /tmp/myirl_top_testbench_sum_pny4lvs2/calc_5.vhdl 
 Elaborating component testbench_sum_1 
 Writing 'testbench_sum_1' to file /tmp/myirl_top_testbench_sum_pny4lvs2/testbench_sum_1.vhdl 
 Elaborating component calc_s4_s6_0 
 Writing 'calc_4' to file /tmp/myirl_top_testbench_sum_pny4lvs2/calc_4.vhdl 
 Elaborating component testbench_sum_0 
 Writing 'testbench_sum' to file /tmp/myirl_top_testbench_sum_pny4lvs2/testbench_sum.vhdl 
 Creating library file /tmp/myirl_module_defs_0v221b33/module_defs.vhdl 
==== COSIM stdout ====

==== COSIM stderr ====

==== COSIM stdout ====
analyze /home/testing/src/myhdl2/myirl/targets/../test/vhdl/txt_util.vhdl
analyze /home/testing/src/myhdl2/myirl/targets/libmyirl.vhdl
analyze /tmp/myirl_top_testbench_sum_pny4lvs2/calc_5.vhdl
analyze /tmp/myirl_top_testbench_sum_pny4lvs2/testbench_sum_1.vhdl
elaborate testbench_sum_1

==== COSIM stderr ====

==== COSIM stdout ====
0x08
0x38
0x10

==== COSIM stderr ====

 TEST PASS 
 =========== TESTING MODE 2 =========== 
 Module top_testbench_sum: Existing instance testbench_sum, rename to testbench_sum_2 
 Module top_testbench_sum: Existing instance calc, rename to calc_6 
Creating sequential 'testbench_sum/stim' 
 Elaborating component calc_s4_s6_2 
 Writing 'calc_6' to file /tmp/myirl_top_testbench_sum_pny4lvs2/calc_6.vhdl 
Warning: Implicit truncation of SUB(ADD(SGN(<myirl.kernel.sig.Resize object at 0x7fb9f4a10610>), SGN(<myirl.kernel.sig.Resize object at 0x7fb9f4a10610>)), C:8) result
 Elaborating component testbench_sum_2 
 Writing 'testbench_sum_2' to file /tmp/myirl_top_testbench_sum_pny4lvs2/testbench_sum_2.vhdl 
 Elaborating component calc_s4_s6_1 
 Writing 'calc_5' to file /tmp/myirl_top_testbench_sum_pny4lvs2/calc_5.vhdl 
 Elaborating component testbench_sum_1 
 Writing 'testbench_sum_1' to file /tmp/myirl_top_testbench_sum_pny4lvs2/testbench_sum_1.vhdl 
 Elaborating component calc_s4_s6_0 
 Writing 'calc_4' to file /tmp/myirl_top_testbench_sum_pny4lvs2/calc_4.vhdl 
 Elaborating component testbench_sum_0 
 Writing 'testbench_sum' to file /tmp/myirl_top_testbench_sum_pny4lvs2/testbench_sum.vhdl 
 Creating library file /tmp/myirl_module_defs_qkry0svb/module_defs.vhdl 
==== COSIM stdout ====

==== COSIM stderr ====

==== COSIM stdout ====
analyze /home/testing/src/myhdl2/myirl/targets/../test/vhdl/txt_util.vhdl
analyze /home/testing/src/myhdl2/myirl/targets/libmyirl.vhdl
analyze /tmp/myirl_top_testbench_sum_pny4lvs2/calc_6.vhdl
analyze /tmp/myirl_top_testbench_sum_pny4lvs2/testbench_sum_2.vhdl
elaborate testbench_sum_2

==== COSIM stderr ====

==== COSIM stdout ====
0x08
0x38
0x10

==== COSIM stderr ====

 TEST PASS 
 =========== TESTING MODE 3 =========== 
 Module top_testbench_sum: Existing instance testbench_sum, rename to testbench_sum_3 
 Module top_testbench_sum: Existing instance calc, rename to calc_7 
Creating sequential 'testbench_sum/stim' 
 Elaborating component calc_s4_s6_3 
 Writing 'calc_7' to file /tmp/myirl_top_testbench_sum_pny4lvs2/calc_7.vhdl 
 Elaborating component testbench_sum_3 
 Writing 'testbench_sum_3' to file /tmp/myirl_top_testbench_sum_pny4lvs2/testbench_sum_3.vhdl 
 Elaborating component calc_s4_s6_2 
 Writing 'calc_6' to file /tmp/myirl_top_testbench_sum_pny4lvs2/calc_6.vhdl 
Warning: Implicit truncation of SUB(ADD(SGN(<myirl.kernel.sig.Resize object at 0x7fb9f4a10610>), SGN(<myirl.kernel.sig.Resize object at 0x7fb9f4a10610>)), C:8) result
 Elaborating component testbench_sum_2 
 Writing 'testbench_sum_2' to file /tmp/myirl_top_testbench_sum_pny4lvs2/testbench_sum_2.vhdl 
 Elaborating component calc_s4_s6_1 
 Writing 'calc_5' to file /tmp/myirl_top_testbench_sum_pny4lvs2/calc_5.vhdl 
 Elaborating component testbench_sum_1 
 Writing 'testbench_sum_1' to file /tmp/myirl_top_testbench_sum_pny4lvs2/testbench_sum_1.vhdl 
 Elaborating component calc_s4_s6_0 
 Writing 'calc_4' to file /tmp/myirl_top_testbench_sum_pny4lvs2/calc_4.vhdl 
 Elaborating component testbench_sum_0 
 Writing 'testbench_sum' to file /tmp/myirl_top_testbench_sum_pny4lvs2/testbench_sum.vhdl 
 Creating library file /tmp/myirl_module_defs_1bjgtk97/module_defs.vhdl 
==== COSIM stdout ====

==== COSIM stderr ====

==== COSIM stdout ====
analyze /home/testing/src/myhdl2/myirl/targets/../test/vhdl/txt_util.vhdl
analyze /home/testing/src/myhdl2/myirl/targets/libmyirl.vhdl
analyze /tmp/myirl_top_testbench_sum_pny4lvs2/calc_7.vhdl
analyze /tmp/myirl_top_testbench_sum_pny4lvs2/testbench_sum_3.vhdl
elaborate testbench_sum_3

==== COSIM stderr ====

==== COSIM stdout ====
0x08
0x38
0x10

==== COSIM stderr ====

 TEST PASS 
../../myirl/kernel/components.py:231: UserWarning: Unspecified port I/O `a` => IN
  base.warnings.warn("Unspecified port I/O `%s` => IN" % n)
../../myirl/kernel/components.py:228: UserWarning: Unspecified port I/O `b` => OUT
  base.warnings.warn("Unspecified port I/O `%s` => OUT" % n)
../../myirl/kernel/components.py:231: UserWarning: Unspecified port I/O `a` => IN
  base.warnings.warn("Unspecified port I/O `%s` => IN" % n)
../../myirl/kernel/components.py:228: UserWarning: Unspecified port I/O `b` => OUT
  base.warnings.warn("Unspecified port I/O `%s` => OUT" % n)
../../myirl/kernel/components.py:231: UserWarning: Unspecified port I/O `a` => IN
  base.warnings.warn("Unspecified port I/O `%s` => IN" % n)
../../myirl/kernel/components.py:228: UserWarning: Unspecified port I/O `b` => OUT
  base.warnings.warn("Unspecified port I/O `%s` => OUT" % n)

Result sizes and truncation

The above calculation a + a - 8 presents a few more cases specific to intbv().

We define again a, because we include '8', we need four bits. This would allow representing 15, in synthesized or simulated hardware. Because we restricted a to maximum 8, we can, however, impose some boundaries on the result.

In [13]:
a = Signal(intbv(min=0, max=9), name = 'a')
b = Signal(intbv(0, min = -8, max = 9), name = 'b')

op0 = (a + a - 8).signed()
op1 = a + (a - 8).signed()

Verify size of a:

In [14]:
a.size()
Out[14]:
4

By default, the operation sizes match the 'hard' bit properties and don't respect the intbv() restrictions:

In [15]:
op0.size(), op1.size(), len(intbv(0, min=-8, max=22))
Out[15]:
(6, 6, 6)

However, for a maximum result of 8, five bits would suffice:

In [16]:
assert len(intbv(0, min=-8, max=8+1)) == 5

We note: the IRL kernel does not account for the effectively needed bits. It will however truncate with a warning, in the special case of an assignment where the source is an explicit addition.

If it's not, an exception will throw. See below:

In [17]:
d = DummyVHDLModule()

gens = [
        b.set(op0), b.set(op1)
    ]
    
for g in gens:
    print("*** EMIT ", g)
    try:
        g.emit(d)
    except base.SizeMismatch as e:
        print("Failed", e)
*** EMIT  b <= SGN(SUB(ADD(a, a), C:8))
Failed Expression SGN(SUB(ADD(a, a), C:8))/<class 'myirl.kernel.sig.Signed'> exceeds bit size of signal (6 > 5)
*** EMIT  b <= ADD(a, SGN(SUB(a, C:8)))
Warning: Implicit truncation of ADD(a, SGN(SUB(a, C:8))) result
b <= resize(signed((signed(resize((a), 6)) + signed((resize((a), 5) - "01000")))), 5);

Negation

Negation of a value will increment the number of bits by one. This obviously turns an unsigned signal into signed, and applies to the special case where a signed signal wire contains the most negative possible value.

In [18]:
sa.is_signed(), a.is_signed()
Out[18]:
(True, False)
In [19]:
sb = -sa
b = - a
sa.size(), sb.size(), a.size(), b.size()
Out[19]:
(6, 7, 4, 5)

Further issues

  • Explicit 'inline' result truncating may be necessary to handle bit size overflows. These don't always transfer correctly to the target HDL yet and require explicit split ups of the signal operations and assignments.
  • As elaborated above, no special intbv handling for specified boundaries is in place. The kernel will always use the minimum bit width necessary to express an operation
  • See also Sign extending for more scenarios.