diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..4025950e --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,14 @@ +name: ci +on: [push, pull_request] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - uses: YosysHQ/setup-oss-cad-suite@v3 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + - name: Run checks + run: pip install xmlschema && make ci diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 00000000..888cbb35 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,28 @@ +name: "CodeQL" + +on: + workflow_dispatch: + schedule: + - cron: '0 2 * * *' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: python + queries: security-extended,security-and-quality + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 diff --git a/.readthedocs.yaml b/.readthedocs.yaml index c42f4736..b72a370a 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -3,6 +3,11 @@ version: 2 +build: + os: ubuntu-22.04 + tools: + python: '3.11' + formats: - pdf diff --git a/Makefile b/Makefile index 3b58d87f..28f05657 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ DESTDIR = -PREFIX = /usr/local +PREFIX ?= /usr/local PROGRAM_PREFIX = # On Windows, manually setting absolute path to Python binary may be required @@ -10,6 +10,8 @@ ifeq ($(OS), Windows_NT) PYTHON = $(shell cygpath -w -m $(PREFIX)/bin/python3) endif +.PHONY: help install ci test html clean + help: @echo "" @echo "sudo make install" @@ -19,7 +21,11 @@ help: @echo " build documentation in docs/build/html/" @echo "" @echo "make test" - @echo " run examples" + @echo " run tests, skipping tests with missing dependencies" + @echo "" + @echo "make ci" + @echo " run all tests, failing tests with missing dependencies" + @echo " note: this requires a full Tabby CAD Suite or OSS CAD Suite install" @echo "" @echo "make clean" @echo " cleanup" @@ -39,17 +45,22 @@ else chmod +x $(DESTDIR)$(PREFIX)/bin/sby endif -ci: \ - test_demo1 test_demo2 test_demo3 \ - test_abstract_abstr test_abstract_props \ - test_demos_fib_cover test_demos_fib_prove test_demos_fib_live \ - test_multiclk_dpmem \ - test_puzzles_djb2hash test_puzzles_pour853to4 test_puzzles_wolfgoatcabbage \ - test_puzzles_primegen_primegen test_puzzles_primegen_primes_pass test_puzzles_primegen_primes_fail \ - test_quickstart_demo test_quickstart_cover test_quickstart_prove test_quickstart_memory \ - run_tests +.PHONY: check_cad_suite run_ci + +ci: check_cad_suite + @$(MAKE) run_ci + +run_ci: + $(MAKE) test NOSKIP=1 if yosys -qp 'read -verific' 2> /dev/null; then set -x; \ - YOSYS_NOVERIFIC=1 $(MAKE) ci; \ + YOSYS_NOVERIFIC=1 $(MAKE) run_ci; \ + fi + +check_cad_suite: + @if ! which tabbypip >/dev/null 2>&1; then \ + echo "'make ci' requries the Tabby CAD Suite or the OSS CAD Suite"; \ + echo "try 'make test' instead or run 'make run_ci' to proceed anyway."; \ + exit 1; \ fi test_demo1: @@ -61,59 +72,7 @@ test_demo2: test_demo3: cd sbysrc && python3 sby.py -f demo3.sby -test_abstract_abstr: - @if yosys -qp 'read -verific' 2> /dev/null; then set -x; \ - cd docs/examples/abstract && python3 ../../../sbysrc/sby.py -f abstr.sby; \ - else echo "skipping $@"; fi - -test_abstract_props: - if yosys -qp 'read -verific' 2> /dev/null; then set -x; \ - cd docs/examples/abstract && python3 ../../../sbysrc/sby.py -f props.sby; \ - else echo "skipping $@"; fi - -test_demos_fib_cover: - cd docs/examples/demos && python3 ../../../sbysrc/sby.py -f fib.sby cover - -test_demos_fib_prove: - cd docs/examples/demos && python3 ../../../sbysrc/sby.py -f fib.sby prove - -test_demos_fib_live: - cd docs/examples/demos && python3 ../../../sbysrc/sby.py -f fib.sby live - -test_multiclk_dpmem: - cd docs/examples/multiclk && python3 ../../../sbysrc/sby.py -f dpmem.sby - -test_puzzles_djb2hash: - cd docs/examples/puzzles && python3 ../../../sbysrc/sby.py -f djb2hash.sby - -test_puzzles_pour853to4: - cd docs/examples/puzzles && python3 ../../../sbysrc/sby.py -f pour_853_to_4.sby - -test_puzzles_wolfgoatcabbage: - cd docs/examples/puzzles && python3 ../../../sbysrc/sby.py -f wolf_goat_cabbage.sby - -test_puzzles_primegen_primegen: - cd docs/examples/puzzles && python3 ../../../sbysrc/sby.py -f primegen.sby primegen - -test_puzzles_primegen_primes_pass: - cd docs/examples/puzzles && python3 ../../../sbysrc/sby.py -f primegen.sby primes_pass - -test_puzzles_primegen_primes_fail: - cd docs/examples/puzzles && python3 ../../../sbysrc/sby.py -f primegen.sby primes_fail - -test_quickstart_demo: - cd docs/examples/quickstart && python3 ../../../sbysrc/sby.py -f demo.sby - -test_quickstart_cover: - cd docs/examples/quickstart && python3 ../../../sbysrc/sby.py -f cover.sby - -test_quickstart_prove: - cd docs/examples/quickstart && python3 ../../../sbysrc/sby.py -f prove.sby - -test_quickstart_memory: - cd docs/examples/quickstart && python3 ../../../sbysrc/sby.py -f memory.sby - -run_tests: +test: $(MAKE) -C tests test html: diff --git a/docs/examples/Makefile b/docs/examples/Makefile new file mode 100644 index 00000000..5cffc833 --- /dev/null +++ b/docs/examples/Makefile @@ -0,0 +1,3 @@ +SUBDIR=../docs/examples +TESTDIR=../../tests +include $(TESTDIR)/make/subdir.mk diff --git a/docs/examples/abstract/Makefile b/docs/examples/abstract/Makefile new file mode 100644 index 00000000..d456aab6 --- /dev/null +++ b/docs/examples/abstract/Makefile @@ -0,0 +1,3 @@ +SUBDIR=../docs/examples/abstract +TESTDIR=../../../tests +include $(TESTDIR)/make/subdir.mk diff --git a/docs/examples/autotune/README.md b/docs/examples/autotune/README.md new file mode 100644 index 00000000..3cdadb72 --- /dev/null +++ b/docs/examples/autotune/README.md @@ -0,0 +1,30 @@ +# Autotune demo + +This directory contains a simple sequential integer divider circuit. The +verilog implementation in [divider.sv](divider.sv) comes with assertions that +this circuit will always produce the correct result and will always finish +within a fixed number of cycles. The circuit has the divider bit-width as +parameter. + +Increasing the WIDTH parameter quickly turns proving those assertions into a +very difficult proof for fully autmated solvers. This makes it a good example +for the `--autotune` option which tries different backend engines to find the +best performing engine configuration for a given verification task. + +The [divider.sby](divider.sby) file defines 3 tasks named `small`, `medium` and +`large` which configure the divider with different bit-widths. To verify the +`small` divider using the default engine run: + + sby -f divider.sby small + +To automatically try different backend engines using autotune, run + + sby --autotune -f divider.sby small + +The `small` task should finish quickly using both the default engine and using +autotune. The `medium` and `large` tasks take significantly longer and show +greater differences between engine configurations. Note that the `large` tasks +can take many minutes to hours, depending on the machine you are using. + +You can learn more about Sby's autotune feature from [Sby's +documentation](https://symbiyosys.readthedocs.io/en/latest/autotune.html). diff --git a/docs/examples/autotune/divider.sby b/docs/examples/autotune/divider.sby new file mode 100644 index 00000000..61bed9a5 --- /dev/null +++ b/docs/examples/autotune/divider.sby @@ -0,0 +1,24 @@ +[tasks] +small default +medium +large + +[options] +mode prove +small: depth 11 +medium: depth 15 +large: depth 19 + +[engines] +smtbmc + +[script] +small: read -define WIDTH=8 +medium: read -define WIDTH=12 +large: read -define WIDTH=16 + +read -formal divider.sv +prep -top divider + +[files] +divider.sv diff --git a/docs/examples/autotune/divider.sv b/docs/examples/autotune/divider.sv new file mode 100644 index 00000000..b2ec2add --- /dev/null +++ b/docs/examples/autotune/divider.sv @@ -0,0 +1,85 @@ +`ifndef WIDTH +`define WIDTH 4 +`endif + +module divider #( + parameter WIDTH=`WIDTH +) ( + input wire clk, + input wire start, + input wire [WIDTH-1:0] dividend, + input wire [WIDTH-1:0] divisor, + + output reg done, + output reg [WIDTH-1:0] quotient, + output wire [WIDTH-1:0] remainder +); + + reg [WIDTH-1:0] acc; + + reg [WIDTH*2-1:0] sub; + reg [WIDTH-1:0] pos; + + assign remainder = acc; + + always @(posedge clk) begin + if (start) begin + acc <= dividend; + quotient <= 0; + sub <= divisor << (WIDTH - 1); + pos <= 1 << (WIDTH - 1); + done <= 0; + end else if (!done) begin + if (acc >= sub) begin + acc <= acc - sub[WIDTH-1:0]; + quotient <= quotient + pos; + end + + sub <= sub >> 1; + {pos, done} <= pos; + end + end + + +`ifdef FORMAL + reg [WIDTH-1:0] start_dividend = 0; + reg [WIDTH-1:0] start_divisor = 0; + + reg started = 0; + reg finished = 0; + reg [$clog2(WIDTH + 1):0] counter = 0; + + always @(posedge clk) begin + // Bound the number of cycles until the result is ready + assert (counter <= WIDTH); + + if (started) begin + if (finished || done) begin + finished <= 1; + // Make sure result stays until we start a new division + assert (done); + + // Check the result + if (start_divisor == 0) begin + assert ("ient); + assert (remainder == start_dividend); + end else begin + assert (quotient == start_dividend / start_divisor); + assert (remainder == start_dividend % start_divisor); + end + end else begin + counter <= counter + 1'b1; + end + end + + // Track the requested inputs + if (start) begin + start_divisor <= divisor; + start_dividend <= dividend; + started <= 1; + counter <= 0; + finished <= 0; + end + end +`endif +endmodule diff --git a/docs/examples/demos/Makefile b/docs/examples/demos/Makefile new file mode 100644 index 00000000..ecd71ac8 --- /dev/null +++ b/docs/examples/demos/Makefile @@ -0,0 +1,3 @@ +SUBDIR=../docs/examples/demos +TESTDIR=../../../tests +include $(TESTDIR)/make/subdir.mk diff --git a/sbysrc/demo3.sby b/docs/examples/demos/memory.sby similarity index 100% rename from sbysrc/demo3.sby rename to docs/examples/demos/memory.sby diff --git a/docs/examples/demos/picorv32_axicheck.sby b/docs/examples/demos/picorv32_axicheck.sby new file mode 100644 index 00000000..61b471a0 --- /dev/null +++ b/docs/examples/demos/picorv32_axicheck.sby @@ -0,0 +1,25 @@ +[tasks] +yices +boolector +z3 +abc + +[options] +mode bmc +depth 10 + +[engines] +yices: smtbmc yices +boolector: smtbmc boolector -ack +z3: smtbmc --nomem z3 +abc: abc bmc3 + +[script] +read_verilog -formal -norestrict -assume-asserts picorv32.v +read_verilog -formal axicheck.v +prep -top testbench + +[files] +picorv32.v ../../../extern/picorv32.v +axicheck.v ../../../extern/axicheck.v + diff --git a/sbysrc/demo2.sby b/docs/examples/demos/up_down_counter.sby similarity index 85% rename from sbysrc/demo2.sby rename to docs/examples/demos/up_down_counter.sby index 1d5639c0..cb922eb3 100644 --- a/sbysrc/demo2.sby +++ b/docs/examples/demos/up_down_counter.sby @@ -1,10 +1,13 @@ +[tasks] +suprove +avy + [options] mode prove -wait on [engines] -aiger suprove -aiger avy +suprove: aiger suprove +avy: aiger avy [script] read_verilog -formal demo.v diff --git a/docs/examples/dft/data_diode.sby b/docs/examples/dft/data_diode.sby new file mode 100644 index 00000000..32092d2c --- /dev/null +++ b/docs/examples/dft/data_diode.sby @@ -0,0 +1,49 @@ +[tasks] +diode +direct + +[options] +mode prove + +diode: expect pass +direct: expect fail + +fst on + +[engines] +smtbmc + +[script] +diode: read -define USE_DIODE + +verific -sv data_diode.sv + +hierarchy -top top + +# $overwrite_tag currently requires these two passes directly after importing +# the design. Otherwise the target signals of $overwrite_tag cannot be properly +# resolved nor can `dft_tag -overwrite-only` itself detect this situation to +# report it as an error. +flatten +dft_tag -overwrite-only + +# Then the design can be prepared as usual +prep + +# And finally the tagging logic can be resolved, which requires converting all +# FFs into simple D-FFs. Here, if this isn't done `dft_tag` will produce +# warnings and tell you to run the required passes. +async2sync +dffunmap +dft_tag -tag-public + +# The `Unhandled cell` warnings produced by `dft_tag` mean that there is no +# bit-precise tag propagation model for the listed cell. The cell in question +# will still propagate tags, but `dft_tag` will use a generic model that +# assumes all inputs can propagate to all outputs independent of the value of +# other inputs on the same cell. For built-in logic cells this is a sound +# over-approximation, but may produce more false-positives than a bit-precise +# approximation would. + +[files] +data_diode.sv diff --git a/docs/examples/dft/data_diode.sv b/docs/examples/dft/data_diode.sv new file mode 100644 index 00000000..fa053a21 --- /dev/null +++ b/docs/examples/dft/data_diode.sv @@ -0,0 +1,278 @@ +// Simple sync FIFO implementation using an extra bit in the read and write +// pointers to distinguish the completely full and completely empty case. +module fifo #( + DEPTH_BITS = 4, + WIDTH = 8 +) ( + input wire clk, + input wire rst, + + input wire in_valid, + input wire [WIDTH-1:0] in_data, + output reg in_ready, + + output reg out_valid, + output reg [WIDTH-1:0] out_data, + input wire out_ready +); + + reg [WIDTH-1:0] buffer [1< rst); +endchecker + +bind top initial_reset initial_reset(clk, rst); diff --git a/docs/examples/fifo/.gitignore b/docs/examples/fifo/.gitignore new file mode 100644 index 00000000..2bcf7d7b --- /dev/null +++ b/docs/examples/fifo/.gitignore @@ -0,0 +1 @@ +fifo_*/ \ No newline at end of file diff --git a/docs/examples/fifo/Makefile b/docs/examples/fifo/Makefile new file mode 100644 index 00000000..c22f5f17 --- /dev/null +++ b/docs/examples/fifo/Makefile @@ -0,0 +1,3 @@ +SUBDIR=../docs/examples/fifo +TESTDIR=../../../tests +include $(TESTDIR)/make/subdir.mk diff --git a/docs/examples/fifo/fifo.sby b/docs/examples/fifo/fifo.sby new file mode 100644 index 00000000..4ca4bc69 --- /dev/null +++ b/docs/examples/fifo/fifo.sby @@ -0,0 +1,29 @@ +[tasks] +basic bmc +nofullskip prove +cover +noverific cover +basic cover : default + +[options] +cover: +mode cover +-- +prove: +mode prove +-- +bmc: +mode bmc +-- + +[engines] +smtbmc boolector + +[script] +nofullskip: read -define NO_FULL_SKIP=1 +noverific: read -noverific +read -formal fifo.sv +prep -top fifo + +[files] +fifo.sv diff --git a/docs/examples/fifo/fifo.sv b/docs/examples/fifo/fifo.sv new file mode 100644 index 00000000..ba4d8e79 --- /dev/null +++ b/docs/examples/fifo/fifo.sv @@ -0,0 +1,197 @@ +// address generator/counter +module addr_gen +#( parameter MAX_DATA=16 +) ( input en, clk, rst, + output reg [3:0] addr +); + initial addr <= 0; + + // async reset + // increment address when enabled + always @(posedge clk or posedge rst) + if (rst) + addr <= 0; + else if (en) begin + if (addr == MAX_DATA-1) + addr <= 0; + else + addr <= addr + 1; + end +endmodule + +// Define our top level fifo entity +module fifo +#( parameter MAX_DATA=16 +) ( input wen, ren, clk, rst, + input [7:0] wdata, + output [7:0] rdata, + output [4:0] count, + output full, empty +); + wire wskip, rskip; + reg [4:0] data_count; + + // fifo storage + // async read, sync write + wire [3:0] waddr, raddr; + reg [7:0] data [MAX_DATA-1:0]; + always @(posedge clk) + if (wen) + data[waddr] <= wdata; + assign rdata = data[raddr]; + // end storage + + // addr_gen for both write and read addresses + addr_gen #(.MAX_DATA(MAX_DATA)) + fifo_writer ( + .en (wen || wskip), + .clk (clk ), + .rst (rst), + .addr (waddr) + ); + + addr_gen #(.MAX_DATA(MAX_DATA)) + fifo_reader ( + .en (ren || rskip), + .clk (clk ), + .rst (rst), + .addr (raddr) + ); + + // status signals + initial data_count <= 0; + + always @(posedge clk or posedge rst) begin + if (rst) + data_count <= 0; + else if (wen && !ren && data_count < MAX_DATA) + data_count <= data_count + 1; + else if (ren && !wen && data_count > 0) + data_count <= data_count - 1; + end + + assign full = data_count == MAX_DATA; + assign empty = (data_count == 0) && ~rst; + assign count = data_count; + + // overflow protection +`ifndef NO_FULL_SKIP + // write while full => overwrite oldest data, move read pointer + assign rskip = wen && !ren && data_count >= MAX_DATA; + // read while empty => read invalid data, keep write pointer in sync + assign wskip = ren && !wen && data_count == 0; +`else + assign rskip = 0; + assign wskip = 0; +`endif // NO_FULL_SKIP + +`ifdef FORMAL + // observers + wire [4:0] addr_diff; + assign addr_diff = waddr >= raddr + ? waddr - raddr + : waddr + MAX_DATA - raddr; + + // tests + always @(posedge clk) begin + if (~rst) begin + // waddr and raddr can only be non zero if reset is low + w_nreset: cover (waddr || raddr); + + // count never more than max + a_oflow: assert (count <= MAX_DATA); + a_oflow2: assert (waddr < MAX_DATA); + + // count should be equal to the difference between writer and reader address + a_count_diff: assert (count == addr_diff + || count == MAX_DATA && addr_diff == 0); + + // count should only be able to increase or decrease by 1 + a_counts: assert (count == 0 + || count == $past(count) + || count == $past(count) + 1 + || count == $past(count) - 1); + + // read/write addresses can only increase (or stay the same) + a_raddr: assert (raddr == 0 + || raddr == $past(raddr) + || raddr == $past(raddr + 1)); + a_waddr: assert (waddr == 0 + || waddr == $past(waddr) + || waddr == $past(waddr + 1)); + + // full and empty work as expected + a_full: assert (!full || count == MAX_DATA); + w_full: cover (wen && !ren && count == MAX_DATA-1); + a_empty: assert (!empty || count == 0); + w_empty: cover (ren && !wen && count == 1); + + // reading/writing non zero values + w_nzero_write: cover (wen && wdata); + w_nzero_read: cover (ren && rdata); + end else begin + // waddr and raddr are zero while reset is high + a_reset: assert (!waddr && !raddr); + w_reset: cover (rst); + + // outputs are zero while reset is high + a_zero_out: assert (!empty && !full && !count); + end + end + +`ifdef VERIFIC + // if we have verific we can also do the following additional tests + // read/write enables enable + ap_raddr2: assert property (@(posedge clk) disable iff (rst) ren |=> $changed(raddr)); + ap_waddr2: assert property (@(posedge clk) disable iff (rst) wen |=> $changed(waddr)); + + // read/write needs enable UNLESS full/empty + ap_raddr3: assert property (@(posedge clk) disable iff (rst) !ren && !full |=> $stable(raddr)); + ap_waddr3: assert property (@(posedge clk) disable iff (rst) !wen && !empty |=> $stable(waddr)); + + // use block formatting for w_underfill so it's easier to describe in docs + // and is more readily comparable with the non SVA implementation + property write_skip; + @(posedge clk) disable iff (rst) + !wen |=> $changed(waddr); + endproperty + w_underfill: cover property (write_skip); + + // look for an overfill where the value in memory changes + // the change in data makes certain that the value is overriden + let d_change = (wdata != rdata); + property read_skip; + @(posedge clk) disable iff (rst) + !ren && d_change |=> $changed(raddr); + endproperty + w_overfill: cover property (read_skip); +`else // !VERIFIC + // implementing w_underfill without properties + // can't use !$past(wen) since it will always trigger in the first cycle + reg past_nwen; + initial past_nwen <= 0; + always @(posedge clk) begin + if (rst) past_nwen <= 0; + if (!rst) begin + w_underfill: cover (past_nwen && $changed(waddr)); + past_nwen <= !wen; + end + end + // end w_underfill + + // w_overfill does the same, but has been separated so that w_underfill + // can be included in the docs more cleanly + reg past_nren; + initial past_nren <= 0; + always @(posedge clk) begin + if (rst) past_nren <= 0; + if (!rst) begin + w_overfill: cover (past_nren && $changed(raddr)); + past_nren <= !ren; + end + end +`endif // VERIFIC + +`endif // FORMAL + +endmodule diff --git a/docs/examples/fifo/fifo_extra_tests.sby b/docs/examples/fifo/fifo_extra_tests.sby new file mode 100644 index 00000000..183def50 --- /dev/null +++ b/docs/examples/fifo/fifo_extra_tests.sby @@ -0,0 +1,18 @@ +--pycode-begin-- +# This is for our test infrastructure and not part of the example + +# Read fifo.sby and patch it on the fly: +for line in open("fifo.sby"): + line = line.rstrip() + + # change the tasks to run as tests + if line.endswith(": default"): + line = "nofullskip noverific : default" + + output(line) + + # add expect fail to the failing tasks + if line == "[options]": + output("nofullskip: expect fail") + +--pycode-end-- diff --git a/docs/examples/fifo/golden/fifo.sby b/docs/examples/fifo/golden/fifo.sby new file mode 100644 index 00000000..10f2d85a --- /dev/null +++ b/docs/examples/fifo/golden/fifo.sby @@ -0,0 +1,34 @@ +[tasks] +basic bmc +nofullskip prove +cover +noverific cover +bigtest cover + +[options] +cover: +mode cover +-- +prove: +mode prove +-- +bmc: +mode bmc +-- +bigtest: depth 120 +~bigtest: depth 10 +nofullskip: expect fail + +[engines] +smtbmc boolector + +[script] +nofullskip: read -define NO_FULL_SKIP=1 +noverific: read -noverific +read -formal fifo.sv +bigtest: hierarchy -check -top fifo -chparam MAX_DATA 100 -chparam ADDR_BITS 7 +~bigtest: hierarchy -check -top fifo -chparam MAX_DATA 5 -chparam ADDR_BITS 3 +prep -top fifo + +[files] +fifo.sv diff --git a/docs/examples/fifo/golden/fifo.sv b/docs/examples/fifo/golden/fifo.sv new file mode 100644 index 00000000..d5ceadc1 --- /dev/null +++ b/docs/examples/fifo/golden/fifo.sv @@ -0,0 +1,199 @@ +// address generator/counter +module addr_gen +#( parameter MAX_DATA=16, + parameter ADDR_BITS=5 +) ( input en, clk, rst, + output reg [ADDR_BITS-1:0] addr +); + initial addr <= 0; + + // async reset + // increment address when enabled + always @(posedge clk or posedge rst) + if (rst) + addr <= 0; + else if (en) begin + if (addr == MAX_DATA-1) + addr <= 0; + else + addr <= addr + 1; + end +endmodule + +// Define our top level fifo entity +module fifo +#( parameter MAX_DATA=16, + parameter ADDR_BITS=5 +) ( input wen, ren, clk, rst, + input [7:0] wdata, + output [7:0] rdata, + output [ADDR_BITS:0] count, + output full, empty +); + wire wskip, rskip; + reg [ADDR_BITS:0] data_count; + + // fifo storage + // async read, sync write + wire [ADDR_BITS-1:0] waddr, raddr; + reg [7:0] data [MAX_DATA-1:0]; + always @(posedge clk) + if (wen) + data[waddr] <= wdata; + assign rdata = data[raddr]; + // end storage + + // addr_gen for both write and read addresses + addr_gen #(.MAX_DATA(MAX_DATA), .ADDR_BITS(ADDR_BITS)) + fifo_writer ( + .en (wen || wskip), + .clk (clk ), + .rst (rst), + .addr (waddr) + ); + + addr_gen #(.MAX_DATA(MAX_DATA), .ADDR_BITS(ADDR_BITS)) + fifo_reader ( + .en (ren || rskip), + .clk (clk ), + .rst (rst), + .addr (raddr) + ); + + // status signals + initial data_count <= 0; + + always @(posedge clk or posedge rst) begin + if (rst) + data_count <= 0; + else if (wen && !ren && data_count < MAX_DATA) + data_count <= data_count + 1; + else if (ren && !wen && data_count > 0) + data_count <= data_count - 1; + end + + assign full = data_count == MAX_DATA; + assign empty = (data_count == 0) && ~rst; + assign count = data_count; + + // overflow protection +`ifndef NO_FULL_SKIP + // write while full => overwrite oldest data, move read pointer + assign rskip = wen && !ren && data_count >= MAX_DATA; + // read while empty => read invalid data, keep write pointer in sync + assign wskip = ren && !wen && data_count == 0; +`else + assign rskip = 0; + assign wskip = 0; +`endif // NO_FULL_SKIP + +`ifdef FORMAL + // observers + wire [ADDR_BITS:0] addr_diff; + assign addr_diff = waddr >= raddr + ? waddr - raddr + : waddr + MAX_DATA - raddr; + + // tests + always @(posedge clk) begin + if (~rst) begin + // waddr and raddr can only be non zero if reset is low + w_nreset: cover (waddr || raddr); + + // count never more than max + a_oflow: assert (count <= MAX_DATA); + a_oflow2: assert (waddr < MAX_DATA); + + // count should be equal to the difference between writer and reader address + a_count_diff: assert (count == addr_diff + || count == MAX_DATA && addr_diff == 0); + + // count should only be able to increase or decrease by 1 + a_counts: assert (count == 0 + || count == $past(count) + || count == $past(count) + 1 + || count == $past(count) - 1); + + // read/write addresses can only increase (or stay the same) + a_raddr: assert (raddr == 0 + || raddr == $past(raddr) + || raddr == $past(raddr + 1)); + a_waddr: assert (waddr == 0 + || waddr == $past(waddr) + || waddr == $past(waddr + 1)); + + // full and empty work as expected + a_full: assert (!full || count == MAX_DATA); + w_full: cover (wen && !ren && count == MAX_DATA-1); + a_empty: assert (!empty || count == 0); + w_empty: cover (ren && !wen && count == 1); + + // reading/writing non zero values + w_nzero_write: cover (wen && wdata); + w_nzero_read: cover (ren && rdata); + end else begin + // waddr and raddr are zero while reset is high + a_reset: assert (!waddr && !raddr); + w_reset: cover (rst); + + // outputs are zero while reset is high + a_zero_out: assert (!empty && !full && !count); + end + end + +`ifdef VERIFIC + // if we have verific we can also do the following additional tests + // read/write enables enable + ap_raddr2: assert property (@(posedge clk) disable iff (rst) ren |=> $changed(raddr)); + ap_waddr2: assert property (@(posedge clk) disable iff (rst) wen |=> $changed(waddr)); + + // read/write needs enable UNLESS full/empty + ap_raddr3: assert property (@(posedge clk) disable iff (rst) !ren && !full |=> $stable(raddr)); + ap_waddr3: assert property (@(posedge clk) disable iff (rst) !wen && !empty |=> $stable(waddr)); + + // use block formatting for w_underfill so it's easier to describe in docs + // and is more readily comparable with the non SVA implementation + property write_skip; + @(posedge clk) disable iff (rst) + !wen |=> $changed(waddr); + endproperty + w_underfill: cover property (write_skip); + + // look for an overfill where the value in memory changes + // the change in data makes certain that the value is overriden + let d_change = (wdata != rdata); + property read_skip; + @(posedge clk) disable iff (rst) + !ren && d_change |=> $changed(raddr); + endproperty + w_overfill: cover property (read_skip); +`else // !VERIFIC + // implementing w_underfill without properties + // can't use !$past(wen) since it will always trigger in the first cycle + reg past_nwen; + initial past_nwen <= 0; + always @(posedge clk) begin + if (rst) past_nwen <= 0; + if (!rst) begin + w_underfill: cover (past_nwen && $changed(waddr)); + past_nwen <= !wen; + end + end + // end w_underfill + + // w_overfill does the same, but has been separated so that w_underfill + // can be included in the docs more cleanly + reg past_nren; + initial past_nren <= 0; + always @(posedge clk) begin + if (rst) past_nren <= 0; + if (!rst) begin + w_overfill: cover (past_nren && $changed(raddr)); + past_nren <= !ren; + end + end +`endif // VERIFIC + +`endif // FORMAL + +endmodule diff --git a/docs/examples/fifo/noskip.gtkw b/docs/examples/fifo/noskip.gtkw new file mode 100644 index 00000000..df81a20a --- /dev/null +++ b/docs/examples/fifo/noskip.gtkw @@ -0,0 +1,28 @@ +[*] +[*] GTKWave Analyzer v3.4.0 (w)1999-2022 BSI +[*] Thu Jun 09 02:02:01 2022 +[*] +[timestart] 0 +[size] 1000 320 +[pos] -1 -1 +*-3.253757 18 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 +[sst_width] 246 +[signals_width] 200 +[sst_expanded] 1 +[sst_vpaned_height] 58 +@28 +fifo.clk +@22 +fifo.data_count[4:0] +fifo.addr_diff[4:0] +@28 +fifo.ren +fifo.wen +@22 +fifo.raddr[3:0] +fifo.waddr[3:0] +@28 +fifo.rskip +fifo.wskip +[pattern_trace] 1 +[pattern_trace] 0 diff --git a/docs/examples/indinv/Makefile b/docs/examples/indinv/Makefile new file mode 100644 index 00000000..c3bf7ac0 --- /dev/null +++ b/docs/examples/indinv/Makefile @@ -0,0 +1,3 @@ +SUBDIR=../docs/examples/indinv +TESTDIR=../../../tests +include $(TESTDIR)/make/subdir.mk diff --git a/docs/examples/multiclk/Makefile b/docs/examples/multiclk/Makefile new file mode 100644 index 00000000..b6c5eb73 --- /dev/null +++ b/docs/examples/multiclk/Makefile @@ -0,0 +1,3 @@ +SUBDIR=../docs/examples/multiclk +TESTDIR=../../../tests +include $(TESTDIR)/make/subdir.mk diff --git a/docs/examples/multiclk/dpmem.sv b/docs/examples/multiclk/dpmem.sv index 87e4f61f..4a920e4e 100644 --- a/docs/examples/multiclk/dpmem.sv +++ b/docs/examples/multiclk/dpmem.sv @@ -47,9 +47,9 @@ module top ( (* gclk *) reg gclk; always @(posedge gclk) begin - assume ($stable(rc) || $stable(wc)); - if (!init) begin + assume ($stable(rc) || $stable(wc)); + if ($rose(rc) && shadow_valid && shadow_addr == $past(ra)) begin assert (shadow_data == rd); end diff --git a/docs/examples/puzzles/Makefile b/docs/examples/puzzles/Makefile new file mode 100644 index 00000000..45293b1b --- /dev/null +++ b/docs/examples/puzzles/Makefile @@ -0,0 +1,3 @@ +SUBDIR=../docs/examples/puzzles +TESTDIR=../../../tests +include $(TESTDIR)/make/subdir.mk diff --git a/docs/examples/quickstart/Makefile b/docs/examples/quickstart/Makefile new file mode 100644 index 00000000..be061940 --- /dev/null +++ b/docs/examples/quickstart/Makefile @@ -0,0 +1,3 @@ +SUBDIR=../docs/examples/quickstart +TESTDIR=../../../tests +include $(TESTDIR)/make/subdir.mk diff --git a/docs/examples/tristate/Makefile b/docs/examples/tristate/Makefile new file mode 100644 index 00000000..11735663 --- /dev/null +++ b/docs/examples/tristate/Makefile @@ -0,0 +1,3 @@ +SUBDIR=../docs/examples/tristate +TESTDIR=../../../tests +include $(TESTDIR)/make/subdir.mk diff --git a/docs/examples/tristate/README.md b/docs/examples/tristate/README.md new file mode 100644 index 00000000..155fab2d --- /dev/null +++ b/docs/examples/tristate/README.md @@ -0,0 +1,13 @@ +# Tristate demo + +Run + + sby -f tristate.sby pass + +to run the pass task. This uses the top module that exclusively enables each of the submodules. + +Run + + sby -f tristate.sby fail + +to run the fail task. This uses the top module that allows submodule to independently enable its tristate outputs. diff --git a/docs/examples/tristate/tristate.sby b/docs/examples/tristate/tristate.sby new file mode 100644 index 00000000..f85e937c --- /dev/null +++ b/docs/examples/tristate/tristate.sby @@ -0,0 +1,20 @@ +[tasks] +pass +fail + +[options] +fail: expect fail +mode prove +depth 5 + +[engines] +smtbmc + +[script] +read -sv tristates.v +pass: prep -top top_pass +fail: prep -top top_fail +flatten; tribuf -formal + +[files] +tristates.v diff --git a/docs/examples/tristate/tristates.v b/docs/examples/tristate/tristates.v new file mode 100644 index 00000000..a41ffc22 --- /dev/null +++ b/docs/examples/tristate/tristates.v @@ -0,0 +1,18 @@ +`default_nettype none +module module1 (input wire active, output wire tri_out); + assign tri_out = active ? 1'b0 : 1'bz; +endmodule + +module module2 (input wire active, output wire tri_out); + assign tri_out = active ? 1'b0 : 1'bz; +endmodule + +module top_pass (input wire clk, input wire active1, output wire out); + module1 module1 (.active(active1), .tri_out(out)); + module2 module2 (.active(!active1), .tri_out(out)); +endmodule + +module top_fail (input wire clk, input wire active1, input wire active2, output wire out); + module1 module1 (.active(active1), .tri_out(out)); + module2 module2 (.active(active2), .tri_out(out)); +endmodule diff --git a/docs/source/_templates/page.html b/docs/source/_templates/page.html new file mode 100644 index 00000000..de334e72 --- /dev/null +++ b/docs/source/_templates/page.html @@ -0,0 +1,43 @@ +{# + +See https://github.com/pradyunsg/furo/blob/main/src/furo/theme/furo/page.html for the original +block this is overwriting. + +The part that is customized is between the "begin of custom part" and "end of custom part" +comments below. It uses the same styles as the existing right sidebar code. + +#} +{% extends "furo/page.html" %} +{% block right_sidebar %} +
+ {# begin of custom part #} +
+ + YosysHQ + +
+ + {# end of custom part #} + {% if not furo_hide_toc %} +
+ + {{ _("On this page") }} + +
+
+
+ {{ toc }} +
+
+ {% endif %} +
+{% endblock %} diff --git a/docs/source/autotune.rst b/docs/source/autotune.rst new file mode 100644 index 00000000..19432186 --- /dev/null +++ b/docs/source/autotune.rst @@ -0,0 +1,218 @@ +Autotune: Automatic Engine Selection +==================================== + +Selecting the best performing engine for a given verification task often +requires some amount of trial and error. To reduce the manual work required for +this, sby offers the ``--autotune`` option. This takes an ``.sby`` file and +runs it using engines and engine configurations. At the end it produces a +report listing the fastest engines among these candidates. + +Using Autotune +-------------- + +To run autotune, you can add the ``--autotune`` option to your usual sby +invocation. For example, if you usually run ``sby demo.sby`` you would run +``sby --autotune demo.sby`` instead. When the ``.sby`` file contains multiple +tasks, autotune is run for each task independently. As without ``--autotune``, +it is possible to specify which tasks to run on the command line. + +Autotune runs without requiring further interaction, and will eventually print a +list of engine configurations and their respective solving times. To +permanently use an engine configuration you can copy it from the +``sby --autotune`` output into the ``[engines]`` section of your ``.sby`` file. + +Example +^^^^^^^ + +The Sby repository contains a `small example`_ in the ``docs/examples/autotune`` +directory. + +The ``divider.sby`` file contains the following ``[engines]`` section: + +.. code-block:: text + + [engines] + smtbmc + +We notice that running ``sby -f divider.sby medium`` takes a long time and want +to see if another engine would speed things up, so we run +``sby --autotune -f divider.sby medium``. After a few minutes this prints: + +.. code-block:: text + + SBY [divider_medium] finished engines: + SBY [divider_medium] #4: engine_7: smtbmc --nopresat bitwuzla -- --noincr (32 seconds, status=PASS) + SBY [divider_medium] #3: engine_2: smtbmc boolector -- --noincr (32 seconds, status=PASS) + SBY [divider_medium] #2: engine_3: smtbmc --nopresat boolector -- --noincr (32 seconds, status=PASS) + SBY [divider_medium] #1: engine_6: smtbmc bitwuzla -- --noincr (31 seconds, status=PASS) + SBY [divider_medium] DONE (AUTOTUNED, rc=0) + +This tells us that for the ``medium`` task, the best engine choice (#1) is +``smtbmc bitwuzla -- --noincr``. To use this engine by default we can change +the ``[engines]`` section of ``divider.sby`` to: + +.. code-block:: text + + [engines] + smtbmc bitwuzla -- --noincr + +.. _`small example`: https://github.com/YosysHQ/sby/tree/master/docs/examples/autotune + +Autotune Log Output +------------------- + +The log output in ``--autotune`` mode differs from the usual sby log output. + +It also starts with preparing the design (this includes running the user +provided ``[script]``) so it can be passed to the solvers. This is only done +once and will be reused to run every candidate engine. + +.. code-block:: text + + SBY [demo] model 'base': preparing now... + SBY [demo] base: starting process "cd demo/src; yosys -ql ../model/design.log ../model/design.ys" + SBY [demo] base: finished (returncode=0) + SBY [demo] prepared model 'base' + +This is followed by selecting the engine candidates to run. The design +and sby configuration are analyzed to skip engines that are not compatible or +unlikely to work well. When an engine is skipped due to a recommendation, a +corresponding log message is displayed as well as the total number of +candidates to try: + +.. code-block:: text + + SBY [demo] checking more than 20 timesteps (100), disabling nonincremental smtbmc + SBY [demo] testing 16 engine configurations... + +After this, the candidate engine configurations are started. Depending on the +configuration, engines can run in parallel. The engine output itself is not +logged to stdout when running autotune, so you will only see messages about +starting an engine: + +.. code-block:: text + + SBY [demo] engine_1 (smtbmc --nopresat boolector): starting... (14 configurations pending) + SBY [demo] engine_2 (smtbmc bitwuzla): starting... (13 configurations pending) + SBY [demo] engine_3 (smtbmc --nopresat bitwuzla): starting... (12 configurations pending) + ... + +The engine log that would normally be printed is instead redirected to files +named ``engine_*_autotune.txt`` within sby's working directory. + +To run an engine, further preparation steps may be necessary. These are cached +and will be reused for every engine requiring them, while properly accounting +for the required prepration time. Below is an example of the log output +produced by such a preparation step. Note that this runs in parallel, so it may +be interspersed with other log output. + +.. code-block:: text + + SBY [demo] model 'smt2': preparing now... + SBY [demo] smt2: starting process "cd demo/model; yosys -ql design_smt2.log design_smt2.ys" + ... + SBY [demo] smt2: finished (returncode=0) + SBY [demo] prepared model 'smt2' + +Whenever an engine finishes, a log message is printed: + +.. code-block:: text + + SBY [demo] engine_4 (smtbmc --unroll yices): succeeded (status=PASS) + SBY [demo] engine_4 (smtbmc --unroll yices): took 30 seconds (first engine to finish) + +When an engine takes longer than the current hard timeout, it is stopped: + +.. code-block:: text + + SBY [demo] engine_2 (smtbmc bitwuzla): timeout (150 seconds) + +Depending on the configuration, autotune will also stop an engine earlier when +reaching a soft timeout. If no other engine finishes in less +time, the engine will be retried later with a longer soft timeout: + +.. code-block:: text + + SBY [demo] engine_0 (smtbmc boolector): timeout (60 seconds, will be retried if necessary) + + +Finally, a summary of all finished engines is printed, sorted by +their solving time: + +.. code-block:: text + + SBY [demo] finished engines: + SBY [demo] #3: engine_1: smtbmc --nopresat boolector (52 seconds, status=PASS) + SBY [demo] #2: engine_5: smtbmc --nopresat --unroll yices (41 seconds, status=PASS) + SBY [demo] #1: engine_4: smtbmc --unroll yices (30 seconds, status=PASS) + SBY [demo] DONE (AUTOTUNED, rc=0) + +If any tried engine encounters an error or produces an unexpected result, +autotune will also output a list of failed engines. Note that when the sby file +does not contain the ``expect`` option, autotune defaults to +``expect pass,fail`` to simplify running autotune on a verification task with a +currently unknown outcome. + +Configuring Autotune +-------------------- + +Autotune can be configured by adding an ``[autotune]`` section to the ``.sby`` +file. Each line in that section has the form ``option_name value``, the +possible options and their supported values are described below. In addition, +the ``--autotune-config`` command line option can be used to specify a file +containing further autotune options, using the same syntax. When both are used, +the command line option takes precedence. This makes it easy to run autotune +with existing ``.sby`` files without having to modify them. + +Autotune Options +---------------- + ++--------------------+------------------------------------------------------+ +| Autotune Option | Description | ++====================+======================================================+ +| ``timeout`` | Set a different timeout value (in seconds) used only | +| | for autotune. The value ``none`` can be used to | +| | disable the timeout. Default: uses the non-autotune | +| | timeout option. | ++--------------------+------------------------------------------------------+ +| ``soft_timeout`` | Initial timeout value (in seconds) used to interrupt | +| | a candidate engine when other candidates are | +| | pending. Increased every time a candidate is retried | +| | to ensure progress. Default: ``60`` | ++--------------------+------------------------------------------------------+ +| ``wait`` | Additional time to wait past the time taken by the | +| | fastest finished engine candidate so far. Can be an | +| | absolute time in seconds, a percentage of the | +| | fastest candidate or a sum of both. | +| | Default: ``50%+10`` | ++--------------------+------------------------------------------------------+ +| ``parallel`` | Maximal number of engine candidates to try in | +| | parallel. When set to ``auto``, the number of | +| | available CPUs is used. Default: ``auto`` | ++--------------------+------------------------------------------------------+ +| ``presat`` | Filter candidates by whether they perform a presat | +| | check. Values ``on``, ``off``, ``any``. | +| | Default: ``any`` | ++--------------------+------------------------------------------------------+ +| ``incr`` | Filter candidates by whether they use incremental | +| | solving (when applicable). Values ``on``, ``off``, | +| | ``any``, ``auto`` (see next option). | +| | Default: ``auto`` | ++--------------------+------------------------------------------------------+ +| ``incr_threshold`` | Number of timesteps required to only consider | +| | incremental solving when ``incr`` is set to | +| | ``auto``. Default: ``20`` | ++--------------------+------------------------------------------------------+ +| ``mem`` | Filter candidates by whether they have native | +| | support for memory. Values ``on``, ``any``, ``auto`` | +| | (see next option). Default: ``auto`` | ++--------------------+------------------------------------------------------+ +| ``mem_threshold`` | Number of memory bits required to only consider | +| | candidates with native memory support when ``mem`` | +| | is set to ``auto``. Default: ``10240`` | ++--------------------+------------------------------------------------------+ +| ``forall`` | Filter candidates by whether they support | +| | ``$allconst``/``$allseq``. Values ``on``, ``any``, | +| | ``auto`` (``on`` when ``$allconst``/``allseq`` are | +| | found in the design). Default: ``auto`` | ++--------------------+------------------------------------------------------+ diff --git a/docs/source/conf.py b/docs/source/conf.py index 1d4cbeb6..e18817ce 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -1,28 +1,47 @@ #!/usr/bin/env python3 +import sys +import os + +sys.path.append(os.path.abspath(f"{__file__}/../../../sbysrc")) + project = 'YosysHQ SBY' author = 'YosysHQ GmbH' -copyright ='2021 YosysHQ GmbH' +copyright = '2023 YosysHQ GmbH' # select HTML theme -html_theme = 'press' + +templates_path = ["_templates"] +html_theme = "furo" html_logo = '../static/logo.png' html_favicon = '../static/favico.png' -html_css_files = ['yosyshq.css', 'custom.css'] -html_sidebars = {'**': ['util/searchbox.html', 'util/sidetoc.html']} +html_css_files = ['custom.css'] # These folders are copied to the documentation's HTML output -html_static_path = ['../static', "../images"] +html_static_path = ['../static'] -# code blocks style +# code blocks style pygments_style = 'colorful' highlight_language = 'systemverilog' html_theme_options = { - 'external_links' : [ - ('YosysHQ Docs', 'https://yosyshq.readthedocs.io'), - ('Blog', 'https://blog.yosyshq.com'), - ('Website', 'https://www.yosyshq.com'), - ], + "sidebar_hide_name": True, + + "light_css_variables": { + "color-brand-primary": "#d6368f", + "color-brand-content": "#4b72b8", + "color-api-name": "#8857a3", + "color-api-pre-name": "#4b72b8", + "color-link": "#8857a3", + }, + + "dark_css_variables": { + "color-brand-primary": "#e488bb", + "color-brand-content": "#98bdff", + "color-api-name": "#8857a3", + "color-api-pre-name": "#4b72b8", + "color-link": "#be95d5", + }, } extensions = ['sphinx.ext.autosectionlabel'] +extensions += ['sphinxarg.ext'] diff --git a/docs/source/index.rst b/docs/source/index.rst index 0527fb4e..ab67043e 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -10,18 +10,15 @@ formal tasks: * Unbounded verification of safety properties * Generation of test benches from cover statements * Verification of liveness properties - * Formal equivalence checking [TBD] - * Reactive Synthesis [TBD] - -(Items marked [TBD] are features under construction and not available -at the moment.) .. toctree:: :maxdepth: 3 install.rst quickstart.rst + usage.rst reference.rst + autotune.rst verilog.rst verific.rst license.rst diff --git a/docs/source/install.rst b/docs/source/install.rst index 273b166d..ba578d20 100644 --- a/docs/source/install.rst +++ b/docs/source/install.rst @@ -1,50 +1,107 @@ -Installing -========== +.. _install-doc: -Follow the instructions below to install SymbiYosys and its dependencies. -Yosys, SymbiYosys, and Z3 are non-optional. The other packages are only -required for some engine configurations. +Installation guide +================== + +This document will guide you through the process of installing sby. + +CAD suite(s) +************ + +Sby (SymbiYosys) is part of the `Tabby CAD Suite +`_ and the `OSS CAD Suite +`_! The easiest way to use sby +is to install the binary software suite, which contains all required +dependencies, including all supported solvers. + +* `Contact YosysHQ `_ for a `Tabby CAD Suite + `_ Evaluation License and + download link +* OR go to https://github.com/YosysHQ/oss-cad-suite-build/releases to download + the free OSS CAD Suite +* Follow the `Install Instructions on GitHub + `_ + +Make sure to get a Tabby CAD Suite Evaluation License for extensive +SystemVerilog Assertion (SVA) support, as well as industry-grade SystemVerilog +and VHDL parsers! + +For more information about the difference between Tabby CAD Suite and the OSS +CAD Suite, please visit https://www.yosyshq.com/tabby-cad-datasheet. + +Installing from source +********************** + +Follow the instructions below to install sby and its dependencies. Yosys and sby +are non-optional. Boolector is recommended to install but not required. The +other packages are only required for some engine configurations. Prerequisites ------------- -Installing prerequisites (this command is for Ubuntu 16.04): +Installing prerequisites (this command is for Ubuntu 20.04): .. code-block:: text - sudo apt-get install build-essential clang bison flex libreadline-dev \ - gawk tcl-dev libffi-dev git mercurial graphviz \ - xdot pkg-config python python3 libftdi-dev gperf \ - libboost-program-options-dev autoconf libgmp-dev \ - cmake + sudo apt-get install build-essential clang bison flex \ + libreadline-dev gawk tcl-dev libffi-dev git \ + graphviz xdot pkg-config python3 zlib1g-dev + + python3 -m pip install click + +Required components +------------------- Yosys, Yosys-SMTBMC and ABC ---------------------------- +^^^^^^^^^^^^^^^^^^^^^^^^^^^ https://yosyshq.net/yosys/ https://people.eecs.berkeley.edu/~alanmi/abc/ -Next install Yosys, Yosys-SMTBMC and ABC (``yosys-abc``): +Note that this will install Yosys, Yosys-SMTBMC and ABC (as ``yosys-abc``): .. code-block:: text - git clone https://github.com/YosysHQ/yosys.git yosys + git clone https://github.com/YosysHQ/yosys cd yosys make -j$(nproc) sudo make install -SymbiYosys ----------- +sby +^^^ -https://github.com/YosysHQ/SymbiYosys +https://github.com/YosysHQ/sby .. code-block:: text - git clone https://github.com/YosysHQ/SymbiYosys.git SymbiYosys - cd SymbiYosys + git clone https://github.com/YosysHQ/sby + cd sby sudo make install +Recommended components +---------------------- + +Boolector +^^^^^^^^^ + +https://boolector.github.io + +.. code-block:: text + + git clone https://github.com/boolector/boolector + cd boolector + ./contrib/setup-btor2tools.sh + ./contrib/setup-lingeling.sh + ./configure.sh + make -C build -j$(nproc) + sudo cp build/bin/{boolector,btor*} /usr/local/bin/ + sudo cp deps/btor2tools/bin/btorsim /usr/local/bin/ + +To use the ``btor`` engine you will need to install btor2tools from +`commit c35cf1c `_ or +newer. + Yices 2 ------- @@ -59,86 +116,20 @@ http://yices.csl.sri.com/ make -j$(nproc) sudo make install -Z3 --- - -https://github.com/Z3Prover/z3/wiki +Optional components +------------------- +Additional solver engines can be installed as per their instructions, links are +provided below. -.. code-block:: text +Z3 +^^^ - git clone https://github.com/Z3Prover/z3.git z3 - cd z3 - python scripts/mk_make.py - cd build - make -j$(nproc) - sudo make install + https://github.com/Z3Prover/z3 super_prove ------------ - -https://github.com/sterin/super-prove-build - -.. code-block:: text - - sudo apt-get install cmake ninja-build g++ python-dev python-setuptools \ - python-pip git - git clone --recursive https://github.com/sterin/super-prove-build - cd super-prove-build - mkdir build - cd build - cmake -DCMAKE_BUILD_TYPE=Release -G Ninja .. - ninja - ninja package - -This creates a .tar.gz archive for the target system. Extract it to -``/usr/local/super_prove`` - -.. code-block:: text - - sudo tar -C /usr/local -x super_prove-X-Y-Release.tar.gz - -Then create a wrapper script ``/usr/local/bin/suprove`` with the following contents: - -.. code-block:: text - - #!/bin/bash - tool=super_prove; if [ "$1" != "${1#+}" ]; then tool="${1#+}"; shift; fi - exec /usr/local/super_prove/bin/${tool}.sh "$@" - -And make this wrapper script executable: - -.. code-block:: text - - sudo chmod +x /usr/local/bin/suprove +^^^^^^^^^^^ + https://github.com/sterin/super-prove-build Avy ---- - -https://arieg.bitbucket.io/avy/ - -.. code-block:: text - - git clone https://bitbucket.org/arieg/extavy.git - cd extavy - git submodule update --init - mkdir build; cd build - cmake -DCMAKE_BUILD_TYPE=Release .. - make -j$(nproc) - sudo cp avy/src/{avy,avybmc} /usr/local/bin/ - -Boolector ---------- - -http://fmv.jku.at/boolector/ - -.. code-block:: text - - git clone https://github.com/boolector/boolector - cd boolector - ./contrib/setup-btor2tools.sh - ./contrib/setup-lingeling.sh - ./configure.sh - make -C build -j$(nproc) - sudo cp build/bin/{boolector,btor*} /usr/local/bin/ - sudo cp deps/btor2tools/bin/btorsim /usr/local/bin/ - +^^^ + https://arieg.bitbucket.io/avy/ diff --git a/docs/source/license.rst b/docs/source/license.rst index e102ae18..786dc596 100644 --- a/docs/source/license.rst +++ b/docs/source/license.rst @@ -1,5 +1,5 @@ -SymbiYosys License +SymbiYosys license ================== SymbiYosys (sby) itself is licensed under the ISC license: diff --git a/docs/source/media/gtkwave_coverskip.png b/docs/source/media/gtkwave_coverskip.png new file mode 100644 index 00000000..a0b4d4a4 Binary files /dev/null and b/docs/source/media/gtkwave_coverskip.png differ diff --git a/docs/source/media/gtkwave_noskip.png b/docs/source/media/gtkwave_noskip.png new file mode 100644 index 00000000..74c40d55 Binary files /dev/null and b/docs/source/media/gtkwave_noskip.png differ diff --git a/docs/source/quickstart.rst b/docs/source/quickstart.rst index 1e660393..604d04c7 100644 --- a/docs/source/quickstart.rst +++ b/docs/source/quickstart.rst @@ -1,119 +1,307 @@ -Getting Started +Getting started =============== -The example files used in this chapter can be downloaded from `here -`_. +.. note:: -First step: A simple BMC example --------------------------------- + This tutorial assumes sby and boolector installation as per the + :ref:`install-doc`. For this tutorial, it is also recommended to install + `GTKWave `_, an open source VCD viewer. + `Source files used in this tutorial + `_ can be + found on the sby git, under ``docs/examples/fifo``. -Here is a simple example design with a safety property (assertion). +First In, First Out (FIFO) buffer +********************************* -.. literalinclude:: ../examples/quickstart/demo.sv +From `Wikipedia `_, +a FIFO is + + a method for organizing the manipulation of a data structure (often, + specifically a data buffer) where the oldest (first) entry, or "head" of the + queue, is processed first. + + Such processing is analogous to servicing people in a queue area on a + first-come, first-served (FCFS) basis, i.e. in the same sequence in which + they arrive at the queue's tail. + +In hardware we can create such a construct by providing two addresses into a +register file. This tutorial will use an example implementation provided in +`fifo.sv`. + +First, the address generator module: + +.. literalinclude:: ../examples/fifo/fifo.sv :language: systemverilog + :start-at: address generator + :end-at: endmodule -The property in this example is true. We'd like to verify this using a bounded -model check (BMC) that is 100 cycles deep. +This module is instantiated twice; once for the write address and once for the +read address. In both cases, the address will start at and reset to 0, and will +increment by 1 when an enable signal is received. When the address pointers +increment from the maximum storage value they reset back to 0, providing a +circular queue. -SymbiYosys is controlled by ``.sby`` files. The following file can be used to -configure SymbiYosys to run a BMC for 100 cycles on the design: +Next, the register file: -.. literalinclude:: ../examples/quickstart/demo.sby - :language: text +.. literalinclude:: ../examples/fifo/fifo.sv + :language: systemverilog + :start-at: fifo storage + :end-before: end storage + :dedent: -Simply create a text file ``demo.sv`` with the example design and another text -file ``demo.sby`` with the SymbiYosys configuration. Then run:: +Notice that this register design includes a synchronous write and asynchronous +read. Each word is 8 bits, and up to 16 words can be stored in the buffer. - sby demo.sby +Verification properties +*********************** -This will run a bounded model check for 100 cycles. The last few lines of the -output should look something like this: +In order to verify our design we must first define properties that it must +satisfy. For example, there must never be more than there is memory available. +By assigning a signal to count the number of values in the buffer, we can make +the following assertion in the code: -.. code-block:: text +.. literalinclude:: ../examples/fifo/fifo.sv + :language: systemverilog + :start-at: a_oflow + :end-at: ; + :dedent: + +It is also possible to use the prior value of a signal for comparison. This can +be used, for example, to ensure that the count is only able to increase or +decrease by 1. A case must be added to handle resetting the count directly to +0, as well as if the count does not change. This can be seen in the following +code; at least one of these conditions must be true at all times if our design +is to be correct. + +.. literalinclude:: ../examples/fifo/fifo.sv + :language: systemverilog + :start-at: a_counts + :end-at: ; + :dedent: + +As our count signal is used independently of the read and write pointers, we +must verify that the count is always correct. While the write pointer will +always be at the same point or *after* the read pointer, the circular buffer +means that the write *address* could wrap around and appear *less than* the read +address. So we must first perform some simple arithmetic to find the absolute +difference in addresses, and then compare with the count signal. + +.. literalinclude:: ../examples/fifo/fifo.sv + :language: systemverilog + :start-at: assign addr_diff + :end-at: ; + :dedent: - SBY [demo] engine_0: ## 0 0:00:00 Checking asserts in step 96.. - SBY [demo] engine_0: ## 0 0:00:00 Checking asserts in step 97.. - SBY [demo] engine_0: ## 0 0:00:00 Checking asserts in step 98.. - SBY [demo] engine_0: ## 0 0:00:00 Checking asserts in step 99.. - SBY [demo] engine_0: ## 0 0:00:00 Status: PASSED - SBY [demo] engine_0: Status returned by engine: PASS - SBY [demo] engine_0: finished (returncode=0) - SBY [demo] summary: Elapsed clock time [H:MM:SS (secs)]: 0:00:00 (0) - SBY [demo] summary: Elapsed process time [H:MM:SS (secs)]: 0:00:00 (0) - SBY [demo] summary: engine_0 (smtbmc) returned PASS - SBY [demo] DONE (PASS) - -This will also create a ``demo/`` directory tree with all relevant information, -such as a copy of the design source, various log files, and trace data in case -the proof fails. - -(Use ``sby -f demo.sby`` to re-run the proof. Without ``-f`` the command will -fail because the output directory ``demo/`` already exists.) - -Time for a simple exercise: Modify the design so that the property is false -and the offending state is reachable within 100 cycles. Re-run ``sby`` with -the modified design and see if the proof now fails. Inspect the counterexample -trace (``.vcd`` file) produced by ``sby``. (`GTKWave `_ -is an open source VCD viewer that you can use.) - -Selecting the right engine --------------------------- - -The ``.sby`` file for a project selects one or more engines. (When multiple -engines are selected, all engines are executed in parallel and the result -returned by the first engine to finish is the result returned by SymbiYosys.) - -Each engine has its strengths and weaknesses. Therefore it is important to -select the right engine for each project. The documentation for the individual -engines can provide some guidance for engine selection. (Trial and error can -also be a useful method for evaluating engines.) - -Let's consider the following example: - -.. literalinclude:: ../examples/quickstart/memory.sv +.. literalinclude:: ../examples/fifo/fifo.sv :language: systemverilog + :start-at: a_count_diff + :end-at: ; + :dedent: + +SymbiYosys +********** -This example is expected to fail verification (see the BUG comment). -The following ``.sby`` file can be used to show this: +SymbiYosys (sby) uses a .sby file to define a set of tasks used for +verification. -.. literalinclude:: ../examples/quickstart/memory.sby - :language: text +**basic** + Bounded model check of design. -This project uses the ``smtbmc`` engine, which uses SMT solvers to perform the -proof. This engine uses the array-theories provided by those solvers to -efficiently model memories. Since this example uses large memories, the -``smtbmc`` engine is a good match. +**nofullskip** + Demonstration of failing model using an unbounded model check. -(``smtbmc boolector`` selects Boolector as SMT solver, ``smtbmc z3`` selects -Z3, and ``smtbmc yices`` selects Yices 2. Yices 2 is the default solver when -no argument is used with ``smtbmc``.) +**cover** + Cover mode (testing cover statements). -Exercise: The engine ``abc bmc3`` does not provide abstract memory models. -Therefore SymbiYosys has to synthesize the memories in the example to FFs -and address logic. How does the performance of this project change if -``abc bmc3`` is used as engine instead of ``smtbmc boolector``? How fast -can either engine verify the design when the bug has been fixed? +**noverific** + Test fallback to default Verilog frontend. -Beyond bounded model checks ---------------------------- +The use of the ``:default`` tag indicates that by default, basic and cover +should be run if no tasks are specified, such as when running the command below. -Bounded model checks only prove that the safety properties hold for the first -*N* cycles (where *N* is the depth of the BMC). Sometimes this is insufficient -and we need to prove that the safety properties hold forever, not just the first -*N* cycles. Let us consider the following example: + sby fifo.sby -.. literalinclude:: ../examples/quickstart/prove.sv +.. note:: + + The default set of tests should all pass. If this is not the case there may + be a problem with the installation of sby or one of its solvers. + +To see what happens when a test fails, the below command can be used. Note the +use of the ``-f`` flag to automatically overwrite existing task output. While +this may not be necessary on the first run, it is quite useful when making +adjustments to code and rerunning tests to validate. + + sby -f fifo.sby nofullskip + +The nofullskip task disables the code shown below. Because the count signal has +been written such that it cannot exceed MAX_DATA, removing this code will lead +to the ``a_count_diff`` assertion failing. Without this assertion, there is no +guarantee that data will be read in the same order it was written should an +overflow occur and the oldest data be written. + +.. literalinclude:: ../examples/fifo/fifo.sv :language: systemverilog + :start-at: NO_FULL_SKIP + :end-at: endif + :lines: 1-5,9 + +The last few lines of output for the nofullskip task should be similar to the +following: + +.. code-block:: text + + SBY [fifo_nofullskip] engine_0.basecase: ## Assert failed in fifo: a_count_diff + SBY [fifo_nofullskip] engine_0.basecase: ## Writing trace to VCD file: engine_0/trace.vcd + SBY [fifo_nofullskip] engine_0.basecase: ## Writing trace to Verilog testbench: engine_0/trace_tb.v + SBY [fifo_nofullskip] engine_0.basecase: ## Writing trace to constraints file: engine_0/trace.smtc + SBY [fifo_nofullskip] engine_0.basecase: ## Status: failed + SBY [fifo_nofullskip] engine_0.basecase: finished (returncode=1) + SBY [fifo_nofullskip] engine_0: Status returned by engine for basecase: FAIL + SBY [fifo_nofullskip] engine_0.induction: terminating process + SBY [fifo_nofullskip] summary: Elapsed clock time [H:MM:SS (secs)]: 0:00:02 (2) + SBY [fifo_nofullskip] summary: Elapsed process time unvailable on Windows + SBY [fifo_nofullskip] summary: engine_0 (smtbmc boolector) returned FAIL for basecase + SBY [fifo_nofullskip] summary: counterexample trace: fifo_nofullskip/engine_0/trace.vcd + SBY [fifo_nofullskip] DONE (FAIL, rc=2) + SBY The following tasks failed: ['nofullskip'] + +Using the ``noskip.gtkw`` file provided, use the below command to examine the +error trace. -Proving this design in an unbounded manner can be achieved using the following -SymbiYosys configuration file: + gtkwave fifo_nofullskip/engine_0/trace.vcd noskip.gtkw -.. literalinclude:: ../examples/quickstart/prove.sby - :language: text +This should result in something similar to the below image. We can immediately +see that ``data_count`` and ``addr_diff`` are different. Looking a bit deeper +we can see that in order to reach this state the read enable signal was high in +the first clock cycle while write enable is low. This leads to an underfill +where a value is read while the buffer is empty and the read address increments +to a higher value than the write address. -Note that ``mode`` is now set to ``prove`` instead of ``bmc``. The ``smtbmc`` -engine in ``prove`` mode will perform a k-induction proof. Other engines can -use other methods, e.g. using ``abc pdr`` will prove the design using the IC3 -algorithm. +.. image:: media/gtkwave_noskip.png +During correct operation, the ``w_underfill`` statement will cover the underflow +case. Examining ``fifo_cover/logfile.txt`` will reveal which trace file +includes the cover statment we are looking for. If this file doesn't exist, run +the code below. + + sby fifo.sby cover + +Searching the file for ``w_underfill`` will reveal the below. + +.. code-block:: text + + $ grep "w_underfill" fifo_cover/logfile.txt -A 1 + SBY [fifo_cover] engine_0: ## Reached cover statement at w_underfill in step 2. + SBY [fifo_cover] engine_0: ## Writing trace to VCD file: engine_0/trace4.vcd + +We can then run gtkwave with the trace file indicated to see the correct +operation as in the image below. When the buffer is empty, a read with no write +will result in the ``wksip`` signal going high, incrementing *both* read and +write addresses and avoiding underflow. + + gtkwave fifo_cover/engine_0/trace4.vcd noskip.gtkw + +.. image:: media/gtkwave_coverskip.png + +.. note:: + + Implementation of the ``w_underfill`` cover statement depends on whether + Verific is used or not. See the `Concurrent assertions`_ section for more + detail. + +Exercise +******** + +Adjust the ``[script]`` section of ``fifo.sby`` so that it looks like the below. + +.. code-block:: text + + [script] + nofullskip: read -define NO_FULL_SKIP=1 + noverific: read -noverific + read -formal fifo.sv + hierarchy -check -top fifo -chparam MAX_DATA 17 + prep -top fifo + +The ``hierarchy`` command we added changes the ``MAX_DATA`` parameter of the top +module to be 17. Now run the ``basic`` task and see what happens. It should +fail and give an error like ``Assert failed in fifo: a_count_diff``. Can you +modify the verilog code so that it works with larger values of ``MAX_DATA`` +while still passing all of the tests? + +.. note:: + + If you need a **hint**, try increasing the width of the address wires. 4 bits + supports up to 2\ :sup:`4`\ =16 addresses. Are there other signals that + need to be wider? Can you make the width parameterisable to support + arbitrarily large buffers? + +Once the tests are passing with ``MAX_DATA=17``, try something bigger, like 64, +or 100. Does the ``basic`` task still pass? What about ``cover``? By default, +``bmc`` & ``cover`` modes will run to a depth of 20 cycles. If a maximum of one +value can be loaded in each cycle, how many cycles will it take to load 100 +values? Using the :ref:`.sby reference page `, +try to increase the cover mode depth to be at least a few cycles larger than the +``MAX_DATA``. + +.. note:: + + Reference files are provided in the ``fifo/golden`` directory, showing how + the verilog could have been modified and how a ``bigtest`` task could be + added. + +Concurrent assertions +********************* + +Until this point, all of the properties described have been *immediate* +assertions. As the name suggests, immediate assertions are evaluated +immediately whereas concurrent assertions allow for the capture of sequences of +events which occur across time. The use of concurrent assertions requires a +more advanced series of checks. + +Compare the difference in implementation of ``w_underfill`` depending on the +presence of Verific. ``w_underfill`` looks for a sequence of events where the +write enable is low but the write address changes in the following cycle. This +is the expected behaviour for reading while empty and implies that the +``w_skip`` signal went high. Verific enables elaboration of SystemVerilog +Assertions (SVA) properties. Here we use such a property, ``write_skip``. + +.. literalinclude:: ../examples/fifo/fifo.sv + :language: systemverilog + :start-at: property write_skip + :end-at: w_underfill + :dedent: + +This property describes a *sequence* of events which occurs on the ``clk`` +signal and are disabled/restarted when the ``rst`` signal is high. The property +first waits for a low ``wen`` signal, and then a change in ``waddr`` in the +following cycle. ``w_underfill`` is then a cover of this property to verify +that it is possible. Now look at the implementation without Verific. + +.. literalinclude:: ../examples/fifo/fifo.sv + :language: systemverilog + :start-at: reg past_nwen; + :end-before: end w_underfill + :dedent: + +In this case we do not have access to SVA properties and are more limited in the +tools available to us. Ideally we would use ``$past`` to read the value of +``wen`` in the previous cycle and then check for a change in ``waddr``. However, +in the first cycle of simulation, reading ``$past`` will return a value of +``X``. This results in false triggers of the property so we instead implement +the ``past_nwen`` register which we can initialise to ``0`` and ensure it does +not trigger in the first cycle. + +As verification properties become more complex and check longer sequences, the +additional effort of hand-coding without SVA properties becomes much more +difficult. Using a parser such as Verific supports these checks *without* +having to write out potentially complicated state machines. Verific is included +for use in the *Tabby CAD Suite*. + +Further information +******************* +For more information on the uses of assertions and the difference between +immediate and concurrent assertions, refer to appnote 109: `Property Checking +with SystemVerilog Assertions +`_. diff --git a/docs/source/reference.rst b/docs/source/reference.rst index dc5f3363..083c7f01 100644 --- a/docs/source/reference.rst +++ b/docs/source/reference.rst @@ -120,56 +120,80 @@ Mode Description ``prove`` Unbounded model check to verify safety properties (``assert(...)`` statements) ``live`` Unbounded model check to verify liveness properties (``assert(s_eventually ...)`` statements) ``cover`` Generate set of shortest traces required to reach all cover() statements -``equiv`` Formal equivalence checking (usually to verify pre- and post-synthesis equivalence) -``synth`` Reactive Synthesis (synthesis of circuit from safety properties) ========= =========== +.. + ``equiv`` Formal equivalence checking (usually to verify pre- and post-synthesis equivalence) + ``synth`` Reactive Synthesis (synthesis of circuit from safety properties) + All other options have default values and thus are optional. The available options are: -+------------------+------------+---------------------------------------------------------+ -| Option | Modes | Description | -+==================+============+=========================================================+ -| ``expect`` | All | Expected result as comma-separated list of the tokens | -| | | ``pass``, ``fail``, ``unknown``, ``error``, and | -| | | ``timeout``. Unexpected results yield a nonzero return | -| | | code . Default: ``pass`` | -+------------------+------------+---------------------------------------------------------+ -| ``timeout`` | All | Timeout in seconds. Default: ``none`` (i.e. no timeout) | -+------------------+------------+---------------------------------------------------------+ -| ``multiclock`` | All | Create a model with multiple clocks and/or asynchronous | -| | | logic. Values: ``on``, ``off``. Default: ``off`` | -+------------------+------------+---------------------------------------------------------+ -| ``wait`` | All | Instead of terminating when the first engine returns, | -| | | wait for all engines to return and check for | -| | | consistency. Values: ``on``, ``off``. Default: ``off`` | -+------------------+------------+---------------------------------------------------------+ -| ``aigsmt`` | All | Which SMT2 solver to use for converting AIGER witnesses | -| | | to counter example traces. Use ``none`` to disable | -| | | conversion of AIGER witnesses. Default: ``yices`` | -+------------------+------------+---------------------------------------------------------+ -| ``tbtop`` | All | The top module for generated Verilog test benches, as | -| | | hierarchical path relative to the design top module. | -+------------------+------------+---------------------------------------------------------+ -| ``smtc`` | ``bmc``, | Pass this ``.smtc`` file to the smtbmc engine. All | -| | ``prove``, | other engines are disabled when this option is used. | -| | ``cover`` | Default: None | -+------------------+------------+---------------------------------------------------------+ -| ``depth`` | ``bmc``, | Depth of the bounded model check. Only the specified | -| | ``cover`` | number of cycles are considered. Default: ``20`` | -| +------------+---------------------------------------------------------+ -| | ``prove`` | Depth for the k-induction performed by the ``smtbmc`` | -| | | engine. Other engines ignore this option in ``prove`` | -| | | mode. Default: ``20`` | -+------------------+------------+---------------------------------------------------------+ -| ``skip`` | ``bmc``, | Skip the specified number of time steps. Only valid | -| | ``cover`` | with smtbmc engine. All other engines are disabled when | -| | | this option is used. Default: None | -+------------------+------------+---------------------------------------------------------+ -| ``append`` | ``bmc``, | When generating a counter-example trace, add the | -| | ``prove``, | specified number of cycles at the end of the trace. | -| | ``cover`` | Default: ``0`` | -+------------------+------------+---------------------------------------------------------+ ++-------------------+------------+---------------------------------------------------------+ +| Option | Modes | Description | ++===================+============+=========================================================+ +| ``expect`` | All | Expected result as comma-separated list of the tokens | +| | | ``pass``, ``fail``, ``unknown``, ``error``, and | +| | | ``timeout``. Unexpected results yield a nonzero return | +| | | code . Default: ``pass`` | ++-------------------+------------+---------------------------------------------------------+ +| ``timeout`` | All | Timeout in seconds. Default: ``none`` (i.e. no timeout) | ++-------------------+------------+---------------------------------------------------------+ +| ``multiclock`` | All | Create a model with multiple clocks and/or asynchronous | +| | | logic. Values: ``on``, ``off``. Default: ``off`` | ++-------------------+------------+---------------------------------------------------------+ +| ``wait`` | All | Instead of terminating when the first engine returns, | +| | | wait for all engines to return and check for | +| | | consistency. Values: ``on``, ``off``. Default: ``off`` | ++-------------------+------------+---------------------------------------------------------+ +| ``vcd`` | All | Write VCD traces for counter-example or cover traces. | +| | | Values: ``on``, ``off``. Default: ``on`` | ++-------------------+------------+---------------------------------------------------------+ +| ``vcd_sim`` | All | When generating VCD traces, use Yosys's ``sim`` | +| | | command. Replaces the engine native VCD output. | +| | | Values: ``on``, ``off``. Default: ``off`` | ++-------------------+------------+---------------------------------------------------------+ +| ``fst`` | All | Generate FST traces using Yosys's sim command. | +| | | Values: ``on``, ``off``. Default: ``off`` | ++-------------------+------------+---------------------------------------------------------+ +| ``aigsmt`` | All | Which SMT2 solver to use for converting AIGER witnesses | +| | | to counter example traces. Use ``none`` to disable | +| | | conversion of AIGER witnesses. Default: ``yices`` | ++-------------------+------------+---------------------------------------------------------+ +| ``tbtop`` | All | The top module for generated Verilog test benches, as | +| | | hierarchical path relative to the design top module. | ++-------------------+------------+---------------------------------------------------------+ +| ``make_model`` | All | Force generation of the named formal models. Takes a | +| | | comma-separated list of model names. For a model | +| | | ```` this will generate the | +| | | ``model/design_.*`` files within the working | +| | | directory, even when not required to run the task. | ++-------------------+------------+---------------------------------------------------------+ +| ``smtc`` | ``bmc``, | Pass this ``.smtc`` file to the smtbmc engine. All | +| | ``prove``, | other engines are disabled when this option is used. | +| | ``cover`` | Default: None | ++-------------------+------------+---------------------------------------------------------+ +| ``depth`` | ``bmc``, | Depth of the bounded model check. Only the specified | +| | ``cover`` | number of cycles are considered. Default: ``20`` | +| +------------+---------------------------------------------------------+ +| | ``prove`` | Depth for the k-induction performed by the ``smtbmc`` | +| | | engine. Other engines ignore this option in ``prove`` | +| | | mode. Default: ``20`` | ++-------------------+------------+---------------------------------------------------------+ +| ``skip`` | ``bmc``, | Skip the specified number of time steps. Only valid | +| | ``cover`` | with smtbmc engine. All other engines are disabled when | +| | | this option is used. Default: None | ++-------------------+------------+---------------------------------------------------------+ +| ``append`` | ``bmc``, | When generating a counter-example trace, add the | +| | ``prove``, | specified number of cycles at the end of the trace. | +| | ``cover`` | Default: ``0`` | ++-------------------+------------+---------------------------------------------------------+ +| ``append_assume`` | ``bmc``, | Uphold assumptions when appending cycles at the end of | +| | ``prove``, | the trace. Depending on the engine and options used | +| | ``cover`` | this may be implicitly on or not supported (as | +| | | indicated in SBY's log output). | +| | | Values: ``on``, ``off``. Default: ``on`` | ++-------------------+------------+---------------------------------------------------------+ Engines section --------------- @@ -197,40 +221,75 @@ solver options. In the 2nd line ``abc`` is the engine, there are no engine options, ``sim3`` is the solver, and ``-W 15`` are solver options. +The following mode/engine/solver combinations are currently supported: + ++-----------+--------------------------+ +| Mode | Engine | ++===========+==========================+ +| ``bmc`` | ``smtbmc [all solvers]`` | +| | | +| | ``btor btormc`` | +| | | +| | ``btor pono`` | +| | | +| | ``abc bmc3`` | +| | | +| | ``abc sim3`` | +| | | +| | ``aiger smtbmc`` | ++-----------+--------------------------+ +| ``prove`` | ``smtbmc [all solvers]`` | +| | | +| | ``abc pdr`` | +| | | +| | ``aiger avy`` | +| | | +| | ``aiger suprove`` | ++-----------+--------------------------+ +| ``cover`` | ``smtbmc [all solvers]`` | +| | | +| | ``btor btormc`` | ++-----------+--------------------------+ +| ``live`` | ``aiger suprove`` | ++-----------+--------------------------+ + ``smtbmc`` engine ~~~~~~~~~~~~~~~~~ The ``smtbmc`` engine supports the ``bmc``, ``prove``, and ``cover`` modes and supports the following options: -+-----------------+---------------------------------------------------------+ -| Option | Description | -+=================+=========================================================+ -| ``--nomem`` | Don't use the SMT theory of arrays to model memories. | -| | Instead synthesize memories to registers and address | -| | logic. | -+-----------------+---------------------------------------------------------+ -| ``--syn`` | Synthesize the circuit to a gate-level representation | -| | instead of using word-level SMT operators. This also | -| | runs some low-level logic optimization on the circuit. | -+-----------------+---------------------------------------------------------+ -| ``--stbv`` | Use large bit vectors (instead of uninterpreted | -| | functions) to represent the circuit state. | -+-----------------+---------------------------------------------------------+ -| ``--stdt`` | Use SMT-LIB 2.6 datatypes to represent states. | -+-----------------+---------------------------------------------------------+ -| ``--nopresat`` | Do not run "presat" SMT queries that make sure that | -| | assumptions are non-conflicting (and potentially | -| | warmup the SMT solver). | -+-----------------+---------------------------------------------------------+ -| ``--unroll``, | Disable/enable unrolling of the SMT problem. The | -| ``--nounroll`` | default value depends on the solver being used. | -+-----------------+---------------------------------------------------------+ -| ``--dumpsmt2`` | Write the SMT2 trace to an additional output file. | -| | (Useful for benchmarking and troubleshooting.) | -+-----------------+---------------------------------------------------------+ -| ``--progress`` | Enable Yosys-SMTBMC timer display. | -+-----------------+---------------------------------------------------------+ ++------------------+---------------------------------------------------------+ +| Option | Description | ++==================+=========================================================+ +| ``--nomem`` | Don't use the SMT theory of arrays to model memories. | +| | Instead synthesize memories to registers and address | +| | logic. | ++------------------+---------------------------------------------------------+ +| ``--syn`` | Synthesize the circuit to a gate-level representation | +| | instead of using word-level SMT operators. This also | +| | runs some low-level logic optimization on the circuit. | ++------------------+---------------------------------------------------------+ +| ``--stbv`` | Use large bit vectors (instead of uninterpreted | +| | functions) to represent the circuit state. | ++------------------+---------------------------------------------------------+ +| ``--stdt`` | Use SMT-LIB 2.6 datatypes to represent states. | ++------------------+---------------------------------------------------------+ +| ``--nopresat`` | Do not run "presat" SMT queries that make sure that | +| | assumptions are non-conflicting (and potentially | +| | warmup the SMT solver). | ++------------------+---------------------------------------------------------+ +| ``--keep-going`` | In BMC mode, continue after the first failed assertion | +| | and report further failed assertions. | ++------------------+---------------------------------------------------------+ +| ``--unroll``, | Disable/enable unrolling of the SMT problem. The | +| ``--nounroll`` | default value depends on the solver being used. | ++------------------+---------------------------------------------------------+ +| ``--dumpsmt2`` | Write the SMT2 trace to an additional output file. | +| | (Useful for benchmarking and troubleshooting.) | ++------------------+---------------------------------------------------------+ +| ``--progress`` | Enable Yosys-SMTBMC timer display. | ++------------------+---------------------------------------------------------+ Any SMT2 solver that is compatible with ``yosys-smtbmc`` can be passed as argument to the ``smtbmc`` engine. The solver options are passed to the solver @@ -238,14 +297,32 @@ as additional command line options. The following solvers are currently supported by ``yosys-smtbmc``: - * yices - * boolector - * z3 - * mathsat - * cvc4 +* yices +* boolector +* bitwuzla +* z3 +* mathsat +* cvc4 +* cvc5 Any additional options after ``--`` are passed to ``yosys-smtbmc`` as-is. +``btor`` engine +~~~~~~~~~~~~~~~ + +The ``btor`` engine supports hardware modelcheckers that accept btor2 files. +The engine supports no engine options and supports the following solvers: + ++-------------------------------+---------------------------------+ +| Solver | Modes | ++===============================+=================================+ +| ``btormc`` | ``bmc``, ``cover`` | ++-------------------------------+---------------------------------+ +| ``pono`` | ``bmc`` | ++-------------------------------+---------------------------------+ + +Solver options are passed to the solver as additional command line options. + ``aiger`` engine ~~~~~~~~~~~~~~~~ @@ -260,7 +337,7 @@ solvers: +-------------------------------+---------------------------------+ | ``avy`` | ``prove`` | +-------------------------------+---------------------------------+ -| ``aigbmc`` | ``prove``, ``live`` | +| ``aigbmc`` | ``bmc`` | +-------------------------------+---------------------------------+ Solver options are passed to the solver as additional command line options. @@ -285,6 +362,15 @@ solvers: Solver options are passed as additional arguments to the ABC command implementing the solver. + +``none`` engine +~~~~~~~~~~~~~~~ + +The ``none`` engine does not run any solver. It can be used together with the +``make_model`` option to manually generate any model supported by one of the +other engines. This makes it easier to use the same models outside of sby. + + Script section -------------- @@ -321,6 +407,7 @@ Run ``yosys`` in a terminal window and enter ``help`` on the Yosys prompt for a command list. Run ``help `` for a detailed description of the command, for example ``help prep``. + Files section ------------- diff --git a/docs/source/requirements.txt b/docs/source/requirements.txt index 954b4546..0e4756ee 100644 --- a/docs/source/requirements.txt +++ b/docs/source/requirements.txt @@ -1 +1,2 @@ -sphinx-press-theme +furo +sphinx-argparse diff --git a/docs/source/usage.rst b/docs/source/usage.rst new file mode 100644 index 00000000..bc8e4e98 --- /dev/null +++ b/docs/source/usage.rst @@ -0,0 +1,11 @@ +Using `sby` +=========== + +Once SBY is installed and available on the command line as `sby`, either built from source or using +one of the available CAD suites, it can be called as follows. Note that this information is also +available via `sby --help`. For more information on installation, see :ref:`install-doc`. + +.. argparse:: + :module: sby_cmdline + :func: parser_func + :prog: sby diff --git a/docs/static/custom.css b/docs/static/custom.css index 40a8c178..b23ce2de 100644 --- a/docs/static/custom.css +++ b/docs/static/custom.css @@ -1 +1,18 @@ -/* empty */ +/* Don't hide the right sidebar as we're placing our fixed links there */ +aside.no-toc { + display: block !important; +} + +/* Colorful headings */ +h1 { + color: var(--color-brand-primary); +} + +h2, h3, h4, h5, h6 { + color: var(--color-brand-content); +} + +/* Use a different color for external links */ +a.external { + color: var(--color-brand-primary) !important; +} diff --git a/docs/static/yosyshq.css b/docs/static/yosyshq.css deleted file mode 100644 index 1ceebe91..00000000 --- a/docs/static/yosyshq.css +++ /dev/null @@ -1,64 +0,0 @@ -h1, h3, p.topic-title, .content li.toctree-l1 > a { - color: #d6368f !important; -} - -h2, p.admonition-title, dt, .content li.toctree-l2 > a { - color: #4b72b8; -} - -a { - color: #8857a3; -} - -a.current, a:hover, a.external { - color: #d6368f !important; -} - -a.external:hover { - text-decoration: underline; -} - -p { - text-align: justify; -} - -.vp-sidebar a { - color: #d6368f; -} - -.vp-sidebar li li a { - color: #4b72b8; -} - -.vp-sidebar li li li a { - color: #2c3e50; - font-weight: 400; -} - -.vp-sidebar h3 { - padding-left: 1.5rem !important; -} - -.vp-sidebar ul a { - padding-left: 1.5rem !important; -} - -.vp-sidebar ul ul a { - padding-left: 3rem !important; -} - -.vp-sidebar ul ul ul a { - padding-left: 4.5rem !important; -} - -.vp-sidebar .toctree-l1.current a { - border-left: 0.5rem solid #6ecbd7; -} - -.vp-sidebar .toctree-l1 a.current { - border-left: 0.5rem solid #8857a3; -} - -.injected .rst-current-version, .injected dt { - color: #6ecbd7 !important; -} diff --git a/sbysrc/demo1.sby b/sbysrc/demo1.sby deleted file mode 100644 index 6c89f183..00000000 --- a/sbysrc/demo1.sby +++ /dev/null @@ -1,21 +0,0 @@ - -[options] -mode bmc -depth 10 -wait on - -[engines] -smtbmc yices -smtbmc boolector -ack -smtbmc --nomem z3 -abc bmc3 - -[script] -read_verilog -formal -norestrict -assume-asserts picorv32.v -read_verilog -formal axicheck.v -prep -top testbench - -[files] -picorv32.v ../extern/picorv32.v -axicheck.v ../extern/axicheck.v - diff --git a/sbysrc/sby.py b/sbysrc/sby.py index 58f02d80..c21ab0ba 100644 --- a/sbysrc/sby.py +++ b/sbysrc/sby.py @@ -17,72 +17,17 @@ # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. # -import argparse, os, sys, shutil, tempfile, re +import json, os, sys, shutil, tempfile, re ##yosys-sys-path## -from sby_core import SbyTask, SbyAbort, process_filename -from time import localtime - -class DictAction(argparse.Action): - def __call__(self, parser, namespace, values, option_string=None): - assert isinstance(getattr(namespace, self.dest), dict), f"Use ArgumentParser.set_defaults() to initialize {self.dest} to dict()" - name = option_string.lstrip(parser.prefix_chars).replace("-", "_") - getattr(namespace, self.dest)[name] = values - -parser = argparse.ArgumentParser(prog="sby", - usage="%(prog)s [options] [.sby [tasknames] | ]") -parser.set_defaults(exe_paths=dict()) - -parser.add_argument("-d", metavar="", dest="workdir", - help="set workdir name. default: or _. When there is more than one task, use --prefix instead") -parser.add_argument("--prefix", metavar="", dest="workdir_prefix", - help="set the workdir name prefix. `_` will be appended to the path for each task") -parser.add_argument("-f", action="store_true", dest="force", - help="remove workdir if it already exists") -parser.add_argument("-b", action="store_true", dest="backup", - help="backup workdir if it already exists") -parser.add_argument("-t", action="store_true", dest="tmpdir", - help="run in a temporary workdir (remove when finished)") -parser.add_argument("-T", metavar="", action="append", dest="tasknames", default=list(), - help="add taskname (useful when sby file is read from stdin)") -parser.add_argument("-E", action="store_true", dest="throw_err", - help="throw an exception (incl stack trace) for most errors") - -parser.add_argument("--yosys", metavar="", - action=DictAction, dest="exe_paths") -parser.add_argument("--abc", metavar="", - action=DictAction, dest="exe_paths") -parser.add_argument("--smtbmc", metavar="", - action=DictAction, dest="exe_paths") -parser.add_argument("--suprove", metavar="", - action=DictAction, dest="exe_paths") -parser.add_argument("--aigbmc", metavar="", - action=DictAction, dest="exe_paths") -parser.add_argument("--avy", metavar="", - action=DictAction, dest="exe_paths") -parser.add_argument("--btormc", metavar="", - action=DictAction, dest="exe_paths") -parser.add_argument("--pono", metavar="", - action=DictAction, dest="exe_paths", - help="configure which executable to use for the respective tool") -parser.add_argument("--dumpcfg", action="store_true", dest="dump_cfg", - help="print the pre-processed configuration file") -parser.add_argument("--dumptags", action="store_true", dest="dump_tags", - help="print the list of task tags") -parser.add_argument("--dumptasks", action="store_true", dest="dump_tasks", - help="print the list of tasks") -parser.add_argument("--dumpdefaults", action="store_true", dest="dump_defaults", - help="print the list of default tasks") -parser.add_argument("--dumpfiles", action="store_true", dest="dump_files", - help="print the list of source files") -parser.add_argument("--setup", action="store_true", dest="setupmode", - help="set up the working directory and exit") - -parser.add_argument("--init-config-file", dest="init_config_file", - help="create a default .sby config file") -parser.add_argument("sbyfile", metavar=".sby | ", nargs="?", - help=".sby file OR directory containing config.sby file") -parser.add_argument("arg_tasknames", metavar="tasknames", nargs="*", - help="tasks to run (only valid when .sby is used)") +from sby_cmdline import parser_func +from sby_core import SbyConfig, SbyTask, SbyAbort, SbyTaskloop, process_filename, dress_message +from sby_jobserver import SbyJobClient, process_jobserver_environment +from sby_status import SbyStatusDb +import time, platform, click + +process_jobserver_environment() # needs to be called early + +parser = parser_func() args = parser.parse_args() @@ -102,10 +47,51 @@ def __call__(self, parser, namespace, values, option_string=None): dump_tags = args.dump_tags dump_tasks = args.dump_tasks dump_defaults = args.dump_defaults +dump_taskinfo = args.dump_taskinfo dump_files = args.dump_files reusedir = False setupmode = args.setupmode +autotune = args.autotune +autotune_config = args.autotune_config +sequential = args.sequential +jobcount = args.jobcount init_config_file = args.init_config_file +status_show = args.status +status_reset = args.status_reset + +if status_show or status_reset: + target = workdir_prefix or workdir or sbyfile + if target is None: + print("ERROR: Specify a .sby config file or working directory to use --status.") + sys.exit(1) + if not os.path.isdir(target) and target.endswith('.sby'): + target = target[:-4] + if not os.path.isdir(target): + print(f"ERROR: No directory found at {target!r}.", file=sys.stderr) + sys.exit(1) + + try: + with open(f"{target}/status.path", "r") as status_path_file: + status_path = f"{target}/{status_path_file.read().rstrip()}" + except FileNotFoundError: + status_path = f"{target}/status.sqlite" + + if not os.path.exists(status_path): + print(f"ERROR: No status database found at {status_path!r}.", file=sys.stderr) + sys.exit(1) + + status_db = SbyStatusDb(status_path, task=None) + + if status_show: + status_db.print_status_summary() + sys.exit(0) + + if status_reset: + status_db.reset() + + status_db.db.close() + sys.exit(0) + if sbyfile is not None: if os.path.isdir(sbyfile): @@ -156,9 +142,8 @@ def __call__(self, parser, namespace, values, option_string=None): early_logmsgs = list() def early_log(workdir, msg): - tm = localtime() - early_logmsgs.append("SBY {:2d}:{:02d}:{:02d} [{}] {}".format(tm.tm_hour, tm.tm_min, tm.tm_sec, workdir, msg)) - print(early_logmsgs[-1]) + early_logmsgs.append(dress_message(workdir, msg)) + click.echo(early_logmsgs[-1]) def read_sbyconfig(sbydata, taskname): cfgdata = list() @@ -312,6 +297,8 @@ def handle_line(line): sbydata = list() +if sbyfile is None: + print("Reading .sby configuration from stdin:") with (open(sbyfile, "r") if sbyfile is not None else sys.stdin) as f: for line in f: sbydata.append(line) @@ -367,6 +354,22 @@ def find_files(taskname): print(name) sys.exit(0) +if dump_taskinfo: + _, _, tasknames, _ = read_sbyconfig(sbydata, None) + taskinfo = {} + for taskname in tasknames or [None]: + task_sbyconfig, _, _, _ = read_sbyconfig(sbydata, taskname) + taskinfo[taskname or ""] = info = {} + cfg = SbyConfig() + cfg.parse_config(task_sbyconfig) + taskinfo[taskname or ""] = { + "mode": cfg.options.get("mode"), + "engines": cfg.engines, + "script": cfg.script, + } + print(json.dumps(taskinfo, indent=2)) + sys.exit(0) + if len(tasknames) == 0: _, _, tasknames, _ = read_sbyconfig(sbydata, None) if len(tasknames) == 0: @@ -376,18 +379,37 @@ def find_files(taskname): print("ERROR: Exactly one task is required when workdir is specified. Specify the task or use --prefix instead of -d.", file=sys.stderr) sys.exit(1) -def run_task(taskname): +# Check there are no files in this dir or any of its subdirs +def check_dirtree_empty_of_files(dir): + list = os.listdir(dir) + if list: + for fn in list: + child_dir = os.path.join(dir, fn) + if os.path.isdir(child_dir) and check_dirtree_empty_of_files(child_dir): + continue + return False + return True + +def start_task(taskloop, taskname): + sbyconfig, _, _, _ = read_sbyconfig(sbydata, taskname) + my_opt_tmpdir = opt_tmpdir my_workdir = None + my_status_db = None if workdir is not None: my_workdir = workdir elif workdir_prefix is not None: - my_workdir = workdir_prefix + "_" + taskname + if taskname is None: + my_workdir = workdir_prefix + else: + my_workdir = workdir_prefix + "_" + taskname + my_status_db = f"../{os.path.basename(workdir_prefix)}/status.sqlite" if my_workdir is None and sbyfile is not None and not my_opt_tmpdir: my_workdir = sbyfile[:-4] if taskname is not None: + my_status_db = f"../{os.path.basename(my_workdir)}/status.sqlite" my_workdir += "_" + taskname if my_workdir is not None: @@ -405,16 +427,28 @@ def run_task(taskname): if reusedir: pass - elif os.path.isdir(my_workdir): - print(f"ERROR: Directory '{my_workdir}' already exists.") + elif os.path.isdir(my_workdir) and not check_dirtree_empty_of_files(my_workdir): + print(f"ERROR: Directory '{my_workdir}' already exists, use -f to overwrite the existing directory.") sys.exit(1) - else: + elif not os.path.isdir(my_workdir): os.makedirs(my_workdir) else: my_opt_tmpdir = True my_workdir = tempfile.mkdtemp() + if os.getenv("SBY_WORKDIR_GITIGNORE"): + with open(f"{my_workdir}/.gitignore", "w") as gitignore: + print("*", file=gitignore) + + if my_status_db is not None: + os.makedirs(f"{my_workdir}/{os.path.dirname(my_status_db)}", exist_ok=True) + if os.getenv("SBY_WORKDIR_GITIGNORE"): + with open(f"{my_workdir}/{os.path.dirname(my_status_db)}/.gitignore", "w") as gitignore: + print("*", file=gitignore) + with open(f"{my_workdir}/status.path", "w") as status_path: + print(my_status_db, file=status_path) + junit_ts_name = os.path.basename(sbyfile[:-4]) if sbyfile is not None else workdir if workdir is not None else "stdin" junit_tc_name = taskname if taskname is not None else "default" @@ -429,66 +463,98 @@ def run_task(taskname): else: junit_filename = "junit" - sbyconfig, _, _, _ = read_sbyconfig(sbydata, taskname) - task = SbyTask(sbyconfig, my_workdir, early_logmsgs, reusedir) + task = SbyTask(sbyconfig, my_workdir, early_logmsgs, reusedir, taskloop) for k, v in exe_paths.items(): task.exe_paths[k] = v - if throw_err: - task.run(setupmode) - else: - try: - task.run(setupmode) - except SbyAbort: - pass + def exit_callback(): + if not autotune and not setupmode: + task.summarize() + task.write_summary_file() - if my_opt_tmpdir: - task.log(f"Removing directory '{my_workdir}'.") - shutil.rmtree(my_workdir, ignore_errors=True) + if my_opt_tmpdir: + task.log(f"Removing directory '{my_workdir}'.") + shutil.rmtree(my_workdir, ignore_errors=True) - if setupmode: - task.log(f"SETUP COMPLETE (rc={task.retcode})") - else: - task.log(f"DONE ({task.status}, rc={task.retcode})") - task.logfile.close() - - if not my_opt_tmpdir and not setupmode: - with open("{}/{}.xml".format(task.workdir, junit_filename), "w") as f: - junit_errors = 1 if task.retcode == 16 else 0 - junit_failures = 1 if task.retcode != 0 and junit_errors == 0 else 0 - print('', file=f) - print(f'', file=f) - print(f'', file=f) - print('', file=f) - print(f'', file=f) - print('', file=f) - print(f'', file=f) - if junit_errors: - print(f'', file=f) - if junit_failures: - print(f'', file=f) - print('', end="", file=f) - with open(f"{task.workdir}/logfile.txt", "r") as logf: - for line in logf: - print(line.replace("&", "&").replace("<", "<").replace(">", ">").replace("\"", """), end="", file=f) - print('', file=f) - with open(f"{task.workdir}/status", "w") as f: - print(f"{task.status} {task.retcode} {task.total_time}", file=f) - - return task.retcode + if setupmode: + task.log(f"SETUP COMPLETE (rc={task.retcode})") + else: + task.log(f"DONE ({task.status}, rc={task.retcode})") + task.logfile.close() + + if not my_opt_tmpdir and not setupmode and not autotune: + with open("{}/{}.xml".format(task.workdir, junit_filename), "w") as f: + task.print_junit_result(f, junit_ts_name, junit_tc_name, junit_format_strict=False) + with open(f"{task.workdir}/status", "w") as f: + print(f"{task.status} {task.retcode} {task.total_time}", file=f) + + task.exit_callback = exit_callback + + if not autotune: + task.setup_procs(setupmode) + task.task_local_abort = not throw_err + + return task failed = [] retcode = 0 -for task in tasknames: - task_retcode = run_task(task) - retcode |= task_retcode - if task_retcode: - failed.append(task) + +if jobcount is not None and jobcount < 1: + print("ERROR: The -j option requires a positive number as argument") + sys.exit(1) + +# Autotune is already parallel, parallelizing it across tasks needs some more work +if autotune: + sequential = True + +if sequential: + if autotune: + jobclient = None # TODO make autotune use a jobclient + else: + jobclient = SbyJobClient(jobcount) + + for taskname in tasknames: + taskloop = SbyTaskloop(jobclient) + try: + task = start_task(taskloop, taskname) + except SbyAbort: + if throw_err: + raise + sys.exit(1) + + if autotune: + from sby_autotune import SbyAutotune + SbyAutotune(task, autotune_config).run() + elif setupmode: + task.exit_callback() + else: + taskloop.run() + retcode |= task.retcode + if task.retcode: + failed.append(taskname) +else: + jobclient = SbyJobClient(jobcount) + taskloop = SbyTaskloop(jobclient) + + tasks = {} + for taskname in tasknames: + try: + tasks[taskname] = start_task(taskloop, taskname) + except SbyAbort: + if throw_err: + raise + sys.exit(1) + + taskloop.run() + + for taskname, task in tasks.items(): + retcode |= task.retcode + if task.retcode: + failed.append(taskname) if failed and (len(tasknames) > 1 or tasknames[0] is not None): - tm = localtime() - print("SBY {:2d}:{:02d}:{:02d} The following tasks failed: {}".format(tm.tm_hour, tm.tm_min, tm.tm_sec, failed)) + click.echo(dress_message(None, click.style(f"The following tasks failed: {failed}", fg="red", bold=True))) sys.exit(retcode) diff --git a/sbysrc/sby_autotune.py b/sbysrc/sby_autotune.py new file mode 100644 index 00000000..b861890a --- /dev/null +++ b/sbysrc/sby_autotune.py @@ -0,0 +1,685 @@ +# +# SymbiYosys (sby) -- Front-end for Yosys-based formal verification flows +# +# Copyright (C) 2022 Jannis Harder +# +# Permission to use, copy, modify, and/or distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +# + +import os +import re +import subprocess +from shutil import rmtree, which +from time import monotonic +from sby_core import SbyAbort, SbyTask + + +class SbyAutotuneConfig: + """Autotune configuration parsed from the sby file or an external autotune config + file. + """ + def __init__(self): + self.timeout = None + self.soft_timeout = 60 + self.wait_percentage = 50 + self.wait_seconds = 10 + self.parallel = "auto" + + self.presat = None + self.incr = "auto" + self.incr_threshold = 20 + self.mem = "auto" + self.mem_threshold = 10240 + self.forall = "auto" + + def config_line(self, log, line, file_kind="sby file"): + option, *arg = line.split(None, 1) + if not arg: + log.error(f"{file_kind} syntax error: {line}") + arg = arg[0].strip() + + BOOL_OR_ANY = {"on": True, "off": False, "any": None} + BOOL_ANY_OR_AUTO = {"on": True, "off": False, "any": None, "auto": "auto"} + ON_ANY_OR_AUTO = {"on": True, "any": None, "auto": "auto"} + + def enum_option(values): + if arg not in values: + values_str = ', '.join(repr(value) for value in sorted(values)) + log.error(f"{file_kind}: invalid value '{arg}' for autotune option {option}, valid values are: {values_str}") + return values[arg] + + def int_option(): + try: + return int(arg) + except ValueError: + log.error(f"{file_kind}: invalid value '{arg}' for autotune option {option}, expected an integer value") + + if option == "timeout": + self.timeout = "none" if arg == "none" else int_option() + elif option == "soft_timeout": + self.soft_timeout = int_option() + elif option == "wait": + self.wait_percentage = 0 + self.wait_seconds = 0 + for part in arg.split("+"): + part = part.strip() + if part.endswith("%"): + self.wait_percentage += int(part[:-1].strip()) + else: + self.wait_seconds += int(part) + elif option == "parallel": + self.parallel = "auto" if arg == "auto" else int_option() + elif option == "presat": + self.presat = enum_option(BOOL_OR_ANY) + elif option == "incr": + self.incr = enum_option(BOOL_ANY_OR_AUTO) + elif option == "incr_threshold": + self.incr_threshold = int_option() + elif option == "mem": + self.mem = enum_option(ON_ANY_OR_AUTO) + elif option == "mem_threshold": + self.mem_threshold = int_option() + elif option == "forall": + self.forall = enum_option(ON_ANY_OR_AUTO) + else: + log.error(f"{file_kind} syntax error: {line}") + + def parse_file(self, log, file): + for line in file: + line = re.sub(r"\s*(\s#.*)?$", "", line) + if line == "" or line[0] == "#": + continue + self.config_line(log, line.rstrip(), "autotune configuration file") + +class SbyAutotuneCandidate: + """An engine configuration to try and its current state during autotuning. + """ + def __init__(self, autotune, engine): + self.autotune = autotune + self.engine = engine + + self.state = "pending" + self.engine_idx = None + self.info = f"{' '.join(self.engine)}:" + self.suspended = 0 + self.suspend = 1 + + self.engine_retcode = None + self.engine_status = None + self.total_adjusted_time = None + + self.soft_timeout = self.autotune.config.soft_timeout + + if tuple(self.engine) not in self.autotune.candidate_engines: + self.autotune.active_candidates.append(self) + self.autotune.candidate_engines.add(tuple(self.engine)) + + def set_engine_idx(self, idx): + self.engine_idx = idx + self.info = f"engine_{idx} ({' '.join(self.engine)}):" + + def set_running(self): + assert not self.suspended + assert self.state == "pending" + assert self in self.autotune.active_candidates + self.state = "running" + + def retry_later(self): + assert self.state == "running" + assert self in self.autotune.active_candidates + self.state = "pending" + self.soft_timeout *= 2 + self.suspended = self.suspend + + def timed_out(self): + assert self.state == "running" + self.autotune.active_candidates.remove(self) + self.state = "timeout" + + def failed(self): + assert self.state == "running" + self.autotune.active_candidates.remove(self) + self.autotune.failed_candidates.append(self) + self.state = "failed" + + def finished(self): + assert self.state == "running" + self.autotune.active_candidates.remove(self) + self.autotune.finished_candidates.append(self) + self.state = "finished" + + def threads(self): + if self.autotune.config.mode == "prove" and self.engine[0] == "smtbmc": + return 2 + return 1 + + +class SbyAutotune: + """Performs automatic engine selection for a given task. + """ + def __init__(self, task, config_file=None): + self.task_exit_callback = task.exit_callback + task.exit_callback = lambda: None + task.check_timeout = lambda: None + task.status = "TIMEOUT" + task.retcode = 8 + + task.proc_failed = self.proc_failed + + self.config = None + + if config_file: + with open(config_file) as config: + self.config.parse_file(task, config) + + self.task = task + + self.done = False + self.threads_running = 0 + + self.next_engine_idx = 0 + + self.model_requests = {} + + self.timeout = None + self.best_time = None + self.have_pending_candidates = False + + self.active_candidates = [] + self.finished_candidates = [] + self.failed_candidates = [] + + self.candidate_engines = set() + + def available(self, tool): + if not which(tool): + return False + + if tool == "btorsim": + error_msg = subprocess.run( + ["btorsim", "--vcd"], + capture_output=True, + text=True, + ).stderr + if "invalid command line option" in error_msg: + self.log('found version of "btorsim" is too old and does not support the --vcd option') + return False + + return True + + def candidate(self, *engine): + flat_engine = [] + def flatten(part): + if part is None: + return + elif isinstance(part, (tuple, list)): + for subpart in part: + flatten(subpart) + else: + flat_engine.append(part) + + flatten(engine) + + SbyAutotuneCandidate(self, flat_engine) + + def configure(self): + self.config.mode = self.task.opt_mode + self.config.skip = self.task.opt_skip + + if self.config.incr == "auto": + self.config.incr = None + if self.config.mode != "live": + steps = self.task.opt_depth - (self.config.skip or 0) + if steps > self.config.incr_threshold: + self.log(f"checking more than {self.config.incr_threshold} timesteps ({steps}), disabling nonincremental smtbmc") + self.config.incr = True + + if self.config.mem == "auto": + self.config.mem = None + if self.task.design is None: + self.log("warning: unknown amount of memory bits in design") + elif self.task.design.memory_bits > self.config.mem_threshold: + self.log( + f"more than {self.config.mem_threshold} bits of memory in design ({self.task.design.memory_bits} bits), " + "disabling engines without native memory support" + ) + self.config.mem = True + + if self.config.forall == "auto": + self.config.forall = None + if self.task.design.forall: + self.log("design uses $allconst/$allseq, disabling engines without forall support") + self.config.forall = True + + if self.config.mode not in ["bmc", "prove"]: + self.config.presat = None + + if self.config.parallel == "auto": + try: + self.config.parallel = len(os.sched_getaffinity(0)) + except AttributeError: + self.config.parallel = os.cpu_count() # TODO is this correct? + + if self.config.timeout is None: + self.config.timeout = self.task.opt_timeout + elif self.config.timeout == "none": + self.config.timeout = None + + def build_candidates(self): + if self.config.mode == "live": + # Not much point in autotuning here... + self.candidate("aiger", "suprove") + return + + if self.config.presat is None: + presat_flags = [None, "--nopresat"] + elif self.config.presat: + presat_flags = [None] + else: + presat_flags = ["--nopresat"] + + if self.config.incr is None: + noincr_flags = [None, ["--", "--noincr"]] + elif self.config.incr: + noincr_flags = [None] + else: + noincr_flags = [["--", "--noincr"]] + + if self.config.forall: + self.log('disabling engines "smtbmc boolector" and "smtbmc bitwuzla" as they do not support forall') + else: + for solver in ["boolector", "bitwuzla"]: + if not self.available(solver): + self.log(f'disabling engine "smtbmc {solver}" as the solver "{solver}" was not found') + continue + for noincr in noincr_flags: + for presat in presat_flags: + self.candidate("smtbmc", presat, solver, noincr) + + if not self.available("btorsim"): + self.log('disabling engine "btor" as the "btorsim" tool was not found') + elif self.config.forall: + self.log('disabling engine "btor" as it does not support forall') + else: + if self.config.mode in ["bmc", "cover"]: + if not self.available("btormc"): + self.log('disabling engine "btor btormc" as the "btormc" tool was not found') + elif self.config.presat: + self.log('disabling engine "btor btormc" as it does not support presat checking') + else: + self.candidate("btor", "btormc") + + if self.config.mode == "bmc": + if not self.available("pono"): + self.log('disabling engine "btor btormc" as the "btormc" tool was not found') + elif self.config.presat: + self.log('disabling engine "btor pono" as it does not support presat checking') + elif self.config.skip: + self.log('disabling engine "btor pono" as it does not support the "skip" option') + else: + self.candidate("btor", "pono") + + for solver in ["yices", "z3"]: + if not self.available(solver): + self.log(f'disabling engine "smtbmc {solver}" as the solver "{solver}" was not found') + continue + for unroll in ["--unroll", "--nounroll"]: + if solver == "yices" and self.config.forall: + self.log('disabling engine "smtbmc yices" due to limited forall support') + # TODO yices implicitly uses --noincr for forall problems and + # requires --stbv which does not play well with memory, still test it? + continue + + stmode = "--stdt" if self.config.forall else None + + for noincr in noincr_flags: + for presat in presat_flags: + self.candidate("smtbmc", presat, stmode, unroll, solver, noincr) + + if self.config.mode not in ["bmc", "prove"]: + pass + elif self.config.presat: + self.log('disabling engines "abc" and "aiger" as they do not support presat checking') + elif self.config.forall: + self.log('disabling engines "abc" and "aiger" as they do not support forall') + elif self.config.mem: + self.log('disabling engines "abc" and "aiger" as they do not support memory') + elif self.config.skip: + self.log('disabling engines "abc" and "aiger" as they do not support the "skip" option') + elif self.config.mode == "bmc": + self.candidate("abc", "bmc3") + + if not self.available("aigbmc"): + self.log('disabling engine "aiger aigbmc" as the "aigbmc" tool was not found') + else: + self.candidate("aiger", "aigbmc") + # abc sim3 will never finish + elif self.config.mode == "prove": + self.candidate("abc", "pdr") + + if not self.available("suprove"): + self.log('disabling engine "aiger suprove" as the "suprove" tool was not found') + else: + self.candidate("aiger", "suprove") + # avy seems to crash in the presence of assumptions + + def log(self, message): + self.task.log(message) + + def run(self): + self.task.handle_non_engine_options() + self.task.setup_status_db(':memory:') + self.config = self.task.autotune_config or SbyAutotuneConfig() + + if "expect" not in self.task.options: + self.task.expect = ["PASS", "FAIL"] + # TODO check that solvers produce consistent results? + + if "TIMEOUT" in self.task.expect: + self.task.error("cannot autotune a task with option 'expect timeout'") + + if self.task.reusedir: + rmtree(f"{self.task.workdir}/model", ignore_errors=True) + else: + self.task.copy_src() + + self.model(None, "prep") + self.task.taskloop.run() + + if self.task.status == "ERROR": + return + + self.configure() + + self.build_candidates() + if not self.active_candidates: + self.task.error("no supported engines found for the current configuration and design") + self.log(f"testing {len(self.active_candidates)} engine configurations...") + + self.start_engines() + self.task.taskloop.run() + + self.finished_candidates.sort(key=lambda candidate: candidate.total_adjusted_time) + + if self.failed_candidates: + self.log("failed engines:") + for candidate in self.failed_candidates: + self.log( + f" engine_{candidate.engine_idx}: {' '.join(candidate.engine)}" + f" (returncode={candidate.engine_retcode} status={candidate.engine_status})" + ) + + if self.finished_candidates: + self.log("finished engines:") + for place, candidate in list(enumerate(self.finished_candidates, 1))[::-1]: + self.log( + f" #{place}: engine_{candidate.engine_idx}: {' '.join(candidate.engine)}" + f" ({candidate.total_adjusted_time} seconds, status={candidate.engine_status})" + ) + + if self.finished_candidates: + self.task.status = "AUTOTUNED" + self.task.retcode = 0 + elif self.failed_candidates: + self.task.status = "FAIL" + self.task.retcode = 2 + + self.task_exit_callback() + + def next_candidate(self, peek=False): + # peek=True is used to check whether we need to timeout running candidates to + # give other candidates a chance. + can_retry = None + + for candidate in self.active_candidates: + if candidate.state == "pending": + if not candidate.suspended: + return candidate + if can_retry is None or can_retry.suspended > candidate.suspended: + can_retry = candidate + + if can_retry and not peek: + shift = can_retry.suspended + for candidate in self.active_candidates: + if candidate.state == "pending": + candidate.suspended -= shift + + return can_retry + + def start_engines(self): + self.task.taskloop.poll_now = True + + while self.threads_running < self.config.parallel: + candidate = self.next_candidate() + if candidate is None: + self.have_pending_candidates = False + return + + candidate_threads = candidate.threads() + if self.threads_running: + if self.threads_running + candidate_threads > self.config.parallel: + break + + candidate.set_running() + candidate.set_engine_idx(self.next_engine_idx) + self.next_engine_idx += 1 + + try: + engine_task = SbyAutotuneTask(self, candidate) + pending = sum(c.state == "pending" for c in self.active_candidates) + self.log(f"{candidate.info} starting... ({pending} configurations pending)") + self.threads_running += candidate_threads + engine_task.setup_procs(False) + except SbyAbort: + pass + + self.have_pending_candidates = bool(self.next_candidate(peek=True)) + + def engine_finished(self, engine_task): + self.threads_running -= engine_task.candidate.threads() + + candidate = engine_task.candidate + + time = candidate.total_adjusted_time + + if engine_task.status == "TIMEOUT": + if self.timeout is None or time < self.timeout: + candidate.retry_later() + self.log(f"{candidate.info} timeout ({time} seconds, will be retried if necessary)") + else: + candidate.timed_out() + self.log(f"{candidate.info} timeout ({time} seconds)") + elif engine_task.retcode: + candidate.failed() + self.log(f"{candidate.info} failed (returncode={candidate.engine_retcode} status={candidate.engine_status})") + else: + candidate.finished() + + self.log(f"{candidate.info} succeeded (status={candidate.engine_status})") + + if self.best_time is None: + self.log(f"{candidate.info} took {time} seconds (first engine to finish)") + self.best_time = time + elif time < self.best_time: + self.log(f"{candidate.info} took {time} seconds (best candidate, previous best: {self.best_time} seconds)") + self.best_time = time + else: + self.log(f"{candidate.info} took {time} seconds") + + new_timeout = int(time + self.config.wait_seconds + time * self.config.wait_percentage // 100) + + if self.timeout is None or new_timeout < self.timeout: + self.timeout = new_timeout + + self.start_engines() + + def model(self, engine_task, name): + if self.task not in self.task.taskloop.tasks: + self.task.taskloop.tasks.append(self.task) + if name in self.model_requests: + request = self.model_requests[name] + else: + self.model_requests[name] = request = SbyModelRequest(self, name) + + request.attach_engine_task(engine_task) + + return request.procs + + def proc_failed(self, proc): + for name, request in self.model_requests.items(): + if proc in request.procs: + for task in request.engine_tasks: + task = task or self.task + task.status = "ERROR" + task.log(f"could not prepare model '{name}', see toplevel logfile") + task.terminate() + pass + + +class SbyModelRequest: + """Handles sharing and canceling of model generation from several SbyAutotuneTask + instances. + """ + def __init__(self, autotune, name): + self.autotune = autotune + self.name = name + self.engine_tasks = [] + + autotune.log(f"model '{name}': preparing now...") + + self.make_model() + + def make_model(self): + self.start_time = monotonic() + self.total_time = None + self.min_time = 0 + + self.procs = self.autotune.task.model(self.name) + for proc in self.procs: + proc.register_dep(self) + + def attach_engine_task(self, engine_task): + if self.total_time is None: + if engine_task: + if self.start_time is None: + model_time = 0 + extra_time = self.min_time + else: + model_time = monotonic() - self.start_time + extra_time = max(0, self.min_time - model_time) + + engine_task.model_time += model_time + + engine_task.check_timeout(extra_time) + + if self.start_time is None: + self.make_model() + + self.engine_tasks.append(engine_task) + if engine_task: + engine_task.model_requests.append(self) + + else: + if engine_task: + engine_task.model_time += self.total_time + + def detach_engine_task(self, engine_task): + self.engine_tasks.remove(engine_task) + if not self.engine_tasks and self.total_time is None: + self.autotune.log(f"cancelled model '{self.name}'") + del self.autotune.task.models[self.name] + for proc in self.procs: + proc.terminate(True) + + self.min_time = max(self.min_time, monotonic() - self.start_time) + self.start_time = None + + self.procs = [] + + def poll(self): + if self.total_time is None and all(proc.finished for proc in self.procs): + self.autotune.log(f"prepared model '{self.name}'") + + self.total_time = self.min_time = monotonic() - self.start_time + + +class SbyAutotuneTask(SbyTask): + """Task that shares the workdir with a parent task, runs in parallel to other + autotune tasks and can be cancelled independent from other autotune tasks while + sharing model generation with other tasks. + """ + def __init__(self, autotune, candidate): + task = autotune.task + self.autotune = autotune + self.candidate = candidate + super().__init__( + sbyconfig=None, + workdir=task.workdir, + early_logs=[], + reusedir=True, + taskloop=task.taskloop, + logfile=open(f"{task.workdir}/engine_{candidate.engine_idx}_autotune.txt", "a"), + ) + self.task_local_abort = True + self.log_targets = [self.logfile] + self.exe_paths = autotune.task.exe_paths + self.reusedir = False + self.design = autotune.task.design + + self.model_time = 0 + self.model_requests = [] + + self.exit_callback = self.autotune_exit_callback + + + def parse_config(self, f): + super().parse_config(f) + self.engines = [] + + def engine_list(self): + return [(self.candidate.engine_idx, self.candidate.engine)] + + def copy_src(self): + pass + + def model(self, model_name): + self.log(f"using model '{model_name}'") + return self.autotune.model(self, model_name) + + def autotune_exit_callback(self): + self.summarize() + + self.candidate.total_adjusted_time = int(monotonic() - self.start_clock_time + self.model_time) + self.candidate.engine_retcode = self.retcode + self.candidate.engine_status = self.status + + self.autotune.engine_finished(self) + for request in self.model_requests: + request.detach_engine_task(self) + + def check_timeout(self, extra_time=0): + model_time = self.model_time + extra_time + total_adjusted_time = int(monotonic() - self.start_clock_time + model_time) + + if self.autotune.timeout is not None: + timeout = self.autotune.timeout + else: + if not self.autotune.have_pending_candidates: + return + timeout = self.candidate.soft_timeout + + if not self.timeout_reached and total_adjusted_time >= timeout: + self.log(f"Reached autotune TIMEOUT ({timeout} seconds). Terminating all subprocesses.") + self.status = "TIMEOUT" + self.total_adjusted_time = total_adjusted_time + self.terminate(timeout=True) diff --git a/sbysrc/sby_cmdline.py b/sbysrc/sby_cmdline.py new file mode 100644 index 00000000..bc45b4a5 --- /dev/null +++ b/sbysrc/sby_cmdline.py @@ -0,0 +1,84 @@ +import argparse + +class DictAction(argparse.Action): + def __call__(self, parser, namespace, values, option_string=None): + assert isinstance(getattr(namespace, self.dest), dict), f"Use ArgumentParser.set_defaults() to initialize {self.dest} to dict()" + name = option_string.lstrip(parser.prefix_chars).replace("-", "_") + getattr(namespace, self.dest)[name] = values + +def parser_func(): + parser = argparse.ArgumentParser(prog="sby", + usage="%(prog)s [options] [.sby [tasknames] | ]") + parser.set_defaults(exe_paths=dict()) + + parser.add_argument("-d", metavar="", dest="workdir", + help="set workdir name. default: or _. When there is more than one task, use --prefix instead") + parser.add_argument("--prefix", metavar="", dest="workdir_prefix", + help="set the workdir name prefix. `_` will be appended to the path for each task") + parser.add_argument("-f", action="store_true", dest="force", + help="remove workdir if it already exists") + parser.add_argument("-b", action="store_true", dest="backup", + help="backup workdir if it already exists") + parser.add_argument("-t", action="store_true", dest="tmpdir", + help="run in a temporary workdir (remove when finished)") + parser.add_argument("-T", metavar="", action="append", dest="tasknames", default=list(), + help="add taskname (useful when sby file is read from stdin)") + parser.add_argument("-E", action="store_true", dest="throw_err", + help="throw an exception (incl stack trace) for most errors") + parser.add_argument("-j", metavar="", type=int, dest="jobcount", + help="maximum number of processes to run in parallel") + parser.add_argument("--sequential", action="store_true", dest="sequential", + help="run tasks in sequence, not in parallel") + + parser.add_argument("--autotune", action="store_true", dest="autotune", + help="automatically find a well performing engine and engine configuration for each task") + parser.add_argument("--autotune-config", dest="autotune_config", + help="read an autotune configuration file (overrides the sby file's autotune options)") + + parser.add_argument("--yosys", metavar="", + action=DictAction, dest="exe_paths") + parser.add_argument("--abc", metavar="", + action=DictAction, dest="exe_paths") + parser.add_argument("--smtbmc", metavar="", + action=DictAction, dest="exe_paths") + parser.add_argument("--witness", metavar="", + action=DictAction, dest="exe_paths") + parser.add_argument("--suprove", metavar="", + action=DictAction, dest="exe_paths") + parser.add_argument("--aigbmc", metavar="", + action=DictAction, dest="exe_paths") + parser.add_argument("--avy", metavar="", + action=DictAction, dest="exe_paths") + parser.add_argument("--btormc", metavar="", + action=DictAction, dest="exe_paths") + parser.add_argument("--pono", metavar="", + action=DictAction, dest="exe_paths", + help="configure which executable to use for the respective tool") + parser.add_argument("--dumpcfg", action="store_true", dest="dump_cfg", + help="print the pre-processed configuration file") + parser.add_argument("--dumptags", action="store_true", dest="dump_tags", + help="print the list of task tags") + parser.add_argument("--dumptasks", action="store_true", dest="dump_tasks", + help="print the list of tasks") + parser.add_argument("--dumpdefaults", action="store_true", dest="dump_defaults", + help="print the list of default tasks") + parser.add_argument("--dumptaskinfo", action="store_true", dest="dump_taskinfo", + help="output a summary of tasks as JSON") + parser.add_argument("--dumpfiles", action="store_true", dest="dump_files", + help="print the list of source files") + parser.add_argument("--setup", action="store_true", dest="setupmode", + help="set up the working directory and exit") + + parser.add_argument("--status", action="store_true", dest="status", + help="summarize the contents of the status database") + parser.add_argument("--statusreset", action="store_true", dest="status_reset", + help="reset the contents of the status database") + + parser.add_argument("--init-config-file", dest="init_config_file", + help="create a default .sby config file") + parser.add_argument("sbyfile", metavar=".sby | ", nargs="?", + help=".sby file OR directory containing config.sby file") + parser.add_argument("arg_tasknames", metavar="tasknames", nargs="*", + help="tasks to run (only valid when .sby is used)") + + return parser diff --git a/sbysrc/sby_core.py b/sbysrc/sby_core.py index 372cc9b5..c366e1be 100644 --- a/sbysrc/sby_core.py +++ b/sbysrc/sby_core.py @@ -16,18 +16,23 @@ # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. # -import os, re, sys, signal +import os, re, sys, signal, platform, click if os.name == "posix": import resource, fcntl import subprocess +from dataclasses import dataclass, field +from collections import defaultdict +from typing import Optional from shutil import copyfile, copytree, rmtree from select import select -from time import time, localtime, sleep +from time import monotonic, localtime, sleep, strftime +from sby_design import SbyProperty, SbyModule, design_hierarchy +from sby_status import SbyStatusDb all_procs_running = [] def force_shutdown(signum, frame): - print("SBY ---- Keyboard interrupt or external termination signal ----", flush=True) + click.echo("SBY ---- Keyboard interrupt or external termination signal ----") for proc in list(all_procs_running): proc.terminate() sys.exit(1) @@ -45,12 +50,24 @@ def process_filename(filename): return filename +def dress_message(workdir, logmessage): + tm = localtime() + if workdir is not None: + logmessage = "[" + click.style(workdir, fg="blue") + "] " + logmessage + return " ".join([ + click.style("SBY", fg="blue"), + click.style("{:2d}:{:02d}:{:02d}".format(tm.tm_hour, tm.tm_min, tm.tm_sec), fg="green"), + logmessage + ]) + class SbyProc: def __init__(self, task, info, deps, cmdline, logfile=None, logstderr=True, silent=False): self.running = False self.finished = False self.terminated = False + self.exited = False self.checkretcode = False + self.retcodes = [0] self.task = task self.info = info self.deps = deps @@ -78,14 +95,20 @@ def __init__(self, task, info, deps, cmdline, logfile=None, logstderr=True, sile self.linebuffer = "" self.logstderr = logstderr self.silent = silent + self.wait = False + self.job_lease = None - self.task.procs_pending.append(self) + self.task.update_proc_pending(self) for dep in self.deps: dep.register_dep(self) self.output_callback = None - self.exit_callback = None + self.exit_callbacks = [] + self.error_callback = None + + if self.task.timeout_reached: + self.terminate(True) def register_dep(self, next_proc): if self.finished: @@ -93,11 +116,14 @@ def register_dep(self, next_proc): else: self.notify.append(next_proc) + def register_exit_callback(self, callback): + self.exit_callbacks.append(callback) + def log(self, line): if line is not None and (self.noprintregex is None or not self.noprintregex.match(line)): if self.logfile is not None: - print(line, file=self.logfile) - self.task.log(f"{self.info}: {line}") + click.echo(line, file=self.logfile) + self.task.log(f"{click.style(self.info, fg='magenta')}: {line}") def handle_output(self, line): if self.terminated or len(line) == 0: @@ -111,27 +137,42 @@ def handle_exit(self, retcode): return if self.logfile is not None: self.logfile.close() - if self.exit_callback is not None: - self.exit_callback(retcode) + for callback in self.exit_callbacks: + callback(retcode) + + def handle_error(self, retcode): + if self.terminated: + return + if self.logfile is not None: + self.logfile.close() + if self.error_callback is not None: + self.error_callback(retcode) def terminate(self, timeout=False): - if self.task.opt_wait and not timeout: + if (self.task.opt_wait or self.wait) and not timeout: return if self.running: if not self.silent: - self.task.log(f"{self.info}: terminating process") + self.task.log(f"{click.style(self.info, fg='magenta')}: terminating process") if os.name == "posix": try: os.killpg(self.p.pid, signal.SIGTERM) except PermissionError: pass self.p.terminate() - self.task.procs_running.remove(self) - all_procs_running.remove(self) + self.task.update_proc_stopped(self) + elif not self.finished and not self.terminated and not self.exited: + self.task.update_proc_canceled(self) self.terminated = True - def poll(self): - if self.finished or self.terminated: + def poll(self, force_unchecked=False): + if self.task.task_local_abort and not force_unchecked: + try: + self.poll(True) + except SbyAbort: + self.task.terminate(True) + return + if self.finished or self.terminated or self.exited: return if not self.running: @@ -139,8 +180,15 @@ def poll(self): if not dep.finished: return + if self.task.taskloop.jobclient: + if self.job_lease is None: + self.job_lease = self.task.taskloop.jobclient.request_lease() + + if not self.job_lease.is_ready: + return + if not self.silent: - self.task.log(f"{self.info}: starting process \"{self.cmdline}\"") + self.task.log(f"{click.style(self.info, fg='magenta')}: starting process \"{self.cmdline}\"") if os.name == "posix": def preexec_fn(): @@ -154,80 +202,650 @@ def preexec_fn(): fcntl.fcntl(self.p.stdout, fcntl.F_SETFL, fl | os.O_NONBLOCK) else: - self.p = subprocess.Popen(self.cmdline, shell=True, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, + self.p = subprocess.Popen(self.cmdline + " & exit !errorlevel!", shell=True, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=(subprocess.STDOUT if self.logstderr else None)) - self.task.procs_pending.remove(self) - self.task.procs_running.append(self) - all_procs_running.append(self) + self.task.update_proc_running(self) self.running = True return - while True: - outs = self.p.stdout.readline().decode("utf-8") - if len(outs) == 0: break - if outs[-1] != '\n': - self.linebuffer += outs - break - outs = (self.linebuffer + outs).strip() - self.linebuffer = "" - self.handle_output(outs) + self.read_output() if self.p.poll() is not None: + # The process might have written something since the last time we checked + self.read_output() + + if self.job_lease: + self.job_lease.done() + if not self.silent: - self.task.log(f"{self.info}: finished (returncode={self.p.returncode})") - self.task.procs_running.remove(self) - all_procs_running.remove(self) + self.task.log(f"{click.style(self.info, fg='magenta')}: finished (returncode={self.p.returncode})") + + self.task.update_proc_stopped(self) self.running = False + self.exited = True + + if os.name == "nt": + if self.p.returncode == 9009: + returncode = 127 + else: + returncode = self.p.returncode & 0xff + else: + returncode = self.p.returncode - if self.p.returncode == 127: - self.task.status = "ERROR" + if returncode == 127: if not self.silent: - self.task.log(f"{self.info}: COMMAND NOT FOUND. ERROR.") + self.task.log(f"{click.style(self.info, fg='magenta')}: COMMAND NOT FOUND. ERROR.") + self.handle_error(returncode) self.terminated = True - self.task.terminate() + self.task.proc_failed(self) return - self.handle_exit(self.p.returncode) - - if self.checkretcode and self.p.returncode != 0: - self.task.status = "ERROR" + if self.checkretcode and returncode not in self.retcodes: if not self.silent: - self.task.log(f"{self.info}: task failed. ERROR.") + self.task.log(f"{click.style(self.info, fg='magenta')}: task failed. ERROR.") + self.handle_error(returncode) self.terminated = True - self.task.terminate() + self.task.proc_failed(self) return + self.handle_exit(returncode) + self.finished = True for next_proc in self.notify: next_proc.poll() return + def read_output(self): + while True: + outs = self.p.stdout.readline().decode("utf-8") + if len(outs) == 0: break + if outs[-1] != '\n': + self.linebuffer += outs + break + outs = (self.linebuffer + outs).strip() + self.linebuffer = "" + self.handle_output(outs) + class SbyAbort(BaseException): pass -class SbyTask: - def __init__(self, sbyconfig, workdir, early_logs, reusedir): +class SbyConfig: + def __init__(self): self.options = dict() - self.used_options = set() - self.engines = list() + self.engines = dict() + self.setup = dict() + self.stage = dict() self.script = list() + self.autotune_config = None self.files = dict() self.verbatim_files = dict() + pass + + def parse_config(self, f): + mode = None + engine_mode = None + stage_name = None + + for line in f: + raw_line = line + if mode in ["options", "engines", "files", "autotune", "setup", "stage"]: + line = re.sub(r"\s*(\s#.*)?$", "", line) + if line == "" or line[0] == "#": + continue + else: + line = line.rstrip() + # print(line) + if mode is None and (len(line) == 0 or line[0] == "#"): + continue + match = re.match(r"^\s*\[(.*)\]\s*$", line) + if match: + entries = match.group(1).strip().split(maxsplit = 1) + if len(entries) == 0: + self.error(f"sby file syntax error: Expected section header, got '{line}'") + elif len(entries) == 1: + section, args = (*entries, None) + else: + section, args = entries + + if section == "options": + mode = "options" + if len(self.options) != 0: + self.error(f"sby file syntax error: '[options]' section already defined") + + if args is not None: + self.error(f"sby file syntax error: '[options]' section does not accept any arguments. got {args}") + continue + + if section == "engines": + mode = "engines" + + if args is None: + engine_mode = None + else: + section_args = args.split() + + if len(section_args) > 1: + self.error(f"sby file syntax error: '[engines]' section expects at most 1 argument, got '{' '.join(section_args)}'") + + if section_args[0] not in ("bmc", "prove", "cover", "live"): + self.error(f"sby file syntax error: Expected one of 'bmc', 'prove', 'cover', 'live' as '[engines]' argument, got '{section_args[0]}'") + + engine_mode = section_args[0] + + if engine_mode in self.engines: + if engine_mode is None: + self.error(f"Already defined engine block") + else: + self.error(f"Already defined engine block for mode '{engine_mode}'") + else: + self.engines[engine_mode] = list() + + continue + + if section == "setup": + self.error(f"sby file syntax error: the '[setup]' section is not yet supported") + + mode = "setup" + if len(self.setup) != 0: + self.error(f"sby file syntax error: '[setup]' section already defined") + + if args is not None: + self.error(f"sby file syntax error: '[setup]' section does not accept any arguments, got '{args}'") + + continue + + # [stage (PARENTS,...)] + if section == "stage": + self.error(f"sby file syntax error: the '[stage]' section is not yet supported") + + mode = "stage" + + if args is None: + self.error(f"sby file syntax error: '[stage]' section expects arguments, got none") + + section_args = args.strip().split(maxsplit = 1) + + + if len(section_args) == 1: + parents = None + else: + parents = list(map(lambda a: a.strip(), section_args[1].split(','))) + + stage_name = section_args[0] + + if stage_name in self.stage: + self.error(f"stage '{stage_name}' already defined") + + self.stage[stage_name] = { + 'parents': parents + } + + continue + + if section == "script": + mode = "script" + if len(self.script) != 0: + self.error(f"sby file syntax error: '[script]' section already defined") + if args is not None: + self.error(f"sby file syntax error: '[script]' section does not accept any arguments. got {args}") + + continue + + if section == "autotune": + mode = "autotune" + if self.autotune_config: + self.error(f"sby file syntax error: '[autotune]' section already defined") + + import sby_autotune + self.autotune_config = sby_autotune.SbyAutotuneConfig() + continue + + if section == "file": + mode = "file" + if args is None: + self.error(f"sby file syntax error: '[file]' section expects a file name argument") + + section_args = args.split() + + if len(section_args) > 1: + self.error(f"sby file syntax error: '[file]' section expects exactly one file name argument, got {len(section_args)}") + current_verbatim_file = section_args[0] + if current_verbatim_file in self.verbatim_files: + self.error(f"duplicate file: {current_verbatim_file}") + self.verbatim_files[current_verbatim_file] = list() + continue + + if section == "files": + mode = "files" + if args is not None: + self.error(f"sby file syntax error: '[files]' section does not accept any arguments. got {args}") + continue + + self.error(f"sby file syntax error: unexpected section '{section}', expected one of 'options, engines, script, autotune, file, files'") + + if mode == "options": + entries = line.strip().split(maxsplit = 1) + if len(entries) != 2: + self.error(f"sby file syntax error: '[options]' section entry does not have an argument '{line}'") + self.options[entries[0]] = entries[1] + continue + + if mode == "autotune": + self.autotune_config.config_line(self, line) + continue + + if mode == "engines": + args = line.strip().split() + self.engines[engine_mode].append(args) + continue + + if mode == "setup": + _valid_options = ( + "cutpoint", "disable", "enable", "assume", "define" + ) + + args = line.strip().split(maxsplit = 1) + + if len(args) < 2: + self.error(f"sby file syntax error: entry in '[setup]' must have an argument, got '{' '.join(args)}'") + + if args[0] not in _valid_options: + self.error(f"sby file syntax error: expected one of '{', '.join(_valid_options)}' in '[setup]' section, got '{args[0]}'") + + else: + opt_key = args[0] + opt_args = args[1].strip().split() + + if opt_key == 'define': + if 'define' not in self.setup: + self.setup['define'] = {} + + if len(opt_args) != 2: + self.error(f"sby file syntax error: 'define' statement in '[setup]' section takes exactly 2 arguments, got '{' '.join(opt_args)}'") + + if opt_args[0][0] != '@': + self.error(f"sby file syntax error: 'define' statement in '[setup]' section expects an '@' prefixed name as the first parameter, got '{opt_args[0]}'") + + name = opt_args[0][1:] + self.setup['define'][name] = opt_args[2:] + else: + self.setup[opt_key] = opt_args[1:] + continue + + if mode == "stage": + _valid_options = ( + "mode", "depth", "timeout", "expect", "engine", + "cutpoint", "enable", "disable", "assume", "skip", + "check", "prove", "abstract", "setsel" + ) + + args = line.strip().split(maxsplit = 1) + + if args is None: + self.error(f"sby file syntax error: unknown key in '[stage]' section") + + if len(args) < 2: + self.error(f"sby file syntax error: entry in '[stage]' must have an argument, got {' '.join(args)}") + + if args[0] not in _valid_options: + self.error(f"sby file syntax error: expected one of '{', '.join(map(repr, _valid_options))}' in '[stage]' section, got '{args[0]}'") + else: + opt_key = args[0] + opt_args = args[1].strip().split() + if opt_key == 'setsel': + + if len(opt_args) != 2: + self.error(f"sby file syntax error: 'setsel' statement in '[stage]' section takes exactly 2 arguments, got '{' '.join(opt_args)}'") + + if opt_args[0][0] != '@': + self.error(f"sby file syntax error: 'setsel' statement in '[stage]' section expects an '@' prefixed name as the first parameter, got '{opt_args[0]}'") + + name = opt_args[0][1:] + + if stage_name not in self.stage: + self.stage[stage_name] = dict() + + self.stage[stage_name][opt_key] = { + 'name': name, 'pattern': opt_args[2:] + } + + else: + if stage_name not in self.stage: + self.stage[stage_name] = dict() + + self.stage[stage_name][opt_key] = opt_args[1:] + continue + + if mode == "script": + self.script.append(line) + continue + + if mode == "files": + entries = line.split() + if len(entries) < 1 or len(entries) > 2: + self.error(f"sby file syntax error: '[files]' section entry expects up to 2 arguments, {len(entries)} specified") + + if len(entries) == 1: + self.files[os.path.basename(entries[0])] = entries[0] + elif len(entries) == 2: + self.files[entries[0]] = entries[1] + + continue + + if mode == "file": + self.verbatim_files[current_verbatim_file].append(raw_line) + continue + + self.error(f"sby file syntax error: In an incomprehensible mode '{mode}'") + + if len(self.stage.keys()) == 0: + self.stage['default'] = { 'enable': '*' } + + def error(self, logmessage): + raise SbyAbort(logmessage) + + +class SbyTaskloop: + def __init__(self, jobclient=None): + self.procs_pending = [] + self.procs_running = [] + self.tasks = [] + self.poll_now = False + self.jobclient = jobclient + + def run(self): + for proc in self.procs_pending: + proc.poll() + + + waiting_for_jobslots = False + if self.jobclient: + waiting_for_jobslots = self.jobclient.has_pending_leases() + + while self.procs_running or waiting_for_jobslots or self.poll_now: + fds = [] + if self.jobclient: + fds.extend(self.jobclient.poll_fds()) + for proc in self.procs_running: + if proc.running: + fds.append(proc.p.stdout) + + if not self.poll_now: + if os.name == "posix": + try: + select(fds, [], [], 1.0) == ([], [], []) + except InterruptedError: + pass + else: + sleep(0.1) + self.poll_now = False + + if self.jobclient: + self.jobclient.poll() + + self.procs_waiting = [] + + for proc in self.procs_running: + proc.poll() + + for proc in self.procs_pending: + proc.poll() + + if self.jobclient: + waiting_for_jobslots = self.jobclient.has_pending_leases() + + tasks = self.tasks + self.tasks = [] + for task in tasks: + task.check_timeout() + if task.procs_pending or task.procs_running: + self.tasks.append(task) + else: + task.exit_callback() + + for task in self.tasks: + task.exit_callback() + +@dataclass +class SbySummaryEvent: + engine_idx: int + trace: Optional[str] = field(default=None) + path: Optional[str] = field(default=None) + hdlname: Optional[str] = field(default=None) + type: Optional[str] = field(default=None) + src: Optional[str] = field(default=None) + step: Optional[int] = field(default=None) + prop: Optional[SbyProperty] = field(default=None) + engine_case: Optional[str] = field(default=None) + + @property + def engine(self): + return f"engine_{self.engine_idx}" + +@dataclass +class SbyTraceSummary: + trace: str + path: Optional[str] = field(default=None) + engine_case: Optional[str] = field(default=None) + events: dict = field(default_factory=lambda: defaultdict(lambda: defaultdict(list))) + + @property + def kind(self): + if '$assert' in self.events: + kind = 'counterexample trace' + elif '$cover' in self.events: + kind = 'cover trace' + else: + kind = 'trace' + return kind + +@dataclass +class SbyEngineSummary: + engine_idx: int + traces: dict = field(default_factory=dict) + status: Optional[str] = field(default=None) + unreached_covers: Optional[list] = field(default=None) + + @property + def engine(self): + return f"engine_{self.engine_idx}" + +class SbySummary: + def __init__(self, task): + self.task = task + self.timing = [] + self.lines = [] + + self.engine_summaries = {} + self.traces = defaultdict(dict) + self.engine_status = {} + self.unreached_covers = None + + def append(self, line): + self.lines.append(line) + + def extend(self, lines): + self.lines.extend(lines) + + def engine_summary(self, engine_idx): + if engine_idx not in self.engine_summaries: + self.engine_summaries[engine_idx] = SbyEngineSummary(engine_idx) + return self.engine_summaries[engine_idx] + + def add_event(self, *args, update_status=True, **kwargs): + event = SbySummaryEvent(*args, **kwargs) + + engine = self.engine_summary(event.engine_idx) + + if update_status: + status_metadata = dict(source="summary_event", engine=engine.engine) + + if event.prop: + if event.type == "$assert": + event.prop.status = "FAIL" + if event.path: + event.prop.tracefiles.append(event.path) + if update_status: + self.task.status_db.add_task_property_data( + event.prop, + "trace", + data=dict(path=event.path, step=event.step, **status_metadata), + ) + if event.prop: + if event.type == "$cover": + event.prop.status = "PASS" + if event.path: + event.prop.tracefiles.append(event.path) + if update_status: + self.task.status_db.add_task_property_data( + event.prop, + "trace", + data=dict(path=event.path, step=event.step, **status_metadata), + ) + if event.prop and update_status: + self.task.status_db.set_task_property_status( + event.prop, + data=status_metadata + ) + + if event.trace not in engine.traces: + engine.traces[event.trace] = SbyTraceSummary(event.trace, path=event.path, engine_case=event.engine_case) + + if event.type: + by_type = engine.traces[event.trace].events[event.type] + if event.hdlname: + by_type[event.hdlname].append(event) + + def set_engine_status(self, engine_idx, status, case=None): + engine_summary = self.engine_summary(engine_idx) + if case is None: + self.task.log(f"{click.style(f'engine_{engine_idx}', fg='magenta')}: Status returned by engine: {status}") + self.engine_summary(engine_idx).status = status + else: + self.task.log(f"{click.style(f'engine_{engine_idx}.{case}', fg='magenta')}: Status returned by engine for {case}: {status}") + if engine_summary.status is None: + engine_summary.status = {} + engine_summary.status[case] = status + + def summarize(self, short): + omitted_excess = False + for line in self.timing: + yield line + + for engine_idx, engine_cmd in self.task.engine_list(): + engine_cmd = ' '.join(engine_cmd) + trace_limit = 5 + prop_limit = 5 + step_limit = 5 + engine = self.engine_summary(engine_idx) + if isinstance(engine.status, dict): + for case, status in sorted(engine.status.items()): + yield f"{engine.engine} ({engine_cmd}) returned {status} for {case}" + elif engine.status: + yield f"{engine.engine} ({engine_cmd}) returned {engine.status}" + else: + yield f"{engine.engine} ({engine_cmd}) did not return a status" + + produced_traces = False + + for i, (trace_name, trace) in enumerate(sorted(engine.traces.items())): + if short and i == trace_limit: + excess = len(engine.traces) - trace_limit + omitted_excess = True + yield f"and {excess} further trace{'s' if excess != 1 else ''}" + break + case_suffix = f" [{trace.engine_case}]" if trace.engine_case else "" + if trace.path: + if short: + yield f"{trace.kind}{case_suffix}: {self.task.workdir}/{trace.path}" + else: + yield f"{trace.kind}{case_suffix}: {trace.path}" + else: + yield f"{trace.kind}{case_suffix}: <{trace.trace}>" + produced_traces = True + for event_type, events in sorted(trace.events.items()): + if event_type == '$assert': + desc = "failed assertion" + short_desc = 'assertion' + elif event_type == '$cover': + desc = "reached cover statement" + short_desc = 'cover statement' + elif event_type == '$assume': + desc = "violated assumption" + short_desc = 'assumption' + else: + continue + for j, (hdlname, same_events) in enumerate(sorted(events.items())): + if short and j == prop_limit: + excess = len(events) - prop_limit + yield f" and {excess} further {short_desc}{'s' if excess != 1 else ''}" + break + + event = same_events[0] + steps = sorted(e.step for e in same_events) + if short and len(steps) > step_limit: + excess = len(steps) - step_limit + steps = [str(step) for step in steps[:step_limit]] + omitted_excess = True + steps[-1] += f" and {excess} further step{'s' if excess != 1 else ''}" + + steps = f"step{'s' if len(steps) > 1 else ''} {', '.join(map(str, steps))}" + yield f" {desc} {event.hdlname} at {event.src} in {steps}" + + if not produced_traces: + yield f"{engine.engine} did not produce any traces" + + if self.unreached_covers is None and self.task.opt_mode == 'cover' and self.task.status != "PASS" and self.task.design: + self.unreached_covers = [] + for prop in self.task.design.hierarchy: + if prop.type == prop.Type.COVER and prop.status == "UNKNOWN": + self.unreached_covers.append(prop) + + if self.unreached_covers: + yield f"unreached cover statements:" + for j, prop in enumerate(self.unreached_covers): + if short and j == prop_limit: + excess = len(self.unreached_covers) - prop_limit + omitted_excess = True + yield f" and {excess} further propert{'ies' if excess != 1 else 'y'}" + break + yield f" {prop.hdlname} at {prop.location}" + + for line in self.lines: + yield line + + if omitted_excess: + yield f"see {self.task.workdir}/{self.task.status} for a complete summary" + def __iter__(self): + yield from self.summarize(True) + + + +class SbyTask(SbyConfig): + def __init__(self, sbyconfig, workdir, early_logs, reusedir, taskloop=None, logfile=None): + super().__init__() + self.used_options = set() self.models = dict() self.workdir = workdir self.reusedir = reusedir self.status = "UNKNOWN" self.total_time = 0 - self.expect = [] + self.expect = list() + self.design = None + self.precise_prop_status = False + self.timeout_reached = False + self.task_local_abort = False + self.exit_callback = self.summarize yosys_program_prefix = "" ##yosys-program-prefix## self.exe_paths = { "yosys": os.getenv("YOSYS", yosys_program_prefix + "yosys"), "abc": os.getenv("ABC", yosys_program_prefix + "yosys-abc"), "smtbmc": os.getenv("SMTBMC", yosys_program_prefix + "yosys-smtbmc"), + "witness": os.getenv("WITNESS", yosys_program_prefix + "yosys-witness"), "suprove": os.getenv("SUPROVE", "suprove"), "aigbmc": os.getenv("AIGBMC", "aigbmc"), "avy": os.getenv("AVY", "avy"), @@ -235,82 +853,100 @@ def __init__(self, sbyconfig, workdir, early_logs, reusedir): "pono": os.getenv("PONO", "pono"), } + self.taskloop = taskloop or SbyTaskloop() + self.taskloop.tasks.append(self) + self.procs_running = [] self.procs_pending = [] - self.start_clock_time = time() + self.start_clock_time = monotonic() if os.name == "posix": ru = resource.getrusage(resource.RUSAGE_CHILDREN) self.start_process_time = ru.ru_utime + ru.ru_stime - self.summary = list() + self.summary = SbySummary(self) - self.logfile = open(f"{workdir}/logfile.txt", "a") + self.logfile = logfile or open(f"{workdir}/logfile.txt", "a") + self.log_targets = [sys.stdout, self.logfile] for line in early_logs: - print(line, file=self.logfile, flush=True) + click.echo(line, file=self.logfile) if not reusedir: with open(f"{workdir}/config.sby", "w") as f: for line in sbyconfig: - print(line, file=f) + click.echo(line, file=f) - def taskloop(self): - for proc in self.procs_pending: - proc.poll() + def engine_list(self): + engines = self.engines.get(None, []) + self.engines.get(self.opt_mode, []) + return list(enumerate(engines)) - while len(self.procs_running): - fds = [] - for proc in self.procs_running: - if proc.running: - fds.append(proc.p.stdout) + def check_timeout(self): + if self.opt_timeout is not None: + total_clock_time = int(monotonic() - self.start_clock_time) + if total_clock_time > self.opt_timeout: + self.log(f"Reached TIMEOUT ({self.opt_timeout} seconds). Terminating all subprocesses.") + self.status = "TIMEOUT" + self.terminate(timeout=True) - if os.name == "posix": - try: - select(fds, [], [], 1.0) == ([], [], []) - except InterruptedError: - pass - else: - sleep(0.1) + def update_proc_pending(self, proc): + self.procs_pending.append(proc) + self.taskloop.procs_pending.append(proc) - for proc in self.procs_running: - proc.poll() + def update_proc_running(self, proc): + self.procs_pending.remove(proc) + self.taskloop.procs_pending.remove(proc) - for proc in self.procs_pending: - proc.poll() + self.procs_running.append(proc) + self.taskloop.procs_running.append(proc) + all_procs_running.append(proc) - if self.opt_timeout is not None: - total_clock_time = int(time() - self.start_clock_time) - if total_clock_time > self.opt_timeout: - self.log(f"Reached TIMEOUT ({self.opt_timeout} seconds). Terminating all subprocesses.") - self.status = "TIMEOUT" - self.terminate(timeout=True) + def update_proc_stopped(self, proc): + self.procs_running.remove(proc) + self.taskloop.procs_running.remove(proc) + all_procs_running.remove(proc) + + def update_proc_canceled(self, proc): + self.procs_pending.remove(proc) + self.taskloop.procs_pending.remove(proc) def log(self, logmessage): tm = localtime() - print("SBY {:2d}:{:02d}:{:02d} [{}] {}".format(tm.tm_hour, tm.tm_min, tm.tm_sec, self.workdir, logmessage), flush=True) - print("SBY {:2d}:{:02d}:{:02d} [{}] {}".format(tm.tm_hour, tm.tm_min, tm.tm_sec, self.workdir, logmessage), file=self.logfile, flush=True) + line = dress_message(self.workdir, logmessage) + for target in self.log_targets: + click.echo(line, file=target) + + def log_prefix(self, prefix, message=None): + prefix = f"{click.style(prefix, fg='magenta')}: " + def log(message): + self.log(f"{prefix}{message}") + if message is None: + return log + else: + log(message) def error(self, logmessage): tm = localtime() - print("SBY {:2d}:{:02d}:{:02d} [{}] ERROR: {}".format(tm.tm_hour, tm.tm_min, tm.tm_sec, self.workdir, logmessage), flush=True) - print("SBY {:2d}:{:02d}:{:02d} [{}] ERROR: {}".format(tm.tm_hour, tm.tm_min, tm.tm_sec, self.workdir, logmessage), file=self.logfile, flush=True) + self.log(click.style(f"ERROR: {logmessage}", fg="red", bold=True)) self.status = "ERROR" if "ERROR" not in self.expect: self.retcode = 16 + else: + self.retcode = 0 self.terminate() with open(f"{self.workdir}/{self.status}", "w") as f: - print(f"ERROR: {logmessage}", file=f) + click.echo(f"ERROR: {logmessage}", file=f) raise SbyAbort(logmessage) def makedirs(self, path): if self.reusedir and os.path.isdir(path): rmtree(path, ignore_errors=True) - os.makedirs(path) + if not os.path.isdir(path): + os.makedirs(path) def copy_src(self): - os.makedirs(self.workdir + "/src") + self.makedirs(self.workdir + "/src") for dstfile, lines in self.verbatim_files.items(): dstfile = self.workdir + "/src/" + dstfile @@ -364,49 +1000,96 @@ def make_model(self, model_name): if not os.path.isdir(f"{self.workdir}/model"): os.makedirs(f"{self.workdir}/model") - if model_name in ["base", "nomem"]: - with open(f"""{self.workdir}/model/design{"" if model_name == "base" else "_nomem"}.ys""", "w") as f: + if model_name == "prep": + with open(f"""{self.workdir}/model/design_prep.ys""", "w") as f: + print(f"# running in {self.workdir}/model/", file=f) + print(f"""read_ilang design.il""", file=f) + if not self.opt_skip_prep: + print("scc -select; simplemap; select -clear", file=f) + print("memory_nordff", file=f) + if self.opt_multiclock: + print("clk2fflogic", file=f) + else: + print("async2sync", file=f) + if self.opt_assume_early: + print("chformal -assume -early", file=f) + print("opt_clean", file=f) + print("formalff -setundef -clk2ff -ff2anyinit -hierarchy", file=f) + if self.opt_mode in ["bmc", "prove"]: + print("chformal -live -fair -cover -remove", file=f) + if self.opt_mode == "cover": + print("chformal -live -fair -remove", file=f) + if self.opt_mode == "live": + print("chformal -assert2assume", file=f) + print("chformal -cover -remove", file=f) + print("opt_clean", file=f) + print("check", file=f) # can't detect undriven wires past this point + print("setundef -undriven -anyseq", file=f) + print("opt -fast", file=f) + if self.opt_witrename: + # we need to run this a second time to handle anything added by prep + print("rename -witness", file=f) + print("opt_clean", file=f) + print(f"""write_rtlil ../model/design_prep.il""", file=f) + + proc = SbyProc( + self, + model_name, + self.model("base"), + "cd {}/model; {} -ql design_{s}.log design_{s}.ys".format(self.workdir, self.exe_paths["yosys"], s=model_name) + ) + proc.checkretcode = True + + return [proc] + + if model_name == "base": + with open(f"""{self.workdir}/model/design.ys""", "w") as f: print(f"# running in {self.workdir}/src/", file=f) for cmd in self.script: print(cmd, file=f) - if model_name == "base": - print("memory_nordff", file=f) - else: - print("memory_map", file=f) - if self.opt_multiclock: - print("clk2fflogic", file=f) - else: - print("async2sync", file=f) - print("chformal -assume -early", file=f) - if self.opt_mode in ["bmc", "prove"]: - print("chformal -live -fair -cover -remove", file=f) - if self.opt_mode == "cover": - print("chformal -live -fair -remove", file=f) - if self.opt_mode == "live": - print("chformal -assert2assume", file=f) - print("chformal -cover -remove", file=f) - print("opt_clean", file=f) - print("setundef -anyseq", file=f) - print("opt -keepdc -fast", file=f) - print("check", file=f) - print("hierarchy -simcheck", file=f) - print(f"""write_ilang ../model/design{"" if model_name == "base" else "_nomem"}.il""", file=f) + # the user must designate a top module in [script] + print("hierarchy -smtcheck", file=f) + # we need to give flatten-preserved names before write_jny + if self.opt_witrename: + print("rename -witness", file=f) + print(f"""write_jny -no-connections ../model/design.json""", file=f) + print(f"""write_rtlil ../model/design.il""", file=f) proc = SbyProc( self, model_name, [], - "cd {}/src; {} -ql ../model/design{s}.log ../model/design{s}.ys".format(self.workdir, self.exe_paths["yosys"], - s="" if model_name == "base" else "_nomem") + "cd {}/src; {} -ql ../model/design.log ../model/design.ys".format(self.workdir, self.exe_paths["yosys"]) ) proc.checkretcode = True + def instance_hierarchy_callback(retcode): + if self.design == None: + with open(f"{self.workdir}/model/design.json") as f: + self.design = design_hierarchy(f) + self.status_db.create_task_properties([ + prop for prop in self.design.properties_by_path.values() + if not prop.type.assume_like + ]) + + def instance_hierarchy_error_callback(retcode): + self.precise_prop_status = False + + proc.register_exit_callback(instance_hierarchy_callback) + proc.error_callback = instance_hierarchy_error_callback + return [proc] if re.match(r"^smt2(_syn)?(_nomem)?(_stbv|_stdt)?$", model_name): with open(f"{self.workdir}/model/design_{model_name}.ys", "w") as f: print(f"# running in {self.workdir}/model/", file=f) - print(f"""read_ilang design{"_nomem" if "_nomem" in model_name else ""}.il""", file=f) + print(f"""read_ilang design_prep.il""", file=f) + print("hierarchy -smtcheck", file=f) + print("delete */t:$print", file=f) + print("formalff -assume", file=f) + if "_nomem" in model_name: + print("memory_map -formal", file=f) + print("formalff -setundef -clk2ff -ff2anyinit", file=f) if "_syn" in model_name: print("techmap", file=f) print("opt -fast", file=f) @@ -424,7 +1107,7 @@ def make_model(self, model_name): proc = SbyProc( self, model_name, - self.model("nomem" if "_nomem" in model_name else "base"), + self.model("prep"), "cd {}/model; {} -ql design_{s}.log design_{s}.ys".format(self.workdir, self.exe_paths["yosys"], s=model_name) ) proc.checkretcode = True @@ -434,7 +1117,13 @@ def make_model(self, model_name): if re.match(r"^btor(_syn)?(_nomem)?$", model_name): with open(f"{self.workdir}/model/design_{model_name}.ys", "w") as f: print(f"# running in {self.workdir}/model/", file=f) - print(f"""read_ilang design{"_nomem" if "_nomem" in model_name else ""}.il""", file=f) + print(f"""read_ilang design_prep.il""", file=f) + print("hierarchy -simcheck", file=f) + print("delete */t:$print", file=f) + print("formalff -assume", file=f) + if "_nomem" in model_name: + print("memory_map -formal", file=f) + print("formalff -setundef -clk2ff -ff2anyinit", file=f) print("flatten", file=f) print("setundef -undriven -anyseq", file=f) if "_syn" in model_name: @@ -448,12 +1137,13 @@ def make_model(self, model_name): print("delete -output", file=f) print("dffunmap", file=f) print("stat", file=f) - print("write_btor {}-i design_{m}.info design_{m}.btor".format("-c " if self.opt_mode == "cover" else "", m=model_name), file=f) + print("write_btor {}-i design_{m}.info -ywmap design_btor.ywb design_{m}.btor".format("-c " if self.opt_mode == "cover" else "", m=model_name), file=f) + print("write_btor -s {}-i design_{m}_single.info -ywmap design_btor_single.ywb design_{m}_single.btor".format("-c " if self.opt_mode == "cover" else "", m=model_name), file=f) proc = SbyProc( self, model_name, - self.model("nomem" if "_nomem" in model_name else "base"), + self.model("prep"), "cd {}/model; {} -ql design_{s}.log design_{s}.ys".format(self.workdir, self.exe_paths["yosys"], s=model_name) ) proc.checkretcode = True @@ -463,7 +1153,10 @@ def make_model(self, model_name): if model_name == "aig": with open(f"{self.workdir}/model/design_aiger.ys", "w") as f: print(f"# running in {self.workdir}/model/", file=f) - print("read_ilang design_nomem.il", file=f) + print("read_ilang design_prep.il", file=f) + print("delete */t:$print", file=f) + print("hierarchy -simcheck", file=f) + print("formalff -assume", file=f) print("flatten", file=f) print("setundef -undriven -anyseq", file=f) print("setattr -unset keep", file=f) @@ -471,23 +1164,39 @@ def make_model(self, model_name): print("opt -full", file=f) print("techmap", file=f) print("opt -fast", file=f) + print("memory_map -formal", file=f) + print("formalff -clk2ff -ff2anyinit", file=f) + print("simplemap", file=f) print("dffunmap", file=f) print("abc -g AND -fast", file=f) print("opt_clean", file=f) print("stat", file=f) - print("write_aiger -I -B -zinit -map design_aiger.aim design_aiger.aig", file=f) + print(f"write_aiger -I -B -zinit -no-startoffset {'-vmap' if self.opt_aigvmap else '-map'} design_aiger.aim" + + f"{' -symbols' if self.opt_aigsyms else ''} -ywmap design_aiger.ywa design_aiger.aig", file=f) proc = SbyProc( self, "aig", - self.model("nomem"), + self.model("prep"), f"""cd {self.workdir}/model; {self.exe_paths["yosys"]} -ql design_aiger.log design_aiger.ys""" ) proc.checkretcode = True return [proc] - assert False + if model_name == "aig_fold": + proc = SbyProc( + self, + model_name, + self.model("aig"), + f"""cd {self.workdir}/model; {self.exe_paths["abc"]} -c 'read_aiger design_aiger.aig; fold{" -s" if self.opt_aigfolds else ""}; strash; write_aiger design_aiger_fold.aig'""", + logfile=open(f"{self.workdir}/model/design_aiger_fold.log", "w") + ) + proc.checkretcode = True + + return [proc] + + self.error(f"Invalid model name: {model_name}") def model(self, model_name): if model_name not in self.models: @@ -495,11 +1204,25 @@ def model(self, model_name): return self.models[model_name] def terminate(self, timeout=False): + if timeout: + self.timeout_reached = True for proc in list(self.procs_running): proc.terminate(timeout=timeout) + for proc in list(self.procs_pending): + proc.terminate(timeout=timeout) + + def proc_failed(self, proc): + # proc parameter used by autotune override + self.status = "ERROR" + self.terminate() + + def pass_unknown_asserts(self, data): + for prop in self.design.pass_unknown_asserts(): + self.status_db.set_task_property_status(prop, data=data) def update_status(self, new_status): assert new_status in ["PASS", "FAIL", "UNKNOWN", "ERROR"] + self.status_db.set_task_status(new_status) if new_status == "UNKNOWN": return @@ -510,6 +1233,8 @@ def update_status(self, new_status): if new_status == "PASS": assert self.status != "FAIL" self.status = "PASS" + if self.opt_mode in ("bmc", "prove") and self.design: + self.pass_unknown_asserts(dict(source="task_status")) elif new_status == "FAIL": assert self.status != "PASS" @@ -521,99 +1246,13 @@ def update_status(self, new_status): else: assert 0 - def run(self, setupmode): - mode = None - key = None - + def handle_non_engine_options(self): with open(f"{self.workdir}/config.sby", "r") as f: - for line in f: - raw_line = line - if mode in ["options", "engines", "files"]: - line = re.sub(r"\s*(\s#.*)?$", "", line) - if line == "" or line[0] == "#": - continue - else: - line = line.rstrip() - # print(line) - if mode is None and (len(line) == 0 or line[0] == "#"): - continue - match = re.match(r"^\s*\[(.*)\]\s*$", line) - if match: - entries = match.group(1).split() - if len(entries) == 0: - self.error(f"sby file syntax error: {line}") - - if entries[0] == "options": - mode = "options" - if len(self.options) != 0 or len(entries) != 1: - self.error(f"sby file syntax error: {line}") - continue - - if entries[0] == "engines": - mode = "engines" - if len(self.engines) != 0 or len(entries) != 1: - self.error(f"sby file syntax error: {line}") - continue - - if entries[0] == "script": - mode = "script" - if len(self.script) != 0 or len(entries) != 1: - self.error(f"sby file syntax error: {line}") - continue - - if entries[0] == "file": - mode = "file" - if len(entries) != 2: - self.error(f"sby file syntax error: {line}") - current_verbatim_file = entries[1] - if current_verbatim_file in self.verbatim_files: - self.error(f"duplicate file: {entries[1]}") - self.verbatim_files[current_verbatim_file] = list() - continue - - if entries[0] == "files": - mode = "files" - if len(entries) != 1: - self.error(f"sby file syntax error: {line}") - continue - - self.error(f"sby file syntax error: {line}") - - if mode == "options": - entries = line.split() - if len(entries) != 2: - self.error(f"sby file syntax error: {line}") - self.options[entries[0]] = entries[1] - continue - - if mode == "engines": - entries = line.split() - self.engines.append(entries) - continue - - if mode == "script": - self.script.append(line) - continue - - if mode == "files": - entries = line.split() - if len(entries) == 1: - self.files[os.path.basename(entries[0])] = entries[0] - elif len(entries) == 2: - self.files[entries[0]] = entries[1] - else: - self.error(f"sby file syntax error: {line}") - continue - - if mode == "file": - self.verbatim_files[current_verbatim_file].append(raw_line) - continue - - self.error(f"sby file syntax error: {line}") + self.parse_config(f) self.handle_str_option("mode", None) - if self.opt_mode not in ["bmc", "prove", "cover", "live"]: + if self.opt_mode not in ["bmc", "prove", "cover", "live", "prep"]: self.error(f"Invalid mode: {self.opt_mode}") self.expect = ["PASS"] @@ -625,16 +1264,52 @@ def run(self, setupmode): if s not in ["PASS", "FAIL", "UNKNOWN", "ERROR", "TIMEOUT"]: self.error(f"Invalid expect value: {s}") + if self.opt_mode != "live": + self.handle_int_option("depth", 20) + self.handle_bool_option("multiclock", False) self.handle_bool_option("wait", False) self.handle_int_option("timeout", None) + self.handle_bool_option("vcd", True) + self.handle_bool_option("vcd_sim", False) + self.handle_bool_option("fst", False) + + self.handle_bool_option("witrename", True) + self.handle_bool_option("aigfolds", False) + self.handle_bool_option("aigvmap", False) + self.handle_bool_option("aigsyms", False) + self.handle_str_option("smtc", None) self.handle_int_option("skip", None) self.handle_str_option("tbtop", None) + if self.opt_mode != "live": + self.handle_int_option("append", 0) + self.handle_bool_option("append_assume", True) + + self.handle_str_option("make_model", None) + self.handle_bool_option("skip_prep", False) + + self.handle_bool_option("assume_early", True) + + def setup_status_db(self, status_path=None): + if hasattr(self, 'status_db'): + return + + if status_path is None: + try: + with open(f"{self.workdir}/status.path", "r") as status_path_file: + status_path = f"{self.workdir}/{status_path_file.read().rstrip()}" + except FileNotFoundError: + status_path = f"{self.workdir}/status.sqlite" + + self.status_db = SbyStatusDb(status_path, self) + + def setup_procs(self, setupmode): + self.handle_non_engine_options() if self.opt_smtc is not None: - for engine in self.engines: + for engine_idx, engine in self.engine_list(): if engine[0] != "smtbmc": self.error("Option smtc is only valid for smtbmc engine.") @@ -642,11 +1317,11 @@ def run(self, setupmode): if self.opt_skip == 0: self.opt_skip = None else: - for engine in self.engines: + for engine_idx, engine in self.engine_list(): if engine[0] not in ["smtbmc", "btor"]: self.error("Option skip is only valid for smtbmc and btor engines.") - if len(self.engines) == 0: + if len(self.engine_list()) == 0 and self.opt_mode != "prep": self.error("Config file is lacking engine configuration.") if self.reusedir: @@ -658,6 +1333,15 @@ def run(self, setupmode): self.retcode = 0 return + self.setup_status_db() + + if self.opt_make_model is not None: + for name in self.opt_make_model.split(","): + self.model(name.strip()) + + for proc in self.procs_pending: + proc.wait = True + if self.opt_mode == "bmc": import sby_mode_bmc sby_mode_bmc.run(self) @@ -674,6 +1358,10 @@ def run(self, setupmode): import sby_mode_cover sby_mode_cover.run(self) + elif self.opt_mode == "prep": + self.model("prep") + self.update_status("PASS") + else: assert False @@ -681,30 +1369,34 @@ def run(self, setupmode): if opt not in self.used_options: self.error(f"Unused option: {opt}") - self.taskloop() - - total_clock_time = int(time() - self.start_clock_time) + def summarize(self): + total_clock_time = int(monotonic() - self.start_clock_time) if os.name == "posix": ru = resource.getrusage(resource.RUSAGE_CHILDREN) total_process_time = int((ru.ru_utime + ru.ru_stime) - self.start_process_time) self.total_time = total_process_time - self.summary = [ + # TODO process time is incorrect when running in parallel + + self.summary.timing = [ "Elapsed clock time [H:MM:SS (secs)]: {}:{:02d}:{:02d} ({})".format (total_clock_time // (60*60), (total_clock_time // 60) % 60, total_clock_time % 60, total_clock_time), "Elapsed process time [H:MM:SS (secs)]: {}:{:02d}:{:02d} ({})".format (total_process_time // (60*60), (total_process_time // 60) % 60, total_process_time % 60, total_process_time), - ] + self.summary + ] else: - self.summary = [ + self.summary.timing = [ "Elapsed clock time [H:MM:SS (secs)]: {}:{:02d}:{:02d} ({})".format (total_clock_time // (60*60), (total_clock_time // 60) % 60, total_clock_time % 60, total_clock_time), "Elapsed process time unvailable on Windows" - ] + self.summary + ] for line in self.summary: - self.log(f"summary: {line}") + if line.startswith("Elapsed"): + self.log(f"summary: {line}") + else: + self.log("summary: " + click.style(line, fg="green" if self.status in self.expect else "red", bold=True)) assert self.status in ["PASS", "FAIL", "UNKNOWN", "ERROR", "TIMEOUT"] @@ -717,6 +1409,96 @@ def run(self, setupmode): if self.status == "TIMEOUT": self.retcode = 8 if self.status == "ERROR": self.retcode = 16 + def write_summary_file(self): with open(f"{self.workdir}/{self.status}", "w") as f: - for line in self.summary: - print(line, file=f) + for line in self.summary.summarize(short=False): + click.echo(line, file=f) + + def print_junit_result(self, f, junit_ts_name, junit_tc_name, junit_format_strict=False): + junit_time = strftime('%Y-%m-%dT%H:%M:%S') + if not self.design: + self.precise_prop_status = False + if self.precise_prop_status: + checks = self.design.hierarchy.get_property_list() + junit_tests = len(checks) + junit_failures = 0 + junit_errors = 0 + junit_skipped = 0 + for check in checks: + if check.status == "PASS": + pass + elif check.status == "FAIL": + junit_failures += 1 + elif check.status == "UNKNOWN": + junit_skipped += 1 + else: + junit_errors += 1 + if self.retcode == 16: + junit_errors += 1 + elif self.retcode != 0: + junit_failures += 1 + else: + junit_tests = 1 + junit_errors = 1 if self.retcode == 16 else 0 + junit_failures = 1 if self.retcode != 0 and junit_errors == 0 else 0 + junit_skipped = 0 + print(f'', file=f) + print(f'', file=f) + print(f'', file=f) + print(f'', file=f) + print(f'', file=f) + print(f'', file=f) + print(f'', file=f) + print(f'', file=f) + if self.precise_prop_status: + print(f'', file=f) + if self.retcode == 16: + print(f'', file=f) # type mandatory, message optional + elif self.retcode != 0: + if len(self.expect) > 1 or "PASS" not in self.expect: + expected = " ".join(self.expect) + print(f'', file=f) + else: + print(f'', file=f) + print(f'', file=f) + + for check in checks: + if junit_format_strict: + detail_attrs = '' + else: + detail_attrs = f' type="{check.type}" location="{check.location}" id="{check.name}"' + if check.tracefile: + detail_attrs += f' tracefile="{check.tracefile}"' + if check.location: + junit_prop_name = f"Property {check.type} in {check.hierarchy} at {check.location}" + else: + junit_prop_name = f"Property {check.type} {check.name} in {check.hierarchy}" + print(f'', file=f) + if check.status == "PASS": + pass + elif check.status == "UNKNOWN": + print(f'', file=f) + elif check.status == "FAIL": + traceinfo = f' Trace file: {check.tracefile}' if check.type == check.Type.ASSERT else '' + print(f'', file=f) + elif check.status == "ERROR": + print(f'', file=f) # type mandatory, message optional + print(f'', file=f) + else: + print(f'', file=f) + if junit_errors: + print(f'', file=f) # type mandatory, message optional + elif junit_failures: + junit_type = "assert" if self.opt_mode in ["bmc", "prove"] else self.opt_mode + print(f'', file=f) + print(f'', file=f) + print('', end="", file=f) + with open(f"{self.workdir}/logfile.txt", "r") as logf: + for line in logf: + print(line.replace("&", "&").replace("<", "<").replace(">", ">").replace("\"", """), end="", file=f) + print('', file=f) + print('', file=f) + #TODO: can we handle errors and still output this file? + print('', file=f) + print(f'', file=f) + print(f'', file=f) diff --git a/sbysrc/sby_design.py b/sbysrc/sby_design.py new file mode 100644 index 00000000..d93d17da --- /dev/null +++ b/sbysrc/sby_design.py @@ -0,0 +1,275 @@ +# +# SymbiYosys (sby) -- Front-end for Yosys-based formal verification flows +# +# Copyright (C) 2022 N. Engelhardt +# +# Permission to use, copy, modify, and/or distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +# + +import json, re +from enum import Enum, auto +from dataclasses import dataclass, field +from typing import Optional, Tuple + + +addr_re = re.compile(r'\\\[[0-9]+\]$') +public_name_re = re.compile(r"\\([a-zA-Z_][a-zA-Z0-9_]*(\[[0-9]+\])?|\[[0-9]+\])$") + +def pretty_name(id): + if public_name_re.match(id): + return id.lstrip("\\") + else: + return id + +def pretty_path(path): + out = "" + for name in path: + name = pretty_name(name) + if name.startswith("["): + out += name + continue + if out: + out += "." + if name.startswith("\\") or name.startswith("$"): + out += name + " " + else: + out += name + + return out + + +@dataclass +class SbyProperty: + class Type(Enum): + ASSUME = auto() + ASSERT = auto() + COVER = auto() + LIVE = auto() + FAIR = auto() + + def __str__(self): + return self.name + + @classmethod + def from_cell(c, name): + if name == "$assume": + return c.ASSUME + if name == "$assert": + return c.ASSERT + if name == "$cover": + return c.COVER + if name == "$live": + return c.LIVE + if name == "$fair": + return c.FAIR + raise ValueError("Unknown property type: " + name) + + @classmethod + def from_flavor(c, name): + if name == "assume": + return c.ASSUME + if name == "assert": + return c.ASSERT + if name == "cover": + return c.COVER + if name == "live": + return c.LIVE + if name == "fair": + return c.FAIR + raise ValueError("Unknown property type: " + name) + + @property + def assume_like(self): + return self in [self.ASSUME, self.FAIR] + + name: str + path: Tuple[str, ...] + type: Type + location: str + hierarchy: str + status: str = field(default="UNKNOWN") + tracefiles: str = field(default_factory=list) + + @property + def tracefile(self): + if self.tracefiles: + return self.tracefiles[0] + else: + return "" + + @property + def celltype(self): + return f"${str(self.type).lower()}" + + @property + def hdlname(self): + return pretty_path(self.path).rstrip() + + + def __repr__(self): + return f"SbyProperty<{self.type} {self.name} {self.path} at {self.location}: status={self.status}, tracefile=\"{self.tracefile}\">" + +@dataclass +class SbyModule: + name: str + path: Tuple[str, ...] + type: str + submodules: dict = field(default_factory=dict) + properties: list = field(default_factory=list) + + def __repr__(self): + return f"SbyModule<{self.name} : {self.type}, submodules={self.submodules}, properties={self.properties}>" + + def __iter__(self): + for prop in self.properties: + yield prop + for submod in self.submodules.values(): + yield from submod.__iter__() + + def get_property_list(self): + return [p for p in self if p.type != p.Type.ASSUME] + + def find_property(self, hierarchy, location): + # FIXME: use that RE that works with escaped paths from https://stackoverflow.com/questions/46207665/regex-pattern-to-split-verilog-path-in-different-instances-using-python + path = hierarchy.split('.') + mod = path.pop(0) + if self.name != mod: + raise ValueError(f"{self.name} is not the first module in hierarchical path {hierarchy}.") + try: + mod_hier = self + while path: + mod = path.pop(0) + mod_hier = mod_hier.submodules[mod] + except KeyError: + raise KeyError(f"Could not find {hierarchy} in design hierarchy!") + try: + prop = next(p for p in mod_hier.properties if location in p.location) + except StopIteration: + raise KeyError(f"Could not find assert at {location} in properties list!") + return prop + + def find_property_by_cellname(self, cell_name, trans_dict=dict()): + # backends may need to mangle names irreversibly, so allow applying + # the same transformation here + for prop in self: + if cell_name == prop.name.translate(str.maketrans(trans_dict)): + return prop + raise KeyError(f"No such property: {cell_name}") + + +@dataclass +class SbyDesign: + hierarchy: SbyModule = None + memory_bits: int = 0 + forall: bool = False + properties_by_path: dict = field(default_factory=dict) + + def pass_unknown_asserts(self): + updated = [] + for prop in self.hierarchy: + if prop.type == prop.Type.ASSERT and prop.status == "UNKNOWN": + prop.status = "PASS" + updated.append(prop) + return updated + + +def cell_path(cell): + path = cell["attributes"].get("hdlname") + if path is None: + if cell["name"].startswith('$'): + return (cell["name"],) + else: + return ("\\" + cell["name"],) + else: + return tuple(f"\\{segment}" for segment in path.split()) + + +def design_hierarchy(filename): + design = SbyDesign(hierarchy=None) + design_json = json.load(filename) + def make_mod_hier(instance_name, module_name, hierarchy="", path=()): + # print(instance_name,":", module_name) + sub_hierarchy=f"{hierarchy}/{instance_name}" if hierarchy else instance_name + mod = SbyModule(name=instance_name, path=path, type=module_name) + + for m in design_json["modules"]: + if m["name"] == module_name: + cell_sorts = m["cell_sorts"] + break + else: + raise ValueError(f"Cannot find module {module_name}") + + for sort in cell_sorts: + if sort["type"] in ["$assume", "$assert", "$cover", "$live", "$fair"]: + for cell in sort["cells"]: + try: + location = cell["attributes"]["src"] + except KeyError: + location = "" + p = SbyProperty( + name=cell["name"], + path=(*path, *cell_path(cell)), + type=SbyProperty.Type.from_cell(sort["type"]), + location=location, + hierarchy=sub_hierarchy) + mod.properties.append(p) + if sort["type"] == "$check": + for cell in sort["cells"]: + try: + location = cell["attributes"]["src"] + except KeyError: + location = "" + p = SbyProperty( + name=cell["name"], + path=(*path, *cell_path(cell)), + type=SbyProperty.Type.from_flavor(cell["parameters"]["FLAVOR"]), + location=location, + hierarchy=sub_hierarchy) + mod.properties.append(p) + + if sort["type"][0] != '$' or sort["type"].startswith("$paramod"): + for cell in sort["cells"]: + mod.submodules[cell["name"]] = make_mod_hier( + cell["name"], sort["type"], sub_hierarchy, (*path, *cell_path(cell))) + if sort["type"] in ["$mem", "$mem_v2"]: + for cell in sort["cells"]: + design.memory_bits += int(cell["parameters"]["WIDTH"], 2) * int(cell["parameters"]["SIZE"], 2) + if sort["type"] in ["$allconst", "$allseq"]: + design.forall = True + + return mod + + for m in design_json["modules"]: + attrs = m["attributes"] + if "top" in attrs and int(attrs["top"]) == 1: + design.hierarchy = make_mod_hier(m["name"], m["name"], "", (m["name"],)) + + for prop in design.hierarchy: + design.properties_by_path[prop.path[1:]] = prop + return design + else: + raise ValueError("Cannot find top module") + +def main(): + import sys + if len(sys.argv) != 2: + print(f"""Usage: {sys.argv[0]} design.json""") + with open(sys.argv[1]) as f: + design = design_hierarchy(f) + print("Design Hierarchy:", design.hierarchy) + for p in design.hierarchy.get_property_list(): + print("Property:", p) + print("Memory Bits:", design.memory_bits) + +if __name__ == '__main__': + main() diff --git a/sbysrc/sby_engine_abc.py b/sbysrc/sby_engine_abc.py index 10e12687..1fabe6fa 100644 --- a/sbysrc/sby_engine_abc.py +++ b/sbysrc/sby_engine_abc.py @@ -16,51 +16,180 @@ # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. # -import re, os, getopt +import re, getopt +import json from sby_core import SbyProc +from sby_engine_aiger import aigsmt_exit_callback, aigsmt_trace_callback + + +def abc_getopt(args, long): + long = set(long) + output = [] + parsed = [] + toggles = set() + pos = 0 + + while pos < len(args): + arg = args[pos] + pos += 1 + if not arg.startswith('-'): + output.append(arg) + elif arg == '--': + output.extend(args[pos:]) + break + elif arg.startswith('--'): + if '=' in arg: + prefix, param = arg.split('=', 1) + if prefix + "=" in long: + parsed.append(prefix, param) + elif arg[2:] in long: + parsed.append((arg, '')) + elif arg[2:] + "=" in long: + parsed.append((arg, args[pos])) + pos += 1 + else: + output.append(arg) + elif arg.startswith('-'): + output.append(arg) + for c in arg[1:]: + if 'A' <= c <= 'Z': + if pos < len(args): + output.append(args[pos]) + pos += 1 + else: + toggles.symmetric_difference_update([c]) + + return output, parsed, toggles + def run(mode, task, engine_idx, engine): - abc_opts, abc_command = getopt.getopt(engine[1:], "", []) + keep_going = False + + fold_command = "fold" + if task.opt_aigfolds: + fold_command += " -s" + + abc_command, custom_options, toggles = abc_getopt(engine[1:], [ + "keep-going", + ]) if len(abc_command) == 0: task.error("Missing ABC command.") - for o, a in abc_opts: - task.error("Unexpected ABC engine options.") + if abc_command[0].startswith('-'): + task.error(f"Unexpected ABC engine option '{abc_command[0]}'.") if abc_command[0] == "bmc3": if mode != "bmc": task.error("ABC command 'bmc3' is only valid in bmc mode.") + for o, a in custom_options: + task.error(f"Option {o} not supported by 'abc {abc_command[0]}'") abc_command[0] += f" -F {task.opt_depth} -v" elif abc_command[0] == "sim3": if mode != "bmc": task.error("ABC command 'sim3' is only valid in bmc mode.") + for o, a in custom_options: + task.error(f"Option {o} not supported by 'abc {abc_command[0]}'") abc_command[0] += f" -F {task.opt_depth} -v" elif abc_command[0] == "pdr": if mode != "prove": task.error("ABC command 'pdr' is only valid in prove mode.") + for o, a in custom_options: + if o == '--keep-going': + keep_going = True + else: + task.error(f"Option {o} not supported by 'abc {abc_command[0]}'") + + abc_command[0] += " -v -l" + + if keep_going: + abc_command += ["-a", "-X", f"engine_{engine_idx}/trace_"] + + if 'd' in toggles: + abc_command += ["-I", f"engine_{engine_idx}/invariants.pla"] + if not task.opt_aigfolds: + fold_command += " -s" + else: task.error(f"Invalid ABC command {abc_command[0]}.") + smtbmc_vcd = task.opt_vcd and not task.opt_vcd_sim + run_aigsmt = smtbmc_vcd or (task.opt_append and task.opt_append_assume) + smtbmc_append = 0 + sim_append = 0 + log = task.log_prefix(f"engine_{engine_idx}") + + if task.opt_append_assume: + smtbmc_append = task.opt_append + elif smtbmc_vcd: + if not task.opt_append_assume: + log("For VCDs generated by smtbmc the option 'append_assume off' is ignored") + smtbmc_append = task.opt_append + else: + sim_append = task.opt_append + proc = SbyProc( task, f"engine_{engine_idx}", task.model("aig"), - f"""cd {task.workdir}; {task.exe_paths["abc"]} -c 'read_aiger model/design_aiger.aig; fold; strash; {" ".join(abc_command)}; write_cex -a engine_{engine_idx}/trace.aiw'""", + f"""cd {task.workdir}; {task.exe_paths["abc"]} -c 'read_aiger model/design_aiger.aig; { + fold_command}; strash; {" ".join(abc_command)}; write_cex -a engine_{engine_idx}/trace.aiw'""", logfile=open(f"{task.workdir}/engine_{engine_idx}/logfile.txt", "w") ) + proc.checkretcode = True proc.noprintregex = re.compile(r"^\.+$") - proc_status = None + proc_status = "UNKNOWN" + + procs_running = 1 + + aiger_props = None + disproved = set() + proved = set() def output_callback(line): nonlocal proc_status - - match = re.match(r"^Output [0-9]+ of miter .* was asserted in frame [0-9]+.", line) - if match: proc_status = "FAIL" + nonlocal procs_running + nonlocal aiger_props + + if aiger_props is None: + with open(f"{task.workdir}/model/design_aiger.ywa") as ywa_file: + ywa = json.load(ywa_file) + aiger_props = [] + for path in ywa["asserts"]: + aiger_props.append(task.design.properties_by_path.get(tuple(path))) + + if keep_going: + match = re.match(r"Writing CEX for output ([0-9]+) to engine_[0-9]+/(.*)\.aiw", line) + if match: + output = int(match[1]) + prop = aiger_props[output] + if prop: + prop.status = "FAIL" + task.status_db.set_task_property_status(prop, data=dict(source="abc pdr", engine=f"engine_{engine_idx}")) + disproved.add(output) + proc_status = "FAIL" + proc = aigsmt_trace_callback(task, engine_idx, proc_status, + run_aigsmt=run_aigsmt, smtbmc_vcd=smtbmc_vcd, smtbmc_append=smtbmc_append, sim_append=sim_append, + name=match[2], + ) + proc.register_exit_callback(exit_callback) + procs_running += 1 + else: + match = re.match(r"^Output [0-9]+ of miter .* was asserted in frame [0-9]+.", line) + if match: proc_status = "FAIL" + + match = re.match(r"^Proved output +([0-9]+) in frame +-?[0-9]+", line) + if match: + output = int(match[1]) + prop = aiger_props[output] + if prop: + prop.status = "PASS" + task.status_db.set_task_property_status(prop, data=dict(source="abc pdr", engine=f"engine_{engine_idx}")) + proved.add(output) match = re.match(r"^Simulation of [0-9]+ frames for [0-9]+ rounds with [0-9]+ restarts did not assert POs.", line) if match: proc_status = "UNKNOWN" @@ -74,53 +203,44 @@ def output_callback(line): match = re.match(r"^Property proved.", line) if match: proc_status = "PASS" + if keep_going: + match = re.match(r"^Properties: All = (\d+). Proved = (\d+). Disproved = (\d+). Undecided = (\d+).", line) + if match: + all_count = int(match[1]) + proved_count = int(match[2]) + disproved_count = int(match[3]) + undecided_count = int(match[4]) + if ( + all_count != len(aiger_props) or + all_count != proved_count + disproved_count + undecided_count or + disproved_count != len(disproved) or + proved_count != len(proved) + ): + log("WARNING: inconsistent status output") + proc_status = "UNKNOWN" + elif proved_count == all_count: + proc_status = "PASS" + elif disproved_count == 0: + proc_status = "UNKNOWN" + else: + proc_status = "FAIL" + return line def exit_callback(retcode): - assert retcode == 0 - assert proc_status is not None - - task.update_status(proc_status) - task.log(f"engine_{engine_idx}: Status returned by engine: {proc_status}") - task.summary.append(f"""engine_{engine_idx} ({" ".join(engine)}) returned {proc_status}""") - - task.terminate() - - if proc_status == "FAIL" and task.opt_aigsmt != "none": - proc2 = SbyProc( - task, - f"engine_{engine_idx}", - task.model("smt2"), - ("cd {}; {} -s {}{} --noprogress --append {} --dump-vcd engine_{i}/trace.vcd --dump-vlogtb engine_{i}/trace_tb.v " + - "--dump-smtc engine_{i}/trace.smtc --aig model/design_aiger.aim:engine_{i}/trace.aiw --aig-noheader model/design_smt2.smt2").format - (task.workdir, task.exe_paths["smtbmc"], task.opt_aigsmt, - "" if task.opt_tbtop is None else f" --vlogtb-top {task.opt_tbtop}", - task.opt_append, i=engine_idx), - logfile=open(f"{task.workdir}/engine_{engine_idx}/logfile2.txt", "w") - ) - - proc2_status = None - - def output_callback2(line): - nonlocal proc2_status - - match = re.match(r"^## [0-9: ]+ Status: FAILED", line) - if match: proc2_status = "FAIL" - - match = re.match(r"^## [0-9: ]+ Status: PASSED", line) - if match: proc2_status = "PASS" - - return line - - def exit_callback2(line): - assert proc2_status is not None - assert proc2_status == "FAIL" - - if os.path.exists(f"{task.workdir}/engine_{engine_idx}/trace.vcd"): - task.summary.append(f"counterexample trace: {task.workdir}/engine_{engine_idx}/trace.vcd") - - proc2.output_callback = output_callback2 - proc2.exit_callback = exit_callback2 + nonlocal procs_running + if keep_going: + procs_running -= 1 + if not procs_running: + if proc_status == "FAIL" and mode == "bmc" and keep_going: + task.pass_unknown_asserts(dict(source="abc pdr", keep_going=True, engine=f"engine_{engine_idx}")) + task.update_status(proc_status) + task.summary.set_engine_status(engine_idx, proc_status) + if proc_status != "UNKNOWN" and not keep_going: + task.terminate() + else: + aigsmt_exit_callback(task, engine_idx, proc_status, + run_aigsmt=run_aigsmt, smtbmc_vcd=smtbmc_vcd, smtbmc_append=smtbmc_append, sim_append=sim_append) proc.output_callback = output_callback - proc.exit_callback = exit_callback + proc.register_exit_callback(exit_callback) diff --git a/sbysrc/sby_engine_aiger.py b/sbysrc/sby_engine_aiger.py index 46656915..fbdf999e 100644 --- a/sbysrc/sby_engine_aiger.py +++ b/sbysrc/sby_engine_aiger.py @@ -16,8 +16,9 @@ # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. # -import re, os, getopt +import re, os, getopt, click from sby_core import SbyProc +from sby_sim import sim_witness_trace def run(mode, task, engine_idx, engine): opts, solver_args = getopt.getopt(engine[1:], "", []) @@ -28,27 +29,57 @@ def run(mode, task, engine_idx, engine): for o, a in opts: task.error("Unexpected AIGER engine options.") + status_2 = "UNKNOWN" + + model_variant = "" + if solver_args[0] == "suprove": + if mode not in ["live", "prove"]: + task.error("The aiger solver 'suprove' is only supported in live and prove modes.") if mode == "live" and (len(solver_args) == 1 or solver_args[1][0] != "+"): solver_args.insert(1, "+simple_liveness") solver_cmd = " ".join([task.exe_paths["suprove"]] + solver_args[1:]) elif solver_args[0] == "avy": + model_variant = "_fold" + if mode != "prove": + task.error("The aiger solver 'avy' is only supported in prove mode.") solver_cmd = " ".join([task.exe_paths["avy"], "--cex", "-"] + solver_args[1:]) elif solver_args[0] == "aigbmc": - solver_cmd = " ".join([task.exe_paths["aigbmc"]] + solver_args[1:]) + if mode != "bmc": + task.error("The aiger solver 'aigbmc' is only supported in bmc mode.") + solver_cmd = " ".join([task.exe_paths["aigbmc"], str(task.opt_depth - 1)] + solver_args[1:]) + status_2 = "PASS" # aigbmc outputs status 2 when BMC passes else: task.error(f"Invalid solver command {solver_args[0]}.") + smtbmc_vcd = task.opt_vcd and not task.opt_vcd_sim + run_aigsmt = (mode != "live") and (smtbmc_vcd or (task.opt_append and task.opt_append_assume)) + smtbmc_append = 0 + sim_append = 0 + log = task.log_prefix(f"engine_{engine_idx}") + + if mode != "live": + if task.opt_append_assume: + smtbmc_append = task.opt_append + elif smtbmc_vcd: + if not task.opt_append_assume: + log("For VCDs generated by smtbmc the option 'append_assume off' is ignored") + smtbmc_append = task.opt_append + else: + sim_append = task.opt_append + proc = SbyProc( task, f"engine_{engine_idx}", - task.model("aig"), - f"cd {task.workdir}; {solver_cmd} model/design_aiger.aig", + task.model(f"aig{model_variant}"), + f"cd {task.workdir}; {solver_cmd} model/design_aiger{model_variant}.aig", logfile=open(f"{task.workdir}/engine_{engine_idx}/logfile.txt", "w") ) + if solver_args[0] not in ["avy"]: + proc.checkretcode = True proc_status = None produced_cex = False @@ -76,78 +107,122 @@ def output_callback(line): print(line, file=aiw_file) if line == "0": proc_status = "PASS" if line == "1": proc_status = "FAIL" - if line == "2": proc_status = "UNKNOWN" + if line == "2": proc_status = status_2 return None def exit_callback(retcode): - if solver_args[0] not in ["avy"]: - assert retcode == 0 - assert proc_status is not None - aiw_file.close() - - task.update_status(proc_status) - task.log(f"engine_{engine_idx}: Status returned by engine: {proc_status}") - task.summary.append(f"""engine_{engine_idx} ({" ".join(engine)}) returned {proc_status}""") - - task.terminate() - - if proc_status == "FAIL" and task.opt_aigsmt != "none": - if produced_cex: - if mode == "live": - proc2 = SbyProc( - task, - f"engine_{engine_idx}", - task.model("smt2"), - ("cd {}; {} -g -s {}{} --noprogress --dump-vcd engine_{i}/trace.vcd --dump-vlogtb engine_{i}/trace_tb.v " + - "--dump-smtc engine_{i}/trace.smtc --aig model/design_aiger.aim:engine_{i}/trace.aiw model/design_smt2.smt2").format - (task.workdir, task.exe_paths["smtbmc"], task.opt_aigsmt, - "" if task.opt_tbtop is None else f" --vlogtb-top {task.opt_tbtop}", - i=engine_idx), - logfile=open(f"{task.workdir}/engine_{engine_idx}/logfile2.txt", "w") - ) - else: - proc2 = SbyProc( - task, - f"engine_{engine_idx}", - task.model("smt2"), - ("cd {}; {} -s {}{} --noprogress --append {} --dump-vcd engine_{i}/trace.vcd --dump-vlogtb engine_{i}/trace_tb.v " + - "--dump-smtc engine_{i}/trace.smtc --aig model/design_aiger.aim:engine_{i}/trace.aiw model/design_smt2.smt2").format - (task.workdir, task.exe_paths["smtbmc"], task.opt_aigsmt, - "" if task.opt_tbtop is None else f" --vlogtb-top {task.opt_tbtop}", - task.opt_append, i=engine_idx), - logfile=open(f"{task.workdir}/engine_{engine_idx}/logfile2.txt", "w") - ) - - proc2_status = None - - def output_callback2(line): - nonlocal proc2_status - - match = re.match(r"^## [0-9: ]+ Status: FAILED", line) - if match: proc2_status = "FAIL" - - match = re.match(r"^## [0-9: ]+ Status: PASSED", line) - if match: proc2_status = "PASS" - - return line - - def exit_callback2(line): - assert proc2_status is not None - if mode == "live": - assert proc2_status == "PASS" - else: - assert proc2_status == "FAIL" - - if os.path.exists(f"{task.workdir}/engine_{engine_idx}/trace.vcd"): - task.summary.append(f"counterexample trace: {task.workdir}/engine_{engine_idx}/trace.vcd") - - proc2.output_callback = output_callback2 - proc2.exit_callback = exit_callback2 - - else: - task.log(f"engine_{engine_idx}: Engine did not produce a counter example.") + aigsmt_exit_callback(task, engine_idx, proc_status, + run_aigsmt=run_aigsmt, smtbmc_vcd=smtbmc_vcd, smtbmc_append=smtbmc_append, sim_append=sim_append, ) proc.output_callback = output_callback - proc.exit_callback = exit_callback + proc.register_exit_callback(exit_callback) + + +def aigsmt_exit_callback(task, engine_idx, proc_status, *, run_aigsmt, smtbmc_vcd, smtbmc_append, sim_append): + if proc_status is None: + task.error(f"engine_{engine_idx}: Could not determine engine status.") + + task.update_status(proc_status) + task.summary.set_engine_status(engine_idx, proc_status) + task.terminate() + if proc_status == "FAIL" and (not run_aigsmt or task.opt_aigsmt != "none"): + aigsmt_trace_callback(task, engine_idx, proc_status, run_aigsmt=run_aigsmt, smtbmc_vcd=smtbmc_vcd, smtbmc_append=smtbmc_append, sim_append=sim_append) + +def aigsmt_trace_callback(task, engine_idx, proc_status, *, run_aigsmt, smtbmc_vcd, smtbmc_append, sim_append, name="trace"): + + trace_prefix = f"engine_{engine_idx}/{name}" + + aiw2yw_suffix = '_aiw' if run_aigsmt else '' + + witness_proc = SbyProc( + task, f"engine_{engine_idx}", [], + f"cd {task.workdir}; {task.exe_paths['witness']} aiw2yw engine_{engine_idx}/{name}.aiw model/design_aiger.ywa engine_{engine_idx}/{name}{aiw2yw_suffix}.yw", + ) + final_proc = witness_proc + + if run_aigsmt: + smtbmc_opts = [] + smtbmc_opts += ["-s", task.opt_aigsmt] + if task.opt_tbtop is not None: + smtbmc_opts += ["--vlogtb-top", task.opt_tbtop] + smtbmc_opts += ["--noprogress", f"--append {smtbmc_append}"] + if smtbmc_vcd: + smtbmc_opts += [f"--dump-vcd {trace_prefix}.vcd"] + smtbmc_opts += [f"--dump-yw {trace_prefix}.yw", f"--dump-vlogtb {trace_prefix}_tb.v", f"--dump-smtc {trace_prefix}.smtc"] + + proc2 = SbyProc( + task, + f"engine_{engine_idx}", + [*task.model("smt2"), witness_proc], + f"cd {task.workdir}; {task.exe_paths['smtbmc']} {' '.join(smtbmc_opts)} --yw engine_{engine_idx}/{name}{aiw2yw_suffix}.yw model/design_smt2.smt2", + logfile=open(f"{task.workdir}/engine_{engine_idx}/logfile2.txt", "w"), + ) + + proc2_status = None + + last_prop = [] + current_step = None + + def output_callback2(line): + nonlocal proc2_status + nonlocal last_prop + nonlocal current_step + + smt2_trans = {'\\':'/', '|':'/'} + + match = re.match(r"^## [0-9: ]+ .* in step ([0-9]+)\.\.", line) + if match: + current_step = int(match[1]) + return line + + match = re.match(r"^## [0-9: ]+ Status: FAILED", line) + if match: proc2_status = "FAIL" + + match = re.match(r"^## [0-9: ]+ Status: PASSED", line) + if match: proc2_status = "PASS" + + match = re.match(r"^## [0-9: ]+ Assert failed in (\S+): (\S+)(?: \((\S+)\))?", line) + if match: + cell_name = match[3] or match[2] + prop = task.design.hierarchy.find_property_by_cellname(cell_name, trans_dict=smt2_trans) + prop.status = "FAIL" + task.status_db.set_task_property_status(prop, data=dict(source="aigsmt", engine=f"engine_{engine_idx}")) + last_prop.append(prop) + return line + + match = re.match(r"^## [0-9: ]+ Writing trace to VCD file: (\S+)", line) + if match: + tracefile = match[1] + trace = os.path.basename(tracefile)[:-4] + task.summary.add_event(engine_idx=engine_idx, trace=trace, path=tracefile) + + if match and last_prop: + for p in last_prop: + task.summary.add_event( + engine_idx=engine_idx, trace=trace, + type=p.celltype, hdlname=p.hdlname, src=p.location, step=current_step) + p.tracefiles.append(tracefile) + last_prop = [] + return line + + return line + + def exit_callback2(retcode): + if proc2_status is None: + task.error(f"engine_{engine_idx}: Could not determine aigsmt status.") + if proc2_status != "FAIL": + task.error(f"engine_{engine_idx}: Unexpected aigsmt status.") + + proc2.output_callback = output_callback2 + proc2.register_exit_callback(exit_callback2) + + final_proc = proc2 + + if task.opt_fst or (task.opt_vcd and task.opt_vcd_sim): + final_proc = sim_witness_trace(f"engine_{engine_idx}", task, engine_idx, f"engine_{engine_idx}/{name}.yw", append=sim_append, deps=[final_proc]) + elif not run_aigsmt: + task.log(f"{click.style(f'engine_{engine_idx}', fg='magenta')}: Engine did not produce a counter example.") + + return final_proc diff --git a/sbysrc/sby_engine_btor.py b/sbysrc/sby_engine_btor.py index 15344d8f..a3e744e3 100644 --- a/sbysrc/sby_engine_btor.py +++ b/sbysrc/sby_engine_btor.py @@ -16,9 +16,10 @@ # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. # -import re, os, getopt +import re, os, getopt, click from types import SimpleNamespace from sby_core import SbyProc +from sby_sim import sim_witness_trace def run(mode, task, engine_idx, engine): random_seed = None @@ -46,19 +47,35 @@ def run(mode, task, engine_idx, engine): elif solver_args[0] == "pono": if random_seed: task.error("Setting the random seed is not available for the pono solver.") - solver_cmd = task.exe_paths["pono"] + f" -v 1 -e bmc -k {task.opt_depth - 1}" + if task.opt_skip is not None: + task.error("The btor engine supports the option skip only for the btormc solver.") + solver_cmd = task.exe_paths["pono"] + f" --witness -v 1 -e bmc -k {task.opt_depth - 1}" + solver_cmd += " ".join([""] + solver_args[1:]) else: task.error(f"Invalid solver command {solver_args[0]}.") + log = task.log_prefix(f"engine_{engine_idx}") + + btorsim_vcd = task.opt_vcd and not task.opt_vcd_sim + run_sim = task.opt_fst or not btorsim_vcd + sim_append = 0 + + if task.opt_append and btorsim_vcd: + log("The BTOR engine does not support the 'append' option when using btorsim.") + else: + sim_append = task.opt_append + + if task.opt_append and task.opt_append_assume: + log("The BTOR engine does not support enforcing assumptions in appended time steps.") + + common_state = SimpleNamespace() common_state.solver_status = None common_state.produced_cex = 0 common_state.expected_cex = 1 common_state.wit_file = None common_state.assert_fail = False - common_state.produced_traces = [] - common_state.print_traces_max = 5 common_state.running_procs = 0 def print_traces_and_terminate(): @@ -84,17 +101,7 @@ def print_traces_and_terminate(): task.error(f"engine_{engine_idx}: Engine terminated without status.") task.update_status(proc_status.upper()) - task.log(f"engine_{engine_idx}: Status returned by engine: {proc_status}") - task.summary.append(f"""engine_{engine_idx} ({" ".join(engine)}) returned {proc_status}""") - - if len(common_state.produced_traces) == 0: - task.log(f"""engine_{engine_idx}: Engine did not produce a{" counter" if mode != "cover" else "n "}example.""") - elif len(common_state.produced_traces) <= common_state.print_traces_max: - task.summary.extend(common_state.produced_traces) - else: - task.summary.extend(common_state.produced_traces[:common_state.print_traces_max]) - excess_traces = len(common_state.produced_traces) - common_state.print_traces_max - task.summary.append(f"""and {excess_traces} further trace{"s" if excess_traces > 1 else ""}""") + task.summary.set_engine_status(engine_idx, proc_status) task.terminate() @@ -110,11 +117,9 @@ def output_callback2(line): def make_exit_callback(suffix): def exit_callback2(retcode): - assert retcode == 0 - - vcdpath = f"{task.workdir}/engine_{engine_idx}/trace{suffix}.vcd" - if os.path.exists(vcdpath): - common_state.produced_traces.append(f"""{"" if mode == "cover" else "counterexample "}trace: {vcdpath}""") + vcdpath = f"engine_{engine_idx}/trace{suffix}.vcd" + if os.path.exists(f"{task.workdir}/{vcdpath}"): + task.summary.add_event(engine_idx=engine_idx, trace=f'trace{suffix}', path=vcdpath, type="$cover" if mode == "cover" else "$assert") common_state.running_procs -= 1 if (common_state.running_procs == 0): @@ -122,19 +127,26 @@ def exit_callback2(retcode): return exit_callback2 + def simple_exit_callback(retcode): + common_state.running_procs -= 1 + if (common_state.running_procs == 0): + print_traces_and_terminate() + def output_callback(line): if mode == "cover": if solver_args[0] == "btormc": match = re.search(r"calling BMC on ([0-9]+) properties", line) if match: common_state.expected_cex = int(match[1]) - assert common_state.produced_cex == 0 + if common_state.produced_cex != 0: + task.error(f"engine_{engine_idx}: Unexpected engine output (property count).") else: task.error(f"engine_{engine_idx}: BTOR solver '{solver_args[0]}' is currently not supported in cover mode.") if (common_state.produced_cex < common_state.expected_cex) and line == "sat": - assert common_state.wit_file == None + if common_state.wit_file != None: + task.error(f"engine_{engine_idx}: Unexpected engine output (sat).") if common_state.expected_cex == 1: common_state.wit_file = open(f"{task.workdir}/engine_{engine_idx}/trace.wit", "w") else: @@ -149,17 +161,39 @@ def output_callback(line): suffix = "" else: suffix = common_state.produced_cex - proc2 = SbyProc( - task, - f"engine_{engine_idx}_{common_state.produced_cex}", - task.model("btor"), - "cd {dir} ; btorsim -c --vcd engine_{idx}/trace{i}.vcd --hierarchical-symbols --info model/design_btor.info model/design_btor.btor engine_{idx}/trace{i}.wit".format(dir=task.workdir, idx=engine_idx, i=suffix), - logfile=open(f"{task.workdir}/engine_{engine_idx}/logfile2.txt", "w") + + model = f"design_btor{'_single' if solver_args[0] == 'pono' else ''}" + + yw_proc = SbyProc( + task, f"engine_{engine_idx}.trace{suffix}", [], + f"cd {task.workdir}; {task.exe_paths['witness']} wit2yw engine_{engine_idx}/trace{suffix}.wit model/{model}.ywb engine_{engine_idx}/trace{suffix}.yw", ) - proc2.output_callback = output_callback2 - proc2.exit_callback = make_exit_callback(suffix) - proc2.checkretcode = True common_state.running_procs += 1 + yw_proc.register_exit_callback(simple_exit_callback) + + btorsim_vcd = (task.opt_vcd and not task.opt_vcd_sim) + + if btorsim_vcd: + # TODO cover runs btorsim not only for trace generation, can we run it without VCD generation in that case? + proc2 = SbyProc( + task, + f"engine_{engine_idx}.trace{suffix}", + task.model("btor"), + "cd {dir} ; btorsim -c --vcd engine_{idx}/trace{i}{i2}.vcd --hierarchical-symbols --info model/design_btor{s}.info model/design_btor{s}.btor engine_{idx}/trace{i}.wit".format(dir=task.workdir, idx=engine_idx, i=suffix, i2='' if btorsim_vcd else '_btorsim', s='_single' if solver_args[0] == 'pono' else ''), + logfile=open(f"{task.workdir}/engine_{engine_idx}/logfile2.txt", "w") + ) + proc2.output_callback = output_callback2 + if run_sim: + proc2.register_exit_callback(simple_exit_callback) + else: + proc2.register_exit_callback(make_exit_callback(suffix)) + proc2.checkretcode = True + common_state.running_procs += 1 + + if run_sim: + sim_proc = sim_witness_trace(f"engine_{engine_idx}", task, engine_idx, f"engine_{engine_idx}/trace{suffix}.yw", append=sim_append, deps=[yw_proc]) + sim_proc.register_exit_callback(simple_exit_callback) + common_state.running_procs += 1 common_state.produced_cex += 1 common_state.wit_file.close() @@ -193,12 +227,9 @@ def output_callback(line): return None def exit_callback(retcode): - if solver_args[0] == "pono": - assert retcode in [0, 1, 255] # UNKNOWN = -1, FALSE = 0, TRUE = 1, ERROR = 2 - else: - assert retcode == 0 if common_state.expected_cex != 0: - assert common_state.solver_status is not None + if common_state.solver_status is None: + task.error(f"engine_{engine_idx}: Could not determine engine status.") if common_state.solver_status == "unsat": if common_state.expected_cex == 1: @@ -216,10 +247,12 @@ def exit_callback(retcode): proc = SbyProc( task, f"engine_{engine_idx}", task.model("btor"), - f"cd {task.workdir}; {solver_cmd} model/design_btor.btor", + f"cd {task.workdir}; {solver_cmd} model/design_btor{'_single' if solver_args[0]=='pono' else ''}.btor", logfile=open(f"{task.workdir}/engine_{engine_idx}/logfile.txt", "w") ) - + proc.checkretcode = True + if solver_args[0] == "pono": + proc.retcodes = [0, 1, 255] # UNKNOWN = -1, FALSE = 0, TRUE = 1, ERROR = 2 proc.output_callback = output_callback - proc.exit_callback = exit_callback + proc.register_exit_callback(exit_callback) common_state.running_procs += 1 diff --git a/sbysrc/sby_engine_smtbmc.py b/sbysrc/sby_engine_smtbmc.py index da2e31c1..5fb0b898 100644 --- a/sbysrc/sby_engine_smtbmc.py +++ b/sbysrc/sby_engine_smtbmc.py @@ -16,8 +16,9 @@ # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. # -import re, os, getopt +import re, os, getopt, click, glob from sby_core import SbyProc +from sby_sim import sim_witness_trace def run(mode, task, engine_idx, engine): smtbmc_opts = [] @@ -31,10 +32,12 @@ def run(mode, task, engine_idx, engine): progress = False basecase_only = False induction_only = False + keep_going = False random_seed = None + task.precise_prop_status = True opts, args = getopt.getopt(engine[1:], "", ["nomem", "syn", "stbv", "stdt", "presat", - "nopresat", "unroll", "nounroll", "dumpsmt2", "progress", "basecase", "induction", "seed="]) + "nopresat", "unroll", "nounroll", "dumpsmt2", "progress", "basecase", "induction", "keep-going", "seed="]) for o, a in opts: if o == "--nomem": @@ -65,6 +68,8 @@ def run(mode, task, engine_idx, engine): if basecase_only: task.error("smtbmc options --basecase and --induction are exclusive.") induction_only = True + elif o == "--keep-going": + keep_going = True elif o == "--seed": random_seed = a else: @@ -125,24 +130,48 @@ def run(mode, task, engine_idx, engine): smtbmc_opts.append("-c") trace_prefix += "%" + if keep_going and mode != "prove_induction": + smtbmc_opts.append("--keep-going") + if mode != "cover": + trace_prefix += "%" + if dumpsmt2: smtbmc_opts += ["--dump-smt2", trace_prefix.replace("%", "") + ".smt2"] if not progress: smtbmc_opts.append("--noprogress") - if task.opt_skip is not None: t_opt = "{}:{}".format(task.opt_skip, task.opt_depth) else: t_opt = "{}".format(task.opt_depth) + smtbmc_vcd = task.opt_vcd and not task.opt_vcd_sim + + smtbmc_append = 0 + sim_append = 0 + + log = task.log_prefix(f"engine_{engine_idx}") + + if task.opt_append_assume: + smtbmc_append = task.opt_append + elif smtbmc_vcd: + if not task.opt_append_assume: + log("For VCDs generated by smtbmc the option 'append_assume off' is ignored") + smtbmc_append = task.opt_append + else: + sim_append = task.opt_append + + trace_ext = 'fst' if task.opt_fst else 'vcd' + random_seed = f"--info \"(set-option :random-seed {random_seed})\"" if random_seed else "" + dump_flags = f"--dump-vcd {trace_prefix}.vcd " if smtbmc_vcd else "" + dump_flags += f"--dump-yw {trace_prefix}.yw --dump-vlogtb {trace_prefix}_tb.v --dump-smtc {trace_prefix}.smtc" proc = SbyProc( task, procname, task.model(model_name), - f"""cd {task.workdir}; {task.exe_paths["smtbmc"]} {" ".join(smtbmc_opts)} -t {t_opt} {random_seed} --append {task.opt_append} --dump-vcd {trace_prefix}.vcd --dump-vlogtb {trace_prefix}_tb.v --dump-smtc {trace_prefix}.smtc model/design_{model_name}.smt2""", + f"""cd {task.workdir}; {task.exe_paths["smtbmc"]} {" ".join(smtbmc_opts)} -t {t_opt} {random_seed} --append {smtbmc_append} {dump_flags} model/design_{model_name}.smt2""", logfile=open(logfile_prefix + ".txt", "w"), logstderr=(not progress) ) @@ -154,9 +183,30 @@ def run(mode, task, engine_idx, engine): task.induction_procs.append(proc) proc_status = None + last_prop = [] + pending_sim = None + current_step = None + procs_running = 1 def output_callback(line): nonlocal proc_status + nonlocal last_prop + nonlocal pending_sim + nonlocal current_step + nonlocal procs_running + + if pending_sim: + sim_proc = sim_witness_trace(procname, task, engine_idx, pending_sim, append=sim_append, inductive=mode == "prove_induction") + sim_proc.register_exit_callback(simple_exit_callback) + procs_running += 1 + pending_sim = None + + smt2_trans = {'\\':'/', '|':'/'} + + match = re.match(r"^## [0-9: ]+ .* in step ([0-9]+)\.\.", line) + if match: + current_step = int(match[1]) + return line match = re.match(r"^## [0-9: ]+ Status: FAILED", line) if match: @@ -178,41 +228,79 @@ def output_callback(line): proc_status = "ERROR" return line + match = re.match(r"^## [0-9: ]+ Assert failed in (\S+): (\S+)(?: \((\S+)\))?", line) + if match: + cell_name = match[3] or match[2] + prop = task.design.hierarchy.find_property_by_cellname(cell_name, trans_dict=smt2_trans) + prop.status = "FAIL" + task.status_db.set_task_property_status(prop, data=dict(source="smtbmc", engine=f"engine_{engine_idx}")) + last_prop.append(prop) + return line + + match = re.match(r"^## [0-9: ]+ Reached cover statement at (\S+)(?: \((\S+)\))? in step \d+\.", line) + if match: + cell_name = match[2] or match[1] + prop = task.design.hierarchy.find_property_by_cellname(cell_name, trans_dict=smt2_trans) + prop.status = "PASS" + task.status_db.set_task_property_status(prop, data=dict(source="smtbmc", engine=f"engine_{engine_idx}")) + last_prop.append(prop) + return line + + if smtbmc_vcd and not task.opt_fst: + match = re.match(r"^## [0-9: ]+ Writing trace to VCD file: (\S+)", line) + if match: + tracefile = match[1] + trace = os.path.basename(tracefile)[:-4] + engine_case = mode.split('_')[1] if '_' in mode else None + task.summary.add_event(engine_idx=engine_idx, trace=trace, path=tracefile, engine_case=engine_case) + + if match and last_prop: + for p in last_prop: + task.summary.add_event( + engine_idx=engine_idx, trace=trace, + type=p.celltype, hdlname=p.hdlname, src=p.location, step=current_step) + p.tracefiles.append(tracefile) + last_prop = [] + return line + else: + match = re.match(r"^## [0-9: ]+ Writing trace to Yosys witness file: (\S+)", line) + if match: + tracefile = match[1] + pending_sim = tracefile + + match = re.match(r"^## [0-9: ]+ Unreached cover statement at (\S+) \((\S+)\)\.", line) + if match: + cell_name = match[2] + prop = task.design.hierarchy.find_property_by_cellname(cell_name, trans_dict=smt2_trans) + prop.status = "FAIL" + task.status_db.set_task_property_status(prop, data=dict(source="smtbmc", engine=f"engine_{engine_idx}")) + return line + def simple_exit_callback(retcode): + nonlocal procs_running + procs_running -= 1 + if not procs_running: + last_exit_callback() + def exit_callback(retcode): if proc_status is None: task.error(f"engine_{engine_idx}: Engine terminated without status.") + simple_exit_callback(retcode) + def last_exit_callback(): if mode == "bmc" or mode == "cover": task.update_status(proc_status) + if proc_status == "FAIL" and mode == "bmc" and keep_going: + task.pass_unknown_asserts(dict(source="smtbmc", keep_going=True, engine=f"engine_{engine_idx}")) proc_status_lower = proc_status.lower() if proc_status == "PASS" else proc_status - task.log(f"engine_{engine_idx}: Status returned by engine: {proc_status_lower}") - task.summary.append(f"""engine_{engine_idx} ({" ".join(engine)}) returned {proc_status_lower}""") - - if proc_status == "FAIL" and mode != "cover": - if os.path.exists(f"{task.workdir}/engine_{engine_idx}/trace.vcd"): - task.summary.append(f"counterexample trace: {task.workdir}/engine_{engine_idx}/trace.vcd") - elif proc_status == "PASS" and mode == "cover": - print_traces_max = 5 - for i in range(print_traces_max): - if os.path.exists(f"{task.workdir}/engine_{engine_idx}/trace{i}.vcd"): - task.summary.append(f"trace: {task.workdir}/engine_{engine_idx}/trace{i}.vcd") - else: - break - else: - excess_traces = 0 - while os.path.exists(f"{task.workdir}/engine_{engine_idx}/trace{print_traces_max + excess_traces}.vcd"): - excess_traces += 1 - if excess_traces > 0: - task.summary.append(f"""and {excess_traces} further trace{"s" if excess_traces > 1 else ""}""") - - task.terminate() + task.summary.set_engine_status(engine_idx, proc_status_lower) + if not keep_going: + task.terminate() elif mode in ["prove_basecase", "prove_induction"]: proc_status_lower = proc_status.lower() if proc_status == "PASS" else proc_status - task.log(f"""engine_{engine_idx}: Status returned by engine for {mode.split("_")[1]}: {proc_status_lower}""") - task.summary.append(f"""engine_{engine_idx} ({" ".join(engine)}) returned {proc_status_lower} for {mode.split("_")[1]}""") + task.summary.set_engine_status(engine_idx, proc_status_lower, mode.split("_")[1]) if mode == "prove_basecase": for proc in task.basecase_procs: @@ -223,8 +311,6 @@ def exit_callback(retcode): else: task.update_status(proc_status) - if os.path.exists(f"{task.workdir}/engine_{engine_idx}/trace.vcd"): - task.summary.append(f"counterexample trace: {task.workdir}/engine_{engine_idx}/trace.vcd") task.terminate() elif mode == "prove_induction": @@ -240,10 +326,11 @@ def exit_callback(retcode): if task.basecase_pass and task.induction_pass: task.update_status("PASS") task.summary.append("successful proof by k-induction.") - task.terminate() + if not keep_going: + task.terminate() else: assert False proc.output_callback = output_callback - proc.exit_callback = exit_callback + proc.register_exit_callback(exit_callback) diff --git a/sbysrc/sby_jobserver.py b/sbysrc/sby_jobserver.py new file mode 100644 index 00000000..57d751db --- /dev/null +++ b/sbysrc/sby_jobserver.py @@ -0,0 +1,338 @@ +# +# SymbiYosys (sby) -- Front-end for Yosys-based formal verification flows +# +# Copyright (C) 2022 Jannis Harder +# +# Permission to use, copy, modify, and/or distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +# + +import atexit +import os +import select +import shlex +import subprocess +import sys +import weakref +import signal + +if os.name == "posix": + import fcntl + +inherited_jobcount = None +inherited_jobserver_auth = None +inherited_jobserver_auth_present = None + +def process_jobserver_environment(): + """Process the environment looking for a make jobserver. This should be called + early (when only inherited fds are present) to reliably detect whether the jobserver + specified in the environment is accessible.""" + global inherited_jobcount + global inherited_jobserver_auth + global inherited_jobserver_auth_present + + if len(sys.argv) >= 2 and sys.argv[1] == '--jobserver-helper': + jobserver_helper(*map(int, sys.argv[2:])) + exit(0) + + inherited_jobserver_auth_present = False + + for flag in shlex.split(os.environ.get("MAKEFLAGS", "")): + if flag.startswith("-j"): + if flag == "-j": + inherited_jobcount = 0 + else: + try: + inherited_jobcount = int(flag[2:]) + except ValueError: + pass + elif flag.startswith("--jobserver-auth=") or flag.startswith("--jobserver-fds="): + inherited_jobserver_auth_present = True + if os.name == "posix": + arg = flag.split("=", 1)[1] + if arg.startswith("fifo:"): + try: + fd = os.open(arg[5:], os.O_RDWR) + except FileNotFoundError: + pass + else: + inherited_jobserver_auth = fd, fd + else: + arg = arg.split(",") + try: + jobserver_fds = int(arg[0]), int(arg[1]) + for fd in jobserver_fds: + fcntl.fcntl(fd, fcntl.F_GETFD) + except (ValueError, OSError): + pass + else: + inherited_jobserver_auth = jobserver_fds + + +def jobserver_helper(jobserver_read_fd, jobserver_write_fd, request_fd, response_fd): + """Helper process to handle blocking jobserver pipes.""" + def handle_sigusr1(*args): + # Since Python doesn't allow user code to handle EINTR anymore, we replace the + # jobserver fd with an fd at EOF to interrupt a blocking read in a way that + # cannot lose any read data + r, w = os.pipe() + os.close(w) + os.dup2(r, jobserver_read_fd) + os.close(r) + signal.signal(signal.SIGINT, signal.SIG_IGN) + signal.signal(signal.SIGUSR1, handle_sigusr1) + pending = 0 + while True: + try: + new_pending = len(os.read(request_fd, 1024)) + if new_pending == 0: + pending = 0 + break + else: + pending += new_pending + continue + except BlockingIOError: + if pending == 0: + select.select([request_fd], [], []) + continue + + if pending > 0: + try: + # Depending on the make version (4.3 vs 4.2) this is blocking or + # non-blocking. As this is an attribute of the pipe not the fd, we + # cannot change it without affecting other processes. Older versions of + # gnu make require this to be blocking, and produce errors if it is + # non-blocking. Newer versions of gnu make set this non-blocking, both, + # as client and as server. The documentation still says it is blocking. + # This leaves us no choice but to handle both cases, which is the reason + # we have this helper process in the first place. + token = os.read(jobserver_read_fd, 1) + except BlockingIOError: + select.select([jobserver_read_fd], [], []) + continue + if not token: + break + + pending -= 1 + + try: + os.write(response_fd, token) + except: + os.write(jobserver_write_fd, token) + raise + os.close(jobserver_write_fd) + + +class SbyJobLease: + def __init__(self, client): + self.client = client + self.is_ready = False + self.is_done = False + + def done(self): + if self.is_ready and not self.is_done: + self.client.return_lease() + + self.is_done = True + + def __repr__(self): + return f"is_ready={self.is_ready} is_done={self.is_done}" + + def __del__(self): + self.done() + + +class SbyJobServer: + def __init__(self, jobcount): + assert jobcount >= 1 + # TODO support unlimited parallelism? + self.jobcount = jobcount + if jobcount == 1: + self.read_fd, self.write_fd = None, None + self.makeflags = None + elif jobcount > 1: + self.read_fd, self.write_fd = os.pipe() + if os.getenv('SBY_BLOCKING_JOBSERVER') != '1': + os.set_blocking(self.read_fd, False) + os.write(self.write_fd, b"*" * (jobcount - 1)) + self.makeflags = f"-j{jobcount} --jobserver-auth={self.read_fd},{self.write_fd} --jobserver-fds={self.read_fd},{self.write_fd}" + + +class SbyJobClient: + def __init__(self, fallback_jobcount=None): + self.jobcount = None + self.read_fd = self.write_fd = None + self.helper_process = None + + self.local_slots = 1 + self.acquired_slots = [] + self.pending_leases = [] + + assert inherited_jobserver_auth_present is not None, "process_jobserver_environment was not called" + + have_jobserver = inherited_jobserver_auth_present + + if os.name == "nt" and inherited_jobserver_auth_present: + # There are even more incompatible variants of the make jobserver on + # windows, none of them are supported for now. + print("WARNING: Found jobserver in MAKEFLAGS, this is not supported on windows.") + have_jobserver = False + + if have_jobserver and inherited_jobserver_auth is None: + print("WARNING: Could not connect to jobserver specified in MAKEFLAGS, disabling parallel execution.") + have_jobserver = False + fallback_jobcount = 1 + + if have_jobserver: + jobcount = inherited_jobcount + elif fallback_jobcount is not None: + jobcount = fallback_jobcount + elif inherited_jobcount is not None and inherited_jobcount > 0: + jobcount = inherited_jobcount + else: + try: + jobcount = len(os.sched_getaffinity(0)) + except AttributeError: + jobcount = os.cpu_count() + + if have_jobserver: + self.read_fd, self.write_fd = inherited_jobserver_auth + elif os.name == "nt": + # On Windows, without a jobserver, use only local slots + self.local_slots = jobcount + else: + self.sby_jobserver = SbyJobServer(jobcount) + self.read_fd = self.sby_jobserver.read_fd + self.write_fd = self.sby_jobserver.write_fd + + self.jobcount = jobcount + + if self.read_fd is not None: + if os.get_blocking(self.read_fd): + request_read_fd, self.request_write_fd = os.pipe() + self.response_read_fd, response_write_fd = os.pipe() + os.set_blocking(self.response_read_fd, False) + os.set_blocking(request_read_fd, False) + + pass_fds = [self.read_fd, self.write_fd, request_read_fd, response_write_fd] + + self.helper_process = subprocess.Popen( + [sys.executable, sys.modules['__main__'].__file__, '--jobserver-helper', *map(str, pass_fds)], + stdin=subprocess.DEVNULL, + pass_fds=pass_fds, + ) + + os.close(request_read_fd) + os.close(response_write_fd) + + atexit.register(self.atexit_blocking) + else: + atexit.register(self.atexit_nonblocking) + + def atexit_nonblocking(self): + while self.acquired_slots: + os.write(self.write_fd, self.acquired_slots.pop()) + + def atexit_blocking(self): + # Return all slot tokens we are currently holding + while self.acquired_slots: + os.write(self.write_fd, self.acquired_slots.pop()) + + if self.helper_process: + # Closing the request pipe singals the helper that we want to exit + os.close(self.request_write_fd) + + # Additionally we send a signal to interrupt a blocking read within the + # helper + self.helper_process.send_signal(signal.SIGUSR1) + + # The helper might have been in the process of sending us some tokens, which + # we still need to return + while True: + try: + token = os.read(self.response_read_fd, 1) + except BlockingIOError: + select.select([self.response_read_fd], [], []) + continue + if not token: + break + os.write(self.write_fd, token) + os.close(self.response_read_fd) + + # Wait for the helper to exit, should be immediate at this point + self.helper_process.wait() + + def request_lease(self): + pending = SbyJobLease(self) + + if self.local_slots > 0: + self.local_slots -= 1 + pending.is_ready = True + else: + self.pending_leases.append(weakref.ref(pending)) + if self.helper_process: + os.write(self.request_write_fd, b"!") + + return pending + + def return_lease(self): + if self.acquired_slots: + os.write(self.write_fd, self.acquired_slots.pop()) + return + + if self.activate_pending_lease(): + return + + self.local_slots += 1 + + def activate_pending_lease(self): + while self.pending_leases: + pending = self.pending_leases.pop(0)() + if pending is None: + continue + pending.is_ready = True + return True + return False + + def has_pending_leases(self): + while self.pending_leases and not self.pending_leases[-1](): + self.pending_leases.pop() + return bool(self.pending_leases) + + def poll_fds(self): + if self.helper_process: + return [self.response_read_fd] + elif self.read_fd is not None and self.has_pending_leases(): + return [self.read_fd] + else: + return [] + + def poll(self): + read_fd = self.response_read_fd if self.helper_process else self.read_fd + if read_fd is None: + return + + while self.helper_process or self.has_pending_leases(): + try: + token = os.read(read_fd, 1) + except BlockingIOError: + break + + self.got_token(token) + + def got_token(self, token): + self.acquired_slots.append(token) + + if self.activate_pending_lease(): + return + + self.return_lease() diff --git a/sbysrc/sby_mode_bmc.py b/sbysrc/sby_mode_bmc.py index fd128edf..173812fd 100644 --- a/sbysrc/sby_mode_bmc.py +++ b/sbysrc/sby_mode_bmc.py @@ -16,19 +16,15 @@ # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. # -import re, os, getopt +import re, os, getopt, click from sby_core import SbyProc def run(task): task.handle_int_option("depth", 20) - task.handle_int_option("append", 0) task.handle_str_option("aigsmt", "yices") - for engine_idx in range(len(task.engines)): - engine = task.engines[engine_idx] - assert len(engine) > 0 - - task.log(f"""engine_{engine_idx}: {" ".join(engine)}""") + for engine_idx, engine in task.engine_list(): + task.log(f"{click.style(f'engine_{engine_idx}', fg='magenta')}: {' '.join(engine)}") task.makedirs(f"{task.workdir}/engine_{engine_idx}") if engine[0] == "smtbmc": @@ -39,9 +35,16 @@ def run(task): import sby_engine_abc sby_engine_abc.run("bmc", task, engine_idx, engine) + elif engine[0] == "aiger": + import sby_engine_aiger + sby_engine_aiger.run("bmc", task, engine_idx, engine) + elif engine[0] == "btor": import sby_engine_btor sby_engine_btor.run("bmc", task, engine_idx, engine) + elif engine[0] == "none": + pass + else: task.error(f"Invalid engine '{engine[0]}' for bmc mode.") diff --git a/sbysrc/sby_mode_cover.py b/sbysrc/sby_mode_cover.py index 858ab9a7..c94c6396 100644 --- a/sbysrc/sby_mode_cover.py +++ b/sbysrc/sby_mode_cover.py @@ -16,18 +16,14 @@ # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. # -import re, os, getopt +import re, os, getopt, click from sby_core import SbyProc def run(task): task.handle_int_option("depth", 20) - task.handle_int_option("append", 0) - for engine_idx in range(len(task.engines)): - engine = task.engines[engine_idx] - assert len(engine) > 0 - - task.log(f"""engine_{engine_idx}: {" ".join(engine)}""") + for engine_idx, engine in task.engine_list(): + task.log(f"{click.style(f'engine_{engine_idx}', fg='magenta')}: {' '.join(engine)}") task.makedirs(f"{task.workdir}/engine_{engine_idx}") if engine[0] == "smtbmc": @@ -38,5 +34,8 @@ def run(task): import sby_engine_btor sby_engine_btor.run("cover", task, engine_idx, engine) + elif engine[0] == "none": + pass + else: task.error(f"Invalid engine '{engine[0]}' for cover mode.") diff --git a/sbysrc/sby_mode_live.py b/sbysrc/sby_mode_live.py index a6330537..89bcc577 100644 --- a/sbysrc/sby_mode_live.py +++ b/sbysrc/sby_mode_live.py @@ -16,7 +16,7 @@ # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. # -import re, os, getopt +import re, os, getopt, click from sby_core import SbyProc def run(task): @@ -24,16 +24,16 @@ def run(task): task.status = "UNKNOWN" - for engine_idx in range(len(task.engines)): - engine = task.engines[engine_idx] - assert len(engine) > 0 - - task.log(f"""engine_{engine_idx}: {" ".join(engine)}""") + for engine_idx, engine in task.engine_list(): + task.log(f"{click.style(f'engine_{engine_idx}', fg='magenta')}: {' '.join(engine)}") task.makedirs(f"{task.workdir}/engine_{engine_idx}") if engine[0] == "aiger": import sby_engine_aiger sby_engine_aiger.run("live", task, engine_idx, engine) + elif engine[0] == "none": + pass + else: task.error(f"Invalid engine '{engine[0]}' for live mode.") diff --git a/sbysrc/sby_mode_prove.py b/sbysrc/sby_mode_prove.py index 6b446a8a..e50fbfdb 100644 --- a/sbysrc/sby_mode_prove.py +++ b/sbysrc/sby_mode_prove.py @@ -16,12 +16,11 @@ # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. # -import re, os, getopt +import re, os, getopt, click from sby_core import SbyProc def run(task): task.handle_int_option("depth", 20) - task.handle_int_option("append", 0) task.handle_str_option("aigsmt", "yices") task.status = "UNKNOWN" @@ -31,11 +30,8 @@ def run(task): task.basecase_procs = list() task.induction_procs = list() - for engine_idx in range(len(task.engines)): - engine = task.engines[engine_idx] - assert len(engine) > 0 - - task.log(f"""engine_{engine_idx}: {" ".join(engine)}""") + for engine_idx, engine in task.engine_list(): + task.log(f"{click.style(f'engine_{engine_idx}', fg='magenta')}: {' '.join(engine)}") task.makedirs(f"{task.workdir}/engine_{engine_idx}") if engine[0] == "smtbmc": @@ -50,5 +46,8 @@ def run(task): import sby_engine_abc sby_engine_abc.run("prove", task, engine_idx, engine) + elif engine[0] == "none": + pass + else: task.error(f"Invalid engine '{engine[0]}' for prove mode.") diff --git a/sbysrc/sby_sim.py b/sbysrc/sby_sim.py new file mode 100644 index 00000000..46584075 --- /dev/null +++ b/sbysrc/sby_sim.py @@ -0,0 +1,102 @@ +# +# SymbiYosys (sby) -- Front-end for Yosys-based formal verification flows +# +# Copyright (C) 2022 Jannis Harder +# +# Permission to use, copy, modify, and/or distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +# + +import os, re, glob, json +from sby_core import SbyProc +from sby_design import pretty_path + +def sim_witness_trace(prefix, task, engine_idx, witness_file, *, append, inductive=False, deps=()): + trace_name = os.path.basename(witness_file)[:-3] + formats = [] + tracefile = None + if task.opt_vcd and task.opt_vcd_sim: + tracefile = f"engine_{engine_idx}/{trace_name}.vcd" + formats.append(f"-vcd {trace_name}.vcd") + if task.opt_fst: + tracefile = f"engine_{engine_idx}/{trace_name}.fst" + formats.append(f"-fst {trace_name}.fst") + + # for warnings / error messages + error_tracefile = f"{task.workdir}/{tracefile}" or f"{task.workdir}/engine_{engine_idx}/{trace_name}.yw" + + sim_log = task.log_prefix(f"{prefix}.{trace_name}") + + sim_log(f"Generating simulation trace for witness file: {witness_file}") + + with open(f"{task.workdir}/engine_{engine_idx}/{trace_name}.ys", "w") as f: + print(f"# running in {task.workdir}/engine_{engine_idx}/", file=f) + print("read_rtlil ../model/design_prep.il", file=f) + sim_args = "" + if inductive: + sim_args += " -noinitstate" + print(f"sim -hdlname -summary {trace_name}.json -append {append}{sim_args} -r {trace_name}.yw {' '.join(formats)}", file=f) + + def exit_callback(retval): + + if task.design: + task.precise_prop_status = True + + assertion_types = set() + + with open(f"{task.workdir}/engine_{engine_idx}/{trace_name}.json") as summary: + summary = json.load(summary) + for assertion in summary["assertions"]: + assertion["path"] = tuple(assertion["path"]) + + first_appended = summary["steps"] + 1 - append + + printed_assumption_warning = False + + task.summary.add_event(engine_idx=engine_idx, trace=trace_name, path=tracefile) + + for assertion in summary["assertions"]: + if task.design: + try: + prop = task.design.properties_by_path[tuple(assertion["path"])] + except KeyError: + prop = None + else: + prop = None + + hdlname = pretty_path((summary['top'], *assertion['path'])).rstrip() + task.summary.add_event( + engine_idx=engine_idx, + trace=trace_name, path=tracefile, hdlname=hdlname, + type=assertion["type"], src=assertion.get("src"), step=assertion["step"], + prop=prop) + + assertion_types.add(assertion["type"]) + + if assertion["type"] == '$assume': + if assertion["step"] < first_appended: + task.error(f"produced trace {error_tracefile!r} violates assumptions during simulation") + elif not printed_assumption_warning: + sim_log(f"Warning: trace {error_tracefile!r} violates assumptions during simulation of the appended time steps.") + if not task.opt_append_assume: + sim_log("For supported engines, the option 'append_assume on' can be used to find inputs that uphold assumptions during appended time steps.") + printed_assumption_warning = True + + proc = SbyProc( + task, + f"{prefix}.{trace_name}", + deps, + f"""cd {task.workdir}/engine_{engine_idx}; {task.exe_paths["yosys"]} -ql {trace_name}.log {trace_name}.ys""", + ) + proc.noprintregex = re.compile(r"Warning: Assert .* failed.*") + proc.register_exit_callback(exit_callback) + return proc diff --git a/sbysrc/sby_status.py b/sbysrc/sby_status.py new file mode 100644 index 00000000..e4722c3c --- /dev/null +++ b/sbysrc/sby_status.py @@ -0,0 +1,344 @@ +from __future__ import annotations + +import sqlite3 +import os +import time +import json +from collections import defaultdict +from functools import wraps +from pathlib import Path +from typing import Any, Callable, TypeVar, Optional, Iterable +from sby_design import SbyProperty, pretty_path + + +Fn = TypeVar("Fn", bound=Callable[..., Any]) + + +def transaction(method: Fn) -> Fn: + @wraps(method) + def wrapper(self: SbyStatusDb, *args: Any, **kwargs: Any) -> Any: + if self._transaction_active: + return method(self, *args, **kwargs) + + try: + self.log_debug(f"begin {method.__name__!r} transaction") + self.db.execute("begin") + self._transaction_active = True + result = method(self, *args, **kwargs) + self.db.execute("commit") + self._transaction_active = False + self.log_debug(f"comitted {method.__name__!r} transaction") + return result + except sqlite3.OperationalError as err: + self.log_debug(f"failed {method.__name__!r} transaction {err}") + self.db.rollback() + self._transaction_active = False + except Exception as err: + self.log_debug(f"failed {method.__name__!r} transaction {err}") + self.db.rollback() + self._transaction_active = False + raise + try: + self.log_debug( + f"retrying {method.__name__!r} transaction once in immediate mode" + ) + self.db.execute("begin immediate") + self._transaction_active = True + result = method(self, *args, **kwargs) + self.db.execute("commit") + self._transaction_active = False + self.log_debug(f"comitted {method.__name__!r} transaction") + return result + except Exception as err: + self.log_debug(f"failed {method.__name__!r} transaction {err}") + self.db.rollback() + self._transaction_active = False + raise + + return wrapper # type: ignore + + +class SbyStatusDb: + def __init__(self, path: Path, task, timeout: float = 5.0): + self.debug = False + self.task = task + self._transaction_active = False + + setup = not os.path.exists(path) + + self.db = sqlite3.connect(path, isolation_level=None, timeout=timeout) + self.db.row_factory = sqlite3.Row + self.db.execute("PRAGMA journal_mode=WAL") + self.db.execute("PRAGMA synchronous=0") + + if setup: + self._setup() + + if task is not None: + self.task_id = self.create_task(workdir=task.workdir, mode=task.opt_mode) + + def log_debug(self, *args): + if self.debug: + if self.task: + self.task.log(" ".join(str(arg) for arg in args)) + else: + print(*args) + + @transaction + def _setup(self): + script = """ + CREATE TABLE task ( + id INTEGER PRIMARY KEY, + workdir TEXT, + mode TEXT, + created REAL + ); + CREATE TABLE task_status ( + id INTEGER PRIMARY KEY, + task INTEGER, + status TEXT, + data TEXT, + created REAL, + FOREIGN KEY(task) REFERENCES task(id) + ); + CREATE TABLE task_property ( + id INTEGER PRIMARY KEY, + task INTEGER, + src TEXT, + name TEXT, + created REAL, + FOREIGN KEY(task) REFERENCES task(id) + ); + CREATE TABLE task_property_status ( + id INTEGER PRIMARY KEY, + task_property INTEGER, + status TEXT, + data TEXT, + created REAL, + FOREIGN KEY(task_property) REFERENCES task_property(id) + ); + CREATE TABLE task_property_data ( + id INTEGER PRIMARY KEY, + task_property INTEGER, + kind TEXT, + data TEXT, + created REAL, + FOREIGN KEY(task_property) REFERENCES task_property(id) + ); + """ + for statement in script.split(";\n"): + statement = statement.strip() + if statement: + self.db.execute(statement) + + @transaction + def create_task(self, workdir: str, mode: str) -> int: + return self.db.execute( + """ + INSERT INTO task (workdir, mode, created) + VALUES (:workdir, :mode, :now) + """, + dict(workdir=workdir, mode=mode, now=time.time()), + ).lastrowid + + @transaction + def create_task_properties( + self, properties: Iterable[SbyProperty], *, task_id: Optional[int] = None + ): + if task_id is None: + task_id = self.task_id + now = time.time() + self.db.executemany( + """ + INSERT INTO task_property (name, src, task, created) + VALUES (:name, :src, :task, :now) + """, + [ + dict( + name=json.dumps(prop.path), + src=prop.location or "", + task=task_id, + now=now, + ) + for prop in properties + ], + ) + + @transaction + def set_task_status( + self, + status: Optional[str] = None, + data: Any = None, + ): + if status is None: + status = property.status + + now = time.time() + self.db.execute( + """ + INSERT INTO task_status ( + task, status, data, created + ) + VALUES ( + :task, :status, :data, :now + ) + """, + dict( + task=self.task_id, + status=status, + data=json.dumps(data), + now=now, + ), + ) + + @transaction + def set_task_property_status( + self, + property: SbyProperty, + status: Optional[str] = None, + data: Any = None, + ): + if status is None: + status = property.status + + now = time.time() + self.db.execute( + """ + INSERT INTO task_property_status ( + task_property, status, data, created + ) + VALUES ( + (SELECT id FROM task_property WHERE task = :task AND name = :name), + :status, :data, :now + ) + """, + dict( + task=self.task_id, + name=json.dumps(property.path), + status=status, + data=json.dumps(data), + now=now, + ), + ) + + @transaction + def add_task_property_data(self, property: SbyProperty, kind: str, data: Any): + now = time.time() + self.db.execute( + """ + INSERT INTO task_property_data ( + task_property, kind, data, created + ) + VALUES ( + (SELECT id FROM task_property WHERE task = :task AND name = :name), + :kind, :data, :now + ) + """, + dict( + task=self.task_id, + name=json.dumps(property.path), + kind=kind, + data=json.dumps(data), + now=now, + ), + ) + + @transaction + def all_tasks(self): + rows = self.db.execute( + """ + SELECT id, workdir, created FROM task + """ + ).fetchall() + + return {row["id"]: dict(row) for row in rows} + + @transaction + def all_task_properties(self): + rows = self.db.execute( + """ + SELECT id, task, src, name, created FROM task_property + """ + ).fetchall() + + def get_result(row): + row = dict(row) + row["name"] = tuple(json.loads(row.get("name", "[]"))) + row["data"] = json.loads(row.get("data", "null")) + return row + + return {row["id"]: get_result(row) for row in rows} + + @transaction + def all_task_property_statuses(self): + rows = self.db.execute( + """ + SELECT id, task_property, status, data, created + FROM task_property_status + """ + ).fetchall() + + def get_result(row): + row = dict(row) + row["data"] = json.loads(row.get("data", "null")) + return row + + return {row["id"]: get_result(row) for row in rows} + + @transaction + def all_status_data(self): + return ( + self.all_tasks(), + self.all_task_properties(), + self.all_task_property_statuses(), + ) + + @transaction + def reset(self): + self.db.execute("""DELETE FROM task_property_status""") + self.db.execute("""DELETE FROM task_property_data""") + self.db.execute("""DELETE FROM task_property""") + self.db.execute("""DELETE FROM task_status""") + self.db.execute("""DELETE FROM task""") + + def print_status_summary(self): + tasks, task_properties, task_property_statuses = self.all_status_data() + properties = defaultdict(set) + + uniquify_paths = defaultdict(dict) + + def add_status(task_property, status): + + display_name = task_property["name"] + if display_name[-1].startswith("$"): + counters = uniquify_paths[task_property["src"]] + counter = counters.setdefault(display_name[-1], len(counters) + 1) + if task_property["src"]: + if counter < 2: + path_based = f"" + else: + path_based = f"" + else: + path_based = f"" + display_name = (*display_name[:-1], path_based) + + properties[display_name].add(status) + + for task_property in task_properties.values(): + add_status(task_property, "UNKNOWN") + + for status in task_property_statuses.values(): + task_property = task_properties[status["task_property"]] + add_status(task_property, status["status"]) + + for display_name, statuses in sorted(properties.items()): + print(pretty_path(display_name), combine_statuses(statuses)) + + +def combine_statuses(statuses): + statuses = set(statuses) + + if len(statuses) > 1: + statuses.discard("UNKNOWN") + + return ",".join(sorted(statuses)) diff --git a/tests/.gitignore b/tests/.gitignore index 1feaa191..9737325a 100644 --- a/tests/.gitignore +++ b/tests/.gitignore @@ -1,9 +1,2 @@ -/both_ex*/ -/cover*/ -/demo*/ -/memory*/ -/mixed*/ -/preunsat*/ -/prv32fmcmp*/ -/redxor*/ -/stopfirst*/ +/make/rules +__pycache__ diff --git a/tests/Makefile b/tests/Makefile index 58971e69..6a586b6e 100644 --- a/tests/Makefile +++ b/tests/Makefile @@ -1,11 +1,56 @@ -SBY_FILES=$(wildcard *.sby) -SBY_TESTS=$(addprefix test_,$(SBY_FILES:.sby=)) +test: -.PHONY: test +.PHONY: test clean refresh help -FORCE: +OS_NAME := $(shell python3 -c "import os;print(os.name)") +ifeq (nt,$(OS_NAME)) +ifeq (quoted,$(shell echo "quoted")) +OS_NAME := nt-unix-like +endif +endif -test: $(SBY_TESTS) +ifeq (nt,$(OS_NAME)) +$(error This Makefile requires unix-like tools and shell, e.g. MSYS2.) +endif -test_%: %.sby FORCE - python3 ../sbysrc/sby.py -f $< +help: + @cat make/help.txt + +export SBY_WORKDIR_GITIGNORE := 1 + +ifeq ($(SBY_CMD),) +SBY_MAIN := $(realpath $(dir $(firstword $(MAKEFILE_LIST)))/../sbysrc/sby.py) +else +SBY_MAIN := $(realpath $(dir $(firstword $(MAKEFILE_LIST)))/make/run_sby.py) +endif + +ifeq (nt-unix-like,$(OS_NAME)) +SBY_MAIN := $(shell cygpath -w $(SBY_MAIN)) +endif +export SBY_MAIN + +make/rules/collect.mk: make/collect_tests.py + python3 make/collect_tests.py + +make/rules/test/%.mk: + python3 make/test_rules.py $< + +ifneq (help,$(MAKECMDGOALS)) + +# This should run every time but only trigger anything depending on it whenever +# the script overwrites make/rules/found_tools. This doesn't really match how +# make targets usually work, so we manually shell out here. + +FIND_TOOLS := $(shell python3 make/required_tools.py || echo error) + +ifneq (,$(findstring error,$(FIND_TOOLS))) +$(error could not run 'python3 make/required_tools.py') +endif + +ifneq (,$(FIND_TOOLS)) +$(warning $(FIND_TOOLS)) +endif + +include make/rules/collect.mk + +endif diff --git a/tests/autotune/Makefile b/tests/autotune/Makefile new file mode 100644 index 00000000..44a02a73 --- /dev/null +++ b/tests/autotune/Makefile @@ -0,0 +1,2 @@ +SUBDIR=autotune +include ../make/subdir.mk diff --git a/tests/autotune/autotune_div.sby b/tests/autotune/autotune_div.sby new file mode 100644 index 00000000..863e1607 --- /dev/null +++ b/tests/autotune/autotune_div.sby @@ -0,0 +1,85 @@ +[tasks] +bmc +cover +prove + +[options] +bmc: mode bmc +cover: mode cover +prove: mode prove +expect pass + +[engines] +smtbmc boolector + +[script] +read -sv autotune_div.sv +prep -top top + +[file autotune_div.sv] +module top #( + parameter WIDTH = 4 // Reduce this if it takes too long on CI +) ( + input clk, + input load, + input [WIDTH-1:0] a, + input [WIDTH-1:0] b, + output reg [WIDTH-1:0] q, + output reg [WIDTH-1:0] r, + output reg done +); + + reg [WIDTH-1:0] a_reg = 0; + reg [WIDTH-1:0] b_reg = 1; + + initial begin + q <= 0; + r <= 0; + done <= 1; + end + + reg [WIDTH-1:0] q_step = 1; + reg [WIDTH-1:0] r_step = 1; + + // This is not how you design a good divider circuit! + always @(posedge clk) begin + if (load) begin + a_reg <= a; + b_reg <= b; + q <= 0; + r <= a; + q_step <= 1; + r_step <= b; + done <= b == 0; + end else begin + if (r_step <= r) begin + q <= q + q_step; + r <= r - r_step; + + if (!r_step[WIDTH-1]) begin + r_step <= r_step << 1; + q_step <= q_step << 1; + end + end else begin + if (!q_step[0]) begin + r_step <= r_step >> 1; + q_step <= q_step >> 1; + end else begin + done <= 1; + end + end + end + end + + always @(posedge clk) begin + assert (r_step == b_reg * q_step); // Helper invariant + + assert (q * b_reg + r == a_reg); // Main invariant & correct output relationship + if (done) assert (r <= b_reg - 1); // Output range + + cover (done); + cover (done && b_reg == 0); + cover (r != a_reg); + cover (r == a_reg); + end +endmodule diff --git a/tests/autotune/autotune_div.sh b/tests/autotune/autotune_div.sh new file mode 100644 index 00000000..e22aa5dd --- /dev/null +++ b/tests/autotune/autotune_div.sh @@ -0,0 +1,3 @@ +#!/bin/bash +set -e +python3 $SBY_MAIN --autotune -f $SBY_FILE $TASK diff --git a/tests/autotune/autotune_options.sby b/tests/autotune/autotune_options.sby new file mode 100644 index 00000000..daacb3f6 --- /dev/null +++ b/tests/autotune/autotune_options.sby @@ -0,0 +1,50 @@ +[tasks] +a +b +c +d + +[options] +mode bmc +expect fail + +[engines] +smtbmc boolector + +[script] +read -sv autotune_div.sv +prep -top top + +[autotune] +a: timeout 60 +a: wait 10%+20 +a: parallel 1 +a: presat on +a: incr on +a: mem on +a: forall on + +b: timeout none +b: parallel auto +b: presat off +b: incr off +b: mem auto +b: mem_threshold 20 +b: forall any + +c: presat any +c: incr any +c: mem any +c: forall auto + +d: incr auto +d: incr_threshold 10 + +[file autotune_div.sv] +module top (input clk); + reg [7:0] counter = 0; + always @(posedge clk) begin + counter <= counter + 1; + assert (counter != 4); + end +endmodule diff --git a/tests/autotune/autotune_options.sh b/tests/autotune/autotune_options.sh new file mode 100644 index 00000000..e22aa5dd --- /dev/null +++ b/tests/autotune/autotune_options.sh @@ -0,0 +1,3 @@ +#!/bin/bash +set -e +python3 $SBY_MAIN --autotune -f $SBY_FILE $TASK diff --git a/tests/junit/JUnit.xsd b/tests/junit/JUnit.xsd new file mode 100644 index 00000000..7a5f1846 --- /dev/null +++ b/tests/junit/JUnit.xsd @@ -0,0 +1,232 @@ + + + + JUnit test result schema for the Apache Ant JUnit and JUnitReport tasks +Copyright © 2011, Windy Road Technology Pty. Limited +The Apache Ant JUnit XML Schema is distributed under the terms of the Apache License Version 2.0 http://www.apache.org/licenses/ +Permission to waive conditions of this license may be requested from Windy Road Support (http://windyroad.org/support). + + + + + + + + + + Contains an aggregation of testsuite results + + + + + + + + + + Derived from testsuite/@name in the non-aggregated documents + + + + + Starts at '0' for the first testsuite and is incremented by 1 for each following testsuite + + + + + + + + + + + + Contains the results of exexuting a testsuite + + + + + Properties (e.g., environment settings) set during test execution + + + + + + + + + + + + + + + + + + + + + + + + + Indicates that the test errored. An errored test is one that had an unanticipated problem. e.g., an unchecked throwable; or a problem with the implementation of the test. Contains as a text node relevant data for the error, e.g., a stack trace + + + + + + + The error message. e.g., if a java exception is thrown, the return value of getMessage() + + + + + The type of error that occured. e.g., if a java execption is thrown the full class name of the exception. + + + + + + + + + Indicates that the test failed. A failure is a test which the code has explicitly failed by using the mechanisms for that purpose. e.g., via an assertEquals. Contains as a text node relevant data for the failure, e.g., a stack trace + + + + + + + The message specified in the assert + + + + + The type of the assert. + + + + + + + + + + Name of the test method + + + + + Full class name for the class the test method is in. + + + + + Time taken (in seconds) to execute the test + + + + + Cell ID of the property + + + + + Kind of property (assert, cover, live) + + + + + Source location of the property + + + + + Tracefile for the property + + + + + + + Data that was written to standard out while the test was executed + + + + + + + + + + Data that was written to standard error while the test was executed + + + + + + + + + + + Full class name of the test for non-aggregated testsuite documents. Class name without the package for aggregated testsuites documents + + + + + + + + + + when the test was executed. Timezone may not be specified. + + + + + Host on which the tests were executed. 'localhost' should be used if the hostname cannot be determined. + + + + + + + + + + The total number of tests in the suite + + + + + The total number of tests in the suite that failed. A failure is a test which the code has explicitly failed by using the mechanisms for that purpose. e.g., via an assertEquals + + + + + The total number of tests in the suite that errored. An errored test is one that had an unanticipated problem. e.g., an unchecked throwable; or a problem with the implementation of the test. + + + + + The total number of ignored or skipped tests in the suite. + + + + + Time taken (in seconds) to execute the tests in the suite + + + + + + + + + diff --git a/tests/junit/Makefile b/tests/junit/Makefile new file mode 100644 index 00000000..dd894033 --- /dev/null +++ b/tests/junit/Makefile @@ -0,0 +1,2 @@ +SUBDIR=junit +include ../make/subdir.mk diff --git a/tests/junit/junit_assert.sby b/tests/junit/junit_assert.sby new file mode 100644 index 00000000..e13f3750 --- /dev/null +++ b/tests/junit/junit_assert.sby @@ -0,0 +1,38 @@ +[tasks] +pass +fail +preunsat + +[options] +mode bmc +depth 1 + +pass: expect pass +fail: expect fail +preunsat: expect error + +[engines] +smtbmc boolector + +[script] +fail: read -define FAIL +preunsat: read -define PREUNSAT +read -sv test.sv +prep -top top + +[file test.sv] +module test(input foo); +always @* assert(foo); +`ifdef FAIL +always @* assert(!foo); +`endif +`ifdef PREUNSAT +always @* assume(!foo); +`endif +endmodule + +module top(); +test test_i ( +.foo(1'b1) +); +endmodule diff --git a/tests/junit/junit_assert.sh b/tests/junit/junit_assert.sh new file mode 100644 index 00000000..f18d8caa --- /dev/null +++ b/tests/junit/junit_assert.sh @@ -0,0 +1,4 @@ +#!/bin/bash +set -e +python3 $SBY_MAIN -f $SBY_FILE $TASK +python3 validate_junit.py $WORKDIR/$WORKDIR.xml diff --git a/tests/junit/junit_cover.sby b/tests/junit/junit_cover.sby new file mode 100644 index 00000000..53747ba8 --- /dev/null +++ b/tests/junit/junit_cover.sby @@ -0,0 +1,43 @@ +[tasks] +pass +uncovered fail +assert fail +preunsat + +[options] +mode cover +depth 1 + +pass: expect pass +fail: expect fail +preunsat: expect fail + +[engines] +smtbmc boolector + +[script] +uncovered: read -define FAIL +assert: read -define FAIL_ASSERT +preunsat: read -define PREUNSAT +read -sv test.sv +prep -top top + +[file test.sv] +module test(input foo); +`ifdef PREUNSAT +always @* assume(!foo); +`endif +always @* cover(foo); +`ifdef FAIL +always @* cover(!foo); +`endif +`ifdef FAIL_ASSERT +always @* assert(!foo); +`endif +endmodule + +module top(); +test test_i ( +.foo(1'b1) +); +endmodule diff --git a/tests/junit/junit_cover.sh b/tests/junit/junit_cover.sh new file mode 100644 index 00000000..f18d8caa --- /dev/null +++ b/tests/junit/junit_cover.sh @@ -0,0 +1,4 @@ +#!/bin/bash +set -e +python3 $SBY_MAIN -f $SBY_FILE $TASK +python3 validate_junit.py $WORKDIR/$WORKDIR.xml diff --git a/tests/junit/junit_expect.sby b/tests/junit/junit_expect.sby new file mode 100644 index 00000000..63d65a6e --- /dev/null +++ b/tests/junit/junit_expect.sby @@ -0,0 +1,16 @@ +[options] +mode bmc +depth 1 +expect fail,timeout + +[engines] +smtbmc + +[script] +read -formal foo.v +prep -top foo + +[file foo.v] +module foo; +always_comb assert(1); +endmodule diff --git a/tests/junit/junit_expect.sh b/tests/junit/junit_expect.sh new file mode 100644 index 00000000..cb66b10c --- /dev/null +++ b/tests/junit/junit_expect.sh @@ -0,0 +1,4 @@ +#!/bin/bash +set -e +! python3 $SBY_MAIN -f $SBY_FILE $TASK +grep '' $WORKDIR/$WORKDIR.xml diff --git a/tests/junit/junit_nocodeloc.sby b/tests/junit/junit_nocodeloc.sby new file mode 100644 index 00000000..5d2afc88 --- /dev/null +++ b/tests/junit/junit_nocodeloc.sby @@ -0,0 +1,20 @@ +[options] +mode bmc + +expect fail + +[engines] +smtbmc boolector + +[script] +read -sv multi_assert.v +prep -top test +setattr -unset src + +[file multi_assert.v] +module test(); +always @* begin +assert (1); +assert (0); +end +endmodule diff --git a/tests/junit/junit_nocodeloc.sh b/tests/junit/junit_nocodeloc.sh new file mode 100644 index 00000000..f18d8caa --- /dev/null +++ b/tests/junit/junit_nocodeloc.sh @@ -0,0 +1,4 @@ +#!/bin/bash +set -e +python3 $SBY_MAIN -f $SBY_FILE $TASK +python3 validate_junit.py $WORKDIR/$WORKDIR.xml diff --git a/tests/junit/junit_timeout_error.sby b/tests/junit/junit_timeout_error.sby new file mode 100644 index 00000000..551de49e --- /dev/null +++ b/tests/junit/junit_timeout_error.sby @@ -0,0 +1,42 @@ +[tasks] +syntax error +solver error +timeout + +[options] +mode cover +depth 1 +timeout: timeout 1 +error: expect error +timeout: expect timeout + +[engines] +~solver: smtbmc --dumpsmt2 --progress --stbv z3 +solver: smtbmc foo + +[script] +read -noverific +syntax: read -define SYNTAX_ERROR +read -sv primes.sv +prep -top primes + +[file primes.sv] +module primes; + parameter [8:0] offset = 7; + (* anyconst *) reg [8:0] prime1; + wire [9:0] prime2 = prime1 + offset; + (* allconst *) reg [4:0] factor; + +`ifdef SYNTAX_ERROR + foo +`endif + + always @* begin + if (1 < factor && factor < prime1) + assume ((prime1 % factor) != 0); + if (1 < factor && factor < prime2) + assume ((prime2 % factor) != 0); + assume (1 < prime1); + cover (1); + end +endmodule diff --git a/tests/junit/junit_timeout_error.sh b/tests/junit/junit_timeout_error.sh new file mode 100644 index 00000000..f18d8caa --- /dev/null +++ b/tests/junit/junit_timeout_error.sh @@ -0,0 +1,4 @@ +#!/bin/bash +set -e +python3 $SBY_MAIN -f $SBY_FILE $TASK +python3 validate_junit.py $WORKDIR/$WORKDIR.xml diff --git a/tests/junit/validate_junit.py b/tests/junit/validate_junit.py new file mode 100644 index 00000000..1999551c --- /dev/null +++ b/tests/junit/validate_junit.py @@ -0,0 +1,28 @@ +try: + from xmlschema import XMLSchema, XMLSchemaValidationError +except ImportError: + import os + if "NOSKIP" not in os.environ.get("MAKEFLAGS", ""): + print() + print("SKIPPING python library xmlschema not found, skipping JUnit output validation") + print() + exit(0) + +import argparse + +def main(): + parser = argparse.ArgumentParser(description="Validate JUnit output") + parser.add_argument('xml') + parser.add_argument('--xsd', default="JUnit.xsd") + + args = parser.parse_args() + + schema = XMLSchema(args.xsd) + try: + schema.validate(args.xml) + except XMLSchemaValidationError as e: + print(e) + exit(1) + +if __name__ == '__main__': + main() diff --git a/tests/keepgoing/Makefile b/tests/keepgoing/Makefile new file mode 100644 index 00000000..0727e8b6 --- /dev/null +++ b/tests/keepgoing/Makefile @@ -0,0 +1,2 @@ +SUBDIR=keepgoing +include ../make/subdir.mk diff --git a/tests/keepgoing/check_output.py b/tests/keepgoing/check_output.py new file mode 100644 index 00000000..fb2b969b --- /dev/null +++ b/tests/keepgoing/check_output.py @@ -0,0 +1,17 @@ +import re + + +def line_ref(dir, filename, pattern): + with open(dir + "/src/" + filename) as file: + if isinstance(pattern, str): + pattern_re = re.compile(re.escape(pattern)) + else: + pattern_re = pattern + pattern = pattern.pattern + + for number, line in enumerate(file, 1): + if pattern_re.search(line): + # Needs to match source locations for both verilog frontends + return fr"{filename}:(?:{number}|\d+\.\d+-{number}\.\d+)" + + raise RuntimeError("%s: pattern `%s` not found" % (filename, pattern)) diff --git a/tests/keepgoing/keepgoing_multi_step.py b/tests/keepgoing/keepgoing_multi_step.py new file mode 100644 index 00000000..d250614b --- /dev/null +++ b/tests/keepgoing/keepgoing_multi_step.py @@ -0,0 +1,44 @@ +import sys +from check_output import * + +src = "keepgoing_multi_step.sv" + +workdir = sys.argv[1] + +assert_0 = line_ref(workdir, src, "assert(0)") +step_3_7 = line_ref(workdir, src, "step 3,7") +step_5 = line_ref(workdir, src, "step 5") +step_7 = line_ref(workdir, src, "step 7") + +log = open(workdir + "/logfile.txt").read() + +if "_abc]" not in log: + log_per_trace = log.split("Writing trace to Yosys witness file")[:-1] + assert len(log_per_trace) == 4 + assert re.search(r"Assert failed in test: %s \(.*\)$" % assert_0, log_per_trace[0], re.M) + + for i in range(1, 4): + assert re.search(r"Assert failed in test: %s \(.*\) \[failed before\]$" % assert_0, log_per_trace[i], re.M) + + + assert re.search(r"Assert failed in test: %s \(.*\)$" % step_3_7, log_per_trace[1], re.M) + assert re.search(r"Assert failed in test: %s \(.*\)$" % step_5, log_per_trace[2], re.M) + assert re.search(r"Assert failed in test: %s \(.*\) \[failed before\]$" % step_3_7, log_per_trace[3], re.M) + assert re.search(r"Assert failed in test: %s \(.*\)$" % step_7, log_per_trace[3], re.M) + + pattern = f"Property ASSERT in test at {assert_0} failed. Trace file: engine_0/trace0.(vcd|fst)" + assert re.search(pattern, open(f"{workdir}/{workdir}.xml").read()) + +log_per_trace = log.split("summary: counterexample trace")[1:] +assert len(log_per_trace) == 4 + +for i in range(4): + assert re.search(r"failed assertion test\..* at %s" % assert_0, log_per_trace[i], re.M) + +step_3_7_traces = [i for i, t in enumerate(log_per_trace) if re.search(r"failed assertion test\..* at %s" % step_3_7, t, re.M)] +step_5_traces = [i for i, t in enumerate(log_per_trace) if re.search(r"failed assertion test\..* at %s" % step_5, t, re.M)] +step_7_traces = [i for i, t in enumerate(log_per_trace) if re.search(r"failed assertion test\..* at %s" % step_7, t, re.M)] + +assert len(step_3_7_traces) == 2 +assert len(step_5_traces) == 1 +assert len(step_7_traces) == 1 diff --git a/tests/keepgoing/keepgoing_multi_step.sby b/tests/keepgoing/keepgoing_multi_step.sby new file mode 100644 index 00000000..5e6a985e --- /dev/null +++ b/tests/keepgoing/keepgoing_multi_step.sby @@ -0,0 +1,20 @@ +[tasks] +bmc +prove +abc : prove + +[options] +bmc: mode bmc +prove: mode prove +expect fail + +[engines] +~abc: smtbmc --keep-going boolector +abc: abc --keep-going pdr + +[script] +read -sv keepgoing_multi_step.sv +prep -top test + +[files] +keepgoing_multi_step.sv diff --git a/tests/keepgoing/keepgoing_multi_step.sh b/tests/keepgoing/keepgoing_multi_step.sh new file mode 100644 index 00000000..aca8be67 --- /dev/null +++ b/tests/keepgoing/keepgoing_multi_step.sh @@ -0,0 +1,4 @@ +#!/bin/bash +set -e +python3 $SBY_MAIN -f $SBY_FILE $TASK +python3 ${SBY_FILE%.sby}.py $WORKDIR diff --git a/tests/keepgoing/keepgoing_multi_step.sv b/tests/keepgoing/keepgoing_multi_step.sv new file mode 100644 index 00000000..553b13ca --- /dev/null +++ b/tests/keepgoing/keepgoing_multi_step.sv @@ -0,0 +1,23 @@ +module test ( + input clk, a +); + reg [7:0] counter = 0; + + always @(posedge clk) begin + counter <= counter + 1; + end + + always @(posedge clk) begin + assert(0); + if (counter == 3 || counter == 7) begin + assert(a); // step 3,7 + end + if (counter == 5) begin + assert(a); // step 5 + end + if (counter == 7) begin + assert(a); // step 7 + end + assert(1); + end +endmodule diff --git a/tests/keepgoing/keepgoing_same_step.py b/tests/keepgoing/keepgoing_same_step.py new file mode 100644 index 00000000..206d1b3e --- /dev/null +++ b/tests/keepgoing/keepgoing_same_step.py @@ -0,0 +1,19 @@ +import sys +from check_output import * + +workdir = sys.argv[1] +src = "keepgoing_same_step.sv" + +assert_a = line_ref(workdir, src, "assert(a)") +assert_not_a = line_ref(workdir, src, "assert(!a)") +assert_0 = line_ref(workdir, src, "assert(0)") + +log = open(workdir + "/logfile.txt").read() +log_per_trace = log.split("Writing trace to Yosys witness file")[:-1] + +assert len(log_per_trace) == 2 + +assert re.search(r"Assert failed in test: %s \(.*\)$" % assert_a, log, re.M) +assert re.search(r"Assert failed in test: %s \(.*\)$" % assert_not_a, log, re.M) +assert re.search(r"Assert failed in test: %s \(.*\)$" % assert_0, log_per_trace[0], re.M) +assert re.search(r"Assert failed in test: %s \(.*\) \[failed before\]$" % assert_0, log_per_trace[1], re.M) diff --git a/tests/keepgoing/keepgoing_same_step.sby b/tests/keepgoing/keepgoing_same_step.sby new file mode 100644 index 00000000..d344dcc5 --- /dev/null +++ b/tests/keepgoing/keepgoing_same_step.sby @@ -0,0 +1,13 @@ +[options] +mode bmc +expect fail + +[engines] +smtbmc --keep-going boolector + +[script] +read -sv keepgoing_same_step.sv +prep -top test + +[files] +keepgoing_same_step.sv diff --git a/tests/keepgoing/keepgoing_same_step.sh b/tests/keepgoing/keepgoing_same_step.sh new file mode 100644 index 00000000..aca8be67 --- /dev/null +++ b/tests/keepgoing/keepgoing_same_step.sh @@ -0,0 +1,4 @@ +#!/bin/bash +set -e +python3 $SBY_MAIN -f $SBY_FILE $TASK +python3 ${SBY_FILE%.sby}.py $WORKDIR diff --git a/tests/keepgoing/keepgoing_same_step.sv b/tests/keepgoing/keepgoing_same_step.sv new file mode 100644 index 00000000..98fe6d0c --- /dev/null +++ b/tests/keepgoing/keepgoing_same_step.sv @@ -0,0 +1,17 @@ +module test ( + input clk, a +); + reg [7:0] counter = 0; + + always @(posedge clk) begin + counter <= counter + 1; + end + + always @(posedge clk) begin + if (counter == 3) begin + assert(a); + assert(!a); + assert(0); + end + end +endmodule diff --git a/tests/keepgoing/keepgoing_smtc.py b/tests/keepgoing/keepgoing_smtc.py new file mode 100644 index 00000000..b41a7988 --- /dev/null +++ b/tests/keepgoing/keepgoing_smtc.py @@ -0,0 +1,25 @@ +import sys +from check_output import * + +workdir = sys.argv[1] +src = "keepgoing_same_step.sv" + +assert_a = line_ref(workdir, src, "assert(a)") +assert_not_a = line_ref(workdir, src, "assert(!a)") +assert_0 = line_ref(workdir, src, "assert(0)") + +assert_false = line_ref(workdir, "extra.smtc", "assert false") +assert_distinct = line_ref(workdir, "extra.smtc", "assert (distinct") + +log = open(workdir + "/logfile.txt").read() +log_per_trace = log.split("Writing trace to Yosys witness file")[:-1] + +assert len(log_per_trace) == 4 + +assert re.search(r"Assert failed in test: %s \(.*\)$" % assert_a, log, re.M) +assert re.search(r"Assert failed in test: %s \(.*\)$" % assert_not_a, log, re.M) + +assert re.search(r"Assert src/%s failed: false" % assert_false, log_per_trace[0], re.M) +assert re.search(r"Assert failed in test: %s \(.*\)$" % assert_0, log_per_trace[1], re.M) +assert re.search(r"Assert failed in test: %s \(.*\) \[failed before\]$" % assert_0, log_per_trace[2], re.M) +assert re.search(r"Assert src/%s failed: \(distinct" % assert_distinct, log_per_trace[3], re.M) diff --git a/tests/keepgoing/keepgoing_smtc.sby b/tests/keepgoing/keepgoing_smtc.sby new file mode 100644 index 00000000..a4cc7621 --- /dev/null +++ b/tests/keepgoing/keepgoing_smtc.sby @@ -0,0 +1,19 @@ +[options] +mode bmc +expect fail + +[engines] +smtbmc --keep-going boolector -- --smtc src/extra.smtc + +[script] +read -sv keepgoing_same_step.sv +prep -top test + +[files] +keepgoing_same_step.sv + +[file extra.smtc] +state 2 +assert false +always +assert (distinct [counter] #b00000111) diff --git a/tests/keepgoing/keepgoing_smtc.sh b/tests/keepgoing/keepgoing_smtc.sh new file mode 100644 index 00000000..aca8be67 --- /dev/null +++ b/tests/keepgoing/keepgoing_smtc.sh @@ -0,0 +1,4 @@ +#!/bin/bash +set -e +python3 $SBY_MAIN -f $SBY_FILE $TASK +python3 ${SBY_FILE%.sby}.py $WORKDIR diff --git a/tests/make/collect_tests.py b/tests/make/collect_tests.py new file mode 100644 index 00000000..636ecb61 --- /dev/null +++ b/tests/make/collect_tests.py @@ -0,0 +1,59 @@ +from pathlib import Path +import re + +tests = [] +checked_dirs = [] + +SAFE_PATH = re.compile(r"^[a-zA-Z0-9_./\\]*$") + + +def collect(path): + # don't pick up any paths that need escaping nor any sby workdirs + if ( + not SAFE_PATH.match(str(path)) + or (path / "config.sby").exists() + or (path / "status.sqlite").exists() + ): + return + + checked_dirs.append(path) + for entry in path.glob("*.sby"): + filename = str(entry) + if not SAFE_PATH.match(filename): + print(f"skipping {filename!r}, use only [a-zA-Z0-9_./] in filenames") + continue + if entry.name.startswith("skip_"): + continue + tests.append(entry) + for entry in path.glob("*"): + if entry.is_dir(): + collect(entry) + + +def unix_path(path): + return "/".join(path.parts) + + +collect(Path(".")) +collect(Path("../docs/examples")) + +out_file = Path("make/rules/collect.mk") +out_file.parent.mkdir(exist_ok=True) + +with out_file.open("w") as output: + + for checked_dir in checked_dirs: + print(f"{out_file}: {checked_dir}", file=output) + + for test in tests: + test_unix = unix_path(test) + print(f"make/rules/test/{test_unix}.mk: {test_unix}", file=output) + for ext in [".sh", ".py"]: + script_file = test.parent / (test.stem + ext) + if script_file.exists(): + script_file_unix = unix_path(script_file) + print(f"make/rules/test/{test_unix}.mk: {script_file_unix}", file=output) + print(f"make/rules/test/{test_unix}.mk: make/test_rules.py", file=output) + for test in tests: + test_unix = unix_path(test) + print(f"-include make/rules/test/{test_unix}.mk", file=output) diff --git a/tests/make/help.txt b/tests/make/help.txt new file mode 100644 index 00000000..c840c4c6 --- /dev/null +++ b/tests/make/help.txt @@ -0,0 +1,20 @@ +make test: + run all tests (default) + +make clean: + remove all sby workdirs + +make test[_m_][_e_][_s_]: + run all tests that use a specific mode, engine and solver + +make : + run the test for .sby + +make refresh: + do nothing apart from refreshing generated make rules + +make help: + show this help + +running make in a subdirectory or prefixing the target with the subdirectory +limits the test selection to that directory diff --git a/tests/make/required_tools.py b/tests/make/required_tools.py new file mode 100644 index 00000000..82b5f499 --- /dev/null +++ b/tests/make/required_tools.py @@ -0,0 +1,100 @@ +import shutil + +REQUIRED_TOOLS = { + ("smtbmc", "yices"): ["yices-smt2"], + ("smtbmc", "z3"): ["z3"], + ("smtbmc", "cvc4"): ["cvc4"], + ("smtbmc", "cvc5"): ["cvc5"], + ("smtbmc", "mathsat"): ["mathsat"], + ("smtbmc", "boolector"): ["boolector"], + ("smtbmc", "bitwuzla"): ["bitwuzla"], + ("smtbmc", "abc"): ["yosys-abc"], + ("aiger", "suprove"): ["suprove", "yices"], + ("aiger", "avy"): ["avy", "yices"], + ("aiger", "aigbmc"): ["aigbmc", "yices"], + ("btor", "btormc"): ["btormc", "btorsim"], + ("btor", "pono"): ["pono", "btorsim"], + ("abc"): ["yices"], +} + + +def found_tools(): + with open("make/rules/found_tools") as found_tools_file: + return [tool.strip() for tool in found_tools_file.readlines()] + + +if __name__ == "__main__": + import subprocess + import sys + import os + from pathlib import Path + + args = sys.argv[1:] + + if args and args[0] == "run": + target, command, *required_tools = args[1:] + + with open("make/rules/found_tools") as found_tools_file: + found_tools = set(tool.strip() for tool in found_tools_file.readlines()) + + if 'verific' in required_tools: + result = subprocess.run(["yosys", "-qp", "read -verific"], capture_output=True) + if result.returncode: + print() + print(f"SKIPPING {target}: requires yosys with verific support") + print() + exit() + required_tools.remove('verific') + + missing_tools = sorted( + f"`{tool}`" for tool in required_tools if tool not in found_tools + ) + if missing_tools: + noskip = "NOSKIP" in os.environ.get("MAKEFLAGS", "") + print() + print(f"SKIPPING {target}: {', '.join(missing_tools)} not found") + if noskip: + print("NOSKIP was set, treating this as an error") + print() + exit(noskip) + + print(command, flush=True) + exit(subprocess.call(command, shell=True, close_fds=False)) + + found_tools = [] + check_tools = set() + for tools in REQUIRED_TOOLS.values(): + check_tools.update(tools) + + for tool in sorted(check_tools): + if not shutil.which(tool): + continue + + if tool == "btorsim": + error_msg = subprocess.run( + ["btorsim", "--vcd"], + capture_output=True, + text=True, + ).stderr + if "invalid command line option" in error_msg: + print( + "found `btorsim` binary is too old " + "to support the `--vcd` option, ignoring" + ) + continue + + found_tools.append(tool) + + found_tools = "\n".join(found_tools + [""]) + + try: + with open("make/rules/found_tools") as found_tools_file: + if found_tools_file.read() == found_tools: + exit(0) + except FileNotFoundError: + pass + + Path("make/rules").mkdir(exist_ok=True) + + with open("make/rules/found_tools", "w") as found_tools_file: + found_tools_file.write(found_tools) diff --git a/tests/make/run_sby.py b/tests/make/run_sby.py new file mode 100644 index 00000000..9fb7de46 --- /dev/null +++ b/tests/make/run_sby.py @@ -0,0 +1,4 @@ +import os +import sys +prog = os.environ.get("SBY_CMD", "sby") +os.execvp(prog, [prog, *sys.argv[1:]]) diff --git a/tests/make/subdir.mk b/tests/make/subdir.mk new file mode 100644 index 00000000..86b680f9 --- /dev/null +++ b/tests/make/subdir.mk @@ -0,0 +1,17 @@ +TESTDIR ?= .. + +test: + @$(MAKE) -C $(TESTDIR) $(SUBDIR)/$@ + +.PHONY: test refresh IMPLICIT_PHONY + +IMPLICIT_PHONY: + +refresh: + @$(MAKE) -C $(TESTDIR) refresh + +help: + @$(MAKE) -C $(TESTDIR) help + +%: IMPLICIT_PHONY + @$(MAKE) -C $(TESTDIR) $(SUBDIR)/$@ diff --git a/tests/make/test_rules.py b/tests/make/test_rules.py new file mode 100644 index 00000000..8e91bfd0 --- /dev/null +++ b/tests/make/test_rules.py @@ -0,0 +1,129 @@ +import sys +import os +import subprocess +import json +import shlex +from pathlib import Path + +from required_tools import REQUIRED_TOOLS + + +def unix_path(path): + return "/".join(path.parts) + + +sby_file = Path(sys.argv[1]) +sby_dir = sby_file.parent + + +taskinfo = json.loads( + subprocess.check_output( + [sys.executable, os.getenv("SBY_MAIN"), "--dumptaskinfo", sby_file.name], + cwd=sby_dir, + ) +) + + +def parse_engine(engine): + engine, *args = engine + default_solvers = {"smtbmc": "yices"} + for arg in args: + if not arg.startswith("-"): + return engine, arg + return engine, default_solvers.get(engine) + + +rules_file = Path("make/rules/test") / sby_dir / (sby_file.name + ".mk") +rules_file.parent.mkdir(exist_ok=True, parents=True) + +with rules_file.open("w") as rules: + name = str(sby_dir / sby_file.stem) + + for task, info in taskinfo.items(): + target = name + workdirname = sby_file.stem + if task: + target += f"_{task}" + workdirname += f"_{task}" + + engines = set() + solvers = set() + engine_solvers = set() + + required_tools = set() + + for mode_engines in info["engines"].values(): + for engine in mode_engines: + engine, solver = parse_engine(engine) + engines.add(engine) + required_tools.update( + REQUIRED_TOOLS.get((engine, solver), REQUIRED_TOOLS.get(engine, ())) + ) + if solver: + solvers.add(solver) + engine_solvers.add((engine, solver)) + + if any( + line.startswith("read -verific") or line.startswith("verific") + for line in info["script"] + ): + required_tools.add("verific") + + required_tools = sorted(required_tools) + + print(f".PHONY: {target}", file=rules) + print(f"{target}:", file=rules) + + shell_script = sby_dir / f"{sby_file.stem}.sh" + + sby_dir_unix = unix_path(sby_dir) + + if shell_script.exists(): + command = f"cd {sby_dir_unix} && env SBY_FILE={sby_file.name} WORKDIR={workdirname} TASK={task} bash {shell_script.name}" + else: + command = f"cd {sby_dir_unix} && python3 $(SBY_MAIN) -f {sby_file.name} {task}" + + print( + f"\t+@python3 make/required_tools.py run {target} {shlex.quote(command)} {shlex.join(required_tools)}", + file=rules, + ) + + print(f".PHONY: clean-{target}", file=rules) + print(f"clean-{target}:", file=rules) + print(f"\trm -rf {target}", file=rules) + + test_groups = [] + + mode = info["mode"] + + test_groups.append(f"test_m_{mode}") + + for engine in sorted(engines): + test_groups.append(f"test_e_{engine}") + test_groups.append(f"test_m_{mode}_e_{engine}") + + for solver in sorted(solvers): + test_groups.append(f"test_s_{solver}") + test_groups.append(f"test_m_{mode}_s_{solver}") + + for engine, solver in sorted(engine_solvers): + test_groups.append(f"test_e_{engine}_s_{solver}") + test_groups.append(f"test_m_{mode}_e_{engine}_s_{solver}") + + prefix = "" + + for part in [*sby_dir.parts, ""]: + print(f".PHONY: {prefix}clean {prefix}test", file=rules) + print(f"{prefix}clean: clean-{target}", file=rules) + print(f"{prefix}test: {target}", file=rules) + + for test_group in test_groups: + print(f".PHONY: {prefix}{test_group}", file=rules) + print(f"{prefix}{test_group}: {target}", file=rules) + prefix += f"{part}/" + + tasks = [task for task in taskinfo.keys() if task] + + if tasks: + print(f".PHONY: {name}", file=rules) + print(f"{name}:", *(f"{name}_{task}" for task in tasks), file=rules) diff --git a/tests/mixed.v b/tests/mixed.v deleted file mode 100644 index fa3cf2c4..00000000 --- a/tests/mixed.v +++ /dev/null @@ -1,17 +0,0 @@ -module test (input CP, CN, CX, input A, B, output reg XP, XN, YP, YN); - always @* begin - assume (A || B); - assume (!A || !B); - assert (A != B); - cover (A); - cover (B); - end - always @(posedge CP) - XP <= A; - always @(negedge CN) - XN <= B; - always @(posedge CX) - YP <= A; - always @(negedge CX) - YN <= B; -endmodule diff --git a/tests/parser/.gitignore b/tests/parser/.gitignore new file mode 100644 index 00000000..b87b1902 --- /dev/null +++ b/tests/parser/.gitignore @@ -0,0 +1,4 @@ +* +!Makefile +!.gitignore +!*.sby diff --git a/tests/parser/Makefile b/tests/parser/Makefile new file mode 100644 index 00000000..7827c43e --- /dev/null +++ b/tests/parser/Makefile @@ -0,0 +1,2 @@ +SUBDIR=parser +include ../make/subdir.mk diff --git a/tests/regression/Makefile b/tests/regression/Makefile new file mode 100644 index 00000000..0d9b6848 --- /dev/null +++ b/tests/regression/Makefile @@ -0,0 +1,2 @@ +SUBDIR=regression +include ../make/subdir.mk diff --git a/tests/regression/aim_vs_smt2_nonzero_start_offset.sby b/tests/regression/aim_vs_smt2_nonzero_start_offset.sby new file mode 100644 index 00000000..94591d74 --- /dev/null +++ b/tests/regression/aim_vs_smt2_nonzero_start_offset.sby @@ -0,0 +1,36 @@ +[tasks] +abc_bmc3 bmc +abc_sim3 bmc +aiger_avy prove +aiger_suprove prove +abc_pdr prove + +[options] +bmc: mode bmc +prove: mode prove +expect fail +wait on + +[engines] +abc_bmc3: abc bmc3 +abc_sim3: abc sim3 +aiger_avy: aiger avy +aiger_suprove: aiger suprove +abc_pdr: abc pdr + +[script] +read -sv test.sv +prep -top test + +[file test.sv] +module test ( + input clk, + input [8:1] nonzero_offset +); + reg [7:0] counter = 0; + + always @(posedge clk) begin + if (counter == 3) assert(nonzero_offset[1]); + counter <= counter + 1; + end +endmodule diff --git a/tests/regression/const_clocks.sby b/tests/regression/const_clocks.sby new file mode 100644 index 00000000..245358bf --- /dev/null +++ b/tests/regression/const_clocks.sby @@ -0,0 +1,43 @@ +[tasks] +btor +smt +btor_m btor multiclock +smt_m smt multiclock + +[options] +mode bmc + +multiclock: multiclock on + +[engines] +#smtbmc +btor: btor btormc +smt: smtbmc boolector + +[script] +read_verilog -formal const_clocks.sv +prep -flatten -top top + +[file const_clocks.sv] +module top( + input clk, + input [7:0] d +); + + (* keep *) + wire [7:0] some_const = $anyconst; + + wire [7:0] q; + + ff ff1(.clk(1'b0), .d(d), .q(q)); + + initial assume (some_const == q); + initial assume (q != 0); + + + always @(posedge clk) assert(some_const == q); +endmodule + +module ff(input clk, input [7:0] d, (* keep *) output reg [7:0] q); + always @(posedge clk) q <= d; +endmodule diff --git a/tests/regression/fake_loop.sby b/tests/regression/fake_loop.sby new file mode 100644 index 00000000..419e2674 --- /dev/null +++ b/tests/regression/fake_loop.sby @@ -0,0 +1,23 @@ +[options] +mode cover + +[engines] +smtbmc boolector + +[script] +read -formal fake_loop.sv +hierarchy -top fake_loop +proc + +[file fake_loop.sv] +module fake_loop(input clk, input a, input b, output [9:0] x); + wire [9:0] ripple; + reg [9:0] prev_ripple = 9'b0; + + always @(posedge clk) prev_ripple <= ripple; + + assign ripple = {ripple[8:0], a} ^ prev_ripple; // only cyclic at the coarse-grain level + assign x = ripple[9] + b; + + always @(posedge clk) cover(ripple[9]); +endmodule diff --git a/tests/regression/ff_xinit_opt.sby b/tests/regression/ff_xinit_opt.sby new file mode 100644 index 00000000..2078ad1b --- /dev/null +++ b/tests/regression/ff_xinit_opt.sby @@ -0,0 +1,39 @@ +[options] +mode bmc + +[engines] +smtbmc boolector + +[script] +read_verilog -formal ff_xinit_opt.sv +prep -flatten -top top + +opt -fast -keepdc + +[file ff_xinit_opt.sv] +module top( + input clk, + input [7:0] d +); + + (* keep *) + wire [7:0] some_const = $anyconst; + + wire [7:0] q1; + wire [7:0] q2; + + ff ff1(.clk(clk), .d(q1), .q(q1)); + ff ff2(.clk(1'b0), .d(d), .q(q2)); + + initial assume (some_const == q1); + initial assume (some_const == q2); + initial assume (q1 != 0); + initial assume (q2 != 0); + + always @(posedge clk) assert(some_const == q1); + always @(posedge clk) assert(some_const == q2); +endmodule + +module ff(input clk, input [7:0] d, (* keep *) output reg [7:0] q); + always @(posedge clk) q <= d; +endmodule diff --git a/tests/regression/invalid_ff_dcinit_merge.sby b/tests/regression/invalid_ff_dcinit_merge.sby new file mode 100644 index 00000000..a23d8f02 --- /dev/null +++ b/tests/regression/invalid_ff_dcinit_merge.sby @@ -0,0 +1,29 @@ +[options] +mode bmc +depth 4 +expect fail + +[engines] +smtbmc + +[script] +read -formal top.sv +prep -top top + +[file top.sv] +module top( +input clk, d +); + +reg q1; +reg q2; + +always @(posedge clk) begin + q1 <= d; + q2 <= d; +end; + +// q1 and q2 are unconstrained in the initial state, so this should fail +always @(*) assert(q1 == q2); + +endmodule diff --git a/tests/regression/option_skip.sby b/tests/regression/option_skip.sby new file mode 100644 index 00000000..75a2bd54 --- /dev/null +++ b/tests/regression/option_skip.sby @@ -0,0 +1,33 @@ +[tasks] +smtbmc_pass: smtbmc pass +smtbmc_fail: smtbmc fail +btormc_pass: btormc pass +btormc_fail: btormc fail + +[options] +mode bmc +pass: expect pass +fail: expect fail +pass: depth 5 +fail: depth 6 + +skip 2 + +[engines] +smtbmc: smtbmc boolector +[engines bmc] +btormc: btor btormc + +[script] +read -formal top.sv +prep -top top + +[file top.sv] +module top(input clk); + reg [7:0] counter = 0; + + always @(posedge clk) begin + counter <= counter + 1; + assert (counter < 4); + end +endmodule diff --git a/tests/regression/smt_dynamic_index_assign.sby b/tests/regression/smt_dynamic_index_assign.sby new file mode 100644 index 00000000..993d75a5 --- /dev/null +++ b/tests/regression/smt_dynamic_index_assign.sby @@ -0,0 +1,22 @@ +[options] +mode cover +depth 36 + +[engines] +smtbmc boolector + +[script] +read -formal top.sv +prep -top top + +[file top.sv] +module top(input clk); + reg [33:0] bits = 0; + reg [5:0] counter = 0; + + always @(posedge clk) begin + counter <= counter + 1; + bits[counter] <= 1; + cover (&bits); + end +endmodule diff --git a/tests/regression/unroll_noincr_traces.sby b/tests/regression/unroll_noincr_traces.sby new file mode 100644 index 00000000..e93d18fc --- /dev/null +++ b/tests/regression/unroll_noincr_traces.sby @@ -0,0 +1,29 @@ +[tasks] +boolector +yices +z3 + +[options] +mode bmc +expect fail + +[engines] +boolector: smtbmc boolector -- --noincr +yices: smtbmc --unroll yices -- --noincr +z3: smtbmc --unroll z3 -- --noincr + + +[script] +read -formal top.sv +prep -top top + +[file top.sv] +module top(input clk); + reg [7:0] counter = 0; + wire derived = counter * 7; + + always @(posedge clk) begin + counter <= counter + 1; + assert (counter < 4); + end +endmodule diff --git a/tests/unsorted/2props1trace.sby b/tests/unsorted/2props1trace.sby new file mode 100644 index 00000000..8f51fde6 --- /dev/null +++ b/tests/unsorted/2props1trace.sby @@ -0,0 +1,22 @@ +[options] +mode bmc +depth 1 +expect fail + +[engines] +smtbmc + +[script] +read -sv top.sv +prep -top top + +[file top.sv] +module top( +input foo, +input bar +); +always @(*) begin + assert (foo); + assert (bar); +end +endmodule diff --git a/tests/unsorted/Makefile b/tests/unsorted/Makefile new file mode 100644 index 00000000..61c3a6fd --- /dev/null +++ b/tests/unsorted/Makefile @@ -0,0 +1,2 @@ +SUBDIR=unsorted +include ../make/subdir.mk diff --git a/tests/unsorted/allconst.sby b/tests/unsorted/allconst.sby new file mode 100644 index 00000000..0d43f12a --- /dev/null +++ b/tests/unsorted/allconst.sby @@ -0,0 +1,30 @@ +[tasks] +yices +z3 + +[options] +mode cover +depth 1 + +[engines] +yices: smtbmc --stbv yices +z3: smtbmc --stdt z3 + +[script] +read -noverific +read -formal primegen.sv +prep -top primegen + +[file primegen.sv] + +module primegen; + (* anyconst *) reg [9:0] prime; + (* allconst *) reg [4:0] factor; + + always @* begin + if (1 < factor && factor < prime) + assume ((prime % factor) != 0); + assume (prime > 800); + cover (1); + end +endmodule diff --git a/tests/unsorted/blackbox.sby b/tests/unsorted/blackbox.sby new file mode 100644 index 00000000..ca9400e2 --- /dev/null +++ b/tests/unsorted/blackbox.sby @@ -0,0 +1,31 @@ +[options] +mode bmc +depth 1 +expect error + +[engines] +smtbmc + +[script] +read_verilog -formal test.v +prep -top top + +[file test.v] +(* blackbox *) +module submod(a, b); + input [7:0] a; + output [7:0] b; +endmodule + +module top; + wire [7:0] a = $anyconst, b; + + submod submod( + .a(a), + .b(b) + ); + + always @* begin + assert(~a == b); + end +endmodule diff --git a/tests/unsorted/bmc_len.sby b/tests/unsorted/bmc_len.sby new file mode 100644 index 00000000..938a1bdc --- /dev/null +++ b/tests/unsorted/bmc_len.sby @@ -0,0 +1,36 @@ +[tasks] +smtbmc_pass: smtbmc pass +smtbmc_fail: smtbmc fail +aigbmc_pass: aigbmc pass +aigbmc_fail: aigbmc fail +btormc_pass: btormc pass +btormc_fail: btormc fail +abc_pass: abc pass +abc_fail: abc fail + +[options] +mode bmc +pass: expect pass +fail: expect fail +pass: depth 5 +fail: depth 6 + +[engines] +smtbmc: smtbmc boolector +aigbmc: aiger aigbmc +btormc: btor btormc +abc: abc bmc3 + +[script] +read -formal top.sv +prep -top top + +[file top.sv] +module top(input clk); + reg [7:0] counter = 0; + + always @(posedge clk) begin + counter <= counter + 1; + assert (counter < 4); + end +endmodule diff --git a/tests/both_ex.sby b/tests/unsorted/both_ex.sby similarity index 88% rename from tests/both_ex.sby rename to tests/unsorted/both_ex.sby index f83f2b17..81773747 100644 --- a/tests/both_ex.sby +++ b/tests/unsorted/both_ex.sby @@ -15,7 +15,7 @@ pono: btor pono cover: btor btormc [script] -read_verilog -sv both_ex.v +read -sv both_ex.v prep -top test [files] diff --git a/tests/both_ex.v b/tests/unsorted/both_ex.v similarity index 100% rename from tests/both_ex.v rename to tests/unsorted/both_ex.v diff --git a/tests/unsorted/btor_meminit.sby b/tests/unsorted/btor_meminit.sby new file mode 100644 index 00000000..ca584a5f --- /dev/null +++ b/tests/unsorted/btor_meminit.sby @@ -0,0 +1,48 @@ +[tasks] +btormc +#pono +smtbmc + +[options] +mode bmc +expect fail + +[engines] +btormc: btor btormc +# pono: btor pono +smtbmc: smtbmc + +[script] +read -formal top.sv +prep -top top -flatten + +[file top.sv] + +module top(input clk); + + inner inner(clk); + +endmodule + +module inner(input clk); + reg [7:0] counter = 0; + + reg [1:0] mem [0:255]; + + initial begin + mem[0] = 0; + mem[1] = 1; + mem[2] = 2; + mem[3] = 2; + mem[4] = 0; + mem[7] = 0; + end + + always @(posedge clk) begin + counter <= counter + 1; + foo: assert (mem[counter] < 3); + bar: assume (counter < 7); + + mem[counter] <= 0; + end +endmodule diff --git a/tests/cover.sby b/tests/unsorted/cover.sby similarity index 100% rename from tests/cover.sby rename to tests/unsorted/cover.sby diff --git a/tests/cover.sv b/tests/unsorted/cover.sv similarity index 100% rename from tests/cover.sv rename to tests/unsorted/cover.sv diff --git a/tests/unsorted/cover_fail.sby b/tests/unsorted/cover_fail.sby new file mode 100644 index 00000000..391e0a83 --- /dev/null +++ b/tests/unsorted/cover_fail.sby @@ -0,0 +1,31 @@ +[options] +mode cover +depth 5 +expect pass,fail + +[engines] +smtbmc boolector + +[script] +read -sv test.v +prep -top test + +[file test.v] +module test( +input clk, +input rst, +output reg [3:0] count +); + +initial assume (rst == 1'b1); + +always @(posedge clk) begin +if (rst) + count <= 4'b0; +else + count <= count + 1'b1; + +cover (count == 0 && !rst); +cover (count == 4'd11 && !rst); +end +endmodule diff --git a/tests/unsorted/cover_unreachable.sby b/tests/unsorted/cover_unreachable.sby new file mode 100644 index 00000000..63ebcc75 --- /dev/null +++ b/tests/unsorted/cover_unreachable.sby @@ -0,0 +1,34 @@ +[tasks] +btormc +smtbmc + +[options] +mode cover +expect fail + +[engines] +btormc: btor btormc +smtbmc: smtbmc + +[script] +read -formal top.sv +prep -top top -flatten + +[file top.sv] + +module top(input clk); + + inner inner(clk); + +endmodule + +module inner(input clk); + reg [7:0] counter = 0; + + always @(posedge clk) begin + counter <= counter == 4 ? 0 : counter + 1; + + reachable: cover (counter == 3); + unreachable: cover (counter == 5); + end +endmodule diff --git a/tests/demo.sby b/tests/unsorted/demo.sby similarity index 78% rename from tests/demo.sby rename to tests/unsorted/demo.sby index bc40cd68..c6965714 100644 --- a/tests/demo.sby +++ b/tests/unsorted/demo.sby @@ -1,6 +1,8 @@ [tasks] btormc pono +cvc4 +cvc5 [options] mode bmc @@ -10,6 +12,8 @@ expect fail [engines] btormc: btor btormc pono: btor pono +cvc4: smtbmc cvc4 +cvc5: smtbmc cvc5 [script] read -formal demo.sv diff --git a/tests/demo.sv b/tests/unsorted/demo.sv similarity index 100% rename from tests/demo.sv rename to tests/unsorted/demo.sv diff --git a/tests/unsorted/floor_divmod.sby b/tests/unsorted/floor_divmod.sby new file mode 100644 index 00000000..df35f8a2 --- /dev/null +++ b/tests/unsorted/floor_divmod.sby @@ -0,0 +1,45 @@ +[options] +mode bmc +depth 1 + +[engines] +smtbmc + +[script] +read_verilog -icells -formal test.v +prep -top top + +[file test.v] +module top; + wire [7:0] a = $anyconst, b = $anyconst, fdiv, fmod, a2; + assign a2 = b * fdiv + fmod; + + \$divfloor #( + .A_WIDTH(8), + .B_WIDTH(8), + .A_SIGNED(1), + .B_SIGNED(1), + .Y_WIDTH(8), + ) fdiv_m ( + .A(a), + .B(b), + .Y(fdiv) + ); + + \$modfloor #( + .A_WIDTH(8), + .B_WIDTH(8), + .A_SIGNED(1), + .B_SIGNED(1), + .Y_WIDTH(8), + ) fmod_m ( + .A(a), + .B(b), + .Y(fmod) + ); + + always @* begin + assume(b != 0); + assert(a == a2); + end +endmodule diff --git a/tests/memory.sby b/tests/unsorted/memory.sby similarity index 100% rename from tests/memory.sby rename to tests/unsorted/memory.sby diff --git a/tests/memory.sv b/tests/unsorted/memory.sv similarity index 100% rename from tests/memory.sv rename to tests/unsorted/memory.sv diff --git a/tests/mixed.sby b/tests/unsorted/mixed.sby similarity index 100% rename from tests/mixed.sby rename to tests/unsorted/mixed.sby diff --git a/tests/unsorted/mixed.v b/tests/unsorted/mixed.v new file mode 100644 index 00000000..26bf3c9e --- /dev/null +++ b/tests/unsorted/mixed.v @@ -0,0 +1,16 @@ +module test (input CP, CN, input A, B, output reg XP, XN); + reg [7:0] counter = 0; + always @* begin + assume (A || B); + assume (!A || !B); + assert (A != B); + cover (counter == 3 && A); + cover (counter == 3 && B); + end + always @(posedge CP) + counter <= counter + 1; + always @(posedge CP) + XP <= A; + always @(negedge CN) + XN <= B; +endmodule diff --git a/tests/unsorted/multi_assert.sby b/tests/unsorted/multi_assert.sby new file mode 100644 index 00000000..883181a8 --- /dev/null +++ b/tests/unsorted/multi_assert.sby @@ -0,0 +1,24 @@ +[tasks] +btormc +pono + +[options] +mode bmc +depth 5 +expect fail + +[engines] +btormc: btor btormc +pono: btor pono + +[script] +read -sv multi_assert.v +prep -top test + +[file multi_assert.v] +module test(); +always @* begin +assert (1); +assert (0); +end +endmodule diff --git a/tests/unsorted/no_vcd.sby b/tests/unsorted/no_vcd.sby new file mode 100644 index 00000000..ea58b12d --- /dev/null +++ b/tests/unsorted/no_vcd.sby @@ -0,0 +1,37 @@ +[tasks] +smtbmc mode_bmc +btor_bmc engine_btor mode_bmc +btor_cover engine_btor mode_cover +abc mode_bmc +aiger engine_aiger mode_prove + +[options] +mode_bmc: mode bmc +mode_prove: mode prove +mode_cover: mode cover +vcd off +~mode_cover: expect fail + +[engines] +smtbmc: smtbmc +engine_btor: btor btormc +abc: abc bmc3 +aiger: aiger suprove + +[script] +read_verilog -formal no_vcd.sv +prep -top top + +[file no_vcd.sv] +module top(input clk, input force); + +reg [4:0] counter = 0; + +always @(posedge clk) begin + if (!counter[4] || force) + counter <= counter + 1; + assert (counter < 10); + cover (counter == 4); +end + +endmodule diff --git a/tests/preunsat.sby b/tests/unsorted/preunsat.sby similarity index 90% rename from tests/preunsat.sby rename to tests/unsorted/preunsat.sby index 6694a6c3..98255c61 100644 --- a/tests/preunsat.sby +++ b/tests/unsorted/preunsat.sby @@ -12,7 +12,7 @@ btormc: btor btormc yices: smtbmc yices [script] -read_verilog -sv test.sv +read -sv test.sv prep -top test [file test.sv] diff --git a/tests/prv32fmcmp.sby b/tests/unsorted/prv32fmcmp.sby similarity index 89% rename from tests/prv32fmcmp.sby rename to tests/unsorted/prv32fmcmp.sby index 2412eb86..bd4e0964 100644 --- a/tests/prv32fmcmp.sby +++ b/tests/unsorted/prv32fmcmp.sby @@ -17,5 +17,5 @@ read -sv prv32fmcmp.v prep -top prv32fmcmp [files] -../extern/picorv32.v +../../extern/picorv32.v prv32fmcmp.v diff --git a/tests/prv32fmcmp.v b/tests/unsorted/prv32fmcmp.v similarity index 100% rename from tests/prv32fmcmp.v rename to tests/unsorted/prv32fmcmp.v diff --git a/tests/redxor.sby b/tests/unsorted/redxor.sby similarity index 76% rename from tests/redxor.sby rename to tests/unsorted/redxor.sby index 6e6e9f8b..0746861e 100644 --- a/tests/redxor.sby +++ b/tests/unsorted/redxor.sby @@ -6,7 +6,7 @@ expect pass btor btormc [script] -read_verilog -formal redxor.v +read -formal redxor.v prep -top test [files] diff --git a/tests/redxor.v b/tests/unsorted/redxor.v similarity index 100% rename from tests/redxor.v rename to tests/unsorted/redxor.v diff --git a/tests/unsorted/smtlib2_module.sby b/tests/unsorted/smtlib2_module.sby new file mode 100644 index 00000000..43dfcb28 --- /dev/null +++ b/tests/unsorted/smtlib2_module.sby @@ -0,0 +1,32 @@ +[options] +mode bmc +depth 1 + +[engines] +smtbmc + +[script] +read_verilog -formal test.v +prep -top top + +[file test.v] +(* blackbox *) +(* smtlib2_module *) +module submod(a, b); + input [7:0] a; + (* smtlib2_comb_expr = "(bvnot a)" *) + output [7:0] b; +endmodule + +module top; + wire [7:0] a = $anyconst, b; + + submod submod( + .a(a), + .b(b) + ); + + always @* begin + assert(~a == b); + end +endmodule diff --git a/tests/stopfirst.sby b/tests/unsorted/stopfirst.sby similarity index 87% rename from tests/stopfirst.sby rename to tests/unsorted/stopfirst.sby index 35ed539c..782f7919 100644 --- a/tests/stopfirst.sby +++ b/tests/unsorted/stopfirst.sby @@ -6,7 +6,7 @@ expect fail btor btormc [script] -read_verilog -sv test.sv +read -sv test.sv prep -top test [file test.sv] diff --git a/tests/unsorted/submod_props.sby b/tests/unsorted/submod_props.sby new file mode 100644 index 00000000..99336767 --- /dev/null +++ b/tests/unsorted/submod_props.sby @@ -0,0 +1,33 @@ +[tasks] +bmc +cover +flatten + +[options] +bmc: mode bmc +cover: mode cover +flatten: mode bmc + +expect fail + +[engines] +smtbmc boolector + +[script] +read -sv test.sv +prep -top top +flatten: flatten + +[file test.sv] +module test(input foo); +always @* assert(foo); +always @* assert(!foo); +always @* cover(foo); +always @* cover(!foo); +endmodule + +module top(); +test test_i ( +.foo(1'b1) +); +endmodule diff --git a/tools/README.md b/tools/README.md new file mode 100644 index 00000000..1036f1c9 --- /dev/null +++ b/tools/README.md @@ -0,0 +1,6 @@ +# SBY - Additional Tools + +This directory contains various tools that can be used in conjunction with SBY. + +* [`aigcexmin`](./aigcexmin) Counter-example minimization of AIGER witness (.aiw) files +* [`cexenum`](./cexenum) Enumeration of minimized counter-examples diff --git a/tools/aigcexmin/.gitignore b/tools/aigcexmin/.gitignore new file mode 100644 index 00000000..ea8c4bf7 --- /dev/null +++ b/tools/aigcexmin/.gitignore @@ -0,0 +1 @@ +/target diff --git a/tools/aigcexmin/Cargo.lock b/tools/aigcexmin/Cargo.lock new file mode 100644 index 00000000..828358bb --- /dev/null +++ b/tools/aigcexmin/Cargo.lock @@ -0,0 +1,562 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "aigcexmin" +version = "0.1.0" +dependencies = [ + "clap", + "color-eyre", + "flussab", + "flussab-aiger", + "zwohash", +] + +[[package]] +name = "anstream" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab91ebe16eb252986481c5b62f6098f3b698a45e34b5b98200cf20dd2484a44" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" + +[[package]] +name = "anstyle-parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "317b9a89c1868f5ea6ff1d9539a69f45dffc21ce321ac1fd1160dfa48c8e2140" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0699d10d2f4d628a98ee7b57b289abbc98ff3bad977cb3152709d4bf2330628" +dependencies = [ + "anstyle", + "windows-sys", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "backtrace" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "bitflags" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" + +[[package]] +name = "cc" +version = "1.0.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +dependencies = [ + "libc", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "clap" +version = "4.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2275f18819641850fa26c89acc84d465c1bf91ce57bc2748b28c420473352f64" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07cdf1b148b25c1e1f7a42225e30a0d99a615cd4637eae7365548dd4529b95bc" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", + "terminal_size", +] + +[[package]] +name = "clap_derive" +version = "4.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf9804afaaf59a91e75b022a30fb7229a7901f60c755489cc61c9b423b836442" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1" + +[[package]] +name = "color-eyre" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a667583cca8c4f8436db8de46ea8233c42a7d9ae424a82d338f2e4675229204" +dependencies = [ + "backtrace", + "color-spantrace", + "eyre", + "indenter", + "once_cell", + "owo-colors", + "tracing-error", +] + +[[package]] +name = "color-spantrace" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ba75b3d9449ecdccb27ecbc479fdc0b87fa2dd43d2f8298f9bf0e59aacc8dce" +dependencies = [ + "once_cell", + "owo-colors", + "tracing-core", + "tracing-error", +] + +[[package]] +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" + +[[package]] +name = "errno" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c18ee0ed65a5f1f81cac6b1d213b69c35fa47d4252ad41f1486dbd8226fe36e" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "eyre" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c2b6b5a29c02cdc822728b7d7b8ae1bab3e3b05d44522770ddd49722eeac7eb" +dependencies = [ + "indenter", + "once_cell", +] + +[[package]] +name = "flussab" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcd46d8f41aa1e4d79ba21282dd39a9c539d610ab336fc56a48dccdd7c82b12f" +dependencies = [ + "itoap", + "num-traits", +] + +[[package]] +name = "flussab-aiger" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "378b3a9970d0163162e8b3c9a4d9b2eef98be95d624cbac5b207278b157886d2" +dependencies = [ + "flussab", + "num-traits", + "thiserror", + "zwohash", +] + +[[package]] +name = "gimli" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "indenter" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" + +[[package]] +name = "itoap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9028f49264629065d057f340a86acb84867925865f73bbf8d47b4d149a7e88b8" + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" + +[[package]] +name = "linux-raw-sys" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "969488b55f8ac402214f3f5fd243ebb7206cf82de60d3172994707a4bcc2b829" + +[[package]] +name = "memchr" +version = "2.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" + +[[package]] +name = "miniz_oxide" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +dependencies = [ + "adler", +] + +[[package]] +name = "num-traits" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" +dependencies = [ + "autocfg", +] + +[[package]] +name = "object" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" + +[[package]] +name = "owo-colors" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" + +[[package]] +name = "pin-project-lite" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" + +[[package]] +name = "proc-macro2" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" + +[[package]] +name = "rustix" +version = "0.38.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ad981d6c340a49cdc40a1028d9c6084ec7e9fa33fcb839cab656a267071e234" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "syn" +version = "2.0.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "terminal_size" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21bebf2b7c9e0a515f6e0f8c51dc0f8e4696391e6f1ff30379559f8365fb0df7" +dependencies = [ + "rustix", + "windows-sys", +] + +[[package]] +name = "thiserror" +version = "1.0.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9a7210f5c9a7156bb50aa36aed4c95afb51df0df00713949448cf9e97d382d2" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" +dependencies = [ + "cfg-if", + "once_cell", +] + +[[package]] +name = "tracing" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-error" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d686ec1c0f384b1277f097b2f279a2ecc11afe8c133c1aabf036a27cb4cd206e" +dependencies = [ + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" +dependencies = [ + "sharded-slab", + "thread_local", + "tracing-core", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "zwohash" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "beaf63e0740cea93ca85de39611a8bc8262a50adacd6321cd209a123676d0447" diff --git a/tools/aigcexmin/Cargo.toml b/tools/aigcexmin/Cargo.toml new file mode 100644 index 00000000..7a08f347 --- /dev/null +++ b/tools/aigcexmin/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "aigcexmin" +version = "0.1.0" +edition = "2021" +authors = ["Jannis Harder "] + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[profile.release] +debug = true # profiling + +[dependencies] +clap = { version = "4.4.8", features = ["derive", "cargo", "wrap_help"] } +color-eyre = "0.6.2" +flussab = "0.3.1" +flussab-aiger = "0.1.0" +zwohash = "0.1.2" diff --git a/tools/aigcexmin/src/aig_eval.rs b/tools/aigcexmin/src/aig_eval.rs new file mode 100644 index 00000000..7e5543c6 --- /dev/null +++ b/tools/aigcexmin/src/aig_eval.rs @@ -0,0 +1,104 @@ +use flussab_aiger::{aig::OrderedAig, Lit}; + +use crate::util::unpack_lit; + +pub trait AigValue: Copy { + fn invert_if(self, en: bool, ctx: &mut Context) -> Self; + fn and(self, other: Self, ctx: &mut Context) -> Self; + fn constant(value: bool, ctx: &mut Context) -> Self; +} + +pub fn initial_frame( + aig: &OrderedAig, + state: &mut Vec, + mut latch_init: impl FnMut(usize, &mut Context) -> V, + mut input: impl FnMut(usize, &mut Context) -> V, + ctx: &mut Context, +) where + L: Lit, + V: AigValue, +{ + state.clear(); + state.push(V::constant(false, ctx)); + + for i in 0..aig.input_count { + state.push(input(i, ctx)); + } + + for i in 0..aig.latches.len() { + state.push(latch_init(i, ctx)); + } + + for and_gate in aig.and_gates.iter() { + let [a, b] = and_gate.inputs.map(|lit| { + let (var, polarity) = unpack_lit(lit); + state[var].invert_if(polarity, ctx) + }); + + state.push(a.and(b, ctx)); + } +} + +pub fn successor_frame( + aig: &OrderedAig, + state: &mut Vec, + mut input: impl FnMut(usize, &mut Context) -> V, + ctx: &mut Context, +) where + L: Lit, + V: AigValue, +{ + assert_eq!(state.len(), 1 + aig.max_var_index); + + for i in 0..aig.input_count { + state.push(input(i, ctx)); + } + + for latch in aig.latches.iter() { + let (var, polarity) = unpack_lit(latch.next_state); + state.push(state[var].invert_if(polarity, ctx)); + } + + state.drain(1..1 + aig.max_var_index); + + for and_gate in aig.and_gates.iter() { + let [a, b] = and_gate.inputs.map(|lit| { + let (var, polarity) = unpack_lit(lit); + state[var].invert_if(polarity, ctx) + }); + + state.push(a.and(b, ctx)); + } +} + +impl AigValue<()> for bool { + fn invert_if(self, en: bool, _ctx: &mut ()) -> Self { + self ^ en + } + + fn and(self, other: Self, _ctx: &mut ()) -> Self { + self & other + } + + fn constant(value: bool, _ctx: &mut ()) -> Self { + value + } +} + +impl AigValue<()> for Option { + fn invert_if(self, en: bool, _ctx: &mut ()) -> Self { + self.map(|b| b ^ en) + } + + fn and(self, other: Self, _ctx: &mut ()) -> Self { + match (self, other) { + (Some(true), Some(true)) => Some(true), + (Some(false), _) | (_, Some(false)) => Some(false), + _ => None, + } + } + + fn constant(value: bool, _ctx: &mut ()) -> Self { + Some(value) + } +} diff --git a/tools/aigcexmin/src/care_graph.rs b/tools/aigcexmin/src/care_graph.rs new file mode 100644 index 00000000..0f608fe2 --- /dev/null +++ b/tools/aigcexmin/src/care_graph.rs @@ -0,0 +1,730 @@ +use std::{ + cmp::Reverse, + collections::{BTreeSet, BinaryHeap}, + mem::{replace, take}, + num::NonZeroU32, +}; + +use color_eyre::eyre::bail; +use flussab::DeferredWriter; +use flussab_aiger::{aig::OrderedAig, Lit}; +use zwohash::HashMap; + +use crate::{ + aig_eval::{initial_frame, successor_frame, AigValue}, + util::{unpack_lit, write_output_bit}, +}; + +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[repr(transparent)] +pub struct NodeRef { + code: Reverse, +} + +impl std::fmt::Debug for NodeRef { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_tuple("NodeRef::new").field(&self.index()).finish() + } +} + +impl NodeRef { + const INVALID_INDEX: usize = u32::MAX as usize; + const TRUE_INDEX: usize = Self::INVALID_INDEX - 1; + const FALSE_INDEX: usize = Self::INVALID_INDEX - 2; + + pub const TRUE: Self = Self::new(Self::TRUE_INDEX); + pub const FALSE: Self = Self::new(Self::FALSE_INDEX); + + pub const fn new(index: usize) -> Self { + assert!(index < u32::MAX as usize); + let Some(code) = NonZeroU32::new(!(index as u32)) else { + unreachable!(); + }; + Self { + code: Reverse(code), + } + } + + pub fn index(self) -> usize { + !(self.code.0.get()) as usize + } +} + +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] +enum Gate { + And, + Or, +} + +#[derive(Debug)] +enum NodeDef { + Gate([NodeRef; 2]), + Input(u32), +} + +impl NodeDef { + fn and(inputs: [NodeRef; 2]) -> Self { + assert!(inputs[0] < inputs[1]); + Self::Gate(inputs) + } + + fn or(inputs: [NodeRef; 2]) -> Self { + assert!(inputs[0] < inputs[1]); + Self::Gate([inputs[1], inputs[0]]) + } + + fn input(id: u32) -> Self { + Self::Input(id) + } + + fn as_gate(&self) -> Result<(Gate, [NodeRef; 2]), u32> { + match *self { + NodeDef::Gate(inputs) => { + if inputs[0] < inputs[1] { + Ok((Gate::And, inputs)) + } else { + Ok((Gate::Or, [inputs[1], inputs[0]])) + } + } + NodeDef::Input(input) => Err(input), + } + } +} + +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default, Debug)] +enum NodeState { + #[default] + Unknown, + Nonselected, + Selected, + Required, +} + +#[derive(Debug)] +struct Node { + def: NodeDef, + priority: u32, + state: NodeState, + renamed: Option, +} + +impl Node { + fn update_state(&mut self, state: NodeState) -> NodeState { + let old_state = self.state; + self.state = self.state.max(state); + old_state + } +} + +#[derive(Default)] +pub struct AndOrGraph { + find_input: HashMap, + find_and: HashMap<[NodeRef; 2], NodeRef>, + find_or: HashMap<[NodeRef; 2], NodeRef>, + + // find_renamed: HashMap, + nodes: Vec, + queue: BinaryHeap, + stack: Vec, + + unknown_inputs: BTreeSet, + required_inputs: BTreeSet, + active_node_count: usize, + + input_order: Vec<(NodeRef, u32)>, + cache: bool, +} + +impl AndOrGraph { + pub fn input(&mut self, id: u32) -> NodeRef { + assert!(id <= u32::MAX - 2); + *self.find_input.entry(id).or_insert_with(|| { + let node_ref = NodeRef::new(self.nodes.len()); + let node = Node { + def: NodeDef::input(id), + priority: id, + state: NodeState::Unknown, + renamed: None, + }; + self.nodes.push(node); + self.unknown_inputs.insert(id); + node_ref + }) + } + + pub fn and(&mut self, mut inputs: [NodeRef; 2]) -> NodeRef { + inputs.sort_unstable(); + if inputs[1] == NodeRef::FALSE { + NodeRef::FALSE + } else if inputs[1] == NodeRef::TRUE || inputs[1] == inputs[0] { + inputs[0] + } else { + let [a, b] = inputs; + match inputs.map(|input| self.nodes[input.index()].def.as_gate()) { + [Ok((Gate::And, [a0, a1])), _] if b == a0 || b == a1 => { + return a; + } + [_, Ok((Gate::And, [b0, b1]))] if a == b0 || a == b1 => { + return b; + } + + [Ok((Gate::Or, [a0, a1])), _] if b == a0 || b == a1 => { + return b; + } + [_, Ok((Gate::Or, [b0, b1]))] if a == b0 || a == b1 => { + return a; + } + + _ => (), + } + + let mut mknode = || { + let node_ref = NodeRef::new(self.nodes.len()); + + let [a, b] = inputs.map(|input| self.nodes[input.index()].priority); + + let node = Node { + def: NodeDef::and(inputs), + priority: a.min(b), + state: NodeState::Unknown, + renamed: None, + }; + self.nodes.push(node); + node_ref + }; + + if self.cache { + *self.find_and.entry(inputs).or_insert_with(mknode) + } else { + mknode() + } + } + } + + pub fn or(&mut self, mut inputs: [NodeRef; 2]) -> NodeRef { + inputs.sort_unstable(); + + if inputs[1] == NodeRef::TRUE { + NodeRef::TRUE + } else if inputs[1] == NodeRef::FALSE || inputs[1] == inputs[0] { + inputs[0] + } else { + let [a, b] = inputs; + match inputs.map(|input| self.nodes[input.index()].def.as_gate()) { + [Ok((Gate::Or, [a0, a1])), _] if b == a0 || b == a1 => { + return a; + } + [_, Ok((Gate::Or, [b0, b1]))] if a == b0 || a == b1 => { + return b; + } + + [Ok((Gate::And, [a0, a1])), _] if b == a0 || b == a1 => { + return b; + } + [_, Ok((Gate::And, [b0, b1]))] if a == b0 || a == b1 => { + return a; + } + + _ => (), + } + + let mut mknode = || { + let node_ref = NodeRef::new(self.nodes.len()); + + let [a, b] = inputs.map(|input| self.nodes[input.index()].priority); + + let node = Node { + def: NodeDef::or(inputs), + priority: a.max(b), + state: NodeState::Unknown, + renamed: None, + }; + self.nodes.push(node); + node_ref + }; + + if self.cache { + *self.find_or.entry(inputs).or_insert_with(mknode) + } else { + mknode() + } + } + } + + pub fn pass(&mut self, target: NodeRef, shuffle: usize, mut enable_cache: bool) -> NodeRef { + if self.cache { + enable_cache = false; + } + + self.nodes[target.index()].state = NodeState::Required; + self.queue.push(target); + + let target_priority = self.nodes[target.index()].priority; + + 'queue: while let Some(current) = self.queue.pop() { + let node = &self.nodes[current.index()]; + let state = node.state; + + self.stack.push(current); + + match node.def.as_gate() { + Ok((Gate::And, inputs)) => { + if enable_cache { + self.find_and.insert(inputs, current); + } + for input in inputs { + let node = &mut self.nodes[input.index()]; + if node.update_state(state) == NodeState::Unknown { + self.queue.push(input); + } + } + } + Ok((Gate::Or, inputs)) => { + if enable_cache { + self.find_or.insert(inputs, current); + } + for input in inputs { + let node = &mut self.nodes[input.index()]; + if node.update_state(NodeState::Nonselected) == NodeState::Unknown { + self.queue.push(input); + } + } + + if state <= NodeState::Nonselected { + continue; + } + + let input_priorities = inputs.map(|input| self.nodes[input.index()].priority); + + for (i, input_priority) in input_priorities.into_iter().enumerate() { + if input_priority < target_priority { + // The other input will be false, so propagate the state + self.nodes[inputs[i ^ 1].index()].update_state(state); + continue; + } + } + + for input in inputs { + let input_state = self.nodes[input.index()].state; + if input_state >= NodeState::Selected { + // One input of the or is already marked, no need to mark the other + continue 'queue; + } + } + + // Mark the highest priority input + let input = inputs[(input_priorities[1] > input_priorities[0]) as usize]; + self.nodes[input.index()].update_state(NodeState::Selected); + } + Err(_input) => (), + } + } + + if enable_cache { + self.cache = true; + } + + let mut stack = take(&mut self.stack); + + self.active_node_count = stack.len(); + + self.unknown_inputs.clear(); + + for current in stack.drain(..).rev() { + let node = &mut self.nodes[current.index()]; + let state = replace(&mut node.state, NodeState::Unknown); + let priority = node.priority; + + match node.def.as_gate() { + Ok((gate, inputs)) => { + let new_inputs = inputs.map(|input| self.nodes[input.index()].renamed.unwrap()); + + let output = if new_inputs == inputs { + current + } else { + match gate { + Gate::And => self.and(new_inputs), + Gate::Or => self.or(new_inputs), + } + }; + + if shuffle > 0 && output != NodeRef::FALSE && output != NodeRef::TRUE { + if let Ok((gate, inputs)) = self.nodes[output.index()].def.as_gate() { + let [a, b] = inputs.map(|input| self.nodes[input.index()].priority); + + self.nodes[output.index()].priority = match gate { + Gate::And => a.min(b), + Gate::Or => a.max(b), + }; + } + } + + self.nodes[current.index()].renamed = Some(output); + } + Err(input) => match priority.cmp(&target_priority) { + std::cmp::Ordering::Less => { + self.nodes[current.index()].renamed = Some(NodeRef::FALSE); + } + std::cmp::Ordering::Equal => { + self.required_inputs.insert(input); + self.nodes[current.index()].renamed = Some(NodeRef::TRUE); + } + std::cmp::Ordering::Greater => match state { + NodeState::Required => { + self.required_inputs.insert(input); + self.nodes[current.index()].renamed = Some(NodeRef::TRUE); + } + NodeState::Selected => { + self.unknown_inputs.insert(input); + self.nodes[current.index()].renamed = Some(current); + + if shuffle > 0 { + let priority = &mut self.nodes[current.index()].priority; + let mask = !(u64::MAX << 32.min(shuffle - 1)) as u32; + + *priority ^= + !(*priority ^ priority.wrapping_mul(0x2c9277b5)) & mask; + } + } + NodeState::Nonselected => { + self.nodes[current.index()].renamed = Some(NodeRef::FALSE); + } + NodeState::Unknown => { + unreachable!(); + } + }, + }, + } + } + + self.input_order.clear(); + + let result = self.nodes[target.index()].renamed.unwrap(); + self.stack = stack; + + result + } +} + +impl AigValue for (Option, NodeRef) { + fn invert_if(self, en: bool, _: &mut AndOrGraph) -> Self { + let (value, care) = self; + (value.map(|b| b ^ en), care) + } + + fn and(self, other: Self, ctx: &mut AndOrGraph) -> Self { + let (value_a, care_a) = self; + let (value_b, care_b) = other; + + match (value_a, value_b) { + (Some(true), Some(true)) => (Some(true), ctx.and([care_a, care_b])), + (Some(false), Some(false)) => (Some(false), ctx.or([care_a, care_b])), + (Some(false), _) => (Some(false), care_a), + (_, Some(false)) => (Some(false), care_b), + _ => (None, NodeRef::FALSE), + } + } + + fn constant(value: bool, _: &mut AndOrGraph) -> Self { + (Some(value), NodeRef::TRUE) + } +} + +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum Verification { + Cex, + Full, +} + +pub struct MinimizationOptions { + pub fixed_init: bool, + pub verify: Option, +} + +pub fn minimize( + aig: &OrderedAig, + latch_init: &[Option], + frame_inputs: &[Vec>], + writer: &mut DeferredWriter, + options: &MinimizationOptions, +) -> color_eyre::Result<()> { + let Some(initial_inputs) = frame_inputs.first() else { + bail!("no inputs found"); + }; + + let mut state = vec![]; + + let mut graph = AndOrGraph::default(); + + let input_id = |frame: Option, index: usize| -> u32 { + (if let Some(frame) = frame { + latch_init.len() + frame * initial_inputs.len() + index + } else { + index + }) + .try_into() + .unwrap() + }; + + let decode_input_id = |id: u32| -> (Option, usize) { + let id = id as usize; + if id < latch_init.len() { + (None, id) + } else { + let id = id - latch_init.len(); + let frame = id / initial_inputs.len(); + let index = id % initial_inputs.len(); + (Some(frame), index) + } + }; + + initial_frame( + aig, + &mut state, + |i, ctx| { + ( + latch_init[i], + if latch_init[i].is_some() { + if options.fixed_init { + NodeRef::TRUE + } else { + ctx.input(input_id(None, i)) + } + } else { + NodeRef::FALSE + }, + ) + }, + |i, ctx| { + ( + initial_inputs[i], + if initial_inputs[i].is_some() { + ctx.input(input_id(Some(0), i)) + } else { + NodeRef::FALSE + }, + ) + }, + &mut graph, + ); + + let mut minimization_target = 'minimization_target: { + for (t, inputs) in frame_inputs.iter().enumerate() { + if t > 0 { + successor_frame( + aig, + &mut state, + |i, ctx| { + ( + inputs[i], + if inputs[i].is_some() { + ctx.input(input_id(Some(t), i)) + } else { + NodeRef::FALSE + }, + ) + }, + &mut graph, + ); + } + let mut good_state = (Some(true), NodeRef::TRUE); + + for (i, bad) in aig.bad_state_properties.iter().enumerate() { + let (var, polarity) = unpack_lit(*bad); + let inv_bad = state[var].invert_if(!polarity, &mut graph); + + if inv_bad.0 == Some(false) { + println!("bad state property {i} active in frame {t}"); + } + + good_state = good_state.and(inv_bad, &mut graph); + } + if good_state.0 == Some(false) { + println!("bad state found in frame {t}"); + + break 'minimization_target good_state.1; + } + + if t > 0 && t % 500 == 0 { + println!( + "traced frame {t}/{frames}: node count = {node_count}", + frames = frame_inputs.len(), + node_count = graph.nodes.len(), + ); + } + } + + bail!("no bad state found"); + }; + + let node_count_width = (graph.nodes.len().max(2) - 1).ilog10() as usize + 1; + let input_count_width = (graph.unknown_inputs.len().max(2) - 1).ilog10() as usize + 1; + + println!( + "input: node count = {node_count:w0$}, defined inputs = {defined_inputs:w1$}", + node_count = graph.nodes.len(), + defined_inputs = graph.unknown_inputs.len(), + w0 = node_count_width, + w1 = input_count_width, + ); + + let mut shuffle = 0; + + let mut iteration = 0; + + while minimization_target != NodeRef::TRUE { + let prev_unknown_inputs = graph.unknown_inputs.len(); + minimization_target = graph.pass(minimization_target, shuffle, iteration >= 1); + let unknown_inputs = graph.unknown_inputs.len(); + let required_inputs = graph.required_inputs.len(); + println!( + concat!( + "iter: node count = {node_count:w0$}, defined inputs = {defined_inputs:w1$}, ", + "required inputs = {required_inputs:w1$}, shuffle = {shuffle}" + ), + node_count = graph.active_node_count, + required_inputs = required_inputs, + defined_inputs = unknown_inputs + required_inputs, + shuffle = shuffle, + w0 = node_count_width, + w1 = input_count_width, + ); + + if unknown_inputs + (unknown_inputs / 4) < prev_unknown_inputs { + shuffle = 0; + } else { + shuffle += 1; + } + iteration += 1; + } + + println!("minimization complete"); + + for i in 0..aig.latches.len() { + let bit = if options.fixed_init || graph.required_inputs.contains(&input_id(None, i)) { + latch_init[i] + } else { + None + }; + + write_output_bit(writer, bit); + } + + writer.write_all_defer_err(b"\n"); + + for (t, inputs) in frame_inputs.iter().enumerate() { + for i in 0..aig.input_count { + let bit = if graph.required_inputs.contains(&input_id(Some(t), i)) { + inputs[i] + } else { + None + }; + + write_output_bit(writer, bit); + } + writer.write_all_defer_err(b"\n"); + } + + writer.write_all_defer_err(b"# DONE\n"); + writer.flush_defer_err(); + writer.check_io_error()?; + + let Some(verify) = options.verify else { + return Ok(()); + }; + + let mut check_state: Vec> = vec![]; + + let empty_set = BTreeSet::new(); + + let verify_from = match verify { + Verification::Cex => &empty_set, + Verification::Full => &graph.required_inputs, + }; + + for check in [None] + .into_iter() + .chain(verify_from.iter().copied().map(Some)) + { + check_state.clear(); + + initial_frame( + aig, + &mut check_state, + |i, _| { + let input = input_id(None, i); + if options.fixed_init + || (Some(input) != check && graph.required_inputs.contains(&input)) + { + latch_init[i] + } else { + None + } + }, + |i, _| { + let input = input_id(Some(0), i); + if Some(input) != check && graph.required_inputs.contains(&input) { + initial_inputs[i] + } else { + None + } + }, + &mut (), + ); + + let mut bad_state = false; + + 'frame: for (t, inputs) in frame_inputs.iter().enumerate() { + if t > 0 { + successor_frame( + aig, + &mut check_state, + |i, _| { + let input = input_id(Some(t), i); + if Some(input) != check && graph.required_inputs.contains(&input) { + inputs[i] + } else { + None + } + }, + &mut (), + ); + } + + for bad in aig.bad_state_properties.iter() { + let (var, polarity) = unpack_lit(*bad); + let bad_output = check_state[var].invert_if(polarity, &mut ()); + if bad_output == Some(true) { + bad_state = true; + break 'frame; + } + } + } + + if bad_state != check.is_none() { + if let Some(check) = check { + let (frame, input) = decode_input_id(check); + if let Some(frame) = frame { + bail!("minimality verification wrt. frame {frame} input {input} failed"); + } else { + bail!("minimality verification wrt. initial latch {input} failed"); + } + } else { + bail!("counter example verification failed"); + } + } + + if let Some(check) = check { + let (frame, input) = decode_input_id(check); + if let Some(frame) = frame { + println!("verified minimality wrt. frame {frame} input {input}"); + } else { + println!("verified minimality wrt. initial latch {input}"); + } + } else { + println!("verified counter example"); + } + } + + Ok(()) +} diff --git a/tools/aigcexmin/src/main.rs b/tools/aigcexmin/src/main.rs new file mode 100644 index 00000000..30874db9 --- /dev/null +++ b/tools/aigcexmin/src/main.rs @@ -0,0 +1,145 @@ +#![allow(clippy::needless_range_loop)] + +use std::{fs, mem::replace, path::PathBuf}; + +use clap::{Parser, ValueEnum}; +use color_eyre::eyre::bail; + +use flussab_aiger::binary; + +pub mod aig_eval; +pub mod care_graph; +pub mod util; + +/// AIG counter example minimization +#[derive(clap::Parser)] +#[command(author, version, about, long_about = None, help_template="\ +{before-help}{name} {version} +{author-with-newline}{about-with-newline} +{usage-heading} {usage} + +{all-args}{after-help} +")] +pub struct Options { + /// Input AIGER file + aig: PathBuf, + /// Input AIGER witness file + witness: PathBuf, + /// Output AIGER witness file + output: PathBuf, + + /// Verify the minimized counter example + #[clap(long, default_value = "cex")] + verify: VerificationOption, + + /// Minimize latch initialization values + /// + /// Without this option the latch initialization values of the witness file are assumed to be + /// fixed and will remain as-is in the minimized witness file. + /// + /// Note that some tools (including the current Yosys/SBY flow) do not use AIGER native latch + /// initialization but instead perform initialization using inputs in the first frame. + #[clap(long)] + latches: bool, +} + +#[derive(Copy, Clone, ValueEnum)] +enum VerificationOption { + /// Skip verification + Off, + /// Verify the counter example + Cex, + /// Verify the counter example and that it is minimal (expensive) + Full, +} + +fn main() -> color_eyre::Result<()> { + let options = Options::parse(); + + color_eyre::install()?; + + let file_input = fs::File::open(options.aig)?; + let file_witness = fs::File::open(options.witness)?; + let file_output = fs::File::create(options.output)?; + + let mut writer_output = flussab::DeferredWriter::from_write(file_output); + + let mut read_witness_owned = flussab::DeferredReader::from_read(file_witness); + let read_witness = &mut read_witness_owned; + + let aig_reader = binary::Parser::::from_read(file_input, binary::Config::default())?; + + let aig = aig_reader.parse()?; + + let mut offset = 0; + offset = flussab::text::next_newline(read_witness, offset); + + if offset == 2 { + read_witness.advance(replace(&mut offset, 0)); + offset = flussab::text::next_newline(read_witness, offset); + read_witness.advance(replace(&mut offset, 0)); + + offset = flussab::text::next_newline(read_witness, offset); + } + + if offset != aig.latches.len() + 1 { + bail!( + "unexpected number of initial latch states, found {} expected {}", + offset.saturating_sub(1), + aig.latches.len() + ); + } + + let latch_init = read_witness.buf()[..aig.latches.len()] + .iter() + .copied() + .map(util::parse_input_bit) + .collect::, _>>()?; + + read_witness.advance(replace(&mut offset, 0)); + + let mut frame_inputs = vec![]; + + loop { + offset = flussab::text::next_newline(read_witness, offset); + + if matches!(read_witness.buf().first(), None | Some(b'.') | Some(b'#')) { + read_witness.check_io_error()?; + break; + } + + if offset != aig.input_count + 1 { + bail!( + "unexpected number of input bits, found {} expected {}", + offset.saturating_sub(1), + aig.input_count + ); + } + + frame_inputs.push( + read_witness.buf()[..aig.input_count] + .iter() + .copied() + .map(util::parse_input_bit) + .collect::, _>>()?, + ); + read_witness.advance(replace(&mut offset, 0)); + } + + care_graph::minimize( + &aig, + &latch_init, + &frame_inputs, + &mut writer_output, + &care_graph::MinimizationOptions { + fixed_init: !options.latches, + verify: match options.verify { + VerificationOption::Off => None, + VerificationOption::Cex => Some(care_graph::Verification::Cex), + VerificationOption::Full => Some(care_graph::Verification::Full), + }, + }, + )?; + + Ok(()) +} diff --git a/tools/aigcexmin/src/util.rs b/tools/aigcexmin/src/util.rs new file mode 100644 index 00000000..fb24bc89 --- /dev/null +++ b/tools/aigcexmin/src/util.rs @@ -0,0 +1,25 @@ +use color_eyre::eyre::bail; +use flussab::DeferredWriter; +use flussab_aiger::Lit; + +pub fn unpack_lit(lit: L) -> (usize, bool) { + let lit = lit.code(); + (lit >> 1, lit & 1 != 0) +} + +pub fn parse_input_bit(byte: u8) -> color_eyre::Result> { + Ok(match byte { + b'0' => Some(false), + b'1' => Some(true), + b'x' => None, + _ => bail!("unexpected input bit {byte:?}"), + }) +} + +pub fn write_output_bit(writer: &mut DeferredWriter, bit: Option) { + writer.write_all_defer_err(match bit { + Some(false) => b"0", + Some(true) => b"1", + None => b"x", + }) +} diff --git a/tools/cexenum/cexenum.py b/tools/cexenum/cexenum.py new file mode 100755 index 00000000..3ac88916 --- /dev/null +++ b/tools/cexenum/cexenum.py @@ -0,0 +1,584 @@ +#!/usr/bin/env tabbypy3 +from __future__ import annotations +import asyncio + +import json +import traceback +import argparse +import shutil +import shlex +import os +from pathlib import Path +from typing import Any, Awaitable, Literal + +import yosys_mau.task_loop.job_server as job +from yosys_mau import task_loop as tl + + +libexec = Path(__file__).parent.resolve() / "libexec" + +if libexec.exists(): + os.environb[b"PATH"] = bytes(libexec) + b":" + os.environb[b"PATH"] + + +def arg_parser(): + parser = argparse.ArgumentParser( + prog="cexenum", usage="%(prog)s [options] " + ) + + parser.add_argument( + "work_dir", + metavar="", + help="existing SBY work directory", + type=Path, + ) + + parser.add_argument( + "--depth", + type=int, + metavar="N", + help="BMC depth for the initial assertion failure (default: %(default)s)", + default=100, + ) + + parser.add_argument( + "--enum-depth", + type=int, + metavar="N", + help="number of time steps to run enumeration for, starting with" + " and including the time step of the first assertion failure" + " (default: %(default)s)", + default=10, + ) + + parser.add_argument( + "--no-sim", + dest="sim", + action="store_false", + help="do not run sim to obtain .fst traces for the enumerated counter examples", + ) + + parser.add_argument( + "--smtbmc-options", + metavar='"..."', + type=shlex.split, + help='command line options to pass to smtbmc (default: "%(default)s")', + default="-s yices --unroll", + ) + + parser.add_argument("--debug", action="store_true", help="enable debug logging") + parser.add_argument( + "--debug-events", action="store_true", help="enable debug event logging" + ) + + parser.add_argument( + "-j", + metavar="", + type=int, + dest="jobs", + help="maximum number of processes to run in parallel", + default=None, + ) + + return parser + + +def lines(*args): + return "".join(f"{line}\n" for line in args) + + +@tl.task_context +class App: + raw_args: argparse.Namespace + + debug: bool = False + debug_events: bool = False + + depth: int + enum_depth: int + sim: bool + + smtbmc_options: list[str] + + work_dir: Path + + work_subdir: Path + trace_dir_full: Path + trace_dir_min: Path + cache_dir: Path + + +def main() -> None: + args = arg_parser().parse_args() + + job.global_client(args.jobs) + + # Move command line arguments into the App context + for name in dir(args): + if name in type(App).__mro__[1].__annotations__: + setattr(App, name, getattr(args, name)) + + App.raw_args = args + + try: + tl.run_task_loop(task_loop_main) + except tl.TaskCancelled: + exit(1) + except BaseException as e: + if App.debug or App.debug_events: + traceback.print_exc() + tl.log_exception(e, raise_error=False) # Automatically avoids double logging + exit(1) + + +def setup_logging(): + tl.LogContext.app_name = "CEXENUM" + tl.logging.start_logging() + + if App.debug_events: + tl.logging.start_debug_event_logging() + if App.debug: + tl.LogContext.level = "debug" + + def error_handler(err: BaseException): + if isinstance(err, tl.TaskCancelled): + return + tl.log_exception(err, raise_error=True) + + tl.current_task().set_error_handler(None, error_handler) + + +async def batch(*args): + result = None + for arg in args: + result = await arg + return result + + +async def task_loop_main() -> None: + setup_logging() + + cached = False + + App.cache_dir = App.work_dir / "cexenum_cache" + try: + App.cache_dir.mkdir() + except FileExistsError: + if (App.cache_dir / "done").exists(): + cached = True + else: + shutil.rmtree(App.cache_dir) + App.cache_dir.mkdir() + + App.work_subdir = App.work_dir / "cexenum" + try: + App.work_subdir.mkdir() + except FileExistsError: + shutil.rmtree(App.work_subdir) + App.work_subdir.mkdir() + + App.trace_dir_full = App.work_subdir / "full" + App.trace_dir_full.mkdir() + App.trace_dir_min = App.work_subdir / "min" + App.trace_dir_min.mkdir() + + if cached: + tl.log("Reusing cached AIGER model") + aig_model = tl.Task() + else: + aig_model = AigModel() + + Enumeration(aig_model) + + +class AigModel(tl.process.Process): + def __init__(self): + self[tl.LogContext].scope = "aiger" + (App.cache_dir / "design_aiger.ys").write_text( + lines( + "read_ilang ../model/design_prep.il", + "hierarchy -simcheck", + "flatten", + "setundef -undriven -anyseq", + "setattr -set keep 1 w:\*", + "delete -output", + "opt -full", + "techmap", + "opt -fast", + "memory_map -formal", + "formalff -clk2ff -ff2anyinit", + "simplemap", + "dffunmap", + "abc -g AND -fast", + "opt_clean", + "stat", + "write_rtlil design_aiger.il", + "write_aiger -I -B -zinit" + " -map design_aiger.aim -ywmap design_aiger.ywa design_aiger.aig", + ) + ) + super().__init__( + ["yosys", "-ql", "design_aiger.log", "design_aiger.ys"], cwd=App.cache_dir + ) + self.name = "aiger" + self.log_output() + + async def on_run(self) -> None: + await super().on_run() + (App.cache_dir / "done").write_text("") + + +class MinimizeTrace(tl.Task): + def __init__(self, trace_name: str, aig_model: tl.Task): + super().__init__() + self.trace_name = trace_name + + full_yw = App.trace_dir_full / self.trace_name + min_yw = App.trace_dir_min / self.trace_name + + stem = full_yw.stem + + full_aiw = full_yw.with_suffix(".aiw") + min_aiw = min_yw.with_suffix(".aiw") + + yw2aiw = YosysWitness( + "yw2aiw", + full_yw, + App.cache_dir / "design_aiger.ywa", + full_aiw, + cwd=App.trace_dir_full, + ) + yw2aiw.depends_on(aig_model) + yw2aiw[tl.LogContext].scope = f"yw2aiw[{stem}]" + + aigcexmin = AigCexMin( + App.cache_dir / "design_aiger.aig", + full_aiw, + min_aiw, + cwd=App.trace_dir_min, + ) + aigcexmin.depends_on(yw2aiw) + aigcexmin[tl.LogContext].scope = f"aigcexmin[{stem}]" + + self.aiw2yw = aiw2yw = YosysWitness( + "aiw2yw", + min_aiw, + App.cache_dir / "design_aiger.ywa", + min_yw, + cwd=App.trace_dir_min, + ) + aiw2yw[tl.LogContext].scope = f"aiw2yw[{stem}]" + aiw2yw.depends_on(aigcexmin) + + if App.sim: + sim = SimTrace( + App.cache_dir / "design_aiger.il", + min_yw, + min_yw.with_suffix(".fst"), + cwd=App.trace_dir_min, + ) + + sim[tl.LogContext].scope = f"sim[{stem}]" + sim.depends_on(aiw2yw) + + +def relative_to(target: Path, cwd: Path) -> Path: + prefix = Path("") + target = target.resolve() + cwd = cwd.resolve() + while True: + try: + return prefix / (target.relative_to(cwd)) + except ValueError: + prefix = prefix / ".." + if cwd == cwd.parent: + return target + cwd = cwd.parent + + +class YosysWitness(tl.process.Process): + def __init__( + self, + mode: Literal["yw2aiw"] | Literal["aiw2yw"], + input: Path, + mapfile: Path, + output: Path, + cwd: Path, + ): + super().__init__( + [ + "yosys-witness", + mode, + str(relative_to(input, cwd)), + str(relative_to(mapfile, cwd)), + str(relative_to(output, cwd)), + ], + cwd=cwd, + ) + + def handler(event: tl.process.OutputEvent): + tl.log_debug(event.output.rstrip("\n")) + + self.sync_handle_events(tl.process.OutputEvent, handler) + + +class AigCexMin(tl.process.Process): + def __init__(self, design_aig: Path, input_aiw: Path, output_aiw: Path, cwd: Path): + super().__init__( + [ + "aigcexmin", + str(relative_to(design_aig, cwd)), + str(relative_to(input_aiw, cwd)), + str(relative_to(output_aiw, cwd)), + ], + cwd=cwd, + ) + + self.log_path = output_aiw.with_suffix(".log") + self.log_file = None + + def handler(event: tl.process.OutputEvent): + if self.log_file is None: + self.log_file = self.log_path.open("w") + self.log_file.write(event.output) + self.log_file.flush() + tl.log_debug(event.output.rstrip("\n")) + + self.sync_handle_events(tl.process.OutputEvent, handler) + + def on_cleanup(self): + if self.log_file is not None: + self.log_file.close() + super().on_cleanup() + + +class SimTrace(tl.process.Process): + def __init__(self, design_il: Path, input_yw: Path, output_fst: Path, cwd: Path): + self[tl.LogContext].scope = "sim" + + script_file = output_fst.with_suffix(".fst.ys") + log_file = output_fst.with_suffix(".fst.log") + + script_file.write_text( + lines( + f"read_rtlil {relative_to(design_il, cwd)}", + "logger -nowarn" + ' "Yosys witness trace has an unexpected value for the clock input"', + f"sim -zinit -r {relative_to(input_yw, cwd)} -hdlname" + f" -fst {relative_to(output_fst, cwd)}", + ) + ) + super().__init__( + [ + "yosys", + "-ql", + str(relative_to(log_file, cwd)), + str(relative_to(script_file, cwd)), + ], + cwd=cwd, + ) + self.name = "sim" + self.log_output() + + +class Enumeration(tl.Task): + def __init__(self, aig_model: tl.Task): + self.aig_model = aig_model + super().__init__() + + async def on_run(self) -> None: + smtbmc = Smtbmc(App.work_dir / "model" / "design_smt2.smt2") + + await smtbmc.ping() + + pred = None + + i = 0 + limit = App.depth + first_failure = None + + while i <= limit: + tl.log(f"Checking assumptions in step {i}..") + presat_checked = await batch( + smtbmc.bmc_step(i, initial=i == 0, assertions=None, pred=pred), + smtbmc.check(), + ) + if presat_checked != "sat": + if first_failure is None: + tl.log_error("Assumptions are not satisfiable") + else: + tl.log("No further counter-examples are reachable") + return + + tl.log(f"Checking assertions in step {i}..") + checked = await batch( + smtbmc.push(), + smtbmc.assertions(i, False), + smtbmc.check(), + ) + pred = i + if checked != "unsat": + if first_failure is None: + first_failure = i + limit = i + App.enum_depth + tl.log("BMC failed! Enumerating counter-examples..") + counter = 0 + + assert checked == "sat" + path = App.trace_dir_full / f"trace{i}_{counter}.yw" + + while checked == "sat": + await smtbmc.incremental_command( + cmd="write_yw_trace", path=str(path) + ) + tl.log(f"Written counter-example to {path.name}") + + minimize = MinimizeTrace(path.name, self.aig_model) + minimize.depends_on(self.aig_model) + + await minimize.aiw2yw.finished + + min_path = App.trace_dir_min / f"trace{i}_{counter}.yw" + + checked = await batch( + smtbmc.incremental_command( + cmd="read_yw_trace", + name="last", + path=str(min_path), + skip_x=True, + ), + smtbmc.assert_( + ["not", ["and", *(["yw", "last", k] for k in range(i + 1))]] + ), + smtbmc.check(), + ) + + counter += 1 + path = App.trace_dir_full / f"trace{i}_{counter}.yw" + + await batch(smtbmc.pop(), smtbmc.assertions(i)) + + i += 1 + + smtbmc.close_stdin() + + +class Smtbmc(tl.process.Process): + def __init__(self, smt2_model: Path): + self[tl.LogContext].scope = "smtbmc" + super().__init__( + [ + "yosys-smtbmc", + "--incremental", + *App.smtbmc_options, + str(smt2_model), + ], + interact=True, + ) + self.name = "smtbmc" + + self.expected_results = [] + + async def on_run(self) -> None: + def output_handler(event: tl.process.StderrEvent): + result = json.loads(event.output) + tl.log_debug(f"smtbmc > {result!r}") + if "err" in result: + exception = tl.logging.LoggedError( + tl.log_error(result["err"], raise_error=False) + ) + self.expected_results.pop(0).set_exception(exception) + if "msg" in result: + tl.log(result["msg"]) + if "ok" in result: + assert self.expected_results + self.expected_results.pop(0).set_result(result["ok"]) + + self.sync_handle_events(tl.process.StdoutEvent, output_handler) + + return await super().on_run() + + def ping(self) -> Awaitable[None]: + return self.incremental_command(cmd="ping") + + def incremental_command(self, **command: dict[Any]) -> Awaitable[Any]: + tl.log_debug(f"smtbmc < {command!r}") + self.write(json.dumps(command)) + self.write("\n") + result = asyncio.Future() + self.expected_results.append(result) + + return result + + def new_step(self, step: int) -> Awaitable[None]: + return self.incremental_command(cmd="new_step", step=step) + + def push(self) -> Awaitable[None]: + return self.incremental_command(cmd="push") + + def pop(self) -> Awaitable[None]: + return self.incremental_command(cmd="pop") + + def check(self) -> Awaitable[str]: + return self.incremental_command(cmd="check") + + def assert_antecedent(self, expr: Any) -> Awaitable[None]: + return self.incremental_command(cmd="assert_antecedent", expr=expr) + + def assert_consequent(self, expr: Any) -> Awaitable[None]: + return self.incremental_command(cmd="assert_consequent", expr=expr) + + def assert_(self, expr: Any) -> Awaitable[None]: + return self.incremental_command(cmd="assert", expr=expr) + + def hierarchy(self, step: int) -> Awaitable[None]: + return self.assert_antecedent(["mod_h", ["step", step]]) + + def assumptions(self, step: int, valid: bool = True) -> Awaitable[None]: + expr = ["mod_u", ["step", step]] + if not valid: + expr = ["not", expr] + return self.assert_consequent(expr) + + def assertions(self, step: int, valid: bool = True) -> Awaitable[None]: + expr = ["mod_a", ["step", step]] + if not valid: + expr = ["not", expr] + return self.assert_(expr) + + def initial(self, step: int, initial: bool) -> Awaitable[None]: + if initial: + return batch( + self.assert_antecedent(["mod_i", ["step", step]]), + self.assert_antecedent(["mod_is", ["step", step]]), + ) + else: + return self.assert_antecedent(["not", ["mod_is", ["step", step]]]) + + def transition(self, pred: int, succ: int) -> Awaitable[None]: + return self.assert_antecedent(["mod_t", ["step", pred], ["step", succ]]) + + def bmc_step( + self, + step: int, + initial: bool = False, + assertions: bool | None = True, + pred: int | None = None, + ) -> Awaitable[None]: + futures = [] + futures.append(self.new_step(step)) + futures.append(self.hierarchy(step)) + futures.append(self.assumptions(step)) + futures.append(self.initial(step, initial)) + + if pred is not None: + futures.append(self.transition(pred, step)) + + if assertions is not None: + futures.append(self.assertions(assertions)) + + return batch(*futures) + + +if __name__ == "__main__": + main() diff --git a/tools/cexenum/examples/.gitignore b/tools/cexenum/examples/.gitignore new file mode 100644 index 00000000..e69de29b diff --git a/tools/cexenum/examples/factor.sby b/tools/cexenum/examples/factor.sby new file mode 100644 index 00000000..5fe21fcd --- /dev/null +++ b/tools/cexenum/examples/factor.sby @@ -0,0 +1,49 @@ +# Run using: +# +# sby -f factor.sby +# tabbypy3 cexenum.py factor --enum-depth=0 +# +[options] +mode bmc +make_model prep,smt2 +expect unknown + +[engines] +none + +[script] +read_verilog -sv top.sv +prep -top top + +[file top.sv] +module top(input clk, input b_bit, output [15:0] acc); + reg [7:0] a; + reg [7:0] b_mask = 8'hff; + + + reg [15:0] a_shift = 0; + reg [15:0] acc = 0; + + + always @(posedge clk) begin + assume (!clk); + if ($initstate) begin + a_shift <= a; + acc <= 0; + end else begin + + if (b_bit) begin + acc <= acc + a_shift; + end + a_shift <= a_shift << 1; + b_mask <= b_mask >> 1; + end + + if (b_mask == 0) begin + a <= 0; + assert (acc != 100); + end; + + end + +endmodule