diff --git a/.github/actions/setup-build-env/action.yml b/.github/actions/setup-build-env/action.yml index 86d7c21ec..8b3e2d769 100644 --- a/.github/actions/setup-build-env/action.yml +++ b/.github/actions/setup-build-env/action.yml @@ -56,7 +56,7 @@ runs: if: runner.os == 'macOS' shell: bash run: | - brew bundle + brew install bison flex gawk libffi git pkg-config python3 bash googletest tcl-tk llvm - name: Linux runtime environment if: runner.os == 'Linux' diff --git a/.github/workflows/extra-builds.yml b/.github/workflows/extra-builds.yml index 644529c9d..d041794d7 100644 --- a/.github/workflows/extra-builds.yml +++ b/.github/workflows/extra-builds.yml @@ -172,32 +172,12 @@ jobs: cmake -B build -DCMAKE_TOOLCHAIN_FILE=${WASI_SDK_PATH}/share/cmake/wasi-sdk-p1.cmake -DCMAKE_BUILD_TYPE=Release -DYOSYS_COMPILER_LAUNCHER=ccache . cmake --build build -j$(nproc) - nix-build: - name: "Build nix flake" - needs: pre_job - if: (github.event_name == 'merge_group' || github.event_name == 'workflow_dispatch') && needs.pre_job.outputs.should_skip != 'true' - runs-on: ${{ matrix.os }} - strategy: - matrix: - os: [ubuntu-latest, macos-latest] - fail-fast: false - steps: - - uses: actions/checkout@v7 - with: - submodules: true - persist-credentials: false - - uses: cachix/install-nix-action@v31 - with: - install_url: https://releases.nixos.org/nix/nix-2.30.0/install - - run: nix build -L - extra-builds-result: runs-on: ubuntu-latest needs: - vs-build - mingw-build - wasi-build - - nix-build if: always() steps: - name: Check results diff --git a/.github/workflows/nix.yml b/.github/workflows/nix.yml new file mode 100644 index 000000000..120240bd6 --- /dev/null +++ b/.github/workflows/nix.yml @@ -0,0 +1,24 @@ +name: Test nix build + +on: + workflow_dispatch: + schedule: + - cron: '0 5 * * 6' + +jobs: + nix-build: + name: "Build nix flake" + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, macos-latest] + fail-fast: false + steps: + - uses: actions/checkout@v7 + with: + submodules: true + persist-credentials: false + - uses: cachix/install-nix-action@v31 + with: + install_url: https://releases.nixos.org/nix/nix-2.30.0/install + - run: nix build -L diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 86ae739ae..bfd7140e9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -22,6 +22,13 @@ jobs: persist-credentials: false ssh-key: ${{ secrets.SSH_PRIVATE_KEY }} + - name: Cache Dist Directory + uses: actions/cache@v6 + with: + path: dist-x86_64-musllinux + key: ${{ github.run_id }}-x86_64-musllinux-wheel + enableCrossOsArchive: true + - name: Build wheel in Alpine container run: | docker run --rm \ @@ -51,15 +58,10 @@ jobs: # Build the wheel via setup.py pip install --break-system-packages pybind11 cxxheaderparser - mkdir -p /src/dist - python3 -m pip wheel . --wheel-dir /src/dist + mkdir -p /src/dist-x86_64-musllinux + python3 -m pip wheel . --wheel-dir /src/dist-x86_64-musllinux ' - - uses: actions/upload-artifact@v4 - with: - name: linux-musl-wheel - path: dist/*.whl - build-manylinux-wheel: runs-on: ubuntu-latest name: Build Linux amd64 wheel (manylinux2014, glibc 2.17+) @@ -71,6 +73,13 @@ jobs: submodules: recursive ssh-key: ${{ secrets.SSH_PRIVATE_KEY }} + - name: Cache Dist Directory + uses: actions/cache@v6 + with: + path: dist-x86_64-manylinux + key: ${{ github.run_id }}-x86_64-manylinux-wheel + enableCrossOsArchive: true + - name: Build wheel in manylinux2014 container run: | docker run --rm \ @@ -110,22 +119,18 @@ jobs: make cd /src + WHEEL_DIR=/src/dist-x86_64-manylinux pip3 install --upgrade pip setuptools wheel pybind11 cxxheaderparser mkdir -p dist-manylinux - python3 -m pip wheel . --wheel-dir /src/dist-manylinux + python3 -m pip wheel . --wheel-dir $WHEEL_DIR # Tag wheel as manylinux2014 pip3 install auditwheel || true - for whl in /src/dist-manylinux/*.whl; do - auditwheel repair "$whl" --plat manylinux2014_x86_64 -w /src/dist-manylinux/ 2>/dev/null || true + for whl in $WHEEL_DIR/*.whl; do + auditwheel repair "$whl" --plat manylinux2014_x86_64 -w $WHEEL_DIR/ 2>/dev/null || true done ' - - uses: actions/upload-artifact@v4 - with: - name: linux-manylinux-wheel - path: dist-manylinux/*.whl - build-macos-wheel: runs-on: macos-15 name: Build macOS arm64 wheel @@ -155,49 +160,76 @@ jobs: cd verific/tclmain make + - name: Cache Dist Directory + uses: actions/cache@v6 + with: + path: dist-aarch64-macos + key: ${{ github.run_id }}-aarch64-macos-wheel + enableCrossOsArchive: true + - name: Build wheel run: | export PATH="$(brew --prefix bison)/bin:$(brew --prefix flex)/bin:$PATH" export MACOSX_DEPLOYMENT_TARGET=11.0 mkdir -p dist - python3 -m pip wheel . --wheel-dir dist - - - uses: actions/upload-artifact@v4 - with: - name: macos-wheel - path: dist/*.whl + python3 -m pip wheel . --wheel-dir dist-aarch64-macos release: # allow testing outside main branch - if: github.ref_name == 'main' + # if: github.ref_name == 'main' runs-on: ubuntu-latest needs: [build-linux-wheel, build-manylinux-wheel, build-macos-wheel] name: Create GitHub releases steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: + ssh-key: ${{ secrets.SSH_PRIVATE_KEY }} fetch-depth: 0 - - - name: Download artifacts - uses: actions/download-artifact@v4 - with: - path: all-wheels - merge-multiple: true - - - name: Generate release notes - id: meta + - id: meta + name: Get repo metadata and clean up run: | SHORT_SHA=$(git rev-parse --short HEAD) FULL_SHA=$(git rev-parse HEAD) DATE=$(date -u +%Y-%m-%d) - echo "short_sha=$SHORT_SHA" >> "$GITHUB_OUTPUT" - echo "date=$DATE" >> "$GITHUB_OUTPUT" REPO_URL="${{ github.server_url }}/${{ github.repository }}" + echo "short_sha=$SHORT_SHA" >> "$GITHUB_OUTPUT" + echo "full_sha=$FULL_SHA" >> "$GITHUB_OUTPUT" + echo "date=$DATE" >> "$GITHUB_OUTPUT" + echo "repo_url=$REPO_URL" >> "$GITHUB_OUTPUT" + rm -rf ./* ./.* + - uses: actions/checkout@v6 + with: + repository: silimate/yosys-wheels + ssh-key: ${{ secrets.SSH_PRIVATE_KEY }} + fetch-depth: 0 + - name: Restore dist from cache (x86_64-musllinux) + uses: actions/cache@v6 + with: + path: dist-x86_64-musllinux + key: ${{ github.run_id }}-x86_64-musllinux-wheel + enableCrossOsArchive: true + fail-on-cache-miss: true + - name: Restore dist from cache (x86_64-manylinux) + uses: actions/cache@v6 + with: + path: dist-x86_64-manylinux + key: ${{ github.run_id }}-x86_64-manylinux-wheel + enableCrossOsArchive: true + fail-on-cache-miss: true + - name: Restore dist from cache (aarch64-macos) + uses: actions/cache@v6 + with: + path: dist-aarch64-macos + key: ${{ github.run_id }}-aarch64-macos-wheel + enableCrossOsArchive: true + fail-on-cache-miss: true + - name: Update release notes + run: | printf '%s\n' \ - "Automated build from \`main\` @ [\`${SHORT_SHA}\`](${REPO_URL}/commit/${FULL_SHA})" \ + "Automated build from \`main\` @ [\`${{ steps.meta.outputs.short_sha }}\`](${{ steps.meta.outputs.repo_url }}/commit/${{ steps.meta.outputs.full_sha }})" \ "" \ - "**Built:** ${DATE}" \ + "**Built:** ${{ steps.meta.outputs.date }}" \ "" \ "### Assets" \ "| File | Platform |" \ @@ -211,17 +243,22 @@ jobs: "pip install pyosys-*.whl" \ "\`\`\`" \ > release_notes.md - + - name: Commit and push + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git commit --all --message="Build for ${{ steps.meta.outputs.date }}-${{ steps.meta.outputs.short_sha }}" + git push - name: Create permanent release run: | TAG="build-${{ steps.meta.outputs.date }}-${{ steps.meta.outputs.short_sha }}" gh release create "$TAG" \ - all-wheels/*.whl \ - --target "${{ github.sha }}" \ + dist-*/*.whl \ + --target "$(git rev-parse HEAD)" \ --title "Build ${{ steps.meta.outputs.date }} (${{ steps.meta.outputs.short_sha }})" \ --notes-file release_notes.md env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_TOKEN: ${{ secrets.YOSYS_WHEELS_TOKEN }} - name: Update latest release run: | @@ -229,10 +266,10 @@ jobs: git push -f origin latest gh release delete latest --yes 2>/dev/null || true gh release create latest \ - all-wheels/*.whl \ - --target "${{ github.sha }}" \ + dist-*/*.whl \ + --target "$(git rev-parse HEAD)" \ --title "Latest Build (${{ steps.meta.outputs.date }})" \ --notes-file release_notes.md \ --prerelease env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_TOKEN: ${{ secrets.YOSYS_WHEELS_TOKEN }} diff --git a/.github/workflows/test-build.yml b/.github/workflows/test-build.yml index 56e94328e..2d641deec 100644 --- a/.github/workflows/test-build.yml +++ b/.github/workflows/test-build.yml @@ -49,7 +49,7 @@ jobs: - id: set_output run: | if [ "${{ github.event_name }}" = "merge_group" ]; then - echo "should_skip=false" >> $GITHUB_OUTPUT + echo "should_skip=true" >> $GITHUB_OUTPUT elif [ "${{ github.event_name }}" = "push" ]; then should_skip=false else @@ -90,7 +90,13 @@ jobs: key: test-build-${{ matrix.os }} save: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} restore-keys: | - test-build-${{ matrix.os }}- + test-build-${{ matrix.os }} + + - name: Cache Build Directory + uses: actions/cache@v6 + with: + path: build + key: ${{ runner.os }}-build-${{ github.run_id }} - name: Build shell: bash @@ -104,19 +110,6 @@ jobs: run: | ./build/yosys-config || true - - name: Compress build - shell: bash - run: | - cd build - tar -cvf ../build.tar share/ yosys yosys-* - - - name: Store build artifact - uses: actions/upload-artifact@v7 - with: - name: build-${{ matrix.os }} - path: build.tar - retention-days: 1 - test-yosys: name: Run tests runs-on: ${{ matrix.os }} @@ -142,16 +135,12 @@ jobs: get-build-deps: true get-iverilog: true - - name: Download build artifact - uses: actions/download-artifact@v8 + - name: Restore Build Directory from Cache + uses: actions/cache@v6 with: - name: build-${{ matrix.os }} - - - name: Uncompress build - shell: bash - run: | - mkdir -p build - tar -xvf build.tar -C build + path: build + key: ${{ runner.os }}-build-${{ github.run_id }} + fail-on-cache-miss: true - name: Log yosys-config output run: | @@ -192,19 +181,17 @@ jobs: with: runs-on: ${{ matrix.os }} - - name: Download build artifact - uses: actions/download-artifact@v8 + - name: Restore Build Directory from Cache + uses: actions/cache@v6 with: - name: build-${{ matrix.os }} - - - name: Uncompress build - shell: bash - run: - tar -xvf build.tar + path: build + key: ${{ runner.os }}-build-${{ github.run_id }} + fail-on-cache-miss: true - name: test_cell shell: bash run: | + cd build ./yosys -p 'test_cell -n 20 -s 1 all' ./yosys -p 'test_cell -n 20 -s 1 -nosat -aigmap $pow $pmux' ./yosys -p 'test_cell -n 20 -s 1 -nosat -aigmap $eqx $nex $bweqx' @@ -233,16 +220,12 @@ jobs: get-build-deps: true get-docs-deps: true - - name: Download build artifact - uses: actions/download-artifact@v8 + - name: Restore Build Directory from Cache + uses: actions/cache@v6 with: - name: build-${{ matrix.os }} - - - name: Uncompress build - shell: bash - run: | - mkdir -p build - tar -xvf build.tar -C build + path: build + key: ${{ runner.os }}-build-${{ github.run_id }} + fail-on-cache-miss: true - name: Log yosys-config output run: | diff --git a/.vscode/settings.json b/.vscode/settings.json index 25ffb81ba..f91385f77 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -71,6 +71,9 @@ "variant": "cpp", "algorithm": "cpp", "*.inc": "cpp", - "tuple": "cpp" + "tuple": "cpp", + "__verbose_trap": "cpp", + "cstddef": "cpp", + "__log_hardening_failure": "cpp" } } diff --git a/abc b/abc index 648de7a21..bec0695f0 160000 --- a/abc +++ b/abc @@ -1 +1 @@ -Subproject commit 648de7a21751cfab01c253aca6d4751f642577df +Subproject commit bec0695f0de539e974d95ab845e581da326ea3d7 diff --git a/kernel/log.cc b/kernel/log.cc index 2f5a6350b..2c3e45c2b 100644 --- a/kernel/log.cc +++ b/kernel/log.cc @@ -388,12 +388,6 @@ void log_formatted_file_error(std::string_view filename, int lineno, std::string log_error_with_prefix(prefix, str); } -void logv_file_error(const string &filename, int lineno, - const char *format, va_list ap) -{ - log_formatted_file_error(filename, lineno, vstringf(format, ap)); -} - void log_experimental(const std::string &str) { if (log_experimentals_ignored.count(str) == 0 && log_experimentals.count(str) == 0) { diff --git a/kernel/log.h b/kernel/log.h index 673c27ce2..f320537ad 100644 --- a/kernel/log.h +++ b/kernel/log.h @@ -24,7 +24,6 @@ #include -#include #include #define YS_REGEX_COMPILE(param) std::regex(param, \ std::regex_constants::nosubs | \ @@ -44,20 +43,11 @@ # endif #endif -#if defined(_MSC_VER) -// At least this is not in MSVC++ 2013. -# define __PRETTY_FUNCTION__ __FUNCTION__ -#endif - // from libs/sha1/sha1.h class SHA1; YOSYS_NAMESPACE_BEGIN -#define S__LINE__sub2(x) #x -#define S__LINE__sub1(x) S__LINE__sub2(x) -#define S__LINE__ S__LINE__sub1(__LINE__) - // YS_DEBUGTRAP is a macro that is functionally equivalent to a breakpoint // if the platform provides such functionality, and does nothing otherwise. // If no debugger is attached, it starts a just-in-time debugger if available, @@ -120,9 +110,6 @@ extern int log_make_debug; extern int log_force_debug; extern int log_debug_suppressed; -[[deprecated]] -[[noreturn]] void logv_file_error(const string &filename, int lineno, const char *format, va_list ap); - void set_verific_logging(void (*cb)(int msg_type, const char *message_id, const char* file_path, unsigned int left_line, unsigned int left_col, unsigned int right_line, unsigned int right_col, const char *msg)); extern void (*log_verific_callback)(int msg_type, const char *message_id, const char* file_path, unsigned int left_line, unsigned int left_col, unsigned int right_line, unsigned int right_col, const char *msg); diff --git a/passes/opt/CMakeLists.txt b/passes/opt/CMakeLists.txt index 8e7d866bd..a651c400d 100644 --- a/passes/opt/CMakeLists.txt +++ b/passes/opt/CMakeLists.txt @@ -108,6 +108,10 @@ yosys_pass(opt_first_fit_alloc opt_first_fit_alloc.cc ) +yosys_pass(opt_priokey + opt_priokey.cc +) + pmgen_command(peepopt peepopt_shiftmul_right.pmg peepopt_shiftmul_left.pmg diff --git a/passes/opt/cut_region.h b/passes/opt/cut_region.h index 3387922ca..d5384b631 100644 --- a/passes/opt/cut_region.h +++ b/passes/opt/cut_region.h @@ -826,6 +826,20 @@ struct CutRegionWorker return sig; } + // Shared ConstEval for the whole matching phase. The netlist is not + // modified until rewrites are applied, so one instance (built once, at + // O(module) cost) serves every fingerprint eval instead of rebuilding + // one per candidate. Callers keep push/pop balanced, so the base state + // stays clean between uses (asserted on each hand-out). + std::unique_ptr shared_ce_ptr; + ConstEval &shared_ce() + { + if (!shared_ce_ptr) + shared_ce_ptr = std::make_unique(module); + log_assert(shared_ce_ptr->stack.empty()); + return *shared_ce_ptr; + } + // Evaluate `out_sig` under the given input assignments; returns false if // the cut does not fully determine the output. Charges the eval budget // by `cone_cells_estimate`. diff --git a/passes/opt/opt_dff.cc b/passes/opt/opt_dff.cc index 9563a672b..a68184cae 100644 --- a/passes/opt/opt_dff.cc +++ b/passes/opt/opt_dff.cc @@ -43,6 +43,91 @@ struct OptDffOptions bool keepdc; }; +// Bit-parallel random simulation used as a cheap pre-filter for equivalence +struct BitSim { + Module *module; + SigMap &sigmap; + ModWalker &modwalker; + dict sim_vals; + uint64_t rng_state; + int max_depth; + int evals_left; + + BitSim(Module *m, SigMap &sm, ModWalker &mw) + : module(m), sigmap(sm), modwalker(mw), rng_state(1337) + { + max_depth = module->design->scratchpad_get_int("opt_dff.sim_depth", 10000); + evals_left = module->design->scratchpad_get_int("opt_dff.sim_evals", 1000000); + } + + uint64_t next_rand() { + uint32_t lo = mkhash_xorshift((uint32_t)rng_state); + uint32_t hi = mkhash_xorshift((uint32_t)(rng_state >> 32) ^ lo); + rng_state = ((uint64_t)hi << 32) | lo; + return rng_state; + } + + uint64_t eval_bit(SigBit b, int depth = 0) { + SigBit mapped = sigmap(b); + if (mapped == State::S0) return 0ULL; + if (mapped == State::S1) return ~0ULL; + if (mapped == State::Sx || mapped == State::Sz) return 0ULL; + + auto it = sim_vals.find(mapped); + if (it != sim_vals.end()) return it->second; + + // Failsafe for huge designs + if (depth >= max_depth || evals_left <= 0) { + uint64_t r = next_rand(); + sim_vals[mapped] = r; + return r; + } + evals_left--; + + sim_vals[mapped] = 0; + uint64_t res = 0; + + if (!modwalker.has_drivers(mapped)) { + res = next_rand(); + } else { + auto &drivers = modwalker.signal_drivers[mapped]; + if (drivers.empty()) { + res = next_rand(); + } else { + auto driver = *drivers.begin(); + Cell *cell = driver.cell; + + if (cell->is_builtin_ff()) { + res = next_rand(); + } else if (cell->type == ID($_AND_)) { + res = eval_bit(cell->getPort(ID::A)[0], depth+1) & eval_bit(cell->getPort(ID::B)[0], depth+1); + } else if (cell->type == ID($_OR_)) { + res = eval_bit(cell->getPort(ID::A)[0], depth+1) | eval_bit(cell->getPort(ID::B)[0], depth+1); + } else if (cell->type == ID($_XOR_)) { + res = eval_bit(cell->getPort(ID::A)[0], depth+1) ^ eval_bit(cell->getPort(ID::B)[0], depth+1); + } else if (cell->type == ID($_NOT_)) { + res = ~eval_bit(cell->getPort(ID::A)[0], depth+1); + } else if (cell->type == ID($_MUX_)) { + uint64_t s = eval_bit(cell->getPort(ID::S)[0], depth+1); + uint64_t a = eval_bit(cell->getPort(ID::A)[0], depth+1); + uint64_t b = eval_bit(cell->getPort(ID::B)[0], depth+1); + res = (a & ~s) | (b & s); + } else if (cell->type == ID($mux)) { + uint64_t s = eval_bit(cell->getPort(ID::S)[0], depth+1); + uint64_t a = eval_bit(cell->getPort(ID::A)[driver.offset], depth+1); + uint64_t b = eval_bit(cell->getPort(ID::B)[driver.offset], depth+1); + res = (a & ~s) | (b & s); + } else { + res = next_rand(); + } + } + } + + sim_vals[mapped] = res; + return res; + } +}; + struct OptDffWorker { const OptDffOptions &opt; @@ -702,7 +787,7 @@ struct OptDffWorker if (new_cell) dff_cells.push_back(new_cell); - log("Adding EN signal on %s (%s) from module %s (D = %s, Q = %s).\n", + log_debug("Adding EN signal on %s (%s) from module %s (D = %s, Q = %s).\n", cell, cell->type.unescape(), module, log_signal(new_ff.sig_d), log_signal(new_ff.sig_q)); } @@ -921,6 +1006,397 @@ struct OptDffWorker return did_something; } + + struct EqBit { + Cell *cell; + int idx; + SigBit q; + }; + + // NOTE: This intentionally duplicates a subset of FfData, as flattening just the + // fields that matter for merging into a single comparable/hashable key is cheaper + struct SigKey { + enum Flag : uint16_t { + InitOne = 1u << 0, + InitX = 1u << 1, + PolClk = 1u << 2, + PolCe = 1u << 3, + PolSrst = 1u << 4, + PolArst = 1u << 5, + PolAload = 1u << 6, + PolClr = 1u << 7, + PolSet = 1u << 8, + CeOverSrst = 1u << 9, + }; + + SigBit clk, ce, srst, arst, aload, clr, set; + IdString cell_type; // for SR + uint16_t flags; + + bool operator==(const SigKey &o) const { + return flags == o.flags && clk == o.clk && ce == o.ce && srst == o.srst && arst == o.arst + && aload == o.aload && clr == o.clr && set == o.set && cell_type == o.cell_type; + } + + Hasher hash_into(Hasher h) const { + h.eat(flags); + h.eat(clk); + h.eat(ce); + h.eat(srst); + h.eat(arst); + h.eat(aload); + h.eat(clr); + h.eat(set); + h.eat(cell_type); + return h; + } + }; + + bool is_def(State s) { + // Concrete constant bit (0 or 1), as opposed to x/z + return s == State::S0 || s == State::S1; + } + + std::vector> gather_initial_eq_classes(std::vector &bits, dict &ff_for_cell) + { + std::vector keys; + + // Collect FF bits eligible for merging + for (auto cell : module->selected_cells()) { + if (!cell->is_builtin_ff()) + continue; + + FfData ff(&initvals, cell); + if (!ff.has_clk && !ff.has_gclk) + continue; + + ff_for_cell.emplace(cell, ff); + + for (int i = 0; i < ff.width; i++) { + // Skip bits whose reset value is undefined (x) + if (ff.has_srst && !is_def(ff.val_srst[i])) continue; + if (ff.has_arst && !is_def(ff.val_arst[i])) continue; + + // Class members are assumed equal in the current cycle and proven equal in the next, which needs + // a base case anchoring them to a common known value + bool def_init = is_def(ff.val_init[i]); + if (!def_init && !ff.has_srst && !ff.has_arst) + continue; + + SigKey k = {}; + + // Flags + if (def_init && ff.val_init[i] == State::S1) + k.flags |= SigKey::InitOne; + else if (!def_init) + k.flags |= SigKey::InitX; + + if (ff.has_clk) { + k.clk = ff.sig_clk; + if (ff.pol_clk) k.flags |= SigKey::PolClk; + } + if (ff.has_ce) { + k.ce = ff.sig_ce; + if (ff.pol_ce) k.flags |= SigKey::PolCe; + } + if (ff.has_srst) { + k.srst = ff.sig_srst; + if (ff.pol_srst) k.flags |= SigKey::PolSrst; + if (ff.ce_over_srst) k.flags |= SigKey::CeOverSrst; + } + if (ff.has_arst) { + k.arst = ff.sig_arst; + if (ff.pol_arst) k.flags |= SigKey::PolArst; + } + if (ff.has_aload) { + k.aload = ff.sig_aload; + if (ff.pol_aload) k.flags |= SigKey::PolAload; + } + if (ff.has_sr) { + k.clr = ff.sig_clr[i]; + k.set = ff.sig_set[i]; + k.cell_type = cell->type; + if (ff.pol_clr) k.flags |= SigKey::PolClr; + if (ff.pol_set) k.flags |= SigKey::PolSet; + } + + bits.push_back({cell, i, ff.sig_q[i]}); + keys.push_back(k); + } + } + + dict> buckets; + for (int i = 0; i < GetSize(bits); i++) + buckets[keys[i]].push_back(i); + + std::vector> classes; + for (auto &kv : buckets) + if (GetSize(kv.second) >= 2) + classes.push_back(std::move(kv.second)); + + return classes; + } + + std::vector> filter_classes_sim( + const std::vector> &classes, + const std::vector &bits, + const dict &ff_for_cell, + ModWalker &modwalker + ) { + BitSim sim(module, sigmap, modwalker); + + // Assume same class + for (auto &cls : classes) { + uint64_t class_q_val = sim.next_rand(); + for (int idx : cls) { + sim.sim_vals[sigmap(bits[idx].q)] = class_q_val; + } + } + + std::vector> refined_classes; + for (auto &cls : classes) { + dict> sim_buckets; + for (int idx : cls) { + const EqBit &eb = bits[idx]; + const FfData &ff = ff_for_cell.at(eb.cell); + uint64_t n_val = sim.eval_bit(ff.sig_d[eb.idx]); + + if (ff.has_aload) { + uint64_t al = sim.eval_bit(ff.sig_aload); + if (!ff.pol_aload) al = ~al; + uint64_t ad = sim.eval_bit(ff.sig_ad[eb.idx]); + n_val = (n_val & ~al) | (ad & al); + } + if (ff.has_arst) { + uint64_t ar = sim.eval_bit(ff.sig_arst); + if (!ff.pol_arst) ar = ~ar; + uint64_t ar_val = (ff.val_arst[eb.idx] == State::S1) ? ~0ULL : 0ULL; + n_val = (n_val & ~ar) | (ar_val & ar); + } + if (ff.has_sr) { + uint64_t clr = sim.eval_bit(ff.sig_clr[eb.idx]); + if (!ff.pol_clr) clr = ~clr; + uint64_t set = sim.eval_bit(ff.sig_set[eb.idx]); + if (!ff.pol_set) set = ~set; + n_val = ~clr & (set | n_val); + } + if (ff.has_srst) { + uint64_t srst = sim.eval_bit(ff.sig_srst); + if (!ff.pol_srst) srst = ~srst; + uint64_t srst_val = (ff.val_srst[eb.idx] == State::S1) ? ~0ULL : 0ULL; + n_val = (n_val & ~srst) | (srst_val & srst); + } + + sim_buckets[n_val].push_back(idx); + } + + for (auto &kv : sim_buckets) + if (GetSize(kv.second) >= 2) + refined_classes.push_back(std::move(kv.second)); + } + + return refined_classes; + } + + std::vector> filter_classes_sat( + std::vector> classes, + const std::vector &bits, + const dict &ff_for_cell, + ModWalker &modwalker + ) { + QuickConeSat qcsat(modwalker); + std::vector q_lit(bits.size(), -1); + std::vector n_lit(bits.size(), -1); + + // Build the next-state function n_lit[idx] of every candidate bit by + // folding the FF's control logic on top of the D input (-> next value) + + // Two bits are equivalent if their next states always agree whenever their + // current states (and those of every other candidate pair) agree + for (auto &cls : classes) { + for (int idx : cls) { + const EqBit &eb = bits[idx]; + const FfData &ff = ff_for_cell.at(eb.cell); + q_lit[idx] = qcsat.importSigBit(eb.q); + int n = qcsat.importSigBit(ff.sig_d[eb.idx]); + + if (ff.has_aload) { + int al = qcsat.importSigBit(ff.sig_aload); + if (!ff.pol_aload) al = qcsat.ez->NOT(al); + n = qcsat.ez->ITE(al, qcsat.importSigBit(ff.sig_ad[eb.idx]), n); + } + if (ff.has_arst) { + int ar = qcsat.importSigBit(ff.sig_arst); + if (!ff.pol_arst) ar = qcsat.ez->NOT(ar); + n = qcsat.ez->ITE(ar, qcsat.ez->value(ff.val_arst[eb.idx] == State::S1), n); + } + if (ff.has_sr) { + int clr = qcsat.importSigBit(ff.sig_clr[eb.idx]); + if (!ff.pol_clr) clr = qcsat.ez->NOT(clr); + int set = qcsat.importSigBit(ff.sig_set[eb.idx]); + if (!ff.pol_set) set = qcsat.ez->NOT(set); + n = qcsat.ez->AND(qcsat.ez->NOT(clr), qcsat.ez->OR(set, n)); + } + if (ff.has_srst) { + int srst = qcsat.importSigBit(ff.sig_srst); + if (!ff.pol_srst) srst = qcsat.ez->NOT(srst); + n = qcsat.ez->ITE(srst, qcsat.ez->value(ff.val_srst[eb.idx] == State::S1), n); + } + + n_lit[idx] = n; + } + } + + qcsat.prepare(); + + // Assume the induction hypo (that every current class is internally equal in the present cycle), and try + // to prove that the members of each class therefore also agree in the next cycle + + // A class survives only if no counterexample exists under that hypo, so combined with the common init/reset + // value that every class shares, this makes the equality an inductive invariant -> bits are eq and safe to merge + std::vector worklist; + std::vector in_worklist(GetSize(classes), true); + + for (int i = 0; i < GetSize(classes); i++) + worklist.push_back(i); + + while (!worklist.empty()) { + int cls_idx = worklist.back(); + worklist.pop_back(); + in_worklist[cls_idx] = false; + + auto &cls = classes[cls_idx]; + if (GetSize(cls) < 2) continue; + + // Induction hypo: assume every candidate class is equal + std::vector assumptions; + for (auto &c : classes) { + if (GetSize(c) < 2) continue; + int rep = c[0]; + for (int k = 1; k < GetSize(c); k++) + assumptions.push_back(qcsat.ez->IFF(q_lit[rep], q_lit[c[k]])); + } + + // Scan the class members against the representative and issue a query per pair, + // stopping early at the first counterexample, which is reused to split the entire + // class at once + int rep = cls[0]; + for (int i = 1; i < GetSize(cls); i++) { + if (n_lit[rep] == n_lit[cls[i]]) + continue; + + // Can the next state of the rep and this member ever differ? + int query = qcsat.ez->XOR(n_lit[rep], n_lit[cls[i]]); + // Capture every member's next-state value in that model so one counterexample + // partitions the whole class + std::vector modelExprs; + for (int b : cls) + modelExprs.push_back(n_lit[b]); + + std::vector modelVals; + assumptions.push_back(query); + + if (qcsat.ez->solve(modelExprs, modelVals, assumptions)) { + // SAT -> partition entire class + std::vector sub0; + std::vector sub1; + + for (size_t b_idx = 0; b_idx < cls.size(); b_idx++) { + if (modelVals[b_idx]) + sub1.push_back(cls[b_idx]); + else + sub0.push_back(cls[b_idx]); + } + + classes[cls_idx] = std::move(sub0); + classes.push_back(std::move(sub1)); + in_worklist.push_back(false); + + // Partition was split -> the induction hypo weakened + for (int j = 0; j < GetSize(classes); j++) { + if (GetSize(classes[j]) >= 2 && !in_worklist[j]) { + worklist.push_back(j); + in_worklist[j] = true; + } + } + + break; // Process new splits + } + + assumptions.pop_back(); // Remove query for the next pairwise check if UNSAT + } + } + + return classes; + } + + bool apply_eq_merges(const std::vector> &classes, const std::vector &bits, dict &ff_for_cell) + { + bool any_change = false; + dict> remove_bits; + + // Drive every non-rep Q from its class rep, drop merged bits from their FFs + for (auto &cls : classes) { + if (GetSize(cls) < 2) + continue; + SigBit rep_q = bits[cls[0]].q; + any_change = true; + for (int k = 1; k < GetSize(cls); k++) { + const EqBit &eb = bits[cls[k]]; + initvals.remove_init(eb.q); + module->connect(eb.q, rep_q); + remove_bits[eb.cell].insert(eb.idx); + } + } + + for (auto &kv : remove_bits) { + Cell *cell = kv.first; + const std::set &drop = kv.second; + FfData &ff = ff_for_cell.at(cell); + std::vector keep; + + for (int i = 0; i < ff.width; i++) + if (!drop.count(i)) + keep.push_back(i); + + if (keep.empty()) { + module->remove(cell); + } else { + FfData new_ff = ff.slice(keep); + new_ff.cell = cell; + new_ff.emit(); + } + } + + return any_change; + } + + bool run_eqbits() + { + if (!opt.sat) + return false; + + std::vector bits; + dict ff_for_cell; + + std::vector> classes = gather_initial_eq_classes(bits, ff_for_cell); + if (classes.empty()) + return false; + + ModWalker modwalker(module->design, module); + + // Simulation prepass + classes = filter_classes_sim(classes, bits, ff_for_cell, modwalker); + if (classes.empty()) + return false; + + // SAT prove + classes = filter_classes_sat(std::move(classes), bits, ff_for_cell, modwalker); + if (classes.empty()) + return false; + + return apply_eq_merges(classes, bits, ff_for_cell); + } }; struct OptDffPass : public Pass { @@ -950,7 +1426,9 @@ struct OptDffPass : public Pass { log("\n"); log(" -sat\n"); log(" additionally invoke SAT solver to detect and remove flip-flops (with\n"); - log(" non-constant inputs) that can also be replaced with a constant driver\n"); + log(" non-constant inputs) that can also be replaced with a constant driver,\n"); + log(" or merged with equivalent flip-flops. this reasons in 2-valued logic\n"); + log(" and may resolve don't-care bits, so it is incompatible with -keepdc.\n"); log("\n"); log(" -keepdc\n"); log(" some optimizations change the behavior of the circuit with respect to\n"); @@ -982,6 +1460,13 @@ struct OptDffPass : public Pass { } extra_args(args, argidx, design); + // The SAT engine reasons in 2-valued logic (a constant x is treated as + // 0), so it can resolve don't-care bits to concrete values -- exactly + // what -keepdc promises not to do. Refuse the combination rather than + // silently ignore -keepdc. + if (opt.sat && opt.keepdc) + log_cmd_error("The -sat and -keepdc options are mutually exclusive.\n"); + bool did_something = false; for (auto mod : design->selected_modules()) { OptDffWorker worker(opt, mod); @@ -989,6 +1474,8 @@ struct OptDffPass : public Pass { did_something = true; if (worker.run_constbits()) did_something = true; + if (worker.run_eqbits()) + did_something = true; } if (did_something) diff --git a/passes/opt/opt_first_fit_alloc.cc b/passes/opt/opt_first_fit_alloc.cc index a2e2af74b..f8a51d766 100644 --- a/passes/opt/opt_first_fit_alloc.cc +++ b/passes/opt/opt_first_fit_alloc.cc @@ -165,6 +165,56 @@ struct OptFirstFitAllocWorker : CutRegionWorker { return r; } + // ---------------------------------------------------------------- + // Reference semantics of the "coalesce matrix" allocator variant. + // + // Leadership and slot assignment are identical to the greedy first-fit + // above, but the per-lane rank does NOT depend on the lane's own enable: + // every lane k (enabled or not) inherits the slot of the first leader at + // or before k (in priority order) whose category matches cat[k]. This + // models RTL that precomputes a per-leader "same_cat[i][k]" mask (gated + // only on the leader's enable) and forward-coalesces into lane k without + // re-checking en[k]. There is no broadcast lane in this variant. + // ---------------------------------------------------------------- + AllocResult compute_alloc_coalesce(const vector &en, const vector &cat, + int n) const + { + AllocResult r = compute_alloc(en, vector(n, 0), cat, n); + for (int k = 0; k < n; k++) { + r.dsel[k] = 0; + for (int i = 0; i <= k; i++) + if (r.leader[i] && cat[i] == cat[k]) { + r.dsel[k] = r.slot[i]; + break; + } + } + return r; + } + + AllocResult compute_alloc_coalesce_dir(const vector &en, const vector &cat, + int n, bool msb_first) const + { + if (!msb_first) + return compute_alloc_coalesce(en, cat, n); + vector er(n), cr(n); + for (int i = 0; i < n; i++) { + er[i] = en[n - 1 - i]; + cr[i] = cat[n - 1 - i]; + } + AllocResult rr = compute_alloc_coalesce(er, cr, n); + AllocResult r; + r.dsel.assign(n, 0); + r.leader.assign(n, 0); + r.slot.assign(n, 0); + r.M = rr.M; + for (int i = 0; i < n; i++) { + r.dsel[i] = rr.dsel[n - 1 - i]; + r.leader[i] = rr.leader[n - 1 - i]; + r.slot[i] = rr.slot[n - 1 - i]; + } + return r; + } + // ---------------------------------------------------------------- // Test vectors. `nval` is the number of distinct label values (2^c for // the category, 2^a for the xbar attribute). The vectors deliberately @@ -412,6 +462,10 @@ struct OptFirstFitAllocWorker : CutRegionWorker { int field_w = 0; SigSpec en_sig, bc_sig, cat_sig; bool has_bc = false; + // Enable-independent forward coalescing: lanes inherit the slot of the + // first same-category leader at or before them in priority order, + // regardless of their own enable (the "same_cat matrix" RTL shape). + bool coalesce = false; int c = 0; bool msb_first = false; Cell *anchor = nullptr; @@ -594,9 +648,39 @@ struct OptFirstFitAllocWorker : CutRegionWorker { { Cell *anchor = rg.anchor; int n = rg.n, c = rg.c, w = rg.field_w; + SigSpec cat = sigmap(rg.cat_sig); + + // Enable-independent forward coalescing: lane k inherits the slot of the + // unique same-category leader at or before k in priority order, with no + // enable/broadcast gating. The priority position of a lane is a compile- + // time constant, so the "leader at or before k" restriction is static. + if (rg.coalesce) { + auto pos = [&](int l) { return rg.msb_first ? (n - 1 - l) : l; }; + SigSpec out; + for (int k = 0; k < n; k++) { + SigSpec cat_k = cat.extract(k * c, c); + vector g(n, SigBit(State::S0)); + for (int i = 0; i < n; i++) { + if (pos(i) > pos(k)) + continue; + SigBit eq = emit_eq_sig(anchor, cat.extract(i * c, c), cat_k); + g[i] = emit_and(anchor, leader[i], eq); + } + SigSpec rank(Const(0, cnt_w)); + for (int b = 0; b < cnt_w; b++) { + SigSpec terms; + for (int i = 0; i < n; i++) + if (pos(i) <= pos(k)) + terms.append(emit_and(anchor, g[i], slot[i][b])); + rank[b] = emit_reduce_or(anchor, terms); + } + out.append(zext_sig(rank, w)); + } + return out; + } + SigSpec en = sigmap(rg.en_sig); SigSpec bc = rg.has_bc ? sigmap(rg.bc_sig) : SigSpec(); - SigSpec cat = sigmap(rg.cat_sig); // bc rank: (M>=1) ? M-1 : 0 SigBit any_leader = emit_reduce_or(anchor, total); @@ -707,15 +791,22 @@ struct OptFirstFitAllocWorker : CutRegionWorker { internal_bits.insert(bit); pool seen; - auto all_internal = [&](const SigSpec &s) { + // Accept a bus bit if it is a cone-internal (computed) signal or a + // primary input / undriven bit. The enable/broadcast lanes are usually + // computed signals (e.g. valid & format), but some RTL drives the scan + // straight from a top-level request port (e.g. lane_en), so input buses + // must be admissible too. Inputs sort shallowest (depth 0) below, so they + // survive the candidate cap ahead of the deep intermediate nets. + auto all_internal_or_input = [&](const SigSpec &s) { for (auto bit : s) - if (!bit.wire || !internal_bits.count(bit)) + if (!bit.wire || (!internal_bits.count(bit) && + bit_to_driver.at(bit, nullptr) != nullptr)) return false; return true; }; auto add = [&](const SigSpec &sig, const std::string &nm) { SigSpec s = sigmap(sig); - if (GetSize(s) != n || !sig_bus_ok(s) || !all_internal(s)) + if (GetSize(s) != n || !sig_bus_ok(s) || !all_internal_or_input(s)) return; if (!seen.insert(s).second) return; @@ -868,17 +959,28 @@ struct OptFirstFitAllocWorker : CutRegionWorker { bool fpm = fingerprint_dsel(ce, root_sig, n, field_w, en_bus.sig, bc_bus ? bc_bus->sig : SigSpec(), bc_bus != nullptr, cat_sig, c, msb_first, cone_est); - log_debug(" en=%s bc=%s cat=%dx%d %s: fingerprint %s\n", en_bus.name.c_str(), + // Standard first-fit failed: try the enable-independent + // forward-coalescing variant (no broadcast lane). + bool coalesce = false; + if (!fpm && bc_bus == nullptr) { + fpm = fingerprint_dsel(ce, root_sig, n, field_w, en_bus.sig, + SigSpec(), false, cat_sig, c, msb_first, + cone_est, /*coalesce=*/true); + coalesce = fpm; + } + log_debug(" en=%s bc=%s cat=%dx%d %s: fingerprint %s%s\n", en_bus.name.c_str(), bc_bus ? bc_bus->name.c_str() : "-", n, c, - msb_first ? "MSB" : "LSB", fpm ? "MATCH" : "no"); + msb_first ? "MSB" : "LSB", fpm ? "MATCH" : "no", + coalesce ? " (coalesce)" : ""); if (fpm) { out.dsel_sig = root_sig; out.dsel_name = root_name; out.n = n; out.field_w = field_w; out.en_sig = en_bus.sig; - out.bc_sig = bc_bus ? bc_bus->sig : SigSpec(); - out.has_bc = (bc_bus != nullptr); + out.bc_sig = (!coalesce && bc_bus) ? bc_bus->sig : SigSpec(); + out.has_bc = (!coalesce && bc_bus != nullptr); + out.coalesce = coalesce; out.cat_sig = cat_sig; out.c = c; out.msb_first = msb_first; @@ -901,7 +1003,8 @@ struct OptFirstFitAllocWorker : CutRegionWorker { // Any eval failure or single mismatch rejects the candidate. bool fingerprint_dsel(ConstEval &ce, const SigSpec &root, int n, int field_w, const SigSpec &en_sig, const SigSpec &bc_sig, bool has_bc, - const SigSpec &cat_sig, int c, bool msb_first, int64_t cone_est) + const SigSpec &cat_sig, int c, bool msb_first, int64_t cone_est, + bool coalesce = false) { int nval = 1 << c; vector vs = make_vectors(n, nval, has_bc); @@ -920,7 +1023,9 @@ struct OptFirstFitAllocWorker : CutRegionWorker { if (!eval_root(ce, sets, root, res, cone_est)) return false; - AllocResult ar = compute_alloc_dir(tv.en, tv.bc, tv.label, n, msb_first); + AllocResult ar = coalesce + ? compute_alloc_coalesce_dir(tv.en, tv.label, n, msb_first) + : compute_alloc_dir(tv.en, tv.bc, tv.label, n, msb_first); for (int k = 0; k < n; k++) { int got = lane_val(res, k, field_w); int exp = ar.dsel[k] & ((1 << field_w) - 1); @@ -1239,11 +1344,12 @@ struct OptFirstFitAllocWorker : CutRegionWorker { claim_region(rg.dsel_sig, rg.dsel_cut_cells); regions_rewritten++; - log(" %s: %s <- first_fit_alloc(en=%s%s, cat=%dx%d, %s)\n", + log(" %s: %s <- first_fit_alloc(en=%s%s, cat=%dx%d, %s%s)\n", log_id(module), rg.dsel_name.c_str(), log_signal(rg.en_sig), rg.has_bc ? stringf(", bc=%s", log_signal(rg.bc_sig)).c_str() : "", - rg.n, rg.c, rg.msb_first ? "MSB-first" : "LSB-first"); + rg.n, rg.c, rg.msb_first ? "MSB-first" : "LSB-first", + rg.coalesce ? ", coalesce" : ""); if (have_xbar) { SigSpec new_xbar = emit_xbar(rg, xb, leader, slot, cnt_w); diff --git a/passes/opt/opt_prienc.cc b/passes/opt/opt_prienc.cc index acb7b78f4..030cc9f60 100644 --- a/passes/opt/opt_prienc.cc +++ b/passes/opt/opt_prienc.cc @@ -82,6 +82,7 @@ struct OptPriEncWorker { // Configuration. bool detect_clz = true; bool detect_ctz = true; + bool detect_rr = true; int max_input_width = 256; int min_input_width = 4; @@ -242,6 +243,56 @@ struct OptPriEncWorker { return vs; } + // ConstEval::set() requires every (sigmap-canonical) bit it pins to be a + // distinct free wire bit. Real designs can tie parts of a bus to constants + // or alias nets together, so guard the fingerprint inputs: reject signals + // containing constant or repeated bits, and (across the whole set) any + // overlap between them. This prevents a ConstEval assertion; skipping an + // unclean candidate only forgoes a possible rewrite, never yields a wrong + // one. + static bool clean_set_signals(std::initializer_list sigs) { + pool seen; + for (const SigSpec* sp : sigs) + for (auto bit : *sp) { + if (bit.wire == nullptr) return false; + if (!seen.insert(bit).second) return false; + } + return true; + } + + // A set of signals is a valid ConstEval "cut" to pin as free inputs only if + // pinning them can never collide with a value ConstEval derives while + // evaluating the cone. ConstEval::eval() re-computes and re-set()s the FULL + // output of any combinational cell it needs: so if a pinned bit is a + // combinational-cell output and a *sibling* output bit of that same cell + // lies outside the cut (and is pulled into the cone), evaluating the sibling + // re-sets the pinned bit to the cell's real value, which contradicts the + // free value we pinned -> the ConstEval assertion in set() fires. + // + // A bit is a safe leaf when it is a primary input, sequential-cell output or + // undriven (all absent from bit_to_driver, which holds combinational drivers + // only). A combinational-cell output is safe only if that cell's entire + // output lies within the cut. `cut` must be the union of every signal pinned + // together before a shared eval. + bool is_valid_consteval_cut(const SigSpec& cut) { + pool cut_bits; + for (auto bit : cut) + if (bit.wire) cut_bits.insert(bit); + for (auto bit : cut) { + if (bit.wire == nullptr) return false; + auto it = bit_to_driver.find(bit); + if (it == bit_to_driver.end()) continue; // safe leaf + Cell* d = it->second; + for (auto& conn : d->connections()) { + if (!d->output(conn.first)) continue; + for (auto ob : sigmap(conn.second)) + if (ob.wire && !cut_bits.count(ob)) + return false; + } + } + return true; + } + // Run all candidate test vectors through ConstEval and try to match each of // the four PE variants against the recorded outputs. Returns the matched // variant, or NONE. @@ -256,6 +307,12 @@ struct OptPriEncWorker { if (!clz_full_ok && !ctz_full_ok && !clz_short_ok && !ctz_short_ok) return PEVariant::NONE; + if (!clean_set_signals({&T_sig})) + return PEVariant::NONE; + + if (!is_valid_consteval_cut(T_sig)) + return PEVariant::NONE; + auto vs = gen_test_vectors(N); for (auto& v : vs) { ce.push(); @@ -390,6 +447,177 @@ struct OptPriEncWorker { return full; } + // ------------------------------------------------------------------ + // Round-robin (rotated priority) detection + rewrite. + // + // A round-robin arbiter grants the first set request bit scanning + // *upward* (increasing index, wrapping) starting just after a stored + // pointer `s` (= idx_last): + // + // grant = anyreq ? (first set bit at index > s, else first set + // bit overall) : 0 + // idx_next = anyreq ? grant : s + // + // RTL usually spells this as a DEPTH-iteration loop that walks `idx` + // downward from idx_last with wraparound and keeps the last hit, which + // elaborates into a serial mux/shift chain of depth ~DEPTH. The rewrite + // below is log-depth: + // + // above[i] = (i > s) (per-bit threshold mask) + // mask_hi = req & above + // grant = anyreq ? (|mask_hi ? ctz(mask_hi) : ctz(req)) : 0 + // + // where ctz() reuses the log-depth CTZ network. For power-of-2 DEPTH the + // rewrite is fully combinationally equivalent for every pointer value; + // for non-power-of-2 DEPTH it is equivalent for every *reachable* pointer + // (idx_last only ever holds a valid index in [0,DEPTH)), which is the + // range the fingerprint checks. Detection therefore sweeps s over + // [0,DEPTH) only. + // ------------------------------------------------------------------ + + // kind: 0 = grant, 1 = idx_next. + int rr_expected(const Const& reqv, int s, int N, int W, int kind) { + auto bits = reqv.to_bits(); + int lo_all = -1, lo_hi = -1; + for (int i = 0; i < N; i++) { + bool set = (i < (int)bits.size() && bits[i] == State::S1); + if (!set) continue; + if (lo_all < 0) lo_all = i; + if (i > s && lo_hi < 0) lo_hi = i; + } + bool anyreq = (lo_all >= 0); + int gsel = (lo_hi >= 0) ? lo_hi : (lo_all >= 0 ? lo_all : 0); + int val = kind == 0 ? (anyreq ? gsel : 0) + : (anyreq ? gsel : s); + return val & ((W >= 31) ? -1 : ((1 << W) - 1)); + } + + // Returns matched kind (0 grant, 1 idx_next), or -1 for no match. + int fingerprint_rr(SigSpec req_sig, SigSpec start_sig, SigSpec S_sig, + int N, int W) { + ConstEval ce(module); + if (!clean_set_signals({&req_sig, &start_sig})) + return -1; + SigSpec cut = req_sig; + cut.append(start_sig); + if (!is_valid_consteval_cut(cut)) + return -1; + bool ok0 = true, ok1 = true; + auto deck = gen_test_vectors(N); + int checks = 0; + for (auto& rv : deck) { + for (int s = 0; s < N; s++) { + ce.push(); + ce.set(req_sig, rv); + ce.set(start_sig, Const(s, W)); + SigSpec out = S_sig, undef; + bool ok = ce.eval(out, undef); + ce.pop(); + if (!ok || !out.is_fully_const()) return -1; + int ov = out.as_const().as_int(); + if (ok0 && ov != rr_expected(rv, s, N, W, 0)) ok0 = false; + if (ok1 && ov != rr_expected(rv, s, N, W, 1)) ok1 = false; + checks++; + if (!ok0 && !ok1) return -1; + } + } + if (checks < 2 * N) return -1; + if (ok0) return 0; + if (ok1) return 1; + return -1; + } + + // Emit the log-depth round-robin network. Shared subexpressions across + // the grant / idx_next pair for the same (req, start) inputs are cached. + dict, std::tuple> rr_core_cache; + + SigSpec emit_rr(Wire* req_wire, Wire* start_wire, int N, int W, int kind) { + SigSpec req = sigmap(SigSpec(req_wire)); + SigSpec s = sigmap(SigSpec(start_wire)); + + SigSpec gsel; + SigBit anyreq; + auto key = std::make_pair(req_wire, start_wire); + auto it = rr_core_cache.find(key); + if (it != rr_core_cache.end()) { + SigSpec cached_gsel; + std::tie(cached_gsel, std::ignore, anyreq) = it->second; + gsel = cached_gsel; + } else { + SigSpec above; + for (int i = 0; i < N; i++) { + above.append(module->Lt(NEW_ID2_SUFFIX("rrabove"), s, SigSpec(Const(i, W)))); + cells_added++; + } + SigSpec mask_hi = module->And(NEW_ID2_SUFFIX("rrmask"), req, above); + cells_added++; + + SigSpec cz_hi = emit_ctz_full(mask_hi, N); + SigSpec cz_all = emit_ctz_full(req, N); + auto low_w = [&](SigSpec x) { + if (GetSize(x) > W) return x.extract(0, W); + while (GetSize(x) < W) x.append(SigSpec(State::S0)); + return x; + }; + cz_hi = low_w(cz_hi); + cz_all = low_w(cz_all); + + SigBit any_hi = module->ReduceOr(NEW_ID2_SUFFIX("rranyhi"), mask_hi); + cells_added++; + anyreq = module->ReduceOr(NEW_ID2_SUFFIX("rranyreq"), req); + cells_added++; + // any_hi ? cz_hi : cz_all + gsel = module->Mux(NEW_ID2_SUFFIX("rrgsel"), cz_all, cz_hi, any_hi); + cells_added++; + rr_core_cache[key] = std::make_tuple(gsel, SigSpec(), anyreq); + } + + SigSpec fallback = (kind == 0) ? SigSpec(Const(0, W)) : s; + // anyreq ? gsel : fallback + SigSpec res = module->Mux(NEW_ID2_SUFFIX("rrsel"), fallback, gsel, anyreq); + cells_added++; + return res; + } + + // Generalisation of cone_depends_only_on_T to a set of allowed leaf bits. + bool cone_depends_only_on_set(SigSpec S_sig, const pool& allowed) { + pool visited; + std::queue worklist; + for (auto bit : sigmap(S_sig)) { + if (!bit.wire) continue; + if (visited.insert(bit).second) worklist.push(bit); + } + while (!worklist.empty()) { + SigBit bit = worklist.front(); + worklist.pop(); + if (allowed.count(bit)) continue; + if (input_port_bits.count(bit)) return false; + auto it = bit_to_driver.find(bit); + if (it == bit_to_driver.end()) return false; + Cell* drv = it->second; + if (sequential_cells.count(drv)) return false; + for (auto& conn : drv->connections()) { + if (!drv->input(conn.first)) continue; + for (auto in_bit : sigmap(conn.second)) { + if (!in_bit.wire) continue; + if (visited.insert(in_bit).second) worklist.push(in_bit); + } + } + } + return true; + } + + struct RRRewrite { + Wire* S_wire; + Wire* req_wire; + Wire* start_wire; + int N; + int W; + int kind; + Cell* sole_driver; + IdString out_port; + }; + struct Rewrite { Wire* S_wire; Wire* T_wire; @@ -584,6 +812,77 @@ struct OptPriEncWorker { } } + // Stage 3: round-robin (rotated priority) detection. Reuses the same + // candidate cones; an output S is grant/idx_next of a round-robin + // arbiter over a wide request bus `req` and a same-width-as-S pointer + // `start`, both bottoming out the cone. + vector rr_rewrites; + if (detect_rr) { + const int max_pairs = 64; + for (auto& cand : candidates) { + if (claimed_outputs.count(cand.S_wire)) continue; + if (claimed_drivers.count(cand.sole_driver)) continue; + + int W = cand.S_wire->width; + if (W < 2 || W > max_W) continue; + SigSpec S_sig = sigmap(SigSpec(cand.S_wire)); + + vector req_cands, start_cands; + for (Wire* w : wires_snapshot) { + if (w == cand.S_wire) continue; + bool all_in = true; + for (auto bit : sigmap(SigSpec(w))) + if (!cand.cone_bits.count(bit)) { all_in = false; break; } + if (!all_in) continue; + int wn = w->width; + if (wn >= min_input_width && wn <= max_input_width && + clog2_int(wn) == W) + req_cands.push_back(w); + if (wn == W) + start_cands.push_back(w); + } + std::sort(req_cands.begin(), req_cands.end(), + [](Wire* a, Wire* b) { return a->width > b->width; }); + + bool matched = false; + for (Wire* req_wire : req_cands) { + if (matched) break; + int N = req_wire->width; + SigSpec req_sig = sigmap(SigSpec(req_wire)); + pool req_bits; + for (auto bit : req_sig) + if (bit.wire) req_bits.insert(bit); + // Per-req_wire fingerprint budget: a start-candidate-heavy + // first req size must not exhaust a shared budget and starve + // later (narrower) req sizes. + int pairs = 0; + for (Wire* start_wire : start_cands) { + if (start_wire == req_wire) continue; + if (++pairs > max_pairs) break; + SigSpec start_sig = sigmap(SigSpec(start_wire)); + pool allowed = req_bits; + for (auto bit : start_sig) + if (bit.wire) allowed.insert(bit); + if (!cone_depends_only_on_set(S_sig, allowed)) continue; + + int kind = fingerprint_rr(req_sig, start_sig, S_sig, N, W); + if (kind < 0) continue; + + log(" %s: %s <- round_robin_%s(req=%s, start=%s) [N=%d, W=%d]\n", + log_id(module), log_id(cand.S_wire), + kind == 0 ? "grant" : "next", + log_id(req_wire), log_id(start_wire), N, W); + rr_rewrites.push_back({cand.S_wire, req_wire, start_wire, N, W, + kind, cand.sole_driver, cand.out_port}); + claimed_outputs.insert(cand.S_wire); + claimed_drivers.insert(cand.sole_driver); + matched = true; + break; + } + } + } + } + // Apply rewrites. We collected first to avoid the index growing stale // while we add new cells/wires. for (auto& r : rewrites) { @@ -595,6 +894,14 @@ struct OptPriEncWorker { module->connect(SigSpec(r.S_wire), new_S); regions_rewritten++; } + for (auto& r : rr_rewrites) { + cell = r.sole_driver; + SigSpec new_S = emit_rr(r.req_wire, r.start_wire, r.N, r.W, r.kind); + Wire* dangling = module->addWire(NEW_ID2_SUFFIX("dangling"), r.W); + r.sole_driver->setPort(r.out_port, dangling); + module->connect(SigSpec(r.S_wire), new_S); + regions_rewritten++; + } } }; @@ -624,11 +931,23 @@ struct OptPriEncPass : public Pass { log(" ctz_full : symmetric to clz_full from the LSB side.\n"); log(" ctz_short : symmetric to clz_short from the LSB side.\n"); log("\n"); + log("In addition, the pass detects round-robin (rotated priority)\n"); + log("arbiters: grant / idx_next = first set request bit scanning upward\n"); + log("(wrapping) from just after a stored pointer idx_last. RTL typically\n"); + log("spells this as a DEPTH-iteration idx-- loop over req[idx], which\n"); + log("elaborates into a serial chain; it is replaced with a log-depth\n"); + log("threshold-mask + CTZ network. For power-of-2 DEPTH the rewrite is\n"); + log("equivalent for every pointer value; for other widths it is\n"); + log("equivalent for every reachable pointer (idx_last in [0,DEPTH)).\n"); + log("\n"); log(" -clz\n"); - log(" detect CLZ patterns only.\n"); + log(" detect CLZ patterns only (also disables round-robin).\n"); log("\n"); log(" -ctz\n"); - log(" detect CTZ patterns only.\n"); + log(" detect CTZ patterns only (also disables round-robin).\n"); + log("\n"); + log(" -no-rr\n"); + log(" disable round-robin / rotated-priority detection.\n"); log("\n"); log(" -max-width N\n"); log(" maximum input bus width to consider (default 64).\n"); @@ -648,6 +967,7 @@ struct OptPriEncPass : public Pass { bool only_clz = false; bool only_ctz = false; + bool no_rr = false; int max_width = 64; int min_width = 4; @@ -655,6 +975,7 @@ struct OptPriEncPass : public Pass { for (argidx = 1; argidx < args.size(); argidx++) { if (args[argidx] == "-clz") { only_clz = true; continue; } if (args[argidx] == "-ctz") { only_ctz = true; continue; } + if (args[argidx] == "-no-rr") { no_rr = true; continue; } if (args[argidx] == "-max-width" && argidx + 1 < args.size()) { max_width = std::stoi(args[++argidx]); continue; } @@ -664,6 +985,9 @@ struct OptPriEncPass : public Pass { break; } extra_args(args, argidx, design); + // -clz / -ctz select a single leading/trailing variant and disable + // round-robin detection unless the user re-enables it explicitly. + if (only_clz || only_ctz) no_rr = true; int total_regions = 0; int total_cells_added = 0; @@ -671,6 +995,7 @@ struct OptPriEncPass : public Pass { OptPriEncWorker worker(module); worker.detect_clz = !only_ctz; worker.detect_ctz = !only_clz; + worker.detect_rr = !no_rr; worker.max_input_width = max_width; worker.min_input_width = min_width; worker.run(); diff --git a/passes/opt/opt_priokey.cc b/passes/opt/opt_priokey.cc new file mode 100644 index 000000000..f505c1e7e --- /dev/null +++ b/passes/opt/opt_priokey.cc @@ -0,0 +1,464 @@ +/* + * yosys -- Yosys Open SYnthesis Suite + * + * Copyright (C) 2026 Akash Levy + * + * 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. + * + */ + +#include "kernel/yosys.h" +#include "kernel/sigtools.h" +#include "kernel/consteval.h" + +USING_YOSYS_NAMESPACE +PRIVATE_NAMESPACE_BEGIN + +// --------------------------------------------------------------------------- +// opt_priokey: priority-by-key deduplication ("taken" accumulator) rewrite. +// +// RTL that resolves conflicts between several sources that each carry a small +// key is often written as a serial scan over a wide one-hot "set" accumulator +// indexed by the key: +// +// taken = '0; +// for (i = 0; i < P; i++) +// if (act[i] && !taken[key[i]]) begin // this source wins its key +// taken[key[i]] = 1'b1; // claim the key +// ...use win[i]... +// end +// +// This elaborates into a serial chain of dynamic-index reads (taken[key[i]] = +// $shiftx into an S-bit vector) and dynamic-index writes (taken | 1< bit_to_driver; + + int max_slots = 1 << 14; // maximum accumulator width S + int max_chain = 256; // maximum number of sources P + int fp_trials = 256; // ConstEval validation vectors + bool strict = false; // validate over the full key range, not [0,S) + + int regions_rewritten = 0; + int cells_added = 0; + + OptPrioKeyWorker(Module *m) : module(m), sigmap(m) { build_index(); } + + void build_index() { + for (auto c : module->cells()) + for (auto &conn : c->connections()) + if (c->output(conn.first)) + for (auto bit : sigmap(conn.second)) + if (bit.wire) bit_to_driver[bit] = c; + } + + Cell *sole_driver(const SigSpec &s) { + SigSpec ss = sigmap(s); + Cell *d = nullptr; + for (auto bit : ss) { + if (!bit.wire) return nullptr; + auto it = bit_to_driver.find(bit); + if (it == bit_to_driver.end()) return nullptr; + if (d && d != it->second) return nullptr; + d = it->second; + } + return d; + } + + bool is_all_zero(const SigSpec &s) { + for (auto bit : s) + if (bit != SigBit(State::S0)) return false; + return true; + } + + bool is_const_one(const SigSpec &s) { + SigSpec ss = sigmap(s); + if (!ss.is_fully_const()) return false; + return ss.as_const().as_int() == 1; + } + + // Recover the index bus `key` from a one-hot set-mask (1 << key), spelled + // as $shl(1, key) or $shift(1, -key). + SigSpec decode_onehot_key(const SigSpec &mask) { + Cell *d = sole_driver(mask); + if (!d) return SigSpec(); + if ((d->type == ID($shl) || d->type == ID($sshl)) && + is_const_one(d->getPort(ID::A))) + return sigmap(d->getPort(ID::B)); + if (d->type == ID($shift) && is_const_one(d->getPort(ID::A))) { + Cell *neg = sole_driver(d->getPort(ID::B)); + if (neg && neg->type == ID($neg)) + return sigmap(neg->getPort(ID::A)); + } + return SigSpec(); + } + + // A set-arm is `taken_prev` with one bit (1 << key) OR'ed in. After opt it + // appears either as the raw one-hot (prev was 0) or as an $or whose two + // operands are the one-hot mask and the (masked) previous state. Return the + // key bus of the one-hot mask. + SigSpec extract_key_from_setarm(const SigSpec &arm) { + SigSpec k = decode_onehot_key(arm); + if (GetSize(k)) return k; + Cell *d = sole_driver(arm); + if (d && d->type == ID($or)) { + k = decode_onehot_key(d->getPort(ID::A)); + if (GetSize(k)) return k; + k = decode_onehot_key(d->getPort(ID::B)); + if (GetSize(k)) return k; + } + return SigSpec(); + } + + // One guarded "set key" applied to the accumulator (guard may be const-1 + // for an unconditional set). + struct SetStep { + SigBit guard; + SigSpec key; + }; + + // Cache of trace results: chained reads share accumulator prefixes (read j + // indexes the state produced by sets 0..j-1), so without memoization the + // same chain is re-walked once per read -> O(P^2 * S). The cache makes the + // total walk O(P * S). + dict>> acc_memo; + + // Walk an accumulator value back to constant zero, collecting the guarded + // key-sets that produced it. Returns false (leaving `steps` unchanged) if it + // is not a pure set-only accumulator (mux/or of prev with a one-hot key) + // rooted at 0. + bool trace_acc(SigSpec acc, vector &steps, int depth) { + acc = sigmap(acc); + auto mit = acc_memo.find(acc); + if (mit != acc_memo.end()) { + if (mit->second.first) + steps.insert(steps.end(), mit->second.second.begin(), + mit->second.second.end()); + return mit->second.first; + } + int start = GetSize(steps); + bool ok = trace_acc_uncached(acc, steps, depth); + if (ok) { + acc_memo[acc] = {true, vector(steps.begin() + start, + steps.end())}; + } else { + steps.resize(start); // discard any partial trace + acc_memo[acc] = {false, {}}; + } + return ok; + } + + bool trace_acc_uncached(SigSpec acc, vector &steps, int depth) { + if (is_all_zero(acc)) + return true; + if (depth > max_chain) + return false; + Cell *d = sole_driver(acc); + if (!d) + return false; + if (d->type == ID($mux)) { + SigSpec s = sigmap(d->getPort(ID::S)); + if (GetSize(s) != 1) + return false; + if (!trace_acc(d->getPort(ID::A), steps, depth + 1)) + return false; + SigSpec key = extract_key_from_setarm(d->getPort(ID::B)); + if (GetSize(key) == 0) + return false; + steps.push_back(SetStep{s[0], key}); + return true; + } + if (d->type == ID($or)) { + SigSpec a = d->getPort(ID::A), b = d->getPort(ID::B); + SigSpec ka = decode_onehot_key(a), kb = decode_onehot_key(b); + if (GetSize(ka) && trace_acc(b, steps, depth + 1)) { + steps.push_back(SetStep{SigBit(State::S1), ka}); + return true; + } + if (GetSize(kb) && trace_acc(a, steps, depth + 1)) { + steps.push_back(SetStep{SigBit(State::S1), kb}); + return true; + } + } + return false; + } + + // A dynamic read of the accumulator: the 1-bit read cell, the accumulator + // value it indexes (with any out-of-range x-padding stripped) and the index. + struct Read { + Cell *cell; + SigSpec acc; + SigSpec key; + }; + + // Recognize a 1-bit dynamic read of a vector: either $shiftx(A=acc, B=key) + // or $bmux(A={x.., acc}, S=key). Returns false otherwise. + bool match_read(Cell *c, Read &r) { + if (c->type == ID($shiftx)) { + if (GetSize(c->getPort(ID::Y)) != 1) + return false; + r.cell = c; + r.acc = sigmap(c->getPort(ID::A)); + r.key = sigmap(c->getPort(ID::B)); + return GetSize(r.acc) >= 2; + } + if (c->type == ID($bmux)) { + if (c->getParam(ID::WIDTH).as_int() != 1) + return false; + SigSpec a = sigmap(c->getPort(ID::A)); + // Strip the high x-padding to recover the real accumulator bits. + int w = 0; + while (w < GetSize(a) && a[w] != SigBit(State::Sx)) + w++; + if (w < 2) + return false; + r.cell = c; + r.acc = a.extract(0, w); + r.key = sigmap(c->getPort(ID::S)); + return true; + } + return false; + } + + // Prove read == OR over steps of ( guard & key == read_key ) by ConstEval + // fingerprinting over the reachable key range [0,S). Guards and key buses + // (which are disjoint sel slices) are driven as free inputs. + bool validate_read(const Read &rd, const vector &steps, int S) { + Cell *read = rd.cell; + SigSpec read_key = rd.key; + // In strict mode sweep the full key range so the rewrite is proven for + // every value the index bus can take (out-of-range reads included); + // otherwise sweep only the reachable slots [0,S). + int kw = GetSize(read_key); + uint64_t range = (uint64_t)S; + if (strict) { + int cap = kw < 30 ? kw : 30; + range = 1ULL << cap; + } + ConstEval ce(module); + uint64_t lfsr = 0x9e3779b97f4a7c15ULL ^ (uintptr_t)read; + auto rnd = [&]() { + lfsr ^= lfsr << 13; lfsr ^= lfsr >> 7; lfsr ^= lfsr << 17; + return lfsr; + }; + for (int t = 0; t < fp_trials; t++) { + ce.push(); + int rk = (int)(rnd() % range); + ce.set(read_key, Const(rk, GetSize(read_key))); + vector kv(GetSize(steps)); + vector gv(GetSize(steps)); + for (int i = 0; i < GetSize(steps); i++) { + kv[i] = (int)(rnd() % range); + ce.set(steps[i].key, Const(kv[i], GetSize(steps[i].key))); + if (steps[i].guard == State::S1) { + gv[i] = 1; + } else { + gv[i] = (int)(rnd() & 1); + ce.set(SigSpec(steps[i].guard), Const(gv[i], 1)); + } + } + SigSpec out(read->getPort(ID::Y)); + SigSpec undef; + bool ok = ce.eval(out, undef) && out.is_fully_const(); + int actual = ok ? (out.as_const().as_int() & 1) : -1; + int expect = 0; + for (int i = 0; i < GetSize(steps); i++) + if (gv[i] && kv[i] == rk) { expect = 1; break; } + ce.pop(); + if (!ok || actual != expect) + return false; + } + return true; + } + + void rewrite_read(const Read &rd, const vector &steps) { + Cell *read = rd.cell; + cell = read; + SigSpec read_key = rd.key; + SigSpec new_r; + if (steps.empty()) { + new_r = SigSpec(State::S0); + } else { + SigSpec terms; + for (auto &st : steps) { + SigSpec eq = module->Eq(NEW_ID2_SUFFIX("priokey_eq"), + st.key, read_key); + cells_added++; + SigSpec g = module->And(NEW_ID2_SUFFIX("priokey_and"), + SigSpec(st.guard), eq); + cells_added++; + terms.append(g); + } + new_r = module->ReduceOr(NEW_ID2_SUFFIX("priokey_or"), terms); + cells_added++; + } + // Tag wire so the rewrite is externally observable, then detach the old + // dynamic read and drive its consumers from the reduction. + Wire *tag = module->addWire(NEW_ID2_SUFFIX("priokey_read"), 1); + module->connect(SigSpec(tag), new_r); + SigSpec old_y = sigmap(read->getPort(ID::Y)); + Wire *dangling = module->addWire(NEW_ID2_SUFFIX("priokey_dangling"), + GetSize(read->getPort(ID::Y))); + read->setPort(ID::Y, dangling); + module->connect(old_y, SigSpec(tag)); + regions_rewritten++; + } + + void run() { + vector reads; + for (auto c : module->cells()) { + Read r; + if (!match_read(c, r)) + continue; + if (GetSize(r.acc) > max_slots) + continue; + reads.push_back(r); + } + + int max_sources = 0; + int accum_width = 0; + vector zero_reads; // read of the all-zero head (== 0) + for (auto &rd : reads) { + int S = GetSize(rd.acc); + vector steps; + if (!trace_acc(rd.acc, steps, 0)) + continue; + // The all-zero head read is only rewritten (to 0) once we know the + // pattern is really present in this module. + if (steps.empty()) { + zero_reads.push_back(rd); + continue; + } + if (!validate_read(rd, steps, S)) + continue; + rewrite_read(rd, steps); + max_sources = std::max(max_sources, GetSize(steps) + 1); + accum_width = S; + } + if (regions_rewritten) + for (auto &rd : zero_reads) + rewrite_read(rd, {}); + + if (regions_rewritten) + log(" %s: priority-by-key dedup, up to %d source(s), " + "%d-slot accumulator\n", + log_id(module), max_sources, accum_width); + } +}; + +struct OptPrioKeyPass : public Pass { + OptPrioKeyPass() : Pass("opt_priokey", + "detect and rewrite priority-by-key deduplication accumulators") {} + + void help() override { + // |---v---|---v---|---v---|---v---|---v---|---v---|---v---|---v---|---v---|---v---| + log("\n"); + log(" opt_priokey [options] [selection]\n"); + log("\n"); + log("This pass detects a serial 'set accumulator' that resolves conflicts\n"); + log("between several sources that each carry a small key:\n"); + log("\n"); + log(" taken = '0;\n"); + log(" for (i = 0; i < P; i++)\n"); + log(" if (act[i] && !taken[key[i]]) begin taken[key[i]] = 1; ... end\n"); + log("\n"); + log("Such RTL elaborates into a chain of dynamic-index reads/writes into a\n"); + log("wide one-hot vector ($shiftx / $shift), whose depth grows with both the\n"); + log("number of sources and the accumulator width. Each dynamic read\n"); + log("taken[key[j]] is provably equal to\n"); + log("\n"); + log(" OR over i args, RTLIL::Design *design) override { + log_header(design, "Executing OPT_PRIOKEY pass (priority-by-key dedup).\n"); + + int max_slots = 1 << 14; + int max_sources = 256; + bool strict = false; + size_t argidx; + for (argidx = 1; argidx < args.size(); argidx++) { + if (args[argidx] == "-max-slots" && argidx + 1 < args.size()) { + max_slots = std::stoi(args[++argidx]); continue; + } + if (args[argidx] == "-max-sources" && argidx + 1 < args.size()) { + max_sources = std::stoi(args[++argidx]); continue; + } + if (args[argidx] == "-strict") { + strict = true; continue; + } + break; + } + extra_args(args, argidx, design); + + int total_regions = 0, total_cells = 0; + for (auto module : design->selected_modules()) { + OptPrioKeyWorker worker(module); + worker.max_slots = max_slots; + worker.max_chain = max_sources; + worker.strict = strict; + worker.run(); + total_regions += worker.regions_rewritten; + total_cells += worker.cells_added; + } + + log("Rewrote %d dynamic key-read(s); emitted %d new cell(s).\n", + total_regions, total_cells); + + if (total_regions) + Yosys::run_pass("clean -purge"); + } +} OptPrioKeyPass; + +PRIVATE_NAMESPACE_END diff --git a/passes/opt/opt_priority_onehot.cc b/passes/opt/opt_priority_onehot.cc index 180a6cb33..eadc7615e 100644 --- a/passes/opt/opt_priority_onehot.cc +++ b/passes/opt/opt_priority_onehot.cc @@ -256,7 +256,7 @@ struct OptPriorityOnehotWorker : CutRegionWorker { bool fingerprint(const Candidate &cand, bool msb_first) { - ConstEval ce(module); + ConstEval &ce = shared_ce(); SigSpec out_sig = sigmap(cand.out_sig); SigSpec valid_sig = sigmap(cand.valid_sig); SigSpec field_sig = sigmap(cand.field_sig); diff --git a/passes/sat/async2sync.cc b/passes/sat/async2sync.cc index be2355e00..316f672a9 100644 --- a/passes/sat/async2sync.cc +++ b/passes/sat/async2sync.cc @@ -95,27 +95,27 @@ struct Async2syncPass : public Pass { if (trg_width == 0) { if (initstate == State::S0) - initstate = module->Initstate(NEW_ID); + initstate = module->Initstate(NEW_ID2_SUFFIX("initstate")); // SILIMATE: Improve the naming SigBit sig_en = cell->getPort(ID::EN); - cell->setPort(ID::EN, module->And(NEW_ID, sig_en, initstate)); + cell->setPort(ID::EN, module->And(NEW_ID2_SUFFIX("en_init"), sig_en, initstate)); // SILIMATE: Improve the naming } else { SigBit sig_en = cell->getPort(ID::EN); SigSpec sig_args = cell->getPort(ID::ARGS); bool trg_polarity = cell->getParam(ID(TRG_POLARITY)).as_bool(); SigBit sig_trg = cell->getPort(ID::TRG); - Wire *sig_en_q = module->addWire(NEW_ID); - Wire *sig_args_q = module->addWire(NEW_ID, GetSize(sig_args)); + Wire *sig_en_q = module->addWire(NEW_ID2_SUFFIX("en_q")); // SILIMATE: Improve the naming + Wire *sig_args_q = module->addWire(NEW_ID2_SUFFIX("args_q"), GetSize(sig_args)); // SILIMATE: Improve the naming sig_en_q->attributes.emplace(ID::init, State::S0); - module->addDff(NEW_ID, sig_trg, sig_en, sig_en_q, trg_polarity, cell->get_src_attribute()); - module->addDff(NEW_ID, sig_trg, sig_args, sig_args_q, trg_polarity, cell->get_src_attribute()); + module->addDff(NEW_ID2_SUFFIX("en_dff"), sig_trg, sig_en, sig_en_q, trg_polarity, cell->get_src_attribute()); // SILIMATE: Improve the naming + module->addDff(NEW_ID2_SUFFIX("args_dff"), sig_trg, sig_args, sig_args_q, trg_polarity, cell->get_src_attribute()); // SILIMATE: Improve the naming cell->setPort(ID::EN, sig_en_q); cell->setPort(ID::ARGS, sig_args_q); if (cell->type == ID($check)) { SigBit sig_a = cell->getPort(ID::A); - Wire *sig_a_q = module->addWire(NEW_ID); + Wire *sig_a_q = module->addWire(NEW_ID2_SUFFIX("a_q")); // SILIMATE: Improve the naming sig_a_q->attributes.emplace(ID::init, State::S1); - module->addDff(NEW_ID, sig_trg, sig_a, sig_a_q, trg_polarity, cell->get_src_attribute()); + module->addDff(NEW_ID2_SUFFIX("a_dff"), sig_trg, sig_a, sig_a_q, trg_polarity, cell->get_src_attribute()); // SILIMATE: Improve the naming cell->setPort(ID::A, sig_a_q); } } @@ -152,8 +152,8 @@ struct Async2syncPass : public Pass { initvals.remove_init(ff.sig_q); - Wire *new_d = module->addWire(NEW_ID, ff.width); - Wire *new_q = module->addWire(NEW_ID, ff.width); + Wire *new_d = module->addWire(NEW_ID2_SUFFIX("new_d"), ff.width); // SILIMATE: Improve the naming + Wire *new_q = module->addWire(NEW_ID2_SUFFIX("new_q"), ff.width); // SILIMATE: Improve the naming SigSpec sig_set = ff.sig_set; SigSpec sig_clr = ff.sig_clr; @@ -161,21 +161,21 @@ struct Async2syncPass : public Pass { if (!ff.pol_set) { if (!ff.is_fine) - sig_set = module->Not(NEW_ID, sig_set); + sig_set = module->Not(NEW_ID2_SUFFIX("set_hi"), sig_set); // SILIMATE: Improve the naming else - sig_set = module->NotGate(NEW_ID, sig_set); + sig_set = module->NotGate(NEW_ID2_SUFFIX("set_hi"), sig_set); // SILIMATE: Improve the naming } if (ff.pol_clr) { if (!ff.is_fine) - sig_clr_inv = module->Not(NEW_ID, sig_clr); + sig_clr_inv = module->Not(NEW_ID2_SUFFIX("clr_inv"), sig_clr); // SILIMATE: Improve the naming else - sig_clr_inv = module->NotGate(NEW_ID, sig_clr); + sig_clr_inv = module->NotGate(NEW_ID2_SUFFIX("clr_inv"), sig_clr); // SILIMATE: Improve the naming } else { if (!ff.is_fine) - sig_clr = module->Not(NEW_ID, sig_clr); + sig_clr = module->Not(NEW_ID2_SUFFIX("clr_hi"), sig_clr); // SILIMATE: Improve the naming else - sig_clr = module->NotGate(NEW_ID, sig_clr); + sig_clr = module->NotGate(NEW_ID2_SUFFIX("clr_hi"), sig_clr); // SILIMATE: Improve the naming } // At this point, sig_set and sig_clr are now unconditionally @@ -183,26 +183,26 @@ struct Async2syncPass : public Pass { SigSpec set_and_clr; if (!ff.is_fine) - set_and_clr = module->And(NEW_ID, sig_set, sig_clr); + set_and_clr = module->And(NEW_ID2_SUFFIX("set_and_clr"), sig_set, sig_clr); // SILIMATE: Improve the naming else - set_and_clr = module->AndGate(NEW_ID, sig_set, sig_clr); + set_and_clr = module->AndGate(NEW_ID2_SUFFIX("set_and_clr"), sig_set, sig_clr); // SILIMATE: Improve the naming if (!ff.is_fine) { - SigSpec tmp = module->Or(NEW_ID, ff.sig_d, sig_set); - tmp = module->And(NEW_ID, tmp, sig_clr_inv); - module->addBwmux(NEW_ID, tmp, Const(State::Sx, ff.width), set_and_clr, new_d); + SigSpec tmp = module->Or(NEW_ID2_SUFFIX("d_or_set"), ff.sig_d, sig_set); // SILIMATE: Improve the naming + tmp = module->And(NEW_ID2_SUFFIX("d_masked"), tmp, sig_clr_inv); // SILIMATE: Improve the naming + module->addBwmux(NEW_ID2_SUFFIX("d_bwmux"), tmp, Const(State::Sx, ff.width), set_and_clr, new_d); // SILIMATE: Improve the naming - tmp = module->Or(NEW_ID, new_q, sig_set); - tmp = module->And(NEW_ID, tmp, sig_clr_inv); - module->addBwmux(NEW_ID, tmp, Const(State::Sx, ff.width), set_and_clr, ff.sig_q); + tmp = module->Or(NEW_ID2_SUFFIX("q_or_set"), new_q, sig_set); // SILIMATE: Improve the naming + tmp = module->And(NEW_ID2_SUFFIX("q_masked"), tmp, sig_clr_inv); // SILIMATE: Improve the naming + module->addBwmux(NEW_ID2_SUFFIX("q_bwmux"), tmp, Const(State::Sx, ff.width), set_and_clr, ff.sig_q); // SILIMATE: Improve the naming } else { - SigSpec tmp = module->OrGate(NEW_ID, ff.sig_d, sig_set); - tmp = module->AndGate(NEW_ID, tmp, sig_clr_inv); - module->addMuxGate(NEW_ID, tmp, State::Sx, set_and_clr, new_d); + SigSpec tmp = module->OrGate(NEW_ID2_SUFFIX("d_or_set"), ff.sig_d, sig_set); // SILIMATE: Improve the naming + tmp = module->AndGate(NEW_ID2_SUFFIX("d_masked"), tmp, sig_clr_inv); // SILIMATE: Improve the naming + module->addMuxGate(NEW_ID2_SUFFIX("d_mux"), tmp, State::Sx, set_and_clr, new_d); // SILIMATE: Improve the naming - tmp = module->OrGate(NEW_ID, new_q, sig_set); - tmp = module->AndGate(NEW_ID, tmp, sig_clr_inv); - module->addMuxGate(NEW_ID, tmp, State::Sx, set_and_clr, ff.sig_q); + tmp = module->OrGate(NEW_ID2_SUFFIX("q_or_set"), new_q, sig_set); // SILIMATE: Improve the naming + tmp = module->AndGate(NEW_ID2_SUFFIX("q_masked"), tmp, sig_clr_inv); // SILIMATE: Improve the naming + module->addMuxGate(NEW_ID2_SUFFIX("q_mux"), tmp, State::Sx, set_and_clr, ff.sig_q); // SILIMATE: Improve the naming } ff.sig_d = new_d; @@ -217,24 +217,24 @@ struct Async2syncPass : public Pass { initvals.remove_init(ff.sig_q); - Wire *new_d = module->addWire(NEW_ID, ff.width); - Wire *new_q = module->addWire(NEW_ID, ff.width); + Wire *new_d = module->addWire(NEW_ID2_SUFFIX("new_d"), ff.width); // SILIMATE: Improve the naming + Wire *new_q = module->addWire(NEW_ID2_SUFFIX("new_q"), ff.width); // SILIMATE: Improve the naming if (ff.pol_aload) { if (!ff.is_fine) { - module->addMux(NEW_ID, new_q, ff.sig_ad, ff.sig_aload, ff.sig_q); - module->addMux(NEW_ID, ff.sig_d, ff.sig_ad, ff.sig_aload, new_d); + module->addMux(NEW_ID2_SUFFIX("q_aload_mux"), new_q, ff.sig_ad, ff.sig_aload, ff.sig_q); // SILIMATE: Improve the naming + module->addMux(NEW_ID2_SUFFIX("d_aload_mux"), ff.sig_d, ff.sig_ad, ff.sig_aload, new_d); // SILIMATE: Improve the naming } else { - module->addMuxGate(NEW_ID, new_q, ff.sig_ad, ff.sig_aload, ff.sig_q); - module->addMuxGate(NEW_ID, ff.sig_d, ff.sig_ad, ff.sig_aload, new_d); + module->addMuxGate(NEW_ID2_SUFFIX("q_aload_mux"), new_q, ff.sig_ad, ff.sig_aload, ff.sig_q); // SILIMATE: Improve the naming + module->addMuxGate(NEW_ID2_SUFFIX("d_aload_mux"), ff.sig_d, ff.sig_ad, ff.sig_aload, new_d); // SILIMATE: Improve the naming } } else { if (!ff.is_fine) { - module->addMux(NEW_ID, ff.sig_ad, new_q, ff.sig_aload, ff.sig_q); - module->addMux(NEW_ID, ff.sig_ad, ff.sig_d, ff.sig_aload, new_d); + module->addMux(NEW_ID2_SUFFIX("q_aload_mux"), ff.sig_ad, new_q, ff.sig_aload, ff.sig_q); // SILIMATE: Improve the naming + module->addMux(NEW_ID2_SUFFIX("d_aload_mux"), ff.sig_ad, ff.sig_d, ff.sig_aload, new_d); // SILIMATE: Improve the naming } else { - module->addMuxGate(NEW_ID, ff.sig_ad, new_q, ff.sig_aload, ff.sig_q); - module->addMuxGate(NEW_ID, ff.sig_ad, ff.sig_d, ff.sig_aload, new_d); + module->addMuxGate(NEW_ID2_SUFFIX("q_aload_mux"), ff.sig_ad, new_q, ff.sig_aload, ff.sig_q); // SILIMATE: Improve the naming + module->addMuxGate(NEW_ID2_SUFFIX("d_aload_mux"), ff.sig_ad, ff.sig_d, ff.sig_aload, new_d); // SILIMATE: Improve the naming } } @@ -250,18 +250,18 @@ struct Async2syncPass : public Pass { initvals.remove_init(ff.sig_q); - Wire *new_q = module->addWire(NEW_ID, ff.width); + Wire *new_q = module->addWire(NEW_ID2_SUFFIX("new_q"), ff.width); // SILIMATE: Improve the naming if (ff.pol_arst) { if (!ff.is_fine) - module->addMux(NEW_ID, new_q, ff.val_arst, ff.sig_arst, ff.sig_q); + module->addMux(NEW_ID2_SUFFIX("arst_mux"), new_q, ff.val_arst, ff.sig_arst, ff.sig_q); // SILIMATE: Improve the naming else - module->addMuxGate(NEW_ID, new_q, ff.val_arst[0], ff.sig_arst, ff.sig_q); + module->addMuxGate(NEW_ID2_SUFFIX("arst_mux"), new_q, ff.val_arst[0], ff.sig_arst, ff.sig_q); // SILIMATE: Improve the naming } else { if (!ff.is_fine) - module->addMux(NEW_ID, ff.val_arst, new_q, ff.sig_arst, ff.sig_q); + module->addMux(NEW_ID2_SUFFIX("arst_mux"), ff.val_arst, new_q, ff.sig_arst, ff.sig_q); // SILIMATE: Improve the naming else - module->addMuxGate(NEW_ID, ff.val_arst[0], new_q, ff.sig_arst, ff.sig_q); + module->addMuxGate(NEW_ID2_SUFFIX("arst_mux"), ff.val_arst[0], new_q, ff.sig_arst, ff.sig_q); // SILIMATE: Improve the naming } ff.sig_q = new_q; @@ -284,21 +284,21 @@ struct Async2syncPass : public Pass { initvals.remove_init(ff.sig_q); - Wire *new_q = module->addWire(NEW_ID, ff.width); + Wire *new_q = module->addWire(NEW_ID2_SUFFIX("new_q"), ff.width); // SILIMATE: Improve the naming Wire *new_d; if (ff.has_aload) { - new_d = module->addWire(NEW_ID, ff.width); + new_d = module->addWire(NEW_ID2_SUFFIX("new_d"), ff.width); // SILIMATE: Improve the naming if (ff.pol_aload) { if (!ff.is_fine) - module->addMux(NEW_ID, new_q, ff.sig_ad, ff.sig_aload, new_d); + module->addMux(NEW_ID2_SUFFIX("d_aload_mux"), new_q, ff.sig_ad, ff.sig_aload, new_d); // SILIMATE: Improve the naming else - module->addMuxGate(NEW_ID, new_q, ff.sig_ad, ff.sig_aload, new_d); + module->addMuxGate(NEW_ID2_SUFFIX("d_aload_mux"), new_q, ff.sig_ad, ff.sig_aload, new_d); // SILIMATE: Improve the naming } else { if (!ff.is_fine) - module->addMux(NEW_ID, ff.sig_ad, new_q, ff.sig_aload, new_d); + module->addMux(NEW_ID2_SUFFIX("d_aload_mux"), ff.sig_ad, new_q, ff.sig_aload, new_d); // SILIMATE: Improve the naming else - module->addMuxGate(NEW_ID, ff.sig_ad, new_q, ff.sig_aload, new_d); + module->addMuxGate(NEW_ID2_SUFFIX("d_aload_mux"), ff.sig_ad, new_q, ff.sig_aload, new_d); // SILIMATE: Improve the naming } } else { new_d = new_q; @@ -310,36 +310,36 @@ struct Async2syncPass : public Pass { if (!ff.pol_set) { if (!ff.is_fine) - sig_set = module->Not(NEW_ID, sig_set); + sig_set = module->Not(NEW_ID2_SUFFIX("set_hi"), sig_set); // SILIMATE: Improve the naming else - sig_set = module->NotGate(NEW_ID, sig_set); + sig_set = module->NotGate(NEW_ID2_SUFFIX("set_hi"), sig_set); // SILIMATE: Improve the naming } if (ff.pol_clr) { if (!ff.is_fine) - sig_clr = module->Not(NEW_ID, sig_clr); + sig_clr = module->Not(NEW_ID2_SUFFIX("clr_lo"), sig_clr); // SILIMATE: Improve the naming else - sig_clr = module->NotGate(NEW_ID, sig_clr); + sig_clr = module->NotGate(NEW_ID2_SUFFIX("clr_lo"), sig_clr); // SILIMATE: Improve the naming } if (!ff.is_fine) { - SigSpec tmp = module->Or(NEW_ID, new_d, sig_set); - module->addAnd(NEW_ID, tmp, sig_clr, ff.sig_q); + SigSpec tmp = module->Or(NEW_ID2_SUFFIX("d_or_set"), new_d, sig_set); // SILIMATE: Improve the naming + module->addAnd(NEW_ID2_SUFFIX("q_sr"), tmp, sig_clr, ff.sig_q); // SILIMATE: Improve the naming } else { - SigSpec tmp = module->OrGate(NEW_ID, new_d, sig_set); - module->addAndGate(NEW_ID, tmp, sig_clr, ff.sig_q); + SigSpec tmp = module->OrGate(NEW_ID2_SUFFIX("d_or_set"), new_d, sig_set); // SILIMATE: Improve the naming + module->addAndGate(NEW_ID2_SUFFIX("q_sr"), tmp, sig_clr, ff.sig_q); // SILIMATE: Improve the naming } } else if (ff.has_arst) { if (ff.pol_arst) { if (!ff.is_fine) - module->addMux(NEW_ID, new_d, ff.val_arst, ff.sig_arst, ff.sig_q); + module->addMux(NEW_ID2_SUFFIX("arst_mux"), new_d, ff.val_arst, ff.sig_arst, ff.sig_q); // SILIMATE: Improve the naming else - module->addMuxGate(NEW_ID, new_d, ff.val_arst[0], ff.sig_arst, ff.sig_q); + module->addMuxGate(NEW_ID2_SUFFIX("arst_mux"), new_d, ff.val_arst[0], ff.sig_arst, ff.sig_q); // SILIMATE: Improve the naming } else { if (!ff.is_fine) - module->addMux(NEW_ID, ff.val_arst, new_d, ff.sig_arst, ff.sig_q); + module->addMux(NEW_ID2_SUFFIX("arst_mux"), ff.val_arst, new_d, ff.sig_arst, ff.sig_q); // SILIMATE: Improve the naming else - module->addMuxGate(NEW_ID, ff.val_arst[0], new_d, ff.sig_arst, ff.sig_q); + module->addMuxGate(NEW_ID2_SUFFIX("arst_mux"), ff.val_arst[0], new_d, ff.sig_arst, ff.sig_q); // SILIMATE: Improve the naming } } else { module->connect(ff.sig_q, new_d); diff --git a/passes/sat/sim.cc b/passes/sat/sim.cc index 503e2dcb7..03c21649b 100644 --- a/passes/sat/sim.cc +++ b/passes/sat/sim.cc @@ -658,6 +658,13 @@ struct SimInstance else if (has_a && has_b && !has_c && !has_d && has_s && has_y) // (A,B,S -> Y) cells eval_state = CellTypes::eval(cell, get_state(sig_a), get_state(sig_b), get_state(sig_s), &err); + else if (has_a && has_b && has_c && has_d && !has_s && has_y) + // (A,B,C,D -> Y) cells, e.g. $_AOI4_/$_OAI4_ (CellTypes::eval implements these); + // without this case every 4-input gate fell through to the "unsupported" warning + // below, flooding the log (millions of lines on AOI/OAI-dense designs like AES) + // and leaving the output stuck at X. + eval_state = CellTypes::eval(cell, get_state(sig_a), get_state(sig_b), + get_state(sig_c), get_state(sig_d), &err); else err = true; diff --git a/passes/silimate/CMakeLists.txt b/passes/silimate/CMakeLists.txt index 734a5a7cd..f948202fd 100644 --- a/passes/silimate/CMakeLists.txt +++ b/passes/silimate/CMakeLists.txt @@ -50,6 +50,9 @@ yosys_pass(splitlarge yosys_pass(splitnetlist splitnetlist.cc ) +yosys_pass(carvenetlist + carvenetlist.cc +) yosys_pass(opt_timing_balance opt_timing_balance.cc ) diff --git a/passes/silimate/carvenetlist.cc b/passes/silimate/carvenetlist.cc new file mode 100644 index 000000000..4e99c4486 --- /dev/null +++ b/passes/silimate/carvenetlist.cc @@ -0,0 +1,1045 @@ +/* + * yosys -- Yosys Open SYnthesis Suite + * + * Copyright (C) 2025 Silimate Inc. + * + * 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. + * + */ + +#include "kernel/sigtools.h" +#include "kernel/yosys.h" +#include +#include + +USING_YOSYS_NAMESPACE +PRIVATE_NAMESPACE_BEGIN + +// Strip a leading RTLIL "\" escape from an IdString's text. +static std::string unescape(const std::string &s) { return (!s.empty() && s[0] == '\\') ? s.substr(1) : s; } + +// A buffer/inverter has exactly one input and one output pin (a 2-port cell). Synthesis +// only ever inserts such pass-throughs (drive-strength buffers/inverters) on the single +// net between a train top port and its surrounding flop, so hopping over them reaches the +// real boundary flop regardless of how many were added; any flop/gate has >2 ports. +static bool is_passthrough(Cell *cell) +{ + int n_in = 0, n_out = 0; + for (auto &conn : cell->connections()) { + if (cell->input(conn.first)) { + if (++n_in > 1) + return false; + } else if (cell->output(conn.first)) { + if (++n_out > 1) + return false; + } + } + return n_in == 1 && n_out == 1; +} + +// True if a port's trailing "_" segment names a clock (clk/clock, case-insensitive), +// so shared/per-cell clock ports aren't treated as data inputs. +static bool is_clock_pin(const std::string &pin) +{ + std::string p; + for (char c : pin) + p += std::tolower((unsigned char)c); + return p.find("clk") != std::string::npos || p.find("clock") != std::string::npos; +} + +// True if a flop input pin is an (a)synchronous set/reset/clear/preset/load control pin +// (case-insensitive). Such pins are not the data (D) input, so they must be skipped when +// finding the captured-data pin: an async set/reset flop (e.g. ASYNC_DFFHx1 with pins +// D/RESET/SET/CLK, used by adff_surround) would otherwise let a RESET/SET net (the shared +// arst) be mistaken for the data pin, tying the carved cell's output to arst. +static bool is_set_reset_pin(const std::string &pin) +{ + std::string p; + for (char c : pin) + p += std::tolower((unsigned char)c); + for (const char *k : {"reset", "set", "clr", "clear", "arst", "srst", "aload", "sload"}) + if (p.find(k) != std::string::npos) + return true; + return false; +} + +// Map a carved cell's port-base name to the Preqorsor dataset naming convention: carved +// cells come in as slow_/fast_ and OpenIP designs as slow_design_/ +// fast_design_, but downstream parsing/decryption expects and design__ +// . The design_ rewrites run first so "slow_"/"fast_" can't clip them. +static std::string pq_rename(const std::string &base) +{ + if (base.rfind("slow_design_", 0) == 0) + return "design_slow_" + base.substr(12); + if (base.rfind("fast_design_", 0) == 0) + return "design_fast_" + base.substr(12); + if (base.rfind("slow_", 0) == 0) + return base.substr(5); + if (base.rfind("fast_", 0) == 0) + return base.substr(5); + return base; +} + +struct BoundaryRec { + std::string speed, driving_cell, driving_pin, load_cell, load_pin; +}; + +struct CarveNetlistPass : public Pass { + CarveNetlistPass() + : Pass("carvenetlist", "carve surround-with-flops cells into per-cell modules and record their flop boundary") + { + } + void help() override + { + // |---v---|---v---|---v---|---v---|---v---|---v---|---v---|---v---|---v---|---v---| + log("\n"); + log(" carvenetlist\n"); + log("\n"); + log("Carve each surround-with-flops cell-under-test out of the flat 'train' module into\n"); + log("its own flop-free module and record the surrounding flops as that cell's boundary\n"); + log("conditions.\n"); + log("\n"); + log("The training netlist is one giant flat module (\"train\") of independent\n"); + log("cells-under-test, each surrounded by flip-flops (see ml.dataset.surround_with_flops):\n"); + log("every data input is driven by a launch flop's Q and every data output is captured\n"); + log("by a load flop's D, with two shared clocks (fast_clk/slow_clk). This pass carves\n"); + log("each cell-under-test out into its own flop-free module (named after the dataset\n"); + log("cell) and records the surrounding flops as \\pq_* string attributes on the carved\n"); + log("module (\\pq_speed, \\pq_driving_cell, \\pq_driving_pin, \\pq_load_cell,\n"); + log("\\pq_load_pin), so OpenSTA can re-impose them (set_driving_cell on the inputs,\n"); + log("set_load resolved from the load flop's data pin on the outputs).\n"); + log("\n"); + log("Anchored on the train top-level ports (the only structure guaranteed to survive\n"); + log("synthesis), grouped by cell base name \"_\":\n"); + log("\n"); + log(" - launch flop = hop forward from each data input port through any\n"); + log(" drive-strength buffers/inverters (1-in/1-out cells) to the\n"); + log(" first >2-port cell;\n"); + log(" - capture flop = the mirror backward hop from each data output port;\n"); + log(" - cell-under-test = the logic cone walked forward from the launch flops'\n"); + log(" outputs, excluding the boundary flops; tagged \\submod and\n"); + log(" carved by submod.\n"); + log("\n"); + log("The shared clock network (fast_clk/slow_clk and their buffer tree) is treated as\n"); + log("a hard boundary: cones never expand through it, so a carve can't leak across the\n"); + log("clock into another cell's surrounding flops.\n"); + log("\n"); + } + + void execute(std::vector args, RTLIL::Design *design) override + { + log_header(design, "Executing CARVENETLIST pass (carve surround-with-flops cells into " + "per-cell modules and record their flop boundary).\n"); + + size_t argidx = 1; + extra_args(args, argidx, design); + + Module *train = design->module(ID(train)); + if (train == nullptr) { + log_warning("No 'train' module found; nothing to carve.\n"); + return; + } + + // Marks the zero-area $_BUF_ cells we insert at the capture-flop boundary (below) so + // they can be turned back into plain assigns after carving. + const IdString PQ_PASS = RTLIL::escape_id("pq_passthrough"); + + // Marks the reconstructed per-pin output bus wire ("__pqcarve._") so the + // port-name cleanup can tell it apart from a colliding stale "__pqo" cone-output + // fragment (which must be demoted to an internal net, not aliased onto the output). + const IdString PQ_CARVE_OUT = RTLIL::escape_id("pq_carve_out"); + + SigMap sigmap(train); + + // Re-root *only* fused feed-through net groups toward their launch-side (input) net. + // A feed-through output bit (Y[i] = some input) lets synthesis fuse the launch-flop + // input net ("...___pqi[j]") and the capture-flop output net + // ("...___pqo[i]") into one SigMap group. Whichever bit SigMap happens to elect + // as the group rep is the net the carve reads and reconstructs from; if it elects the + // "__pqo" output fragment, that fragment is only *driven* by the launch flop (a + // boundary cell carved away), so the cone reads it but nothing drives it -> floating + // bit + inout output bus. Promote the "__pqi" bit, but ONLY for groups that also hold + // a "__pqo" bit (true feed-throughs). Promoting every "__pqi" net would re-root the + // rep of ordinary input groups too, churning internal net names across nearly every + // carved cell (and needlessly invalidating their cached power VCDs). + { + dict pqi_bit_of_group; // group rep -> a "__pqi" bit in that group + pool groups_with_pqo; // group reps that contain a "__pqo" bit + for (auto wire : train->wires()) { + std::string nm = unescape(wire->name.str()); + bool is_pqi = nm.find("__pqi") != std::string::npos; + bool is_pqo = nm.find("__pqo") != std::string::npos; + if (!is_pqi && !is_pqo) + continue; + for (auto bit : SigSpec(wire)) { + SigBit r = sigmap(bit); + if (r.wire == nullptr) + continue; + if (is_pqi && !pqi_bit_of_group.count(r)) + pqi_bit_of_group[r] = bit; + if (is_pqo) + groups_with_pqo.insert(r); + } + } + for (auto &it : pqi_bit_of_group) + if (groups_with_pqo.count(it.first)) + sigmap.add(it.second); + } + + // Instance-level driver/load maps over canonicalized nets. Keyed by net bit so the + // surrounding flops can be found by topology and excluded from the carve. + dict> driver; // net -> (cell, out pin) + dict>> loads; // net -> [(cell, in pin)] + dict> cell_out_bits, cell_in_bits; + for (auto cell : train->cells()) { + for (auto &conn : cell->connections()) { + bool is_out = cell->output(conn.first); + bool is_in = cell->input(conn.first); + for (auto bit : sigmap(conn.second)) { + if (bit.wire == nullptr) + continue; // skip constants + if (is_out) { + driver[bit] = {cell, conn.first}; + cell_out_bits[cell].push_back(bit); + } + if (is_in) { + loads[bit].push_back({cell, conn.first}); + cell_in_bits[cell].push_back(bit); + } + } + } + } + + // Identify the shared clock distribution network. Every cell-under-test is driven + // by the same two top-level clocks (fast_clk/slow_clk, named so by + // ml.dataset.surround_with_flops), so the clock tree is the one net set that + // connects otherwise-independent cells: its leaf nets fan out to the CLK pin of + // every flop across every cell/design. Seed from the clock top ports and propagate + // forward only through pass-through clock buffers (stopping at flops). Excluding + // these nets from the cone below keeps each carve bounded by data connectivity + // alone, so it can never leak across the shared clock into a neighbor's flops. + pool clk_nets; + { + std::vector wl; + for (auto wire : train->wires()) { + if (!wire->port_input && !wire->port_output) + continue; + std::string name = unescape(wire->name.str()); + auto us = name.rfind('_'); + std::string pin = (us == std::string::npos) ? name : name.substr(us + 1); + if (!is_clock_pin(pin)) + continue; + for (auto bit : sigmap(SigSpec(wire))) + if (bit.wire) + wl.push_back(bit); + } + while (!wl.empty()) { + SigBit b = wl.back(); + wl.pop_back(); + if (clk_nets.count(b)) + continue; + clk_nets.insert(b); + auto it = loads.find(b); + if (it == loads.end()) + continue; + for (auto &pr : it->second) { + if (!is_passthrough(pr.first)) + continue; // stop at flops; only follow clock buffers + for (auto ob : cell_out_bits[pr.first]) + wl.push_back(ob); + } + } + } + + // Group top-level data ports by cell base name (skip clock ports). out_wires keeps the + // actual output port wires (not just their sigmapped bits) so the carved output bus + // can be rebuilt one clean port per pin at the capture-flop boundary below. + struct Group { + std::vector ins, outs; + std::vector in_wires, out_wires; + }; + std::map groups; + for (auto wire : train->wires()) { + if (!wire->port_input && !wire->port_output) + continue; + std::string name = unescape(wire->name.str()); + auto us = name.rfind('_'); + if (us == std::string::npos || us == 0) + continue; + std::string pin = name.substr(us + 1); + std::string base = name.substr(0, us); + if (base.empty() || is_clock_pin(pin)) + continue; + Group &g = groups[base]; + if (wire->port_output) + g.out_wires.push_back(wire); + else + g.in_wires.push_back(wire); + for (auto bit : sigmap(SigSpec(wire))) { + if (bit.wire == nullptr) + continue; + if (wire->port_input) + g.ins.push_back(bit); + else + g.outs.push_back(bit); + } + } + + // Forward-hop from an input-port net through buffers to the launch flop (or null) + auto hop_fwd = [&](SigBit bit) -> Cell * { + pool seen; + while (true) { + if (clk_nets.count(bit)) + return nullptr; // never walk into the shared clock network + auto it = loads.find(bit); + if (it == loads.end()) + return nullptr; + Cell *c = nullptr; + for (auto &pr : it->second) { + if (c && pr.first != c) + return nullptr; // fork: no clean single boundary + c = pr.first; + } + if (c == nullptr || seen.count(c)) + return nullptr; + seen.insert(c); + if (!is_passthrough(c)) + return c; + auto &outs = cell_out_bits[c]; + if (outs.size() != 1) + return nullptr; + bit = outs[0]; + } + }; + // Backward-hop from an output-port net through buffers to the capture flop (or null) + auto hop_back = [&](SigBit bit) -> Cell * { + pool seen; + while (true) { + if (clk_nets.count(bit)) + return nullptr; // never walk into the shared clock network + auto it = driver.find(bit); + if (it == driver.end()) + return nullptr; + Cell *c = it->second.first; + if (seen.count(c)) + return nullptr; + seen.insert(c); + if (!is_passthrough(c)) + return c; + auto &ins = cell_in_bits[c]; + if (ins.size() != 1) + return nullptr; + bit = ins[0]; + } + }; + + std::map boundary; + std::map final_of_base; // carved module name -> the base it came from + int tagged_total = 0; + for (auto &grp : groups) { + const std::string &base = grp.first; + Group &g = grp.second; + + // Launch flops drive the inputs; hop through buffers, seed the cone from the + // flop output that drives the cell, and record (type, out-pin) as the driver. + pool launch, capture, boundary_cells; + pool seed; + std::string drv_cell, drv_pin; + for (auto bit : g.ins) { + Cell *flop = hop_fwd(bit); + if (flop == nullptr) + continue; + launch.insert(flop); + for (auto ob : cell_out_bits[flop]) { + if (clk_nets.count(ob)) + continue; // a launch flop's data output, never a generated clock + if (!loads.count(ob)) + continue; // only the flop output that actually drives the cell + if (drv_cell.empty()) { + drv_cell = unescape(flop->type.str()); + drv_pin = unescape(driver[ob].second.str()); + } + seed.insert(ob); + } + } + // Capture flops load the outputs; hop back through buffers to the real flop. + for (auto bit : g.outs) { + Cell *flop = hop_back(bit); + if (flop != nullptr) + capture.insert(flop); + } + boundary_cells = launch; + for (auto c : capture) + boundary_cells.insert(c); + + // Walk the logic cone forward from the launch outputs, excluding boundary flops + pool logic; + pool driven = seed; + std::vector stack(seed.begin(), seed.end()); + while (!stack.empty()) { + SigBit k = stack.back(); + stack.pop_back(); + if (clk_nets.count(k)) + continue; // hard boundary: never expand through the shared clock tree + auto it = loads.find(k); + if (it == loads.end()) + continue; + for (auto &pr : it->second) { + Cell *c = pr.first; + if (boundary_cells.count(c) || logic.count(c)) + continue; + logic.insert(c); + for (auto ob : cell_out_bits[c]) { + if (clk_nets.count(ob)) + continue; // don't follow a cell's generated/forwarded clock + driven.insert(ob); + stack.push_back(ob); + } + } + } + + // Redirect cone-cell inputs that physically read a fused feed-through "__pqo" + // fragment to that group's launch-side rep (an input net, after the promotion + // above). The "__pqo" fragment is driven only by the launch flop, which is carved + // away as a boundary cell, so a cone cell left wired to it would read a floating + // net. Restrict the rewrite to bits whose wire is "__pqo"-named so ordinary cone + // reads keep their original net (no cosmetic net-name churn across cells). + for (auto c : logic) { + dict rewire; + for (auto &conn : c->connections()) { + if (!c->input(conn.first)) + continue; + SigSpec cs = conn.second; + bool changed = false; + for (auto &bit : cs) { + if (bit.wire == nullptr || + unescape(bit.wire->name.str()).find("__pqo") == std::string::npos) + continue; + SigBit r = sigmap(bit); + if (r != bit) { + bit = r; + changed = true; + } + } + if (changed) + rewire[conn.first] = cs; + } + for (auto &r : rewire) + c->setPort(r.first, r.second); + } + + // Tag the cell logic for submod. + std::string final = pq_rename(base); + // pq_rename strips the "slow_"/"fast_" speed prefix off plain cells, so a train + // holding both "slow_" and "fast_" would map both to "" -- carving + // two different cells into one module and silently overwriting the first cell's + // \pq_* boundary record with the second. Refuse rather than emit a wrong netlist. + if (final_of_base.count(final)) + log_error("carvenetlist: cell groups '%s' and '%s' both map to carved module " + "name '%s'; cannot carve two cells into one module.\n", + final_of_base.at(final).c_str(), base.c_str(), final.c_str()); + final_of_base[final] = base; + for (auto c : logic) + c->set_string_attribute(RTLIL::escape_id("submod"), final); + tagged_total += GetSize(logic); + + // A DESIGN cell-under-test (slow_design_/fast_design_) is a whole sequential design, + // not a single mapped cell; flag it so the self-containment pass below also rebuilds + // its launch-flop input boundary (step (1)), which a single-cell carve never needs. + bool is_design = base.rfind("slow_design_", 0) == 0 || base.rfind("fast_design_", 0) == 0; + + // Speed comes from the pre-rename base's "_" prefix ("fast_" covers both + // "fast_" and "fast_design_"). The post-rename `final` can't be used: + // pq_rename strips the speed prefix off plain cells, so `final` carries no speed. + std::string speed = base.rfind("fast_", 0) == 0 ? "fast" : "slow"; + + // Rebuild the output boundary at the capture flops. submod can only export a + // clean output port for a net driven *inside* the carve and read *outside* it. + // After synthesis the bit a capture flop captures often is not such a net: a + // degenerate cell (shifter/mod/div in const or var mode) collapses an output into + // a bare copy of an input (Y=A -- the flop reads the launch-flop net directly), a + // routed input bit, or a constant driven by an abc-mapped tie cell the forward + // cone walk can never reach. Left alone that output bit has no in-cone driver and + // drags its whole bus to inout, or the port vanishes. So, per output-port pin, + // build one fresh bus `cw` driven by a per-bit tagged $_BUF_ copying whatever the + // capture flop reads, then rewire the flop to read `cw`. The buffer's source sets + // the input side: an in-cone net stays internal, a launch-flop/boundary net + // becomes a clean input port, a constant stays constant, and an otherwise + // unreachable source cell (a tie) is cloned into the cell so its (tiny) power is + // characterized rather than stranded in the deleted train module. The carve wire + // is named "__pqcarve._" so the trailing port cleanup (which drops the + // scope-dot prefix) recovers the clean "_" port name. + std::string load_cell, load_pin; + for (auto pw : g.out_wires) { + Wire *cw = train->addWire( + RTLIL::escape_id("__pqcarve." + unescape(pw->name.str())), pw->width); + cw->set_bool_attribute(PQ_CARVE_OUT); + for (int i = 0; i < pw->width; i++) { + Cell *fc = hop_back(sigmap(SigBit(pw, i))); + if (fc == nullptr || !capture.count(fc)) + continue; + // Locate the flop's data pin (the lone input that is neither a clock nor a + // set/reset/clear control pin -- an async set/reset flop like ASYNC_DFFHx1 + // also exposes RESET/SET inputs that must not be mistaken for D). + IdString dpin; + int dpos = -1; + SigBit draw; + for (auto &conn : fc->connections()) { + if (!fc->input(conn.first) || is_clock_pin(unescape(conn.first.str())) || + is_set_reset_pin(unescape(conn.first.str()))) + continue; + const SigSpec &s = conn.second; + for (int j = 0; j < GetSize(s); j++) { + if (clk_nets.count(sigmap(s[j]))) + continue; + dpin = conn.first, dpos = j, draw = s[j]; + break; + } + if (dpos >= 0) + break; + } + if (dpos < 0) + continue; + if (load_cell.empty()) { + load_cell = unescape(fc->type.str()); + load_pin = unescape(dpin.str()); + } + // Pick the buffer's source: clone an otherwise-unreachable constant/source + // cell (e.g. a tie) into the cell so the output bit gains an in-cone + // driver; otherwise copy the net the flop reads as-is. + SigBit src = sigmap(draw); + if (src.wire != nullptr) { + auto dit = driver.find(src); + if (dit != driver.end()) { + Cell *dc = dit->second.first; + IdString opin = dit->second.second; + bool is_source = !cell_in_bits.count(dc) || cell_in_bits.at(dc).empty(); + // A genuine constant source (a tie): 0 data inputs, single-bit + // output, unreachable by the forward cone. Clone it into the cell + // (rather than steal the shared original) so the output bit gets an + // in-cone driver and the tie's power is characterized. + if (is_source && GetSize(dc->getPort(opin)) == 1 && + !logic.count(dc) && !boundary_cells.count(dc)) { + Cell *clone = train->addCell(NEW_ID, dc->type); + clone->parameters = dc->parameters; + // Copy every port (e.g. a $lut's zero-width A pin must exist for + // the cell to pass check) before redirecting the output to a + // fresh net. + for (auto &cc : dc->connections()) + clone->setPort(cc.first, cc.second); + Wire *tn = train->addWire(NEW_ID, 1); + clone->setPort(opin, SigSpec(tn)); + clone->set_string_attribute(RTLIL::escape_id("submod"), final); + src = SigBit(tn); + } + } + } + Cell *buf = train->addBufGate(NEW_ID, src, SigBit(cw, i)); + buf->set_string_attribute(RTLIL::escape_id("submod"), final); + buf->set_string_attribute(PQ_PASS, "1"); + // Keep the $_BUF_ alive through submod's internal clean (it would + // otherwise re-alias Y to A, re-merging the boundary); converted to a + // plain assign after the carve. + buf->set_bool_attribute(ID::keep); + std::vector dbits = fc->getPort(dpin).bits(); + dbits[dpos] = SigBit(cw, i); + fc->setPort(dpin, SigSpec(dbits)); + } + } + + // Self-containment repair, in two parts. A single mapped cell's cone is seeded from + // its launch-flop inputs and needs only part (2); a DESIGN cell-under-test + // (slow_design_/fast_design_) is a whole sequential design and also needs + // part (1). Left unrepaired, either leaks bogus top-level *input* ports out of the + // carved module (internal flattened nets, constants, clock-gate scan pins, undriven + // opt artifacts, ...), which both mis-drives the gate-level power testbench and escapes + // char.sdc's "slow_*"/"fast_*" data-port matching. + // (1) Primary-input naming: after flatten the launch-flop output net (the carve's + // data input) can keep an internal flattened name ("u0_key", "n_1598636") that + // won SigMap election over the "___pqi" boundary name. Per input pin, + // build a fresh "__pqcarve._" bus, copy the launch-flop output into it + // with a train-side (boundary) buffer, and rewire the cone to read it, so submod + // exports the input under the clean "_" name. A whole design renames + // every input; a plain cell's boundary usually survives clean already, so it only + // reconstructs a pin whose seed lost its "__pqi" name (the mirror of the output + // reconstruction below, which already runs for every cell). + // (2) ALL cells -- self-containment: a cone-cell input that reads a net driven from + // *outside* the cone leaks out as a spurious input port (submod can only export a + // port for a net driven inside the carve). The forward walk never reaches a + // constant/tie source (0-input cell, e.g. an async flop's inactive SET/RESET), an + // undriven internal net (inserted scan/"test" pin, opt artifact), or a gate fed + // only by such constants. Per cone-cell input: fold a name-recoverable tie + // (logic_0/logic_1 net) to a literal for a plain cell (area-neutral -- keeps a + // sequential cell at a single mapped instance), clone the source otherwise and + // for design cells (so a shared global tie can't drag in a neighbour's cone and + // the tie's tiny power is characterized in-cone), tie an undriven net to 0, and + // pull a forward-walk-missed gate into the cone. + // + // KNOWN LIMITATION (~3.6% of carved cells): a launch-flop seed that lost its "__pqi" + // name can still leak as a bare "n_" input port even after part (1) rebuilds the + // clean "__pqcarve._" bus -- the reconstructed port does not survive to the + // final module (the cone re-reads the flattened seed net). It concentrates in the WIDE + // cells (multi-bit shifters sshl/shl/sshr/shr, mul, mem, and the wider add/sub/div/mod) + // plus the fast aes/aes_inv key (a QN-output DFFHQNx1 launch flop, where the whole + // 128-bit key leaks). Triage narrowed it down: + // - it is deterministic, and the cone walk DOES reach the seed (launch flop found, + // output seeded, reader in the cone -- so it is NOT a cone-walk exclusion); + // - part (1) builds cwi and rewires the cone, but cwi is re-merged/dropped, so the + // final module still exports the "n_" seed. + // Pinpointing the exact step still needs instrumentation of the reconstruction REWRITE + // and the post-submod cleanup (not just the cone walk) to trace the cwi lifecycle. Such + // cells fall back to the model rather than a clean gate-level testbench. See + // docs/dev/carvenetlist_dev.md. + const IdString SUBMOD = RTLIL::escape_id("submod"); + dict in_remap; // launch-flop output bit -> clean input-port bit + // (1) Reconstruct clean input ports at the launch-flop boundary. A whole design's + // launch-flop outputs all keep internal flattened names and are always renamed; a + // plain cell's "__pqi" boundary name usually survives SigMap election, so only a pin + // whose seed net lost it (and would otherwise leak as a bare "n_" input port, + // the mirror of the output reconstruction below which already runs for every cell) is + // reconstructed. The clean majority is left untouched -- no net-name churn, and no + // risk to the QN-launch-flop cells whose reconstruction is known not to survive. + { + for (auto pw : g.in_wires) { + Wire *cwi = nullptr; + for (int i = 0; i < pw->width; i++) { + Cell *fc = hop_fwd(sigmap(SigBit(pw, i))); + if (fc == nullptr || !launch.count(fc)) + continue; + SigBit qi; + bool found = false; + for (auto ob : cell_out_bits[fc]) { + if (clk_nets.count(ob) || !loads.count(ob)) + continue; // the launch flop's data output that drives the cone + qi = ob; + found = true; + break; + } + if (!found) + continue; + if (!is_design) { + std::string qn = qi.wire ? unescape(qi.wire->name.str()) : ""; + if (qn.find("__pqi") != std::string::npos || + qn.find(base) != std::string::npos) + continue; // seed already recovers a clean port name: leave it + } + if (cwi == nullptr) + cwi = train->addWire( + RTLIL::escape_id("__pqcarve." + unescape(pw->name.str())), pw->width); + // Copy the launch-flop seed into the clean carve wire with a boundary buffer + // that stays in 'train' (untagged), so cwi is driven from outside the cone and + // submod exports it as the clean "_" input port. The buffer MUST be + // kept: submod runs opt_clean before extraction, and opt_clean dissolves a + // plain $_BUF_ (aliasing Y=A) -- that would collapse cwi back onto the + // flattened "n_" seed net, so submod would then name the port after the + // seed (the leak). The capture-flop OUTPUT reconstruction keeps its buffer for + // the identical reason. The buffer is untagged, so it is dropped with 'train' + // after the carve; the carved cell keeps only its real gate(s) -- no area hit. + Cell *ib = train->addBufGate(NEW_ID, qi, SigBit(cwi, i)); + ib->set_bool_attribute(ID::keep); + in_remap[qi] = SigBit(cwi, i); + } + } + } + + // (2) Self-containment (runs for EVERY carved cell): one rewrite pass over the + // (growing) cone. For a design cell in_remap also applies part (1); for a plain cell + // in_remap is empty, so this only folds constants/ties, ties undriven nets to 0, and + // pulls in forward-walk-missed gates -- a no-op on an already-clean cone. + { + std::vector proc(logic.begin(), logic.end()); + pool queued = logic; + while (!proc.empty()) { + Cell *c = proc.back(); + proc.pop_back(); + dict rewire; + for (auto &conn : c->connections()) { + if (!c->input(conn.first)) + continue; + SigSpec cs = conn.second; + bool changed = false; + for (auto &bit : cs) { + if (bit.wire == nullptr) + continue; // already a constant literal + SigBit s = sigmap(bit); + auto rm = in_remap.find(s); + if (rm != in_remap.end()) { + bit = rm->second; // clean primary input + changed = true; + continue; + } + if (s.wire == nullptr || clk_nets.count(s) || driven.count(s)) + continue; // constant / shared clock / produced in-cone: fine + auto dit = driver.find(s); + if (dit != driver.end() && boundary_cells.count(dit->second.first)) + continue; // launch-flop output the remap above missed: keep as input + if (dit == driver.end()) { + // Undriven internal net (inserted scan/test pin, etc.): tie to 0. + bit = State::S0; + changed = true; + continue; + } + Cell *dc = dit->second.first; + IdString opin = dit->second.second; + bool is_source = !cell_in_bits.count(dc) || cell_in_bits.at(dc).empty(); + if (is_source && GetSize(dc->getPort(opin)) == 1) { + // Pure constant/tie source read from outside the cone. For a plain + // cell fold it to a literal using the synth's logic_0/logic_1 tie-net + // naming (area-neutral -- keeps a sequential cell at a single mapped + // instance). Otherwise (design cell, or an unrecognizable value) + // clone the source into the carve rather than steal a possibly-shared + // original, so the cone bit gains an in-cone driver and the tie's + // (tiny) power is characterized here. + std::string wn = s.wire ? unescape(s.wire->name.str()) : ""; + if (!is_design && wn.find("logic_1") != std::string::npos) { + bit = State::S1; + } else if (!is_design && wn.find("logic_0") != std::string::npos) { + bit = State::S0; + } else { + Cell *clone = train->addCell(NEW_ID, dc->type); + clone->parameters = dc->parameters; + for (auto &cc : dc->connections()) + clone->setPort(cc.first, cc.second); + Wire *tn = train->addWire(NEW_ID, 1); + clone->setPort(opin, SigSpec(tn)); + clone->set_string_attribute(SUBMOD, final); + logic.insert(clone); + driven.insert(SigBit(tn)); + bit = SigBit(tn); + } + changed = true; + } else if (!queued.count(dc)) { + // A non-source cell the forward walk missed (only constant-fed): + // pull it into the carve and process its inputs too. + dc->set_string_attribute(SUBMOD, final); + logic.insert(dc); + queued.insert(dc); + proc.push_back(dc); + for (auto ob : cell_out_bits[dc]) + driven.insert(ob); + } + } + if (changed) + rewire[conn.first] = cs; + } + for (auto &r : rewire) + c->setPort(r.first, r.second); + } + } + + boundary[final] = {speed, drv_cell, drv_pin, load_cell, load_pin}; + } + + log("Carved %d cells (%d logic cells tagged)\n", GetSize(boundary), tagged_total); + + // Carve every tagged cell into its own module (submod names it "train."); + // the untagged flops/clock-network cells stay in 'train'. + Pass::call(design, "submod -separator ."); + + // Strip the "train." prefix off the carved module names + std::string mprefix = "\\train."; + for (auto mod : design->modules().to_vector()) { + std::string n = mod->name.str(); + if (n.rfind(mprefix, 0) == 0) + design->rename(mod, RTLIL::escape_id(n.substr(mprefix.size()))); + } + + // Map every design module to its instantiations so that renaming a module's + // port wires below can keep the matching port-connection keys on its instances + // in sync (Module::rename only touches the wire, not the instance connections). + dict> insts_by_type; + for (auto mod : design->modules()) + for (auto cell : mod->cells()) + if (design->module(cell->type) != nullptr) + insts_by_type[cell->type].push_back(cell); + + // Clean up the carved cell port names, updating each instance's port-connection key + // to match the renamed port (Module::rename only touches the wire). Two fixups: + // 1. Strip leftover __pqi/__pqo boundary suffixes (present when synthesis kept the + // carved net names; harmless no-op otherwise). + // 2. Drop the flatten instance-scope prefix (everything up to and including the + // first '.') so a port recovers its clean train top-port name, e.g. + // "slow_.slow__A" -> "slow__A", then collapse any remaining dots + // (e.g. exposed clock-gate pins like "slow_.CG_INST.enable") to '_'. The + // flatten earlier scope-prefixes every inlined net with a '.', which OpenSTA + // tolerates but the cocotb/iverilog power flow cannot (it can't access a port + // whose name contains '.', treating the dot as a hierarchy separator). + for (auto mod : design->modules()) { + if (mod->name == ID(train)) + continue; + bool stripped = false; + // Process the reconstructed capture-flop output buses ("__pqcarve._", + // tagged PQ_CARVE_OUT) first. They collapse to the clean "_" output + // port and are the authoritative output. A degenerate output bus (const-shift / + // feed-through / self-referential cell) can leave a stale "___pqo" + // cone-output fragment that cleans to the same name; whoever is renamed first + // wins the collision below, so the reconstructed bus must go first or the carved + // cell loses its output port (degenerating to input/inout with the result + // stranded). See the collision handling below for how the stale fragment is + // then demoted to an internal net rather than aliased onto the output. + std::vector wires_ordered = mod->wires().to_vector(); + std::stable_partition(wires_ordered.begin(), wires_ordered.end(), [](Wire *w) { + return unescape(w->name.str()).rfind("__pqcarve.", 0) == 0; + }); + for (auto wire : wires_ordered) { + if (!wire->port_input && !wire->port_output) + continue; + std::string wn = unescape(wire->name.str()); + std::string nn = wn; + for (const char *suf : {"__pqi", "__pqo"}) { + size_t pos; + while ((pos = nn.find(suf)) != std::string::npos) + nn.erase(pos, strlen(suf)); + } + size_t dot = nn.find('.'); + if (dot != std::string::npos) + nn = nn.substr(dot + 1); + std::replace(nn.begin(), nn.end(), '.', '_'); + if (nn == wn) + continue; + IdString oldid = wire->name; + IdString newid = RTLIL::escape_id(nn); + // Two boundary wires can clean up to the same per-pin name. This happens + // when synthesis splits one logical cell-input bus across more than one net + // -- e.g. a drive-strength buffer inserted on a subset of the bits, so the + // flattened submodule reads {buffered_bits, flop_bits}. flatten then leaves + // both the train-side "___pqi" (carrying the directly-read bits) + // and the scope-prefixed ".___pqi" (carrying the buffered + // bits), and submod exports each as its own port. They are the same logical + // pin, so fold the fragments onto one bus port instead of uniquifying (a + // phantom second port the OpenSTA/cocotb flows could not drive): alias the + // loser onto the already-renamed winner -- a no-op on bits they share, and on + // the disjoint bits it wires the winner's otherwise-dangling bit to the + // loser's reader (the in-cone buffer) -- and drop the loser's port flag so the + // trailing clean collapses it away. + if (Wire *ex = mod->wire(newid); ex != nullptr && ex != wire) { + // The reconstructed capture-flop output bus (cw, tagged PQ_CARVE_OUT, + // processed first above so it always wins the name) is the authoritative + // clean output port. The colliding wire is then a stale cone-output + // fragment ("___pqo") that the per-bit buffers already *source* + // from -- aliasing it onto cw would turn every buffer into Y[i]=Y[i] (a + // combinational self-loop) and strand the real gate/feed-through/tie + // drivers. So demote the fragment to an internal net (leaving it driven + // in-cone and read by the buffers); the later __pqo neutralization renames + // it. Do NOT connect it to cw. + if (ex->get_bool_attribute(PQ_CARVE_OUT)) { + wire->port_input = false; + wire->port_output = false; + stripped = true; + continue; + } + if (ex->width == wire->width) { + log("carvenetlist: unifying split boundary port '%s' into '%s' in " + "module %s\n", log_id(oldid), log_id(newid), log_id(mod->name)); + wire->port_input = false; + wire->port_output = false; + mod->connect(SigSpec(wire), SigSpec(ex)); + stripped = true; + continue; + } + log_warning("carvenetlist: cannot unify boundary port '%s' -> '%s' in " + "module %s (widths %d vs %d differ); leaving original name.\n", + log_id(oldid), log_id(newid), log_id(mod->name), wire->width, + ex->width); + continue; + } + mod->rename(wire, newid); + stripped = true; + for (auto inst : insts_by_type[mod->name]) { + if (!inst->hasPort(oldid)) + continue; + SigSpec sig = inst->getPort(oldid); + inst->unsetPort(oldid); + inst->setPort(newid, sig); + } + } + // Drop the internal carve-output marker now that collisions are resolved so it + // does not leak into the emitted netlist. + for (auto wire : mod->wires()) + if (wire->get_bool_attribute(PQ_CARVE_OUT)) + wire->attributes.erase(PQ_CARVE_OUT); + if (stripped) + mod->fixup_ports(); + } + + // Simplify away the wire-to-wire aliases left by flatten and by the split-port + // unification above, dropping each redundant copy down to the single surviving + // (port-named) net so every carved boundary becomes exactly one clean port. + Pass::call(design, "clean -purge"); + + // Turn the temporary passthrough buffers back into plain assigns (zero-area + // feed-throughs) now that submod has given each its own clean input/output port. + // Done after the final clean so the assign isn't re-collapsed back into one net. + for (auto mod : design->modules()) { + if (mod->name == ID(train)) + continue; + for (auto cell : mod->cells().to_vector()) { + if (cell->type != ID($_BUF_) || !cell->has_attribute(PQ_PASS)) + continue; + SigSpec a = cell->getPort(ID::A); + SigSpec y = cell->getPort(ID::Y); + mod->remove(cell); + mod->connect(y, a); + } + } + + // Neutralize any __pqi/__pqo token left on an *internal* net. Every output bit is + // now driven through a capture-flop-boundary buffer whose source is the in-cone net + // the flop used to read -- that net keeps its original "___pqo" boundary + // name but is no longer a port (the buffer's fresh carve wire is). The leftover name + // is purely cosmetic for an internal net, but strip the token (uniquifying on + // collision) so no boundary marker survives the carve. + for (auto mod : design->modules()) { + if (mod->name == ID(train)) + continue; + for (auto wire : mod->wires().to_vector()) { + if (wire->port_input || wire->port_output) + continue; + std::string wn = unescape(wire->name.str()); + if (wn.find("__pqi") == std::string::npos && wn.find("__pqo") == std::string::npos) + continue; + std::string nn = wn; + for (const char *suf : {"__pqi", "__pqo"}) { + size_t pos; + while ((pos = nn.find(suf)) != std::string::npos) + nn.erase(pos, strlen(suf)); + } + IdString newid = RTLIL::escape_id(nn); + mod->rename(wire, (nn.empty() || mod->wire(newid) != nullptr) ? NEW_ID : newid); + } + } + + // Replace a REDUNDANT SINGLE-FANOUT 1-input/1-output cell (a lone drive-strength buffer + // or a cell->capture-flop output buffer sitting on top of a real in-cone gate) with a + // direct wire: its one load simply moves to the upstream driver, so delay/power are + // unchanged, and the cone still carves to its one real gate (matching the normal flow). + // Dropping such an inverter's inversion is fine -- cells are characterized for PPA, not + // function, and toggle activity (hence power) is polarity-independent. + // + // KEEP a passthrough that is the cone's BOUNDARY GATE -- one whose input is not driven by + // another in-cone cell (it reads a primary input / launch-flop boundary net). It is the + // cell-under-test's only logic, not a redundant re-driver, so shorting it would replace + // the whole cone with a bare wire: nothing left to characterize, and for an inverter the + // inversion is silently dropped (the carved cell no longer matches the RTL). + // + // KEEP a passthrough whose output has FANOUT > 1. Such a buffer/inverter is a fanout + // (re-drive) buffer -- e.g. a wide multiplier/adder's input and adder-tree buffer trees, + // where a single input bit fans out to hundreds of partial-product gates. Shorting it + // dumps its whole fanout onto the upstream driver; since characterization drives the + // boundary with set_driving_cell, STA then charges a single flop driving that entire + // fanout, massively inflating the measured delay. (no_inv_delay_calc excludes the buffer's + // own stage delay, but NOT the loading it removes.) So fanout buffers are load-bearing for + // delay and must be kept. + // + // EXCEPTION: a passthrough on a flop's feedback path (e.g. the QN -> INV -> D loop that + // implements the hold of a scan-flop-based enable flop like dffe/sdffce) must be kept -- + // shorting it would tie the flop's D onto its own Q/QN and turn a hold into a toggle. + // Detect this as "the cell driving the passthrough's input also reads its output". + // + // DESIGN CELLS (design_slow_/design_fast_) are exempted entirely: a whole + // sequential design is characterized for its real function, not just PPA, so its logic + // inverters carry genuine inversions. Shorting them (out=in) would drop every inversion + // and make the carved design inequivalent to RTL (its testbench fails). + for (auto mod : design->modules()) { + if (mod->name == ID(train)) + continue; + std::string mn = unescape(mod->name.str()); + if (mn.rfind("design_slow_", 0) == 0 || mn.rfind("design_fast_", 0) == 0) + continue; + // Original-topology driver/reader maps (net bit -> output cell / input cells) plus a + // per-net load count (input-pin occurrences) and the output-port bit set, so a + // passthrough's output fanout (loads + whether it drives a module output) is known. + SigMap sm(mod); + dict drv; + dict> rdr; + dict load_cnt; + for (auto cell : mod->cells()) + for (auto &conn : cell->connections()) + for (auto bit : sm(conn.second)) { + if (bit.wire == nullptr) + continue; + if (cell->output(conn.first)) + drv[bit] = cell; + if (cell->input(conn.first)) { + rdr[bit].insert(cell); + load_cnt[bit]++; + } + } + pool outport_bits; + for (auto w : mod->wires()) + if (w->port_output) + for (auto bit : sm(SigSpec(w))) + if (bit.wire) + outport_bits.insert(bit); + for (auto cell : mod->cells().to_vector()) { + if (!is_passthrough(cell)) + continue; + SigSpec in, out; + for (auto &conn : cell->connections()) { + if (cell->input(conn.first)) + in = conn.second; + else if (cell->output(conn.first)) + out = conn.second; + } + if (GetSize(in) != 1 || GetSize(out) != 1) + continue; + SigBit ib = sm(in[0]), ob = sm(out[0]); + // Keep the cone's boundary gate: a passthrough whose input has no in-cone driver + // (reads a primary input / boundary net) is the cell-under-test's only logic, so + // shorting it would strand the cone as a bare wire (dropping the gate, and for an + // inverter its inversion). Only redundant re-drivers stacked on a real in-cone + // gate -- whose input IS driven by another cell -- may be shorted away. + if (!drv.count(ib)) + continue; + // Keep fanout>1 passthroughs: a fanout (re-drive) buffer/inverter whose removal + // would dump its load onto the upstream driver and inflate its characterized delay. + int fanout = (load_cnt.count(ob) ? load_cnt.at(ob) : 0) + + (outport_bits.count(ob) ? 1 : 0); + if (fanout > 1) + continue; + // Feedback guard: the cell driving this passthrough's input also reads its output. + auto dit = drv.find(ib); + if (dit != drv.end() && dit->second != cell && rdr.count(ob) && + rdr.at(ob).count(dit->second)) + continue; + mod->remove(cell); + mod->connect(out, in); + } + } + // Drop the now-dangling internal nets and fold the wire-to-wire aliases left behind. + Pass::call(design, "clean"); + + // Record each carved cell's surround boundary as \pq_* string attributes on its + // module, so the Python flow can re-impose the launch/capture flops during OpenSTA + // characterization without a separate boundary side file. + for (auto mod : design->modules()) { + if (mod->name == ID(train)) + continue; + auto it = boundary.find(unescape(mod->name.str())); + if (it == boundary.end()) + continue; + const BoundaryRec &r = it->second; + mod->set_string_attribute(RTLIL::escape_id("pq_speed"), r.speed); + mod->set_string_attribute(RTLIL::escape_id("pq_driving_cell"), r.driving_cell); + mod->set_string_attribute(RTLIL::escape_id("pq_driving_pin"), r.driving_pin); + mod->set_string_attribute(RTLIL::escape_id("pq_load_cell"), r.load_cell); + mod->set_string_attribute(RTLIL::escape_id("pq_load_pin"), r.load_pin); + } + + // Drop the flop-bearing train module + if (Module *t = design->module(ID(train))) + design->remove(t); + } +} CarveNetlistPass; + +PRIVATE_NAMESPACE_END diff --git a/passes/silimate/opt_compact_prefix.cc b/passes/silimate/opt_compact_prefix.cc index fdc902a64..c7b443682 100644 --- a/passes/silimate/opt_compact_prefix.cc +++ b/passes/silimate/opt_compact_prefix.cc @@ -236,7 +236,7 @@ struct OptCompactPrefixWorker : CutRegionWorker for (auto bit : en_cands) cand_bits.push_back(bit); - ConstEval ce(module); + ConstEval &ce = shared_ce(); SigSpec out_s = sigmap(root_sig); SigSpec data_s = sigmap(data_sig); SigSpec cand_s; @@ -415,7 +415,7 @@ struct OptCompactPrefixWorker : CutRegionWorker { if (width < 4) return false; - ConstEval ce(module); + ConstEval &ce = shared_ce(); SigSpec in_s = sigmap(in_sig); SigSpec out_s = sigmap(out_sig); @@ -511,7 +511,7 @@ struct OptCompactPrefixWorker : CutRegionWorker int wd = GetSize(dis_s); int wt = GetSize(data_s); bool self = sigmap(dis_s) == sigmap(data_s); - ConstEval ce(module); + ConstEval &ce = shared_ce(); for (int pol = 0; pol < 2; pol++) { // A self-pair (data == disable bus) can only be probed with all // lanes enabled AND data all-ones, i.e. enable-high polarity. @@ -546,7 +546,7 @@ struct OptCompactPrefixWorker : CutRegionWorker return false; if (lw > wd || lw > wt || wd > 62 || wt > 62) return false; - ConstEval ce(module); + ConstEval &ce = shared_ce(); SigSpec dis_s = sigmap(dis_sig); SigSpec data_s = sigmap(data_sig); SigSpec out_s = sigmap(out_sig); @@ -615,8 +615,14 @@ struct OptCompactPrefixWorker : CutRegionWorker return mask; } + // Evaluate the modulo vector set once and test all four + // (msb_first, offset) variants against the recorded outputs, in the + // original priority order with first-match tie-break. This replaces up + // to four full ConstEval sweeps (one per variant) with a single sweep; + // sets out_msb_first/out_offset on the first surviving variant. bool fingerprint_modulo(const SigSpec &en_sig, const SigSpec &n_sig, - const SigSpec &mask_sig, int width, bool msb_first, int offset) + const SigSpec &mask_sig, int width, + bool &out_msb_first, int &out_offset) { if (width <= 0 || width > 62) return false; @@ -624,7 +630,7 @@ struct OptCompactPrefixWorker : CutRegionWorker // also matches plain gating muxes; require a real counter range. if (GetSize(n_sig) < 2) return false; - ConstEval ce(module); + ConstEval &ce = shared_ce(); SigSpec en_s = sigmap(en_sig); SigSpec n_s = sigmap(n_sig); SigSpec mask_s = sigmap(mask_sig); @@ -653,6 +659,12 @@ struct OptCompactPrefixWorker : CutRegionWorker envals.push_back(lfsr & full); } + // Variants in the original check order (msb/lsb x offset 0/1). + static const struct { bool msb; int off; } variants[] = { + {true, 0}, {false, 0}, {false, 1}, {true, 1} + }; + bool alive[4] = {true, true, true, true}; + for (uint64_t nv : nvals) for (uint64_t ev : envals) { uint64_t actual; @@ -660,10 +672,27 @@ struct OptCompactPrefixWorker : CutRegionWorker {n_s, const_u64(nv, nw)}}, mask_s, actual)) return false; - if (actual != expected_modulo_mask(ev, nv, width, msb_first, offset)) + bool any = false; + for (int v = 0; v < 4; v++) { + if (!alive[v]) + continue; + if (actual != expected_modulo_mask(ev, nv, width, + variants[v].msb, variants[v].off)) + alive[v] = false; + else + any = true; + } + if (!any) return false; } - return true; + + for (int v = 0; v < 4; v++) + if (alive[v]) { + out_msb_first = variants[v].msb; + out_offset = variants[v].off; + return true; + } + return false; } // --- Forward gather/compress: out[k] = data[i_k] where i_k is the @@ -693,7 +722,7 @@ struct OptCompactPrefixWorker : CutRegionWorker { if (width < 4 || width > 62) return false; - ConstEval ce(module); + ConstEval &ce = shared_ce(); SigSpec en_s = sigmap(en_sig); SigSpec data_s = sigmap(data_sig); SigSpec out_s = sigmap(out_sig); @@ -1397,19 +1426,7 @@ struct OptCompactPrefixWorker : CutRegionWorker fp_attempts++; bool msb_first = false; int offset = -1; - if (fingerprint_modulo(en.sig, n.sig, root.sig, width, true, 0)) { - msb_first = true; - offset = 0; - } else if (fingerprint_modulo(en.sig, n.sig, root.sig, width, false, 0)) { - msb_first = false; - offset = 0; - } else if (fingerprint_modulo(en.sig, n.sig, root.sig, width, false, 1)) { - msb_first = false; - offset = 1; - } else if (fingerprint_modulo(en.sig, n.sig, root.sig, width, true, 1)) { - msb_first = true; - offset = 1; - } else { + if (!fingerprint_modulo(en.sig, n.sig, root.sig, width, msb_first, offset)) { log_debug(" mod %s/%s: fingerprint mismatch\n", en.name.c_str(), n.name.c_str()); continue; } diff --git a/passes/techmap/bwmuxmap.cc b/passes/techmap/bwmuxmap.cc index 7fe1cded7..3988daeb5 100644 --- a/passes/techmap/bwmuxmap.cc +++ b/passes/techmap/bwmuxmap.cc @@ -57,10 +57,10 @@ struct BwmuxmapPass : public Pass { auto &sig_b = cell->getPort(ID::B); auto &sig_s = cell->getPort(ID::S); - auto not_s = module->Not(NEW_ID, sig_s); - auto masked_b = module->And(NEW_ID, sig_s, sig_b); - auto masked_a = module->And(NEW_ID, not_s, sig_a); - module->addOr(NEW_ID, masked_a, masked_b, sig_y); + auto not_s = module->Not(NEW_ID2_SUFFIX("not_s"), sig_s); // SILIMATE: Improve the naming + auto masked_b = module->And(NEW_ID2_SUFFIX("masked_b"), sig_s, sig_b); // SILIMATE: Improve the naming + auto masked_a = module->And(NEW_ID2_SUFFIX("masked_a"), not_s, sig_a); // SILIMATE: Improve the naming + module->addOr(NEW_ID2_SUFFIX("y"), masked_a, masked_b, sig_y); // SILIMATE: Improve the naming module->remove(cell); } diff --git a/silimate-shell.nix b/silimate-shell.nix index beef60ae6..01ac4b657 100644 --- a/silimate-shell.nix +++ b/silimate-shell.nix @@ -29,7 +29,6 @@ pkgs.mkShell { iverilog # tests gtkwave # vcd2fst (python3.withPackages(ps: with ps; [pip wheel pybind11 cxxheaderparser])) - gnu-ar gtest ] ++ lib.optionals stdenv.isLinux [ elfutils # provides libdw.so (not to be confused with libdwarf.so) diff --git a/tests/opt/opt_dff_eqbits.ys b/tests/opt/opt_dff_eqbits.ys new file mode 100644 index 000000000..f990fe71a --- /dev/null +++ b/tests/opt/opt_dff_eqbits.ys @@ -0,0 +1,89 @@ +# small test case +design -reset +read_verilog -sv opt_dff_eqbits_small.sv +hierarchy -top test_case +techmap +opt_dff -sat +synth +opt_dff -sat +opt_clean -purge + +select -assert-count 2 t:$_SDFF_PN0_ + +# equivalence +design -reset +read_verilog -sv opt_dff_eqbits_small.sv +hierarchy -top test_case +prep +design -save gold + +opt_dff -sat +design -save gate + +design -copy-from gold -as gold test_case +design -copy-from gate -as gate test_case +equiv_make gold gate equiv +equiv_induct equiv +equiv_status -assert + + +# large test case +design -reset +read_verilog -sv opt_dff_eqbits_large.sv +hierarchy -top test_case +techmap +opt_dff -sat +synth +opt_dff -sat +opt_clean -purge + +select -assert-count 6 t:$_SDFFE_PN0P_ + +# equivalence +design -reset +read_verilog -sv opt_dff_eqbits_large.sv +hierarchy -top test_case +prep +design -save gold + +opt_dff -sat +design -save gate + +design -copy-from gold -as gold test_case +design -copy-from gate -as gate test_case +equiv_make gold gate equiv +equiv_induct equiv +equiv_status -assert + +# verify keepdc exclusivity +design -reset +read_verilog -sv < no rewrite" +log -push +design -reset +read_verilog -sv <= N) ? (si - N) : si; + rr_dut #(N,W) u1(.req(req),.s(s),.grant(g1),.idx_next(n1)); + rr_ref #(N,W) u2(.req(req),.s(s),.grant(g2),.idx_next(n2)); + always_comb begin + assert (g1 == g2); + assert (n1 == n2); + end +endmodule +EOF +hierarchy -top tb +flatten +chformal -lower +opt -full +sat -verify -prove-asserts -show-ports tb +design -reset +log -pop + +# RR3: negative -- a downward-scanning arbiter (opposite rotation) is a +# different function and must not be rewritten as round-robin. +log -header "RR3: downward-scan arbiter -> no round-robin rewrite" +log -push +design -reset +read_verilog -sv <= N) ? (si - N) : si; + rr_dut #(N,W) u1(.req(req),.s(s),.grant(g1),.idx_next(n1)); + rr_ref #(N,W) u2(.req(req),.s(s),.grant(g2),.idx_next(n2)); + always_comb begin + assert (g1 == g2); + assert (n1 == n2); + end +endmodule +EOF +hierarchy -top tb +flatten +chformal -lower +opt -full +sat -verify -prove-asserts -show-ports tb +design -reset +log -pop + +# RR7: DIFFERENT RTL SPELLING of the same rotated-priority function. Instead of +# the customer's downward idx-- last-write-wins loop, scan UPWARD from s+1 with +# wraparound and keep the FIRST hit. This is a functionally identical arbiter +# written in an unrelated style; detection is functional so it must still fire +# and prove equivalent. (Combinational, pointer `s` an input -> full equiv at +# power-of-2 N over all pointer values.) +log -header "RR7: upward-scan spelling variant, N=16 (equiv + fires)" +log -push +design -reset +read_verilog -sv < no round-robin rewrite" +log -push +design -reset +read_verilog -sv < full equiv). +# B: structural win -- the $shiftx chain is gone after the rewrite. +# C: negative / no-op cases (no false rewrites). +# +# Convention: every object the pass emits is named with a `priokey_` suffix, so +# `select w:*priokey*` is a reliable "did the rewrite fire" probe. + +# ============================================================================ +# Group A: formal equivalence (equiv_opt -assert) +# ============================================================================ + +# A1: P=3 sources into a 64-slot accumulator (SW=6, all keys reachable). +log -header "A1: priority-by-key dedup P=3 S=64 (equiv)" +log -push +design -reset +read_verilog -sv < no rewrite" +log -push +design -reset +read_verilog -sv < no rewrite" +log -push +design -reset +read_verilog -sv < no rewrite" +log -push +design -reset +read_verilog -sv <= SW'(S)) ? (k - SW'(S)) : k; // into [0,S) + end + logic [P-1:0] w1, w2; + pk_dut #(P,S,SW) u1(.act(act), .sel_flat(sel_c), .win(w1)); + pk_ref #(P,S,SW) u2(.act(act), .sel_flat(sel_c), .win(w2)); + always_comb assert (w1 == w2); +endmodule +EOF +hierarchy -top tb +flatten +chformal -lower +opt -full +sat -verify -prove-asserts -show-ports tb +design -reset +log -pop + +# E4: SAME non-pow2 S=12 under -strict. Strict validation sweeps the FULL key +# range and rejects rewrites that only hold via out-of-range don't-cares, so +# the pass must decline to rewrite. This is the safety mode used by formal +# synthesis flows: no reliance on out-of-range freedom. +log -header "E4: non-pow2 S=12 -strict -> no rewrite (safety)" +log -push +design -reset +read_verilog -sv < no rewrite" +log -push +design -reset +read_verilog -sv < no rewrite" +log -push +design -reset +read_verilog -sv <___pqi) and every data output feeds a capture flop's +# D (named ___pqo), with a shared fast_clk/slow_clk. carvenetlist must +# carve the logic between the flops into module `cell` with a clean input port +# fast_cell_A and a clean output port fast_cell_Y (never inout), preserving +# feed-throughs (output bit = input bit, or = constant) as plain assigns. +# ============================================================================= + +# ----------------------------------------------------------------------------- +# Test 1: feed-through output bit (Y[0] = A[0]). +# Regression: the bare alias used to fuse the launch-side input net with the +# capture-side output net, leaving Y[0] undriven and dragging the Y bus to inout. +# ----------------------------------------------------------------------------- +log -header "Feed-through output bit (Y[0] = A[0])" +log -push +design -reset +read_rtlil <