diff --git a/tests/functional/rkt_vcd.py b/tests/functional/rkt_vcd.py index 1b2cf31e3..f06c2dc27 100644 --- a/tests/functional/rkt_vcd.py +++ b/tests/functional/rkt_vcd.py @@ -43,21 +43,37 @@ def write_vcd(filename: Path, signals: SignalStepMap, timescale='1 ns', date='to if change_time == time: f.write(f"{value} {signal_name}\n") -def simulate_rosette(rkt_file_path: Path, vcd_path: Path, num_steps: int, rnd: Random): + +def simulate_rosette( + rkt_file_path: Path, + vcd_path: Path, + num_steps: int, + rnd: Random, + use_assoc_list_helpers: bool = False, +): + """ + Args: + - use_assoc_list_helpers: If True, will use the association list helpers + in the Racket file. The file should have been generated with the + -assoc-list-helpers flag in the yosys command. + """ signals: dict[str, list[str]] = {} inputs: SignalWidthMap = {} outputs: SignalWidthMap = {} current_struct_name: str = "" - with open(rkt_file_path, 'r') as rkt_file: + with open(rkt_file_path, "r") as rkt_file: for line in rkt_file: - m = re.search(r'gold_(Inputs|Outputs|State)', line) + m = re.search(r"gold_(Inputs|Outputs|State)", line) if m: current_struct_name = m.group(1) - if current_struct_name == "State": break - elif not current_struct_name: continue # skip lines before structs - m = re.search(r'; (.+?)\b \(bitvector (\d+)\)', line) - if not m: continue # skip non matching lines (probably closing the struct) + if current_struct_name == "State": + break + elif not current_struct_name: + continue # skip lines before structs + m = re.search(r"; (.+?)\b \(bitvector (\d+)\)", line) + if not m: + continue # skip non matching lines (probably closing the struct) signal = m.group(1) width = int(m.group(2)) if current_struct_name == "Inputs": @@ -69,43 +85,86 @@ def simulate_rosette(rkt_file_path: Path, vcd_path: Path, num_steps: int, rnd: R step_list: list[int] = [] for step in range(num_steps): value = rnd.getrandbits(width) - binary_string = format(value, '0{}b'.format(width)) + binary_string = format(value, "0{}b".format(width)) step_list.append(binary_string) signals[signal] = step_list - test_rkt_file_path = rkt_file_path.with_suffix('.tst.rkt') - with open(test_rkt_file_path, 'w') as test_rkt_file: - test_rkt_file.writelines([ - '#lang rosette\n', - f'(require "{rkt_file_path.name}")\n', - ]) + test_rkt_file_path = rkt_file_path.with_suffix(".tst.rkt") + with open(test_rkt_file_path, "w") as test_rkt_file: + test_rkt_file.writelines( + [ + "#lang rosette\n", + f'(require "{rkt_file_path.name}")\n', + ] + ) for step in range(num_steps): this_step = f"step_{step}" value_list: list[str] = [] - for signal, width in inputs.items(): - value = signals[signal][step] - value_list.append(f"(bv #b{value} {width})") - gold_Inputs = f"(gold_Inputs {' '.join(value_list)})" + if use_assoc_list_helpers: + # Generate inputs as a list of cons pairs making up the + # association list. + for signal, width in inputs.items(): + value = signals[signal][step] + value_list.append(f'(cons "{signal}" (bv #b{value} {width}))') + else: + # Otherwise, we generate the inputs as a list of bitvectors. + for signal, width in inputs.items(): + value = signals[signal][step] + value_list.append(f"(bv #b{value} {width})") + gold_Inputs = ( + f"(gold_inputs_helper (list {' '.join(value_list)}))" + if use_assoc_list_helpers + else f"(gold_Inputs {' '.join(value_list)})" + ) gold_State = f"(cdr step_{step-1})" if step else "gold_initial" - test_rkt_file.write(f"(define {this_step} (gold {gold_Inputs} {gold_State})) (car {this_step})\n") + get_value_expr = ( + f"(gold_outputs_helper (car {this_step}))" + if use_assoc_list_helpers + else f"(car {this_step})" + ) + test_rkt_file.write( + f"(define {this_step} (gold {gold_Inputs} {gold_State})) {get_value_expr}\n" + ) + cmd = ["racket", test_rkt_file_path] - status = subprocess.run(cmd, capture_output=True) - assert status.returncode == 0, f"{cmd[0]} failed" + try: + status = subprocess.run(cmd, capture_output=True, check=True) + except subprocess.CalledProcessError as e: + raise RuntimeError( + f"Racket simulation failed with command: {cmd}\n" + f"Error: {e.stderr.decode()}" + ) from e for signal in outputs.keys(): signals[signal] = [] for line in status.stdout.decode().splitlines(): - m = re.match(r'\(gold_Outputs( \(bv \S+ \d+\))+\)', line) + m = ( + re.match(r"\(list( \(cons \"\S+\" \(bv \S+ \d+\)\))+\)", line) + if use_assoc_list_helpers + else re.match(r"\(gold_Outputs( \(bv \S+ \d+\))+\)", line) + ) assert m, f"Incomplete output definition {line!r}" - for output, (value, width) in zip(outputs.keys(), re.findall(r'\(bv (\S+) (\d+)\)', line)): + outputs_values_and_widths = ( + { + output: re.findall( + r"\(cons \"" + output + r"\" \(bv (\S+) (\d+)\)\)", line + )[0] + for output in outputs.keys() + }.items() + if use_assoc_list_helpers + else zip(outputs.keys(), re.findall(r"\(bv (\S+) (\d+)\)", line)) + ) + for output, (value, width) in outputs_values_and_widths: assert isinstance(value, str), f"Bad value {value!r}" - assert value.startswith(('#b', '#x')), f"Non-binary value {value!r}" - assert int(width) == outputs[output], f"Width mismatch for output {output!r} (got {width}, expected {outputs[output]})" - int_value = int(value[2:], 16 if value.startswith('#x') else 2) - binary_string = format(int_value, '0{}b'.format(width)) + assert value.startswith(("#b", "#x")), f"Non-binary value {value!r}" + assert ( + int(width) == outputs[output] + ), f"Width mismatch for output {output!r} (got {width}, expected {outputs[output]})" + int_value = int(value[2:], 16 if value.startswith("#x") else 2) + binary_string = format(int_value, "0{}b".format(width)) signals[output].append(binary_string) vcd_signals: SignalStepMap = {} diff --git a/tests/functional/simulate_rosette.rkt b/tests/functional/simulate_rosette.rkt deleted file mode 100644 index aa838a5a0..000000000 --- a/tests/functional/simulate_rosette.rkt +++ /dev/null @@ -1,157 +0,0 @@ -; Utilities for simulating Rosette programs. -; -; Tests can be run with `raco test `. -#lang racket/base - -(provide simulate-rosette) - -(require (only-in rosette bv) - racket/list) - -; Inputs: -; - function: The function for the module to simulate. This should be a Rosette function generated by -; Yosys's `write_fuctional_rosette` backend. -; - initial-state: The initial state of the module, as generated by Yosys's `write_fuctional_rosette` -; backend. -; - inputs: A list of association lists. The function will be called with each association list as -; inputs, and the state will be threaded through each call. -; -; Outputs: -; - A list of outputs, one for each cycle. The outputs are a list of the output objects generated by -; `function`. -(define (simulate-rosette #:function function #:initial-state initial-state #:inputs inputs) - - (define outputs-and-states - (drop (reverse (foldl (lambda (input acc) - (let* ([outputs (function input (cdr (car acc)))]) (cons outputs acc))) - (list (cons 'unused initial-state)) - inputs)) - 1)) - - (define outputs (map car outputs-and-states)) - - outputs) - -; Inputs: -; - inputs: association list mapping string name to bitwidth. -; - num-inputs: number of inputs to generate. -; TODO(@gussmith23): If `num-inputs` is more than the number of possible values, just enumerate. -(define (generate-inputs #:inputs inputs #:num-inputs num-inputs) - (define (generate-random-input inputs) - (map (lambda (pair) (cons (car pair) (bv (random (expt 2 (cdr pair))) (cdr pair)))) inputs)) - (for/list ([_ (range num-inputs)]) - (generate-random-input inputs))) - -; Generates a clock signal for the given inputs. -; -; Given a string of inputs, one per clock cycle, this function generates a clock signal alongside the -; inputs. It does so by alternating the clock signal between 0 and 1 for each cycle, starting with 0. -; For example, if the inputs are (list inputs1 inputs2 inputs3), the output will be (list (cons (cons -; "clk" (bv 0 1)) inputs1) (cons (cons "clk" (bv 1 1)) inputs1) (cons (cons "clk" (bv 0 1)) inputs2) -; (cons (cons "clk" (bv 1 1)) inputs2) ... ). -; -; Inputs: -; - clock-name: The name of the clock signal. -; - inputs: A list of inputs in association list form, as output by `generate-inputs`. -; -; Outputs: -; - A list of association lists, each containing a new clock signal. Will be twice the length of the -; inputs list. -(define (generate-clock #:clock-name clock-name #:inputs inputs) - (apply append - (for/list ([this-cycle-inputs inputs]) - (list (cons (cons clock-name (bv 0 1)) this-cycle-inputs) - (cons (cons clock-name (bv 1 1)) this-cycle-inputs))))) - -; This is what gets executed when the script is run. -(module main racket/base - (require racket/cmdline) - - ; - input-helper, output-helper: association-list-based helpers for input and output struct, generated - ; by Yosys's `write_fuctional_rosette` backend with `-assoc-list-helpers` enabled. - ) - -(module+ test - (require rackunit - (only-in rosette bv bvadd)) - (test-case "generate-inputs" - (check-equal? (length (generate-inputs #:inputs (list (cons "input1" 4)) #:num-inputs 10)) 10) - ; Check that this call generates a list of one-length lists, each containing a single association - ; list with the key "input1" and a random value. - (check-true (foldl (lambda (input acc) - (and acc (equal? (length input) 1) (equal? (car (first input)) "input1"))) - #t - (generate-inputs #:inputs (list (cons "input1" 4)) #:num-inputs 10)))) - - (test-case "generate-clock" - (define inputs - (list (list (cons "input1" (bv 4 4)) (cons "input2" (bv 3 3)) (cons "input3" (bv 2 2))) - (list (cons "input1" (bv 3 4)) (cons "input2" (bv 4 3)) (cons "input3" (bv 1 2))) - (list (cons "input1" (bv 2 4)) (cons "input2" (bv 5 3)) (cons "input3" (bv 0 2))))) - - (check-equal? (length (generate-clock #:clock-name "clk" #:inputs inputs)) 6) - - (check-equal? - (generate-clock #:clock-name "clk" #:inputs inputs) - (list (cons (cons "clk" (bv 0 1)) - (list (cons "input1" (bv 4 4)) (cons "input2" (bv 3 3)) (cons "input3" (bv 2 2)))) - (cons (cons "clk" (bv 1 1)) - (list (cons "input1" (bv 4 4)) (cons "input2" (bv 3 3)) (cons "input3" (bv 2 2)))) - (cons (cons "clk" (bv 0 1)) - (list (cons "input1" (bv 3 4)) (cons "input2" (bv 4 3)) (cons "input3" (bv 1 2)))) - (cons (cons "clk" (bv 1 1)) - (list (cons "input1" (bv 3 4)) (cons "input2" (bv 4 3)) (cons "input3" (bv 1 2)))) - (cons (cons "clk" (bv 0 1)) - (list (cons "input1" (bv 2 4)) (cons "input2" (bv 5 3)) (cons "input3" (bv 0 2)))) - (cons (cons "clk" (bv 1 1)) - (list (cons "input1" (bv 2 4)) (cons "input2" (bv 5 3)) (cons "input3" (bv 0 2))))))) - (test-case "simulate-rosette" - - ; This function will take association lists as inputs, so the helper function is simply identity. - ; This is not generally true of Yosys-generated code. Similarly, this function uses an association - ; list for state, which is not what Yosys generates, but it's easier for testing. - ; - ; A one-stage adder. Inputs are registered in one clock cycle, and the output is the sum of the - ; two registered inputs. - (define (module-function inputs state) - (let* ([a (cdr (assoc "a" inputs))] - [b (cdr (assoc "b" inputs))] - [clk (cdr (assoc "clk" inputs))] - [old-clk (cdr (assoc "clk" state))] - [prev-a (cdr (assoc "prev-a" state))] - [prev-b (cdr (assoc "prev-b" state))] - [a-reg (cdr (assoc "a-reg" state))] - [b-reg (cdr (assoc "b-reg" state))] - [clk-ticked (and (equal? clk (bv 1 1)) (equal? old-clk (bv 0 1)))] - [new-a-reg (if clk-ticked prev-a a-reg)] - [new-b-reg (if clk-ticked prev-b b-reg)] - [out (list (cons "o" (bvadd new-a-reg new-b-reg)))] - [new-state (list (cons "prev-a" a) - (cons "a-reg" new-a-reg) - (cons "prev-b" b) - (cons "b-reg" new-b-reg) - (cons "clk" clk))]) - (cons out new-state))) - - (define outputs - (simulate-rosette #:function module-function - #:initial-state (list (cons "a-reg" (bv 0 4)) - (cons "b-reg" (bv 0 4)) - (cons "prev-a" (bv 0 4)) - (cons "prev-b" (bv 0 4)) - (cons "clk" (bv 0 1))) - #:inputs - (list (list (cons "clk" (bv 0 1)) (cons "a" (bv 4 4)) (cons "b" (bv 4 4))) - (list (cons "clk" (bv 1 1)) (cons "a" (bv 3 4)) (cons "b" (bv 0 4))) - (list (cons "clk" (bv 0 1)) (cons "a" (bv 10 4)) (cons "b" (bv 9 4))) - (list (cons "clk" (bv 1 1)) (cons "a" (bv 2 4)) (cons "b" (bv -1 4))) - (list (cons "clk" (bv 0 1)) (cons "a" (bv 4 4)) (cons "b" (bv -15 4))) - (list (cons "clk" (bv 1 1)) (cons "a" (bv 0 4)) (cons "b" (bv 0 4)))))) - - (check-equal? outputs - (list (list (cons "o" (bv 0 4))) - (list (cons "o" (bv 8 4))) - (list (cons "o" (bv 8 4))) - (list (cons "o" (bv 3 4))) - (list (cons "o" (bv 3 4))) - (list (cons "o" (bv -11 4))))))) diff --git a/tests/functional/test_functional.py b/tests/functional/test_functional.py index 7a09966d8..86eabef1e 100644 --- a/tests/functional/test_functional.py +++ b/tests/functional/test_functional.py @@ -74,7 +74,8 @@ def test_smt(cell, parameters, tmp_path, num_steps, rnd): yosys_sim(rtlil_file, vcd_functional_file, vcd_yosys_sim_file, getattr(cell, 'sim_preprocessing', '')) @pytest.mark.rkt -def test_rkt(cell, parameters, tmp_path, num_steps, rnd): +@pytest.mark.parametrize("use_assoc_list_helpers", [True, False]) +def test_rkt(cell, parameters, tmp_path, num_steps, rnd, use_assoc_list_helpers): import rkt_vcd rtlil_file = tmp_path / 'rtlil.il' @@ -83,8 +84,9 @@ def test_rkt(cell, parameters, tmp_path, num_steps, rnd): vcd_yosys_sim_file = tmp_path / 'yosys.vcd' cell.write_rtlil_file(rtlil_file, parameters) - yosys(f"read_rtlil {quote(rtlil_file)} ; clk2fflogic ; write_functional_rosette -provides {quote(rkt_file)}") - rkt_vcd.simulate_rosette(rkt_file, vcd_functional_file, num_steps, rnd(cell.name + "-rkt")) + use_assoc_helpers_flag = '-assoc-list-helpers' if use_assoc_list_helpers else '' + yosys(f"read_rtlil {quote(rtlil_file)} ; clk2fflogic ; write_functional_rosette -provides {use_assoc_helpers_flag} {quote(rkt_file)}") + rkt_vcd.simulate_rosette(rkt_file, vcd_functional_file, num_steps, rnd(cell.name + "-rkt"), use_assoc_list_helpers=use_assoc_list_helpers) yosys_sim(rtlil_file, vcd_functional_file, vcd_yosys_sim_file, getattr(cell, 'sim_preprocessing', '')) def test_print_graph(tmp_path):