diff --git a/.github/workflows/regress-sec.yml b/.github/workflows/regress-sec.yml index 1a102dba..8b01aae4 100644 --- a/.github/workflows/regress-sec.yml +++ b/.github/workflows/regress-sec.yml @@ -22,9 +22,19 @@ jobs: submodules: true - name: Install dependencies + timeout-minutes: 10 run: | - sudo apt-get update - sudo apt-get install -y \ + sudo apt-get \ + -o Acquire::Retries=3 \ + -o Acquire::http::Timeout=30 \ + -o Acquire::https::Timeout=30 \ + update + sudo apt-get \ + -o Acquire::Retries=3 \ + -o Acquire::http::Timeout=30 \ + -o Acquire::https::Timeout=30 \ + -o DPkg::Lock::Timeout=60 \ + install -y \ libgtest-dev cmake pkg-config libboost-dev libfl-dev \ capnproto libcapnp-dev ninja-build libtbb-dev libspdlog-dev \ libboost-iostreams-dev zlib1g-dev @@ -79,6 +89,7 @@ jobs: run: | cmake --build ${{github.workspace}}/build-sec-regress --target install --config Release chmod +x ${{github.workspace}}/stage/bin/kepler-formal + bash tools/bundle_linux_runtime_dependencies.sh "${{github.workspace}}/stage" - name: Upload SEC runtime uses: actions/upload-artifact@v4 @@ -90,7 +101,15 @@ jobs: run-sec-regress: name: ${{ matrix.flow.name }} / ${{ matrix.case.name }} - runs-on: ubuntu-22.04 + # SEC_HEAVY_RUNNER may name a larger or self-hosted runner for full-depth + # GCD KI and MockAlu PDR jobs. Pull requests also use bounded smoke depths. + runs-on: >- + ${{ + ( + (matrix.flow.name == 'ki-dual-rail' && matrix.case.name == 'sky130hd_gcd') || + (matrix.flow.name == 'pdr-dual-rail' && matrix.case.name == 'asap7_mock_alu') + ) && vars.SEC_HEAVY_RUNNER || 'ubuntu-22.04' + }} needs: build-sec-regress strategy: fail-fast: false @@ -139,6 +158,7 @@ jobs: case_dir: kepler-formal-regress/sky130hd_gcd config: objects/sky130hd/gcd/base/4_rsz_lec_test.yml expectation: positive + ki_dual_rail_pr_max_k: 8 - name: nangate45_black_parrot source: regress case_dir: kepler-formal-regress/nangate45_black_parrot @@ -190,6 +210,7 @@ jobs: case_dir: kepler-formal-examples/nangate45_ariane136 config: 6_final_sec_test.yml expectation: positive + pdr_dual_rail_expectation: allow-inconclusive - name: asap7_ethmac source: examples case_dir: kepler-formal-examples/asap7_ethmac @@ -201,6 +222,7 @@ jobs: config: 6_final_sec_test.yml expectation: positive pdr_dual_rail_expectation: allow-inconclusive + pdr_dual_rail_pr_max_k: 2 - name: asap7_mock_cpu source: examples case_dir: kepler-formal-examples/asap7_mock_cpu @@ -269,13 +291,6 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Install runtime dependencies - run: | - sudo apt-get update - sudo apt-get install -y \ - git python3 python3-pip capnproto libcapnp-dev libtbb-dev \ - libboost-iostreams-dev zlib1g-dev - - name: Download SEC runtime uses: actions/download-artifact@v4 with: @@ -311,6 +326,7 @@ jobs: env: CASE_GENERATOR: ${{ matrix.case.generator }} run: | + export LD_LIBRARY_PATH="${{github.workspace}}/stage/lib:${LD_LIBRARY_PATH:-}" python3 -m pip install \ "${{github.workspace}}"/stage/najaeda/najaeda-*.whl case "${CASE_GENERATOR}" in @@ -337,6 +353,8 @@ jobs: CASE_CONFIG: ${{ matrix.case.config }} CASE_EXPECTATION: ${{ matrix.case.expectation }} CASE_MAX_K: ${{ matrix.case.max_k }} + CASE_KI_DUAL_RAIL_PR_MAX_K: ${{ matrix.case.ki_dual_rail_pr_max_k }} + CASE_PDR_DUAL_RAIL_PR_MAX_K: ${{ matrix.case.pdr_dual_rail_pr_max_k }} CASE_PDR_DUAL_RAIL_EXPECTATION: ${{ matrix.case.pdr_dual_rail_expectation }} SEC_ENGINE: ${{ matrix.flow.engine }} SEC_ENCODING: ${{ matrix.flow.sec_encoding }} @@ -361,6 +379,19 @@ jobs: expectation="${CASE_PDR_DUAL_RAIL_EXPECTATION}" fi + max_k="${CASE_MAX_K}" + if [[ "${GITHUB_EVENT_NAME}" == "pull_request" && + "${SEC_ENGINE}" == "k_induction" && + "${SEC_ENCODING}" == "dual_rail_steady" && + -n "${CASE_KI_DUAL_RAIL_PR_MAX_K}" ]]; then + max_k="${CASE_KI_DUAL_RAIL_PR_MAX_K}" + elif [[ "${GITHUB_EVENT_NAME}" == "pull_request" && + "${SEC_ENGINE}" == "pdr" && + "${SEC_ENCODING}" == "dual_rail_steady" && + -n "${CASE_PDR_DUAL_RAIL_PR_MAX_K}" ]]; then + max_k="${CASE_PDR_DUAL_RAIL_PR_MAX_K}" + fi + args=( "${CASE_NAME}" "${{github.workspace}}/${CASE_DIR}" @@ -371,8 +402,8 @@ jobs: "engine=${SEC_ENGINE}" "sec-encoding=${SEC_ENCODING}" ) - if [[ -n "${CASE_MAX_K}" ]]; then - args+=("max-k=${CASE_MAX_K}") + if [[ -n "${max_k}" ]]; then + args+=("max-k=${max_k}") fi bash regress/run_sec_strategies_regress.sh "${args[@]}" @@ -544,13 +575,6 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Install runtime dependencies - run: | - sudo apt-get update - sudo apt-get install -y \ - git python3 python3-pip capnproto libcapnp-dev libtbb-dev \ - libboost-iostreams-dev zlib1g-dev - - name: Download SEC runtime uses: actions/download-artifact@v4 with: @@ -649,13 +673,6 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Install runtime dependencies - run: | - sudo apt-get update - sudo apt-get install -y \ - git python3 python3-pip capnproto libcapnp-dev libtbb-dev \ - libboost-iostreams-dev zlib1g-dev - - name: Download SEC runtime uses: actions/download-artifact@v4 with: diff --git a/bazel/deps.bzl b/bazel/deps.bzl index 4ae3c741..1c0e3594 100644 --- a/bazel/deps.bzl +++ b/bazel/deps.bzl @@ -33,7 +33,7 @@ _FLEX_VERSION = "2.6.4" _CADICAL_COMMIT = "7b99c07f0bcab5824a5a3ce62c7066554017f641" _GLUCOSE_COMMIT = "7f887abba7cf13636a5ac2d28653668a20a91b25" _KISSAT_COMMIT = "8af8e56f174b778aef3aa45af9f739b2a5f492c2" -_NAJA_COMMIT = "abe47a6d0ff1c9392e08834440a5f3b50da134af" +_NAJA_COMMIT = "c96e1b18bf6e411a9ed5273eac0d4cea1c79c73f" _NAJA_VERILOG_COMMIT = "5da040bb34f0e4e5bb8d67223b999a0132fb401f" _NAJA_IF_COMMIT = "099677d9f52c0db11b12c08d03e32543eebc7888" _SLANG_COMMIT = "512c327c209d3043aa98ecfd02d06a1b73fcd5fb" @@ -156,7 +156,7 @@ def _deps_impl(_module_ctx): http_archive( name = "naja", url = "https://github.com/nanocoh/naja/archive/{}.tar.gz".format(_NAJA_COMMIT), - sha256 = "d7121316309a47f8fd39fe07efa308ca3e935c3ad8f0131443c3666a96d6bb17", + sha256 = "055337bc81b41cf6f10720950d99afed7c4ae7c328f42ec28d9185d392f33f13", strip_prefix = "naja-{}".format(_NAJA_COMMIT), build_file = Label("//bazel:naja.BUILD.bazel"), patch_args = ["-p0", "-f"], diff --git a/docs/flags-spec.md b/docs/flags-spec.md index d14db224..29ebdd3e 100644 --- a/docs/flags-spec.md +++ b/docs/flags-spec.md @@ -29,8 +29,6 @@ LEC is the default. Select SEC with `-v sec`, `--verification sec`, or | `--max-k `, `-k ` | Set the SEC proof/search bound. Defaults to `32`; SEC only. | | `--sec-engine ` | Select the SEC engine. Defaults to `pdr`; SEC only. | | `--sec-encoding ` | Select the SEC encoding. Defaults to `dual_rail_steady`; SEC only. | -| `--sec-uncomputable-seq-boundary` | Abstract unsupported sequential instances as SEC boundaries. This is the default. | -| `--no-sec-uncomputable-seq-boundary` | Fail SEC when an unsupported sequential instance is encountered. | | `--allow-boundary-mismatch` | Allow LEC to continue when top-level inputs or sequential-element outputs do not match by name. Without this flag, a mismatch stops the run before SAT solving. LEC only. | | `-verilog` | Use Verilog Format. | | `-naja_if` | Use naja-if format. | @@ -57,7 +55,6 @@ LEC is the default. Select SEC with `-v sec`, `--verification sec`, or | `max_k` | integer | SEC proof/search bound. Defaults to `32`. | | `sec_engine` | string | `k_induction`, `imc`, or `pdr`. Defaults to `pdr`. | | `sec_encoding` | string | `binary` or `dual_rail_steady`. Defaults to `dual_rail_steady`. | -| `sec_uncomputable_seq_as_boundary` | bool | Abstract unsupported sequential instances as SEC boundaries. Defaults to `true`. | | `allow-boundary-mismatch` | bool | Allow an LEC boundary mismatch. Defaults to `false`; ignored for SEC. | | `input_paths` | list | Required for normal runs. Accepts either `[design0, design1]` or `[[design0_file...], [design1_file...]]`. The nested form is for multi-file Verilog. | | `liberty_files` | list[string] | Liberty libraries loaded through `SNLLibertyConstructor`. | @@ -109,7 +106,6 @@ verification: sec max_k: 32 sec_engine: pdr sec_encoding: dual_rail_steady -sec_uncomputable_seq_as_boundary: true input_paths: - [rtl_pkg.sv, rtl_top.sv] - [gate_top.v] diff --git a/docs/sec-clock-handling.md b/docs/sec-clock-handling.md index b60d4380..cc80c040 100644 --- a/docs/sec-clock-handling.md +++ b/docs/sec-clock-handling.md @@ -107,6 +107,9 @@ This means clock gating is modeled as state enable behavior rather than as a new independent clock. Clock-gate latch data that has been folded during SEC extraction is substituted before the clock event is classified, so common integrated clock-gating structures can still expose the intended enable. +Generic latch models are not consumed as SEC state; the validated clock-gate +rewrite and strict fallback behavior are documented in +[sec-sequential-models.md](sec-sequential-models.md). ## Complex Clock Trees diff --git a/docs/sec-flags-spec.md b/docs/sec-flags-spec.md index 9c629907..5816b75a 100644 --- a/docs/sec-flags-spec.md +++ b/docs/sec-flags-spec.md @@ -14,6 +14,10 @@ library, logging, solver, CNF export, and LEC flags remain documented in SEC clock extraction and multi-clock-domain coverage handling are documented in [sec-clock-handling.md](sec-clock-handling.md). +Sequential primitive support, generic latch opacity, and the clock-gate latch +rewrite are documented in +[sec-sequential-models.md](sec-sequential-models.md). + Supported SEC flows: | Flow | Typical inputs | @@ -33,7 +37,6 @@ kepler-formal -verilog \ -k 4 \ --sec-engine pdr \ --sec-encoding dual_rail_steady \ - --sec-uncomputable-seq-boundary \ --report-skipped-pos \ design0.v design1.v library.lib ``` @@ -84,7 +87,6 @@ verification: sec sec_engine: pdr sec_encoding: dual_rail_steady max_k: 32 -sec_uncomputable_seq_as_boundary: true compact_mode: true report_skipped_pos: true solver: kissat @@ -104,8 +106,6 @@ liberty_files: | `-k `, `--max-k ` | `max_k: ` | `32` | Non-negative integer | Sets the SEC proof/search bound. | | `--sec-engine ` | `sec_engine: ` | `pdr` | `k_induction`, `imc`, `pdr` | Selects the top-level SEC proof engine. Engine names are lowercase. | | `--sec-encoding ` | `sec_encoding: ` | `dual_rail_steady` | `binary`, `dual_rail_steady` | Selects how SEC models unknown or reset-unanchored state values. Omit the key/flag to use the dual-rail default. | -| `--sec-uncomputable-seq-boundary` | `sec_uncomputable_seq_as_boundary: true` | `true` | boolean | Abstracts unsupported sequential instances as SEC boundaries instead of failing immediately. | -| `--no-sec-uncomputable-seq-boundary` | `sec_uncomputable_seq_as_boundary: false` | `true` | boolean | Uses strict mode: unsupported sequential interfaces cause SEC to fail as unsupported. | | `--compact` | `compact_mode: true` | `false` | boolean | Enables compact SEC extraction: design 1 is extracted and released before design 2 is loaded; identical SEC inputs can reuse the extracted design 1 model. | | `--report-skipped-pos` | `report_skipped_pos: true` | `false` | boolean | Enables skipped-output reporting and writes SEC boundary reporting when entries exist. | @@ -147,6 +147,14 @@ cross-design equivalence assumption. Internal names may still be used inside a single extracted design for diagnostics, state updates, and local recovery heuristics. +Opaque internal elements always use strict per-output handling. If backward +cone construction reaches any internal cell or pin without usable SEC +semantics, traversal for that top-level output stops and the entire output is +removed from the proof surface. SEC never substitutes a free, shared, or +design-local proof symbol for that element. Other modeled outputs remain +eligible for proof, so the result is partial when only some outputs are skipped +and unsupported when no aligned verifiable output remains. + ## Bounds And Results `max_k` is parsed as a non-negative integer. @@ -174,7 +182,6 @@ The log always prints: SEC max_k: SEC engine: SEC encoding: binary|dual_rail_steady -SEC uncomputable sequentials: boundary abstraction|strict failure Compact mode: enabled|disabled Skipped PO reports: enabled|disabled ``` @@ -186,12 +193,13 @@ write the following files in the current working directory: | File | Producer | Contents | | --- | --- | --- | -| `boundary_terms.txt` | SEC | Extracted SEC boundary surface. Includes top inputs, top outputs, opaque internal cut points, abstracted sequential state terms, abstracted sequential observed terms, and connectivity-skip annotations when present. | +| `boundary_terms.txt` | SEC | Top-level SEC input/output surface and skip annotations when present. | | `skipped_no_driver_pos.txt` | shared cone builder | Outputs skipped because the relevant iso has no driver. | | `skipped_multi_driver_pos.txt` | shared cone builder | Outputs skipped because the relevant iso has multiple drivers. | | `skipped_logical_loop_pos.txt` | shared cone builder | Outputs skipped because the relevant cone contains a logical loop. | | `skipped_reset_unanchored_pos.txt` | SEC | Outputs skipped in binary SEC because their cones depend on reset-unanchored internal state. | | `skipped_multi_clock_domain_pos.txt` | SEC clock model | Outputs skipped because the observed output cone spans multiple extracted clock domains. | +| `skipped_opaque_cells_pos.txt` | SEC | One entry per ignored top-level output whose backward cone reached an opaque internal cell or pin. Each entry names the output, cell, pin, and reason. | `boundary_terms.txt` starts with a category legend. Current categories are: @@ -199,14 +207,10 @@ write the following files in the current working directory: | --- | --- | | `top_input` | Original top-level input term. | | `top_output` | Original top-level output term. | -| `opaque_internal_input` | Internal cut-point input that SEC could not reconstruct combinationally and did not model as sequential. | -| `opaque_internal_output` | Internal cut-point output paired with an opaque internal boundary. | -| `abstracted_sequential_state` | State-facing term exposed when an uncomputable sequential instance is abstracted as a SEC boundary. | -| `abstracted_sequential_observed` | Observed-output-facing term exposed when an uncomputable sequential instance is abstracted as a SEC boundary. | - -Connectivity skipped outputs are also summarized in the main run log, including -no-driver, multi-driver, logical-loop, reset-unanchored, and multi-clock-domain -skips. + +Skipped outputs are also summarized in the main run log, including no-driver, +multi-driver, logical-loop, reset-unanchored, multi-clock-domain, and opaque +internal skips. ## Compact SEC diff --git a/docs/sec-sequential-models.md b/docs/sec-sequential-models.md new file mode 100644 index 00000000..18630ba7 --- /dev/null +++ b/docs/sec-sequential-models.md @@ -0,0 +1,80 @@ +# SEC Sequential Models + +This document describes which Naja sequential models Kepler Formal can use +when extracting a transition system for Sequential Equivalence Checking (SEC). + +## Naja Model Contents + +A Naja sequential model records: + +- whether the element is a flip-flop or latch +- the clock or enable expression in `clockedOn` +- each state's next-state, clear, and preset expressions +- the relationship between physical output pins and modeled state + +Models can come from Naja DB0 primitives, supported frontend lowering, +explicit Python primitive descriptions, or Liberty construction. A cell can +still lack a usable model when its sequential behavior is absent, incomplete, +or not representable by the current constructor. + +## Flip-Flops + +SEC consumes flip-flop models when their state expressions and physical output +mapping are valid. The clock expression is classified using the clock model +described in [sec-clock-handling.md](sec-clock-handling.md), and the extracted +state is used by all three SEC engines. + +Invalid or incomplete state fragments are opaque at output-terminal +granularity. Other independently modeled outputs of the same instance remain +eligible unless their own dependency cones reach an opaque terminal. + +## Latches + +A latch is level-sensitive. For an active-high latch, its behavior is: + +```text +next_q = enable ? data : q +``` + +This is different from a flip-flop's edge-triggered update. Naja represents +that distinction with `SequentialModel::Kind::Latch`, and its built-in +`naja_dlatch` has such a model. + +Kepler SEC does not currently implement generic level-sensitive transition +semantics. It therefore does not consume a Naja latch model as ordinary SEC +state. A generic latch output is opaque and any requested top-level output +whose cone reaches it is skipped. + +## Clock-Gate Latch Rewrite + +SEC has one narrow latch-specific rewrite for recognized clock-gating +structures. This is not a general latch transition model. + +A candidate must have one latch-like output, one data input, and a gate input. +If the primitive has a Naja sequential model, its kind must explicitly be +`Latch`; a `FlipFlop` model never enters this rewrite. The gate must +structurally trace to a pure clock carrier, and both the data and gate +dependency cones must be fully modelable. When all checks pass, SEC +substitutes the latch output with its data expression while reconstructing the +clock-gate enable. The latch output is then removed from the extracted SEC +state and is not published as an environment input. + +This rewrite captures the clock-gating abstraction used by SEC: the latch +holds an enable stable around the active clock edge, while the consuming +flip-flop remains the actual modeled state. If candidate recognition or +dependency validation fails, the latch output remains opaque. + +## Opaque Outputs + +Opacity is strict and local to an output terminal. During backward cone +construction, reaching an opaque internal output stops construction of that +requested top-level output. SEC does not create a free, shared, or substitute +symbol for the opaque value and does not compare the affected output. + +Unaffected top-level outputs remain eligible. The main result reports partial +checked-output coverage, and `--report-skipped-pos` writes details to +`skipped_opaque_cells_pos.txt`, including the top-level output, opaque cell and +pin, and reason. + +The complete SEC reporting and flag behavior is documented in +[sec-flags-spec.md](sec-flags-spec.md). diff --git a/examples/xilinx/register_slice/test_config_verilog_xilinx_sec.yaml b/examples/xilinx/register_slice/test_config_verilog_xilinx_sec.yaml index 9d5a1334..ead8070b 100644 --- a/examples/xilinx/register_slice/test_config_verilog_xilinx_sec.yaml +++ b/examples/xilinx/register_slice/test_config_verilog_xilinx_sec.yaml @@ -5,7 +5,6 @@ format: verilog verification: sec sec_engine: pdr sec_encoding: dual_rail_steady -sec_uncomputable_seq_as_boundary: false input_paths: - xilinx_register_slice_mapped.v - xilinx_register_slice_compact.v diff --git a/regress/run_sec_strategies_regress.sh b/regress/run_sec_strategies_regress.sh index 3ed9b21c..060315db 100644 --- a/regress/run_sec_strategies_regress.sh +++ b/regress/run_sec_strategies_regress.sh @@ -399,6 +399,11 @@ run_engine() { grep "SEC was inconclusive" "${stdout_log}" return 0 fi + if [[ "${kepler_status}" -eq 2 ]] && + grep -q "No aligned observed outputs remain after skipping unverifiable cones" "${stdout_log}"; then + grep "SEC cannot run on this design pair" "${stdout_log}" + return 0 + fi if [[ "${kepler_status}" -ne 0 ]]; then return "${kepler_status}" fi @@ -407,8 +412,8 @@ run_engine() { fi # Non-dual positive SEC regressions may have all observed outputs skipped - # because both sides depend on reset-unanchored internal state. Treat that - # as measurement-only inconclusive when the workflow explicitly asks for it. + # because both sides depend on reset-unanchored state or opaque internal + # cones. Treat that as measurement-only when the workflow explicitly asks. if [[ "${expectation}" == "allow-unset-state-inconclusive" ]]; then if [[ "${kepler_status}" -eq 0 ]] && grep -q "SEC proved equivalence" "${stdout_log}"; then @@ -425,6 +430,11 @@ run_engine() { grep "SEC cannot run on this design pair" "${stdout_log}" return 0 fi + if [[ "${kepler_status}" -eq 2 ]] && + grep -q "No aligned observed outputs remain after skipping unverifiable cones" "${stdout_log}"; then + grep "SEC cannot run on this design pair" "${stdout_log}" + return 0 + fi if [[ "${kepler_status}" -ne 0 ]]; then return "${kepler_status}" fi diff --git a/src/bin/KeplerFormal.cpp b/src/bin/KeplerFormal.cpp index 425711f5..69535c7e 100644 --- a/src/bin/KeplerFormal.cpp +++ b/src/bin/KeplerFormal.cpp @@ -56,6 +56,8 @@ static const char* kSkippedResetUnanchoredPOReport = "skipped_reset_unanchored_pos.txt"; static const char* kSkippedMultiClockDomainPOReport = "skipped_multi_clock_domain_pos.txt"; +static const char* kSkippedOpaqueCellPOReport = + "skipped_opaque_cells_pos.txt"; static void addNajaPythonPath(const char* argv0) { if (!argv0 || !*argv0) { @@ -117,12 +119,12 @@ static void print_usage(const char* prog) { " [...] | " "<-naja_if/-verilog/-systemverilog/-sv/-sv2v> --design1 --design2 " " [--verilog_design1_top ] [--verilog_design2_top ] [--liberty ...] [-v ] [-k ] [--sec-engine ] [--sec-encoding ] " - "[--no-sec-uncomputable-seq-boundary] [--allow-boundary-mismatch] [--compact] " + "[--allow-boundary-mismatch] [--compact] " "[--report-skipped-pos] | " "-systemverilog/-sv [--sv_design1_flist ] [--sv_design1_top ] " "[--sv_design2_flist ] [--sv_design2_top ] [-v ] [-k ] [--sec-engine ] [--sec-encoding ] " "[--design1 ] [--design2 ] " - "[--no-sec-uncomputable-seq-boundary] [--allow-boundary-mismatch] [--compact] " + "[--allow-boundary-mismatch] [--compact] " "[--report-skipped-pos]", prog); // LCOV_EXCL_START @@ -387,7 +389,6 @@ static bool validateConfigKeys(const YAML::Node& cfg) { "max_k", "sec_engine", "sec_encoding", - "sec_uncomputable_seq_as_boundary", "allow-boundary-mismatch", "input_paths", "liberty_files", @@ -493,11 +494,7 @@ void writeBoundaryTermsReport( std::ofstream report(reportPath, std::ios::trunc); report << "# SEC boundary terms report\n"; report << "# Categories:\n"; - report << "# - top_input / top_output: original top-level interface terms.\n"; - report << "# - opaque_internal_input / opaque_internal_output: internal leaf cut points\n"; - report << "# that SEC could not reconstruct combinationally and did not model as sequential.\n"; - report << "# - abstracted_sequential_state / abstracted_sequential_observed: interface terms\n"; - report << "# exposed when an uncomputable sequential instance is abstracted as a SEC boundary.\n\n"; + report << "# - top_input / top_output: original top-level interface terms.\n\n"; for (size_t i = 0; i < reports.size(); ++i) { const auto& entry = reports[i]; report << "- design: " << entry.design << "\n"; @@ -565,6 +562,23 @@ void writeMultiClockDomainSkippedOutputsReport( } // LCOV_EXCL_STOP +void writeOpaqueCellSkippedOutputsReport( + const std::filesystem::path& reportPath, + const std::vector& skippedOutputs) { + if (skippedOutputs.empty()) { + return; + } + + std::ofstream report(reportPath, std::ios::trunc); + report << "# SEC opaque-cell skipped top-level outputs\n"; + report << "# These outputs were not verified because backward cone traversal\n"; + report << "# reached an internal cell or pin without usable semantics. No free\n"; + report << "# or shared proof symbol was substituted for the opaque element.\n\n"; + for (const auto& skippedOutput : skippedOutputs) { + report << "- " << skippedOutput << "\n"; + } +} + struct DesignInputs { std::vector design0; std::vector design1; @@ -1187,8 +1201,6 @@ int KeplerFormalMain(int argc, char** argv) { bool secEncodingExplicit = false; size_t secMaxK = kDefaultSecMaxK; bool secMaxKExplicit = false; - bool secTreatUncomputableSeqAsBoundary = true; - // Basic argument sanity if (argc < 2) { // LCOV_EXCL_START @@ -1214,7 +1226,6 @@ int KeplerFormalMain(int argc, char** argv) { std::string dumpPoCnfPath; KEPLER_FORMAL::Config::setReportSkippedPOs(false); - KEPLER_FORMAL::Config::setSecTreatUncomputableSeqAsBoundary(true); for (int i = 1; i < argc; ++i) { std::string a = argv[i]; @@ -1332,22 +1343,6 @@ int KeplerFormalMain(int argc, char** argv) { secEncodingExplicit = true; // LCOV_EXCL_LINE } - if (cfg["sec_uncomputable_seq_as_boundary"]) { - // LCOV_EXCL_START - if (!cfg["sec_uncomputable_seq_as_boundary"].IsScalar()) { - SPDLOG_CRITICAL( - // LCOV_EXCL_STOP - "sec_uncomputable_seq_as_boundary must be a scalar"); - // LCOV_EXCL_START - return EXIT_FAILURE; - // LCOV_EXCL_STOP - } - // LCOV_EXCL_START - secTreatUncomputableSeqAsBoundary = - cfg["sec_uncomputable_seq_as_boundary"].as(); - } - // LCOV_EXCL_STOP - // input_paths if (cfg["input_paths"]) { std::string inputError; @@ -1576,19 +1571,6 @@ int KeplerFormalMain(int argc, char** argv) { parseStart += 2; continue; } - if (arg == "--sec-uncomputable-seq-boundary") { - secTreatUncomputableSeqAsBoundary = true; - ++parseStart; - continue; - // LCOV_EXCL_STOP - } - // LCOV_EXCL_START - if (arg == "--no-sec-uncomputable-seq-boundary") { - secTreatUncomputableSeqAsBoundary = false; - ++parseStart; - continue; - // LCOV_EXCL_STOP - } if (arg == "--allow-boundary-mismatch") { allowBoundaryMismatch = true; ++parseStart; @@ -1711,17 +1693,6 @@ int KeplerFormalMain(int argc, char** argv) { secEncodingExplicit = true; continue; } - if (arg == "--sec-uncomputable-seq-boundary") { - secTreatUncomputableSeqAsBoundary = true; - continue; - // LCOV_EXCL_STOP - } - // LCOV_EXCL_START - if (arg == "--no-sec-uncomputable-seq-boundary") { - secTreatUncomputableSeqAsBoundary = false; - continue; - // LCOV_EXCL_STOP - } if (arg == "--allow-boundary-mismatch") { allowBoundaryMismatch = true; continue; @@ -2012,8 +1983,6 @@ int KeplerFormalMain(int argc, char** argv) { auto solverType = KEPLER_FORMAL::Config::getSolverType(); KEPLER_FORMAL::Config::setReportSkippedPOs(reportSkippedPOs); - KEPLER_FORMAL::Config::setSecTreatUncomputableSeqAsBoundary( - secTreatUncomputableSeqAsBoundary); const char* solverName = solverType == KEPLER_FORMAL::Config::SolverType::KISSAT ? "KISSAT" @@ -2028,10 +1997,6 @@ int KeplerFormalMain(int argc, char** argv) { SPDLOG_INFO("SEC max_k: {}", secMaxK); SPDLOG_INFO("SEC engine: {}", secEngineName(secEngine)); SPDLOG_INFO("SEC encoding: {}", secEncodingName(secEncoding)); - SPDLOG_INFO( - "SEC uncomputable sequentials: {}", - secTreatUncomputableSeqAsBoundary ? "boundary abstraction" - : "strict failure"); } SPDLOG_INFO("Compact mode: {}", compactMode ? "enabled" : "disabled"); SPDLOG_INFO("Skipped PO reports: {}", reportSkippedPOs ? "enabled" : "disabled"); @@ -2091,24 +2056,6 @@ int KeplerFormalMain(int argc, char** argv) { // LCOV_EXCL_START } // LCOV_EXCL_STOP - // LCOV_EXCL_START - if (!result.abstractedSequentialBoundaries.empty()) { - // LCOV_DISABLED_START - std::ostringstream abstractedBoundaries; - for (const auto& abstractedBoundary : - result.abstractedSequentialBoundaries) { - abstractedBoundaries << " - " << abstractedBoundary << "\n"; - // LCOV_DISABLED_STOP - } - // LCOV_DISABLED_START - SPDLOG_INFO( - // LCOV_DISABLED_STOP - "SEC abstracted uncomputable sequential interfaces as " - "boundaries:\n{}", - abstractedBoundaries.str()); - // LCOV_DISABLED_START - } - // LCOV_DISABLED_STOP // LCOV_EXCL_STOP if (reportSkippedPOs) { // LCOV_EXCL_START @@ -2120,6 +2067,9 @@ int KeplerFormalMain(int argc, char** argv) { writeMultiClockDomainSkippedOutputsReport( kSkippedMultiClockDomainPOReport, result.multiClockDomainSkippedOutputs); + writeOpaqueCellSkippedOutputsReport( + kSkippedOpaqueCellPOReport, + result.opaqueCellSkippedOutputs); } // LCOV_EXCL_STOP switch (result.status) { diff --git a/src/bin/KeplerFormalUtils.h b/src/bin/KeplerFormalUtils.h index 241b719d..6908a9ea 100644 --- a/src/bin/KeplerFormalUtils.h +++ b/src/bin/KeplerFormalUtils.h @@ -29,3 +29,7 @@ void writeResetUnanchoredSkippedOutputsReport( void writeMultiClockDomainSkippedOutputsReport( const std::filesystem::path& reportPath, const std::vector& skippedOutputs); + +void writeOpaqueCellSkippedOutputsReport( + const std::filesystem::path& reportPath, + const std::vector& skippedOutputs); diff --git a/src/clauses/SNLLogicCloud.cpp b/src/clauses/SNLLogicCloud.cpp index c976b011..d096c676 100644 --- a/src/clauses/SNLLogicCloud.cpp +++ b/src/clauses/SNLLogicCloud.cpp @@ -1240,6 +1240,63 @@ naja::DNL::DNLID SNLLogicCloud::resolveTransparentLoopTarget( return currentTermID; } +bool SNLLogicCloud::rejectOpaqueInternalOutput(naja::DNL::DNLID termID) { + if (!stopAtOpaqueInternalOutputs_ || isInput(termID)) { + return false; + } + + const auto& term = dnl_.getDNLTerminalFromID(termID); + if (term.isNull() || term.isTopPort() || + term.getSnlBitTerm()->getDirection() != SNLBitTerm::Direction::Output) { + return false; // LCOV_EXCL_LINE - callers pass an internal iso driver. + } + const auto truthTable = SNLDesignModeling::getTruthTable( + term.getDNLInstance().getSNLInstance(), + term.getSnlBitTerm()->getOrderID()); + if (truthTable.isInitialized()) { + return false; + } + + std::string reason = + "no initialized combinational truth table or usable sequential model"; + const auto relatedClocks = + SNLDesignModeling::getOutputRelatedClocks(term.getSnlBitTerm()); + if (!relatedClocks.empty()) { + const auto* model = term.getDNLInstance().getSNLModel(); + if (model == nullptr || !SNLDesignModeling::hasSequentialModel(model)) { + reason = "Missing Naja sequential model"; + } else if (SNLDesignModeling::getSequentialModel(model).kind == + SNLDesignModeling::SequentialModel::Kind::Latch) { + reason = "Naja latch sequential models are not supported by SEC"; + } else { + reason = "the sequential output has no usable SEC model"; + } + } + + std::string instance = term.getDNLInstance().getFullPath(); + while (!instance.empty() && + (instance.back() == '/' || instance.back() == '.')) { + instance.pop_back(); + } + if (instance.empty()) { + // DNL uses the instance ID when an SNL instance has no name. + instance = ""; // LCOV_EXCL_LINE + } + std::ostringstream detail; + detail << "opaque internal cell `" << instance << "`"; + if (const auto* model = term.getDNLInstance().getSNLModel()) { + detail << " (model `" << model->getName().getString() << "`)"; + } + detail << " pin `" << term.getSnlBitTerm()->getName().getString() << "[" + << term.getSnlBitTerm()->getBit() << "]`: " << reason; + + skipReason_ = SkipReason::OpaqueInternal; + skipReasonText_ = detail.str(); + opaqueInternalTerm_ = termID; + table_ = SNLTruthTableTree(); + return true; +} + void SNLLogicCloud::compute() { refreshPerDnlCaches(dnl_); clearNewIterationInputsTL(); @@ -1286,6 +1343,9 @@ void SNLLogicCloud::compute() { // LCOV_EXCL_STOP const auto& driver = iso.getDrivers().front(); auto& inst = dnl_.getDNLTerminalFromID(driver).getDNLInstance(); + if (rejectOpaqueInternalOutput(driver)) { + return; + } if (isInput(driver)) { currentIterationInputs.emplace_back(driver); table_ = SNLTruthTableTree(inst.getID(), driver, @@ -1468,7 +1528,9 @@ void SNLLogicCloud::compute() { // LCOV_EXCL_STOP } const auto& driver = iso.getDrivers().front(); - + if (rejectOpaqueInternalOutput(driver)) { + return; + } if (isInput(driver) || canUseCachedIsoShortcut(iso, driver)) { newIterationInputs.emplace_back(driver); DEBUG_LOG( diff --git a/src/clauses/SNLLogicCloud.h b/src/clauses/SNLLogicCloud.h index 6c8687ba..fbf8bff6 100644 --- a/src/clauses/SNLLogicCloud.h +++ b/src/clauses/SNLLogicCloud.h @@ -17,13 +17,16 @@ class SNLLogicCloud { MultiDriver, NoDriver, LogicalLoop, + OpaqueInternal, }; SNLLogicCloud(naja::DNL::DNLID seedOutputTerm, const std::vector& PIs, - const std::vector& POs) + const std::vector& POs, + bool stopAtOpaqueInternalOutputs = false) : seedOutputTerm_(seedOutputTerm), dnl_(*naja::DNL::get()), - PIs_(PIs), POs_(POs) { + PIs_(PIs), POs_(POs), + stopAtOpaqueInternalOutputs_(stopAtOpaqueInternalOutputs) { } void compute(); static void flushSkippedPOReports(); @@ -31,6 +34,9 @@ class SNLLogicCloud { bool isOutput(naja::DNL::DNLID inputTerm); SkipReason getSkipReason() const { return skipReason_; } const std::string& getSkipReasonText() const { return skipReasonText_; } + naja::DNL::DNLID getOpaqueInternalTerm() const { + return opaqueInternalTerm_; + } SNLTruthTableTree& getTruthTable() { return table_; } const std::vector>& getInputs() const { return currentIterationInputs_; @@ -95,6 +101,7 @@ class SNLLogicCloud { naja::DNL::DNLID termID, const std::shared_ptr>& termIsoIDs) const; + bool rejectOpaqueInternalOutput(naja::DNL::DNLID termID); naja::DNL::DNLID seedOutputTerm_; TermIDVector currentIterationInputs_; @@ -102,8 +109,10 @@ class SNLLogicCloud { const naja::DNL::DNLFull& dnl_; const std::vector& PIs_; const std::vector& POs_; + bool stopAtOpaqueInternalOutputs_ = false; SkipReason skipReason_ = SkipReason::None; std::string skipReasonText_; + naja::DNL::DNLID opaqueInternalTerm_ = naja::DNL::DNLID_MAX; }; } // namespace KEPLER_FORMAL diff --git a/src/config/Config.h b/src/config/Config.h index d570e173..5799f4c7 100644 --- a/src/config/Config.h +++ b/src/config/Config.h @@ -39,21 +39,12 @@ class Config { return reportSkippedPOs_; } - static void setSecTreatUncomputableSeqAsBoundary(bool enabled) { - secTreatUncomputableSeqAsBoundary_ = enabled; - } - - static bool getSecTreatUncomputableSeqAsBoundary() { // LCOV_EXCL_LINE - return secTreatUncomputableSeqAsBoundary_; // LCOV_EXCL_LINE - } - private: Config() = default; ~Config() = default; inline static SolverType solverType_ = KISSAT; inline static bool reportSkippedPOs_ = false; - inline static bool secTreatUncomputableSeqAsBoundary_ = true; }; } // namespace kepler diff --git a/src/sec/BUILD.bazel b/src/sec/BUILD.bazel index 2d5cf35a..eb67fc78 100644 --- a/src/sec/BUILD.bazel +++ b/src/sec/BUILD.bazel @@ -6,7 +6,6 @@ cc_library( name = "sec_common_headers", hdrs = [ "common/AlignedSignals.h", - "common/PrivateProofSymbol.h", "common/SignalKey.h", ], includes = [ @@ -31,6 +30,7 @@ cc_library( "kinduction/KInductionEngine.cpp", "kinduction/OutputBatching.cpp", "kinduction/SatEncoding.cpp", + "model/SecNetlistChecks.cpp", "model/SequentialDesignModel.cpp", "pdr/PDREngine.cpp", "proof/DualRailEncoding.cpp", @@ -41,7 +41,6 @@ cc_library( hdrs = [ "common/AlignedSignals.h", "common/BoolExprUtils.h", - "common/PrivateProofSymbol.h", "common/ProofProblemDebug.h", "common/SecDiag.h", "common/SignalKey.h", @@ -54,6 +53,7 @@ cc_library( "kinduction/KInductionProblem.h", "kinduction/OutputBatching.h", "kinduction/SatEncoding.h", + "model/SecNetlistChecks.h", "model/SequentialDesignModel.h", "pdr/PDREngine.h", "proof/DualRailEncoding.h", diff --git a/src/sec/CMakeLists.txt b/src/sec/CMakeLists.txt index 8bf26089..cc91b8a5 100644 --- a/src/sec/CMakeLists.txt +++ b/src/sec/CMakeLists.txt @@ -9,6 +9,7 @@ add_library(kepler_sec STATIC imc/IMCEngine.cpp kinduction/KInductionEngine.cpp kinduction/OutputBatching.cpp + model/SecNetlistChecks.cpp model/SequentialDesignModel.cpp pdr/PDREngine.cpp proof/DualRailEncoding.cpp diff --git a/src/sec/common/PrivateProofSymbol.h b/src/sec/common/PrivateProofSymbol.h deleted file mode 100644 index bf197a01..00000000 --- a/src/sec/common/PrivateProofSymbol.h +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright 2024-2026 keplertech.io -// SPDX-License-Identifier: GPL-3.0-only - -#pragma once - -#include -#include - -namespace KEPLER_FORMAL::SEC { - -// LCOV_EXCL_START -inline size_t makePrivateProofLeafSymbol(size_t designIndex, size_t localSymbol) { -// LCOV_EXCL_STOP - // Lazy SEC transitions may expose design-local support that is not a top - // input or a modeled state bit. Keep it in a high, design-salted symbol range - // so it cannot collide with the dense shared SEC symbols allocated from 2 up. - // LCOV_EXCL_START - const size_t max = std::numeric_limits::max(); - const size_t salt = - localSymbol <= (max - 3) / 2 ? localSymbol * 2 + (designIndex & 1) : localSymbol; - return max - salt; - // LCOV_EXCL_STOP -} - -} // namespace KEPLER_FORMAL::SEC diff --git a/src/sec/model/SecNetlistChecks.cpp b/src/sec/model/SecNetlistChecks.cpp new file mode 100644 index 00000000..2f33fff3 --- /dev/null +++ b/src/sec/model/SecNetlistChecks.cpp @@ -0,0 +1,451 @@ +// Copyright 2024-2026 keplertech.io +// SPDX-License-Identifier: GPL-3.0-only + +#include "model/SecNetlistChecks.h" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "SNLDesignModeling.h" + +namespace KEPLER_FORMAL::SEC { + +namespace { + +using DNLID = naja::DNL::DNLID; +using BooleanExpression = naja::NL::SNLDesignModeling::BooleanExpression; +using DependencyGraph = std::vector>; + +void addDependency(DependencyGraph& dependencies, DNLID source, DNLID output) { + if (source == naja::DNL::DNLID_MAX || output == naja::DNL::DNLID_MAX || + source >= dependencies.size() || output >= dependencies.size()) { + return; + } + dependencies[source].push_back(output); +} + +struct ExpressionDependencies { + std::vector terms; + std::vector states; +}; + +ExpressionDependencies +collectExpressionDependencies(const BooleanExpression& expression) { + ExpressionDependencies dependencies; + if (!expression.isValid()) { + return dependencies; + } + + std::vector visited(expression.nodes.size(), 0); + std::vector work{expression.root}; + std::unordered_set seenTerms; + std::unordered_set seenStates; + while (!work.empty()) { + const auto nodeID = work.back(); + work.pop_back(); + if (nodeID >= expression.nodes.size() || visited[nodeID] != 0) { + continue; + } + visited[nodeID] = 1; + const auto& node = expression.nodes[nodeID]; + if (node.operation == BooleanExpression::Operator::Term && + node.term != nullptr && seenTerms.insert(node.term).second) { + dependencies.terms.push_back(node.term); + } else if (node.operation == BooleanExpression::Operator::State && + seenStates.insert(node.state).second) { + dependencies.states.push_back(node.state); + } + work.insert(work.end(), node.operands.begin(), node.operands.end()); + } + return dependencies; +} + +using InstanceTermMap = std::unordered_map; + +void addExpressionDependencies( + const BooleanExpression& expression, const std::vector& targets, + const InstanceTermMap& instanceTerms, + const std::vector>& stateSources, + DependencyGraph& dependencies) { + if (targets.empty()) { + return; + } + const auto sources = collectExpressionDependencies(expression); + for (const auto* sourceTerm : sources.terms) { + const auto sourceIt = instanceTerms.find(sourceTerm); + if (sourceIt == instanceTerms.end()) { + continue; + } + for (const auto target : targets) { + addDependency(dependencies, sourceIt->second, target); + } + } + for (const auto sourceState : sources.states) { + if (sourceState >= stateSources.size()) { + continue; + } + for (const auto source : stateSources[sourceState]) { + for (const auto target : targets) { + addDependency(dependencies, source, target); + } + } + } +} + +std::optional findInstanceTerm(const InstanceTermMap& instanceTerms, + const naja::NL::SNLBitTerm* term) { + if (term == nullptr) { + return std::nullopt; + } + const auto it = instanceTerms.find(term); + return it == instanceTerms.end() ? std::nullopt + : std::optional{it->second}; +} + +bool supportsStructuredMemoryModel( + const naja::NL::SNLDesignModeling::MemoryInterface& interface) { + if (!interface.isValid()) { + return false; // LCOV_EXCL_LINE - Naja rejects invalid interfaces on set. + } + for (const auto& readPort : interface.readPorts) { + if (readPort.address.size() != interface.abits || + readPort.data.size() != interface.width) { + return false; + } + } + for (const auto& writePort : interface.writePorts) { + if (writePort.address.size() != interface.abits || + writePort.data.size() != interface.width || + (!writePort.mask.empty() && writePort.mask.size() != interface.width) || + !writePort.extraWriteInputs.empty()) { + return false; + } + } + return true; +} + +bool isDisabledMemoryWriteEnable(naja::DNL::DNLFull* dnl, DNLID termID) { + if (termID == naja::DNL::DNLID_MAX) { + return true; // LCOV_EXCL_LINE - callers pass only mapped instance terms. + } + const auto& term = dnl->getDNLTerminalFromID(termID); + if (term.isNull() || term.getIsoID() == naja::DNL::DNLID_MAX) { + return true; // LCOV_EXCL_LINE - mapped terms receive an iso from DNL. + } + const auto& iso = dnl->getDNLIsoDB().getIsoFromIsoIDconst(term.getIsoID()); + return iso.isConstant0() || (!iso.isConstant() && iso.getDrivers().empty()); +} + +void addCombinationalDependencies(const naja::DNL::DNLInstanceFull& instance, + const InstanceTermMap& instanceTerms, + const std::unordered_set& opaqueTerms, + DependencyGraph& dependencies) { + for (const auto& [outputBitTerm, outputTermID] : instanceTerms) { + if (outputBitTerm->getDirection() == + naja::NL::SNLBitTerm::Direction::Input || + opaqueTerms.find(outputTermID) != opaqueTerms.end()) { + continue; + } + const auto truthTable = naja::NL::SNLDesignModeling::getTruthTable( + instance.getSNLInstance(), outputBitTerm->getOrderID()); + if (!truthTable.isInitialized()) { + continue; + } + for (auto* inputBitTerm : + naja::NL::SNLDesignModeling::getCombinatorialInputs( + const_cast(outputBitTerm))) { + const auto inputIt = instanceTerms.find(inputBitTerm); + if (inputIt != instanceTerms.end()) { + addDependency(dependencies, inputIt->second, outputTermID); + } + } + } +} + +void addSequentialDependencies(const naja::DNL::DNLInstanceFull& instance, + const InstanceTermMap& instanceTerms, + const std::unordered_set& opaqueTerms, + DependencyGraph& dependencies) { + const auto* primitive = instance.getSNLModel(); + if (primitive == nullptr || + !naja::NL::SNLDesignModeling::hasSequentialModel(primitive)) { + return; + } + const auto& model = + naja::NL::SNLDesignModeling::getSequentialModel(primitive); + if (model.kind != + naja::NL::SNLDesignModeling::SequentialModel::Kind::FlipFlop) { + return; + } + + std::vector> physicalOutputsByState(model.states.size()); + std::vector> modeledOutputsByState(model.states.size()); + std::vector allModeledOutputs; + for (const auto& output : model.outputs) { + const auto outputIt = instanceTerms.find(output.term); + if (outputIt == instanceTerms.end()) { + continue; + } + const auto outputDependencies = + collectExpressionDependencies(output.function); + for (const auto state : outputDependencies.states) { + if (state >= model.states.size()) { + continue; + } + physicalOutputsByState[state].push_back(outputIt->second); + if (opaqueTerms.find(outputIt->second) == opaqueTerms.end()) { + modeledOutputsByState[state].push_back(outputIt->second); + allModeledOutputs.push_back(outputIt->second); + } + } + } + + std::vector> stateSources = modeledOutputsByState; + for (size_t state = 0; state < stateSources.size(); ++state) { + if (stateSources[state].empty()) { + stateSources[state] = physicalOutputsByState[state]; + } + } + std::sort(allModeledOutputs.begin(), allModeledOutputs.end()); + allModeledOutputs.erase( + std::unique(allModeledOutputs.begin(), allModeledOutputs.end()), + allModeledOutputs.end()); + + addExpressionDependencies(model.clockedOn, allModeledOutputs, instanceTerms, + stateSources, dependencies); + for (size_t state = 0; state < model.states.size(); ++state) { + auto& targets = modeledOutputsByState[state]; + std::sort(targets.begin(), targets.end()); + targets.erase(std::unique(targets.begin(), targets.end()), targets.end()); + const auto& sequentialState = model.states[state]; + addExpressionDependencies(sequentialState.nextState, targets, instanceTerms, + stateSources, dependencies); + if (sequentialState.clear.has_value()) { + addExpressionDependencies(*sequentialState.clear, targets, instanceTerms, + stateSources, dependencies); + } + if (sequentialState.preset.has_value()) { + addExpressionDependencies(*sequentialState.preset, targets, instanceTerms, + stateSources, dependencies); + } + } +} + +void addStructuredMemoryDependencies( + naja::DNL::DNLFull* dnl, const naja::DNL::DNLInstanceFull& instance, + const InstanceTermMap& instanceTerms, + const std::unordered_set& opaqueTerms, + DependencyGraph& dependencies) { + const auto* primitive = instance.getSNLModel(); + if (primitive == nullptr || + !naja::NL::SNLDesignModeling::hasMemoryInterface(primitive)) { + return; + } + const auto memory = naja::NL::SNLDesignModeling::getMemoryInterface( + instance.getSNLInstance()); + if (!supportsStructuredMemoryModel(memory)) { + return; + } + + std::vector allReadOutputs; + std::vector> readOutputs(memory.readPorts.size()); + for (size_t port = 0; port < memory.readPorts.size(); ++port) { + for (const auto* dataTerm : memory.readPorts[port].data) { + const auto output = findInstanceTerm(instanceTerms, dataTerm); + if (output.has_value() && + opaqueTerms.find(*output) == opaqueTerms.end()) { + readOutputs[port].push_back(*output); + allReadOutputs.push_back(*output); + } + } + } + std::sort(allReadOutputs.begin(), allReadOutputs.end()); + allReadOutputs.erase( + std::unique(allReadOutputs.begin(), allReadOutputs.end()), + allReadOutputs.end()); + + auto addTermsToTargets = [&](const auto& terms, + const std::vector& targets) { + for (const auto* term : terms) { + const auto source = findInstanceTerm(instanceTerms, term); + if (!source.has_value()) { + continue; // LCOV_EXCL_LINE - Naja validates memory-term ownership. + } + for (const auto target : targets) { + addDependency(dependencies, *source, target); + } + } + }; + for (size_t port = 0; port < memory.readPorts.size(); ++port) { + addTermsToTargets(memory.readPorts[port].address, readOutputs[port]); + } + for (const auto& writePort : memory.writePorts) { + const bool disabled = + std::any_of(writePort.enables.begin(), writePort.enables.end(), + [&](const auto* enableTerm) { + const auto enable = + findInstanceTerm(instanceTerms, enableTerm); + return !enable.has_value() || + isDisabledMemoryWriteEnable(dnl, *enable); + }); + if (disabled) { + continue; + } + addTermsToTargets(writePort.address, allReadOutputs); + addTermsToTargets(writePort.data, allReadOutputs); + addTermsToTargets(writePort.mask, allReadOutputs); + addTermsToTargets(writePort.enables, allReadOutputs); + } + if (memory.resetMode != naja::NL::SNLDesignModeling::MemoryResetMode::None) { + const auto source = findInstanceTerm(instanceTerms, memory.reset); + if (!source.has_value()) { + return; + } + for (const auto target : allReadOutputs) { + addDependency(dependencies, *source, target); + } + } +} + +DependencyGraph buildLocalDependencyGraph( + naja::DNL::DNLFull* dnl, const std::unordered_set& opaqueTerms, + const std::vector& extraDependencies) { + DependencyGraph dependencies(dnl->getNBterms()); + for (const auto leafID : dnl->getLeaves()) { + const auto& instance = dnl->getDNLInstanceFromID(leafID); + InstanceTermMap instanceTerms; + for (DNLID termID = instance.getTermIndexes().first; + termID != naja::DNL::DNLID_MAX && + termID <= instance.getTermIndexes().second; + ++termID) { + const auto& term = dnl->getDNLTerminalFromID(termID); + if (!term.isNull()) { + instanceTerms.emplace(term.getSnlBitTerm(), termID); + } + } + addCombinationalDependencies(instance, instanceTerms, opaqueTerms, + dependencies); + addSequentialDependencies(instance, instanceTerms, opaqueTerms, + dependencies); + addStructuredMemoryDependencies(dnl, instance, instanceTerms, opaqueTerms, + dependencies); + } + for (const auto& dependency : extraDependencies) { + addDependency(dependencies, dependency.sourceTermID, + dependency.outputTermID); + } + for (auto& outputs : dependencies) { + std::sort(outputs.begin(), outputs.end()); + outputs.erase(std::unique(outputs.begin(), outputs.end()), outputs.end()); + } + return dependencies; +} + +std::vector findReachedTopOutputs(naja::DNL::DNLFull* dnl, + const DependencyGraph& dependencies, + DNLID seed, + std::vector& visited, + std::vector& work) { + std::vector reached; + auto enqueue = [&](DNLID termID) { + if (termID < visited.size() && visited[termID] == 0) { + visited[termID] = 1; + work.push_back(termID); + } + }; + enqueue(seed); + for (size_t next = 0; next < work.size(); ++next) { + const DNLID termID = work[next]; + const auto& term = dnl->getDNLTerminalFromID(termID); + if (term.isNull()) { + continue; + } + if (term.isTopPort() && term.getSnlBitTerm()->getDirection() != + naja::NL::SNLBitTerm::Direction::Input) { + reached.push_back(termID); + continue; + } + for (const auto output : dependencies[termID]) { + enqueue(output); + } + if (term.getSnlBitTerm()->getDirection() == + naja::NL::SNLBitTerm::Direction::Input || + term.getIsoID() == naja::DNL::DNLID_MAX) { + continue; + } + const auto& iso = dnl->getDNLIsoDB().getIsoFromIsoIDconst(term.getIsoID()); + for (const auto reader : iso.getReaders()) { + enqueue(reader); + } + } + std::sort(reached.begin(), reached.end()); + reached.erase(std::unique(reached.begin(), reached.end()), reached.end()); + return reached; +} + +} // namespace + +std::vector +SecNetlistChecks::findTopOutputsReachedByOpaqueTerminals( + std::vector opaqueSeeds, + const std::vector& extraDependencies) const { + if (dnl_ == nullptr || opaqueSeeds.empty()) { + return {}; + } + std::sort( + opaqueSeeds.begin(), opaqueSeeds.end(), + [](const auto& lhs, const auto& rhs) { return lhs.termID < rhs.termID; }); + opaqueSeeds.erase(std::unique(opaqueSeeds.begin(), opaqueSeeds.end(), + [](const auto& lhs, const auto& rhs) { + return lhs.termID == rhs.termID; + }), + opaqueSeeds.end()); + + std::unordered_set opaqueTerms; + opaqueTerms.reserve(opaqueSeeds.size()); + for (const auto& seed : opaqueSeeds) { + opaqueTerms.insert(seed.termID); + } + const auto dependencies = + buildLocalDependencyGraph(dnl_, opaqueTerms, extraDependencies); + + std::vector> reachedBySeed(opaqueSeeds.size()); + tbb::parallel_for(tbb::blocked_range(0, opaqueSeeds.size()), + [&](const tbb::blocked_range& range) { + std::vector visited(dnl_->getNBterms(), 0); + std::vector work; + for (size_t seedIndex = range.begin(); + seedIndex != range.end(); ++seedIndex) { + reachedBySeed[seedIndex] = findReachedTopOutputs( + dnl_, dependencies, opaqueSeeds[seedIndex].termID, + visited, work); + for (const auto termID : work) { + visited[termID] = 0; + } + work.clear(); + } + }); + + std::map sourceByTopOutput; + for (size_t seedIndex = 0; seedIndex < opaqueSeeds.size(); ++seedIndex) { + for (const auto topOutput : reachedBySeed[seedIndex]) { + sourceByTopOutput.try_emplace(topOutput, opaqueSeeds[seedIndex]); + } + } + std::vector result; + result.reserve(sourceByTopOutput.size()); + for (auto& [topOutput, source] : sourceByTopOutput) { + result.push_back({topOutput, std::move(source)}); + } + return result; +} + +} // namespace KEPLER_FORMAL::SEC diff --git a/src/sec/model/SecNetlistChecks.h b/src/sec/model/SecNetlistChecks.h new file mode 100644 index 00000000..8b3258f7 --- /dev/null +++ b/src/sec/model/SecNetlistChecks.h @@ -0,0 +1,41 @@ +// Copyright 2024-2026 keplertech.io +// SPDX-License-Identifier: GPL-3.0-only + +#pragma once + +#include +#include + +#include "DNL.h" + +namespace KEPLER_FORMAL::SEC { + +struct OpaqueTerminalSeed { + naja::DNL::DNLID termID = naja::DNL::DNLID_MAX; + std::string detail; +}; + +struct SecLocalTerminalDependency { + naja::DNL::DNLID sourceTermID = naja::DNL::DNLID_MAX; + naja::DNL::DNLID outputTermID = naja::DNL::DNLID_MAX; +}; + +struct OpaqueReachedTopOutput { + naja::DNL::DNLID topOutputTermID = naja::DNL::DNLID_MAX; + OpaqueTerminalSeed source; +}; + +class SecNetlistChecks { + public: + explicit SecNetlistChecks(naja::DNL::DNLFull* dnl) : dnl_(dnl) {} + + std::vector findTopOutputsReachedByOpaqueTerminals( + std::vector opaqueSeeds, + const std::vector& extraDependencies = {}) + const; + + private: + naja::DNL::DNLFull* dnl_ = nullptr; +}; + +} // namespace KEPLER_FORMAL::SEC diff --git a/src/sec/model/SequentialDesignModel.cpp b/src/sec/model/SequentialDesignModel.cpp index c9bb1bcf..5ea8ba08 100644 --- a/src/sec/model/SequentialDesignModel.cpp +++ b/src/sec/model/SequentialDesignModel.cpp @@ -6,13 +6,11 @@ #include #include #include -#include #include #include #include #include #include -#include #include #include #include @@ -33,8 +31,8 @@ #include "SNLPath.h" #include "../../clauses/SNLLogicCloud.h" #include "../../clauses/Tree2BoolExpr.h" -#include "../../config/Config.h" #include "common/BoolExprUtils.h" +#include "model/SecNetlistChecks.h" #include "../../strategies/miter/BuildPrimaryOutputClauses.h" namespace KEPLER_FORMAL::SEC { @@ -56,6 +54,11 @@ struct StateOutputTerm { bool complemented = false; }; +struct OpaqueStateOutputTerm { + naja::DNL::DNLID termID = naja::DNL::DNLID_MAX; + std::string reason; +}; + struct PendingStateReference { naja::DNL::DNLID termID = naja::DNL::DNLID_MAX; bool complemented = false; @@ -66,7 +69,6 @@ struct PendingTransition { // LCOV_EXCL_LINE naja::DNL::DNLID stateTermID = naja::DNL::DNLID_MAX; bool stateOutputIsComplemented = false; size_t modelStateIndex = 0; - size_t boundaryInfoIndex = std::numeric_limits::max(); std::vector complementedStateKeys; PendingTermMap modelTermIDs; std::vector stateReferences; @@ -108,44 +110,20 @@ struct PendingMemoryInstance { naja::NL::SNLDesignModeling::MemoryResetMode resetMode = naja::NL::SNLDesignModeling::MemoryResetMode::None; std::optional resetTermID; - size_t boundaryInfoIndex = std::numeric_limits::max(); std::vector readPorts; std::vector writePorts; std::vector cellStates; std::vector readOutputs; }; -struct BoundaryObservedTerm { - naja::DNL::DNLID termID = naja::DNL::DNLID_MAX; - SignalKey key; -}; - -struct InstanceBoundaryInfo { - std::string instancePath; - std::vector stateKeys; - std::vector observedTerms; -}; - struct SequentialInstanceScan { PendingTermMap modelTermIDs; std::vector stateOutputs; + std::vector opaqueStateOutputs; + std::vector> opaqueStateReasons; const naja::NL::SNLDesignModeling::SequentialModel* model = nullptr; - std::string unsupportedReason; - InstanceBoundaryInfo boundaryInfo; }; -AbstractedSequentialBoundaryDetail makeAbstractedBoundaryDetail( - const InstanceBoundaryInfo& info) { - AbstractedSequentialBoundaryDetail detail; - detail.instancePath = info.instancePath; - detail.stateKeys = info.stateKeys; - detail.observedKeys.reserve(info.observedTerms.size()); - for (const auto& observedTerm : info.observedTerms) { - detail.observedKeys.push_back(observedTerm.key); - } - return detail; -} - struct BuiltObservedExpr { // LCOV_EXCL_LINE BoolExpr* expr = nullptr; // LCOV_EXCL_LINE std::optional connectivitySkip; @@ -158,6 +136,11 @@ using BuilderSkippedOutputReason = std::string normalizePinName(const std::string& name); +ConnectivitySkipInfo makeOpaqueInternalSkip(std::string detail); +ConnectivitySkipInfo makeOpaqueInternalSkip( + const naja::DNL::DNLTerminalFull& terminal, + std::string reason); + bool isClockTreeBufferCell(const naja::DNL::DNLTerminalFull& term); bool isPotentialClockTreeBufferCell(const naja::DNL::DNLTerminalFull& term); @@ -295,24 +278,6 @@ bool hasBuildableCombinationalRoot( // LCOV_EXCL_STOP } -std::string describeConnectivitySkipOrigin(ConnectivitySkipOrigin origin) { - switch (origin) { - case ConnectivitySkipOrigin::NoDriver: - return "no-driver"; - case ConnectivitySkipOrigin::MultiDriver: - return "multi-driver"; - case ConnectivitySkipOrigin::LogicalLoop: - return "logical-loop"; - case ConnectivitySkipOrigin::MultiClockDomain: - // LCOV_EXCL_START - return "multi-clock-domain"; // LCOV_EXCL_LINE - // LCOV_EXCL_STOP - } - // LCOV_EXCL_START - return "connectivity"; // LCOV_EXCL_LINE - // LCOV_EXCL_STOP -} - const char* describeBuilderSkippedOutputReason( BuilderSkippedOutputReason reason) { switch (reason) { @@ -326,6 +291,8 @@ const char* describeBuilderSkippedOutputReason( // LCOV_EXCL_START return "logical_loop"; // LCOV_EXCL_LINE // LCOV_EXCL_STOP + case BuilderSkippedOutputReason::OpaqueInternal: + return "opaque_internal"; case BuilderSkippedOutputReason::None: // LCOV_EXCL_START return "none"; // LCOV_EXCL_LINE @@ -346,6 +313,9 @@ std::optional getConnectivitySkipInfo( case BuilderSkippedOutputReason::LogicalLoop: return ConnectivitySkipInfo{ ConnectivitySkipOrigin::LogicalLoop, info.detail}; + case BuilderSkippedOutputReason::OpaqueInternal: + return ConnectivitySkipInfo{ + ConnectivitySkipOrigin::OpaqueInternal, info.detail}; // LCOV_EXCL_START case BuilderSkippedOutputReason::None: // LCOV_EXCL_LINE // LCOV_EXCL_STOP @@ -417,7 +387,8 @@ BuiltObservedExpr buildObservedExprForTerm( // LCOV_EXCL_LINE outputTermID, // LCOV_EXCL_LINE isPIs, // LCOV_EXCL_LINE // LCOV_EXCL_STOP - localIsPOs); + localIsPOs, + true); // LCOV_EXCL_START cloud.compute(); // LCOV_EXCL_LINE if (cloud.getTruthTable().isValid()) { // LCOV_EXCL_LINE @@ -472,6 +443,10 @@ BuiltObservedExpr buildObservedExprForTerm( // LCOV_EXCL_LINE localResult.connectivitySkip = ConnectivitySkipInfo{ // LCOV_EXCL_LINE ConnectivitySkipOrigin::LogicalLoop, cloud.getSkipReasonText()}; // LCOV_EXCL_LINE break; // LCOV_EXCL_LINE + case KEPLER_FORMAL::SNLLogicCloud::SkipReason::OpaqueInternal: + localResult.connectivitySkip = makeOpaqueInternalSkip( + cloud.getSkipReasonText()); + break; case KEPLER_FORMAL::SNLLogicCloud::SkipReason::None: // LCOV_EXCL_LINE // LCOV_EXCL_STOP default: @@ -706,6 +681,7 @@ MaterializedBuilderOutputs materializeBuilderOutputs( KEPLER_FORMAL::BuildPrimaryOutputClauses builder; builder.setRetainDnl(true); + builder.setStopAtOpaqueInternalOutputs(true); std::vector normalizedRoots; normalizedRoots.reserve(requestedOutputs.size()); std::unordered_map> requestedByRoot; @@ -1093,76 +1069,6 @@ void collectCandidateDependenciesIntoSet( } } -// LCOV_EXCL_START -std::optional findFirstUnpublishedSupportVar( // LCOV_EXCL_LINE -// LCOV_EXCL_STOP - BoolExpr* expr, - const std::vector& isPublishedVar, - CandidateDependencyScratch& scratch) { - // LCOV_EXCL_START - if (expr == nullptr || !expr->isValid()) { // LCOV_EXCL_LINE - return std::nullopt; // LCOV_EXCL_LINE - // LCOV_EXCL_STOP - } - - // We only need the first unpublished symbol. Avoid BoolExpr::getSupportVars() - // here: BlackParrot-scale formulas spend minutes allocating full support - // sets. A PMR-backed visited set keeps the DAG walk exact without the tiny - // per-node heap churn of std::unordered_map/operator[]. - // LCOV_EXCL_START - std::pmr::monotonic_buffer_resource visitedResource; // LCOV_EXCL_LINE - std::pmr::unordered_set visited{&visitedResource}; // LCOV_EXCL_LINE - visited.reserve(4096); // LCOV_EXCL_LINE - scratch.stack.clear(); // LCOV_EXCL_LINE - scratch.stack.push_back(expr); // LCOV_EXCL_LINE - while (!scratch.stack.empty()) { // LCOV_EXCL_LINE - const BoolExpr* node = scratch.stack.back(); // LCOV_EXCL_LINE - scratch.stack.pop_back(); // LCOV_EXCL_LINE - if (node == nullptr || !visited.insert(node).second) { // LCOV_EXCL_LINE - continue; // LCOV_EXCL_LINE - // LCOV_EXCL_STOP - } - - // LCOV_EXCL_START - switch (node->getOp()) { // LCOV_EXCL_LINE - // LCOV_EXCL_STOP - case Op::VAR: { - // LCOV_EXCL_START - const size_t symbol = node->getId(); // LCOV_EXCL_LINE - if (symbol >= 2 && // LCOV_EXCL_LINE - (symbol >= isPublishedVar.size() || isPublishedVar[symbol] == 0)) { // LCOV_EXCL_LINE - return symbol; // LCOV_EXCL_LINE - // LCOV_EXCL_STOP - } - // LCOV_EXCL_START - break; // LCOV_EXCL_LINE - // LCOV_EXCL_STOP - } - case Op::NOT: - // LCOV_EXCL_START - scratch.stack.push_back(node->getLeft()); // LCOV_EXCL_LINE - break; // LCOV_EXCL_LINE - // LCOV_EXCL_STOP - case Op::AND: - case Op::OR: - case Op::XOR: - // LCOV_EXCL_START - scratch.stack.push_back(node->getLeft()); // LCOV_EXCL_LINE - scratch.stack.push_back(node->getRight()); // LCOV_EXCL_LINE - break; // LCOV_EXCL_LINE - case Op::NONE: // LCOV_EXCL_LINE - // LCOV_EXCL_STOP - default: - // LCOV_EXCL_START - break; // LCOV_EXCL_LINE - // LCOV_EXCL_STOP - } - } - // LCOV_EXCL_START - return std::nullopt; // LCOV_EXCL_LINE -} // LCOV_EXCL_LINE -// LCOV_EXCL_STOP - std::vector buildCandidateVarMask( const std::unordered_map& candidateExprByVarID) { size_t maxVarID = 0; @@ -1176,31 +1082,6 @@ std::vector buildCandidateVarMask( return isCandidateVar; } -// LCOV_EXCL_START -std::string displayNameForSignalKey( // LCOV_EXCL_LINE -// LCOV_EXCL_STOP - const SequentialDesignModel& model, - const SignalKey& key) { - // LCOV_EXCL_START - const auto displayIt = model.displayNameByKey.find(key); // LCOV_EXCL_LINE - return displayIt == model.displayNameByKey.end() ? signalKeyToString(key) // LCOV_EXCL_LINE - : displayIt->second; // LCOV_EXCL_LINE - // LCOV_EXCL_STOP -} - -// LCOV_EXCL_START -ConnectivitySkipInfo makeUnpublishedSupportSkip(size_t varID) { // LCOV_EXCL_LINE - return ConnectivitySkipInfo{ // LCOV_EXCL_LINE - // LCOV_EXCL_STOP - ConnectivitySkipOrigin::NoDriver, - // LCOV_EXCL_START - "Depends on unpublished internal support variable v" + std::to_string(varID), // LCOV_EXCL_LINE - // LCOV_EXCL_STOP - }; -// LCOV_EXCL_START -} // LCOV_EXCL_LINE -// LCOV_EXCL_STOP - SignalKey getTerminalPathKey(const naja::DNL::DNLTerminalFull& terminal) { SignalKey key; const auto pathNames = terminal.getDNLInstance().getPath().getPathNames(); @@ -1225,6 +1106,34 @@ std::string getTerminalDisplayName(const naja::DNL::DNLTerminalFull& terminal) { return oss.str(); } +ConnectivitySkipInfo makeOpaqueInternalSkip( + const naja::DNL::DNLTerminalFull& terminal, + std::string reason) { + std::string instance = terminal.getDNLInstance().getFullPath(); + while (!instance.empty() && + (instance.back() == '/' || instance.back() == '.')) { + instance.pop_back(); + } + if (instance.empty()) { + instance = ""; // LCOV_EXCL_LINE + } + std::ostringstream detail; + detail << "opaque internal cell `" << instance << "`"; + if (const auto* model = terminal.getDNLInstance().getSNLModel()) { + detail << " (model `" << model->getName().getString() << "`)"; + } + detail << " pin `" << terminal.getSnlBitTerm()->getName().getString() + << "[" << terminal.getSnlBitTerm()->getBit() << "]`: " << reason; + return makeOpaqueInternalSkip(detail.str()); +} + +ConnectivitySkipInfo makeOpaqueInternalSkip(std::string detail) { + return ConnectivitySkipInfo{ + ConnectivitySkipOrigin::OpaqueInternal, + std::move(detail), + }; +} + std::string normalizePinName(const std::string& name) { std::string normalized = name; for (char& ch : normalized) { @@ -1552,6 +1461,8 @@ BoolExpr* buildSequentialModelExpr( result = exprIt->second; // LCOV_EXCL_START } else if (termIt->second.termID < termDNLID2varID.size() && + termDNLID2varID[termIt->second.termID] != + static_cast(-1) && termDNLID2varID[termIt->second.termID] >= 2) { result = BoolExpr::Var(termDNLID2varID[termIt->second.termID]); // LCOV_EXCL_STOP @@ -1569,6 +1480,7 @@ BoolExpr* buildSequentialModelExpr( } const auto& state = pending.stateReferences[node.state]; if (state.termID >= termDNLID2varID.size() || + termDNLID2varID[state.termID] == static_cast(-1) || termDNLID2varID[state.termID] < 2) { throw std::runtime_error("Sequential state bit was mapped to a constant"); } @@ -1696,7 +1608,10 @@ BoolExpr* getLocalClockEnableExpr( } std::optional getPendingTransitionUnsupportedReason( - const PendingTransition& pending) { + const PendingTransition& pending, + const std::unordered_map& inputKeyByTerm, + const std::unordered_map& + skipInfoByKey) { using Expression = naja::NL::SNLDesignModeling::BooleanExpression; using Operator = Expression::Operator; if (pending.sequentialState == nullptr || pending.clockedOn == nullptr) { @@ -1723,6 +1638,24 @@ std::optional getPendingTransitionUnsupportedReason( node.state >= pending.stateReferences.size()) { return "Naja sequential expression references an unmapped state"; } + if (node.operation == Operator::State && + pending.stateReferences[node.state].termID == + naja::DNL::DNLID_MAX) { + return "Naja sequential expression references a state without a " + "modelable output"; + } + if (node.operation == Operator::State) { + const auto keyIt = inputKeyByTerm.find( + pending.stateReferences[node.state].termID); + if (keyIt != inputKeyByTerm.end()) { + const auto skipIt = skipInfoByKey.find(keyIt->second); + if (skipIt != skipInfoByKey.end() && + skipIt->second.origin == ConnectivitySkipOrigin::OpaqueInternal) { + return "Naja sequential expression depends on " + + skipIt->second.detail; + } + } + } for (const auto operand : node.operands) { if (operand >= expression.nodes.size()) { return "Naja sequential expression operand is out of range"; @@ -1798,6 +1731,7 @@ BoolExpr* buildNextStateExpr( const auto& currentReference = pending.stateReferences.at(pending.modelStateIndex); if (currentReference.termID >= termDNLID2varID.size() || + termDNLID2varID[currentReference.termID] == static_cast(-1) || termDNLID2varID[currentReference.termID] < 2) { // LCOV_EXCL_START throw std::runtime_error("Sequential state bit was mapped to a constant"); @@ -2352,6 +2286,11 @@ class PureClockCarrierStructureIndex { return pureClockCarrierTermIDs_; } + bool contains(naja::DNL::DNLID termID) const { + return termID < pureClockMemoStrict_.size() && + pureClockMemoStrict_[termID] == 1; + } + size_t addMappedCarrierVarIDs( const SequentialDesignModel& model, const std::vector& termDNLID2varID, @@ -2470,35 +2409,28 @@ struct ExtractContext { naja::NL::SNLDesign* previousTop = nullptr; std::string topName; bool secDiagEnabled = false; - bool abstractUncomputableSequentialBoundaries = false; // LCOV_EXCL_START KEPLER_FORMAL::BuildPrimaryOutputClauses builder; // LCOV_EXCL_STOP decltype(naja::DNL::get()) dnl = nullptr; std::unordered_map inputKeyByTerm; - std::unordered_map outputKeyByTerm; std::unordered_map topOutputKeyByTerm; // LCOV_EXCL_START std::set topInputKeys; std::set topOutputKeys; - std::set internalBoundaryInputKeys; - std::set internalBoundaryOutputKeys; std::set environmentInputs; std::set stateBits; // LCOV_EXCL_STOP std::set allObservedOutputs; // LCOV_EXCL_START - std::unordered_set prunedBuilderOutputTerms; std::unordered_map collectedSkippedOutputs; - std::set abstractedBoundaryStateKeys; - std::vector> abstractedBoundaryObservedTerms; - std::unordered_set abstractedBoundaryObservedKeys; - std::unordered_set unsupportedStateBits; + bool hasOpaqueInternalTerminals = false; + std::unordered_set opaqueSkippedTopOutputTerms; + std::vector opaqueConeExtraDependencies; + std::unique_ptr pureClockCarrierStructure; std::vector pendingTransitions; std::vector pendingMemoryInstances; - std::vector instanceBoundaryInfos; - std::unordered_map sequentialInstanceCache; }; std::string describeSupportVarOrigins( // LCOV_EXCL_LINE @@ -2641,21 +2573,26 @@ bool isLatchLikeClockPinName(const std::string& pinName) { pinName == "CK"; } -std::vector collectClockGateLatchDependencyTerms( - const ExtractContext& ctx, - const SequentialDesignModel& model) { - std::vector terms; - std::unordered_set stateKeys( - model.stateBits.begin(), model.stateBits.end()); - std::unordered_set topInputKeys( - model.topInputKeys.begin(), model.topInputKeys.end()); +struct ClockGateLatchCandidate { + naja::DNL::DNLID outputTerm = naja::DNL::DNLID_MAX; + naja::DNL::DNLID dataTerm = naja::DNL::DNLID_MAX; + std::vector clockTerms; +}; +std::vector collectClockGateLatchCandidates( + const ExtractContext& ctx) { + std::vector candidates; for (auto leafID : ctx.dnl->getLeaves()) { const auto& instance = ctx.dnl->getDNLInstanceFromID(leafID); - std::vector latchOutputTerms; - std::vector latchDataTerms; - std::vector latchClockTerms; - + const auto* primitive = instance.getSNLModel(); + if (naja::NL::SNLDesignModeling::hasSequentialModel(primitive) && + naja::NL::SNLDesignModeling::getSequentialModel(primitive).kind != + naja::NL::SNLDesignModeling::SequentialModel::Kind::Latch) { + continue; + } + std::vector outputTerms; + std::vector dataTerms; + std::vector clockTerms; for (naja::DNL::DNLID termID = instance.getTermIndexes().first; termID != naja::DNL::DNLID_MAX && termID <= instance.getTermIndexes().second; @@ -2666,23 +2603,196 @@ std::vector collectClockGateLatchDependencyTerms( if (term.getSnlBitTerm()->getDirection() == naja::NL::SNLBitTerm::Direction::Output) { if (isLatchLikeOutputPinName(pinName)) { - latchOutputTerms.push_back(termID); + outputTerms.push_back(termID); } + } else if (isLatchLikeDataPinName(pinName)) { + dataTerms.push_back(termID); + } else if (isLatchLikeClockPinName(pinName)) { + clockTerms.push_back(termID); + } + } + if (outputTerms.size() == 1 && dataTerms.size() == 1 && + !clockTerms.empty()) { + candidates.push_back( + {outputTerms.front(), dataTerms.front(), std::move(clockTerms)}); + } + } + return candidates; +} + +void filterOpaqueBuilderInputs( + ExtractContext& ctx, + const SequentialDesignModel& model, + const std::vector& allowedLatchCandidates = {}) { + std::vector retainedInputs; + retainedInputs.reserve(ctx.builder.getInputs().size()); + for (const auto termID : ctx.builder.getInputs()) { + const auto keyIt = ctx.inputKeyByTerm.find(termID); + const auto skipIt = keyIt == ctx.inputKeyByTerm.end() + ? model.connectivitySkipInfoByKey.end() + : model.connectivitySkipInfoByKey.find(keyIt->second); + const bool opaque = + skipIt != model.connectivitySkipInfoByKey.end() && + skipIt->second.origin == ConnectivitySkipOrigin::OpaqueInternal; + const bool allowedLatch = std::any_of( + allowedLatchCandidates.begin(), + allowedLatchCandidates.end(), + [&](const ClockGateLatchCandidate& candidate) { + return candidate.outputTerm == termID; + }); + if (!opaque || allowedLatch) { + retainedInputs.push_back(termID); + } + } + ctx.builder.setInputs(retainedInputs); +} + +void retainModelableClockGateLatchOutputs( + ExtractContext& ctx, + SequentialDesignModel& model) { + const auto candidates = collectClockGateLatchCandidates(ctx); + filterOpaqueBuilderInputs(ctx, model, candidates); + if (candidates.empty()) { + return; + } + + if (ctx.pureClockCarrierStructure == nullptr) { + ctx.pureClockCarrierStructure = + std::make_unique(ctx.dnl); + } + const auto& pureClockTerms = *ctx.pureClockCarrierStructure; + std::vector eligible(candidates.size(), false); + std::vector dependencyTerms; + for (size_t i = 0; i < candidates.size(); ++i) { + const auto& candidate = candidates[i]; + const auto keyIt = ctx.inputKeyByTerm.find(candidate.outputTerm); + const auto skipIt = keyIt == ctx.inputKeyByTerm.end() + ? model.connectivitySkipInfoByKey.end() + : model.connectivitySkipInfoByKey.find(keyIt->second); + if (skipIt == model.connectivitySkipInfoByKey.end() || + skipIt->second.origin != ConnectivitySkipOrigin::OpaqueInternal || + !std::all_of( + candidate.clockTerms.begin(), + candidate.clockTerms.end(), + [&](naja::DNL::DNLID termID) { + return pureClockTerms.contains(termID); + })) { + continue; + } + eligible[i] = true; + dependencyTerms.push_back(candidate.dataTerm); + dependencyTerms.insert( + dependencyTerms.end(), + candidate.clockTerms.begin(), + candidate.clockTerms.end()); + } + if (dependencyTerms.empty()) { + filterOpaqueBuilderInputs(ctx, model); + return; + } + std::sort(dependencyTerms.begin(), dependencyTerms.end()); + dependencyTerms.erase( + std::unique(dependencyTerms.begin(), dependencyTerms.end()), + dependencyTerms.end()); + + const auto dependencies = materializeBuilderOutputs( + dependencyTerms, + ctx.builder.getInputs(), + {}, + ctx.collectedSkippedOutputs, + ctx.secDiagEnabled, + ctx.topName.c_str(), + "clock-gate latch preflight"); + for (size_t i = 0; i < candidates.size(); ++i) { + if (!eligible[i]) { + continue; + } + const auto& candidate = candidates[i]; + const bool hasData = + dependencies.outputExprByTerm.find(candidate.dataTerm) != + dependencies.outputExprByTerm.end(); + const bool hasClocks = std::all_of( + candidate.clockTerms.begin(), + candidate.clockTerms.end(), + [&](naja::DNL::DNLID termID) { + return dependencies.outputExprByTerm.find(termID) != + dependencies.outputExprByTerm.end(); + }); + if (hasData && hasClocks) { + const auto keyIt = ctx.inputKeyByTerm.find(candidate.outputTerm); + if (keyIt != ctx.inputKeyByTerm.end()) { + ctx.stateBits.erase(keyIt->second); + model.connectivitySkipInfoByKey.erase(keyIt->second); + ctx.opaqueConeExtraDependencies.push_back( + {candidate.dataTerm, candidate.outputTerm}); + } + } + } + filterOpaqueBuilderInputs(ctx, model); +} + +void skipTopOutputsReachedByOpaqueTerminals( + ExtractContext& ctx, + SequentialDesignModel& model) { + std::vector opaqueSeeds; + for (const auto leafID : ctx.dnl->getLeaves()) { + const auto& instance = ctx.dnl->getDNLInstanceFromID(leafID); + for (naja::DNL::DNLID termID = instance.getTermIndexes().first; + termID != naja::DNL::DNLID_MAX && + termID <= instance.getTermIndexes().second; + ++termID) { + const auto& term = ctx.dnl->getDNLTerminalFromID(termID); + if (term.isNull() || + term.getSnlBitTerm()->getDirection() == + naja::NL::SNLBitTerm::Direction::Input) { continue; } - if (isLatchLikeDataPinName(pinName)) { - latchDataTerms.push_back(termID); - } else if (isLatchLikeClockPinName(pinName)) { - latchClockTerms.push_back(termID); + const auto skipIt = + model.connectivitySkipInfoByKey.find(getTerminalPathKey(term)); + if (skipIt != model.connectivitySkipInfoByKey.end() && + skipIt->second.origin == ConnectivitySkipOrigin::OpaqueInternal) { + opaqueSeeds.push_back({termID, skipIt->second.detail}); } } + } - if (latchOutputTerms.size() != 1 || latchDataTerms.size() != 1 || - latchClockTerms.empty()) { + ctx.hasOpaqueInternalTerminals = !opaqueSeeds.empty(); + const SecNetlistChecks checks(ctx.dnl); + const auto reachedOutputs = checks.findTopOutputsReachedByOpaqueTerminals( + std::move(opaqueSeeds), ctx.opaqueConeExtraDependencies); + for (const auto& reached : reachedOutputs) { + const auto outputIt = + ctx.topOutputKeyByTerm.find(reached.topOutputTermID); + if (outputIt == ctx.topOutputKeyByTerm.end()) { continue; } + ctx.opaqueSkippedTopOutputTerms.insert(reached.topOutputTermID); + model.connectivitySkipInfoByKey.insert_or_assign( + outputIt->second, + makeOpaqueInternalSkip(reached.source.detail)); + } + if (ctx.secDiagEnabled) { + fprintf( + stderr, + "SEC diag: extract(%s) opaque_reached_outputs=%zu\n", + ctx.topName.c_str(), + ctx.opaqueSkippedTopOutputTerms.size()); + fflush(stderr); + } +} + +std::vector collectClockGateLatchDependencyTerms( + const ExtractContext& ctx, + const SequentialDesignModel& model) { + std::vector terms; + std::unordered_set stateKeys( + model.stateBits.begin(), model.stateBits.end()); + std::unordered_set topInputKeys( + model.topInputKeys.begin(), model.topInputKeys.end()); + + for (const auto& candidate : collectClockGateLatchCandidates(ctx)) { const auto& outputTerm = - ctx.dnl->getDNLTerminalFromID(latchOutputTerms.front()); + ctx.dnl->getDNLTerminalFromID(candidate.outputTerm); const SignalKey outputKey = getTerminalPathKey(outputTerm); if (stateKeys.find(outputKey) != stateKeys.end() || topInputKeys.find(outputKey) != topInputKeys.end() || @@ -2690,8 +2800,9 @@ std::vector collectClockGateLatchDependencyTerms( continue; } - terms.push_back(latchDataTerms.front()); - terms.insert(terms.end(), latchClockTerms.begin(), latchClockTerms.end()); + terms.push_back(candidate.dataTerm); + terms.insert( + terms.end(), candidate.clockTerms.begin(), candidate.clockTerms.end()); } std::sort(terms.begin(), terms.end()); @@ -2722,54 +2833,17 @@ std::unordered_map collectClockGateLatchDataExprByVarID( std::unordered_set topInputKeys( model.topInputKeys.begin(), model.topInputKeys.end()); - for (auto leafID : ctx.dnl->getLeaves()) { - const auto& instance = ctx.dnl->getDNLInstanceFromID(leafID); - std::vector latchOutputTerms; - std::vector latchDataTerms; - std::vector latchClockTerms; - - for (naja::DNL::DNLID termID = instance.getTermIndexes().first; - termID != naja::DNL::DNLID_MAX && - termID <= instance.getTermIndexes().second; - ++termID) { - const auto& term = ctx.dnl->getDNLTerminalFromID(termID); - const std::string pinName = - normalizePinName(term.getSnlBitTerm()->getName().getString()); - if (term.getSnlBitTerm()->getDirection() == - naja::NL::SNLBitTerm::Direction::Output) { - if (isLatchLikeOutputPinName(pinName)) { - latchOutputTerms.push_back(termID); - } - // LCOV_EXCL_START - continue; - // LCOV_EXCL_STOP - } - if (isLatchLikeDataPinName(pinName)) { - latchDataTerms.push_back(termID); - // LCOV_EXCL_START - } else if (isLatchLikeClockPinName(pinName)) { - // LCOV_EXCL_STOP - latchClockTerms.push_back(termID); - } - // LCOV_EXCL_START - } - - -// LCOV_EXCL_STOP - if (latchOutputTerms.size() != 1 || latchDataTerms.size() != 1 || - latchClockTerms.empty()) { - continue; - } + for (const auto& candidate : collectClockGateLatchCandidates(ctx)) { ++latchLikeCandidates; - const auto dataExprIt = outputExprByTerm.find(latchDataTerms.front()); + const auto dataExprIt = outputExprByTerm.find(candidate.dataTerm); if (dataExprIt == outputExprByTerm.end()) { ++latchLikeMissingDataExpr; continue; } BoolExpr* latchClockExpr = nullptr; - for (const auto clockTermID : latchClockTerms) { + for (const auto clockTermID : candidate.clockTerms) { const auto clockExprIt = outputExprByTerm.find(clockTermID); if (clockExprIt == outputExprByTerm.end()) { continue; // LCOV_EXCL_LINE @@ -2803,7 +2877,7 @@ std::unordered_map collectClockGateLatchDataExprByVarID( // LCOV_EXCL_START const auto& outputTerm = - ctx.dnl->getDNLTerminalFromID(latchOutputTerms.front()); + ctx.dnl->getDNLTerminalFromID(candidate.outputTerm); const SignalKey outputKey = getTerminalPathKey(outputTerm); if (stateKeys.find(outputKey) != stateKeys.end() || topInputKeys.find(outputKey) != topInputKeys.end()) { @@ -2843,34 +2917,6 @@ std::unordered_map collectClockGateLatchDataExprByVarID( return dataExprByVarID; } -bool isSequentialInstanceTerm(ExtractContext& ctx, - const naja::DNL::DNLTerminalFull& term) { - const auto& instance = term.getDNLInstance(); - const auto instanceID = instance.getID(); - if (const auto cached = ctx.sequentialInstanceCache.find(instanceID); - cached != ctx.sequentialInstanceCache.end()) { - return cached->second; - } - - bool isSequentialInstance = false; - for (naja::DNL::DNLID termID = instance.getTermIndexes().first; - termID != naja::DNL::DNLID_MAX && termID <= instance.getTermIndexes().second; - ++termID) { - const auto& instanceTerm = ctx.dnl->getDNLTerminalFromID(termID); - if (instanceTerm.isNull()) { - continue; // LCOV_EXCL_LINE - } - if (isSequentialStateOutput(instanceTerm) || - isSequentialNextStateInput(instanceTerm)) { - isSequentialInstance = true; - break; - } - } - - ctx.sequentialInstanceCache.emplace(instanceID, isSequentialInstance); - return isSequentialInstance; -} - void classifyBuilderBoundaryTerms(ExtractContext& ctx, SequentialDesignModel& model) { // The miter builder already exposes sequential outputs as "inputs" and // sequential next-state pins as "outputs"; we normalize those into SEC's @@ -2885,22 +2931,18 @@ void classifyBuilderBoundaryTerms(ExtractContext& ctx, SequentialDesignModel& mo model.displayNameByKey.try_emplace(key, getTerminalDisplayName(term)); if (isSequentialStateOutput(term)) { ctx.stateBits.insert(key); - } else { + } else if (term.isTopPort()) { ctx.environmentInputs.insert(key); - if (!term.isTopPort() && !isSequentialInstanceTerm(ctx, term)) { - ctx.internalBoundaryInputKeys.insert(key); - } - } - } - - for (const auto outputTermID : ctx.builder.getOutputs()) { - const auto& term = ctx.dnl->getDNLTerminalFromID(outputTermID); - SignalKey key = getTerminalPathKey(term); - ctx.outputKeyByTerm.emplace(outputTermID, key); - model.displayNameByKey.try_emplace(key, getTerminalDisplayName(term)); - if (ctx.topOutputKeys.find(key) == ctx.topOutputKeys.end() && - !isSequentialInstanceTerm(ctx, term)) { - ctx.internalBoundaryOutputKeys.insert(key); + } else { + const auto truthTable = naja::NL::SNLDesignModeling::getTruthTable( + term.getDNLInstance().getSNLInstance(), + term.getSnlBitTerm()->getOrderID()); + const std::string reason = truthTable.isInitialized() + ? "the internal output has no usable SEC model" + : "no initialized combinational truth table or usable sequential model"; + model.connectivitySkipInfoByKey.insert_or_assign( + key, + makeOpaqueInternalSkip(term, reason)); } } // LCOV_EXCL_START @@ -2908,13 +2950,11 @@ void classifyBuilderBoundaryTerms(ExtractContext& ctx, SequentialDesignModel& mo // LCOV_EXCL_STOP std::optional scanSequentialInstance( - const naja::DNL::DNLInstanceFull& instance, - const std::unordered_map& inputKeyByTerm, - SequentialDesignModel& model) { + const naja::DNL::DNLInstanceFull& instance) { // LCOV_EXCL_START SequentialInstanceScan scan; // LCOV_EXCL_STOP - scan.boundaryInfo.instancePath = instance.getFullPath(); + std::vector physicalStateOutputTermIDs; for (naja::DNL::DNLID termID = instance.getTermIndexes().first; termID != naja::DNL::DNLID_MAX && @@ -2925,26 +2965,40 @@ std::optional scanSequentialInstance( scan.modelTermIDs.emplace( term.getSnlBitTerm(), PendingPinTerm{termID, term.getSnlBitTerm()->getBit()}); + if (isSequentialStateOutput(term) && + term.getSnlBitTerm()->getDirection() != + naja::NL::SNLBitTerm::Direction::Input) { + physicalStateOutputTermIDs.push_back(termID); + } } } const auto* primitive = instance.getSNLModel(); - if (naja::NL::SNLDesignModeling::hasSequentialModel(primitive)) { + const bool hasSequentialModel = + naja::NL::SNLDesignModeling::hasSequentialModel(primitive); + if (hasSequentialModel && + naja::NL::SNLDesignModeling::getSequentialModel(primitive).kind == + naja::NL::SNLDesignModeling::SequentialModel::Kind::Latch) { + for (const auto termID : physicalStateOutputTermIDs) { + scan.opaqueStateOutputs.push_back( + {termID, "Naja latch sequential models are not supported by SEC"}); + } + } else if (hasSequentialModel) { scan.model = &naja::NL::SNLDesignModeling::getSequentialModel(primitive); + scan.opaqueStateReasons.resize(scan.model->states.size()); + std::unordered_set modeledPhysicalOutputs; for (const auto& output : scan.model->outputs) { const auto termIt = scan.modelTermIDs.find(output.term); if (termIt == scan.modelTermIDs.end()) { - scan.unsupportedReason = - "Naja sequential output is not present on the instance"; continue; } + modeledPhysicalOutputs.insert(termIt->second.termID); const auto stateReference = getStateOutputReference(output.function); if (!stateReference.has_value() || stateReference->first >= scan.model->states.size()) { - scan.unsupportedReason = - "Unsupported Naja sequential output function"; - scan.stateOutputs.push_back({ - termIt->second.termID, termIt->second.bit, 0, false}); + scan.opaqueStateOutputs.push_back( + {termIt->second.termID, + "Unsupported Naja sequential output function"}); continue; } scan.stateOutputs.push_back({ @@ -2953,6 +3007,12 @@ std::optional scanSequentialInstance( stateReference->first, stateReference->second}); } + for (const auto termID : physicalStateOutputTermIDs) { + if (modeledPhysicalOutputs.find(termID) == modeledPhysicalOutputs.end()) { + scan.opaqueStateOutputs.push_back( + {termID, "Missing Naja sequential output model"}); + } + } using Operator = naja::NL::SNLDesignModeling::BooleanExpression::Operator; @@ -2975,10 +3035,29 @@ std::optional scanSequentialInstance( instance.getTerminalFromBitTerm( const_cast(modelTerm))) && modeledUpdateTerms.find(modelTerm) == modeledUpdateTerms.end()) { - scan.unsupportedReason = + const std::string reason = "Unsupported sequential primitive with update pin `" + modelTerm->getName().getString() + "`"; - break; + std::unordered_set relatedClocks; + for (auto* clock : naja::NL::SNLDesignModeling::getInputRelatedClocks( + const_cast(modelTerm))) { + relatedClocks.insert(clock); + } + for (const auto& stateOutput : scan.stateOutputs) { + auto* outputTerm = naja::DNL::get() + ->getDNLTerminalFromID(stateOutput.termID) + .getSnlBitTerm(); + const auto outputClocks = + naja::NL::SNLDesignModeling::getOutputRelatedClocks(outputTerm); + if (std::any_of( + outputClocks.begin(), + outputClocks.end(), + [&](const auto* clock) { + return relatedClocks.find(clock) != relatedClocks.end(); + })) { + scan.opaqueStateReasons[stateOutput.stateIndex] = reason; + } + } } } @@ -2998,58 +3077,25 @@ std::optional scanSequentialInstance( sharedDataTerm = root.term; } if (hasSharedScalarData) { - scan.unsupportedReason = - "Shared scalar data input cannot define multiple independent state outputs"; + std::fill( + scan.opaqueStateReasons.begin(), + scan.opaqueStateReasons.end(), + "Shared scalar data input cannot define multiple independent state " + "outputs"); } } else { - scan.unsupportedReason = "Missing Naja sequential model"; - size_t stateIndex = 0; - for (naja::DNL::DNLID termID = instance.getTermIndexes().first; - termID != naja::DNL::DNLID_MAX && - termID <= instance.getTermIndexes().second; - ++termID) { - const auto& term = naja::DNL::get()->getDNLTerminalFromID(termID); - if (isSequentialStateOutput(term) && - term.getSnlBitTerm()->getDirection() != - naja::NL::SNLBitTerm::Direction::Input) { - scan.stateOutputs.push_back( - {termID, term.getSnlBitTerm()->getBit(), stateIndex++, false}); - } + for (const auto termID : physicalStateOutputTermIDs) { + scan.opaqueStateOutputs.push_back( + {termID, "Missing Naja sequential model"}); } } - if (scan.stateOutputs.empty()) { + if (scan.stateOutputs.empty() && scan.opaqueStateOutputs.empty()) { return std::nullopt; } - std::set boundaryStateKeys; - for (const auto& stateOutput : scan.stateOutputs) { - const auto keyIt = inputKeyByTerm.find(stateOutput.termID); - if (keyIt != inputKeyByTerm.end()) { - boundaryStateKeys.insert(keyIt->second); - } - } - scan.boundaryInfo.stateKeys.assign(boundaryStateKeys.begin(), boundaryStateKeys.end()); - - std::set boundaryObservedKeys; - for (naja::DNL::DNLID termID = instance.getTermIndexes().first; - termID != naja::DNL::DNLID_MAX && - termID <= instance.getTermIndexes().second; - ++termID) { - const auto& term = naja::DNL::get()->getDNLTerminalFromID(termID); - if (term.isNull() || - term.getSnlBitTerm()->getDirection() == naja::NL::SNLBitTerm::Direction::Output) { - continue; - } - const SignalKey key = getTerminalPathKey(term); - model.displayNameByKey.try_emplace(key, getTerminalDisplayName(term)); - if (boundaryObservedKeys.insert(key).second) { - scan.boundaryInfo.observedTerms.push_back({termID, key}); - } - } - - return scan; -} + return scan; +} SignalKey makeMemoryCellStateKey( const naja::DNL::DNLInstanceFull& instance, @@ -3208,30 +3254,13 @@ void appendPendingMemoryInstance( pending.depth = interface.depth; pending.abits = interface.abits; pending.resetMode = interface.resetMode; - - InstanceBoundaryInfo boundaryInfo; - boundaryInfo.instancePath = instance.getFullPath(); - for (naja::DNL::DNLID termID = instance.getTermIndexes().first; - termID != naja::DNL::DNLID_MAX && - termID <= instance.getTermIndexes().second; - ++termID) { - const auto& term = naja::DNL::get()->getDNLTerminalFromID(termID); - if (term.isNull() || - term.getSnlBitTerm()->getDirection() == - naja::NL::SNLBitTerm::Direction::Output) { - continue; - } - const SignalKey key = getTerminalPathKey(term); - model.displayNameByKey.try_emplace(key, getTerminalDisplayName(term)); - boundaryInfo.observedTerms.push_back({termID, key}); - } + const std::string instancePath = instance.getFullPath(); for (size_t cellIndex = 0; cellIndex < interface.depth; ++cellIndex) { for (size_t bitIndex = 0; bitIndex < interface.width; ++bitIndex) { auto key = makeMemoryCellStateKey(instance, cellIndex, bitIndex); auto displayName = makeMemoryCellStateDisplayName(instance, cellIndex, bitIndex); model.displayNameByKey.try_emplace(key, displayName); - boundaryInfo.stateKeys.push_back(key); pending.cellStates.push_back({key, std::move(displayName), cellIndex, bitIndex}); } } @@ -3246,7 +3275,7 @@ void appendPendingMemoryInstance( termIDsByBitTerm, // LCOV_EXCL_START interface.reset, - boundaryInfo.instancePath, + instancePath, "reset"); // LCOV_EXCL_STOP } @@ -3262,18 +3291,18 @@ void appendPendingMemoryInstance( getRequiredInstanceTermID( termIDsByBitTerm, addressTerm, - boundaryInfo.instancePath, + instancePath, "read-address")); } for (auto* dataTerm : readPort.data) { const auto termID = getRequiredInstanceTermID( - termIDsByBitTerm, dataTerm, boundaryInfo.instancePath, "read-data"); + termIDsByBitTerm, dataTerm, instancePath, "read-data"); pendingReadPort.dataTermIDs.push_back(termID); const auto keyIt = ctx.inputKeyByTerm.find(termID); if (keyIt == ctx.inputKeyByTerm.end()) { throw std::runtime_error( // LCOV_EXCL_LINE "Missing SEC state key for memory read-data bit in instance `" + // LCOV_EXCL_LINE - boundaryInfo.instancePath + "`"); // LCOV_EXCL_LINE + instancePath + "`"); // LCOV_EXCL_LINE } pending.readOutputs.push_back({keyIt->second, portIndex, bitIndex}); ++bitIndex; @@ -3290,7 +3319,7 @@ void appendPendingMemoryInstance( getRequiredInstanceTermID( termIDsByBitTerm, enableTerm, - boundaryInfo.instancePath, + instancePath, "write-enable")); } if (std::any_of( @@ -3317,7 +3346,7 @@ void appendPendingMemoryInstance( // LCOV_EXCL_START addressTerm, // LCOV_EXCL_STOP - boundaryInfo.instancePath, + instancePath, // LCOV_EXCL_START "write-address")); // LCOV_EXCL_STOP @@ -3325,12 +3354,12 @@ void appendPendingMemoryInstance( for (auto* dataTerm : writePort.data) { pendingWritePort.dataTermIDs.push_back( getRequiredInstanceTermID( - termIDsByBitTerm, dataTerm, boundaryInfo.instancePath, "write-data")); + termIDsByBitTerm, dataTerm, instancePath, "write-data")); } for (auto* maskTerm : writePort.mask) { pendingWritePort.maskTermIDs.push_back( getRequiredInstanceTermID( - termIDsByBitTerm, maskTerm, boundaryInfo.instancePath, "write-mask")); + termIDsByBitTerm, maskTerm, instancePath, "write-mask")); } for (const auto& extraWriteTerms : writePort.extraWriteInputs) { std::vector pendingExtraTerms; // LCOV_EXCL_LINE @@ -3340,7 +3369,7 @@ void appendPendingMemoryInstance( getRequiredInstanceTermID( // LCOV_EXCL_LINE termIDsByBitTerm, extraTerm, // LCOV_EXCL_LINE - boundaryInfo.instancePath, // LCOV_EXCL_LINE + instancePath, // LCOV_EXCL_LINE "extra-write")); } pendingWritePort.extraWriteInputTermIDs.push_back( // LCOV_EXCL_LINE @@ -3349,8 +3378,6 @@ void appendPendingMemoryInstance( pending.writePorts.push_back(std::move(pendingWritePort)); } - ctx.instanceBoundaryInfos.push_back(std::move(boundaryInfo)); - pending.boundaryInfoIndex = ctx.instanceBoundaryInfos.size() - 1; ctx.pendingMemoryInstances.push_back(std::move(pending)); } @@ -3358,53 +3385,36 @@ void appendPendingTransitionsForInstance( ExtractContext& ctx, SequentialDesignModel& model, const SequentialInstanceScan& scan) { - ctx.instanceBoundaryInfos.push_back(scan.boundaryInfo); - const size_t boundaryInfoIndex = ctx.instanceBoundaryInfos.size() - 1; - - auto markUnsupportedInstanceStateOutputs = [&]() { - for (const auto& key : ctx.instanceBoundaryInfos[boundaryInfoIndex].stateKeys) { - ctx.unsupportedStateBits.insert(key); + auto markOpaqueOutput = [&](naja::DNL::DNLID termID, + const std::string& reason) { + const auto keyIt = ctx.inputKeyByTerm.find(termID); + if (keyIt == ctx.inputKeyByTerm.end()) { + return; } + const auto& term = ctx.dnl->getDNLTerminalFromID(termID); + model.connectivitySkipInfoByKey.insert_or_assign( + keyIt->second, + makeOpaqueInternalSkip(term, reason)); }; - auto abstractUnsupportedInstanceAsBoundary = [&](const std::string& reason) { - const auto& info = ctx.instanceBoundaryInfos[boundaryInfoIndex]; - model.abstractedSequentialBoundaries.push_back( - "Abstracted uncomputable sequential instance `" + info.instancePath + - "` as a SEC boundary: " + reason); - model.abstractedSequentialBoundaryDetails.push_back( - makeAbstractedBoundaryDetail(info)); - - for (const auto& key : info.stateKeys) { - ctx.abstractedBoundaryStateKeys.insert(key); - } - - for (const auto& observedTerm : info.observedTerms) { - if (ctx.abstractedBoundaryObservedKeys.insert(observedTerm.key).second) { - ctx.abstractedBoundaryObservedTerms.emplace_back(observedTerm.termID, observedTerm.key); - ctx.allObservedOutputs.insert(observedTerm.key); + auto markOpaqueState = [&](size_t stateIndex, const std::string& reason) { + for (const auto& stateOutput : scan.stateOutputs) { + if (stateOutput.stateIndex == stateIndex) { + markOpaqueOutput(stateOutput.termID, reason); } - ctx.prunedBuilderOutputTerms.insert(observedTerm.termID); } }; - const size_t pendingStart = ctx.pendingTransitions.size(); - const size_t complementedStart = model.complementedStateRelations.size(); - bool unsupportedInstance = false; - bool abstractedUnsupportedInstance = false; - std::string abstractedUnsupportedReason; - - if (!scan.unsupportedReason.empty() || scan.model == nullptr) { - const std::string reason = scan.unsupportedReason.empty() - ? "Missing Naja sequential model" - : scan.unsupportedReason; - if (ctx.abstractUncomputableSequentialBoundaries) { - abstractUnsupportedInstanceAsBoundary(reason); - } else { - markUnsupportedInstanceStateOutputs(); - model.unsupportedReasons.push_back( - "Unsupported sequential primitive for `" + - scan.boundaryInfo.instancePath + "`: " + reason); + for (const auto& opaqueOutput : scan.opaqueStateOutputs) { + markOpaqueOutput(opaqueOutput.termID, opaqueOutput.reason); + } + for (size_t stateIndex = 0; + stateIndex < scan.opaqueStateReasons.size(); ++stateIndex) { + if (scan.opaqueStateReasons[stateIndex].has_value()) { + markOpaqueState(stateIndex, *scan.opaqueStateReasons[stateIndex]); } + } + + if (scan.model == nullptr) { return; } @@ -3415,24 +3425,11 @@ void appendPendingTransitionsForInstance( primary = &output; } } - if (std::any_of(primaryOutputs.begin(), primaryOutputs.end(), - [](const auto* output) { return output == nullptr; })) { - const std::string reason = "Naja sequential state has no physical output"; - if (ctx.abstractUncomputableSequentialBoundaries) { - abstractUnsupportedInstanceAsBoundary(reason); - } else { - markUnsupportedInstanceStateOutputs(); - model.unsupportedReasons.push_back( - "Unsupported sequential primitive for `" + - scan.boundaryInfo.instancePath + "`: " + reason); + std::vector stateReferences(primaryOutputs.size()); + for (size_t stateIndex = 0; stateIndex < primaryOutputs.size(); ++stateIndex) { + if (const auto* output = primaryOutputs[stateIndex]) { + stateReferences[stateIndex] = {output->termID, output->complemented}; } - return; - } - - std::vector stateReferences; - stateReferences.reserve(primaryOutputs.size()); - for (const auto* output : primaryOutputs) { - stateReferences.push_back({output->termID, output->complemented}); } std::vector clockTermIDs; std::unordered_set seenClockTerms; @@ -3449,7 +3446,15 @@ void appendPendingTransitionsForInstance( } } + std::vector> pendingByState( + primaryOutputs.size()); for (size_t stateIndex = 0; stateIndex < primaryOutputs.size(); ++stateIndex) { + if (primaryOutputs[stateIndex] == nullptr) { + continue; + } + if (scan.opaqueStateReasons[stateIndex].has_value()) { + continue; + } const auto& stateOutput = *primaryOutputs[stateIndex]; PendingTransition pending; @@ -3457,14 +3462,12 @@ void appendPendingTransitionsForInstance( pending.stateKey = ctx.inputKeyByTerm.at(pending.stateTermID); pending.stateOutputIsComplemented = stateOutput.complemented; pending.modelStateIndex = stateIndex; - pending.boundaryInfoIndex = boundaryInfoIndex; pending.modelTermIDs = scan.modelTermIDs; pending.stateReferences = stateReferences; pending.sequentialState = &scan.model->states[stateIndex]; pending.clockedOn = &scan.model->clockedOn; pending.clockTermIDs = clockTermIDs; - std::vector complementedRelations; for (const auto& candidate : scan.stateOutputs) { if (candidate.termID == stateOutput.termID || candidate.stateIndex != stateIndex || @@ -3473,47 +3476,38 @@ void appendPendingTransitionsForInstance( } const SignalKey complementedKey = ctx.inputKeyByTerm.at(candidate.termID); pending.complementedStateKeys.push_back(complementedKey); - complementedRelations.push_back({pending.stateKey, complementedKey}); } + pendingByState[stateIndex] = std::move(pending); + } - if (const auto unsupportedReason = - getPendingTransitionUnsupportedReason(pending)) { - // Unsupported sequential cells are classified before cone construction. - // Boundary mode exposes their interface; strict mode reports a structural - // unsupported reason without relying on exception-to-result conversion. - if (ctx.abstractUncomputableSequentialBoundaries) { - abstractedUnsupportedInstance = true; - abstractedUnsupportedReason = *unsupportedReason; - break; + bool foundNewOpaqueState = false; + do { + foundNewOpaqueState = false; + for (size_t stateIndex = 0; stateIndex < pendingByState.size(); ++stateIndex) { + auto& pending = pendingByState[stateIndex]; + if (!pending.has_value()) { + continue; + } + if (const auto reason = getPendingTransitionUnsupportedReason( + *pending, + ctx.inputKeyByTerm, + model.connectivitySkipInfoByKey)) { + markOpaqueState(stateIndex, *reason); + pending.reset(); + foundNewOpaqueState = true; } - unsupportedInstance = true; - model.unsupportedReasons.push_back( - "Unsupported sequential primitive for `" + - signalKeyToString(pending.stateKey) + "`: " + *unsupportedReason); - continue; } + } while (foundNewOpaqueState); - model.complementedStateRelations.insert( - model.complementedStateRelations.end(), - complementedRelations.begin(), - complementedRelations.end()); - ctx.pendingTransitions.push_back(std::move(pending)); - } - - if (abstractedUnsupportedInstance) { - ctx.pendingTransitions.erase( - ctx.pendingTransitions.begin() + static_cast(pendingStart), - ctx.pendingTransitions.end()); - model.complementedStateRelations.erase( - model.complementedStateRelations.begin() + - static_cast(complementedStart), - model.complementedStateRelations.end()); - abstractUnsupportedInstanceAsBoundary(abstractedUnsupportedReason); - return; - } - - if (unsupportedInstance) { - markUnsupportedInstanceStateOutputs(); + for (auto& pending : pendingByState) { + if (!pending.has_value()) { + continue; + } + for (const auto& complementedKey : pending->complementedStateKeys) { + model.complementedStateRelations.push_back( + {pending->stateKey, complementedKey}); + } + ctx.pendingTransitions.push_back(std::move(*pending)); } } @@ -3530,7 +3524,7 @@ void collectSequentialTransitions(ExtractContext& ctx, SequentialDesignModel& mo appendPendingMemoryInstance(ctx, model, instance); continue; } - const auto scan = scanSequentialInstance(instance, ctx.inputKeyByTerm, model); + const auto scan = scanSequentialInstance(instance); if (!scan.has_value()) { continue; } @@ -3540,15 +3534,13 @@ void collectSequentialTransitions(ExtractContext& ctx, SequentialDesignModel& mo std::vector collectInitialObservedTerms(const ExtractContext& ctx) { std::vector initialObservedTerms; - initialObservedTerms.reserve( - ctx.topOutputKeyByTerm.size() + ctx.abstractedBoundaryObservedTerms.size()); + initialObservedTerms.reserve(ctx.topOutputKeyByTerm.size()); std::unordered_set initialObservedTermSet; for (const auto& [termID, _] : ctx.topOutputKeyByTerm) { - if (initialObservedTermSet.insert(termID).second) { - initialObservedTerms.push_back(termID); + if (ctx.opaqueSkippedTopOutputTerms.find(termID) != + ctx.opaqueSkippedTopOutputTerms.end()) { + continue; } - } - for (const auto& [termID, _] : ctx.abstractedBoundaryObservedTerms) { if (initialObservedTermSet.insert(termID).second) { initialObservedTerms.push_back(termID); } @@ -3556,7 +3548,7 @@ std::vector collectInitialObservedTerms(const ExtractContext& return initialObservedTerms; } -void buildInitialObservedOutputClouds(ExtractContext& ctx, SequentialDesignModel& model) { +void buildInitialObservedOutputClouds(ExtractContext& ctx) { const auto initialObservedTerms = collectInitialObservedTerms(ctx); std::unordered_set collectedOutputSet( ctx.builder.getOutputs().begin(), ctx.builder.getOutputs().end()); @@ -3568,13 +3560,12 @@ void buildInitialObservedOutputClouds(ExtractContext& ctx, SequentialDesignModel } } ctx.builder.setOutputs(initialMaterializedOutputs); + ctx.builder.setStopAtOpaqueInternalOutputs(true); if (ctx.secDiagEnabled) { fprintf( stderr, - "SEC diag: extract(%s) abstracted_boundaries=%zu pruned_builder_outputs=%zu initial_observed_outputs=%zu\n", + "SEC diag: extract(%s) initial_observed_outputs=%zu\n", ctx.topName.c_str(), - model.abstractedSequentialBoundaries.size(), - ctx.prunedBuilderOutputTerms.size(), initialMaterializedOutputs.size()); fflush(stderr); } @@ -3643,24 +3634,9 @@ void orderComplementedStateBitsByPrimary(SequentialDesignModel& model) { } void publishNormalizedBoundary(ExtractContext& ctx, SequentialDesignModel& model) { - for (const auto& key : ctx.abstractedBoundaryStateKeys) { - ctx.stateBits.erase(key); - ctx.environmentInputs.insert(key); - } - // Opaque internal boundary inputs behave exactly like additional SEC - // environment inputs: extraction gives them symbolic leaf variables because - // the surrounding cone cannot be modeled combinationally, so the published - // interface must carry them forward into the shared proof symbol space. - ctx.environmentInputs.insert( - ctx.internalBoundaryInputKeys.begin(), ctx.internalBoundaryInputKeys.end()); - model.topInputKeys.assign(ctx.topInputKeys.begin(), ctx.topInputKeys.end()); model.topOutputKeys.assign(ctx.topOutputKeys.begin(), ctx.topOutputKeys.end()); model.environmentInputs.assign(ctx.environmentInputs.begin(), ctx.environmentInputs.end()); - model.internalBoundaryInputKeys.assign( - ctx.internalBoundaryInputKeys.begin(), ctx.internalBoundaryInputKeys.end()); - model.internalBoundaryOutputKeys.assign( - ctx.internalBoundaryOutputKeys.begin(), ctx.internalBoundaryOutputKeys.end()); model.stateBits.assign(ctx.stateBits.begin(), ctx.stateBits.end()); orderComplementedStateBitsByPrimary(model); model.allObservedOutputs.assign(ctx.allObservedOutputs.begin(), ctx.allObservedOutputs.end()); @@ -3703,7 +3679,7 @@ void recordBoundaryInputVars( continue; // LCOV_EXCL_LINE } const size_t varID = termDNLID2varID[inputTermID]; - if (varID < 2) { + if (varID == static_cast(-1) || varID < 2) { continue; // LCOV_EXCL_LINE } model.inputVarByKey.emplace(keyIt->second, varID); @@ -4151,60 +4127,6 @@ void buildStructuredMemoryTransitions( // LCOV_EXCL_STOP -void materializeBoundaryObservedOutputs( - const std::vector>& observedTerms, - // LCOV_EXCL_START - const std::unordered_map& outputExprByTerm, - const std::unordered_map& skippedOutputsByTerm, - const std::vector& builderInputs, - const std::vector& builderOutputs, - const std::vector& termDNLID2varID, - // LCOV_EXCL_STOP - SequentialDesignModel& model) { - // LCOV_EXCL_START - for (const auto& [termID, key] : observedTerms) { - if (const auto exprIt = outputExprByTerm.find(termID); - exprIt != outputExprByTerm.end()) { - // LCOV_EXCL_STOP - model.observedOutputExprByKey.emplace(key, exprIt->second); - // LCOV_EXCL_START - continue; - } - if (const auto skippedIt = skippedOutputsByTerm.find(termID); // LCOV_EXCL_LINE - skippedIt != skippedOutputsByTerm.end()) { // LCOV_EXCL_LINE - // LCOV_EXCL_STOP - if (auto skipInfo = getConnectivitySkipInfo(skippedIt->second); // LCOV_EXCL_LINE - skipInfo.has_value()) { // LCOV_EXCL_LINE - model.connectivitySkipInfoByKey.emplace(key, *skipInfo); // LCOV_EXCL_LINE - continue; // LCOV_EXCL_LINE - } - model.unsupportedReasons.push_back( // LCOV_EXCL_LINE - "Unsupported SEC boundary output `" + model.displayNameByKey.at(key) + // LCOV_EXCL_LINE - "`: " + skippedIt->second.detail); // LCOV_EXCL_LINE - continue; // LCOV_EXCL_LINE - } - - const auto built = buildObservedExprForTerm( // LCOV_EXCL_LINE - termID, outputExprByTerm, builderInputs, builderOutputs, termDNLID2varID); // LCOV_EXCL_LINE - if (built.expr != nullptr) { // LCOV_EXCL_LINE - // LCOV_EXCL_START - model.observedOutputExprByKey.emplace(key, built.expr); // LCOV_EXCL_LINE - continue; // LCOV_EXCL_LINE - } - if (built.connectivitySkip.has_value()) { // LCOV_EXCL_LINE - model.connectivitySkipInfoByKey.emplace(key, *built.connectivitySkip); // LCOV_EXCL_LINE - continue; // LCOV_EXCL_LINE - // LCOV_EXCL_STOP - } - // LCOV_EXCL_START - model.unsupportedReasons.push_back( // LCOV_EXCL_LINE - "Unsupported SEC boundary output `" + model.displayNameByKey.at(key) + // LCOV_EXCL_LINE - "`: " + built.unsupportedReason); // LCOV_EXCL_LINE - } // LCOV_EXCL_LINE - // LCOV_EXCL_STOP -} - -// LCOV_EXCL_START void materializeTopObservedOutputs( const std::unordered_map& topOutputKeyByTerm, // LCOV_EXCL_STOP @@ -4212,6 +4134,11 @@ void materializeTopObservedOutputs( const std::unordered_map& skippedOutputsByTerm, SequentialDesignModel& model) { for (const auto& [termID, key] : topOutputKeyByTerm) { + const auto existingSkip = model.connectivitySkipInfoByKey.find(key); + if (existingSkip != model.connectivitySkipInfoByKey.end() && + existingSkip->second.origin == ConnectivitySkipOrigin::OpaqueInternal) { + continue; + } auto exprIt = outputExprByTerm.find(termID); if (exprIt != outputExprByTerm.end()) { model.observedOutputExprByKey.emplace(key, exprIt->second); @@ -4242,22 +4169,8 @@ void materializeTopObservedOutputs( // LCOV_EXCL_STOP struct RebuiltTransitionArtifacts { std::unordered_set requiredStateKeys; - std::set lateAbstractedBoundaryStateKeys; - std::vector> lateAbstractedBoundaryObservedTerms; }; -constexpr size_t kMaxCompleteStateFrontierForStartupMatching = 5000; - -bool shouldRetainCompleteStateFrontierForStartupMatching(size_t stateCount) { - // Moderate-size SEC cases benefit from the complete transition relation: - // reset/startup checks can inspect local sequential cones without relying on - // internal flop names. Large ASICs still use the COI frontier so - // LCOV_EXCL_START - // BlackParrot-scale proofs do not materialize every sequential cone up front. - // LCOV_EXCL_STOP - return stateCount <= kMaxCompleteStateFrontierForStartupMatching; -} - template void enqueueStateDependenciesFromFormula( BoolExpr* expr, @@ -4320,80 +4233,35 @@ void removeDeadFoldedClockGateLatchInputs( void substituteFoldedClockGateLatchVarsInModel( SequentialDesignModel& model, const std::unordered_map& clockGateLatchDataExprByVarID); -// LCOV_EXCL_START -void markFormulasWithUnpublishedSupportAsSkipped( - const ExtractContext& ctx, - // LCOV_EXCL_STOP - SequentialDesignModel& model); // LCOV_EXCL_START RebuiltTransitionArtifacts rebuildRequiredStateTransitions( -// LCOV_EXCL_STOP - ExtractContext& ctx, - SequentialDesignModel& model, + // LCOV_EXCL_STOP + ExtractContext& ctx, SequentialDesignModel& model, // LCOV_EXCL_START std::vector& builderInputs, std::vector& builderOutputs, std::vector& termDNLID2varID, std::unordered_map& outputExprByTerm, // LCOV_EXCL_STOP - std::unordered_map& skippedOutputsByTerm) { + std::unordered_map& + skippedOutputsByTerm) { // LCOV_EXCL_START RebuiltTransitionArtifacts artifacts; - auto markConnectivitySkippedState = - [&](const SignalKey& key, const ConnectivitySkipInfo& info) { - model.connectivitySkipInfoByKey.emplace(key, info); - }; - auto markUnsupportedState = [&](const SignalKey& key) { - ctx.unsupportedStateBits.insert(key); // LCOV_EXCL_LINE - }; // LCOV_EXCL_LINE - - std::unordered_set lateAbstractedBoundaryObservedKeys; - std::unordered_set lateAbstractedBoundaryIndexes; - auto recordLateAbstractedInstanceBoundary = - // LCOV_EXCL_STOP - [&](size_t boundaryInfoIndex, const std::string& reason) { // LCOV_EXCL_LINE - // LCOV_EXCL_START - if (boundaryInfoIndex == std::numeric_limits::max()) { // LCOV_EXCL_LINE - return; // LCOV_EXCL_LINE - } - if (!lateAbstractedBoundaryIndexes.insert(boundaryInfoIndex).second) { // LCOV_EXCL_LINE - return; // LCOV_EXCL_LINE - // LCOV_EXCL_STOP - } - -// LCOV_EXCL_START - - -// LCOV_EXCL_STOP - const auto& info = ctx.instanceBoundaryInfos[boundaryInfoIndex]; // LCOV_EXCL_LINE - if (ctx.secDiagEnabled) { // LCOV_EXCL_LINE - fprintf( // LCOV_EXCL_LINE - stderr, // LCOV_EXCL_LINE - "SEC diag: extract(%s) late abstracted sequential instance `%s`: %s\n", - ctx.topName.c_str(), // LCOV_EXCL_LINE - info.instancePath.c_str(), // LCOV_EXCL_LINE - reason.c_str()); // LCOV_EXCL_LINE - // LCOV_EXCL_START - fflush(stderr); // LCOV_EXCL_LINE - // LCOV_EXCL_STOP - } // LCOV_EXCL_LINE - model.abstractedSequentialBoundaries.push_back( // LCOV_EXCL_LINE - "Abstracted uncomputable sequential instance `" + // LCOV_EXCL_LINE - info.instancePath + "` as a SEC boundary: " + reason); // LCOV_EXCL_LINE - model.abstractedSequentialBoundaryDetails.push_back( // LCOV_EXCL_LINE - makeAbstractedBoundaryDetail(info)); // LCOV_EXCL_LINE - for (const auto& key : info.stateKeys) { // LCOV_EXCL_LINE - artifacts.lateAbstractedBoundaryStateKeys.insert(key); // LCOV_EXCL_LINE - } - for (const auto& observedTerm : info.observedTerms) { // LCOV_EXCL_LINE - if (lateAbstractedBoundaryObservedKeys.insert(observedTerm.key).second) { // LCOV_EXCL_LINE - artifacts.lateAbstractedBoundaryObservedTerms.emplace_back( // LCOV_EXCL_LINE - observedTerm.termID, observedTerm.key); // LCOV_EXCL_LINE - } // LCOV_EXCL_LINE - } - }; // LCOV_EXCL_LINE + auto markConnectivitySkippedState = [&](const SignalKey& key, + const ConnectivitySkipInfo& info) { + model.connectivitySkipInfoByKey.insert_or_assign(key, info); + }; + auto markOpaqueState = [&](const PendingTransition& pending, + const std::string& reason) { + const auto info = makeOpaqueInternalSkip( + ctx.dnl->getDNLTerminalFromID(pending.stateTermID), reason); + markConnectivitySkippedState(pending.stateKey, info); + for (const auto& key : pending.complementedStateKeys) { + markConnectivitySkippedState(key, info); + } + }; std::unordered_map requiredStateKeyByVarID; requiredStateKeyByVarID.reserve(model.stateBits.size()); @@ -4402,7 +4270,7 @@ RebuiltTransitionArtifacts rebuildRequiredStateTransitions( for (const auto& key : model.stateBits) { const auto varIt = model.inputVarByKey.find(key); if (varIt == model.inputVarByKey.end()) { - continue; // LCOV_EXCL_LINE + continue; // LCOV_EXCL_LINE } requiredStateKeyByVarID.emplace(varIt->second, key); requiredStateVarIDs.insert(varIt->second); @@ -4410,7 +4278,8 @@ RebuiltTransitionArtifacts rebuildRequiredStateTransitions( std::unordered_map pendingIndexByStateKey; pendingIndexByStateKey.reserve(ctx.pendingTransitions.size() * 2); - for (size_t pendingIndex = 0; pendingIndex < ctx.pendingTransitions.size(); ++pendingIndex) { + for (size_t pendingIndex = 0; pendingIndex < ctx.pendingTransitions.size(); + ++pendingIndex) { const auto& pending = ctx.pendingTransitions[pendingIndex]; pendingIndexByStateKey.emplace(pending.stateKey, pendingIndex); for (const auto& complementedKey : pending.complementedStateKeys) { @@ -4424,7 +4293,11 @@ RebuiltTransitionArtifacts rebuildRequiredStateTransitions( // DNL connectivity and cell identities are immutable during extraction. // Classify the structural clock tree once, then only apply variable mappings // that become available as the dependency frontier is materialized. - PureClockCarrierStructureIndex pureClockCarrierStructure(ctx.dnl); + if (ctx.pureClockCarrierStructure == nullptr) { + ctx.pureClockCarrierStructure = + std::make_unique(ctx.dnl); + } + const auto& pureClockCarrierStructure = *ctx.pureClockCarrierStructure; std::unordered_set pureClockCarrierTermIDs( pureClockCarrierStructure.pureClockCarrierTermIDs().begin(), pureClockCarrierStructure.pureClockCarrierTermIDs().end()); @@ -4433,8 +4306,7 @@ RebuiltTransitionArtifacts rebuildRequiredStateTransitions( stderr, "SEC diag: extract(%s) immutable clock structure indexed terms=%zu " "pure_terms=%zu\n", - ctx.topName.c_str(), - ctx.dnl == nullptr ? 0 : ctx.dnl->getNBterms(), + ctx.topName.c_str(), ctx.dnl == nullptr ? 0 : ctx.dnl->getNBterms(), pureClockCarrierTermIDs.size()); std::fflush(stderr); } @@ -4451,26 +4323,28 @@ RebuiltTransitionArtifacts rebuildRequiredStateTransitions( ++addedTop; } } - const size_t addedBoundary = expandClockCarrierVarIDsFromBoundaryNames( - model, topClockCarrierVarIDs); + const size_t addedBoundary = + expandClockCarrierVarIDsFromBoundaryNames(model, topClockCarrierVarIDs); // LCOV_EXCL_START const size_t addedTermNames = expandClockCarrierVarIDsFromTermNames( ctx.dnl, termDNLID2varID, topClockCarrierVarIDs); - // LCOV_EXCL_STOP + // LCOV_EXCL_STOP const size_t addedStructure = - pureClockCarrierStructure.addMappedCarrierVarIDs( - model, termDNLID2varID, topClockCarrierVarIDs); - const size_t addedPureExprs = expandClockCarrierVarIDsFromPureClockTermExprs( - pureClockCarrierTermIDs, outputExprByTerm, topClockCarrierVarIDs); - seedTopClockCarrierEvents(model, topClockCarrierVarIDs, clockEventByCarrierVarID); - const size_t addedMaterialized = expandClockCarrierVarIDsFromMaterializedTerms( - termDNLID2varID, - outputExprByTerm, - // LCOV_EXCL_STOP - topClockCarrierVarIDs, - // LCOV_EXCL_START - clockEventByCarrierVarID); - // LCOV_EXCL_STOP + pureClockCarrierStructure.addMappedCarrierVarIDs(model, termDNLID2varID, + topClockCarrierVarIDs); + const size_t addedPureExprs = + expandClockCarrierVarIDsFromPureClockTermExprs( + pureClockCarrierTermIDs, outputExprByTerm, topClockCarrierVarIDs); + seedTopClockCarrierEvents(model, topClockCarrierVarIDs, + clockEventByCarrierVarID); + const size_t addedMaterialized = + expandClockCarrierVarIDsFromMaterializedTerms(termDNLID2varID, + outputExprByTerm, + // LCOV_EXCL_STOP + topClockCarrierVarIDs, + // LCOV_EXCL_START + clockEventByCarrierVarID); + // LCOV_EXCL_STOP const size_t added = addedTop + addedBoundary + addedTermNames + addedStructure + addedPureExprs + addedMaterialized; if (added != 0) { @@ -4480,22 +4354,23 @@ RebuiltTransitionArtifacts rebuildRequiredStateTransitions( transitionClockStripMemo.clear(); } if (std::getenv("KEPLER_SEC_CLOCK_GATE_DIAG") != nullptr) { - std::fprintf( // LCOV_EXCL_LINE - stderr, // LCOV_EXCL_LINE + std::fprintf( // LCOV_EXCL_LINE + stderr, // LCOV_EXCL_LINE "SEC diag: clock carriers top=%zu boundary=%zu term_names=%zu " - "structure=%zu pure_exprs=%zu materialized=%zu total=%zu pure_terms=%zu\n", - addedTop, // LCOV_EXCL_LINE - addedBoundary, // LCOV_EXCL_LINE - addedTermNames, // LCOV_EXCL_LINE - addedStructure, // LCOV_EXCL_LINE - addedPureExprs, // LCOV_EXCL_LINE - addedMaterialized, // LCOV_EXCL_LINE - topClockCarrierVarIDs.size(), // LCOV_EXCL_LINE - pureClockCarrierTermIDs.size()); // LCOV_EXCL_LINE - std::fflush(stderr); // LCOV_EXCL_LINE - } // LCOV_EXCL_LINE + "structure=%zu pure_exprs=%zu materialized=%zu total=%zu " + "pure_terms=%zu\n", + addedTop, // LCOV_EXCL_LINE + addedBoundary, // LCOV_EXCL_LINE + addedTermNames, // LCOV_EXCL_LINE + addedStructure, // LCOV_EXCL_LINE + addedPureExprs, // LCOV_EXCL_LINE + addedMaterialized, // LCOV_EXCL_LINE + topClockCarrierVarIDs.size(), // LCOV_EXCL_LINE + pureClockCarrierTermIDs.size()); // LCOV_EXCL_LINE + std::fflush(stderr); // LCOV_EXCL_LINE + } // LCOV_EXCL_LINE return added; - }; // LCOV_EXCL_LINE + }; // LCOV_EXCL_LINE // LCOV_EXCL_START refreshClockCarrierVarIDs(); // LCOV_EXCL_STOP @@ -4508,42 +4383,37 @@ RebuiltTransitionArtifacts rebuildRequiredStateTransitions( } if (!latchDependencyTerms.empty()) { const auto dependencyOutputs = materializeBuilderOutputs( - latchDependencyTerms, - builderInputs, - termDNLID2varID, - ctx.collectedSkippedOutputs, - ctx.secDiagEnabled, - ctx.topName.c_str(), + latchDependencyTerms, builderInputs, termDNLID2varID, + ctx.collectedSkippedOutputs, ctx.secDiagEnabled, ctx.topName.c_str(), "clock-gate latch dependency build"); appendUniqueTermIDs(builderInputs, dependencyOutputs.inputs); appendUniqueTermIDs(builderOutputs, dependencyOutputs.outputs); - mergeBuilderTermVarIDs(termDNLID2varID, dependencyOutputs.termDNLID2varID); + mergeBuilderTermVarIDs(termDNLID2varID, + dependencyOutputs.termDNLID2varID); recordBoundaryInputVars(ctx, builderInputs, termDNLID2varID, model); for (const auto& [termID, expr] : dependencyOutputs.outputExprByTerm) { outputExprByTerm.insert_or_assign(termID, expr); } - for (const auto& [termID, info] : dependencyOutputs.skippedOutputsByTerm) { - skippedOutputsByTerm.emplace(termID, info); // LCOV_EXCL_LINE + for (const auto& [termID, info] : + dependencyOutputs.skippedOutputsByTerm) { + skippedOutputsByTerm.emplace(termID, info); // LCOV_EXCL_LINE } refreshClockCarrierVarIDs(); } - // LCOV_EXCL_START + // LCOV_EXCL_START } // LCOV_EXCL_STOP std::unordered_map clockGateLatchDataExprByVarID = - collectClockGateLatchDataExprByVarID( - ctx, model, topClockCarrierVarIDs, outputExprByTerm); - // LCOV_EXCL_START + collectClockGateLatchDataExprByVarID(ctx, model, topClockCarrierVarIDs, + outputExprByTerm); std::deque pendingWorkQueue; - // LCOV_EXCL_STOP std::deque stateDependencyWorkQueue; std::unordered_set expandedStateDependencies; std::vector dependencyWalkStack; std::unordered_set scannedDependencyNodes; - scannedDependencyNodes.reserve(std::max(1024, outputExprByTerm.size() * 16)); - // LCOV_EXCL_START + scannedDependencyNodes.reserve( + std::max(1024, outputExprByTerm.size() * 16)); std::unordered_set enqueuedStateVarIDs; - // LCOV_EXCL_STOP enqueuedStateVarIDs.reserve(requiredStateVarIDs.size()); auto enqueueRequiredStateKey = [&](const SignalKey& key) { if (!artifacts.requiredStateKeys.insert(key).second) { @@ -4558,29 +4428,25 @@ RebuiltTransitionArtifacts rebuildRequiredStateTransitions( }; auto enqueueRequiredStateVarID = [&](size_t varID) { if (!enqueuedStateVarIDs.insert(varID).second) { - return; // LCOV_EXCL_LINE + return; } const auto stateIt = requiredStateKeyByVarID.find(varID); - if (stateIt == requiredStateKeyByVarID.end()) { - return; // LCOV_EXCL_LINE + if (stateIt != requiredStateKeyByVarID.end()) { + enqueueRequiredStateKey(stateIt->second); } - enqueueRequiredStateKey(stateIt->second); }; auto enqueueStateDependenciesFromExpr = [&](BoolExpr* expr) { if (expr == nullptr || !expr->isValid()) { - return; // LCOV_EXCL_LINE + return; } enqueueStateDependenciesFromFormula( - expr, - requiredStateVarIDs, - dependencyWalkStack, - scannedDependencyNodes, + expr, requiredStateVarIDs, dependencyWalkStack, scannedDependencyNodes, enqueueRequiredStateVarID); - // LCOV_EXCL_START }; - // LCOV_EXCL_STOP - if (shouldRetainCompleteStateFrontierForStartupMatching(model.stateBits.size())) { + constexpr size_t kMaxCompleteStateFrontierForStartupMatching = 5000; + if (!ctx.hasOpaqueInternalTerminals && + model.stateBits.size() <= kMaxCompleteStateFrontierForStartupMatching) { for (const auto& key : model.stateBits) { enqueueRequiredStateKey(key); } @@ -4590,10 +4456,6 @@ RebuiltTransitionArtifacts rebuildRequiredStateTransitions( enqueueStateDependenciesFromExpr(expr); } - // Follow the state/output dependency frontier lazily so SEC only rebuilds the - // sequential update cones that can actually influence covered observations. - // States with prebuilt next-state relations, such as structured memories, - // participate in the same frontier expansion before we trim the SEC model. while (!stateDependencyWorkQueue.empty() || !pendingWorkQueue.empty()) { while (!stateDependencyWorkQueue.empty()) { const SignalKey key = stateDependencyWorkQueue.front(); @@ -4603,7 +4465,7 @@ RebuiltTransitionArtifacts rebuildRequiredStateTransitions( continue; } if (!expandedStateDependencies.insert(key).second) { - continue; // LCOV_EXCL_LINE + continue; // LCOV_EXCL_LINE } enqueueStateDependenciesFromExpr(nextStateIt->second); } @@ -4618,13 +4480,11 @@ RebuiltTransitionArtifacts rebuildRequiredStateTransitions( while (!pendingWorkQueue.empty()) { const size_t pendingIndex = pendingWorkQueue.front(); pendingWorkQueue.pop_front(); - batchPendingIndexes.push_back(pendingIndex); - const auto& pending = ctx.pendingTransitions[pendingIndex]; + batchPendingIndexes.push_back(pendingIndex); artifacts.requiredStateKeys.insert(pending.stateKey); - for (const auto& complementedKey : pending.complementedStateKeys) { - artifacts.requiredStateKeys.insert(complementedKey); - } + artifacts.requiredStateKeys.insert(pending.complementedStateKeys.begin(), + pending.complementedStateKeys.end()); for (const auto& [_, term] : getPendingUpdateTerms(pending)) { if (materializedOutputTerms.insert(term.termID).second && @@ -4633,9 +4493,6 @@ RebuiltTransitionArtifacts rebuildRequiredStateTransitions( } } for (const auto& clockTerm : pending.clockTermIDs) { - // Clock pins may have been materialized earlier as plain carriers. - // Rebuild required clocks after boundary/latch dependencies are known - // so gated clocks become clk & enable instead of a stale clk frontier. const bool alreadyMaterialized = !materializedOutputTerms.insert(clockTerm.termID).second; if ((!alreadyMaterialized || @@ -4648,218 +4505,120 @@ RebuiltTransitionArtifacts rebuildRequiredStateTransitions( if (!batchOutputTerms.empty()) { const auto dependencyOutputs = materializeBuilderOutputs( - batchOutputTerms, - builderInputs, - termDNLID2varID, - ctx.collectedSkippedOutputs, - ctx.secDiagEnabled, - // LCOV_EXCL_START - ctx.topName.c_str(), - // LCOV_EXCL_STOP + batchOutputTerms, builderInputs, termDNLID2varID, + ctx.collectedSkippedOutputs, ctx.secDiagEnabled, ctx.topName.c_str(), "dependency build"); appendUniqueTermIDs(builderInputs, dependencyOutputs.inputs); appendUniqueTermIDs(builderOutputs, dependencyOutputs.outputs); - mergeBuilderTermVarIDs(termDNLID2varID, dependencyOutputs.termDNLID2varID); + mergeBuilderTermVarIDs(termDNLID2varID, + dependencyOutputs.termDNLID2varID); recordBoundaryInputVars(ctx, builderInputs, termDNLID2varID, model); for (const auto& [termID, expr] : dependencyOutputs.outputExprByTerm) { outputExprByTerm.insert_or_assign(termID, expr); } - for (const auto& [termID, info] : dependencyOutputs.skippedOutputsByTerm) { + for (const auto& [termID, info] : + dependencyOutputs.skippedOutputsByTerm) { skippedOutputsByTerm.emplace(termID, info); - // LCOV_EXCL_START } - // LCOV_EXCL_STOP refreshClockCarrierVarIDs(); - clockGateLatchDataExprByVarID = - collectClockGateLatchDataExprByVarID( - ctx, model, topClockCarrierVarIDs, outputExprByTerm); + clockGateLatchDataExprByVarID = collectClockGateLatchDataExprByVarID( + ctx, model, topClockCarrierVarIDs, outputExprByTerm); } for (const auto pendingIndex : batchPendingIndexes) { const auto& pending = ctx.pendingTransitions[pendingIndex]; std::optional skippedPinInfo; - // LCOV_EXCL_START bool abortPending = false; for (const auto& [modelTerm, term] : getPendingUpdateTerms(pending)) { - auto skippedIt = skippedOutputsByTerm.find(term.termID); - // LCOV_EXCL_STOP + const auto skippedIt = skippedOutputsByTerm.find(term.termID); if (skippedIt == skippedOutputsByTerm.end()) { continue; - // LCOV_EXCL_START } - - if (auto skipInfo = getConnectivitySkipInfo(skippedIt->second); - skipInfo.has_value()) { - // LCOV_EXCL_START - skippedPinInfo = { - skipInfo->origin, - // LCOV_EXCL_STOP + if (auto skipInfo = getConnectivitySkipInfo(skippedIt->second)) { + skippedPinInfo = *skipInfo; + skippedPinInfo->detail = "Sequential terminal `" + modelTerm->getName().getString() + - "` was skipped because " + - skippedIt->second.detail, - }; + "` was skipped because " + skippedIt->second.detail; break; } - - if (ctx.abstractUncomputableSequentialBoundaries) { // LCOV_EXCL_LINE - recordLateAbstractedInstanceBoundary( // LCOV_EXCL_LINE - pending.boundaryInfoIndex, // LCOV_EXCL_LINE - "unsupported sequential terminal `" + // LCOV_EXCL_LINE - modelTerm->getName().getString() + "`: " + // LCOV_EXCL_LINE - skippedIt->second.detail); // LCOV_EXCL_LINE - abortPending = true; // LCOV_EXCL_LINE - break; // LCOV_EXCL_LINE - } - - -// LCOV_EXCL_STOP - model.unsupportedReasons.push_back( // LCOV_EXCL_LINE - // LCOV_EXCL_START - "Unsupported sequential primitive for `" + signalKeyToString(pending.stateKey) + // LCOV_EXCL_LINE - // LCOV_EXCL_STOP - "`: Sequential terminal `" + modelTerm->getName().getString() + // LCOV_EXCL_LINE - "` is unsupported: " + // LCOV_EXCL_LINE - skippedIt->second.detail); // LCOV_EXCL_LINE - // LCOV_EXCL_START - markUnsupportedState(pending.stateKey); // LCOV_EXCL_LINE - for (const auto& complementedKey : pending.complementedStateKeys) { // LCOV_EXCL_LINE - markUnsupportedState(complementedKey); // LCOV_EXCL_LINE - } - abortPending = true; // LCOV_EXCL_LINE - break; // LCOV_EXCL_LINE + markOpaqueState(pending, "unsupported sequential terminal `" + + modelTerm->getName().getString() + + "`: " + skippedIt->second.detail); + abortPending = true; + break; } - // LCOV_EXCL_STOP - if (!skippedPinInfo.has_value()) { - // LCOV_EXCL_START + if (!skippedPinInfo.has_value() && !abortPending) { for (const auto& clockTerm : pending.clockTermIDs) { - auto skippedIt = skippedOutputsByTerm.find(clockTerm.termID); + const auto skippedIt = skippedOutputsByTerm.find(clockTerm.termID); if (skippedIt == skippedOutputsByTerm.end()) { continue; } - - if (auto skipInfo = getConnectivitySkipInfo(skippedIt->second); // LCOV_EXCL_LINE - skipInfo.has_value()) { // LCOV_EXCL_LINE - // LCOV_EXCL_STOP - skippedPinInfo = { // LCOV_EXCL_LINE - // LCOV_EXCL_START - skipInfo->origin, // LCOV_EXCL_LINE - "Sequential clock pin was skipped because " + // LCOV_EXCL_LINE - // LCOV_EXCL_STOP - skippedIt->second.detail, // LCOV_EXCL_LINE - }; - break; // LCOV_EXCL_LINE - } - -// LCOV_EXCL_START - - -// LCOV_EXCL_STOP - if (ctx.abstractUncomputableSequentialBoundaries) { // LCOV_EXCL_LINE - recordLateAbstractedInstanceBoundary( // LCOV_EXCL_LINE - pending.boundaryInfoIndex, // LCOV_EXCL_LINE - "unsupported sequential clock pin: " + // LCOV_EXCL_LINE - // LCOV_EXCL_START - skippedIt->second.detail); // LCOV_EXCL_LINE - // LCOV_EXCL_STOP - abortPending = true; // LCOV_EXCL_LINE - break; // LCOV_EXCL_LINE - } - - model.unsupportedReasons.push_back( // LCOV_EXCL_LINE - "Unsupported sequential primitive for `" + // LCOV_EXCL_LINE - signalKeyToString(pending.stateKey) + // LCOV_EXCL_LINE - "`: Sequential clock pin is unsupported: " + // LCOV_EXCL_LINE - skippedIt->second.detail); // LCOV_EXCL_LINE - markUnsupportedState(pending.stateKey); // LCOV_EXCL_LINE - for (const auto& complementedKey : pending.complementedStateKeys) { // LCOV_EXCL_LINE - markUnsupportedState(complementedKey); // LCOV_EXCL_LINE + if (auto skipInfo = getConnectivitySkipInfo(skippedIt->second)) { + skippedPinInfo = *skipInfo; + skippedPinInfo->detail = + "Sequential clock pin was skipped because " + + skippedIt->second.detail; + break; } - abortPending = true; // LCOV_EXCL_LINE - break; // LCOV_EXCL_LINE + markOpaqueState(pending, "unsupported sequential clock pin: " + + skippedIt->second.detail); + abortPending = true; + break; } } if (abortPending) { - continue; // LCOV_EXCL_LINE + continue; } if (skippedPinInfo.has_value()) { markConnectivitySkippedState(pending.stateKey, *skippedPinInfo); for (const auto& complementedKey : pending.complementedStateKeys) { - markConnectivitySkippedState(complementedKey, *skippedPinInfo); // LCOV_EXCL_LINE + markConnectivitySkippedState(complementedKey, *skippedPinInfo); } continue; } BoolExpr* nextStateExpr = - buildNextStateExpr( - pending, - termDNLID2varID, - pureClockCarrierTermIDs, - topClockCarrierVarIDs, - clockEventByCarrierVarID, - clockGateLatchDataExprByVarID, - outputExprByTerm, - transitionClockStripMemo); + buildNextStateExpr(pending, termDNLID2varID, pureClockCarrierTermIDs, + topClockCarrierVarIDs, clockEventByCarrierVarID, + clockGateLatchDataExprByVarID, outputExprByTerm, + transitionClockStripMemo); const auto clockEvent = classifyPendingClockEvent( - pending, - termDNLID2varID, - outputExprByTerm, - clockEventByCarrierVarID); - // This diagnostic is intentionally after clock-carrier stripping: any - // remaining unpublished support would become a private proof input and - // can hide the real reason state matching stopped converging. - logUnpublishedTransitionSupport( - ctx, - // LCOV_EXCL_START - model, - pending, - nextStateExpr, - // LCOV_EXCL_STOP - termDNLID2varID, - topClockCarrierVarIDs, - // LCOV_EXCL_START - pureClockCarrierTermIDs); + pending, termDNLID2varID, outputExprByTerm, clockEventByCarrierVarID); + logUnpublishedTransitionSupport(ctx, model, pending, nextStateExpr, + termDNLID2varID, topClockCarrierVarIDs, + pureClockCarrierTermIDs); model.nextStateExprByStateKey.emplace(pending.stateKey, nextStateExpr); if (clockEvent.has_value()) { model.clockEventByStateKey.emplace(pending.stateKey, *clockEvent); } - // Liberty flops such as DFF_X1 expose both Q and QN. They share one - // LCOV_EXCL_STOP - // storage element, so complementary outputs inherit the same next-state - // LCOV_EXCL_START - // function with a logical inversion. - // LCOV_EXCL_STOP for (const auto& complementedKey : pending.complementedStateKeys) { - model.nextStateExprByStateKey.emplace(complementedKey, BoolExpr::Not(nextStateExpr)); + model.nextStateExprByStateKey.emplace(complementedKey, + BoolExpr::Not(nextStateExpr)); if (clockEvent.has_value()) { model.clockEventByStateKey.emplace(complementedKey, *clockEvent); } - if (artifacts.requiredStateKeys.find(complementedKey) != - artifacts.requiredStateKeys.end()) { - stateDependencyWorkQueue.push_back(complementedKey); - } + stateDependencyWorkQueue.push_back(complementedKey); } stateDependencyWorkQueue.push_back(pending.stateKey); } } - if (ctx.secDiagEnabled) { - fprintf( - stderr, - "SEC diag: extract(%s) rebuilt next-state exprs=%zu init=%zu\n", - ctx.topName.c_str(), - model.nextStateExprByStateKey.size(), - model.initialStateValueByKey.size()); + fprintf(stderr, + "SEC diag: extract(%s) rebuilt next-state exprs=%zu init=%zu\n", + ctx.topName.c_str(), model.nextStateExprByStateKey.size(), + model.initialStateValueByKey.size()); fflush(stderr); } - model.clockCarrierVarIDs.assign( - topClockCarrierVarIDs.begin(), topClockCarrierVarIDs.end()); + model.clockCarrierVarIDs.assign(topClockCarrierVarIDs.begin(), + topClockCarrierVarIDs.end()); std::sort(model.clockCarrierVarIDs.begin(), model.clockCarrierVarIDs.end()); model.clockCarrierClasses = buildClockCarrierClasses( model.clockCarrierVarIDs, clockEventByCarrierVarID); - substituteFoldedClockGateLatchVarsInModel(model, clockGateLatchDataExprByVarID); + substituteFoldedClockGateLatchVarsInModel(model, + clockGateLatchDataExprByVarID); removeDeadFoldedClockGateLatchInputs(model, clockGateLatchDataExprByVarID); return artifacts; @@ -4923,7 +4682,6 @@ void removeDeadFoldedClockGateLatchInputs( continue; // LCOV_EXCL_LINE } eraseSignalKeyFromVector(model.environmentInputs, keyIt->second); - eraseSignalKeyFromVector(model.internalBoundaryInputKeys, keyIt->second); model.inputVarByKey.erase(keyIt->second); } } @@ -5132,14 +4890,8 @@ void markMultiClockDomainConesAsSkipped(SequentialDesignModel& model) { } void applyRebuiltTransitionArtifacts( - const ExtractContext& ctx, const RebuiltTransitionArtifacts& artifacts, - SequentialDesignModel& model, - const std::vector& builderInputs, - const std::vector& builderOutputs, - const std::vector& termDNLID2varID, - const std::unordered_map& outputExprByTerm, - const std::unordered_map& skippedOutputsByTerm) { + SequentialDesignModel& model) { // The transition rebuild already closes the observed-output frontier while it // can still materialize missing next-state cones. Pruning must reuse that // frontier instead of rewalking the whole BoolExpr model again; the latter is @@ -5180,89 +4932,9 @@ void applyRebuiltTransitionArtifacts( retainedStateKeys.end(); }), model.complementedStateRelations.end()); - - for (const auto& key : artifacts.lateAbstractedBoundaryStateKeys) { - model.nextStateExprByStateKey.erase(key); // LCOV_EXCL_LINE - model.initialStateValueByKey.erase(key); // LCOV_EXCL_LINE - model.clockEventByStateKey.erase(key); // LCOV_EXCL_LINE - // LCOV_EXCL_STOP - if (std::find(model.environmentInputs.begin(), model.environmentInputs.end(), key) == // LCOV_EXCL_LINE - // LCOV_EXCL_START - model.environmentInputs.end()) { // LCOV_EXCL_LINE - model.environmentInputs.push_back(key); // LCOV_EXCL_LINE - // LCOV_EXCL_STOP - } // LCOV_EXCL_LINE - } - // LCOV_EXCL_START - if (!artifacts.lateAbstractedBoundaryStateKeys.empty()) { - model.stateBits.erase( // LCOV_EXCL_LINE - std::remove_if( // LCOV_EXCL_LINE - model.stateBits.begin(), // LCOV_EXCL_LINE - // LCOV_EXCL_STOP - model.stateBits.end(), // LCOV_EXCL_LINE - [&](const SignalKey& key) { // LCOV_EXCL_LINE - return artifacts.lateAbstractedBoundaryStateKeys.find(key) != // LCOV_EXCL_LINE - artifacts.lateAbstractedBoundaryStateKeys.end(); // LCOV_EXCL_LINE - }), - model.stateBits.end()); // LCOV_EXCL_LINE - model.complementedStateRelations.erase( // LCOV_EXCL_LINE - std::remove_if( // LCOV_EXCL_LINE - model.complementedStateRelations.begin(), // LCOV_EXCL_LINE - model.complementedStateRelations.end(), // LCOV_EXCL_LINE - [&](const ComplementedStateRelation& relation) { // LCOV_EXCL_LINE - return artifacts.lateAbstractedBoundaryStateKeys.find(relation.primaryKey) != // LCOV_EXCL_LINE - artifacts.lateAbstractedBoundaryStateKeys.end() || // LCOV_EXCL_LINE - artifacts.lateAbstractedBoundaryStateKeys.find( // LCOV_EXCL_LINE - relation.complementedKey) != // LCOV_EXCL_LINE - artifacts.lateAbstractedBoundaryStateKeys.end(); // LCOV_EXCL_LINE - }), - model.complementedStateRelations.end()); // LCOV_EXCL_LINE - } // LCOV_EXCL_LINE - - for (const auto& [_, key] : artifacts.lateAbstractedBoundaryObservedTerms) { - if (std::find(model.allObservedOutputs.begin(), model.allObservedOutputs.end(), key) == // LCOV_EXCL_LINE - model.allObservedOutputs.end()) { // LCOV_EXCL_LINE - model.allObservedOutputs.push_back(key); // LCOV_EXCL_LINE - } // LCOV_EXCL_LINE - } - materializeBoundaryObservedOutputs( - artifacts.lateAbstractedBoundaryObservedTerms, - outputExprByTerm, - // LCOV_EXCL_START - skippedOutputsByTerm, - builderInputs, - // LCOV_EXCL_STOP - builderOutputs, - termDNLID2varID, - model); } void filterUnsupportedAndUnmappedBoundary(ExtractContext& ctx, SequentialDesignModel& model) { - { - // Any published leaf variable that is not retained as sequential state is a - // free SEC environment input, regardless of whether it originated from the - // top interface, an opaque internal boundary, or a later abstraction step. - // Compact SEC rebuilds the proof problem only from this normalized model, - // so leaving such leaves out of the environment interface causes remapped - // formulas to reference symbols that the shared proof symbol space never - // allocates. - std::unordered_set stateKeys( - model.stateBits.begin(), model.stateBits.end()); - std::unordered_set publishedInputs( - model.environmentInputs.begin(), model.environmentInputs.end()); - // LCOV_EXCL_START - for (const auto& [key, _] : model.inputVarByKey) { - if (stateKeys.find(key) != stateKeys.end()) { - continue; - } - if (publishedInputs.insert(key).second) { - model.environmentInputs.push_back(key); // LCOV_EXCL_LINE - } // LCOV_EXCL_LINE - // LCOV_EXCL_STOP - } - // LCOV_EXCL_START - } - // Inputs or state bits can disappear if the underlying BoolExpr builder // optimized them away to constants; remove them from the aligned interface. auto keepMappedInputs = [&](std::vector& keys) { @@ -5277,39 +4949,6 @@ void filterUnsupportedAndUnmappedBoundary(ExtractContext& ctx, SequentialDesignM }; keepMappedInputs(model.environmentInputs); keepMappedInputs(model.stateBits); - // LCOV_EXCL_STOP - if (!ctx.unsupportedStateBits.empty()) { - // LCOV_EXCL_START - model.stateBits.erase( // LCOV_EXCL_LINE - std::remove_if( // LCOV_EXCL_LINE - // LCOV_EXCL_STOP - model.stateBits.begin(), // LCOV_EXCL_LINE - model.stateBits.end(), // LCOV_EXCL_LINE - [&](const SignalKey& key) { // LCOV_EXCL_LINE - if (ctx.unsupportedStateBits.find(key) == ctx.unsupportedStateBits.end()) { // LCOV_EXCL_LINE - return false; // LCOV_EXCL_LINE - } - model.nextStateExprByStateKey.erase(key); // LCOV_EXCL_LINE - model.initialStateValueByKey.erase(key); // LCOV_EXCL_LINE - model.clockEventByStateKey.erase(key); // LCOV_EXCL_LINE - model.inputVarByKey.erase(key); // LCOV_EXCL_LINE - return true; // LCOV_EXCL_LINE - }), // LCOV_EXCL_LINE - model.stateBits.end()); // LCOV_EXCL_LINE - model.complementedStateRelations.erase( // LCOV_EXCL_LINE - std::remove_if( // LCOV_EXCL_LINE - model.complementedStateRelations.begin(), // LCOV_EXCL_LINE - model.complementedStateRelations.end(), // LCOV_EXCL_LINE - [&](const ComplementedStateRelation& relation) { // LCOV_EXCL_LINE - return ctx.unsupportedStateBits.find(relation.primaryKey) != // LCOV_EXCL_LINE - // LCOV_EXCL_START - ctx.unsupportedStateBits.end() || // LCOV_EXCL_LINE - // LCOV_EXCL_STOP - ctx.unsupportedStateBits.find(relation.complementedKey) != // LCOV_EXCL_LINE - ctx.unsupportedStateBits.end(); // LCOV_EXCL_LINE - }), - model.complementedStateRelations.end()); // LCOV_EXCL_LINE - } // LCOV_EXCL_LINE if (ctx.secDiagEnabled) { fprintf( stderr, @@ -5321,7 +4960,8 @@ void filterUnsupportedAndUnmappedBoundary(ExtractContext& ctx, SequentialDesignM } } -void propagateConnectivitySkipsThroughDependencies(SequentialDesignModel& model) { +void propagateConnectivitySkipsThroughDependencies( + SequentialDesignModel& model) { std::unordered_map stateKeyByVarID; size_t maxCandidateStateVarID = 0; std::deque pendingSkippedStateVars; @@ -5333,8 +4973,9 @@ void propagateConnectivitySkipsThroughDependencies(SequentialDesignModel& model) } stateKeyByVarID.emplace(varIt->second, key); maxCandidateStateVarID = std::max(maxCandidateStateVarID, varIt->second); - if (model.connectivitySkipInfoByKey.find(key) != - model.connectivitySkipInfoByKey.end() && + const auto skipIt = model.connectivitySkipInfoByKey.find(key); + if (skipIt != model.connectivitySkipInfoByKey.end() && + skipIt->second.origin != ConnectivitySkipOrigin::OpaqueInternal && enqueuedSkippedStateVars.insert(varIt->second).second) { pendingSkippedStateVars.push_back(varIt->second); } @@ -5477,13 +5118,12 @@ void propagateConnectivitySkipsThroughDependencies(SequentialDesignModel& model) return std::nullopt; // LCOV_EXCL_LINE // LCOV_EXCL_START } - return ConnectivitySkipInfo{ - skipInfoIt->second.origin, - "Depends on skipped state `" + model.displayNameByKey.at(sourceKeyIt->second) + - "` whose cone traces to a " + - // LCOV_EXCL_STOP - describeConnectivitySkipOrigin(skipInfoIt->second.origin) + " issue", - }; + ConnectivitySkipInfo dependencySkip = skipInfoIt->second; + dependencySkip.detail = + "Depends on skipped state `" + + model.displayNameByKey.at(sourceKeyIt->second) + "`: " + + skipInfoIt->second.detail; + return dependencySkip; }; while (!pendingSkippedStateVars.empty()) { @@ -5532,96 +5172,6 @@ void propagateConnectivitySkipsThroughDependencies(SequentialDesignModel& model) } } -void markFormulasWithUnpublishedSupportAsSkipped( // LCOV_EXCL_LINE - const ExtractContext& ctx, - // LCOV_EXCL_STOP - SequentialDesignModel& model) { - // LCOV_EXCL_START - size_t maxPublishedVarID = 1; // LCOV_EXCL_LINE - for (const auto& [_, varID] : model.inputVarByKey) { // LCOV_EXCL_LINE - maxPublishedVarID = std::max(maxPublishedVarID, varID); // LCOV_EXCL_LINE - } - std::vector isPublishedVar(maxPublishedVarID + 1, 0); // LCOV_EXCL_LINE - for (const auto& key : model.environmentInputs) { // LCOV_EXCL_LINE - const auto varIt = model.inputVarByKey.find(key); // LCOV_EXCL_LINE - // LCOV_EXCL_STOP - if (varIt != model.inputVarByKey.end()) { // LCOV_EXCL_LINE - // LCOV_EXCL_START - isPublishedVar[varIt->second] = 1; // LCOV_EXCL_LINE - } // LCOV_EXCL_LINE - } - for (const auto& key : model.stateBits) { // LCOV_EXCL_LINE - const auto varIt = model.inputVarByKey.find(key); // LCOV_EXCL_LINE - // LCOV_EXCL_STOP - if (varIt != model.inputVarByKey.end()) { // LCOV_EXCL_LINE - isPublishedVar[varIt->second] = 1; // LCOV_EXCL_LINE - // LCOV_EXCL_START - } // LCOV_EXCL_LINE - } - CandidateDependencyScratch scratch; // LCOV_EXCL_LINE - - size_t skippedStates = 0; // LCOV_EXCL_LINE - // LCOV_EXCL_STOP - for (const auto& [key, expr] : model.nextStateExprByStateKey) { // LCOV_EXCL_LINE - // LCOV_EXCL_START - if (model.connectivitySkipInfoByKey.find(key) != // LCOV_EXCL_LINE - model.connectivitySkipInfoByKey.end()) { // LCOV_EXCL_LINE - continue; // LCOV_EXCL_LINE - } - if (const auto varID = // LCOV_EXCL_LINE - findFirstUnpublishedSupportVar(expr, isPublishedVar, scratch)) { // LCOV_EXCL_LINE - model.connectivitySkipInfoByKey.emplace(key, makeUnpublishedSupportSkip(*varID)); // LCOV_EXCL_LINE - // LCOV_EXCL_STOP - ++skippedStates; // LCOV_EXCL_LINE - // LCOV_EXCL_START - if (ctx.secDiagEnabled) { // LCOV_EXCL_LINE - fprintf( // LCOV_EXCL_LINE - stderr, // LCOV_EXCL_LINE - "SEC diag: extract(%s) skipping state `%s`: unpublished support v%zu\n", - ctx.topName.c_str(), // LCOV_EXCL_LINE - // LCOV_EXCL_STOP - displayNameForSignalKey(model, key).c_str(), // LCOV_EXCL_LINE - // LCOV_EXCL_START - *varID); // LCOV_EXCL_LINE - } // LCOV_EXCL_LINE - } // LCOV_EXCL_LINE - // LCOV_EXCL_STOP - } - -// LCOV_EXCL_START - - size_t skippedOutputs = 0; // LCOV_EXCL_LINE - for (const auto& [key, expr] : model.observedOutputExprByKey) { // LCOV_EXCL_LINE - if (model.connectivitySkipInfoByKey.find(key) != // LCOV_EXCL_LINE - model.connectivitySkipInfoByKey.end()) { // LCOV_EXCL_LINE - continue; // LCOV_EXCL_LINE - // LCOV_EXCL_STOP - } - if (const auto varID = // LCOV_EXCL_LINE - findFirstUnpublishedSupportVar(expr, isPublishedVar, scratch)) { // LCOV_EXCL_LINE - model.connectivitySkipInfoByKey.emplace(key, makeUnpublishedSupportSkip(*varID)); // LCOV_EXCL_LINE - ++skippedOutputs; // LCOV_EXCL_LINE - if (ctx.secDiagEnabled) { // LCOV_EXCL_LINE - fprintf( // LCOV_EXCL_LINE - stderr, // LCOV_EXCL_LINE - "SEC diag: extract(%s) skipping output `%s`: unpublished support v%zu\n", - ctx.topName.c_str(), // LCOV_EXCL_LINE - displayNameForSignalKey(model, key).c_str(), // LCOV_EXCL_LINE - *varID); // LCOV_EXCL_LINE - } // LCOV_EXCL_LINE - } // LCOV_EXCL_LINE - } - if (ctx.secDiagEnabled && (skippedStates != 0 || skippedOutputs != 0)) { // LCOV_EXCL_LINE - fprintf( // LCOV_EXCL_LINE - stderr, // LCOV_EXCL_LINE - "SEC diag: extract(%s) skipped unpublished-support formulas states=%zu outputs=%zu\n", - ctx.topName.c_str(), // LCOV_EXCL_LINE - skippedStates, // LCOV_EXCL_LINE - skippedOutputs); // LCOV_EXCL_LINE - fflush(stderr); // LCOV_EXCL_LINE - } // LCOV_EXCL_LINE -} // LCOV_EXCL_LINE - void partitionCoveredSignals(SequentialDesignModel& model) { std::vector legalStateBits; legalStateBits.reserve(model.stateBits.size()); @@ -5695,13 +5245,10 @@ void logExtractedModelDebugSummary(const ExtractContext& ctx, } fprintf( stderr, - "SEC diag: extract(%s) structured_memories=%zu structured_memory_cells=%zu abstracted_seq_boundaries=%zu opaque_inputs=%zu opaque_outputs=%zu\n", + "SEC diag: extract(%s) structured_memories=%zu structured_memory_cells=%zu\n", ctx.topName.c_str(), ctx.pendingMemoryInstances.size(), - structuredMemoryCellCount, - model.abstractedSequentialBoundaries.size(), - model.internalBoundaryInputKeys.size(), - model.internalBoundaryOutputKeys.size()); + structuredMemoryCellCount); auto formatSignal = [&](const SignalKey& key) { const auto nameIt = model.displayNameByKey.find(key); @@ -5775,10 +5322,8 @@ SequentialDesignModel SequentialDesignModel::extract(naja::NL::SNLDesign* top) { .universe = universe, // LCOV_EXCL_STOP .previousTop = universe->getTopDesign(), - .topName = top->getName().getString(), + .topName = top->getName().getString(), // LCOV_EXCL_LINE .secDiagEnabled = std::getenv("KEPLER_SEC_DIAG") != nullptr, - .abstractUncomputableSequentialBoundaries = - KEPLER_FORMAL::Config::getSecTreatUncomputableSeqAsBoundary(), }; ctx.builder.setRetainDnl(true); @@ -5811,10 +5356,12 @@ SequentialDesignModel SequentialDesignModel::extract(naja::NL::SNLDesign* top) { return model; } - // Phase 2: build the initial boundary formulas for real top outputs plus any - // already abstracted boundary terms, then publish the normalized SEC - // interface and variable map. - buildInitialObservedOutputClouds(ctx, model); + retainModelableClockGateLatchOutputs(ctx, model); + skipTopOutputsReachedByOpaqueTerminals(ctx, model); + + // Phase 2: build top-output formulas, stopping at the temporary internal + // frontiers identified during collection, then publish the SEC interface. + buildInitialObservedOutputClouds(ctx); publishNormalizedBoundary(ctx, model); std::vector builderInputs = ctx.builder.getInputs(); @@ -5864,16 +5411,11 @@ SequentialDesignModel SequentialDesignModel::extract(naja::NL::SNLDesign* top) { // Phase 3: materialize the observed output formulas that SEC will actually // compare, classifying anything missing as either a skippable connectivity // gap or a hard unsupported boundary. - materializeBoundaryObservedOutputs( - ctx.abstractedBoundaryObservedTerms, + materializeTopObservedOutputs( + ctx.topOutputKeyByTerm, outputExprByTerm, skippedOutputsByTerm, - builderInputs, - builderOutputs, - termDNLID2varID, model); - materializeTopObservedOutputs( - ctx.topOutputKeyByTerm, outputExprByTerm, skippedOutputsByTerm, model); if (ctx.secDiagEnabled) { fprintf( stderr, @@ -5895,9 +5437,8 @@ SequentialDesignModel SequentialDesignModel::extract(naja::NL::SNLDesign* top) { skippedOutputsByTerm); } - // Phase 4: rebuild the next-state relations for just the state that is still - // relevant to covered outputs, then fold any late boundary abstractions back - // into the published interface. + // Phase 4: rebuild the next-state relations for state that can influence a + // requested top output. const auto rebuiltArtifacts = rebuildRequiredStateTransitions( ctx, model, @@ -5908,28 +5449,14 @@ SequentialDesignModel SequentialDesignModel::extract(naja::NL::SNLDesign* top) { termDNLID2varID, outputExprByTerm, skippedOutputsByTerm); - applyRebuiltTransitionArtifacts( - ctx, - rebuiltArtifacts, - model, - builderInputs, - builderOutputs, - termDNLID2varID, - outputExprByTerm, - skippedOutputsByTerm); + applyRebuiltTransitionArtifacts(rebuiltArtifacts, model); filterUnsupportedAndUnmappedBoundary(ctx, model); composeSameDomainPhaseTransitions(model); markMultiClockDomainConesAsSkipped(model); - // Phase 5: propagate connectivity skips through dependent state/output cones, - // then partition the final interface into covered vs skipped signals. The - // proof remapper treats any remaining unpublished internal support as a - // design-private free input, so normal SEC extraction should not skip an - // otherwise covered top output just because a memory/opaque internal leaf was - // not part of the public state/environment interface. - if (std::getenv("KEPLER_SEC_STRICT_UNPUBLISHED_SUPPORT") != nullptr) { - markFormulasWithUnpublishedSupportAsSkipped(ctx, model); // LCOV_EXCL_LINE - } // LCOV_EXCL_LINE + // Phase 5: propagate non-opaque connectivity skips, then retain only the + // outputs whose cones were fully modeled. Opaque cones stopped during their + // per-output state traversal above. propagateConnectivitySkipsThroughDependencies(model); partitionCoveredSignals(model); diff --git a/src/sec/model/SequentialDesignModel.h b/src/sec/model/SequentialDesignModel.h index d47f4f67..d78af46b 100644 --- a/src/sec/model/SequentialDesignModel.h +++ b/src/sec/model/SequentialDesignModel.h @@ -27,6 +27,7 @@ enum class ConnectivitySkipOrigin { MultiDriver, LogicalLoop, MultiClockDomain, + OpaqueInternal, }; struct ConnectivitySkipInfo { // LCOV_EXCL_LINE @@ -34,12 +35,6 @@ struct ConnectivitySkipInfo { // LCOV_EXCL_LINE std::string detail; }; -struct AbstractedSequentialBoundaryDetail { // LCOV_EXCL_LINE - std::string instancePath; - std::vector stateKeys; - std::vector observedKeys; -}; - // Normalized view of a sequential design after extracting the interface we // need for SEC: environment inputs, current-state bits, observed outputs, and // the Boolean formulas that describe outputs and next-state updates. @@ -48,10 +43,6 @@ struct SequentialDesignModel { // LCOV_EXCL_LINE std::vector stateBits; std::vector topInputKeys; std::vector topOutputKeys; - // Opaque internal cut points introduced by the clause builder for leaves - // that are neither modeled sequentially nor reconstructed combinationally. - std::vector internalBoundaryInputKeys; - std::vector internalBoundaryOutputKeys; std::vector allObservedOutputs; std::vector observedOutputs; std::vector skippedStateBits; @@ -70,13 +61,10 @@ struct SequentialDesignModel { // LCOV_EXCL_LINE std::unordered_map connectivitySkipInfoByKey; std::vector complementedStateRelations; - std::vector abstractedSequentialBoundaries; - std::vector - abstractedSequentialBoundaryDetails; std::vector unsupportedReasons; - // Extract the model from the given top design. Unsupported sequential - // structures are recorded in unsupportedReasons instead of being guessed. + // Extract the model from the given top design. Opaque per-output cones are + // skipped; globally unsupported structures are recorded in unsupportedReasons. static SequentialDesignModel extract(naja::NL::SNLDesign* top); bool hasUnsupportedFeatures() const { diff --git a/src/sec/proof/TransitionExprResolver.cpp b/src/sec/proof/TransitionExprResolver.cpp index 5e906060..39deb856 100644 --- a/src/sec/proof/TransitionExprResolver.cpp +++ b/src/sec/proof/TransitionExprResolver.cpp @@ -11,7 +11,6 @@ #include #include "common/BoolExprUtils.h" -#include "common/PrivateProofSymbol.h" namespace KEPLER_FORMAL::SEC { @@ -212,11 +211,10 @@ size_t mapLazyTransitionSymbol( mappedIt != symbolMap.end()) { return mappedIt->second; } - - const size_t privateSymbol = - makePrivateProofLeafSymbol(designIndex, localSymbol); - symbolMap.emplace(localSymbol, privateSymbol); - return privateSymbol; + throw std::runtime_error( + "SEC lazy transition for design" + std::to_string(designIndex) + + " contains unpublished internal support v" + + std::to_string(localSymbol)); } std::set remappedSupport( @@ -370,9 +368,8 @@ BoolExpr* TransitionExprResolver::at(size_t stateSymbol) const { return remapped; } - // Populate design-private mappings for transition-only local support before - // materializing the lazy BoolExpr. This keeps unmodeled internal leaves - // design-local without forcing a full transition remap during COI discovery. + // Validate the lazy transition support before materializing the BoolExpr. + // Extraction must have skipped every cone that reaches an internal frontier. (void)remappedSupport( source.localExpr, source.designIndex, diff --git a/src/sec/strategy/SequentialEquivalenceStrategy.cpp b/src/sec/strategy/SequentialEquivalenceStrategy.cpp index 76c3c2d2..35d850cf 100644 --- a/src/sec/strategy/SequentialEquivalenceStrategy.cpp +++ b/src/sec/strategy/SequentialEquivalenceStrategy.cpp @@ -336,11 +336,16 @@ std::string describeConnectivitySkipOrigin(ConnectivitySkipOrigin origin) { return "logical-loop"; case ConnectivitySkipOrigin::MultiClockDomain: return "multi-clock-domain"; + case ConnectivitySkipOrigin::OpaqueInternal: + return "opaque-internal"; } return "connectivity"; // LCOV_EXCL_LINE } std::string describeConnectivitySkipInfo(const ConnectivitySkipInfo& info) { + if (info.origin == ConnectivitySkipOrigin::OpaqueInternal) { + return describeConnectivitySkipOrigin(info.origin) + ": " + info.detail; + } std::ostringstream oss; oss << describeConnectivitySkipOrigin(info.origin) << " connectivity: " << info.detail; @@ -367,6 +372,7 @@ struct OutputCoverageSelection { std::vector skippedOutputs; std::vector resetUnanchoredSkippedOutputs; std::vector multiClockDomainSkippedOutputs; + std::vector opaqueCellSkippedOutputs; size_t totalOutputs = 0; }; @@ -400,29 +406,14 @@ struct DualRailStateSymbolMaps { std::unordered_map localState1BySymbol; }; -size_t privateSupportSymbol( - size_t designIndex, - size_t localSymbol, - std::unordered_map& symbolMap, - KInductionProblem& problem, - size_t& nextSymbol); - // LCOV_EXCL_START class SecDualRailVariableMapper final : public DualRailVariableMapper { public: SecDualRailVariableMapper( - size_t designIndex, const std::unordered_map& stateRails, // LCOV_EXCL_STOP - std::unordered_map& binarySymbolMap, - KInductionProblem& problem, - size_t& nextSymbol) - : designIndex_(designIndex), - stateRails_(stateRails), - binarySymbolMap_(binarySymbolMap), - problem_(problem), - // LCOV_EXCL_START - nextSymbol_(nextSymbol) {} + const std::unordered_map& binarySymbolMap) + : stateRails_(stateRails), binarySymbolMap_(binarySymbolMap) {} // LCOV_EXCL_STOP @@ -444,27 +435,23 @@ class SecDualRailVariableMapper final : public DualRailVariableMapper { BoolExpr::createFalse(), BoolExpr::createTrue()}; // LCOV_EXCL_LINE } - size_t binarySymbol = 0; - // LCOV_EXCL_START if (const auto mappedIt = binarySymbolMap_.find(symbol); - // LCOV_EXCL_STOP mappedIt != binarySymbolMap_.end()) { - binarySymbol = mappedIt->second; - } else { - binarySymbol = privateSupportSymbol( // LCOV_EXCL_LINE - designIndex_, symbol, binarySymbolMap_, problem_, nextSymbol_); // LCOV_EXCL_LINE + return DualRailBoolExpr{ + BoolExpr::Var(mappedIt->second), + BoolExpr::Not(BoolExpr::Var(mappedIt->second))}; } - return DualRailBoolExpr{ - BoolExpr::Var(binarySymbol), - BoolExpr::Not(BoolExpr::Var(binarySymbol))}; + // Extraction rejects unpublished support before dual-rail conversion. + // LCOV_EXCL_START + throw std::runtime_error( + "SEC formula contains unpublished internal support v" + + std::to_string(symbol)); + // LCOV_EXCL_STOP } private: - size_t designIndex_ = 0; const std::unordered_map& stateRails_; - std::unordered_map& binarySymbolMap_; - KInductionProblem& problem_; - size_t& nextSymbol_; + const std::unordered_map& binarySymbolMap_; }; struct ScopedDnlContext { @@ -923,8 +910,8 @@ OutputCoverageSelection selectCoveredObservedOutputs( const AlignedSignals& allObservedOutputs, const SequentialDesignModel& model0, const SequentialDesignModel& model1) { - // Connectivity skips are the only SEC skips we allow here. Unsupported - // primitive semantics should already have stopped extraction earlier. + // Extraction records both connectivity failures and opaque internal + // frontiers as per-output skips before interface alignment. OutputCoverageSelection selection; selection.totalOutputs = allObservedOutputs.names.size(); selection.checkedOutputs.names.reserve(allObservedOutputs.names.size()); @@ -959,6 +946,14 @@ OutputCoverageSelection selectCoveredObservedOutputs( if (skippedByMultiClockDomain) { selection.multiClockDomainSkippedOutputs.push_back(skippedOutput); } + const bool skippedByOpaqueCell = + (skip0 != model0.connectivitySkipInfoByKey.end() && + skip0->second.origin == ConnectivitySkipOrigin::OpaqueInternal) || + (skip1 != model1.connectivitySkipInfoByKey.end() && + skip1->second.origin == ConnectivitySkipOrigin::OpaqueInternal); + if (skippedByOpaqueCell) { + selection.opaqueCellSkippedOutputs.push_back(skippedOutput); + } continue; } @@ -986,7 +981,6 @@ SequentialEquivalenceResult makeSecResult( std::string reason, // LCOV_EXCL_STOP const OutputCoverageSelection& coverage, - std::vector abstractedSequentialBoundaries = {}, std::vector extractedBoundaryReports = {}) { SequentialEquivalenceResult result; result.status = status; @@ -1008,8 +1002,7 @@ SequentialEquivalenceResult makeSecResult( // LCOV_EXCL_STOP result.multiClockDomainSkippedOutputs = coverage.multiClockDomainSkippedOutputs; - result.abstractedSequentialBoundaries = - std::move(abstractedSequentialBoundaries); + result.opaqueCellSkippedOutputs = coverage.opaqueCellSkippedOutputs; result.extractedBoundaryReports = std::move(extractedBoundaryReports); return result; } @@ -1561,7 +1554,6 @@ bool findAndRecordDualRailResidualCounterexample( naja::NL::SNLDesign* top0, naja::NL::SNLDesign* top1, const OutputCoverageSelection& outputCoverage, - const std::vector& abstractedSequentialBoundaries, const std::vector& extractedBoundaryReports, DualRailResidualEngine engine, DualRailResidualProofState& proofState); @@ -1678,7 +1670,6 @@ void recordDualRailResidualCounterexample( naja::NL::SNLDesign* top0, naja::NL::SNLDesign* top1, const OutputCoverageSelection& outputCoverage, - const std::vector& abstractedSequentialBoundaries, const std::vector& extractedBoundaryReports, DualRailResidualProofState& proofState) { KInductionResult witnessResult{ @@ -1688,7 +1679,6 @@ void recordDualRailResidualCounterexample( witnessResult.bound, formatCounterexampleWitness(witnessResult, model0, model1, top0, top1), outputCoverage, - abstractedSequentialBoundaries, extractedBoundaryReports); } @@ -1702,7 +1692,6 @@ void proveDualRailResidualOutputSet( naja::NL::SNLDesign* top0, naja::NL::SNLDesign* top1, const OutputCoverageSelection& outputCoverage, - const std::vector& abstractedSequentialBoundaries, const std::vector& extractedBoundaryReports, DualRailResidualEngine engine, bool runCounterexampleSweep, @@ -1725,7 +1714,6 @@ void proveDualRailResidualOutputSet( top0, // LCOV_EXCL_LINE top1, // LCOV_EXCL_LINE outputCoverage, // LCOV_EXCL_LINE - abstractedSequentialBoundaries, // LCOV_EXCL_LINE extractedBoundaryReports, // LCOV_EXCL_LINE engine, // LCOV_EXCL_LINE proofState)) { // LCOV_EXCL_LINE @@ -1763,7 +1751,6 @@ void proveDualRailResidualOutputSet( formatCounterexampleWitness( // LCOV_EXCL_LINE witnessResult, model0, model1, top0, top1), // LCOV_EXCL_LINE outputCoverage, // LCOV_EXCL_LINE - abstractedSequentialBoundaries, // LCOV_EXCL_LINE extractedBoundaryReports); // LCOV_EXCL_LINE return; } // LCOV_EXCL_LINE @@ -1791,7 +1778,6 @@ void proveDualRailResidualOutputSet( : "Classic k-induction found a counterexample at k = " + // LCOV_EXCL_LINE std::to_string(result.bound), // LCOV_EXCL_LINE outputCoverage, // LCOV_EXCL_LINE - abstractedSequentialBoundaries, // LCOV_EXCL_LINE extractedBoundaryReports); // LCOV_EXCL_LINE return; // LCOV_EXCL_LINE } @@ -1811,7 +1797,6 @@ void proveDualRailResidualOutputSet( top0, top1, outputCoverage, - abstractedSequentialBoundaries, extractedBoundaryReports, engine, runCounterexampleSweep, @@ -1826,7 +1811,6 @@ void proveDualRailResidualOutputSet( top0, top1, outputCoverage, - abstractedSequentialBoundaries, extractedBoundaryReports, engine, runCounterexampleSweep, @@ -1847,7 +1831,6 @@ bool findAndRecordDualRailResidualCounterexample( naja::NL::SNLDesign* top0, naja::NL::SNLDesign* top1, const OutputCoverageSelection& outputCoverage, - const std::vector& abstractedSequentialBoundaries, const std::vector& extractedBoundaryReports, DualRailResidualEngine engine, DualRailResidualProofState& proofState) { @@ -1870,7 +1853,6 @@ bool findAndRecordDualRailResidualCounterexample( top0, // LCOV_EXCL_LINE top1, // LCOV_EXCL_LINE outputCoverage, // LCOV_EXCL_LINE - abstractedSequentialBoundaries, // LCOV_EXCL_LINE extractedBoundaryReports, // LCOV_EXCL_LINE proofState); // LCOV_EXCL_LINE return true; // LCOV_EXCL_LINE @@ -1887,7 +1869,6 @@ std::optional proveDualRailResidualsWithSelectedEng naja::NL::SNLDesign* top0, naja::NL::SNLDesign* top1, const OutputCoverageSelection& outputCoverage, - const std::vector& abstractedSequentialBoundaries, const std::vector& extractedBoundaryReports, DualRailResidualEngine engine) { if (!problem.usesDualRailStateEncoding || @@ -1920,7 +1901,6 @@ std::optional proveDualRailResidualsWithSelectedEng 0, "Dual-rail output skips left no selected-engine obligation", // LCOV_EXCL_LINE partialCoverage, - abstractedSequentialBoundaries, // LCOV_EXCL_LINE extractedBoundaryReports); // LCOV_EXCL_LINE } // LCOV_EXCL_LINE return std::nullopt; // LCOV_EXCL_LINE @@ -1940,7 +1920,6 @@ std::optional proveDualRailResidualsWithSelectedEng top0, top1, outputCoverage, - abstractedSequentialBoundaries, extractedBoundaryReports, proofState); return proofState.terminalResult; @@ -1965,7 +1944,6 @@ std::optional proveDualRailResidualsWithSelectedEng 0, "Dual-rail residual surface exceeded selected-engine proof limits", // LCOV_EXCL_LINE partialCoverage, - abstractedSequentialBoundaries, // LCOV_EXCL_LINE extractedBoundaryReports); // LCOV_EXCL_LINE } // LCOV_EXCL_LINE @@ -1980,7 +1958,6 @@ std::optional proveDualRailResidualsWithSelectedEng top0, top1, outputCoverage, - abstractedSequentialBoundaries, extractedBoundaryReports, engine, proofState)) { @@ -2009,7 +1986,6 @@ std::optional proveDualRailResidualsWithSelectedEng // LCOV_DISABLED_START outputCoverage, // LCOV_DISABLED_STOP - abstractedSequentialBoundaries, extractedBoundaryReports, engine, /*runCounterexampleSweep=*/false, @@ -2033,7 +2009,6 @@ std::optional proveDualRailResidualsWithSelectedEng dualRailResidualEngineName(engine) + " did not prove any output", finalCoverage, - abstractedSequentialBoundaries, extractedBoundaryReports); } @@ -2046,7 +2021,6 @@ std::optional proveDualRailResidualsWithSelectedEng std::to_string(proofState.coveredOutputs.size()) + " observed outputs; remaining outputs are inconclusive", finalCoverage, - abstractedSequentialBoundaries, extractedBoundaryReports); } @@ -2055,7 +2029,6 @@ std::optional proveDualRailResidualsWithSelectedEng proofState.provedBound, "", finalCoverage, - abstractedSequentialBoundaries, extractedBoundaryReports); } @@ -2074,19 +2047,6 @@ OutputBatchingLimits dualRailPdrOutputBatchingLimits( return defaultLimits; } -void appendAbstractedSequentialBoundaries( - const SequentialDesignModel& model, - const char* designPrefix, - std::vector& abstractedSequentialBoundaries) { - abstractedSequentialBoundaries.reserve( - abstractedSequentialBoundaries.size() + - model.abstractedSequentialBoundaries.size()); - for (const auto& description : model.abstractedSequentialBoundaries) { - abstractedSequentialBoundaries.push_back( - std::string(designPrefix) + " " + description); - } -} - void appendExtractedBoundaryReports( const SequentialDesignModel& model, const char* designPrefix, @@ -2110,32 +2070,13 @@ void appendExtractedBoundaryReports( appendUniqueRole(entry.roles, role); }; - // Boundary terms are the full exposed SEC cut surface: - // - the original top interface - // - opaque internal cut points from leaves SEC cannot model combinationally - // and does not recognize as sequential - // - the interface exposed when an uncomputable sequential is abstracted + // Boundary terms describe the original top interface for diagnostics. for (const auto& key : model.topInputKeys) { addRole(key, "top_input"); } for (const auto& key : model.topOutputKeys) { addRole(key, "top_output"); } - for (const auto& key : model.internalBoundaryInputKeys) { - addRole(key, "opaque_internal_input"); - } - for (const auto& key : model.internalBoundaryOutputKeys) { - addRole(key, "opaque_internal_output"); - } - for (const auto& detail : model.abstractedSequentialBoundaryDetails) { - for (const auto& key : detail.stateKeys) { - addRole(key, "abstracted_sequential_state"); - } - for (const auto& key : detail.observedKeys) { - addRole(key, "abstracted_sequential_observed"); - } - } - reports.reserve(reports.size() + reportsBySignal.size()); for (auto& [_, entry] : reportsBySignal) { reports.push_back(std::move(entry)); @@ -2269,34 +2210,10 @@ size_t nextUnusedProofSymbol(const KInductionProblem& problem) { return nextSymbol; } -size_t privateSupportSymbol( - size_t designIndex, - size_t localSymbol, - std::unordered_map& symbolMap, - KInductionProblem& problem, - size_t& nextSymbol) { - const auto existingIt = symbolMap.find(localSymbol); - if (existingIt != symbolMap.end()) { - return existingIt->second; - } - - const size_t privateSymbol = nextSymbol++; - symbolMap.emplace(localSymbol, privateSymbol); - problem.allSymbols.push_back(privateSymbol); - problem.inputSymbols.push_back(privateSymbol); - problem.environmentInputNames.push_back( - "$private_d" + std::to_string(designIndex) + "_v" + - std::to_string(localSymbol)); - return privateSymbol; -} - -BoolExpr* remapSecFormulaWithPrivateSupport( +BoolExpr* remapSecFormulaStrict( BoolExpr* root, - size_t designIndex, - std::unordered_map& symbolMap, - std::unordered_map& memo, - KInductionProblem& problem, - size_t& nextSymbol) { + const std::unordered_map& symbolMap, + std::unordered_map& memo) { if (root == nullptr) { return nullptr; // LCOV_EXCL_LINE } @@ -2309,17 +2226,9 @@ BoolExpr* remapSecFormulaWithPrivateSupport( bool visited = false; }; - // LCOV_DISABLED_START - // Internal support left after compact extraction is a design-local free - // input, not a name-aligned SEC assumption. Allocate private proof symbols - // LCOV_DISABLED_STOP - // on demand while preserving the same iterative DAG remap used by - // LCOV_DISABLED_START - // BoolExprUtils for large gate-level cones. std::vector stack; stack.push_back({root, false}); while (!stack.empty()) { - // LCOV_DISABLED_STOP const StackFrame current = stack.back(); stack.pop_back(); BoolExpr* node = current.expr; @@ -2329,9 +2238,13 @@ BoolExpr* remapSecFormulaWithPrivateSupport( if (node->getOp() == Op::VAR) { const size_t id = node->getId(); - const size_t mapped = - id < 2 ? id : privateSupportSymbol( - designIndex, id, symbolMap, problem, nextSymbol); + const auto mappedIt = symbolMap.find(id); + if (id >= 2 && mappedIt == symbolMap.end()) { + throw std::runtime_error( + "SEC formula contains unpublished internal support v" + + std::to_string(id)); + } + const size_t mapped = id < 2 ? id : mappedIt->second; memo.emplace(node, BoolExpr::Var(mapped)); continue; } @@ -2385,25 +2298,17 @@ RemappedSecExpressions remapSecExpressions( RemappedSecExpressions remapped; std::unordered_map remapMemo0; std::unordered_map remapMemo1; - size_t nextPrivateSymbol = nextUnusedProofSymbol(problem); - for (size_t i = 0; i < alignedOutputs.names.size(); ++i) { const auto& key0 = alignedOutputs.keys0[i]; const auto& key1 = alignedOutputs.keys1[i]; - const auto remappedOutput0 = remapSecFormulaWithPrivateSupport( + const auto remappedOutput0 = remapSecFormulaStrict( model0.observedOutputExprByKey.at(key0), - /*designIndex=*/0, symbolSpace.localToCombined0, - remapMemo0, - problem, - nextPrivateSymbol); - const auto remappedOutput1 = remapSecFormulaWithPrivateSupport( + remapMemo0); + const auto remappedOutput1 = remapSecFormulaStrict( model1.observedOutputExprByKey.at(key1), - /*designIndex=*/1, symbolSpace.localToCombined1, - remapMemo1, - problem, - nextPrivateSymbol); + remapMemo1); problem.observedOutputExprs0.push_back(remappedOutput0); problem.observedOutputExprs1.push_back(remappedOutput1); } @@ -2413,24 +2318,18 @@ RemappedSecExpressions remapSecExpressions( for (const auto& key : model0.stateBits) { remapped.next0.emplace( key, - remapSecFormulaWithPrivateSupport( + remapSecFormulaStrict( model0.nextStateExprByStateKey.at(key), - /*designIndex=*/0, symbolSpace.localToCombined0, - remapMemo0, - problem, - nextPrivateSymbol)); + remapMemo0)); } for (const auto& key : model1.stateBits) { remapped.next1.emplace( key, - remapSecFormulaWithPrivateSupport( + remapSecFormulaStrict( model1.nextStateExprByStateKey.at(key), - /*designIndex=*/1, symbolSpace.localToCombined1, - remapMemo1, - problem, - nextPrivateSymbol)); + remapMemo1)); } logSecDiagLine(secDiagEnabled, "SEC diag: remapped next-state formulas"); } else { @@ -2878,20 +2777,11 @@ KInductionProblem buildDualRailSecProblem( // LCOV_DISABLED_START SecDualRailVariableMapper mapper0( - 0, railMaps.localState0BySymbol, - symbolSpace.localToCombined0, - // LCOV_DISABLED_STOP - problem, - // LCOV_DISABLED_START - nextSymbol); - // LCOV_DISABLED_STOP + symbolSpace.localToCombined0); SecDualRailVariableMapper mapper1( - 1, railMaps.localState1BySymbol, - symbolSpace.localToCombined1, - problem, - nextSymbol); + symbolSpace.localToCombined1); std::unordered_map memo0; std::unordered_map memo1; @@ -3070,7 +2960,6 @@ SequentialEquivalenceResult runPdrSecEngine( // LCOV_DISABLED_STOP const OutputCoverageSelection& outputCoverage, // LCOV_DISABLED_START - const std::vector& abstractedSequentialBoundaries, const std::vector& extractedBoundaryReports) { // LCOV_DISABLED_STOP const std::vector dualRailEngineOutputIndices = @@ -3100,7 +2989,6 @@ SequentialEquivalenceResult runPdrSecEngine( 0, "Dual-rail PDR has no selected-engine output obligation to prove", // LCOV_EXCL_LINE partialCoverage, - abstractedSequentialBoundaries, // LCOV_EXCL_LINE extractedBoundaryReports); // LCOV_EXCL_LINE } // LCOV_EXCL_LINE @@ -3208,15 +3096,26 @@ SequentialEquivalenceResult runPdrSecEngine( firstOutput, endOutput); break; - case PDRStatus::Different: + case PDRStatus::Different: { + auto witness = SEC::findBaseCounterexampleAtFrontier( + exactBatchProblem, solverType, pdrResult.bound); + KInductionResult witnessResult{ + KInductionStatus::Different, + pdrResult.bound, + std::move(witness)}; + const std::string reason = + witnessResult.witness.has_value() + ? formatCounterexampleWitness( + witnessResult, model0, model1, top0, top1) + : "Exact PDR found a defined-value counterexample at k = " + // LCOV_EXCL_LINE + std::to_string(pdrResult.bound); // LCOV_EXCL_LINE return makeSecResult( SequentialEquivalenceStatus::Different, pdrResult.bound, - "Exact PDR found a defined-value counterexample at k = " + - std::to_string(pdrResult.bound), + reason, outputCoverage, - abstractedSequentialBoundaries, extractedBoundaryReports); + } case PDRStatus::Inconclusive: default: provedBound = std::max(provedBound, pdrResult.bound); @@ -3271,7 +3170,6 @@ SequentialEquivalenceResult runPdrSecEngine( ? "Exact dual-rail PDR did not prove any observed output" : "Exact PDR did not prove any observed output", noProofCoverage, - abstractedSequentialBoundaries, extractedBoundaryReports); } if (coveredOutputCount != pdrCoveredOutputs.size()) { @@ -3285,7 +3183,6 @@ SequentialEquivalenceResult runPdrSecEngine( std::to_string(pdrCoveredOutputs.size()) + " observed outputs; remaining outputs are inconclusive", finalCoverage, - abstractedSequentialBoundaries, extractedBoundaryReports); } return makeSecResult( @@ -3293,7 +3190,6 @@ SequentialEquivalenceResult runPdrSecEngine( provedBound, "", finalCoverage, - abstractedSequentialBoundaries, extractedBoundaryReports); } @@ -3308,7 +3204,6 @@ SequentialEquivalenceResult runKInductionSecEngine( naja::NL::SNLDesign* top0, naja::NL::SNLDesign* top1, const OutputCoverageSelection& outputCoverage, - const std::vector& abstractedSequentialBoundaries, const std::vector& extractedBoundaryReports) { if (auto dualRailResult = proveDualRailResidualsWithSelectedEngine( problem, @@ -3319,7 +3214,6 @@ SequentialEquivalenceResult runKInductionSecEngine( top0, top1, outputCoverage, - abstractedSequentialBoundaries, extractedBoundaryReports, DualRailResidualEngine::KInduction); dualRailResult.has_value()) { @@ -3335,7 +3229,6 @@ SequentialEquivalenceResult runKInductionSecEngine( result.bound, "", outputCoverage, - abstractedSequentialBoundaries, extractedBoundaryReports); case KInductionStatus::Different: return makeSecResult( @@ -3346,7 +3239,6 @@ SequentialEquivalenceResult runKInductionSecEngine( : "Classic k-induction found a counterexample at k = " + // LCOV_EXCL_LINE std::to_string(result.bound), // LCOV_EXCL_LINE outputCoverage, - abstractedSequentialBoundaries, extractedBoundaryReports); case KInductionStatus::Inconclusive: // LCOV_EXCL_LINE default: @@ -3357,7 +3249,6 @@ SequentialEquivalenceResult runKInductionSecEngine( result.bound, // LCOV_EXCL_LINE "Reached max_k without a proof or counterexample", // LCOV_EXCL_LINE outputCoverage, // LCOV_EXCL_LINE - abstractedSequentialBoundaries, // LCOV_EXCL_LINE extractedBoundaryReports); // LCOV_EXCL_LINE } } @@ -3366,7 +3257,6 @@ SequentialEquivalenceResult finishDualRailImcProof( const KInductionProblem& problem, const IMCResult& guardedResult, const OutputCoverageSelection& outputCoverage, - const std::vector& abstractedSequentialBoundaries, const std::vector& extractedBoundaryReports) { DualRailResidualProofState proofState; proofState.coveredOutputs.assign( @@ -3433,7 +3323,6 @@ SequentialEquivalenceResult finishDualRailImcProof( proofState.provedBound, std::move(reason), finalCoverage, - abstractedSequentialBoundaries, extractedBoundaryReports); setExactSecEngineProofProgress( secResult, problem, "IMC", proofState.coveredOutputs); @@ -3449,7 +3338,6 @@ SequentialEquivalenceResult runImcSecEngine( naja::NL::SNLDesign* top0, naja::NL::SNLDesign* top1, const OutputCoverageSelection& outputCoverage, - const std::vector& abstractedSequentialBoundaries, const std::vector& extractedBoundaryReports) { // IMC must remain an interpolation-based engine. Do not route dual-rail // residuals through the KI residual helper before the IMC engine runs. @@ -3461,7 +3349,6 @@ SequentialEquivalenceResult runImcSecEngine( problem, result, outputCoverage, - abstractedSequentialBoundaries, extractedBoundaryReports); } switch (result.status) { @@ -3473,7 +3360,6 @@ SequentialEquivalenceResult runImcSecEngine( result.bound, "", outputCoverage, - abstractedSequentialBoundaries, extractedBoundaryReports); setSecEngineProofProgress( secResult, problem, "IMC", problem.observedOutputExprs0.size()); @@ -3490,7 +3376,6 @@ SequentialEquivalenceResult runImcSecEngine( : "IMC found a counterexample at k = " + // LCOV_EXCL_LINE std::to_string(result.bound), // LCOV_EXCL_LINE outputCoverage, // LCOV_EXCL_LINE - abstractedSequentialBoundaries, // LCOV_EXCL_LINE extractedBoundaryReports); // LCOV_EXCL_LINE } // LCOV_EXCL_LINE case IMCStatus::Inconclusive: // LCOV_EXCL_LINE @@ -3512,7 +3397,6 @@ SequentialEquivalenceResult runImcSecEngine( result.bound, // LCOV_EXCL_LINE "Reached max_k without a proof or counterexample", // LCOV_EXCL_LINE outputCoverage, // LCOV_EXCL_LINE - abstractedSequentialBoundaries, // LCOV_EXCL_LINE extractedBoundaryReports); // LCOV_EXCL_LINE if (result.firstUnprovenOutput.has_value()) { // LCOV_EXCL_LINE setSecEngineProofProgress( // LCOV_EXCL_LINE @@ -3533,7 +3417,6 @@ SequentialEquivalenceResult runSelectedSecEngine( naja::NL::SNLDesign* top0, naja::NL::SNLDesign* top1, const OutputCoverageSelection& outputCoverage, - const std::vector& abstractedSequentialBoundaries, const std::vector& extractedBoundaryReports) { switch (secEngine) { case SecEngine::Pdr: @@ -3546,7 +3429,6 @@ SequentialEquivalenceResult runSelectedSecEngine( top0, top1, outputCoverage, - abstractedSequentialBoundaries, extractedBoundaryReports); case SecEngine::KInduction: return runKInductionSecEngine( @@ -3558,7 +3440,6 @@ SequentialEquivalenceResult runSelectedSecEngine( top0, top1, outputCoverage, - abstractedSequentialBoundaries, extractedBoundaryReports); case SecEngine::Imc: return runImcSecEngine( @@ -3570,7 +3451,6 @@ SequentialEquivalenceResult runSelectedSecEngine( top0, top1, outputCoverage, - abstractedSequentialBoundaries, extractedBoundaryReports); default: // Defensive fallback for corrupted enum values; public parsing rejects @@ -3585,7 +3465,6 @@ SequentialEquivalenceResult runSelectedSecEngine( top0, top1, outputCoverage, - abstractedSequentialBoundaries, extractedBoundaryReports); // LCOV_EXCL_STOP } @@ -3700,17 +3579,13 @@ SequentialEquivalenceResult SequentialEquivalenceStrategy::run(size_t maxK) cons const auto model0 = extractSecDesign(top0_, "SEC diag: extracted design0", secDiagEnabled); if (model0.hasUnsupportedFeatures()) { - std::vector abstractedSequentialBoundaries; std::vector extractedBoundaryReports; - appendAbstractedSequentialBoundaries( - model0, "design0", abstractedSequentialBoundaries); appendExtractedBoundaryReports(model0, "design0", extractedBoundaryReports); return makeSecResult( SequentialEquivalenceStatus::Unsupported, 0, joinReasons(model0.unsupportedReasons), OutputCoverageSelection{}, - abstractedSequentialBoundaries, extractedBoundaryReports); } @@ -3729,10 +3604,7 @@ SequentialEquivalenceResult SequentialEquivalenceStrategy::runExtractedModels( // Compact SEC can release the elaborated Naja DBs after extraction and run // entirely from these value-type models. Rebuilding the boundary summaries // here keeps normal and compact flows reporting the same coverage details. - std::vector abstractedSequentialBoundaries; std::vector extractedBoundaryReports; - appendAbstractedSequentialBoundaries( - model0, "design0", abstractedSequentialBoundaries); appendExtractedBoundaryReports(model0, "design0", extractedBoundaryReports); if (model0.hasUnsupportedFeatures()) { return makeSecResult( @@ -3740,11 +3612,8 @@ SequentialEquivalenceResult SequentialEquivalenceStrategy::runExtractedModels( 0, joinReasons(model0.unsupportedReasons), OutputCoverageSelection{}, - abstractedSequentialBoundaries, extractedBoundaryReports); } - appendAbstractedSequentialBoundaries( - model1, "design1", abstractedSequentialBoundaries); appendExtractedBoundaryReports(model1, "design1", extractedBoundaryReports); if (model1.hasUnsupportedFeatures()) { return makeSecResult( @@ -3752,7 +3621,6 @@ SequentialEquivalenceResult SequentialEquivalenceStrategy::runExtractedModels( 0, joinReasons(model1.unsupportedReasons), OutputCoverageSelection{}, - abstractedSequentialBoundaries, extractedBoundaryReports); } @@ -3768,10 +3636,9 @@ SequentialEquivalenceResult SequentialEquivalenceStrategy::runExtractedModels( return makeSecResult( SequentialEquivalenceStatus::Unsupported, 0, - "No aligned observed outputs remain after skipping cones with no-driver, " - "multi-driver, or logical-loop connectivity.", + "No aligned observed outputs remain after skipping unverifiable cones, " + "including opaque internal cells and connectivity failures.", aligned.outputCoverage, - abstractedSequentialBoundaries, extractedBoundaryReports); } @@ -3820,7 +3687,6 @@ SequentialEquivalenceResult SequentialEquivalenceStrategy::runExtractedModels( "No aligned observed outputs remain after skipping cones that depend " // LCOV_EXCL_LINE "on reset-unanchored internal state.", aligned.outputCoverage, // LCOV_EXCL_LINE - abstractedSequentialBoundaries, // LCOV_EXCL_LINE extractedBoundaryReports); // LCOV_EXCL_LINE } // KI and PDR both work on small COI slices of a potentially huge SEC @@ -3890,7 +3756,6 @@ SequentialEquivalenceResult SequentialEquivalenceStrategy::runExtractedModels( top0_, top1_, aligned.outputCoverage, - abstractedSequentialBoundaries, extractedBoundaryReports); } diff --git a/src/sec/strategy/SequentialEquivalenceStrategy.h b/src/sec/strategy/SequentialEquivalenceStrategy.h index bf8b02cf..ad58f9a5 100644 --- a/src/sec/strategy/SequentialEquivalenceStrategy.h +++ b/src/sec/strategy/SequentialEquivalenceStrategy.h @@ -64,7 +64,7 @@ struct SequentialEquivalenceResult { // LCOV_EXCL_LINE std::vector skippedObservedOutputs; std::vector resetUnanchoredSkippedOutputs; std::vector multiClockDomainSkippedOutputs; - std::vector abstractedSequentialBoundaries; + std::vector opaqueCellSkippedOutputs; std::vector extractedBoundaryReports; double outputCoveragePercent() const { diff --git a/src/strategies/miter/BuildPrimaryOutputClauses.cpp b/src/strategies/miter/BuildPrimaryOutputClauses.cpp index 3ea7ac65..a414349a 100644 --- a/src/strategies/miter/BuildPrimaryOutputClauses.cpp +++ b/src/strategies/miter/BuildPrimaryOutputClauses.cpp @@ -247,8 +247,9 @@ bool containsDependencyBit(const std::vector& deps, uint64_t orderID) BuildPrimaryOutputClauses::SkippedOutputInfo makeSkippedOutputInfo( BuildPrimaryOutputClauses::SkippedOutputReason reason, - std::string detail) { - return {reason, std::move(detail)}; + std::string detail, + DNLID opaqueTerm = DNLID_MAX) { + return {reason, std::move(detail), opaqueTerm}; } void reportSkippedPO(const DNLFull* dnl, @@ -814,7 +815,6 @@ void BuildPrimaryOutputClauses::build() { // LCOV_EXCL_STOP IsPOs_[po] = true; } - std::vector representativeForOutput(outputs_.size()); std::vector representativeOutputs; representativeOutputs.reserve(outputs_.size()); @@ -869,7 +869,11 @@ void BuildPrimaryOutputClauses::build() { return; } - SNLLogicCloud cloud(out, IsPIs_, IsPOs_); + SNLLogicCloud cloud( + out, + IsPIs_, + IsPOs_, + stopAtOpaqueInternalOutputs_); #ifdef DEBUG_CHECKS auto startComp = std::chrono::steady_clock::now(); #endif @@ -972,6 +976,9 @@ void BuildPrimaryOutputClauses::build() { case SNLLogicCloud::SkipReason::LogicalLoop: skipReason = SkippedOutputReason::LogicalLoop; break; + case SNLLogicCloud::SkipReason::OpaqueInternal: + skipReason = SkippedOutputReason::OpaqueInternal; + break; // LCOV_EXCL_START case SNLLogicCloud::SkipReason::None: // LCOV_EXCL_LINE // LCOV_EXCL_STOP @@ -983,7 +990,11 @@ void BuildPrimaryOutputClauses::build() { if (skipReason != SkippedOutputReason::None) { std::lock_guard lock(skippedOutputsMutex_); skippedOutputs_[out] = makeSkippedOutputInfo( - skipReason, cloud.getSkipReasonText()); + skipReason, + cloud.getSkipReasonText(), + skipReason == SkippedOutputReason::OpaqueInternal + ? cloud.getOpaqueInternalTerm() + : DNLID_MAX); } } #ifdef DEBUG_CHECKS diff --git a/src/strategies/miter/BuildPrimaryOutputClauses.h b/src/strategies/miter/BuildPrimaryOutputClauses.h index 04028c19..7b1bfc72 100644 --- a/src/strategies/miter/BuildPrimaryOutputClauses.h +++ b/src/strategies/miter/BuildPrimaryOutputClauses.h @@ -42,11 +42,13 @@ class BuildPrimaryOutputClauses { // LCOV_EXCL_LINE NoDriver, MultiDriver, LogicalLoop, + OpaqueInternal, }; struct SkippedOutputInfo { // LCOV_EXCL_LINE SkippedOutputReason reason = SkippedOutputReason::None; std::string detail; + naja::DNL::DNLID opaqueTerm = naja::DNL::DNLID_MAX; }; BuildPrimaryOutputClauses() = default; @@ -77,6 +79,9 @@ class BuildPrimaryOutputClauses { // LCOV_EXCL_LINE setOutputs2OutputsIDs(); } void setRetainDnl(bool retain) { retainDnl_ = retain; } + void setStopAtOpaqueInternalOutputs(bool stop) { + stopAtOpaqueInternalOutputs_ = stop; + } const std::unordered_map& getInputsMap() const { return inputsMap_; @@ -129,6 +134,7 @@ class BuildPrimaryOutputClauses { // LCOV_EXCL_LINE std::vector termDNLID2varID_; // Only for PIs size_t lastCommonID = 1; std::unordered_map skippedOutputs_; + bool stopAtOpaqueInternalOutputs_ = false; mutable std::mutex skippedOutputsMutex_; struct hash { diff --git a/test/sec/SequentialEquivalenceStrategyTests.cpp b/test/sec/SequentialEquivalenceStrategyTests.cpp index 8416d448..c1a6ede3 100644 --- a/test/sec/SequentialEquivalenceStrategyTests.cpp +++ b/test/sec/SequentialEquivalenceStrategyTests.cpp @@ -41,7 +41,6 @@ #include "SNLScalarTerm.h" #include "common/AlignedSignals.h" #include "common/BoolExprUtils.h" -#include "common/PrivateProofSymbol.h" #include "common/ProofProblemDebug.h" #include "common/SecDiag.h" #include "imc/CraigInterpolatingModelChecker.h" @@ -53,6 +52,7 @@ #include "kinduction/BaseCaseSolver.h" #include "kinduction/SatEncoding.h" #include "kinduction/InductionStepSolver.h" +#include "model/SecNetlistChecks.h" #include "model/SequentialDesignModel.h" #include "proof/TransitionExprResolver.h" #include "BuildPrimaryOutputClauses.h" @@ -1126,6 +1126,7 @@ enum class MalformedSequentialExpression { InvalidRoot, UnmappedTerminal, UnmappedState, + StateWithoutOutput, OutOfRangeOperand, }; @@ -1135,7 +1136,7 @@ class SequentialEquivalenceStrategyTests : public ::testing::Test { MalformedSequentialExpression fault, const char* expectedReason); - void expectMissingSequentialStateOutputHandled(bool boundaryAbstraction); + void expectUnreferencedSequentialStateWithoutOutputIgnored(); void TearDown() override { naja::DNL::destroy(); @@ -1210,22 +1211,6 @@ class ScopedUnsetEnvVar { std::optional previousValue_; }; -class ScopedSecBoundaryAbstraction { - public: - explicit ScopedSecBoundaryAbstraction(bool enabled) - : previousValue_( - KEPLER_FORMAL::Config::getSecTreatUncomputableSeqAsBoundary()) { - KEPLER_FORMAL::Config::setSecTreatUncomputableSeqAsBoundary(enabled); - } - - ~ScopedSecBoundaryAbstraction() { - KEPLER_FORMAL::Config::setSecTreatUncomputableSeqAsBoundary(previousValue_); - } - - private: - bool previousValue_; -}; - // Synthetic tests below do not always build a Naja universe. Production SEC // keys come from NLName::getID() on real terminals; this local allocator only // gives those unit-only artificial keys stable, collision-free identities. @@ -2361,7 +2346,9 @@ SNLDesign* createSinglePortMemoryTop( const std::string& name, SNLDesign* memoryModel, std::optional floatingWriteDataBit = std::nullopt, - bool floatingReset = false) { + bool floatingReset = false, + SNLDesign* opaqueWriteDataModel = nullptr, + bool undrivenWriteEnable = false) { auto* top = SNLDesign::create(library, SNLDesign::Type::Standard, NLName(name)); auto* topClock = @@ -2384,6 +2371,10 @@ SNLDesign* createSinglePortMemoryTop( SNLBusTerm::create(top, SNLTerm::Direction::Output, 3, 0, NLName("out")); auto* memory = SNLInstance::create(top, memoryModel, NLName("mem0")); + auto* opaqueWriteData = opaqueWriteDataModel == nullptr + ? nullptr + : SNLInstance::create( + top, opaqueWriteDataModel, NLName("opaque_wdata")); auto* clockNet = SNLScalarNet::create(top, NLName("clk_net")); auto* chipEnableNet = SNLScalarNet::create(top, NLName("ce_net")); auto* writeEnableNet = SNLScalarNet::create(top, NLName("we_net")); @@ -2397,7 +2388,9 @@ SNLDesign* createSinglePortMemoryTop( topClock->setNet(clockNet); topChipEnable->setNet(chipEnableNet); - topWriteEnable->setNet(writeEnableNet); + if (!undrivenWriteEnable) { + topWriteEnable->setNet(writeEnableNet); + } if (topReset != nullptr) { topReset->setNet(resetNet); } @@ -2412,12 +2405,26 @@ SNLDesign* createSinglePortMemoryTop( auto* modelWriteData = memoryModel->getBusTerm(NLName("WDATA")); auto* modelWriteMask = memoryModel->getBusTerm(NLName("WMASK")); auto* modelReadData = memoryModel->getBusTerm(NLName("RDATA")); + auto* opaqueWriteDataNet = opaqueWriteData == nullptr + ? nullptr + : SNLScalarNet::create(top, NLName("opaque_wdata_net")); for (int bit = 0; bit <= 1; ++bit) { topAddress->getBit(bit)->setNet(addressNet->getBit(bit)); memory->getInstTerm(modelAddress->getBit(bit))->setNet(addressNet->getBit(bit)); } for (int bit = 0; bit <= 3; ++bit) { - if (floatingWriteDataBit.has_value() && bit == *floatingWriteDataBit) { + if (opaqueWriteData != nullptr && bit == 0) { + topWriteData->getBit(bit)->setNet(writeDataNet->getBit(bit)); + opaqueWriteData + ->getInstTerm(opaqueWriteDataModel->getScalarTerm(NLName("A"))) + ->setNet(writeDataNet->getBit(bit)); + opaqueWriteData + ->getInstTerm(opaqueWriteDataModel->getScalarTerm(NLName("Y"))) + ->setNet(opaqueWriteDataNet); + memory->getInstTerm(modelWriteData->getBit(bit)) + ->setNet(opaqueWriteDataNet); + } else if ( + floatingWriteDataBit.has_value() && bit == *floatingWriteDataBit) { auto* floatingWriteDataNet = SNLScalarNet::create( top, NLName("floating_wdata" + std::to_string(bit) + "_net")); memory->getInstTerm(modelWriteData->getBit(bit))->setNet(floatingWriteDataNet); @@ -2920,6 +2927,8 @@ SNLDesign* createOpaqueBoundaryTop( SNLDesign::create(library, SNLDesign::Type::Standard, NLName(name)); auto* topIn = SNLScalarTerm::create(top, SNLTerm::Direction::Input, NLName("in")); + auto* topGood = + SNLScalarTerm::create(top, SNLTerm::Direction::Output, NLName("good")); auto* topOut = SNLScalarTerm::create(top, SNLTerm::Direction::Output, NLName("out")); @@ -2928,6 +2937,7 @@ SNLDesign* createOpaqueBoundaryTop( auto* netOut = SNLScalarNet::create(top, NLName("net_out")); topIn->setNet(netIn); + topGood->setNet(netIn); topOut->setNet(netOut); opaque->getInstTerm(opaqueModel->getScalarTerm(NLName("A")))->setNet(netIn); opaque->getInstTerm(opaqueModel->getScalarTerm(NLName("Y")))->setNet(netOut); @@ -2935,6 +2945,131 @@ SNLDesign* createOpaqueBoundaryTop( return top; } +SNLDesign* createOpaqueIndependentOutputTop( + NLLibrary* library, + const std::string& name, + SNLDesign* opaqueModel, + SNLDesign* muxModel) { + auto* top = + SNLDesign::create(library, SNLDesign::Type::Standard, NLName(name)); + auto* topIn = + SNLScalarTerm::create(top, SNLTerm::Direction::Input, NLName("in")); + auto* topGood = + SNLScalarTerm::create(top, SNLTerm::Direction::Output, NLName("good")); + auto* topBad = + SNLScalarTerm::create(top, SNLTerm::Direction::Output, NLName("bad")); + auto* opaque = SNLInstance::create(top, opaqueModel, NLName("opaque0")); + auto* mux = SNLInstance::create(top, muxModel, NLName("mux0")); + + auto* inputNet = SNLScalarNet::create(top, NLName("input_net")); + auto* opaqueNet = SNLScalarNet::create(top, NLName("opaque_net")); + auto* goodNet = SNLScalarNet::create(top, NLName("good_net")); + auto* badNet = SNLScalarNet::create(top, NLName("bad_net")); + topIn->setNet(inputNet); + topGood->setNet(goodNet); + topBad->setNet(badNet); + opaque->getInstTerm(opaqueModel->getScalarTerm(NLName("A"))) + ->setNet(inputNet); + opaque->getInstTerm(opaqueModel->getScalarTerm(NLName("Y"))) + ->setNet(opaqueNet); + mux->getInstTerm(NLDB0::getMux2InputA(muxModel)->getBit(0)) + ->setNet(opaqueNet); + mux->getInstTerm(NLDB0::getMux2InputA(muxModel)->getBit(1)) + ->setNet(inputNet); + mux->getInstTerm(NLDB0::getMux2InputB(muxModel)->getBit(0)) + ->setNet(inputNet); + mux->getInstTerm(NLDB0::getMux2InputB(muxModel)->getBit(1)) + ->setNet(inputNet); + mux->getInstTerm(NLDB0::getMux2Select(muxModel))->setNet(inputNet); + mux->getInstTerm(NLDB0::getMux2Output(muxModel)->getBit(0))->setNet(badNet); + mux->getInstTerm(NLDB0::getMux2Output(muxModel)->getBit(1))->setNet(goodNet); + return top; +} + +SNLDesign* createLateOpaqueSequentialDependencyTop( + NLLibrary* library, + const std::string& name, + SNLDesign* opaqueModel) { + auto* top = + SNLDesign::create(library, SNLDesign::Type::Standard, NLName(name)); + auto* topIn = + SNLScalarTerm::create(top, SNLTerm::Direction::Input, NLName("in")); + auto* topClock = + SNLScalarTerm::create(top, SNLTerm::Direction::Input, NLName("clk")); + auto* topGood = + SNLScalarTerm::create(top, SNLTerm::Direction::Output, NLName("good")); + auto* topBad0 = + SNLScalarTerm::create(top, SNLTerm::Direction::Output, NLName("bad0")); + auto* topBad1 = + SNLScalarTerm::create(top, SNLTerm::Direction::Output, NLName("bad1")); + + auto* opaque = SNLInstance::create(top, opaqueModel, NLName("opaque0")); + auto* ff0 = SNLInstance::create(top, NLDB0::getDFF(), NLName("ff0")); + auto* ff1 = SNLInstance::create(top, NLDB0::getDFF(), NLName("ff1")); + auto* netIn = SNLScalarNet::create(top, NLName("net_in")); + auto* netClock = SNLScalarNet::create(top, NLName("net_clk")); + auto* netOpaque = SNLScalarNet::create(top, NLName("net_opaque")); + auto* netQ0 = SNLScalarNet::create(top, NLName("net_q0")); + auto* netQ1 = SNLScalarNet::create(top, NLName("net_q1")); + + topIn->setNet(netIn); + topClock->setNet(netClock); + topGood->setNet(netIn); + topBad0->setNet(netQ0); + topBad1->setNet(netQ1); + opaque->getInstTerm(opaqueModel->getScalarTerm(NLName("A")))->setNet(netIn); + opaque->getInstTerm(opaqueModel->getScalarTerm(NLName("Y")))->setNet(netOpaque); + ff0->getInstTerm(NLDB0::getDFFClock())->setNet(netClock); + ff0->getInstTerm(NLDB0::getDFFData())->setNet(netOpaque); + ff0->getInstTerm(NLDB0::getDFFOutput())->setNet(netQ0); + ff1->getInstTerm(NLDB0::getDFFClock())->setNet(netClock); + ff1->getInstTerm(NLDB0::getDFFData())->setNet(netQ0); + ff1->getInstTerm(NLDB0::getDFFOutput())->setNet(netQ1); + + return top; +} + +SNLDesign* createUnobservedOpaqueClockStateChainTop( + NLLibrary* library, + const std::string& name, + SNLDesign* opaqueClockModel) { + auto* top = + SNLDesign::create(library, SNLDesign::Type::Standard, NLName(name)); + auto* topIn = + SNLScalarTerm::create(top, SNLTerm::Direction::Input, NLName("in")); + auto* topClock = + SNLScalarTerm::create(top, SNLTerm::Direction::Input, NLName("clk")); + auto* topGood = + SNLScalarTerm::create(top, SNLTerm::Direction::Output, NLName("good")); + + auto* opaque = + SNLInstance::create(top, opaqueClockModel, NLName("opaque_clock")); + auto* ff0 = SNLInstance::create(top, NLDB0::getDFF(), NLName("ff0")); + auto* ff1 = SNLInstance::create(top, NLDB0::getDFF(), NLName("ff1")); + auto* netIn = SNLScalarNet::create(top, NLName("net_in")); + auto* netClock = SNLScalarNet::create(top, NLName("net_clk")); + auto* netOpaqueClock = + SNLScalarNet::create(top, NLName("net_opaque_clock")); + auto* netQ0 = SNLScalarNet::create(top, NLName("net_q0")); + auto* netQ1 = SNLScalarNet::create(top, NLName("net_q1")); + + topIn->setNet(netIn); + topClock->setNet(netClock); + topGood->setNet(netIn); + opaque->getInstTerm(opaqueClockModel->getScalarTerm(NLName("CK"))) + ->setNet(netClock); + opaque->getInstTerm(opaqueClockModel->getScalarTerm(NLName("Q"))) + ->setNet(netOpaqueClock); + ff0->getInstTerm(NLDB0::getDFFClock())->setNet(netOpaqueClock); + ff0->getInstTerm(NLDB0::getDFFData())->setNet(netIn); + ff0->getInstTerm(NLDB0::getDFFOutput())->setNet(netQ0); + ff1->getInstTerm(NLDB0::getDFFClock())->setNet(netClock); + ff1->getInstTerm(NLDB0::getDFFData())->setNet(netQ0); + ff1->getInstTerm(NLDB0::getDFFOutput())->setNet(netQ1); + + return top; +} + SNLDesign* createDffTop( NLLibrary* library, const std::string& name, @@ -3188,6 +3323,43 @@ SNLDesign* createNamedComplementSequentialModel( return model; } +SNLDesign* createIndependentSequentialOutputModel( + NLLibrary* library, + const std::string& name) { + auto* model = + SNLDesign::create(library, SNLDesign::Type::Primitive, NLName(name)); + auto* goodData = SNLScalarTerm::create( + model, SNLTerm::Direction::Input, NLName("D_GOOD")); + auto* badData = SNLScalarTerm::create( + model, SNLTerm::Direction::Input, NLName("D_BAD")); + auto* clock = + SNLScalarTerm::create(model, SNLTerm::Direction::Input, NLName("CK")); + auto* latchGate = SNLScalarTerm::create( + model, SNLTerm::Direction::Input, NLName("LATCH_GATE")); + auto* good = SNLScalarTerm::create( + model, SNLTerm::Direction::Output, NLName("GOOD")); + auto* bad = SNLScalarTerm::create( + model, SNLTerm::Direction::Output, NLName("BAD")); + SNLDesignModeling::addInputsToClockArcs({goodData}, clock); + SNLDesignModeling::addClockToOutputsArcs(clock, {good}); + SNLDesignModeling::addInputsToClockArcs({badData}, latchGate); + SNLDesignModeling::addClockToOutputsArcs(latchGate, {bad}); + + SNLDesignModeling::SequentialModel sequentialModel; + sequentialModel.clockedOn = makeSequentialTermExpression(clock); + for (auto* data : {goodData, badData}) { + SNLDesignModeling::SequentialState state; + state.nextState = makeSequentialTermExpression(data); + sequentialModel.states.push_back(std::move(state)); + } + sequentialModel.outputs.push_back( + {good, makeSequentialStateExpression(0)}); + sequentialModel.outputs.push_back( + {bad, makeSequentialStateExpression(1)}); + SNLDesignModeling::setSequentialModel(model, sequentialModel); + return model; +} + SNLDesign* createComplementFirstSequentialModel( NLLibrary* library, const std::string& name, @@ -3423,6 +3595,43 @@ SNLDesign* createSequentialOutputPairTop( return top; } +SNLDesign* createIndependentSequentialOutputTop( + NLLibrary* library, + const std::string& name, + SNLDesign* sequentialModel) { + auto* top = + SNLDesign::create(library, SNLDesign::Type::Standard, NLName(name)); + auto* goodData = SNLScalarTerm::create( + top, SNLTerm::Direction::Input, NLName("good_data")); + auto* badData = SNLScalarTerm::create( + top, SNLTerm::Direction::Input, NLName("bad_data")); + auto* clock = + SNLScalarTerm::create(top, SNLTerm::Direction::Input, NLName("clk")); + auto* latchGate = SNLScalarTerm::create( + top, SNLTerm::Direction::Input, NLName("latch_gate")); + auto* good = + SNLScalarTerm::create(top, SNLTerm::Direction::Output, NLName("good")); + auto* bad = + SNLScalarTerm::create(top, SNLTerm::Direction::Output, NLName("bad")); + auto* instance = SNLInstance::create(top, sequentialModel, NLName("ff0")); + + for (const auto& [topTerm, modelTermName] : + std::vector>{ + {goodData, "D_GOOD"}, + {badData, "D_BAD"}, + {clock, "CK"}, + {latchGate, "LATCH_GATE"}, + {good, "GOOD"}, + {bad, "BAD"}}) { + auto* net = SNLScalarNet::create( + top, NLName(std::string("net_") + modelTermName)); + topTerm->setNet(net); + instance->getInstTerm( + sequentialModel->getScalarTerm(NLName(modelTermName)))->setNet(net); + } + return top; +} + SNLDesign* createSetOnlySequentialTop( NLLibrary* library, const std::string& name, @@ -3837,7 +4046,6 @@ void SequentialEquivalenceStrategyTests:: expectMalformedSequentialExpressionUnsupported( MalformedSequentialExpression fault, const char* expectedReason) { - ScopedSecBoundaryAbstraction strictSequentialModeling(false); NLUniverse::create(); auto* db = NLDB::create(NLUniverse::get()); auto* primitives = @@ -3872,6 +4080,16 @@ void SequentialEquivalenceStrategyTests:: expression.addOperation(Operator::And, {dataNode, stateNode}); break; } + case MalformedSequentialExpression::StateWithoutOutput: { + SNLDesignModeling::SequentialState hiddenState; + hiddenState.nextState.root = hiddenState.nextState.addTerm( + model->getScalarTerm(NLName("D"))); + sequentialModel.states.push_back(std::move(hiddenState)); + const auto stateNode = expression.addState(1); + expression.root = + expression.addOperation(Operator::And, {dataNode, stateNode}); + break; + } case MalformedSequentialExpression::OutOfRangeOperand: expression.root = expression.addOperation(Operator::And, {dataNode, 1000}); @@ -3884,16 +4102,21 @@ void SequentialEquivalenceStrategyTests:: const auto extracted = SequentialDesignModel::extract(top); - EXPECT_TRUE(extracted.hasUnsupportedFeatures()); - ASSERT_FALSE(extracted.unsupportedReasons.empty()); - EXPECT_NE( - extracted.unsupportedReasons.front().find(expectedReason), - std::string::npos); + EXPECT_FALSE(extracted.hasUnsupportedFeatures()); + ASSERT_FALSE(extracted.skippedObservedOutputs.empty()); + EXPECT_TRUE(std::any_of( + extracted.skippedObservedOutputs.begin(), + extracted.skippedObservedOutputs.end(), + [&](const SignalKey& key) { + const auto skip = extracted.connectivitySkipInfoByKey.find(key); + return skip != extracted.connectivitySkipInfoByKey.end() && + skip->second.origin == ConnectivitySkipOrigin::OpaqueInternal && + skip->second.detail.find(expectedReason) != std::string::npos; + })); } void SequentialEquivalenceStrategyTests:: - expectMissingSequentialStateOutputHandled(bool boundaryAbstraction) { - ScopedSecBoundaryAbstraction boundaryMode(boundaryAbstraction); + expectUnreferencedSequentialStateWithoutOutputIgnored() { NLUniverse::create(); auto* db = NLDB::create(NLUniverse::get()); auto* primitives = @@ -3915,21 +4138,9 @@ void SequentialEquivalenceStrategyTests:: const auto extracted = SequentialDesignModel::extract(top); - if (boundaryAbstraction) { - EXPECT_FALSE(extracted.hasUnsupportedFeatures()); - ASSERT_FALSE(extracted.abstractedSequentialBoundaries.empty()); - EXPECT_NE( - extracted.abstractedSequentialBoundaries.front().find( - "state has no physical output"), - std::string::npos); - } else { - EXPECT_TRUE(extracted.hasUnsupportedFeatures()); - ASSERT_FALSE(extracted.unsupportedReasons.empty()); - EXPECT_NE( - extracted.unsupportedReasons.front().find( - "state has no physical output"), - std::string::npos); - } + EXPECT_FALSE(extracted.hasUnsupportedFeatures()); + EXPECT_EQ(extracted.observedOutputs.size(), 2u); + EXPECT_TRUE(extracted.skippedObservedOutputs.empty()); } SNLDesign* createDffreTop( @@ -3979,6 +4190,39 @@ SNLDesign* createOpaqueClockGateLatchModel(NLLibrary* library) { return model; } +SNLScalarTerm* getLatchGateTerm(SNLDesign* model) { + if (auto* gate = model->getScalarTerm(NLName("GATE"))) { + return gate; + } + return model->getScalarTerm(NLName("E")); +} + +SNLDesign* createDirectLatchTop( + NLLibrary* library, + const std::string& name, + SNLDesign* latchModel) { + auto* top = + SNLDesign::create(library, SNLDesign::Type::Standard, NLName(name)); + auto* topData = + SNLScalarTerm::create(top, SNLTerm::Direction::Input, NLName("data")); + auto* topEnable = + SNLScalarTerm::create(top, SNLTerm::Direction::Input, NLName("enable")); + auto* topOut = + SNLScalarTerm::create(top, SNLTerm::Direction::Output, NLName("out")); + auto* latch = SNLInstance::create(top, latchModel, NLName("latch0")); + + auto* netData = SNLScalarNet::create(top, NLName("net_data")); + auto* netEnable = SNLScalarNet::create(top, NLName("net_enable")); + auto* netOut = SNLScalarNet::create(top, NLName("net_out")); + topData->setNet(netData); + topEnable->setNet(netEnable); + topOut->setNet(netOut); + latch->getInstTerm(latchModel->getScalarTerm(NLName("D")))->setNet(netData); + latch->getInstTerm(getLatchGateTerm(latchModel))->setNet(netEnable); + latch->getInstTerm(latchModel->getScalarTerm(NLName("Q")))->setNet(netOut); + return top; +} + SNLDesign* createConstantLowModel(NLLibrary* library) { auto* model = SNLDesign::create(library, SNLDesign::Type::Primitive, NLName("CONB")); @@ -4021,7 +4265,7 @@ SNLDesign* createClockGateLatchDffTop( topOut->setNet(netOut); latch->getInstTerm(latchModel->getScalarTerm(NLName("D")))->setNet(netEnable); - latch->getInstTerm(latchModel->getScalarTerm(NLName("GATE")))->setNet(netClock); + latch->getInstTerm(getLatchGateTerm(latchModel))->setNet(netClock); latch->getInstTerm(latchModel->getScalarTerm(NLName("Q")))->setNet(netLatchQ); gateAnd->getInstTerm(andModel->getScalarTerm(NLName("A")))->setNet(netClock); @@ -4295,7 +4539,7 @@ SNLDesign* createClockGateLatchDataDffTop( } latch->getInstTerm(latchModel->getScalarTerm(NLName("D")))->setNet(netEnable); - latch->getInstTerm(latchModel->getScalarTerm(NLName("GATE")))->setNet(netClock); + latch->getInstTerm(getLatchGateTerm(latchModel))->setNet(netClock); latch->getInstTerm(latchModel->getScalarTerm(NLName("Q")))->setNet(netLatchQ); dataAnd->getInstTerm(andModel->getScalarTerm(NLName("A")))->setNet(netIn); @@ -4356,6 +4600,80 @@ SignalKey findKeyByDisplayName(const SequentialDesignModel& model, throw std::runtime_error("Missing display name in extracted model: " + displayName); } +void expectOpaqueOutputSkipWithReason( + const SequentialDesignModel& model, + const std::string& reason) { + EXPECT_FALSE(model.hasUnsupportedFeatures()); + ASSERT_FALSE(model.skippedObservedOutputs.empty()); + EXPECT_TRUE(std::any_of( + model.skippedObservedOutputs.begin(), + model.skippedObservedOutputs.end(), + [&](const SignalKey& key) { + const auto skip = model.connectivitySkipInfoByKey.find(key); + return skip != model.connectivitySkipInfoByKey.end() && + skip->second.origin == ConnectivitySkipOrigin::OpaqueInternal && + skip->second.detail.find(reason) != std::string::npos; + })); +} + +std::vector findOpaqueReachedOutputNamesForTest( + SNLDesign* top, + const std::string& opaqueOutputName) { + detail::ScopedDnlContextForTest dnlContext(top); + auto* dnl = dnlContext.dnl(); + const auto opaqueTerm = + detail::findTermByDisplayNameForTest(dnl, opaqueOutputName); + if (!opaqueTerm.has_value()) { + throw std::runtime_error( + "Missing opaque output terminal: " + opaqueOutputName); + } + + const SecNetlistChecks checks(dnl); + const auto reached = checks.findTopOutputsReachedByOpaqueTerminals( + {{*opaqueTerm, "test opaque terminal"}}); + std::vector names; + names.reserve(reached.size()); + for (const auto& output : reached) { + names.push_back(detail::getTerminalDisplayNameForTest( + dnl->getDNLTerminalFromID(output.topOutputTermID))); + } + std::sort(names.begin(), names.end()); + return names; +} + +std::optional +buildStrictOpaqueOutputForTest( + SNLDesign* top, + const std::string& opaqueOutputName, + const std::string& topOutputName) { + detail::ScopedDnlContextForTest dnlContext(top); + auto* dnl = dnlContext.dnl(); + const auto opaqueTerm = + detail::findTermByDisplayNameForTest(dnl, opaqueOutputName); + const auto topOutput = + detail::findTermByDisplayNameForTest(dnl, topOutputName); + if (!opaqueTerm.has_value() || !topOutput.has_value()) { + throw std::runtime_error("Missing strict opaque-builder test terminal"); + } + + KEPLER_FORMAL::BuildPrimaryOutputClauses builder; + builder.setRetainDnl(true); + builder.collect(); + auto inputs = builder.getInputs(); + inputs.erase(std::remove(inputs.begin(), inputs.end(), *opaqueTerm), + inputs.end()); + builder.setInputs(inputs); + builder.setOutputs({*topOutput}); + builder.setStopAtOpaqueInternalOutputs(true); + builder.build(); + + const auto skipped = builder.getSkippedOutputs().find(*topOutput); + if (skipped == builder.getSkippedOutputs().end()) { + return std::nullopt; + } + return skipped->second; +} + void expectAllExpressionSupportIsPublished(const SequentialDesignModel& model) { std::unordered_set publishedVars; for (const auto& key : model.environmentInputs) { @@ -12674,6 +12992,20 @@ TEST_F(SequentialEquivalenceStrategyTests, } } +TEST_F(SequentialEquivalenceStrategyTests, + RunExtractedModelsBinaryImcRemapsBothTransitionRelations) { + const auto models = makeHeldRailModelsForTest( + "binaryImcTransitionRemap", false, false); + auto strategy = makeBinaryExtractedSecStrategy(SecEngine::Imc); + + const auto result = + strategy.runExtractedModels(models.model0, models.model1, 1); + + EXPECT_EQ(result.status, SequentialEquivalenceStatus::Equivalent); + EXPECT_EQ(result.coveredOutputs, 1u); + EXPECT_EQ(result.totalOutputs, 1u); +} + TEST_F(SequentialEquivalenceStrategyTests, RunExtractedModelsDualRailLeavesResidualsUncoveredWithoutPdrFallback) { const SignalKey good = makeSignalKey("dualRailResidualGood"); @@ -12850,7 +13182,15 @@ TEST_F(SequentialEquivalenceStrategyTests, EXPECT_EQ(result.bound, 1u); EXPECT_EQ(result.coveredOutputs, 1u); EXPECT_EQ(result.totalOutputs, 1u); - EXPECT_FALSE(result.reason.empty()); + EXPECT_NE( + result.reason.find( + "Counterexample reaches the first bad frame at cycle 1."), + std::string::npos); + EXPECT_NE(result.reason.find("Input trace:"), std::string::npos); + EXPECT_NE( + result.reason.find("Observed output mismatches at cycle 1:"), + std::string::npos); + EXPECT_NE(result.reason.find("delayed_out[0]"), std::string::npos); } TEST_F(SequentialEquivalenceStrategyTests, @@ -13397,48 +13737,20 @@ TEST_F(SequentialEquivalenceStrategyTests, RunExtractedModelsStopsOnUnsupportedFirstModelWithBoundaryReports) { auto model0 = makeCombinationalExtractedModel(BoolExpr::Var(2)); auto model1 = makeCombinationalExtractedModel(BoolExpr::Var(2)); - const SignalKey stateKey = makeSignalKey("state"); - const SignalKey internalIn = makeSignalKey("internal_in"); - const SignalKey internalOut = makeSignalKey("internal_out"); model0.unsupportedReasons = {"unsupported sequential state"}; - model0.abstractedSequentialBoundaries = {"abstracted cell u_ff"}; - model0.internalBoundaryInputKeys = {internalIn}; - model0.internalBoundaryOutputKeys = {internalOut}; - model0.displayNameByKey.emplace(stateKey, "u_ff.STATE[0]"); - model0.displayNameByKey.emplace(internalIn, "u_logic.A[0]"); - model0.displayNameByKey.emplace(internalOut, "u_logic.Y[0]"); - model0.abstractedSequentialBoundaryDetails.push_back( - {"u_ff", {stateKey}, model0.allObservedOutputs}); auto strategy = makeBinaryExtractedSecStrategy(); const auto result = strategy.runExtractedModels(model0, model1, 1); EXPECT_EQ(result.status, SequentialEquivalenceStatus::Unsupported); EXPECT_NE(result.reason.find("unsupported sequential state"), std::string::npos); - ASSERT_EQ(result.abstractedSequentialBoundaries.size(), 1u); - EXPECT_EQ(result.abstractedSequentialBoundaries.front(), - "design0 abstracted cell u_ff"); - EXPECT_GE(result.extractedBoundaryReports.size(), 4u); - const auto stateReport = std::find_if( - result.extractedBoundaryReports.begin(), - result.extractedBoundaryReports.end(), - [](const ExtractedBoundaryReportEntry& entry) { - return entry.signal == "u_ff.STATE[0]"; - }); - ASSERT_NE(stateReport, result.extractedBoundaryReports.end()); - EXPECT_NE( - std::find( - stateReport->roles.begin(), - stateReport->roles.end(), - "abstracted_sequential_state"), - stateReport->roles.end()); + EXPECT_EQ(result.extractedBoundaryReports.size(), 2u); } TEST_F(SequentialEquivalenceStrategyTests, RunExtractedModelsStopsOnUnsupportedSecondModelAfterFirstReports) { auto model0 = makeCombinationalExtractedModel(BoolExpr::Var(2)); auto model1 = makeCombinationalExtractedModel(BoolExpr::Var(2)); - model0.abstractedSequentialBoundaries = {"kept first-side boundary"}; model1.unsupportedReasons = {"unsupported second side"}; auto strategy = makeBinaryExtractedSecStrategy(); @@ -13446,20 +13758,18 @@ TEST_F(SequentialEquivalenceStrategyTests, EXPECT_EQ(result.status, SequentialEquivalenceStatus::Unsupported); EXPECT_NE(result.reason.find("unsupported second side"), std::string::npos); - ASSERT_EQ(result.abstractedSequentialBoundaries.size(), 1u); - EXPECT_EQ(result.abstractedSequentialBoundaries.front(), - "design0 kept first-side boundary"); } TEST_F(SequentialEquivalenceStrategyTests, RunExtractedModelsReportsAllConnectivitySkippedOutputs) { SequentialDesignModel model0; SequentialDesignModel model1; - std::array origins = { + std::array origins = { ConnectivitySkipOrigin::NoDriver, ConnectivitySkipOrigin::MultiDriver, ConnectivitySkipOrigin::LogicalLoop, - ConnectivitySkipOrigin::MultiClockDomain}; + ConnectivitySkipOrigin::MultiClockDomain, + ConnectivitySkipOrigin::OpaqueInternal}; for (size_t i = 0; i < origins.size(); ++i) { const SignalKey key = makeSignalKey("skipped_out_" + std::to_string(i)); const std::string name = "out" + std::to_string(i) + "[0]"; @@ -13492,10 +13802,14 @@ TEST_F(SequentialEquivalenceStrategyTests, EXPECT_TRUE(hasSkipText("multi-driver connectivity")); EXPECT_TRUE(hasSkipText("logical-loop connectivity")); EXPECT_TRUE(hasSkipText("multi-clock-domain connectivity")); + EXPECT_TRUE(hasSkipText("opaque-internal")); ASSERT_EQ(result.multiClockDomainSkippedOutputs.size(), 1u); EXPECT_NE( result.multiClockDomainSkippedOutputs.front().find("multi-clock-domain"), std::string::npos); + ASSERT_EQ(result.opaqueCellSkippedOutputs.size(), 1u); + EXPECT_NE(result.opaqueCellSkippedOutputs.front().find("opaque-internal"), + std::string::npos); } TEST_F(SequentialEquivalenceStrategyTests, @@ -14470,7 +14784,7 @@ TEST_F(SequentialEquivalenceStrategyTests, } TEST_F(SequentialEquivalenceStrategyTests, - LazyTransitionUnpublishedSupportStaysDesignPrivate) { + LazyTransitionUnpublishedSupportIsRejected) { KInductionProblem problem; constexpr size_t combinedState0 = 10; constexpr size_t combinedState1 = 20; @@ -14492,29 +14806,18 @@ TEST_F(SequentialEquivalenceStrategyTests, problem.allSymbols = {combinedState0, combinedState1}; const TransitionExprResolver transitionByState(problem); - const size_t private0 = makePrivateProofLeafSymbol(0, unpublishedLocal); - const size_t private1 = makePrivateProofLeafSymbol(1, unpublishedLocal); - - EXPECT_NE(private0, private1); - EXPECT_EQ( - transitionByState.support(combinedState0), - (std::set{combinedState0, private0})); - EXPECT_EQ( - transitionByState.support(combinedState1), - (std::set{combinedState1, private1})); - EXPECT_EQ( - lazyTransitions->localToCombinedByDesign[0].at(unpublishedLocal), - private0); - EXPECT_EQ( - lazyTransitions->localToCombinedByDesign[1].at(unpublishedLocal), - private1); - + EXPECT_THROW( + static_cast(transitionByState.support(combinedState0)), + std::runtime_error); + EXPECT_THROW( + static_cast(transitionByState.support(combinedState1)), + std::runtime_error); EXPECT_EQ( - transitionByState.at(combinedState0)->getSupportVars(), - (std::set{combinedState0, private0})); + lazyTransitions->localToCombinedByDesign[0].find(unpublishedLocal), + lazyTransitions->localToCombinedByDesign[0].end()); EXPECT_EQ( - transitionByState.at(combinedState1)->getSupportVars(), - (std::set{combinedState1, private1})); + lazyTransitions->localToCombinedByDesign[1].find(unpublishedLocal), + lazyTransitions->localToCombinedByDesign[1].end()); } TEST_F(SequentialEquivalenceStrategyTests, @@ -15826,7 +16129,7 @@ TEST_F(SequentialEquivalenceStrategyTests, } TEST_F(SequentialEquivalenceStrategyTests, - SequentialDesignModelExtractModelsStructuredMemoryWithoutBoundaryFallback) { + SequentialDesignModelExtractModelsStructuredMemory) { NLUniverse::create(); auto* db = NLDB::create(NLUniverse::get()); auto* primitives = @@ -15838,25 +16141,13 @@ TEST_F(SequentialEquivalenceStrategyTests, const auto extracted = SequentialDesignModel::extract(top); - auto hasBoundaryRoleName = [&](const std::vector& keys, - const std::string& prefix) { - return std::any_of(keys.begin(), keys.end(), [&](const SignalKey& key) { - const auto it = extracted.displayNameByKey.find(key); - return it != extracted.displayNameByKey.end() && - it->second.rfind(prefix, 0) == 0; - }); - }; - EXPECT_FALSE(extracted.hasUnsupportedFeatures()); - EXPECT_TRUE(extracted.abstractedSequentialBoundaries.empty()); - EXPECT_FALSE(hasBoundaryRoleName(extracted.internalBoundaryInputKeys, "mem0.")); - EXPECT_FALSE(hasBoundaryRoleName(extracted.internalBoundaryOutputKeys, "mem0.")); EXPECT_FALSE(extracted.stateBits.empty()); EXPECT_FALSE(extracted.nextStateExprByStateKey.empty()); } TEST_F(SequentialEquivalenceStrategyTests, - SequentialDesignModelExtractModelsImportedLibertyMemoryWithoutOpaqueBoundaryTerms) { + SequentialDesignModelExtractModelsImportedLibertyMemory) { NLUniverse::create(); auto* db = NLDB::create(NLUniverse::get()); auto* primitives = @@ -15869,19 +16160,7 @@ TEST_F(SequentialEquivalenceStrategyTests, const auto extracted = SequentialDesignModel::extract(top); - auto hasBoundaryRoleName = [&](const std::vector& keys, - const std::string& prefix) { - return std::any_of(keys.begin(), keys.end(), [&](const SignalKey& key) { - const auto it = extracted.displayNameByKey.find(key); - return it != extracted.displayNameByKey.end() && - it->second.rfind(prefix, 0) == 0; - }); - }; - EXPECT_FALSE(extracted.hasUnsupportedFeatures()); - EXPECT_TRUE(extracted.abstractedSequentialBoundaries.empty()); - EXPECT_FALSE(hasBoundaryRoleName(extracted.internalBoundaryInputKeys, "mem0.")); - EXPECT_FALSE(hasBoundaryRoleName(extracted.internalBoundaryOutputKeys, "mem0.")); EXPECT_FALSE(extracted.stateBits.empty()); EXPECT_FALSE(extracted.nextStateExprByStateKey.empty()); } @@ -16166,7 +16445,7 @@ TEST_F( TEST_F( SequentialEquivalenceStrategyTests, - SequentialDesignModelExtractModelsRealCva6PerfCountersTargetConfigMemoryWithoutBoundaryFallback) { + SequentialDesignModelExtractModelsRealCva6PerfCountersTargetConfigMemory) { const auto context = resolveCva6SourceContextForSecTests(); if (!context.has_value()) { GTEST_SKIP() @@ -16183,24 +16462,8 @@ TEST_F( "real_cva6_perf_counters_module_with_target_config_sec_memory_supported"); const auto extracted = SequentialDesignModel::extract(top); - auto hasBoundaryRoleName = [&](const std::vector& keys, - const std::string& needle) { - return std::any_of(keys.begin(), keys.end(), [&](const SignalKey& key) { - const auto it = extracted.displayNameByKey.find(key); - return it != extracted.displayNameByKey.end() && - it->second.find(needle) != std::string::npos; - }); - }; - - // Guard the exact configured CVA6 perf-counter memory path that fails in - // full SEC runs: the inferred memory should stay inside the sequential model - // instead of leaking back out as generic boundary terms. + // Guard the exact configured CVA6 perf-counter memory path used by full SEC. EXPECT_FALSE(extracted.hasUnsupportedFeatures()); - EXPECT_TRUE(extracted.abstractedSequentialBoundaries.empty()); - EXPECT_FALSE( - hasBoundaryRoleName(extracted.internalBoundaryInputKeys, "generic_counter_q_mem")); - EXPECT_FALSE( - hasBoundaryRoleName(extracted.internalBoundaryOutputKeys, "generic_counter_q_mem")); } TEST_F(SequentialEquivalenceStrategyTests, @@ -16246,19 +16509,7 @@ endmodule const auto extracted = SequentialDesignModel::extract(top); - auto hasBoundaryRoleName = [&](const std::vector& keys, - const std::string& prefix) { - return std::any_of(keys.begin(), keys.end(), [&](const SignalKey& key) { - const auto it = extracted.displayNameByKey.find(key); - return it != extracted.displayNameByKey.end() && - it->second.rfind(prefix, 0) == 0; - }); - }; - EXPECT_FALSE(extracted.hasUnsupportedFeatures()); - EXPECT_TRUE(extracted.abstractedSequentialBoundaries.empty()); - EXPECT_FALSE(hasBoundaryRoleName(extracted.internalBoundaryInputKeys, "mem_q")); - EXPECT_FALSE(hasBoundaryRoleName(extracted.internalBoundaryOutputKeys, "mem_q")); EXPECT_FALSE(extracted.stateBits.empty()); EXPECT_FALSE(extracted.nextStateExprByStateKey.empty()); EXPECT_TRUE(extracted.skippedObservedOutputs.empty()); @@ -16403,33 +16654,39 @@ TEST_F(SequentialEquivalenceStrategyTests, } TEST_F(SequentialEquivalenceStrategyTests, - SequentialDesignModelExtractSkipsWholeMemoryForUndrivenWriteData) { + OpaqueEnabledMemoryWriteDataSkipsReadOutputs) { NLUniverse::create(); auto* db = NLDB::create(NLUniverse::get()); auto* primitives = NLLibrary::create(db, NLLibrary::Type::Primitives, NLName("prims")); auto* library = NLLibrary::create(db, NLLibrary::Type::Standard, NLName("designs")); - auto* memoryModel = createSinglePortMemoryModel(primitives, "MEM_FLOAT_WDATA"); + auto* memoryModel = + createSinglePortMemoryModel(primitives, "MEM_OPAQUE_ENABLED_WRITE"); + auto* opaqueModel = createOpaqueLeafModel(primitives); auto* top = createSinglePortMemoryTop( - library, "structured_memory_undriven_write_data", memoryModel, 0); + library, + "structured_memory_opaque_enabled_write", + memoryModel, + std::nullopt, + false, + opaqueModel); const auto extracted = SequentialDesignModel::extract(top); EXPECT_FALSE(extracted.hasUnsupportedFeatures()); - EXPECT_FALSE(extracted.connectivitySkipInfoByKey.empty()); - EXPECT_FALSE(extracted.skippedObservedOutputs.empty()); - EXPECT_TRUE(std::any_of( - extracted.connectivitySkipInfoByKey.begin(), - extracted.connectivitySkipInfoByKey.end(), - [](const auto& entry) { - return entry.second.detail.find("Structured memory dependency") != - std::string::npos; - })); + EXPECT_TRUE(extracted.observedOutputs.empty()); + EXPECT_EQ(extracted.skippedObservedOutputs.size(), 4u); + for (const auto& output : extracted.skippedObservedOutputs) { + const auto skip = extracted.connectivitySkipInfoByKey.find(output); + ASSERT_NE(skip, extracted.connectivitySkipInfoByKey.end()); + EXPECT_EQ(skip->second.origin, ConnectivitySkipOrigin::OpaqueInternal); + EXPECT_NE(skip->second.detail.find("opaque_wdata"), std::string::npos); + } } TEST_F(SequentialEquivalenceStrategyTests, - SequentialDesignModelExtractSkipsWholeMemoryForUndrivenReset) { + OpaqueDisabledMemoryWriteDataDoesNotSkipReadOutputs) { NLUniverse::create(); auto* db = NLDB::create(NLUniverse::get()); auto* primitives = @@ -16437,35 +16694,203 @@ TEST_F(SequentialEquivalenceStrategyTests, auto* library = NLLibrary::create(db, NLLibrary::Type::Standard, NLName("designs")); auto* memoryModel = - createSinglePortMemoryModel(primitives, "MEM_FLOAT_RST", true); + createSinglePortMemoryModel(primitives, "MEM_OPAQUE_DISABLED_WRITE"); + auto* opaqueModel = createOpaqueLeafModel(primitives); auto* top = createSinglePortMemoryTop( library, - "structured_memory_undriven_reset", + "structured_memory_opaque_disabled_write", memoryModel, std::nullopt, + false, + opaqueModel, true); const auto extracted = SequentialDesignModel::extract(top); EXPECT_FALSE(extracted.hasUnsupportedFeatures()); - EXPECT_FALSE(extracted.connectivitySkipInfoByKey.empty()); - EXPECT_FALSE(extracted.skippedObservedOutputs.empty()); - EXPECT_TRUE(std::any_of( - extracted.connectivitySkipInfoByKey.begin(), - extracted.connectivitySkipInfoByKey.end(), - [](const auto& entry) { - return entry.second.detail.find("Structured memory dependency") != - std::string::npos; - })); + EXPECT_EQ(extracted.observedOutputs.size(), 4u); + EXPECT_TRUE(extracted.skippedObservedOutputs.empty()); + const auto opaqueOutput = + findKeyByDisplayName(extracted, "opaque_wdata.Y[0]"); + EXPECT_EQ( + extracted.inputVarByKey.find(opaqueOutput), + extracted.inputVarByKey.end()); } TEST_F(SequentialEquivalenceStrategyTests, - SequentialDesignModelExtractModelsInferredStructMemoryWithLogicalOrCommitGuard) { + SecNetlistChecksPropagatesEnabledMemoryWriteThroughResetModel) { NLUniverse::create(); auto* db = NLDB::create(NLUniverse::get()); + auto* primitives = + NLLibrary::create(db, NLLibrary::Type::Primitives, NLName("prims")); auto* library = NLLibrary::create(db, NLLibrary::Type::Standard, NLName("designs")); - auto* top = loadSystemVerilogTopFromSource( + auto* memoryModel = + createSinglePortMemoryModel(primitives, "MEM_RESET_DEPENDENCY", true); + auto* opaqueModel = createOpaqueLeafModel(primitives); + auto* top = createSinglePortMemoryTop( + library, "memory_reset_dependency", memoryModel, std::nullopt, false, + opaqueModel); + + EXPECT_EQ(findOpaqueReachedOutputNamesForTest(top, "opaque_wdata.Y[0]"), + (std::vector{ + "out[0]", "out[1]", "out[2]", "out[3]"})); +} + +TEST_F(SequentialEquivalenceStrategyTests, + SecNetlistChecksToleratesMissingOptionalMemoryResetTerm) { + NLUniverse::create(); + auto* db = NLDB::create(NLUniverse::get()); + auto* primitives = + NLLibrary::create(db, NLLibrary::Type::Primitives, NLName("prims")); + auto* library = + NLLibrary::create(db, NLLibrary::Type::Standard, NLName("designs")); + auto* memoryModel = + createSinglePortMemoryModel(primitives, "MEM_NULL_RESET_DEPENDENCY"); + auto interface = SNLDesignModeling::getMemoryInterface(memoryModel); + interface.resetMode = SNLDesignModeling::MemoryResetMode::AsyncHigh; + SNLDesignModeling::setMemoryInterface(memoryModel, interface); + auto* opaqueModel = createOpaqueLeafModel(primitives); + auto* top = createSinglePortMemoryTop( + library, "memory_null_reset_dependency", memoryModel, std::nullopt, + false, opaqueModel); + + EXPECT_EQ(findOpaqueReachedOutputNamesForTest(top, "opaque_wdata.Y[0]"), + (std::vector{ + "out[0]", "out[1]", "out[2]", "out[3]"})); +} + +TEST_F(SequentialEquivalenceStrategyTests, + SecNetlistChecksRejectsMismatchedMemoryReadShape) { + NLUniverse::create(); + auto* db = NLDB::create(NLUniverse::get()); + auto* primitives = + NLLibrary::create(db, NLLibrary::Type::Primitives, NLName("prims")); + auto* library = + NLLibrary::create(db, NLLibrary::Type::Standard, NLName("designs")); + auto* memoryModel = + createSinglePortMemoryModel(primitives, "MEM_BAD_READ_SHAPE"); + auto interface = SNLDesignModeling::getMemoryInterface(memoryModel); + interface.readPorts.front().address.pop_back(); + SNLDesignModeling::setMemoryInterface(memoryModel, interface); + auto* opaqueModel = createOpaqueLeafModel(primitives); + auto* top = createSinglePortMemoryTop( + library, "memory_bad_read_shape", memoryModel, std::nullopt, false, + opaqueModel); + + EXPECT_TRUE( + findOpaqueReachedOutputNamesForTest(top, "opaque_wdata.Y[0]").empty()); +} + +TEST_F(SequentialEquivalenceStrategyTests, + SecNetlistChecksRejectsUnsupportedMemoryWriteShape) { + NLUniverse::create(); + auto* db = NLDB::create(NLUniverse::get()); + auto* primitives = + NLLibrary::create(db, NLLibrary::Type::Primitives, NLName("prims")); + auto* library = + NLLibrary::create(db, NLLibrary::Type::Standard, NLName("designs")); + auto* memoryModel = + createSinglePortMemoryModel(primitives, "MEM_BAD_WRITE_SHAPE"); + auto interface = SNLDesignModeling::getMemoryInterface(memoryModel); + interface.writePorts.front().extraWriteInputs.push_back( + {interface.writePorts.front().data.front()}); + SNLDesignModeling::setMemoryInterface(memoryModel, interface); + auto* opaqueModel = createOpaqueLeafModel(primitives); + auto* top = createSinglePortMemoryTop( + library, "memory_bad_write_shape", memoryModel, std::nullopt, false, + opaqueModel); + + EXPECT_TRUE( + findOpaqueReachedOutputNamesForTest(top, "opaque_wdata.Y[0]").empty()); +} + +TEST_F(SequentialEquivalenceStrategyTests, + SecNetlistChecksTreatsDisconnectedMemoryWriteEnableAsDisabled) { + NLUniverse::create(); + auto* db = NLDB::create(NLUniverse::get()); + auto* primitives = + NLLibrary::create(db, NLLibrary::Type::Primitives, NLName("prims")); + auto* library = + NLLibrary::create(db, NLLibrary::Type::Standard, NLName("designs")); + auto* memoryModel = + createSinglePortMemoryModel(primitives, "MEM_DISCONNECTED_WE"); + auto* opaqueModel = createOpaqueLeafModel(primitives); + auto* top = createSinglePortMemoryTop( + library, "memory_disconnected_we", memoryModel, std::nullopt, false, + opaqueModel); + top->getInstance(NLName("mem0")) + ->getInstTerm(memoryModel->getScalarTerm(NLName("WE"))) + ->setNet(nullptr); + + EXPECT_TRUE( + findOpaqueReachedOutputNamesForTest(top, "opaque_wdata.Y[0]").empty()); +} + +TEST_F(SequentialEquivalenceStrategyTests, + SequentialDesignModelExtractSkipsWholeMemoryForUndrivenWriteData) { + NLUniverse::create(); + auto* db = NLDB::create(NLUniverse::get()); + auto* primitives = + NLLibrary::create(db, NLLibrary::Type::Primitives, NLName("prims")); + auto* library = + NLLibrary::create(db, NLLibrary::Type::Standard, NLName("designs")); + auto* memoryModel = createSinglePortMemoryModel(primitives, "MEM_FLOAT_WDATA"); + auto* top = createSinglePortMemoryTop( + library, "structured_memory_undriven_write_data", memoryModel, 0); + + const auto extracted = SequentialDesignModel::extract(top); + + EXPECT_FALSE(extracted.hasUnsupportedFeatures()); + EXPECT_FALSE(extracted.connectivitySkipInfoByKey.empty()); + EXPECT_FALSE(extracted.skippedObservedOutputs.empty()); + EXPECT_TRUE(std::any_of( + extracted.connectivitySkipInfoByKey.begin(), + extracted.connectivitySkipInfoByKey.end(), + [](const auto& entry) { + return entry.second.detail.find("Structured memory dependency") != + std::string::npos; + })); +} + +TEST_F(SequentialEquivalenceStrategyTests, + SequentialDesignModelExtractSkipsWholeMemoryForUndrivenReset) { + NLUniverse::create(); + auto* db = NLDB::create(NLUniverse::get()); + auto* primitives = + NLLibrary::create(db, NLLibrary::Type::Primitives, NLName("prims")); + auto* library = + NLLibrary::create(db, NLLibrary::Type::Standard, NLName("designs")); + auto* memoryModel = + createSinglePortMemoryModel(primitives, "MEM_FLOAT_RST", true); + auto* top = createSinglePortMemoryTop( + library, + "structured_memory_undriven_reset", + memoryModel, + std::nullopt, + true); + + const auto extracted = SequentialDesignModel::extract(top); + + EXPECT_FALSE(extracted.hasUnsupportedFeatures()); + EXPECT_FALSE(extracted.connectivitySkipInfoByKey.empty()); + EXPECT_FALSE(extracted.skippedObservedOutputs.empty()); + EXPECT_TRUE(std::any_of( + extracted.connectivitySkipInfoByKey.begin(), + extracted.connectivitySkipInfoByKey.end(), + [](const auto& entry) { + return entry.second.detail.find("Structured memory dependency") != + std::string::npos; + })); +} + +TEST_F(SequentialEquivalenceStrategyTests, + SequentialDesignModelExtractModelsInferredStructMemoryWithLogicalOrCommitGuard) { + NLUniverse::create(); + auto* db = NLDB::create(NLUniverse::get()); + auto* library = + NLLibrary::create(db, NLLibrary::Type::Standard, NLName("designs")); + auto* top = loadSystemVerilogTopFromSource( library, "qd_next_local_commit_logical_or_supported", R"(module qd_next_local_commit_logical_or_supported( @@ -16512,19 +16937,7 @@ endmodule const auto extracted = SequentialDesignModel::extract(top); - auto hasBoundaryRoleName = [&](const std::vector& keys, - const std::string& prefix) { - return std::any_of(keys.begin(), keys.end(), [&](const SignalKey& key) { - const auto it = extracted.displayNameByKey.find(key); - return it != extracted.displayNameByKey.end() && - it->second.rfind(prefix, 0) == 0; - }); - }; - EXPECT_FALSE(extracted.hasUnsupportedFeatures()); - EXPECT_TRUE(extracted.abstractedSequentialBoundaries.empty()); - EXPECT_FALSE(hasBoundaryRoleName(extracted.internalBoundaryInputKeys, "mem_q")); - EXPECT_FALSE(hasBoundaryRoleName(extracted.internalBoundaryOutputKeys, "mem_q")); EXPECT_FALSE(extracted.stateBits.empty()); EXPECT_FALSE(extracted.nextStateExprByStateKey.empty()); EXPECT_TRUE(extracted.skippedObservedOutputs.empty()); @@ -16775,6 +17188,90 @@ TEST_F(SequentialEquivalenceStrategyTests, {stateVar, true}})); } +TEST_F(SequentialEquivalenceStrategyTests, + SequentialDesignModelExtractTreatsNajaLatchModelAsOpaque) { + NLUniverse::create(); + auto* db = NLDB::create(NLUniverse::get()); + auto* library = + NLLibrary::create(db, NLLibrary::Type::Standard, NLName("designs")); + auto* top = createDirectLatchTop(library, "top", NLDB0::getDLatch()); + + const auto extracted = SequentialDesignModel::extract(top); + const auto outputKey = findKeyByDisplayName(extracted, "out[0]"); + const auto latchKey = findKeyByDisplayName(extracted, "latch0.Q[0]"); + + EXPECT_FALSE(extracted.hasUnsupportedFeatures()); + EXPECT_TRUE(extracted.stateBits.empty()); + EXPECT_TRUE(extracted.observedOutputs.empty()); + ASSERT_EQ(extracted.skippedObservedOutputs.size(), 1u); + EXPECT_EQ(extracted.skippedObservedOutputs.front(), outputKey); + const auto skip = extracted.connectivitySkipInfoByKey.find(outputKey); + ASSERT_NE(skip, extracted.connectivitySkipInfoByKey.end()); + EXPECT_EQ(skip->second.origin, ConnectivitySkipOrigin::OpaqueInternal); + EXPECT_NE(skip->second.detail.find("latch0"), std::string::npos); + EXPECT_NE(skip->second.detail.find("naja_dlatch"), std::string::npos); + EXPECT_NE(skip->second.detail.find("Q[0]"), std::string::npos); + EXPECT_NE( + skip->second.detail.find( + "Naja latch sequential models are not supported by SEC"), + std::string::npos); + EXPECT_EQ(extracted.inputVarByKey.find(latchKey), + extracted.inputVarByKey.end()); + EXPECT_EQ( + std::find( + extracted.environmentInputs.begin(), + extracted.environmentInputs.end(), + latchKey), + extracted.environmentInputs.end()); +} + +TEST_F(SequentialEquivalenceStrategyTests, + SequentialDesignModelExtractFoldsModeledClockGateLatchEnable) { + NLUniverse::create(); + auto* db = NLDB::create(NLUniverse::get()); + auto* primitives = + NLLibrary::create(db, NLLibrary::Type::Primitives, NLName("prims")); + auto* library = + NLLibrary::create(db, NLLibrary::Type::Standard, NLName("designs")); + auto* andModel = createAnd2Model(primitives); + auto* top = createClockGateLatchDffTop( + library, "top", andModel, NLDB0::getDLatch()); + + const auto extracted = SequentialDesignModel::extract(top); + expectAllExpressionSupportIsPublished(extracted); + const auto stateKey = findKeyByDisplayName(extracted, "ff0.Q[0]"); + const auto inKey = findKeyByDisplayName(extracted, "in[0]"); + const auto enableKey = findKeyByDisplayName(extracted, "en[0]"); + const auto latchKey = + findKeyByDisplayName(extracted, "clock_gate_i.en_latch.Q[0]"); + const size_t stateVar = extracted.inputVarByKey.at(stateKey); + auto* expr = extracted.nextStateExprByStateKey.at(stateKey); + + EXPECT_FALSE(extracted.hasUnsupportedFeatures()); + EXPECT_EQ(extracted.observedOutputs.size(), 1u); + EXPECT_TRUE(extracted.skippedObservedOutputs.empty()); + EXPECT_EQ(extracted.inputVarByKey.find(latchKey), + extracted.inputVarByKey.end()); + EXPECT_EQ( + std::find( + extracted.environmentInputs.begin(), + extracted.environmentInputs.end(), + latchKey), + extracted.environmentInputs.end()); + EXPECT_TRUE(expr->evaluate( + {{extracted.inputVarByKey.at(inKey), false}, + {extracted.inputVarByKey.at(enableKey), false}, + {stateVar, true}})); + EXPECT_TRUE(expr->evaluate( + {{extracted.inputVarByKey.at(inKey), true}, + {extracted.inputVarByKey.at(enableKey), true}, + {stateVar, false}})); + EXPECT_FALSE(expr->evaluate( + {{extracted.inputVarByKey.at(inKey), false}, + {extracted.inputVarByKey.at(enableKey), true}, + {stateVar, true}})); +} + TEST_F(SequentialEquivalenceStrategyTests, SequentialDesignModelExtractTreatsNamedClockTreeBufferAsCarrier) { NLUniverse::create(); @@ -16980,8 +17477,7 @@ TEST_F(SequentialEquivalenceStrategyTests, } TEST_F(SequentialEquivalenceStrategyTests, - SequentialDesignModelExtractAbstractsUncomputableSequentialAsBoundaryByDefault) { - ScopedSecBoundaryAbstraction boundaryAbstraction(true); + SequentialDesignModelExtractSkipsOutputDrivenBySequentialWithoutModel) { NLUniverse::create(); auto* db = NLDB::create(NLUniverse::get()); auto* primitives = @@ -16992,30 +17488,33 @@ TEST_F(SequentialEquivalenceStrategyTests, auto* top = createNoDataSequentialTop(library, "top", model); const auto extracted = SequentialDesignModel::extract(top); + const auto opaqueState = findKeyByDisplayName(extracted, "ff0.Q[0]"); EXPECT_FALSE(extracted.hasUnsupportedFeatures()); EXPECT_TRUE(extracted.stateBits.empty()); - EXPECT_NE( + EXPECT_TRUE(extracted.observedOutputs.empty()); + ASSERT_EQ(extracted.skippedObservedOutputs.size(), 1u); + const auto skip = extracted.connectivitySkipInfoByKey.find( + extracted.skippedObservedOutputs.front()); + ASSERT_NE(skip, extracted.connectivitySkipInfoByKey.end()); + EXPECT_EQ(skip->second.origin, ConnectivitySkipOrigin::OpaqueInternal); + EXPECT_NE(skip->second.detail.find("ff0"), std::string::npos); + EXPECT_NE(skip->second.detail.find("SEQ_NO_D"), std::string::npos); + EXPECT_NE(skip->second.detail.find("Q[0]"), std::string::npos); + EXPECT_NE(skip->second.detail.find("Missing Naja sequential model"), + std::string::npos); + EXPECT_EQ( std::find( extracted.environmentInputs.begin(), extracted.environmentInputs.end(), - findKeyByDisplayName(extracted, "ff0.Q[0]")), + opaqueState), extracted.environmentInputs.end()); - EXPECT_NE( - std::find( - extracted.observedOutputs.begin(), - extracted.observedOutputs.end(), - findKeyByDisplayName(extracted, "ff0.CK[0]")), - extracted.observedOutputs.end()); - ASSERT_EQ(extracted.abstractedSequentialBoundaries.size(), 1u); - EXPECT_NE( - extracted.abstractedSequentialBoundaries.front().find("ff0"), - std::string::npos); + EXPECT_EQ(extracted.inputVarByKey.find(opaqueState), + extracted.inputVarByKey.end()); } TEST_F(SequentialEquivalenceStrategyTests, - SequentialDesignModelExtractAbstractsSequentialWithUnsupportedUpdatePinsAsBoundaryByDefault) { - ScopedSecBoundaryAbstraction boundaryAbstraction(true); + SequentialDesignModelExtractSkipsSequentialWithUnsupportedUpdatePin) { NLUniverse::create(); auto* db = NLDB::create(NLUniverse::get()); auto* primitives = @@ -17029,49 +17528,13 @@ TEST_F(SequentialEquivalenceStrategyTests, EXPECT_FALSE(extracted.hasUnsupportedFeatures()); EXPECT_TRUE(extracted.stateBits.empty()); - EXPECT_FALSE(extracted.abstractedSequentialBoundaries.empty()); - EXPECT_NE( - extracted.abstractedSequentialBoundaries.front().find("update pin `A`"), - std::string::npos); -} - -TEST_F(SequentialEquivalenceStrategyTests, - SequentialDesignModelExtractReportsUnsupportedSequentialWithoutDInput) { - ScopedSecBoundaryAbstraction strictSequentialModeling(false); - NLUniverse::create(); - auto* db = NLDB::create(NLUniverse::get()); - auto* primitives = - NLLibrary::create(db, NLLibrary::Type::Primitives, NLName("prims")); - auto* library = - NLLibrary::create(db, NLLibrary::Type::Standard, NLName("designs")); - auto* model = createNoDataSequentialModel(primitives, "SEQ_NO_D"); - auto* top = createNoDataSequentialTop(library, "top", model); - - const auto extracted = SequentialDesignModel::extract(top); - - EXPECT_TRUE(extracted.hasUnsupportedFeatures()); - EXPECT_FALSE(extracted.unsupportedReasons.empty()); -} - -TEST_F(SequentialEquivalenceStrategyTests, - SequentialDesignModelExtractRejectsSequentialWithUnsupportedUpdatePinsInStrictMode) { - ScopedSecBoundaryAbstraction strictSequentialModeling(false); - NLUniverse::create(); - auto* db = NLDB::create(NLUniverse::get()); - auto* primitives = - NLLibrary::create(db, NLLibrary::Type::Primitives, NLName("prims")); - auto* library = - NLLibrary::create(db, NLLibrary::Type::Standard, NLName("designs")); - auto* model = createExtraUpdatePinSequentialModel(primitives, "SEQ_ADDR"); - auto* top = createExtraUpdatePinSequentialTop(library, "top", model); - - const auto extracted = SequentialDesignModel::extract(top); - - EXPECT_TRUE(extracted.hasUnsupportedFeatures()); - ASSERT_FALSE(extracted.unsupportedReasons.empty()); - EXPECT_NE( - extracted.unsupportedReasons.front().find("update pin `A`"), - std::string::npos); + EXPECT_TRUE(extracted.observedOutputs.empty()); + ASSERT_EQ(extracted.skippedObservedOutputs.size(), 1u); + const auto skip = extracted.connectivitySkipInfoByKey.find( + extracted.skippedObservedOutputs.front()); + ASSERT_NE(skip, extracted.connectivitySkipInfoByKey.end()); + EXPECT_EQ(skip->second.origin, ConnectivitySkipOrigin::OpaqueInternal); + EXPECT_NE(skip->second.detail.find("update pin `A`"), std::string::npos); } TEST_F(SequentialEquivalenceStrategyTests, @@ -17088,8 +17551,6 @@ TEST_F(SequentialEquivalenceStrategyTests, const auto extracted = SequentialDesignModel::extract(top); EXPECT_FALSE(extracted.hasUnsupportedFeatures()); - EXPECT_TRUE(extracted.abstractedSequentialBoundaries.empty()); - EXPECT_TRUE(extracted.internalBoundaryOutputKeys.empty()); ASSERT_EQ(extracted.stateBits.size(), 1u); ASSERT_EQ(extracted.topOutputKeys.size(), 1u); EXPECT_EQ(extracted.observedOutputs.size(), extracted.topOutputKeys.size()); @@ -17210,6 +17671,13 @@ TEST_F(SequentialEquivalenceStrategyTests, "references an unmapped state"); } +TEST_F(SequentialEquivalenceStrategyTests, + SequentialDesignModelExtractRejectsStateWithoutModelableOutput) { + expectMalformedSequentialExpressionUnsupported( + MalformedSequentialExpression::StateWithoutOutput, + "state without a modelable output"); +} + TEST_F(SequentialEquivalenceStrategyTests, SequentialDesignModelExtractRejectsOutOfRangeSequentialOperand) { expectMalformedSequentialExpressionUnsupported( @@ -17218,8 +17686,7 @@ TEST_F(SequentialEquivalenceStrategyTests, } TEST_F(SequentialEquivalenceStrategyTests, - SequentialDesignModelExtractRejectsUnsupportedOutputFunction) { - ScopedSecBoundaryAbstraction strictSequentialModeling(false); + SequentialDesignModelExtractSkipsUnsupportedOutputFunction) { NLUniverse::create(); auto* db = NLDB::create(NLUniverse::get()); auto* primitives = @@ -17238,17 +17705,190 @@ TEST_F(SequentialEquivalenceStrategyTests, const auto extracted = SequentialDesignModel::extract(top); - EXPECT_TRUE(extracted.hasUnsupportedFeatures()); - ASSERT_FALSE(extracted.unsupportedReasons.empty()); + expectOpaqueOutputSkipWithReason( + extracted, "Unsupported Naja sequential output function"); +} + +TEST_F(SequentialEquivalenceStrategyTests, + SequentialDesignModelExtractKeepsIndependentSequentialOutput) { + NLUniverse::create(); + auto* db = NLDB::create(NLUniverse::get()); + auto* primitives = + NLLibrary::create(db, NLLibrary::Type::Primitives, NLName("prims")); + auto* library = + NLLibrary::create(db, NLLibrary::Type::Standard, NLName("designs")); + auto* model = + createIndependentSequentialOutputModel(primitives, "SEQ_GOOD_BAD"); + auto sequentialModel = SNLDesignModeling::getSequentialModel(model); + SNLDesignModeling::BooleanExpression invalidBadTransition; + invalidBadTransition.addTerm(model->getScalarTerm(NLName("D_BAD"))); + sequentialModel.states[1].nextState = std::move(invalidBadTransition); + SNLDesignModeling::setSequentialModel(model, sequentialModel); + auto* top = createIndependentSequentialOutputTop(library, "top", model); + + const auto extracted = SequentialDesignModel::extract(top); + + const auto goodOutput = findKeyByDisplayName(extracted, "good[0]"); + const auto badOutput = findKeyByDisplayName(extracted, "bad[0]"); + const auto goodState = findKeyByDisplayName(extracted, "ff0.GOOD[0]"); + const auto badState = findKeyByDisplayName(extracted, "ff0.BAD[0]"); + EXPECT_FALSE(extracted.hasUnsupportedFeatures()); + EXPECT_EQ(extracted.observedOutputs, std::vector{goodOutput}); + EXPECT_EQ(extracted.skippedObservedOutputs, std::vector{badOutput}); EXPECT_NE( - extracted.unsupportedReasons.front().find( - "Unsupported Naja sequential output function"), + std::find(extracted.stateBits.begin(), extracted.stateBits.end(), goodState), + extracted.stateBits.end()); + EXPECT_EQ(extracted.inputVarByKey.find(badState), + extracted.inputVarByKey.end()); + EXPECT_NE(extracted.nextStateExprByStateKey.find(goodState), + extracted.nextStateExprByStateKey.end()); + const auto skip = extracted.connectivitySkipInfoByKey.find(badOutput); + ASSERT_NE(skip, extracted.connectivitySkipInfoByKey.end()); + EXPECT_NE(skip->second.detail.find("pin `BAD[0]`"), std::string::npos); + EXPECT_NE(skip->second.detail.find("Invalid Naja sequential expression"), + std::string::npos); +} + +TEST_F(SequentialEquivalenceStrategyTests, + SequentialDesignModelExtractKeepsModeledOutputBesideOpaqueLatchOutput) { + NLUniverse::create(); + auto* db = NLDB::create(NLUniverse::get()); + auto* primitives = + NLLibrary::create(db, NLLibrary::Type::Primitives, NLName("prims")); + auto* library = + NLLibrary::create(db, NLLibrary::Type::Standard, NLName("designs")); + auto* model = + createIndependentSequentialOutputModel(primitives, "MIXED_FF_LATCH"); + auto sequentialModel = SNLDesignModeling::getSequentialModel(model); + sequentialModel.states.pop_back(); + sequentialModel.outputs.pop_back(); + SNLDesignModeling::setSequentialModel(model, sequentialModel); + auto* top = createIndependentSequentialOutputTop(library, "top", model); + + const auto extracted = SequentialDesignModel::extract(top); + + const auto goodOutput = findKeyByDisplayName(extracted, "good[0]"); + const auto badOutput = findKeyByDisplayName(extracted, "bad[0]"); + EXPECT_FALSE(extracted.hasUnsupportedFeatures()); + EXPECT_EQ(extracted.observedOutputs, std::vector{goodOutput}); + EXPECT_EQ(extracted.skippedObservedOutputs, std::vector{badOutput}); + const auto skip = extracted.connectivitySkipInfoByKey.find(badOutput); + ASSERT_NE(skip, extracted.connectivitySkipInfoByKey.end()); + EXPECT_NE(skip->second.detail.find("pin `BAD[0]`"), std::string::npos); + EXPECT_NE(skip->second.detail.find("Missing Naja sequential output model"), + std::string::npos); +} + +TEST_F(SequentialEquivalenceStrategyTests, + SequentialDesignModelExtractPropagatesOpaqueSequentialDependency) { + NLUniverse::create(); + auto* db = NLDB::create(NLUniverse::get()); + auto* primitives = + NLLibrary::create(db, NLLibrary::Type::Primitives, NLName("prims")); + auto* library = + NLLibrary::create(db, NLLibrary::Type::Standard, NLName("designs")); + auto* model = + createIndependentSequentialOutputModel(primitives, "SEQ_GOOD_BAD_DEP"); + auto sequentialModel = SNLDesignModeling::getSequentialModel(model); + using Operator = SNLDesignModeling::BooleanExpression::Operator; + SNLDesignModeling::BooleanExpression dependentGoodTransition; + const auto goodData = dependentGoodTransition.addTerm( + model->getScalarTerm(NLName("D_GOOD"))); + const auto badState = dependentGoodTransition.addState(1); + dependentGoodTransition.root = dependentGoodTransition.addOperation( + Operator::And, {goodData, badState}); + sequentialModel.states[0].nextState = std::move(dependentGoodTransition); + SNLDesignModeling::BooleanExpression invalidBadTransition; + invalidBadTransition.addTerm(model->getScalarTerm(NLName("D_BAD"))); + sequentialModel.states[1].nextState = std::move(invalidBadTransition); + SNLDesignModeling::setSequentialModel(model, sequentialModel); + auto* top = createIndependentSequentialOutputTop(library, "top", model); + + const auto extracted = SequentialDesignModel::extract(top); + + const auto goodOutput = findKeyByDisplayName(extracted, "good[0]"); + const auto badOutput = findKeyByDisplayName(extracted, "bad[0]"); + EXPECT_FALSE(extracted.hasUnsupportedFeatures()); + EXPECT_TRUE(extracted.observedOutputs.empty()); + EXPECT_EQ(extracted.skippedObservedOutputs.size(), 2u); + for (const auto& output : {goodOutput, badOutput}) { + const auto skip = extracted.connectivitySkipInfoByKey.find(output); + ASSERT_NE(skip, extracted.connectivitySkipInfoByKey.end()); + EXPECT_NE(skip->second.detail.find("pin `BAD[0]`"), std::string::npos); + } + EXPECT_NE( + extracted.connectivitySkipInfoByKey.at(goodOutput).detail.find( + "Naja sequential expression depends on opaque internal cell"), std::string::npos); } TEST_F(SequentialEquivalenceStrategyTests, - SequentialDesignModelExtractRejectsOutputFromAnotherPrimitive) { - ScopedSecBoundaryAbstraction strictSequentialModeling(false); + SecNetlistChecksPropagatesSequentialStateAndControlDependencies) { + NLUniverse::create(); + auto* db = NLDB::create(NLUniverse::get()); + auto* primitives = + NLLibrary::create(db, NLLibrary::Type::Primitives, NLName("prims")); + auto* library = + NLLibrary::create(db, NLLibrary::Type::Standard, NLName("designs")); + auto* model = + createIndependentSequentialOutputModel(primitives, "SEQ_DEP_COVERAGE"); + auto sequentialModel = SNLDesignModeling::getSequentialModel(model); + + auto* foreignModel = SNLDesign::create( + primitives, SNLDesign::Type::Primitive, NLName("FOREIGN_SEQ_METADATA")); + auto* foreignInput = SNLScalarTerm::create( + foreignModel, SNLTerm::Direction::Input, NLName("A")); + auto* foreignOutput = SNLScalarTerm::create( + foreignModel, SNLTerm::Direction::Output, NLName("Y")); + using Expression = SNLDesignModeling::BooleanExpression; + using Operator = Expression::Operator; + Expression nextState; + const auto data = + nextState.addTerm(model->getScalarTerm(NLName("D_GOOD"))); + const auto foreign = nextState.addTerm(foreignInput); + const auto badState = nextState.addState(1); + const auto unmappedState = nextState.addState(99); + nextState.root = nextState.addOperation( + Operator::Or, {data, foreign, badState, badState, unmappedState}); + sequentialModel.states[0].nextState = std::move(nextState); + sequentialModel.states[0].clear = Expression{}; + sequentialModel.states[0].preset = makeSequentialTermExpression( + model->getScalarTerm(NLName("D_BAD"))); + sequentialModel.outputs.push_back( + {foreignOutput, makeSequentialStateExpression(0)}); + sequentialModel.outputs.push_back( + {model->getScalarTerm(NLName("BAD")), + makeSequentialStateExpression(99)}); + SNLDesignModeling::setSequentialModel(model, sequentialModel); + auto* top = createIndependentSequentialOutputTop(library, "top", model); + + detail::ScopedDnlContextForTest dnlContext(top); + auto* dnl = dnlContext.dnl(); + const auto badStateTerm = + detail::findTermByDisplayNameForTest(dnl, "ff0.BAD[0]"); + const auto goodStateTerm = + detail::findTermByDisplayNameForTest(dnl, "ff0.GOOD[0]"); + ASSERT_TRUE(badStateTerm.has_value()); + ASSERT_TRUE(goodStateTerm.has_value()); + + const SecNetlistChecks checks(dnl); + const auto reached = checks.findTopOutputsReachedByOpaqueTerminals( + {{*badStateTerm, "opaque BAD"}, {*badStateTerm, "opaque BAD"}}, + {{naja::DNL::DNLID_MAX, *goodStateTerm}, + {*goodStateTerm, *goodStateTerm}}); + std::vector reachedNames; + for (const auto& output : reached) { + reachedNames.push_back(detail::getTerminalDisplayNameForTest( + dnl->getDNLTerminalFromID(output.topOutputTermID))); + EXPECT_EQ(output.source.detail, "opaque BAD"); + } + std::sort(reachedNames.begin(), reachedNames.end()); + EXPECT_EQ(reachedNames, + (std::vector{"bad[0]", "good[0]"})); +} + +TEST_F(SequentialEquivalenceStrategyTests, + SequentialDesignModelExtractIgnoresOutputFromAnotherPrimitive) { NLUniverse::create(); auto* db = NLDB::create(NLUniverse::get()); auto* primitives = @@ -17270,27 +17910,18 @@ TEST_F(SequentialEquivalenceStrategyTests, const auto extracted = SequentialDesignModel::extract(top); - EXPECT_TRUE(extracted.hasUnsupportedFeatures()); - ASSERT_FALSE(extracted.unsupportedReasons.empty()); - EXPECT_NE( - extracted.unsupportedReasons.front().find( - "sequential output is not present on the instance"), - std::string::npos); -} - -TEST_F(SequentialEquivalenceStrategyTests, - SequentialDesignModelExtractAbstractsStateWithoutPhysicalOutput) { - expectMissingSequentialStateOutputHandled(true); + EXPECT_FALSE(extracted.hasUnsupportedFeatures()); + EXPECT_EQ(extracted.observedOutputs.size(), 2u); + EXPECT_TRUE(extracted.skippedObservedOutputs.empty()); } TEST_F(SequentialEquivalenceStrategyTests, - SequentialDesignModelExtractRejectsStateWithoutPhysicalOutput) { - expectMissingSequentialStateOutputHandled(false); + SequentialDesignModelExtractIgnoresUnreferencedStateWithoutPhysicalOutput) { + expectUnreferencedSequentialStateWithoutOutputIgnored(); } TEST_F(SequentialEquivalenceStrategyTests, - SequentialDesignModelExtractReportsSharedScalarDataForMultiOutputPrimitive) { - ScopedSecBoundaryAbstraction strictSequentialModeling(false); + SequentialDesignModelExtractSkipsSharedScalarDataForMultiOutputPrimitive) { NLUniverse::create(); auto* db = NLDB::create(NLUniverse::get()); auto* primitives = @@ -17304,19 +17935,12 @@ TEST_F(SequentialEquivalenceStrategyTests, const auto extracted = SequentialDesignModel::extract(top); - EXPECT_TRUE(extracted.hasUnsupportedFeatures()); - ASSERT_FALSE(extracted.unsupportedReasons.empty()); - EXPECT_NE( - extracted.unsupportedReasons.front().find("multiple independent state outputs"), - std::string::npos); - for (const auto& reason : extracted.unsupportedReasons) { - EXPECT_EQ(reason.find("Missing next-state relation"), std::string::npos); - } + expectOpaqueOutputSkipWithReason( + extracted, "multiple independent state outputs"); } TEST_F(SequentialEquivalenceStrategyTests, - SequentialDesignModelExtractReportsSharedScalarDataForQAndUnrelatedOutput) { - ScopedSecBoundaryAbstraction strictSequentialModeling(false); + SequentialDesignModelExtractSkipsSharedScalarDataForQAndUnrelatedOutput) { NLUniverse::create(); auto* db = NLDB::create(NLUniverse::get()); auto* primitives = @@ -17330,19 +17954,12 @@ TEST_F(SequentialEquivalenceStrategyTests, const auto extracted = SequentialDesignModel::extract(top); - EXPECT_TRUE(extracted.hasUnsupportedFeatures()); - ASSERT_FALSE(extracted.unsupportedReasons.empty()); - EXPECT_NE( - extracted.unsupportedReasons.front().find("multiple independent state outputs"), - std::string::npos); - for (const auto& reason : extracted.unsupportedReasons) { - EXPECT_EQ(reason.find("Missing next-state relation"), std::string::npos); - } + expectOpaqueOutputSkipWithReason( + extracted, "multiple independent state outputs"); } TEST_F(SequentialEquivalenceStrategyTests, - SequentialDesignModelExtractStopsBeforeConeBuildForUnsupportedPrimitiveInfo) { - ScopedSecBoundaryAbstraction strictSequentialModeling(false); + SequentialDesignModelExtractBuildsConeThenSkipsUnsupportedPrimitiveInfo) { NLUniverse::create(); auto* db = NLDB::create(NLUniverse::get()); auto* primitives = @@ -17359,14 +17976,12 @@ TEST_F(SequentialEquivalenceStrategyTests, const auto extracted = SequentialDesignModel::extract(top); const std::string stderrOutput = testing::internal::GetCapturedStderr(); - EXPECT_TRUE(extracted.hasUnsupportedFeatures()); + expectOpaqueOutputSkipWithReason( + extracted, "multiple independent state outputs"); EXPECT_NE( stderrOutput.find("SEC diag: extract(top) collect begin"), std::string::npos); EXPECT_NE( - stderrOutput.find("SEC diag: extract(top) early unsupported exit before build"), - std::string::npos); - EXPECT_EQ( stderrOutput.find("SEC diag: extract(top) build begin"), std::string::npos); } @@ -17459,7 +18074,6 @@ TEST_F(SequentialEquivalenceStrategyTests, TEST_F(SequentialEquivalenceStrategyTests, UnsupportedReasonsFromBothDesignsAreJoined) { - ScopedSecBoundaryAbstraction strictSequentialModeling(false); NLUniverse::create(); auto* db = NLDB::create(NLUniverse::get()); auto* primitives = @@ -17481,8 +18095,7 @@ TEST_F(SequentialEquivalenceStrategyTests, } TEST_F(SequentialEquivalenceStrategyTests, - StrategyStopsBeforeSecondExtractionAndProofOnUnsupportedPrimitiveInfo) { - ScopedSecBoundaryAbstraction strictSequentialModeling(false); + StrategyStillChecksTopOutputAlignmentAfterOpaqueConeSkipping) { NLUniverse::create(); auto* db = NLDB::create(NLUniverse::get()); auto* primitives = @@ -17496,16 +18109,8 @@ TEST_F(SequentialEquivalenceStrategyTests, library, "top0", unsupportedModel, "STATE", "ALT"); auto* top1 = createDffTop(library, "top1", invModel, false, false); - ScopedEnvVar secDiag("KEPLER_SEC_DIAG", "1"); - testing::internal::CaptureStderr(); auto strategy = makeBinarySecStrategy(top0, top1); - const auto result = strategy.run(1); - const std::string stderrOutput = testing::internal::GetCapturedStderr(); - - EXPECT_EQ(result.status, SequentialEquivalenceStatus::Unsupported); - EXPECT_NE(stderrOutput.find("SEC diag: extracted design0"), std::string::npos); - EXPECT_EQ(stderrOutput.find("SEC diag: extracted design1"), std::string::npos); - EXPECT_EQ(stderrOutput.find("SEC diag: aligning inputs/outputs"), std::string::npos); + EXPECT_THROW(static_cast(strategy.run(1)), std::runtime_error); } TEST_F(SequentialEquivalenceStrategyTests, @@ -17631,7 +18236,7 @@ TEST_F(SequentialEquivalenceStrategyTests, } TEST_F(SequentialEquivalenceStrategyTests, - EquivalentOpaqueLeafDesignsReportOpaqueInternalBoundaryTerms) { + GenericOpaqueCellSkipsOnlyItsTopLevelOutput) { NLUniverse::create(); auto* db = NLDB::create(NLUniverse::get()); auto* primitives = @@ -17642,77 +18247,215 @@ TEST_F(SequentialEquivalenceStrategyTests, auto* top0 = createOpaqueBoundaryTop(library, "top0", opaqueModel); auto* top1 = createOpaqueBoundaryTop(library, "top1", opaqueModel); + const auto extracted = SequentialDesignModel::extract(top0); + const auto opaqueOutput = + findKeyByDisplayName(extracted, "opaque0.Y[0]"); + EXPECT_EQ(extracted.inputVarByKey.find(opaqueOutput), + extracted.inputVarByKey.end()); + auto strategy = makeBinarySecStrategy(top0, top1); const auto result = strategy.run(2); - auto hasRole = [&](const char* design, const char* signal, const char* role) { - return std::any_of( - result.extractedBoundaryReports.begin(), - result.extractedBoundaryReports.end(), - [&](const ExtractedBoundaryReportEntry& entry) { - return entry.design == design && entry.signal == signal && - std::find(entry.roles.begin(), entry.roles.end(), role) != - entry.roles.end(); - }); - }; + EXPECT_EQ(result.status, SequentialEquivalenceStatus::PartiallyProved); + EXPECT_EQ(result.coveredOutputs, 1u); + EXPECT_EQ(result.totalOutputs, 2u); + ASSERT_EQ(result.skippedObservedOutputs.size(), 1u); + ASSERT_EQ(result.opaqueCellSkippedOutputs.size(), 1u); + const auto& report = result.opaqueCellSkippedOutputs.front(); + EXPECT_NE(report.find("out[0]"), std::string::npos); + EXPECT_NE(report.find("opaque0"), std::string::npos); + EXPECT_NE(report.find("OPAQUE"), std::string::npos); + EXPECT_NE(report.find("Y[0]"), std::string::npos); + EXPECT_NE(report.find("no initialized combinational truth table"), + std::string::npos); +} - EXPECT_EQ(result.status, SequentialEquivalenceStatus::Equivalent); - EXPECT_TRUE(hasRole("design0", "in[0]", "top_input")); - EXPECT_TRUE(hasRole("design0", "out[0]", "top_output")); - EXPECT_TRUE(hasRole("design0", "opaque0.Y[0]", "opaque_internal_input")); - EXPECT_TRUE(hasRole("design0", "opaque0.A[0]", "opaque_internal_output")); - EXPECT_TRUE(hasRole("design1", "opaque0.Y[0]", "opaque_internal_input")); - EXPECT_TRUE(hasRole("design1", "opaque0.A[0]", "opaque_internal_output")); +TEST_F(SequentialEquivalenceStrategyTests, + OpaqueConeFollowsOnlyOutputsDependentOnReachedInput) { + NLUniverse::create(); + auto* db = NLDB::create(NLUniverse::get()); + auto* primitives = + NLLibrary::create(db, NLLibrary::Type::Primitives, NLName("prims")); + auto* library = + NLLibrary::create(db, NLLibrary::Type::Standard, NLName("designs")); + auto* opaqueModel = createOpaqueLeafModel(primitives); + auto* muxModel = NLDB0::getOrCreateMux2(2); + auto* top0 = createOpaqueIndependentOutputTop( + library, "top0", opaqueModel, muxModel); + auto* top1 = createOpaqueIndependentOutputTop( + library, "top1", opaqueModel, muxModel); + + const auto extracted = SequentialDesignModel::extract(top0); + const auto good = findKeyByDisplayName(extracted, "good[0]"); + const auto bad = findKeyByDisplayName(extracted, "bad[0]"); + EXPECT_EQ(extracted.observedOutputs, std::vector{good}); + EXPECT_EQ(extracted.skippedObservedOutputs, std::vector{bad}); + const auto skip = extracted.connectivitySkipInfoByKey.find(bad); + ASSERT_NE(skip, extracted.connectivitySkipInfoByKey.end()); + EXPECT_EQ(skip->second.origin, ConnectivitySkipOrigin::OpaqueInternal); + EXPECT_NE(skip->second.detail.find("opaque0"), std::string::npos); + + auto strategy = makeBinarySecStrategy(top0, top1); + const auto result = strategy.run(2); + EXPECT_EQ(result.status, SequentialEquivalenceStatus::PartiallyProved); + EXPECT_EQ(result.coveredOutputs, 1u); + EXPECT_EQ(result.totalOutputs, 2u); + ASSERT_EQ(result.opaqueCellSkippedOutputs.size(), 1u); + EXPECT_NE( + result.opaqueCellSkippedOutputs.front().find("bad[0]"), + std::string::npos); + EXPECT_EQ( + result.opaqueCellSkippedOutputs.front().find("good[0]"), + std::string::npos); } TEST_F(SequentialEquivalenceStrategyTests, - UnsupportedSequentialInterfacesCanBeAbstractedAsSecBoundariesByDefault) { - ScopedSecBoundaryAbstraction boundaryAbstraction(true); + SNLLogicCloudReportsMissingSequentialModelAtStrictBoundary) { NLUniverse::create(); auto* db = NLDB::create(NLUniverse::get()); auto* primitives = NLLibrary::create(db, NLLibrary::Type::Primitives, NLName("prims")); auto* library = NLLibrary::create(db, NLLibrary::Type::Standard, NLName("designs")); - auto* unsupportedModel = createNoDataSequentialModel(primitives, "SEQ_NO_D"); - auto* top0 = - createUnsupportedPrimitiveCoverageTop(library, "top0", unsupportedModel); - auto* top1 = - createUnsupportedPrimitiveCoverageTop(library, "top1", unsupportedModel); + auto* model = createNoDataSequentialModel(primitives, "SEQ_NO_MODEL"); + auto* top = createNoDataSequentialTop(library, "top", model); + + const auto skipped = + buildStrictOpaqueOutputForTest(top, "ff0.Q[0]", "out[0]"); + ASSERT_TRUE(skipped.has_value()); + EXPECT_EQ( + skipped->reason, + KEPLER_FORMAL::BuildPrimaryOutputClauses::SkippedOutputReason:: + OpaqueInternal); + EXPECT_NE(skipped->detail.find("Missing Naja sequential model"), + std::string::npos); +} + +TEST_F(SequentialEquivalenceStrategyTests, + SNLLogicCloudReportsUnsupportedLatchAtStrictBoundary) { + NLUniverse::create(); + auto* db = NLDB::create(NLUniverse::get()); + auto* library = + NLLibrary::create(db, NLLibrary::Type::Standard, NLName("designs")); + auto* top = createDirectLatchTop(library, "top", NLDB0::getDLatch()); + + const auto skipped = + buildStrictOpaqueOutputForTest(top, "latch0.Q[0]", "out[0]"); + ASSERT_TRUE(skipped.has_value()); + EXPECT_NE(skipped->detail.find( + "Naja latch sequential models are not supported by SEC"), + std::string::npos); +} + +TEST_F(SequentialEquivalenceStrategyTests, + SNLLogicCloudReportsUnusableFlipFlopOutputAtStrictBoundary) { + NLUniverse::create(); + auto* db = NLDB::create(NLUniverse::get()); + auto* library = + NLLibrary::create(db, NLLibrary::Type::Standard, NLName("designs")); + auto* top = createDffTop(library, "top", nullptr, false, false); + + const auto skipped = + buildStrictOpaqueOutputForTest(top, "ff0.Q[0]", "out[0]"); + ASSERT_TRUE(skipped.has_value()); + EXPECT_NE(skipped->detail.find("sequential output has no usable SEC model"), + std::string::npos); +} + +TEST_F(SequentialEquivalenceStrategyTests, + SNLLogicCloudStopsWhenOpaqueTerminalIsReachedInsideCone) { + NLUniverse::create(); + auto* db = NLDB::create(NLUniverse::get()); + auto* primitives = + NLLibrary::create(db, NLLibrary::Type::Primitives, NLName("prims")); + auto* library = + NLLibrary::create(db, NLLibrary::Type::Standard, NLName("designs")); + auto* opaqueModel = createOpaqueLeafModel(primitives); + auto* top = createOpaqueIndependentOutputTop( + library, "top", opaqueModel, NLDB0::getOrCreateMux2(2)); + + const auto skipped = + buildStrictOpaqueOutputForTest(top, "opaque0.Y[0]", "bad[0]"); + ASSERT_TRUE(skipped.has_value()); + EXPECT_NE(skipped->detail.find("opaque0"), std::string::npos); + EXPECT_NE(skipped->detail.find( + "no initialized combinational truth table or usable sequential model"), + std::string::npos); +} + +TEST_F(SequentialEquivalenceStrategyTests, + LateOpaqueSequentialDependencySkipsOnlyReachedOutputs) { + NLUniverse::create(); + auto* db = NLDB::create(NLUniverse::get()); + auto* primitives = + NLLibrary::create(db, NLLibrary::Type::Primitives, NLName("prims")); + auto* library = + NLLibrary::create(db, NLLibrary::Type::Standard, NLName("designs")); + auto* opaqueModel = createOpaqueLeafModel(primitives); + auto* top0 = createLateOpaqueSequentialDependencyTop( + library, "top0", opaqueModel); + auto* top1 = createLateOpaqueSequentialDependencyTop( + library, "top1", opaqueModel); + + const auto extracted = SequentialDesignModel::extract(top0); + const auto good = findKeyByDisplayName(extracted, "good[0]"); + const auto bad0 = findKeyByDisplayName(extracted, "bad0[0]"); + const auto bad1 = findKeyByDisplayName(extracted, "bad1[0]"); + EXPECT_EQ(extracted.observedOutputs, std::vector{good}); + EXPECT_EQ( + extracted.skippedObservedOutputs, + (std::vector{bad0, bad1})); + for (const auto& output : {bad0, bad1}) { + const auto skip = extracted.connectivitySkipInfoByKey.find(output); + ASSERT_NE(skip, extracted.connectivitySkipInfoByKey.end()); + EXPECT_EQ(skip->second.origin, ConnectivitySkipOrigin::OpaqueInternal); + EXPECT_NE(skip->second.detail.find("opaque0"), std::string::npos); + EXPECT_NE(skip->second.detail.find("Y[0]"), std::string::npos); + } auto strategy = makeBinarySecStrategy(top0, top1); const auto result = strategy.run(2); + EXPECT_EQ(result.status, SequentialEquivalenceStatus::PartiallyProved); + EXPECT_EQ(result.coveredOutputs, 1u); + EXPECT_EQ(result.totalOutputs, 3u); + ASSERT_EQ(result.opaqueCellSkippedOutputs.size(), 2u); + for (const auto& report : result.opaqueCellSkippedOutputs) { + EXPECT_NE(report.find("opaque0"), std::string::npos); + EXPECT_NE(report.find("Y[0]"), std::string::npos); + } +} - auto hasRole = [&](const char* design, const char* signal, const char* role) { - return std::any_of( - result.extractedBoundaryReports.begin(), - result.extractedBoundaryReports.end(), - [&](const ExtractedBoundaryReportEntry& entry) { - return entry.design == design && entry.signal == signal && - std::find(entry.roles.begin(), entry.roles.end(), role) != - entry.roles.end(); - }); - }; +TEST_F(SequentialEquivalenceStrategyTests, + UnobservedOpaqueStateChainIsExcludedFromSecModel) { + NLUniverse::create(); + auto* db = NLDB::create(NLUniverse::get()); + auto* primitives = + NLLibrary::create(db, NLLibrary::Type::Primitives, NLName("prims")); + auto* library = + NLLibrary::create(db, NLLibrary::Type::Standard, NLName("designs")); + auto* opaqueModel = + createNoDataSequentialModel(primitives, "OPAQUE_CLOCK_STATE"); + auto* top0 = createUnobservedOpaqueClockStateChainTop( + library, "top0", opaqueModel); + auto* top1 = createUnobservedOpaqueClockStateChainTop( + library, "top1", opaqueModel); + + const auto extracted = SequentialDesignModel::extract(top0); + ASSERT_FALSE(extracted.hasUnsupportedFeatures()); + EXPECT_EQ(extracted.observedOutputs.size(), 1u); + EXPECT_TRUE(extracted.skippedObservedOutputs.empty()); + EXPECT_TRUE(extracted.stateBits.empty()); + auto strategy = makeBinarySecStrategy(top0, top1); + const auto result = strategy.run(2); EXPECT_EQ(result.status, SequentialEquivalenceStatus::Equivalent); - EXPECT_FALSE(result.abstractedSequentialBoundaries.empty()); - EXPECT_TRUE(hasRole("design0", "clk[0]", "top_input")); - EXPECT_TRUE(hasRole("design0", "in[0]", "top_input")); - EXPECT_TRUE(hasRole("design0", "good[0]", "top_output")); - EXPECT_TRUE(hasRole("design0", "bad[0]", "top_output")); - EXPECT_FALSE(hasRole("design0", "ff0.Q[0]", "opaque_internal_input")); - EXPECT_FALSE(hasRole("design0", "ff0.CK[0]", "opaque_internal_output")); - EXPECT_TRUE(hasRole("design0", "ff0.Q[0]", "abstracted_sequential_state")); - EXPECT_TRUE(hasRole("design0", "ff0.CK[0]", "abstracted_sequential_observed")); - EXPECT_FALSE(hasRole("design1", "ff0.Q[0]", "opaque_internal_input")); - EXPECT_FALSE(hasRole("design1", "ff0.CK[0]", "opaque_internal_output")); - EXPECT_TRUE(hasRole("design1", "ff0.Q[0]", "abstracted_sequential_state")); - EXPECT_TRUE(hasRole("design1", "ff0.CK[0]", "abstracted_sequential_observed")); + EXPECT_EQ(result.coveredOutputs, 1u); + EXPECT_EQ(result.totalOutputs, 1u); + EXPECT_TRUE(result.skippedObservedOutputs.empty()); } TEST_F(SequentialEquivalenceStrategyTests, - UnsupportedPrimitiveInformationStillFailsEvenWithOtherCoveredOutputs) { - ScopedSecBoundaryAbstraction strictSequentialModeling(false); + SequentialCellWithoutModelSkipsOnlyItsTopLevelOutput) { NLUniverse::create(); auto* db = NLDB::create(NLUniverse::get()); auto* primitives = @@ -17728,9 +18471,63 @@ TEST_F(SequentialEquivalenceStrategyTests, auto strategy = makeBinarySecStrategy(top0, top1); const auto result = strategy.run(2); + EXPECT_EQ(result.status, SequentialEquivalenceStatus::PartiallyProved); + EXPECT_EQ(result.coveredOutputs, 1u); + EXPECT_EQ(result.totalOutputs, 2u); + ASSERT_EQ(result.skippedObservedOutputs.size(), 1u); + ASSERT_EQ(result.opaqueCellSkippedOutputs.size(), 1u); + const auto& report = result.opaqueCellSkippedOutputs.front(); + EXPECT_NE(report.find("bad[0]"), std::string::npos); + EXPECT_NE(report.find("ff0"), std::string::npos); + EXPECT_NE(report.find("SEQ_NO_D"), std::string::npos); + EXPECT_NE(report.find("Q[0]"), std::string::npos); + EXPECT_NE(report.find("Missing Naja sequential model"), std::string::npos); +} + +TEST_F(SequentialEquivalenceStrategyTests, + AllOpaqueTopLevelOutputsReturnUnsupported) { + NLUniverse::create(); + auto* db = NLDB::create(NLUniverse::get()); + auto* primitives = + NLLibrary::create(db, NLLibrary::Type::Primitives, NLName("prims")); + auto* library = + NLLibrary::create(db, NLLibrary::Type::Standard, NLName("designs")); + auto* unsupportedModel = createNoDataSequentialModel(primitives, "SEQ_NO_D"); + auto* top0 = createNoDataSequentialTop(library, "top0", unsupportedModel); + auto* top1 = createNoDataSequentialTop(library, "top1", unsupportedModel); + + auto strategy = makeBinarySecStrategy(top0, top1); + const auto result = strategy.run(2); + EXPECT_EQ(result.status, SequentialEquivalenceStatus::Unsupported); + EXPECT_EQ(result.coveredOutputs, 0u); + EXPECT_EQ(result.totalOutputs, 1u); + ASSERT_EQ(result.skippedObservedOutputs.size(), 1u); + ASSERT_EQ(result.opaqueCellSkippedOutputs.size(), 1u); + EXPECT_NE(result.reason.find("No aligned observed outputs remain"), + std::string::npos); +} + +TEST_F(SequentialEquivalenceStrategyTests, + ModeledCombinationalCellVerifiesAllOutputsWithoutOpaqueSkips) { + NLUniverse::create(); + auto* db = NLDB::create(NLUniverse::get()); + auto* primitives = + NLLibrary::create(db, NLLibrary::Type::Primitives, NLName("prims")); + auto* library = + NLLibrary::create(db, NLLibrary::Type::Standard, NLName("designs")); + auto* invModel = createInvModel(primitives); + auto* top0 = createCombinationalInvTop(library, "top0", invModel); + auto* top1 = createCombinationalInvTop(library, "top1", invModel); + + auto strategy = makeBinarySecStrategy(top0, top1); + const auto result = strategy.run(2); + + EXPECT_EQ(result.status, SequentialEquivalenceStatus::Equivalent); + EXPECT_EQ(result.coveredOutputs, 1u); + EXPECT_EQ(result.totalOutputs, 1u); EXPECT_TRUE(result.skippedObservedOutputs.empty()); - EXPECT_FALSE(result.reason.empty()); + EXPECT_TRUE(result.opaqueCellSkippedOutputs.empty()); } TEST_F(SequentialEquivalenceStrategyTests, @@ -17862,7 +18659,7 @@ TEST_F(SequentialEquivalenceStrategyTests, } TEST_F(SequentialEquivalenceStrategyTests, - UnpublishedInternalSupportIsDesignPrivateDuringSecRemap) { + UnpublishedInternalSupportIsRejectedDuringSecRemap) { SequentialDesignModel model0; SequentialDesignModel model1; const SignalKey out = makeSignalKey("privateSupportOut"); @@ -17881,13 +18678,9 @@ TEST_F(SequentialEquivalenceStrategyTests, model1.observedOutputExprByKey.emplace(out1, BoolExpr::Var(42)); auto strategy = makeBinaryExtractedSecStrategy(SecEngine::Imc); - const auto result = strategy.runExtractedModels(model0, model1, 1); - - // Same local variable ID, different extracted designs: this support is not a - // top terminal, so SEC must not equate it by name or by local BoolExpr ID. - EXPECT_EQ(result.status, SequentialEquivalenceStatus::Different); - EXPECT_EQ(result.coveredOutputs, 1u); - EXPECT_EQ(result.totalOutputs, 1u); + EXPECT_THROW( + static_cast(strategy.runExtractedModels(model0, model1, 1)), + std::runtime_error); } TEST_F(SequentialEquivalenceStrategyTests, diff --git a/test/strategies/miter/KeplerFormalCliTests.cpp b/test/strategies/miter/KeplerFormalCliTests.cpp index daf29d13..53f01536 100644 --- a/test/strategies/miter/KeplerFormalCliTests.cpp +++ b/test/strategies/miter/KeplerFormalCliTests.cpp @@ -305,18 +305,6 @@ struct ReportSkippedPOsGuard { bool oldValue_; }; -struct SecBoundaryAbstractionGuard { - SecBoundaryAbstractionGuard() - : oldValue_( - KEPLER_FORMAL::Config::getSecTreatUncomputableSeqAsBoundary()) {} - - ~SecBoundaryAbstractionGuard() { - KEPLER_FORMAL::Config::setSecTreatUncomputableSeqAsBoundary(oldValue_); - } - - bool oldValue_; -}; - struct CurrentPathGuard { CurrentPathGuard(): oldPath_(std::filesystem::current_path()) {} @@ -794,6 +782,39 @@ class KeplerFormalCliTests : public ::testing::Test { lineEnd == std::string::npos ? std::string::npos : lineEnd - start); } + static void expectSecDifferenceLogIncludesWitnessDetails( + const std::string& engine) { + const auto fixture = createDifferentSequentialNajaIfFixture(); + const auto logPath = + fixture.tmpDir / ("sec_difference_" + engine + ".log"); + const auto cfgPath = writeTempConfig( + "format: naja_if\n" + "verification: sec\n" + "sec_engine: " + engine + "\n" + "sec_encoding: dual_rail_steady\n" + "max_k: 2\n" + "input_paths:\n" + " - " + fixture.design0IfPath.string() + "\n" + " - " + fixture.design1IfPath.string() + "\n" + "log_file: " + logPath.string() + "\n"); + + EXPECT_EQ(runWithConfigFile(cfgPath), kSecCounterexampleExitCode); + ASSERT_TRUE(std::filesystem::exists(logPath)); + const auto contents = readFileContents(logPath); + EXPECT_NE(contents.find("SEC counterexample details:"), std::string::npos); + EXPECT_NE( + contents.find( + "Counterexample reaches the first bad frame at cycle 1."), + std::string::npos); + EXPECT_NE(contents.find("Input trace:"), std::string::npos); + EXPECT_NE( + contents.find("Observed output mismatches at cycle 1:"), + std::string::npos); + + std::filesystem::remove(cfgPath); + std::filesystem::remove_all(fixture.tmpDir); + } + void TearDown() override { KEPLER_FORMAL::Tree2BoolExpr::iso2boolExpr_.clear(); KEPLER_FORMAL::BoolExprCache::destroy(); @@ -1450,7 +1471,6 @@ TEST_F(KeplerFormalCliTests, ConfigXilinxPythonPrimitiveSecExample) { "verification: sec\n" "sec_engine: pdr\n" "sec_encoding: dual_rail_steady\n" - "sec_uncomputable_seq_as_boundary: false\n" "input_paths:\n" << " - " << (exampleDir / "xilinx_register_slice_mapped.v").string() << "\n - " @@ -2457,12 +2477,11 @@ TEST_F(KeplerFormalCliTests, ConfigMaxKMustBeScalar) { std::filesystem::remove(cfgPath); } -TEST_F(KeplerFormalCliTests, ConfigSecBoundaryAbstractionMustBeScalar) { +TEST_F(KeplerFormalCliTests, ConfigRemovedSecBoundaryAbstractionKeyIsRejected) { const auto cfgPath = writeTempConfig( "format: verilog\n" "verification: sec\n" - "sec_uncomputable_seq_as_boundary:\n" - " - false\n"); + "sec_uncomputable_seq_as_boundary: true\n"); EXPECT_EQ(runWithConfigFile(cfgPath), EXIT_FAILURE); std::filesystem::remove(cfgPath); } @@ -2548,7 +2567,6 @@ TEST_F(KeplerFormalCliTests, ConfigSecEngineWithoutSecFails) { } TEST_F(KeplerFormalCliTests, ConfigSecVerificationAccepted) { - SecBoundaryAbstractionGuard boundaryGuard; const auto fixture = createEquivalentSequentialNajaIfFixture(); const auto cfgPath = writeTempConfig( "format: naja_if\n" @@ -2564,7 +2582,6 @@ TEST_F(KeplerFormalCliTests, ConfigSecVerificationAccepted) { } TEST_F(KeplerFormalCliTests, ConfigSecDefaultsToDualRailEncoding) { - SecBoundaryAbstractionGuard boundaryGuard; const auto fixture = createEquivalentSequentialNajaIfFixture(); const auto logPath = fixture.tmpDir / "default_sec_encoding.log"; // Intentionally omit sec_encoding here: this is the regression that guards the @@ -2588,7 +2605,6 @@ TEST_F(KeplerFormalCliTests, ConfigSecDefaultsToDualRailEncoding) { } TEST_F(KeplerFormalCliTests, ConfigSecVerificationAcceptedWithPdrEngine) { - SecBoundaryAbstractionGuard boundaryGuard; const auto fixture = createEquivalentSequentialNajaIfFixture(); const auto cfgPath = writeTempConfig( "format: naja_if\n" @@ -2605,7 +2621,6 @@ TEST_F(KeplerFormalCliTests, ConfigSecVerificationAcceptedWithPdrEngine) { } TEST_F(KeplerFormalCliTests, ConfigSecVerificationRejectsLegacyEngine) { - SecBoundaryAbstractionGuard boundaryGuard; const auto fixture = createEquivalentSequentialNajaIfFixture(); const auto cfgPath = writeTempConfig( "format: naja_if\n" @@ -2622,7 +2637,6 @@ TEST_F(KeplerFormalCliTests, ConfigSecVerificationRejectsLegacyEngine) { } TEST_F(KeplerFormalCliTests, ConfigSecVerificationAcceptedWithKInductionEngine) { - SecBoundaryAbstractionGuard boundaryGuard; const auto fixture = createEquivalentSequentialNajaIfFixture(); const auto cfgPath = writeTempConfig( "format: naja_if\n" @@ -2639,7 +2653,6 @@ TEST_F(KeplerFormalCliTests, ConfigSecVerificationAcceptedWithKInductionEngine) } TEST_F(KeplerFormalCliTests, ConfigSecVerificationAcceptedWithImcEngine) { - SecBoundaryAbstractionGuard boundaryGuard; const auto fixture = createEquivalentSequentialNajaIfFixture(); const auto cfgPath = writeTempConfig( "format: naja_if\n" @@ -2658,26 +2671,50 @@ TEST_F(KeplerFormalCliTests, ConfigSecVerificationAcceptedWithImcEngine) { } TEST_F(KeplerFormalCliTests, - ConfigSecAbstractsUncomputableSequentialBoundariesByDefault) { - SecBoundaryAbstractionGuard boundaryGuard; + ConfigSecReportsPartialProofAndWritesOpaqueOutputReport) { const auto fixture = createUncomputableSequentialNajaIfFixture(); + const auto logPath = fixture.tmpDir / "sec_partial_opaque.log"; + const auto reportPath = fixture.tmpDir / "skipped_opaque_cells_pos.txt"; const auto cfgPath = writeTempConfig( "format: naja_if\n" "verification: sec\n" "sec_encoding: binary\n" "max_k: 2\n" + "report_skipped_pos: true\n" "input_paths:\n" " - " + fixture.design0IfPath.string() + "\n" - " - " + fixture.design1IfPath.string() + "\n"); + " - " + fixture.design1IfPath.string() + "\n" + "log_file: " + logPath.string() + "\n"); - EXPECT_EQ(runWithConfigFile(cfgPath), EXIT_SUCCESS); - EXPECT_TRUE(KEPLER_FORMAL::Config::getSecTreatUncomputableSeqAsBoundary()); + { + CurrentPathGuard currentPathGuard; + std::filesystem::current_path(fixture.tmpDir); + EXPECT_EQ(runWithConfigFile(cfgPath), kSecPartiallyProvedExitCode); + } + const auto contents = readFileContents(logPath); + EXPECT_NE(contents.find("SEC checked-output coverage: 50.00% (1/2"), + std::string::npos); + EXPECT_NE(contents.find("bad[0]:"), std::string::npos); + EXPECT_NE(contents.find("opaque internal cell"), std::string::npos); + EXPECT_NE(contents.find("SEC partially proved equivalence"), std::string::npos); + EXPECT_NE(contents.find("did not prove all observed outputs"), std::string::npos); + + ASSERT_TRUE(std::filesystem::exists(reportPath)); + const auto report = readFileContents(reportPath); + const auto entry = report.find("- bad[0]:"); + ASSERT_NE(entry, std::string::npos); + EXPECT_EQ(report.find("- bad[0]:", entry + 1), std::string::npos); + EXPECT_NE(report.find("outputs were not verified"), std::string::npos); + EXPECT_NE(report.find("ff0"), std::string::npos); + EXPECT_NE(report.find("SEQ_NO_D"), std::string::npos); + EXPECT_NE(report.find("Q[0]"), std::string::npos); + EXPECT_NE(report.find("no initialized combinational truth table"), + std::string::npos); std::filesystem::remove(cfgPath); std::filesystem::remove_all(fixture.tmpDir); } TEST_F(KeplerFormalCliTests, ConfigSecUnsupportedMismatchLogUsesUnsupportedResult) { - SecBoundaryAbstractionGuard boundaryGuard; const auto fixture = createEquivalentSequentialNajaIfFixture("ff0", "ff0", "out", "z"); const auto logPath = fixture.tmpDir / "sec_unsupported_mismatch.log"; @@ -2701,26 +2738,6 @@ TEST_F(KeplerFormalCliTests, ConfigSecUnsupportedMismatchLogUsesUnsupportedResul std::filesystem::remove_all(fixture.tmpDir); } -TEST_F(KeplerFormalCliTests, - ConfigSecCanDisableBoundaryAbstractionForUncomputableSequentials) { - SecBoundaryAbstractionGuard boundaryGuard; - const auto fixture = createEquivalentSequentialNajaIfFixture(); - const auto cfgPath = writeTempConfig( - "format: naja_if\n" - "verification: sec\n" - "sec_encoding: dual_rail_steady\n" - "max_k: 2\n" - "sec_uncomputable_seq_as_boundary: false\n" - "input_paths:\n" - " - " + fixture.design0IfPath.string() + "\n" - " - " + fixture.design1IfPath.string() + "\n"); - - EXPECT_EQ(runWithConfigFile(cfgPath), kSecProvedExitCode); - EXPECT_FALSE(KEPLER_FORMAL::Config::getSecTreatUncomputableSeqAsBoundary()); - std::filesystem::remove(cfgPath); - std::filesystem::remove_all(fixture.tmpDir); -} - TEST_F(KeplerFormalCliTests, ConfigSecIgnoresRenamedInternalState) { const auto fixture = createEquivalentSequentialNajaIfFixture("state_a", "state_b"); @@ -3157,29 +3174,17 @@ TEST_F(KeplerFormalCliTests, ConfigSecReportsPartialObservedOutputCoverage) { std::filesystem::remove_all(fixture.tmpDir); } -TEST_F(KeplerFormalCliTests, ConfigSecDifferenceLogIncludesWitnessDetails) { - const auto fixture = createDifferentSequentialNajaIfFixture(); - const auto logPath = fixture.tmpDir / "sec_difference.log"; - const auto cfgPath = writeTempConfig( - "format: naja_if\n" - "verification: sec\n" - "sec_encoding: dual_rail_steady\n" - "max_k: 2\n" - "input_paths:\n" - " - " + fixture.design0IfPath.string() + "\n" - " - " + fixture.design1IfPath.string() + "\n" - "log_file: " + logPath.string() + "\n"); +TEST_F(KeplerFormalCliTests, ConfigSecPdrDifferenceLogIncludesWitnessDetails) { + expectSecDifferenceLogIncludesWitnessDetails("pdr"); +} - EXPECT_EQ(runWithConfigFile(cfgPath), kSecCounterexampleExitCode); - ASSERT_TRUE(std::filesystem::exists(logPath)); - const auto contents = readFileContents(logPath); - EXPECT_NE(contents.find("SEC counterexample details:"), std::string::npos); - EXPECT_NE( - contents.find("Exact PDR found a defined-value counterexample at k = "), - std::string::npos); +TEST_F(KeplerFormalCliTests, + ConfigSecKInductionDifferenceLogIncludesWitnessDetails) { + expectSecDifferenceLogIncludesWitnessDetails("k_induction"); +} - std::filesystem::remove(cfgPath); - std::filesystem::remove_all(fixture.tmpDir); +TEST_F(KeplerFormalCliTests, ConfigSecImcDifferenceLogIncludesWitnessDetails) { + expectSecDifferenceLogIncludesWitnessDetails("imc"); } TEST_F(KeplerFormalCliTests, ConfigTinyRocketSecVerificationAccepted) { @@ -3313,25 +3318,18 @@ TEST_F(KeplerFormalCliTests, WriteBoundaryTermsReportFormatsEntries) { {.design = "design0", .signal = "clk[0]", .roles = {"top_input"}}, {.design = "design0", .signal = "bad[0]", - .roles = {"top_output", "opaque_internal_output", "abstracted_sequential_observed"}, + .roles = {"top_output"}, .connectivitySkip = "logical-loop connectivity: cycle"}}; writeBoundaryTermsReport(reportPath, reports); const auto content = readFileContents(reportPath); EXPECT_NE(content.find("# SEC boundary terms report"), std::string::npos); - EXPECT_NE(content.find("opaque_internal_input / opaque_internal_output"), - std::string::npos); - EXPECT_NE(content.find("abstracted_sequential_state / abstracted_sequential_observed"), - std::string::npos); EXPECT_NE(content.find("design: design0"), std::string::npos); EXPECT_NE(content.find("signal: clk[0]"), std::string::npos); EXPECT_NE(content.find("roles: [top_input]"), std::string::npos); EXPECT_NE(content.find("signal: bad[0]"), std::string::npos); - EXPECT_NE( - content.find( - "roles: [top_output, opaque_internal_output, abstracted_sequential_observed]"), - std::string::npos); + EXPECT_NE(content.find("roles: [top_output]"), std::string::npos); EXPECT_NE( content.find("connectivity_skip: logical-loop connectivity: cycle"), std::string::npos); @@ -3418,6 +3416,41 @@ TEST_F(KeplerFormalCliTests, WriteMultiClockDomainSkippedOutputsReportSkipsEmpty std::filesystem::remove_all(tempDir); } +TEST_F(KeplerFormalCliTests, WriteOpaqueCellSkippedOutputsReportFormatsEntries) { + const auto tempDir = + makeUniqueTempDir("kepler_formal_cli_opaque_cell_report"); + const auto reportPath = tempDir / "skipped_opaque_cells_pos.txt"; + + writeOpaqueCellSkippedOutputsReport( + reportPath, + {"bad[0]: design0 opaque-internal: opaque internal cell `u_opaque` " + "(model `OPAQUE`) pin `Y[0]`: no usable SEC model"}); + + const auto content = readFileContents(reportPath); + EXPECT_NE(content.find("# SEC opaque-cell skipped top-level outputs"), + std::string::npos); + EXPECT_NE(content.find("outputs were not verified"), std::string::npos); + EXPECT_NE(content.find("No free"), std::string::npos); + EXPECT_NE(content.find("bad[0]"), std::string::npos); + EXPECT_NE(content.find("u_opaque"), std::string::npos); + EXPECT_NE(content.find("OPAQUE"), std::string::npos); + EXPECT_NE(content.find("Y[0]"), std::string::npos); + EXPECT_NE(content.find("no usable SEC model"), std::string::npos); + + std::filesystem::remove_all(tempDir); +} + +TEST_F(KeplerFormalCliTests, WriteOpaqueCellSkippedOutputsReportSkipsEmptyEntries) { + const auto tempDir = + makeUniqueTempDir("kepler_formal_cli_empty_opaque_cell_report"); + const auto reportPath = tempDir / "skipped_opaque_cells_pos.txt"; + + writeOpaqueCellSkippedOutputsReport(reportPath, {}); + + EXPECT_FALSE(std::filesystem::exists(reportPath)); + std::filesystem::remove_all(tempDir); +} + TEST_F(KeplerFormalCliTests, ConfigSecFallsBackWhenLogParentCannotBeCreated) { const auto fixture = createEquivalentDesignFixture( "sv", @@ -3699,46 +3732,13 @@ TEST_F(KeplerFormalCliTests, CliRemovedKInductionAliasesAreRejectedBeforeFormat) std::filesystem::remove_all(fixture.tmpDir); } -TEST_F(KeplerFormalCliTests, CliSecBoundaryFlagAcceptedBeforeFormat) { - SecBoundaryAbstractionGuard boundaryGuard; - const auto fixture = createEquivalentSequentialNajaIfFixture(); - - EXPECT_EQ( - runWithArgs({"kepler-formal", - "-v", - "sec", - "-k", - "4", - "--sec-encoding", - "dual_rail_steady", - "--sec-uncomputable-seq-boundary", - "-naja_if", - fixture.design0IfPath.string(), - fixture.design1IfPath.string()}), - kSecProvedExitCode); - EXPECT_TRUE(KEPLER_FORMAL::Config::getSecTreatUncomputableSeqAsBoundary()); - std::filesystem::remove_all(fixture.tmpDir); -} - -TEST_F(KeplerFormalCliTests, CliNoSecBoundaryFlagAcceptedBeforeFormat) { - SecBoundaryAbstractionGuard boundaryGuard; - const auto fixture = createEquivalentSequentialNajaIfFixture(); - - EXPECT_EQ( - runWithArgs({"kepler-formal", - "-v", - "sec", - "-k", - "4", - "--sec-encoding", - "dual_rail_steady", - "--no-sec-uncomputable-seq-boundary", - "-naja_if", - fixture.design0IfPath.string(), - fixture.design1IfPath.string()}), - kSecProvedExitCode); - EXPECT_FALSE(KEPLER_FORMAL::Config::getSecTreatUncomputableSeqAsBoundary()); - std::filesystem::remove_all(fixture.tmpDir); +TEST_F(KeplerFormalCliTests, CliRemovedSecBoundaryFlagsAreRejectedBeforeFormat) { + for (const char* flag : {"--sec-uncomputable-seq-boundary", + "--no-sec-uncomputable-seq-boundary"}) { + EXPECT_EQ( + runWithArgs({"kepler-formal", "-v", "sec", flag, "-verilog"}), + EXIT_FAILURE); + } } TEST_F(KeplerFormalCliTests, CliMissingVerificationAfterFormatFails) { @@ -3802,46 +3802,13 @@ TEST_F(KeplerFormalCliTests, CliInvalidSecEncodingAfterFormatFails) { EXIT_FAILURE); } -TEST_F(KeplerFormalCliTests, CliSecBoundaryFlagAcceptedAfterFormat) { - SecBoundaryAbstractionGuard boundaryGuard; - const auto fixture = createEquivalentSequentialNajaIfFixture(); - - EXPECT_EQ( - runWithArgs({"kepler-formal", - "-naja_if", - "-v", - "sec", - "-k", - "4", - "--sec-encoding", - "dual_rail_steady", - "--sec-uncomputable-seq-boundary", - fixture.design0IfPath.string(), - fixture.design1IfPath.string()}), - kSecProvedExitCode); - EXPECT_TRUE(KEPLER_FORMAL::Config::getSecTreatUncomputableSeqAsBoundary()); - std::filesystem::remove_all(fixture.tmpDir); -} - -TEST_F(KeplerFormalCliTests, CliNoSecBoundaryFlagAcceptedAfterFormat) { - SecBoundaryAbstractionGuard boundaryGuard; - const auto fixture = createEquivalentSequentialNajaIfFixture(); - - EXPECT_EQ( - runWithArgs({"kepler-formal", - "-naja_if", - "-v", - "sec", - "-k", - "4", - "--sec-encoding", - "dual_rail_steady", - "--no-sec-uncomputable-seq-boundary", - fixture.design0IfPath.string(), - fixture.design1IfPath.string()}), - kSecProvedExitCode); - EXPECT_FALSE(KEPLER_FORMAL::Config::getSecTreatUncomputableSeqAsBoundary()); - std::filesystem::remove_all(fixture.tmpDir); +TEST_F(KeplerFormalCliTests, CliRemovedSecBoundaryFlagsAreRejectedAfterFormat) { + for (const char* flag : {"--sec-uncomputable-seq-boundary", + "--no-sec-uncomputable-seq-boundary"}) { + EXPECT_EQ( + runWithArgs({"kepler-formal", "-verilog", "-v", "sec", flag}), + EXIT_FAILURE); + } } TEST_F(KeplerFormalCliTests, CliSecPdrReportsCombinationalMismatchAtFrameZero) { diff --git a/thirdparty/naja b/thirdparty/naja index abe47a6d..c96e1b18 160000 --- a/thirdparty/naja +++ b/thirdparty/naja @@ -1 +1 @@ -Subproject commit abe47a6d0ff1c9392e08834440a5f3b50da134af +Subproject commit c96e1b18bf6e411a9ed5273eac0d4cea1c79c73f diff --git a/tools/bundle_linux_runtime_dependencies.sh b/tools/bundle_linux_runtime_dependencies.sh new file mode 100755 index 00000000..d2ed24fc --- /dev/null +++ b/tools/bundle_linux_runtime_dependencies.sh @@ -0,0 +1,108 @@ +#!/usr/bin/env bash +# Copyright 2024-2026 keplertech.io +# SPDX-License-Identifier: GPL-3.0-only + +set -euo pipefail + +if [[ $# -ne 1 ]]; then + echo "Usage: $0 " >&2 + exit 2 +fi + +stage="$(readlink -f "$1")" +lib_dir="${stage}/lib" + +if [[ ! -x "${stage}/bin/kepler-formal" ]]; then + echo "Missing runtime executable: ${stage}/bin/kepler-formal" >&2 + exit 2 +fi + +mkdir -p "${lib_dir}" + +is_host_runtime_library() { + case "$1" in + linux-vdso.so.*|ld-linux-*.so.*|libanl.so.*|libBrokenLocale.so.*|\ + libc.so.*|libdl.so.*|libgcc_s.so.*|libm.so.*|libmvec.so.*|\ + libnss_*.so.*|libpthread.so.*|libresolv.so.*|librt.so.*|\ + libstdc++.so.*|libthread_db.so.*|libutil.so.*) + return 0 + ;; + esac + return 1 +} + +declare -a queue=() +declare -a roots=() +declare -A scanned=() +dependency_pattern='^[[:space:]]*([^[:space:]]+)[[:space:]]+=>[[:space:]]+([^[:space:]]+)' + +while IFS= read -r -d '' candidate; do + roots+=("${candidate}") + queue+=("${candidate}") +done < <( + find -L "${stage}/bin" "${stage}/lib" \ + -type f \( -name kepler-formal -o -name '*.so' -o -name '*.so.*' \) \ + -print0 +) + +copied=0 +for ((index = 0; index < ${#queue[@]}; ++index)); do + target="${queue[index]}" + canonical="$(readlink -f "${target}")" + if [[ -n "${scanned[${canonical}]:-}" ]]; then + continue + fi + scanned["${canonical}"]=1 + + if ! dependencies="$(LD_LIBRARY_PATH="${lib_dir}:${LD_LIBRARY_PATH:-}" ldd "${target}" 2>&1)"; then + if [[ "${dependencies}" == *"not a dynamic executable"* ]]; then + continue + fi + echo "Failed to inspect runtime dependencies for ${target}:" >&2 + echo "${dependencies}" >&2 + exit 1 + fi + + while IFS= read -r line; do + if [[ "${line}" != *"=>"* ]]; then + continue + fi + if [[ "${line}" == *"=> not found"* ]]; then + echo "Unresolved runtime dependency for ${target}: ${line}" >&2 + exit 1 + fi + if [[ ! "${line}" =~ ${dependency_pattern} ]]; then + continue + fi + + soname="${BASH_REMATCH[1]}" + resolved="${BASH_REMATCH[2]}" + if is_host_runtime_library "${soname}"; then + continue + fi + + resolved="$(readlink -f "${resolved}")" + if [[ "${resolved}" == "${stage}/"* ]]; then + queue+=("${resolved}") + continue + fi + + destination="${lib_dir}/${soname}" + if [[ ! -e "${destination}" && ! -L "${destination}" ]]; then + cp -L -- "${resolved}" "${destination}" + chmod u+w "${destination}" + ((copied += 1)) + fi + queue+=("${destination}") + done <<< "${dependencies}" +done + +for root in "${roots[@]}"; do + if unresolved="$(LD_LIBRARY_PATH="${lib_dir}:${LD_LIBRARY_PATH:-}" ldd "${root}" 2>&1 | grep '=> not found')"; then + echo "Runtime artifact still has unresolved dependencies for ${root}:" >&2 + echo "${unresolved}" >&2 + exit 1 + fi +done + +echo "Bundled ${copied} non-system runtime libraries in ${lib_dir}."