fix a few bugs in the functional backend and refactor the testing

This commit is contained in:
Emily Schmidt 2024-07-16 17:55:26 +01:00
parent 674e6d201d
commit 6922633b0b
6 changed files with 366 additions and 270 deletions

View File

@ -226,26 +226,36 @@ public:
T b = extend(inputs.at(ID(B)), b_width, width, is_signed); T b = extend(inputs.at(ID(B)), b_width, width, is_signed);
if(is_signed) { if(is_signed) {
if(cellType == ID($div)) { if(cellType == ID($div)) {
// divide absolute values, then flip the sign if input signs differ
// but extend the width first, to handle the case (most negative value) / (-1)
T abs_y = factory.unsigned_div(abs(a, width), abs(b, width), width); T abs_y = factory.unsigned_div(abs(a, width), abs(b, width), width);
T out_sign = factory.not_equal(sign(a, width), sign(b, width), 1); T out_sign = factory.not_equal(sign(a, width), sign(b, width), 1);
return neg_if(extend(abs_y, width, y_width, true), y_width, out_sign); return neg_if(extend(abs_y, width, y_width, false), y_width, out_sign);
} else if(cellType == ID($mod)) { } else if(cellType == ID($mod)) {
// similar to division but output sign == divisor sign
T abs_y = factory.unsigned_mod(abs(a, width), abs(b, width), width); T abs_y = factory.unsigned_mod(abs(a, width), abs(b, width), width);
return neg_if(extend(abs_y, width, y_width, true), y_width, sign(a, width)); return neg_if(extend(abs_y, width, y_width, false), y_width, sign(a, width));
} else if(cellType == ID($divfloor)) { } else if(cellType == ID($divfloor)) {
// if b is negative, flip both signs so that b is positive
T b_sign = sign(b, width); T b_sign = sign(b, width);
T a1 = neg_if(a, width, b_sign); T a1 = neg_if(a, width, b_sign);
T b1 = neg_if(b, width, b_sign); T b1 = neg_if(b, width, b_sign);
T a1_sign = sign(a1, width); // if a is now negative, calculate ~((~a) / b) = -((-a - 1) / b + 1)
// which equals the negative of (-a) / b with rounding up rather than down
// note that to handle the case where a = most negative value properly,
// we have to calculate a1_sign from the original values rather than using sign(a1, width)
T a1_sign = factory.bitwise_and(factory.not_equal(sign(a, width), sign(b, width), 1), reduce_or(a, width), 1);
T a2 = factory.mux(a1, factory.bitwise_not(a1, width), a1_sign, width); T a2 = factory.mux(a1, factory.bitwise_not(a1, width), a1_sign, width);
T y1 = factory.unsigned_div(a2, b1, width); T y1 = factory.unsigned_div(a2, b1, width);
T y2 = factory.mux(y1, factory.bitwise_not(y1, width), a1_sign, width); T y2 = extend(y1, width, y_width, false);
return extend(y2, width, y_width, true); return factory.mux(y2, factory.bitwise_not(y2, y_width), a1_sign, y_width);
} else if(cellType == ID($modfloor)) { } else if(cellType == ID($modfloor)) {
// calculate |a| % |b| and then subtract from |b| if input signs differ and the remainder is non-zero
T abs_b = abs(b, width); T abs_b = abs(b, width);
T abs_y = factory.unsigned_mod(abs(a, width), abs_b, width); T abs_y = factory.unsigned_mod(abs(a, width), abs_b, width);
T flip_y = factory.bitwise_and(factory.bitwise_xor(sign(a, width), sign(b, width), 1), factory.reduce_or(abs_y, width), 1); T flip_y = factory.bitwise_and(factory.bitwise_xor(sign(a, width), sign(b, width), 1), factory.reduce_or(abs_y, width), 1);
T y_flipped = factory.mux(abs_y, factory.sub(abs_b, abs_y, width), flip_y, width); T y_flipped = factory.mux(abs_y, factory.sub(abs_b, abs_y, width), flip_y, width);
// since y_flipped is strictly less than |b|, the top bit is always 0 and we can just sign extend the flipped result
T y = neg_if(y_flipped, width, sign(b, b_width)); T y = neg_if(y_flipped, width, sign(b, b_width));
return extend(y, width, y_width, true); return extend(y, width, y_width, true);
} else } else
@ -261,22 +271,8 @@ public:
} else if (cellType == ID($lut)) { } else if (cellType == ID($lut)) {
int width = parameters.at(ID(WIDTH)).as_int(); int width = parameters.at(ID(WIDTH)).as_int();
Const lut_table = parameters.at(ID(LUT)); Const lut_table = parameters.at(ID(LUT));
T a = inputs.at(ID(A)); lut_table.extu(1 << width);
// Output initialization return handle_bmux(factory.constant(lut_table), inputs.at(ID(A)), 1 << width, 0, 1, width, width);
T y = factory.constant(Const(0, 1));
// Iterate over each possible input combination
for (int i = 0; i < (1 << width); ++i) {
// Create a constant representing the value of i
T i_val = factory.constant(Const(i, width));
// Check if the input matches this value
T match = factory.equal(a, i_val, width);
// Get the corresponding LUT value
bool lut_val = lut_table.bits[i] == State::S1;
T lut_output = factory.constant(Const(lut_val, 1));
// Use a multiplexer to select the correct output based on the match
y = factory.mux(y, lut_output, match, 1);
}
return y;
} else if (cellType == ID($bwmux)) { } else if (cellType == ID($bwmux)) {
int width = parameters.at(ID(WIDTH)).as_int(); int width = parameters.at(ID(WIDTH)).as_int();
T a = inputs.at(ID(A)); T a = inputs.at(ID(A));
@ -526,7 +522,7 @@ void FunctionalIR::topological_sort() {
if(scc) log_error("combinational loops, aborting\n"); if(scc) log_error("combinational loops, aborting\n");
} }
IdString merge_name(IdString a, IdString b) { static IdString merge_name(IdString a, IdString b) {
if(a[0] == '$' && b[0] == '\\') if(a[0] == '$' && b[0] == '\\')
return b; return b;
else else

View File

@ -1,14 +1,30 @@
import pytest import pytest
from rtlil_cells import generate_test_cases from rtlil_cells import generate_test_cases
import random
random_seed = random.getrandbits(32)
def pytest_addoption(parser): def pytest_addoption(parser):
parser.addoption( parser.addoption("--per-cell", type=int, default=None, help="run only N tests per cell")
"--per-cell", type=int, default=None, help="run only N tests per cell" parser.addoption("--steps", type=int, default=1000, help="run each test for N steps")
) parser.addoption("--seed", type=int, default=random_seed, help="seed for random number generation, use random seed if unspecified")
def pytest_collection_finish(session):
print('random seed: {}'.format(session.config.getoption("seed")))
@pytest.fixture
def num_steps(request):
return request.config.getoption("steps")
@pytest.fixture
def rnd(request):
seed1 = request.config.getoption("seed")
return lambda seed2: random.Random('{}-{}'.format(seed1, seed2))
def pytest_generate_tests(metafunc): def pytest_generate_tests(metafunc):
if "cell" in metafunc.fixturenames: if "cell" in metafunc.fixturenames:
print(dir(metafunc.config))
per_cell = metafunc.config.getoption("per_cell", default=None) per_cell = metafunc.config.getoption("per_cell", default=None)
names, cases = generate_test_cases(per_cell) seed1 = metafunc.config.getoption("seed")
rnd = lambda seed2: random.Random('{}-{}'.format(seed1, seed2))
names, cases = generate_test_cases(per_cell, rnd)
metafunc.parametrize("cell,parameters", cases, ids=names) metafunc.parametrize("cell,parameters", cases, ids=names)

View File

@ -1,30 +1,6 @@
from itertools import chain from itertools import chain
import random import random
widths = [
(16, 32, 48, True),
(16, 32, 48, False),
(32, 16, 48, True),
(32, 16, 48, False),
(32, 32, 16, True),
(32, 32, 16, False)
]
shift_widths = [
(32, 6, 32, True, False),
(32, 6, 32, False, False),
(32, 6, 64, True, False),
(32, 6, 64, False, False),
(32, 32, 16, True, False),
(32, 32, 16, False, False),
(32, 6, 32, True, True),
(32, 6, 32, False, True),
(32, 6, 64, True, True),
(32, 6, 64, False, True),
(32, 32, 16, True, True),
(32, 32, 16, False, True),
]
def write_rtlil_cell(f, cell_type, inputs, outputs, parameters): def write_rtlil_cell(f, cell_type, inputs, outputs, parameters):
f.write('autoidx 1\n') f.write('autoidx 1\n')
f.write('module \\gold\n') f.write('module \\gold\n')
@ -37,207 +13,260 @@ def write_rtlil_cell(f, cell_type, inputs, outputs, parameters):
idx += 1 idx += 1
f.write(f'\tcell ${cell_type} \\UUT\n') f.write(f'\tcell ${cell_type} \\UUT\n')
for (name, value) in parameters.items(): for (name, value) in parameters.items():
f.write(f'\t\tparameter \\{name} {value}\n') if value >= 2**32:
f.write(f'\t\tparameter \\{name} {value.bit_length()}\'{value:b}\n')
else:
f.write(f'\t\tparameter \\{name} {value}\n')
for name in chain(inputs.keys(), outputs.keys()): for name in chain(inputs.keys(), outputs.keys()):
f.write(f'\t\tconnect \\{name} \\{name}\n') f.write(f'\t\tconnect \\{name} \\{name}\n')
f.write(f'\tend\nend\n') f.write(f'\tend\nend\n')
class BaseCell: class BaseCell:
def __init__(self, name): def __init__(self, name, parameters, inputs, outputs, test_values):
self.name = name self.name = name
self.parameters = parameters
self.inputs = inputs
self.outputs = outputs
self.test_values = test_values
def get_port_width(self, port, parameters):
def parse_specifier(spec):
if isinstance(spec, int):
return spec
if isinstance(spec, str):
return parameters[spec]
if callable(spec):
return spec(parameters)
assert False, "expected int, str or lambda"
if port in self.inputs:
return parse_specifier(self.inputs[port])
elif port in self.outputs:
return parse_specifier(self.outputs[port])
else:
assert False, "expected input or output"
def generate_tests(self, rnd):
def print_parameter(v):
if isinstance(v, bool):
return "S" if v else "U"
else:
return str(v)
for values in self.test_values:
if isinstance(values, int):
values = [values]
name = '-'.join([print_parameter(v) for v in values])
parameters = {parameter: int(values[i]) for i, parameter in enumerate(self.parameters)}
if self.is_test_valid(values):
yield (name, parameters)
def write_rtlil_file(self, path, parameters):
inputs = {port: self.get_port_width(port, parameters) for port in self.inputs}
outputs = {port: self.get_port_width(port, parameters) for port in self.outputs}
with open(path, 'w') as f:
write_rtlil_cell(f, self.name, inputs, outputs, parameters)
def is_test_valid(self, values):
return True
class UnaryCell(BaseCell): class UnaryCell(BaseCell):
def __init__(self, name): def __init__(self, name, values):
super().__init__(name) super().__init__(name, ['A_WIDTH', 'Y_WIDTH', 'A_SIGNED'], {'A': 'A_WIDTH'}, {'Y': 'Y_WIDTH'}, values)
def generate_tests(self):
for (a_width, _, y_width, signed) in widths:
yield (f'{a_width}-{y_width}-{'S' if signed else 'U'}',
{'A_WIDTH' : a_width,
'A_SIGNED' : int(signed),
'Y_WIDTH' : y_width})
def write_rtlil_file(self, f, parameters):
write_rtlil_cell(f, self.name, {'A': parameters['A_WIDTH']}, {'Y': parameters['Y_WIDTH']}, parameters)
class BinaryCell(BaseCell): class BinaryCell(BaseCell):
def __init__(self, name): def __init__(self, name, values):
super().__init__(name) super().__init__(name, ['A_WIDTH', 'B_WIDTH', 'Y_WIDTH', 'A_SIGNED', 'B_SIGNED'], {'A': 'A_WIDTH', 'B': 'B_WIDTH'}, {'Y': 'Y_WIDTH'}, values)
def generate_tests(self):
for (a_width, b_width, y_width, signed) in widths:
yield (f'{a_width}-{b_width}-{y_width}-{'S' if signed else 'U'}',
{'A_WIDTH' : a_width,
'A_SIGNED' : int(signed),
'B_WIDTH' : b_width,
'B_SIGNED' : int(signed),
'Y_WIDTH' : y_width})
def write_rtlil_file(self, f, parameters):
write_rtlil_cell(f, self.name, {'A': parameters['A_WIDTH'], 'B': parameters['B_WIDTH']}, {'Y': parameters['Y_WIDTH']}, parameters)
class ShiftCell(BaseCell): class ShiftCell(BaseCell):
def __init__(self, name): def __init__(self, name, values):
super().__init__(name) super().__init__(name, ['A_WIDTH', 'B_WIDTH', 'Y_WIDTH', 'A_SIGNED', 'B_SIGNED'], {'A': 'A_WIDTH', 'B': 'B_WIDTH'}, {'Y': 'Y_WIDTH'}, values)
def generate_tests(self): def is_test_valid(self, values):
for (a_width, b_width, y_width, a_signed, b_signed) in shift_widths: (a_width, b_width, y_width, a_signed, b_signed) = values
if not self.name in ('shift', 'shiftx') and b_signed: continue if not self.name in ('shift', 'shiftx') and b_signed: return False
if self.name == 'shiftx' and a_signed: continue if self.name == 'shiftx' and a_signed: return False
yield (f'{a_width}-{b_width}-{y_width}-{'S' if a_signed else 'U'}{'S' if b_signed else 'U'}', return True
{'A_WIDTH' : a_width,
'A_SIGNED' : int(a_signed),
'B_WIDTH' : b_width,
'B_SIGNED' : int(b_signed),
'Y_WIDTH' : y_width})
def write_rtlil_file(self, f, parameters):
write_rtlil_cell(f, self.name, {'A': parameters['A_WIDTH'], 'B': parameters['B_WIDTH']}, {'Y': parameters['Y_WIDTH']}, parameters)
class MuxCell(BaseCell): class MuxCell(BaseCell):
def __init__(self, name): def __init__(self, name, values):
super().__init__(name) super().__init__(name, ['WIDTH'], {'A': 'WIDTH', 'B': 'WIDTH', 'S': 1}, {'Y': 'WIDTH'}, values)
def generate_tests(self):
for width in [10, 20, 40]:
yield (f'{width}', {'WIDTH' : width})
def write_rtlil_file(self, f, parameters):
write_rtlil_cell(f, self.name, {'A': parameters['WIDTH'], 'B': parameters['WIDTH'], 'S': 1}, {'Y': parameters['WIDTH']}, parameters)
class BWCell(BaseCell): class BWCell(BaseCell):
def __init__(self, name): def __init__(self, name, values):
super().__init__(name) inputs = {'A': 'WIDTH', 'B': 'WIDTH'}
def generate_tests(self): if name == "bwmux": inputs['S'] = 'WIDTH'
for width in [10, 20, 40]: super().__init__(name, ['WIDTH'], inputs, {'Y': 'WIDTH'}, values)
yield (f'{width}', {'WIDTH' : width})
def write_rtlil_file(self, f, parameters):
inputs = {'A': parameters['WIDTH'], 'B': parameters['WIDTH']}
if self.name == "bwmux": inputs['S'] = parameters['WIDTH']
write_rtlil_cell(f, self.name, inputs, {'Y': parameters['WIDTH']}, parameters)
class PMuxCell(BaseCell): class PMuxCell(BaseCell):
def __init__(self, name): def __init__(self, name, values):
super().__init__(name) b_width = lambda par: par['WIDTH'] * par['S_WIDTH']
def generate_tests(self): super().__init__(name, ['WIDTH', 'S_WIDTH'], {'A': 'WIDTH', 'B': b_width, 'S': 'S_WIDTH'}, {'Y': 'WIDTH'}, values)
for (width, s_width) in [(10, 1), (10, 4), (20, 4)]:
yield (f'{width}-{s_width}',
{'WIDTH' : width,
'S_WIDTH' : s_width})
def write_rtlil_file(self, f, parameters):
s_width = parameters['S_WIDTH']
b_width = parameters['WIDTH'] * s_width
write_rtlil_cell(f, self.name, {'A': parameters['WIDTH'], 'B': b_width, 'S': s_width}, {'Y': parameters['WIDTH']}, parameters)
class BMuxCell(BaseCell): class BMuxCell(BaseCell):
def __init__(self, name): def __init__(self, name, values):
super().__init__(name) a_width = lambda par: par['WIDTH'] << par['S_WIDTH']
def generate_tests(self): super().__init__(name, ['WIDTH', 'S_WIDTH'], {'A': a_width, 'S': 'S_WIDTH'}, {'Y': 'WIDTH'}, values)
for (width, s_width) in [(10, 1), (10, 2), (10, 4)]:
yield (f'{width}-{s_width}', {'WIDTH' : width, 'S_WIDTH' : s_width})
def write_rtlil_file(self, f, parameters):
write_rtlil_cell(f, self.name, {'A': parameters['WIDTH'] << parameters['S_WIDTH'], 'S': parameters['S_WIDTH']}, {'Y': parameters['WIDTH']}, parameters)
class DemuxCell(BaseCell): class DemuxCell(BaseCell):
def __init__(self, name): def __init__(self, name, values):
super().__init__(name) y_width = lambda par: par['WIDTH'] << par['S_WIDTH']
def generate_tests(self): super().__init__(name, ['WIDTH', 'S_WIDTH'], {'A': 'WIDTH', 'S': 'S_WIDTH'}, {'Y': y_width}, values)
for (width, s_width) in [(10, 1), (32, 2), (16, 4)]:
yield (f'{width}-{s_width}', {'WIDTH' : width, 'S_WIDTH' : s_width})
def write_rtlil_file(self, f, parameters):
write_rtlil_cell(f, self.name, {'A': parameters['WIDTH'], 'S': parameters['S_WIDTH']}, {'Y': parameters['WIDTH'] << parameters['S_WIDTH']}, parameters)
def seeded_randint(seed, a, b):
r = random.getstate()
random.seed(seed)
n = random.randint(a, b)
random.setstate(r)
return n
class LUTCell(BaseCell): class LUTCell(BaseCell):
def __init__(self, name): def __init__(self, name, values):
super().__init__(name) super().__init__(name, ['WIDTH', 'LUT'], {'A': 'WIDTH'}, {'Y': 1}, values)
def generate_tests(self): def generate_tests(self, rnd):
for width in [4, 6, 8]: for width in self.test_values:
lut = seeded_randint(width, 0, 2**width - 1) lut = rnd(f'lut-{width}').getrandbits(2**width)
yield (f'{width}', {'WIDTH' : width, 'LUT' : lut}) yield (f'{width}', {'WIDTH' : width, 'LUT' : lut})
def write_rtlil_file(self, f, parameters):
write_rtlil_cell(f, self.name, {'A': parameters['WIDTH']}, {'Y': 1}, parameters)
class ConcatCell(BaseCell): class ConcatCell(BaseCell):
def __init__(self, name): def __init__(self, name, values):
super().__init__(name) y_width = lambda par: par['A_WIDTH'] + par['B_WIDTH']
def generate_tests(self): super().__init__(name, ['A_WIDTH', 'B_WIDTH'], {'A': 'A_WIDTH', 'B': 'B_WIDTH'}, {'Y': y_width}, values)
for (a_width, b_width) in [(16, 16), (8, 14), (20, 10)]:
yield (f'{a_width}-{b_width}', {'A_WIDTH' : a_width, 'B_WIDTH' : b_width})
def write_rtlil_file(self, f, parameters):
write_rtlil_cell(f, self.name, {'A': parameters['A_WIDTH'], 'B' : parameters['B_WIDTH']}, {'Y': parameters['A_WIDTH'] + parameters['B_WIDTH']}, parameters)
class SliceCell(BaseCell): class SliceCell(BaseCell):
def __init__(self, name): def __init__(self, name, values):
super().__init__(name) super().__init__(name, ['A_WIDTH', 'OFFSET', 'Y_WIDTH'], {'A': 'A_WIDTH'}, {'Y': 'Y_WIDTH'}, values)
def generate_tests(self):
for (a_width, offset, y_width) in [(32, 10, 15), (8, 0, 4), (10, 0, 10)]:
yield (f'{a_width}-{offset}-{y_width}', {'A_WIDTH' : a_width, 'OFFSET' : offset, 'Y_WIDTH': y_width})
def write_rtlil_file(self, f, parameters):
write_rtlil_cell(f, self.name, {'A': parameters['A_WIDTH']}, {'Y': parameters['Y_WIDTH']}, parameters)
class FailCell(BaseCell): class FailCell(BaseCell):
def __init__(self, name): def __init__(self, name):
super().__init__(name) super().__init__(name, [], {}, {})
def generate_tests(self): def generate_tests(self, rnd):
yield ('', {}) yield ('', {})
def write_rtlil_file(self, f, parameters): def write_rtlil_file(self, path, parameters):
raise Exception(f'\'{self.name}\' cell unimplemented in test generator') raise Exception(f'\'{self.name}\' cell unimplemented in test generator')
class FFCell(BaseCell):
def __init__(self, name, values):
super().__init__(name, ['WIDTH'], ['D'], ['Q'], values)
def write_rtlil_file(self, path, parameters):
from test_functional import yosys_synth
verilog_file = path.parent / 'verilog.v'
with open(verilog_file, 'w') as f:
f.write("""
module gold(
input wire clk,
input wire [{0}:0] D,
output reg [{0}:0] Q
);
always @(posedge clk)
Q <= D;
endmodule""".format(parameters['WIDTH'] - 1))
yosys_synth(verilog_file, path)
class MemCell(BaseCell):
def __init__(self, name, values):
super().__init__(name, ['DATA_WIDTH', 'ADDR_WIDTH'], {'WA': 'ADDR_WIDTH', 'RA': 'ADDR_WIDTH', 'WD': 'DATA_WIDTH'}, {'RD': 'DATA_WIDTH'}, values)
def write_rtlil_file(self, path, parameters):
from test_functional import yosys_synth
verilog_file = path.parent / 'verilog.v'
with open(verilog_file, 'w') as f:
f.write("""
module gold(
input wire clk,
input wire [{1}:0] WA,
input wire [{0}:0] WD,
output reg [{0}:0] RD
);
reg [{0}:0] mem[0:{1}];
always @(*)
RD = mem[RA];
always @(posedge clk)
mem[WA] <= WD;
endmodule""".format(parameters['DATA_WIDTH'] - 1, parameters['ADDR_WIDTH'] - 1))
yosys_synth(verilog_file, path)
binary_widths = [
# try to cover extending A operand, extending B operand, extending/truncating result
(16, 32, 48, True, True),
(16, 32, 48, False, False),
(32, 16, 48, True, True),
(32, 16, 48, False, False),
(32, 32, 16, True, True),
(32, 32, 16, False, False),
# have at least one test that checks small inputs, which will exercise the cornercases more
(4, 4, 8, True, True),
(4, 4, 8, False, False)
]
unary_widths = [
(6, 12, True),
(6, 12, False),
(32, 16, True),
(32, 16, False)
]
# note that meaningless combinations of signednesses are eliminated,
# like e.g. most shift operations don't take signed shift amounts
shift_widths = [
# one set of tests that definitely checks all possible shift amounts
# with a bigger result width to make sure it's not truncated
(32, 6, 64, True, False),
(32, 6, 64, False, False),
(32, 6, 64, True, True),
(32, 6, 64, False, True),
# one set that checks very oversized shifts
(32, 32, 64, True, False),
(32, 32, 64, False, False),
(32, 32, 64, True, True),
(32, 32, 64, False, True),
# at least one test where the result is going to be truncated
(32, 6, 16, False, False)
]
rtlil_cells = [ rtlil_cells = [
UnaryCell("not"), UnaryCell("not", unary_widths),
UnaryCell("pos"), UnaryCell("pos", unary_widths),
UnaryCell("neg"), UnaryCell("neg", unary_widths),
BinaryCell("and"), BinaryCell("and", binary_widths),
BinaryCell("or"), BinaryCell("or", binary_widths),
BinaryCell("xor"), BinaryCell("xor", binary_widths),
BinaryCell("xnor"), BinaryCell("xnor", binary_widths),
UnaryCell("reduce_and"), UnaryCell("reduce_and", unary_widths),
UnaryCell("reduce_or"), UnaryCell("reduce_or", unary_widths),
UnaryCell("reduce_xor"), UnaryCell("reduce_xor", unary_widths),
UnaryCell("reduce_xnor"), UnaryCell("reduce_xnor", unary_widths),
UnaryCell("reduce_bool"), UnaryCell("reduce_bool", unary_widths),
ShiftCell("shl"), ShiftCell("shl", shift_widths),
ShiftCell("shr"), ShiftCell("shr", shift_widths),
ShiftCell("sshl"), ShiftCell("sshl", shift_widths),
ShiftCell("sshr"), ShiftCell("sshr", shift_widths),
ShiftCell("shift"), ShiftCell("shift", shift_widths),
ShiftCell("shiftx"), ShiftCell("shiftx", shift_widths),
# ("fa", ["A", "B", "C", "X", "Y"]), # ("fa", ["A", "B", "C", "X", "Y"]),
# ("lcu", ["P", "G", "CI", "CO"]), # ("lcu", ["P", "G", "CI", "CO"]),
# ("alu", ["A", "B", "CI", "BI", "X", "Y", "CO"]), # ("alu", ["A", "B", "CI", "BI", "X", "Y", "CO"]),
BinaryCell("lt"), BinaryCell("lt", binary_widths),
BinaryCell("le"), BinaryCell("le", binary_widths),
BinaryCell("eq"), BinaryCell("eq", binary_widths),
BinaryCell("ne"), BinaryCell("ne", binary_widths),
BinaryCell("eqx"), BinaryCell("eqx", binary_widths),
BinaryCell("nex"), BinaryCell("nex", binary_widths),
BinaryCell("ge"), BinaryCell("ge", binary_widths),
BinaryCell("gt"), BinaryCell("gt", binary_widths),
BinaryCell("add"), BinaryCell("add", binary_widths),
BinaryCell("sub"), BinaryCell("sub", binary_widths),
BinaryCell("mul"), BinaryCell("mul", binary_widths),
# BinaryCell("macc"), # BinaryCell("macc"),
BinaryCell("div"), BinaryCell("div", binary_widths),
BinaryCell("mod"), BinaryCell("mod", binary_widths),
BinaryCell("divfloor"), BinaryCell("divfloor", binary_widths),
BinaryCell("modfloor"), BinaryCell("modfloor", binary_widths),
BinaryCell("pow"), BinaryCell("pow", binary_widths),
UnaryCell("logic_not"), UnaryCell("logic_not", unary_widths),
BinaryCell("logic_and"), BinaryCell("logic_and", binary_widths),
BinaryCell("logic_or"), BinaryCell("logic_or", binary_widths),
SliceCell("slice"), SliceCell("slice", [(32, 10, 15), (8, 0, 4), (10, 0, 10)]),
ConcatCell("concat"), ConcatCell("concat", [(16, 16), (8, 14), (20, 10)]),
MuxCell("mux"), MuxCell("mux", [10, 16, 40]),
BMuxCell("bmux"), BMuxCell("bmux", [(10, 1), (10, 2), (10, 4)]),
PMuxCell("pmux"), PMuxCell("pmux", [(10, 1), (10, 4), (20, 4)]),
DemuxCell("demux"), DemuxCell("demux", [(10, 1), (32, 2), (16, 4)]),
LUTCell("lut"), LUTCell("lut", [4, 6, 8]),
# ("sop", ["A", "Y"]), # ("sop", ["A", "Y"]),
# ("tribuf", ["A", "EN", "Y"]), # ("tribuf", ["A", "EN", "Y"]),
# ("specify2", ["EN", "SRC", "DST"]), # ("specify2", ["EN", "SRC", "DST"]),
# ("specify3", ["EN", "SRC", "DST", "DAT"]), # ("specify3", ["EN", "SRC", "DST", "DAT"]),
# ("specrule", ["EN_SRC", "EN_DST", "SRC", "DST"]), # ("specrule", ["EN_SRC", "EN_DST", "SRC", "DST"]),
BWCell("bweqx"), BWCell("bweqx", [10, 16, 40]),
BWCell("bwmux"), BWCell("bwmux", [10, 16, 40]),
FFCell("ff", [10, 20, 40]),
MemCell("mem", [(32, 4)])
# ("assert", ["A", "EN"]), # ("assert", ["A", "EN"]),
# ("assume", ["A", "EN"]), # ("assume", ["A", "EN"]),
# ("live", ["A", "EN"]), # ("live", ["A", "EN"]),
@ -260,12 +289,12 @@ rtlil_cells = [
# ("scopeinfo", []), # ("scopeinfo", []),
] ]
def generate_test_cases(per_cell): def generate_test_cases(per_cell, rnd):
tests = [] tests = []
names = [] names = []
for cell in rtlil_cells: for cell in rtlil_cells:
seen_names = set() seen_names = set()
for (name, parameters) in cell.generate_tests(): for (name, parameters) in cell.generate_tests(rnd):
if not name in seen_names: if not name in seen_names:
seen_names.add(name) seen_names.add(name)
tests.append((cell, parameters)) tests.append((cell, parameters))

View File

@ -1,6 +1,5 @@
import sys import sys
import argparse import argparse
import random
import os import os
import smtio import smtio
import re import re
@ -40,9 +39,10 @@ class SExprParser:
rv, self.stack[0] = self.stack[0], [] rv, self.stack[0] = self.stack[0], []
return rv return rv
def simulate_smt_with_smtio(smt_file_path, vcd_path, smt_io): def simulate_smt_with_smtio(smt_file_path, vcd_path, smt_io, num_steps, rnd):
inputs = {} inputs = {}
outputs = {} outputs = {}
states = {}
def handle_datatype(lst): def handle_datatype(lst):
print(lst) print(lst)
@ -60,6 +60,14 @@ def simulate_smt_with_smtio(smt_file_path, vcd_path, smt_io):
bitvec_size = declaration[1][2] bitvec_size = declaration[1][2]
assert output_name.startswith("gold_Outputs_") assert output_name.startswith("gold_Outputs_")
outputs[output_name[len("gold_Outputs_"):]] = int(bitvec_size) outputs[output_name[len("gold_Outputs_"):]] = int(bitvec_size)
elif datatype_name.endswith("_State"):
for declaration in declarations:
state_name = declaration[0]
assert state_name.startswith("gold_State_")
if declaration[1][0] == "_":
states[state_name[len("gold_State_"):]] = int(declaration[1][2])
else:
states[state_name[len("gold_State_"):]] = (declaration[1][1][2], declaration[1][2][2])
parser = SExprParser() parser = SExprParser()
with open(smt_file_path, 'r') as smt_file: with open(smt_file_path, 'r') as smt_file:
@ -73,25 +81,44 @@ def simulate_smt_with_smtio(smt_file_path, vcd_path, smt_io):
parser.finish() parser.finish()
assert smt_io.check_sat() == 'sat' assert smt_io.check_sat() == 'sat'
def initial_state(states):
mk_state_parts = []
rv = []
for name, width in states.items():
if isinstance(width, int):
binary_string = format(0, '0{}b'.format(width))
mk_state_parts.append(f"#b{binary_string}")
else:
binary_string = format(0, '0{}b'.format(width[1]))
rv.append(f"(declare-const test_state_initial_mem_{name} (Array (_ BitVec {width[0]}) (_ BitVec {width[1]})))")
rv.append(f"(assert (forall ((i (_ BitVec {width[0]}))) (= (select test_state_initial_mem_{name} i) #b{binary_string})))")
mk_state_parts.append(f"test_state_initial_mem_{name}")
if len(states) == 0:
mk_state_call = "gold_State"
else:
mk_state_call = "(gold_State {})".format(" ".join(mk_state_parts))
rv.append(f"(define-const test_state_step_n0 gold_State {mk_state_call})\n")
return rv
def set_step(inputs, step): def set_step(inputs, step):
# This function assumes 'inputs' is a dictionary like {"A": 5, "B": 4} # This function assumes 'inputs' is a dictionary like {"A": 5, "B": 4}
# and 'input_values' is a dictionary like {"A": 5, "B": 13} specifying the concrete values for each input. # and 'input_values' is a dictionary like {"A": 5, "B": 13} specifying the concrete values for each input.
mk_inputs_parts = [] mk_inputs_parts = []
for input_name, width in inputs.items(): for input_name, width in inputs.items():
value = random.getrandbits(width) # Generate a random number up to the maximum value for the bit size value = rnd.getrandbits(width) # Generate a random number up to the maximum value for the bit size
binary_string = format(value, '0{}b'.format(width)) # Convert value to binary with leading zeros binary_string = format(value, '0{}b'.format(width)) # Convert value to binary with leading zeros
mk_inputs_parts.append(f"#b{binary_string}") mk_inputs_parts.append(f"#b{binary_string}")
mk_inputs_call = "gold_Inputs " + " ".join(mk_inputs_parts) mk_inputs_call = "gold_Inputs " + " ".join(mk_inputs_parts)
define_inputs = f"(define-const test_inputs_step_n{step} gold_Inputs ({mk_inputs_call}))\n" return [
f"(define-const test_inputs_step_n{step} gold_Inputs ({mk_inputs_call}))\n",
f"(define-const test_results_step_n{step} (Pair gold_Outputs gold_State) (gold test_inputs_step_n{step} test_state_step_n{step}))\n",
f"(define-const test_outputs_step_n{step} gold_Outputs (first test_results_step_n{step}))\n",
f"(define-const test_state_step_n{step+1} gold_State (second test_results_step_n{step}))\n",
]
define_outputs = f"(define-const test_outputs_step_n{step} gold_Outputs (first (gold test_inputs_step_n{step} gold_State)))\n" smt_commands = initial_state(states)
smt_commands = [define_inputs, define_outputs]
return smt_commands
num_steps = 1000
smt_commands = []
for step in range(num_steps): for step in range(num_steps):
for step_command in set_step(inputs, step): for step_command in set_step(inputs, step):
smt_commands.append(step_command) smt_commands.append(step_command)
@ -168,13 +195,13 @@ def simulate_smt_with_smtio(smt_file_path, vcd_path, smt_io):
write_vcd(vcd_path, signals) write_vcd(vcd_path, signals)
def simulate_smt(smt_file_path, vcd_path): def simulate_smt(smt_file_path, vcd_path, num_steps, rnd):
so = smtio.SmtOpts() so = smtio.SmtOpts()
so.solver = "z3" so.solver = "z3"
so.logic = "BV" so.logic = "ABV"
so.debug_print = True so.debug_print = True
smt_io = smtio.SmtIo(opts=so) smt_io = smtio.SmtIo(opts=so)
try: try:
simulate_smt_with_smtio(smt_file_path, vcd_path, smt_io) simulate_smt_with_smtio(smt_file_path, vcd_path, smt_io, num_steps, rnd)
finally: finally:
smt_io.p_close() smt_io.p_close()

View File

@ -6,11 +6,14 @@ from pathlib import Path
base_path = Path(__file__).resolve().parent.parent.parent base_path = Path(__file__).resolve().parent.parent.parent
# quote a string or pathlib path so that it can be used by bash or yosys
# TODO: is this really appropriate for yosys?
def quote(path): def quote(path):
return shlex.quote(str(path)) return shlex.quote(str(path))
# run a shell command and require the return code to be 0
def run(cmd, **kwargs): def run(cmd, **kwargs):
print(' '.join([shlex.quote(str(x)) for x in cmd])) print(' '.join([quote(x) for x in cmd]))
status = subprocess.run(cmd, **kwargs) status = subprocess.run(cmd, **kwargs)
assert status.returncode == 0, f"{cmd[0]} failed" assert status.returncode == 0, f"{cmd[0]} failed"
@ -20,7 +23,24 @@ def yosys(script):
def compile_cpp(in_path, out_path, args): def compile_cpp(in_path, out_path, args):
run(['g++', '-g', '-std=c++17'] + args + [str(in_path), '-o', str(out_path)]) run(['g++', '-g', '-std=c++17'] + args + [str(in_path), '-o', str(out_path)])
def test_cxx(cell, parameters, tmp_path): def yosys_synth(verilog_file, rtlil_file):
yosys(f"read_verilog {quote(verilog_file)} ; prep ; clk2fflogic ; write_rtlil {quote(rtlil_file)}")
# simulate an rtlil file with yosys, comparing with a given vcd file, and writing out the yosys simulation results into a second vcd file
def yosys_sim(rtlil_file, vcd_reference_file, vcd_out_file):
try:
yosys(f"read_rtlil {quote(rtlil_file)}; sim -r {quote(vcd_reference_file)} -scope gold -vcd {quote(vcd_out_file)} -timescale 1us -sim-gold")
except:
# if yosys sim fails it's probably because of a simulation mismatch
# since yosys sim aborts on simulation mismatch to generate vcd output
# we have to re-run with a different set of flags
# on this run we ignore output and return code, we just want a best-effort attempt to get a vcd
subprocess.run([base_path / 'yosys', '-Q', '-p',
f'read_rtlil {quote(rtlil_file)}; sim -vcd {quote(vcd_out_file)} -a -r {quote(vcd_reference_file)} -scope gold -timescale 1us'],
capture_output=True, check=False)
raise
def test_cxx(cell, parameters, tmp_path, num_steps, rnd):
rtlil_file = tmp_path / 'rtlil.il' rtlil_file = tmp_path / 'rtlil.il'
vcdharness_cc_file = base_path / 'tests/functional/vcd_harness.cc' vcdharness_cc_file = base_path / 'tests/functional/vcd_harness.cc'
cc_file = tmp_path / 'my_module_functional_cxx.cc' cc_file = tmp_path / 'my_module_functional_cxx.cc'
@ -28,20 +48,14 @@ def test_cxx(cell, parameters, tmp_path):
vcd_functional_file = tmp_path / 'functional.vcd' vcd_functional_file = tmp_path / 'functional.vcd'
vcd_yosys_sim_file = tmp_path / 'yosys.vcd' vcd_yosys_sim_file = tmp_path / 'yosys.vcd'
with open(rtlil_file, 'w') as f: cell.write_rtlil_file(rtlil_file, parameters)
cell.write_rtlil_file(f, parameters)
yosys(f"read_rtlil {quote(rtlil_file)} ; write_functional_cxx {quote(cc_file)}") yosys(f"read_rtlil {quote(rtlil_file)} ; write_functional_cxx {quote(cc_file)}")
compile_cpp(vcdharness_cc_file, vcdharness_exe_file, ['-I', tmp_path, '-I', str(base_path / 'backends/functional/cxx_runtime')]) compile_cpp(vcdharness_cc_file, vcdharness_exe_file, ['-I', tmp_path, '-I', str(base_path / 'backends/functional/cxx_runtime')])
run([str(vcdharness_exe_file.resolve()), str(vcd_functional_file)]) seed = str(rnd(cell.name + "-cxx").getrandbits(32))
try: run([str(vcdharness_exe_file.resolve()), str(vcd_functional_file), str(num_steps), str(seed)])
yosys(f"read_rtlil {quote(rtlil_file)}; sim -r {quote(vcd_functional_file)} -scope gold -vcd {quote(vcd_yosys_sim_file)} -timescale 1us -sim-gold") yosys_sim(rtlil_file, vcd_functional_file, vcd_yosys_sim_file)
except:
subprocess.run([base_path / 'yosys', '-Q', '-p',
f'read_rtlil {quote(rtlil_file)}; sim -vcd {quote(vcd_yosys_sim_file)} -r {quote(vcd_functional_file)} -scope gold -timescale 1us'],
capture_output=True, check=False)
raise
def test_smt(cell, parameters, tmp_path): def test_smt(cell, parameters, tmp_path, num_steps, rnd):
import smt_vcd import smt_vcd
rtlil_file = tmp_path / 'rtlil.il' rtlil_file = tmp_path / 'rtlil.il'
@ -49,15 +63,8 @@ def test_smt(cell, parameters, tmp_path):
vcd_functional_file = tmp_path / 'functional.vcd' vcd_functional_file = tmp_path / 'functional.vcd'
vcd_yosys_sim_file = tmp_path / 'yosys.vcd' vcd_yosys_sim_file = tmp_path / 'yosys.vcd'
with open(rtlil_file, 'w') as f: cell.write_rtlil_file(rtlil_file, parameters)
cell.write_rtlil_file(f, parameters)
yosys(f"read_rtlil {quote(rtlil_file)} ; write_functional_smt2 {quote(smt_file)}") yosys(f"read_rtlil {quote(rtlil_file)} ; write_functional_smt2 {quote(smt_file)}")
run(['z3', smt_file]) run(['z3', smt_file]) # check if output is valid smtlib before continuing
smt_vcd.simulate_smt(smt_file, vcd_functional_file) smt_vcd.simulate_smt(smt_file, vcd_functional_file, num_steps, rnd(cell.name + "-smt"))
try: yosys_sim(rtlil_file, vcd_functional_file, vcd_yosys_sim_file)
yosys(f"read_rtlil {quote(rtlil_file)}; sim -r {quote(vcd_functional_file)} -scope gold -vcd {quote(vcd_yosys_sim_file)} -timescale 1us -sim-gold")
except:
subprocess.run([base_path / 'yosys', '-Q', '-p',
f'read_rtlil {quote(rtlil_file)}; sim -vcd {quote(vcd_yosys_sim_file)} -r {quote(vcd_functional_file)} -scope gold -timescale 1us'],
capture_output=True, check=False)
raise

View File

@ -2,15 +2,46 @@
#include <iostream> #include <iostream>
#include <fstream> #include <fstream>
#include <random> #include <random>
#include <ctype.h>
#include <vector>
#include "my_module_functional_cxx.cc" #include "my_module_functional_cxx.cc"
std::string vcd_name_mangle(std::string name) {
std::string ret = name;
bool escape = ret.empty() || !isalpha(ret[0]) && ret[0] != '_';
for(size_t i = 0; i < ret.size(); i++) {
if(isspace(ret[i])) ret[i] = '_';
if(!isalnum(ret[i]) && ret[i] != '_' && ret[i] != '$')
escape = true;
}
if(escape)
return "\\" + ret;
else
return ret;
}
std::unordered_map<std::string, std::string> codes;
struct DumpHeader { struct DumpHeader {
std::ofstream &ofs; std::ofstream &ofs;
std::string code = "!";
DumpHeader(std::ofstream &ofs) : ofs(ofs) {} DumpHeader(std::ofstream &ofs) : ofs(ofs) {}
void increment_code() {
for(size_t i = 0; i < code.size(); i++)
if(code[i]++ == '~')
code[i] = '!';
else
return;
code.push_back('!');
}
template <size_t n> template <size_t n>
void operator()(const char *name, Signal<n> value) { void operator()(const char *name, Signal<n> value) {
ofs << "$var wire " << n << " " << name[0] << " " << name << " $end\n"; ofs << "$var wire " << n << " " << code << " " << vcd_name_mangle(name) << " $end\n";
codes[name] = code;
increment_code();
}
template <size_t n, size_t m>
void operator()(const char *name, Memory<n, m> value) {
} }
}; };
@ -22,14 +53,17 @@ struct Dump {
// Bit // Bit
if (n == 1) { if (n == 1) {
ofs << (value[0] ? '1' : '0'); ofs << (value[0] ? '1' : '0');
ofs << name[0] << "\n"; ofs << codes[name] << "\n";
return; return;
} }
// vector (multi-bit) signals // vector (multi-bit) signals
ofs << "b"; ofs << "b";
for (size_t i = n; i-- > 0;) for (size_t i = n; i-- > 0;)
ofs << (value[i] ? '1' : '0'); ofs << (value[i] ? '1' : '0');
ofs << " " << name[0] << "\n"; ofs << " " << codes[name] << "\n";
}
template <size_t n, size_t m>
void operator()(const char *name, Memory<n, m> value) {
} }
}; };
@ -61,14 +95,15 @@ struct Randomize {
int main(int argc, char **argv) int main(int argc, char **argv)
{ {
if (argc != 2) { if (argc != 4) {
std::cerr << "Usage: " << argv[0] << " <functional_vcd_filename>\n"; std::cerr << "Usage: " << argv[0] << " <functional_vcd_filename> <steps> <seed>\n";
return 1; return 1;
} }
const std::string functional_vcd_filename = argv[1]; const std::string functional_vcd_filename = argv[1];
const int steps = atoi(argv[2]);
const uint32_t seed = atoi(argv[3]);
constexpr int steps = 1000;
constexpr int number_timescale = 1; constexpr int number_timescale = 1;
const std::string units_timescale = "us"; const std::string units_timescale = "us";
gold::Inputs inputs; gold::Inputs inputs;
@ -87,27 +122,12 @@ int main(int argc, char **argv)
state.visit(d); state.visit(d);
} }
vcd_file << "$enddefinitions $end\n$dumpvars\n"; vcd_file << "$enddefinitions $end\n$dumpvars\n";
vcd_file << "#0\n"; std::mt19937 gen(seed);
// Set all signals to false
inputs.visit(Reset()); inputs.visit(Reset());
gold::eval(inputs, outputs, state, next_state);
{
Dump d(vcd_file);
inputs.visit(d);
outputs.visit(d);
state.visit(d);
}
// Initialize random number generator once
std::random_device rd;
std::mt19937 gen(rd());
for (int step = 0; step < steps; ++step) { for (int step = 0; step < steps; ++step) {
// Functional backend cxx vcd_file << "#" << step << "\n";
vcd_file << "#" << (step + 1) << "\n";
inputs.visit(Randomize(gen));
gold::eval(inputs, outputs, state, next_state); gold::eval(inputs, outputs, state, next_state);
{ {
Dump d(vcd_file); Dump d(vcd_file);
@ -117,6 +137,7 @@ int main(int argc, char **argv)
} }
state = next_state; state = next_state;
inputs.visit(Randomize(gen));
} }
vcd_file.close(); vcd_file.close();