diff --git a/CMakeLists.txt b/CMakeLists.txt index d24928f3..b0c27442 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -159,8 +159,10 @@ ExternalProject_Add(cadical_external CONFIGURE_COMMAND ${CMAKE_COMMAND} -E env "CC=${CMAKE_C_COMPILER}" "CXX=${CMAKE_CXX_COMPILER}" - "CFLAGS=${CADICAL_KITTEN_SYMBOL_PREFIX_FLAGS}" - "CXXFLAGS=${CADICAL_KITTEN_SYMBOL_PREFIX_FLAGS}" + # Match the consumer's sanitizer policy, including libc++ container + # annotations. Mixing instrumented and plain STL instantiations is unsafe. + "CFLAGS=${CADICAL_KITTEN_SYMBOL_PREFIX_FLAGS} $, >" + "CXXFLAGS=${CADICAL_KITTEN_SYMBOL_PREFIX_FLAGS} $, >" ./configure -q --no-tracing --no-contrib ${KEPLER_EXTERNAL_PIC_ARG} BUILD_COMMAND make -C build libcadical.a INSTALL_COMMAND "" diff --git a/docs/sec-clock-handling.md b/docs/sec-clock-handling.md index 106ef69e..4cf9afab 100644 --- a/docs/sec-clock-handling.md +++ b/docs/sec-clock-handling.md @@ -105,9 +105,9 @@ next_state = enable ? data_next : current_state This means clock gating is modeled as state enable behavior rather than as a new independent clock when the gate is combinational and fully modelable. -A gated clock cone that reaches a latch is opaque because SEC does not model -level-sensitive state. Latch handling and strict fallback behavior are documented in -[sec-sequential-models.md](sec-sequential-models.md). +A latch in the clock path requires event modeling to preserve transparency and +generated edges; see the [latch algorithm](sec-latch-support.md). +Without that modeling, latch-dependent observations remain opaque. ## Complex Clock Trees diff --git a/docs/sec-flags-spec.md b/docs/sec-flags-spec.md index 261d79b5..8b07d87d 100644 --- a/docs/sec-flags-spec.md +++ b/docs/sec-flags-spec.md @@ -232,9 +232,6 @@ the final output property. The YAML spelling `learn_ineternal_relations` is accepted as an alias for `learn_internal_relations`; specifying both spellings is an error. -The algorithm follows the candidate/refinement and inductive correspondence -approach in [Mishchenko et al., ICCAD 2008](https://people.eecs.berkeley.edu/~alanmi/publications/2008/iccad08_seq.pdf). -The ternary representation follows [Khasidashvili and Hanna, 2003](https://people.eecs.berkeley.edu/~alanmi/courses/2007_290N/papers/sec_intel_bmc03.pdf). The X option applies only to the internal candidate check; the existing output property and selected engine are unchanged. Turning off both switches retains the pre-learning SEC path. diff --git a/docs/sec-internal-relations.md b/docs/sec-internal-relations.md index 2ac549b1..94427b91 100644 --- a/docs/sec-internal-relations.md +++ b/docs/sec-internal-relations.md @@ -40,7 +40,7 @@ exactly as it does without learning. Counting stops at the limit, so a large design is never built in memory just to be measured. tinyrocket has about 0.9 million nodes; nangate45_black_parrot, with 666,543 candidates, exceeds the limit and would otherwise need over 13 GiB and tens of minutes. The gate is an -engineering limit, not a technique from the papers. +engineering limit. ## 4. Inductive step @@ -51,24 +51,23 @@ one transition. - **Speculative reduction.** The hypotheses are applied by literal substitution: both registers of a pair share one current-frame literal. The two sides' transitions are then encoded over the same literals, so identical - logic collapses structurally and needs no search. (Mony et al., DAC 2005; - Mishchenko et al., ICCAD 2008, section 3.2.) + logic collapses structurally and needs no search. - **Partitioning.** One-step register correspondence needs a single time frame, so the candidates are split into partitions bounded by solver variables. Every hypothesis is merged in every partition and each candidate is proved in exactly one, so splitting loses no relation. A large design is split rather - than skipped. (Mishchenko et al., section 3.3.) + than skipped. - **Variables on first use.** A partition reads a small part of the design, so its solver creates a variable only when the encoded logic first mentions a - symbol, not one per symbol per frame. This is an implementation choice, not a - technique from the papers. It lowers memory and encode time per partition. + symbol, not one per symbol per frame. This implementation choice lowers + memory and encode time per partition. - **Query.** Each partition asks whether some candidate in it can differ in the next frame. - UNSAT: all of its candidates hold under the hypotheses. - SAT: the counterexample is replayed (below). - Undecided within budget: each pair of that partition is asked separately on the same solver with its own budget, and only the pairs that stay - undecided are dropped. (Mony et al., sections 2 and 4.1.) + undecided are dropped. ## 5. Refinement by simulation @@ -76,8 +75,7 @@ A counterexample is replayed on the original transitions as one of 64 parallel patterns; the other 63 are random states that also satisfy the hypotheses. The replay runs for up to 16 steps, and every candidate seen differing on a valid pattern is dropped. One counterexample therefore refines all candidates, not -only the pairs the solver model happens to separate. (Mony et al., section -3.2.) +only the pairs the solver model happens to separate. Simulation only drops candidates. It never proves one. @@ -95,5 +93,5 @@ nothing; if the round limit is reached first, nothing is returned. reach; the output check still reports the counterexample. - Equivalent designs whose logic was rebuilt (for example synthesis netlist versus final netlist) prove few pairs: register equalities alone are often - not inductive there. Mishchenko et al. address this with signal - correspondence, which also relates internal nodes. That is not implemented. + not inductive there. Signal correspondence, which also relates internal + nodes, is not implemented. diff --git a/docs/sec-latch-implementation.md b/docs/sec-latch-implementation.md new file mode 100644 index 00000000..5756eee5 --- /dev/null +++ b/docs/sec-latch-implementation.md @@ -0,0 +1,176 @@ +# Latch Event Behavior + +This is a zero-delay Boolean model of latches, gates, and flip-flops. One +external transaction changes permitted inputs; internal events then propagate +until a complete, stable boundary is reached. Only those boundaries are +observed. The environment cannot interrupt an unfinished settling episode. +The [latch-support design](sec-latch-support.md) gives the proof obligations +and the distinction between current broad-region handling and safe local +scheduling. + +## External transactions + +An ordinary step is an input event, not a hardware clock cycle or one internal +wave. Enables can open and close between flip-flop edges, and data propagates +through an open latch without any clock edge. + +By default, a transaction may change any number of external input bits, +including none. A restriction to at most one external bit per transaction is +a different environment assumption. It does not restrict internally generated +changes or remove their possible arrival orders. Both comparison designs +receive the same external stimulus and use the same event contract. + +## Primitive behavior + +### Basic latch primitive: register plus transparent mux + +For an active-high latch without asynchronous controls, `H` is the remembered +bit, `D` the data, `E` the enable, and `Q` the visible output: + +```text +Q = E ? D : H +next(H) = Q +``` + +```mermaid +flowchart LR + D["Data D"] -->|Open| M["Transparent
mux"] + E["Enable E"] --> M + H["Abstract register
H"] -->|Closed| M + M --> Q["Output Q"] + Q -->|next H = Q| H +``` + +The register represents remembered history, not a flip-flop driven by a +hardware clock. The visible value is the mux result: open selects current +data; closed selects remembered storage. + +The actual event model composes both operations into one primitive. For each +permitted ordering, changed pins are visited once each; every visit uses the +storage and pin history left by the preceding visit: + +```mermaid +flowchart TD + A["Incoming storage
and pin history"] --> P + subgraph L["One latch primitive"] + P["Visit next
changed pin"] --> M + M["Select data
or stored value"] --> R + R["Private update:
storage and output"] + R -->|"More pins:
carry history"| P + end + R -->|Last pin| S["Stage final
storage and output"] + S --> W["Commit with
the whole wave"] + W --> N["Changed outputs
activate consumers
for next wave"] +``` + +Only the ordering's final values are published at the wave boundary; intermediate +storage updates remain part of that primitive's history. These are the same +latch equations, but this correspondence does not prove equivalence to an +arbitrary separately scheduled register/mux decomposition. Splitting the blocks +can add waves, change capture ordering, or alter pulses seen downstream. + +For example, start with `D=0, E=1, H=0`, then change data to `1` while closing +the latch. Data-first can retain `1`; close-first retains `0`. Both orders must +be considered. A unique-result claim cannot silently choose the favorable one. + +Active-low latches reverse the enable polarity. Defined asynchronous clear or +preset rules override transparency. Physical outputs may invert stored state +or combine it with current inputs, as in an integrated clock gate. Undefined +or unsupported control combinations are errors if reached. + +Gates compute their Boolean functions from the current wave's inputs. +Flip-flops instead apply their specified edge and asynchronous-control rules, +threading history through changed-pin visits; a later data visit cannot reuse +an earlier clock edge. No blanket clock-cycle update is applied to all storage. + +## Initialization and propagation + +Unspecified external inputs and storage begin as arbitrary Boolean values, +not assumed zero and not literal unknown-valued logic. Concrete restrictions +apply only when explicitly requested. Initial external levels are shared +between comparison designs; their internal storage origins are independent. + +Initialization is itself a checked settling episode: + +1. Project physical storage outputs from remembered state and the defined output + expressions. Treat auxiliary internal-net seeds as arbitrary. +2. Set previous pin values equal to current values. Starting with a high clock + does not fabricate a rising edge. +3. Force an initial evaluation: gates compute, latches apply transparency and + asynchronous rules, and flip-flops apply asynchronous rules without an + invented edge. Generated clock changes in subsequent waves are real events. +4. Require every auxiliary seed and permitted ordering to settle to the same + complete boundary for each genuine input/storage origin. Different genuine + origins may yield different boundaries; all remain represented. + +A closed, unreset latch therefore keeps its unspecified history even when +other storage is reset. Initialization is not an implicit reset sequence. + +For ordinary propagation, activated primitives read one frozen pre-wave +snapshot. Their final storage/output tuples commit together; changed nets +activate all consumers for the next wave. Independent evaluations can proceed +in parallel without their completion order choosing a capture. Each producer's +result is shared across its fanout. Intermediate pin visits are private, but +transitions between network waves are preserved. + +### Reset-cycle adapter + +A requested reset duration remains a count of complete clock cycles, not input +events. With one unambiguous source clock and one reset, the reset episode +composes already-settled event transitions: + +1. Assert reset and settle; establish a low source clock and settle. Any + alignment edge from an initially high clock is represented while reset is + active. +2. For each requested cycle, sample all non-clock/non-reset inputs. Admit them + together when simultaneous external changes are allowed; otherwise compose + one-bit transactions covering every arrival order. Hold the sampled levels + through both clock edges. +3. Drive the clock high and settle, then low and settle, completing that cycle. +4. After the final cycle, release reset and settle before observation resumes. + +The two designs share these input and ordering choices. This is a cycle-sampled +reset environment, not arbitrary asynchronous activity inside a cycle. No clock +is guessed from signal names or latch enables. Multiple clocks or resets, +ambiguous clock roots, and latch-only designs need an explicit justified +schedule; a cycle request cannot silently become an event count. Afterward, +ordinary event transactions resume with reset held inactive. + +## Regions and safety + +Current whole-design handling groups every primitive connected through +internally driven data or control nets into broad event-connected regions. +Shared read-only external inputs alone do not join regions. Feedback groups +are also identified, but their size does not supply a settling bound, and a +flip-flop does not automatically cut generated-clock or asynchronous-control +dependencies. Certification currently covers each whole broad region; this +can make large, otherwise useful designs impractical to certify. + +Safe event scheduling follows changed-net dependencies and preserves every +consumer-visible wave, including transient clock, enable, and asynchronous +control activity. Replacing broad regions with independently settled local +summaries requires a preservation argument; publishing only final region +outputs can lose consequential pulses. The latch primitive representation +does not itself establish that optimized scheduling is solved. + +Certification requires error-free progress to stability under every admitted +transaction and internal ordering, plus one complete resulting state—not just +matching visible outputs. Complete state includes stored bits and remembered +net/pin values. Initialization must satisfy the same requirements, +and the accepted boundary set must remain closed under future transactions. +Feedback is accepted only when these obligations hold; a possible infinite +internal execution is not repaired by choosing a settling execution. + +Bounded symbolic reasoning or exhaustive finite exploration can establish +these obligations. A failed bound or exhausted resources means unproved, +not necessarily oscillating. Unsupported or uncertified regions remain opaque +by default: affected outputs are excluded, not proved. A strict policy may +instead reject any opaque behavior. No troublesome event or initial origin is +discarded to manufacture success. + +## Limits + +This contract does not model propagation delays, setup/hold violations, +metastability, unknown/high-impedance logic, multiple drivers, or arbitrary +hardware/HDL scheduling. Broad-region certification remains a coverage and +scalability limitation; safe local reductions require additional justification. diff --git a/docs/sec-latch-support.md b/docs/sec-latch-support.md new file mode 100644 index 00000000..18afaf9c --- /dev/null +++ b/docs/sec-latch-support.md @@ -0,0 +1,248 @@ +# Latch Support Algorithm + +This is a Boolean event model for latch behavior and sequential equivalence. +It describes a conditional method, not a guarantee that every latch circuit +settles or can be reduced. The [behavior guide](sec-latch-implementation.md) +contains the primitive diagrams and event examples. + +## 1. Approach + +1. Represent each latch as remembered storage plus a transparent mux. +2. Propagate changes through connected gates and storage elements. +3. Identify feedback loops and event-dependent regions. +4. Prove that every allowed propagation episode settles within a finite bound. +5. Prove its final retained state is independent of internal ordering choices. +6. Unfold those internal rounds into one boundary-to-boundary transition. +7. Compare both designs under the same external stimulus. + +Phase reduction is optional and comes afterward. This document specifies the +model and its proof obligations, not a completed end-to-end proof of the +implementation. + +## 2. Latch and event behavior + +For an active-high latch with data D, enable E, remembered bit H, and output Q: + +```text +Q = E ? D : H +next(H) = Q +``` + +H is abstract storage, not a flip-flop connected to the hardware clock. +The output follows data while open and retains history while closed. +Reset and preset follow the element's declared priority. + +The block is evaluated as one primitive. Combining its register and mux does +not change these equations. Splitting them into independently scheduled +elements can change event timing and requires a separate equivalence argument. + +An external transaction changes permitted inputs at a settled boundary. +Their new values remain fixed until internal propagation finishes. Multiple +inputs may change together unless explicitly restricted. A formal round is +not a hardware clock tick or a chosen physical sampling interval. + +Each round: + +1. Freeze current signals, previous signals, and stored values. +2. Evaluate elements activated by changed inputs. +3. Gates evaluate their Boolean functions. Storage elements process every + permitted order of their changed input pins, retaining storage between visits. +4. Latches apply transparency; flip-flops capture only on the appropriate + modeled edge, subject to asynchronous controls. +5. Publish each element's final output/storage tuple together with the others. +6. Activate receivers of changed signals and repeat. + +Intermediate storage updates within one element's pin ordering are retained; +only its final outputs are published for that round. Changes between rounds +remain visible to downstream elements. This primitive granularity is part of +the model, not a claim to reproduce arbitrary physical glitches. + +```mermaid +flowchart TD + A["External
transaction"] --> E["Evaluate
active elements"] + E --> C["Commit together"] + C -->|Signal changes| E + C -->|"No pending work
and no error"| B["Settled observation"] +``` + +A simultaneous data change and latch closure can retain either old or new +data, depending on event order. Do not silently choose the favorable result. + +## 3. Starting state and reset + +Unspecified inputs and storage are arbitrary Boolean values chosen once, not +repeatedly resampled or forced to zero. They are not literal unknown-valued +signals. Matching external stimulus is shared across the designs; internal +storage is independent unless an explicit relation constrains it. + +Initially project remembered storage to its outputs, then force an evaluation. +Latches apply transparency and asynchronous controls; flip-flops retain state +unless asynchronous controls apply. No clock edge is invented merely because +a clock starts high. Subsequent generated edges are processed normally. + +Auxiliary initial values on other internal signals must not determine the +retained result. For each genuine starting input/storage choice, prove settling +and independence from those auxiliary values and permitted event orders. +This Boolean startup convention is an adaptation, not a universal hardware rule. + +Reset is applied only when requested. Reset cycles mean hardware clock cycles, +not propagation rounds. A cycle schedule must represent assertion, clock edges, +intervening settling, and release. See the +[reset-cycle behavior](sec-latch-implementation.md#reset-cycle-adapter). + +## 4. Feedback loops and regions + +A loop is a directed path returning to itself through latches and logic. +Find these paths using both data and control dependencies. + +- An open latch feeding itself unchanged keeps its previous value. The equation + Q = Q alone loses that history. +- An open latch with inverting feedback can oscillate. +- A closed latch can break a loop. To classify a region as inactive, prove + that every feedback cycle is broken under the allowed conditions. +- A snapshot with no active loop does not prove settling if controls can + change during propagation. + +A scheduling region is not necessarily a feedback loop: it may contain several +loops and connecting logic. Flip-flop data paths can supply boundaries +when capture cannot occur inside an episode; generated clocks and asynchronous +controls cannot be cut without justification. + +If one region briefly raises another latch's enable, sending only the final +low level loses the capture. Preserve that boundary event sequence, enlarge +the region, or prove those intermediate events irrelevant. + +Merging all connected elements avoids such interfaces but can create enormous +regions. A failure then affects every observation retained in that region. +Finer regions require valid event interfaces; connectivity alone does not prove +they may settle independently. + +## 5. Proving and unfolding settling + +Retain the complete state needed for future behavior: storage, current and +previous signals, pending activity, and errors. A state is settled only when +no modeled work remains. + +Before accepting a bound, establish: + +- **Valid starts:** startup includes every admitted initial condition and + establishes a boundary invariant B. +- **Complete entries:** every B-state and allowed transaction has an admission + successor, and Entry includes every admission outcome. Establish this + separately from the bounded query below. +- **Total behavior:** every allowed transaction and internal state has a + successor. Undefined behavior becomes an explicit persistent error, not + a missing execution. +- **Stable padding:** a settled state repeats unchanged for the rest of its + episode. Errors persist and never count as settled. +- **Closure:** completed episodes starting in B return to B. + +For candidate bound K, ask whether any admitted entry and permitted internal +ordering can remain unsettled after K rounds: + +```text +Entry(x0, u) +AND T(x0, x1, u) AND ... AND T(x[K-1], xK, u) +AND NOT Settled(xK) +``` + +Here u is held external stimulus; T is one complete internal round. +Proving this formula impossible establishes the bound. +Keeping errors alive prevents short failing executions from disappearing before K. + +A finite state space alone does not establish convergence. A reachable cycle +outside settled states permits endless propagation. If no such cycle exists, +the longest nonsettled path gives a bound; exhaustive exploration can still +be impractical. Alternatively, test candidate bounds or prove a decreasing +ranking measure. + +A failed candidate may need more rounds; resource exhaustion proves neither +convergence nor oscillation. An apparent failure from an over-approximation +must be shown reachable before calling it a circuit defect. + +### Unique retained result + +Settling alone is insufficient. Run two copies from the same pre-transaction +state and stimulus, with independent admission and internal choices. Require: + +```text +B(q) AND Allowed(u) AND Episode(q, u, a) AND Episode(q, u, b) +IMPLIES a = b +``` + +Compare the complete retained state, not only current outputs: a later input +may expose hidden differences. Startup needs the analogous check with genuine +initial values shared and auxiliary choices independent. + +If uniqueness fails or remains unproved, this deterministic approach does not +support the affected behavior. It must not align favorable choices between designs. + +### Build once, reuse each transition + +After proving the bound and uniqueness, replace one propagation episode with +K chained copies of its internal-round relation. Early completion uses unchanged +padding. Build this chain once and reuse it for successive external transactions. + +Why this is exact under the stated conditions: every permitted episode finishes +by K and can be padded without changing its result; every unfolded execution +therefore corresponds to a permitted completed episode. Closure extends that +argument across transaction sequences. + +The bound concerns complete episodes. Combining independently guessed local +bounds by a maximum or sum is not a proof of the whole episode. + +## 6. Local scheduling and parallel evaluation + +A safe scheduling reduction preserves the reference round number: + +1. Each region reads only complete inputs and history from the same round. +2. Each predecessor supplies an update or an explicit unchanged indication; + silence is not evidence that nothing changed. +3. Regions publish updates for the next round, preserving boundary transitions, + shared choices, and their effects on clocks, enables, and resets. +4. Declare global settling only when all pending work is complete. + +Independent evaluations may run together. Evaluation order must not become +circuit behavior. By induction, equal complete inputs at one round produce +the same permitted updates at the next. + +Never mix an old value on one input with a new value from another round: +doing so can invent a pulse at a reconvergent gate. Likewise, one producer's +choice must remain the same at all its receivers. + +This allows local scheduling without requiring each region to settle privately. +Skipping rounds, publishing only endpoints, or reordering dependent events +needs stronger preservation arguments. + +## 7. Equivalence and unsupported behavior + +Give both designs the same allowed external transactions and specified +initial/reset relation. Compare corresponding settled observations, even when +the designs need different numbers of internal rounds. + +Two obligations remain separate: + +- Every admitted episode completes. +- Required outputs agree at the corresponding boundaries. + +Agreement only when both sides finish can pass vacuously if one never finishes. +Do not discard that execution. Without reset, do not assume independent +uninitialized storage happens to agree. + +Unsupported or unproved behavior remains opaque, with affected observations +excluded and explicitly reported—not proved equivalent or replaced by shared +arbitrary values. Dependencies include controls as well as data. An optional +strict policy stops on any opacity; it is off by default. + +This method does not cover arbitrary delays, metastability, unrestricted input +changes during settling, undefined controls, or general unknown/multi-driver +semantics. Large bounds may be impractical even when behavior is valid. + +## 8. Optional phase reduction + +Only after defining correct transitions, look for deterministic periodic state +signals and use them to reduce repeated phases. Preserve residual gating, +capture effects, and the observation boundary. + +This is not one phase per latch, and internal round numbers are not automatically +hardware clock phases. Failure to find a period leaves the original model intact. diff --git a/docs/sec-reset-bootstrap.md b/docs/sec-reset-bootstrap.md index 77e4ff4f..00c6884f 100644 --- a/docs/sec-reset-bootstrap.md +++ b/docs/sec-reset-bootstrap.md @@ -4,6 +4,10 @@ SEC reset bootstrap constrains user-named top-level reset inputs before the normal SEC property is checked. Use it for designs whose state is initialized by a reset sequence rather than by explicit initial values. +With latch events, a reset cycle still means a clock cycle, with full settling +between its edges. No reset or fixed initial values are assumed without a request. +See the [reset-cycle behavior](sec-latch-implementation.md#reset-cycle-adapter). + ## YAML ```yaml diff --git a/docs/sec-sequential-models.md b/docs/sec-sequential-models.md index b8f69023..b04f84fd 100644 --- a/docs/sec-sequential-models.md +++ b/docs/sec-sequential-models.md @@ -30,21 +30,17 @@ eligible unless their own dependency cones reach an opaque terminal. ## Latches -A latch is level-sensitive. For an active-high latch, its behavior is: +A latch follows data while open and holds its remembered value H while closed: ```text -next_q = enable ? data : q +Q = enable ? data : H +next(H) = 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. Every latch output is opaque, including latches used in clock-gating -structures, and any requested top-level output whose cone reaches it is -skipped. SEC does not infer latch behavior from cell or pin names. +These updates use internal events, not only flip-flop clock edges. +Propagation, feedback, and unsupported cases are explained in the +[latch algorithm](sec-latch-support.md) and [behavior guide](sec-latch-implementation.md). +Without event modeling, latch-dependent observations remain opaque. ## Opaque Outputs diff --git a/regress/run_python_regress.py b/regress/run_python_regress.py index 935c1fa1..0f89ba54 100644 --- a/regress/run_python_regress.py +++ b/regress/run_python_regress.py @@ -102,7 +102,9 @@ def run(*command): run("cmake", "-S", ROOT, "-B", consumer, *common, "-DBUILD_KEPLER_PYTHON=ON", "-DENABLE_UNIT_TESTS=ON") run("cmake", "--build", consumer, "--target", "kepler_formal_native", - "kepler-borrowed-native-tests", "--parallel", args.jobs) + "kepler-borrowed-native-tests", "kepler-borrowed-policy-tests", + "kepler-borrowed-latch-options-tests", "kepler-borrowed-latch-tests", + "--parallel", args.jobs) run("cmake", "--install", consumer, "--component", "python") if sys.platform == "darwin": # The SDK signs the build artifact. CMake then adjusts its @@ -130,7 +132,7 @@ def run(*command): """, stage) run("ctest", "--test-dir", consumer, "--output-on-failure", - "-R", "^kepler-formal-borrowed-native-tests$") + "-R", "^kepler-formal-borrowed-.*-tests$") # CTest's Python fixture replaces PYTHONPATH. Test the installed # sibling packages directly instead, including the real example. run(python, "-m", "unittest", "discover", "-v", "-s", ROOT / "test/python") diff --git a/src/bin/BUILD.bazel b/src/bin/BUILD.bazel index 9e3ea3a6..de046f4c 100644 --- a/src/bin/BUILD.bazel +++ b/src/bin/BUILD.bazel @@ -23,20 +23,56 @@ genrule( cmd = "cp $(location @naja//src/nl/python/naja_wrapping:naja.so) $@", ) +# Share the existing driver compilation inputs with in-process CLI tests without +# changing the executable's Python data files or shared-runtime link contract. +_DRIVER_SRCS = [ + "KeplerFormal.cpp", + "CppDriver.cpp", + "Btor2ExportConfig.cpp", + "KeplerFormalUtils.cpp", + "LatchEventConfig.cpp", + "LibertyLatchModels.cpp", + "RunResult.cpp", +] + +_DRIVER_HDRS = [ + "Btor2ExportConfig.h", + "KeplerFormalDriver.h", + "KeplerFormalUtils.h", + "LatchEventConfig.h", + "LibertyLatchModels.h", + "RunResult.h", +] + +_DRIVER_DEPS = [ + ":kepler_version", + "//src/config:kepler_config", + "//src/sec:kepler_sec", + "//src/scope:scope_extraction", + "//src/strategies:formal_strategies", + "//src/utils:kepler_formal_utils", + "@kissat", + "@naja", + "@naja//:naja_extra_headers", + "@spdlog", + "@yaml-cpp", + "@zlib//:zlib", +] + +cc_library( + name = "kepler_formal_test_driver", + testonly = True, + srcs = _DRIVER_SRCS, + hdrs = _DRIVER_HDRS, + includes = ["."], + copts = NAJA_HEADER_COPTS, + visibility = ["//test/strategies/miter:__pkg__"], + deps = _DRIVER_DEPS, +) + cc_binary( name = "kepler-formal", - srcs = [ - "KeplerFormal.cpp", - "CppDriver.cpp", - "Btor2ExportConfig.cpp", - "Btor2ExportConfig.h", - "KeplerFormalDriver.h", - "KeplerFormalMain.cpp", - "KeplerFormalUtils.h", - "KeplerFormalUtils.cpp", - "RunResult.h", - "RunResult.cpp", - ], + srcs = _DRIVER_SRCS + _DRIVER_HDRS + ["KeplerFormalMain.cpp"], copts = NAJA_HEADER_COPTS, data = [":naja_python_module"], dynamic_deps = ["@naja//src/nl/python/naja_wrapping:naja_runtime"], @@ -45,17 +81,5 @@ cc_binary( # flags. The hermetic toolchain links libc++ statically by default, # which supersedes the former -static-libstdc++/-static-libgcc. visibility = ["//visibility:public"], - deps = [ - ":kepler_version", - "//src/config:kepler_config", - "//src/sec:kepler_sec", - "//src/scope:scope_extraction", - "//src/strategies:formal_strategies", - "//src/utils:kepler_formal_utils", - "@kissat", - "@naja", - "@naja//:naja_extra_headers", - "@spdlog", - "@yaml-cpp", - ], + deps = _DRIVER_DEPS, ) diff --git a/src/bin/CMakeLists.txt b/src/bin/CMakeLists.txt index 485efae1..be6863a4 100644 --- a/src/bin/CMakeLists.txt +++ b/src/bin/CMakeLists.txt @@ -52,8 +52,11 @@ function(configure_kepler_formal_driver target) target_link_libraries(${target} PUBLIC ${KEPLER_FORMAL_DRIVER_LIBRARIES}) endfunction() -add_library(kepler_formal_core STATIC KeplerFormal.cpp Btor2ExportConfig.cpp) +find_package(ZLIB REQUIRED) +add_library(kepler_formal_core STATIC KeplerFormal.cpp Btor2ExportConfig.cpp + LibertyLatchModels.cpp LatchEventConfig.cpp) configure_kepler_formal_driver(kepler_formal_core) +target_link_libraries(kepler_formal_core PRIVATE ZLIB::ZLIB) add_library(kepler_formal_driver STATIC CppDriver.cpp) target_link_libraries(kepler_formal_driver PUBLIC kepler_formal_core naja_snl_pyloader) diff --git a/src/bin/KeplerFormal.cpp b/src/bin/KeplerFormal.cpp index 1af7988d..b5fd9654 100644 --- a/src/bin/KeplerFormal.cpp +++ b/src/bin/KeplerFormal.cpp @@ -44,6 +44,8 @@ #include "SNLUtils.h" #include "ScopeExtraction.h" #include "Btor2ExportConfig.h" +#include "LatchEventConfig.h" +#include "LibertyLatchModels.h" #include "Config.h" #include "DesignBoundary.h" #include "KeplerFormalDriver.h" @@ -73,15 +75,22 @@ static void print_usage(const char* prog) { " [--verilog_design1_top ] [--verilog_design2_top ] [--liberty ...] [-v ] [-k ] [--sec-engine ] [--sec-encoding ] [--learn-internal-relations ] [--allow-x-equality-in-internal-relations ] [--sec-reset-cycles ] [--sec-reset-port ...] " "[--allow-boundary-mismatch] [--compact] " "[--set-as-boundary ]... " - "[--report-skipped-pos] | " + "[--report-skipped-pos] [--error-on-opaque] | " "-systemverilog/-sv [--sv_design1_flist ] [--sv_design1_top ] " "[--sv_design2_flist ] [--sv_design2_top ] [-v ] [-k ] [--sec-engine ] [--sec-encoding ] [--learn-internal-relations ] [--allow-x-equality-in-internal-relations ] [--sec-reset-cycles ] [--sec-reset-port ...] " "[--design1 ] [--design2 ] " "[--allow-boundary-mismatch] [--compact] " "[--set-as-boundary ]... " - "[--report-skipped-pos] " + "[--report-skipped-pos] [--error-on-opaque] " "[--dump-btor2 ] [--dump-only] (BTOR2 export requires SEC)", prog); + SPDLOG_INFO("Latch support is on by default (--no-latch_support disables it). Optional Boolean event SEC settings: [--sec-latch-events ] " + "[--sec-latch-initial-inputs <0|1>] [--sec-latch-initial-storage <0|1>] " + "[--sec-latch-workers ] [--sec-latch-max-waves ] " + "[--sec-latch-max-states ] [--sec-latch-max-transactions ] " + "[--sec-latch-max-nodes ] [--sec-latch-sat-conflicts ] [--sec-latch-sat-decisions ]. " + "Input changes default to any; unspecified starting inputs and storage remain symbolic. " + "Normal event-mode steps are settled external events; sec_reset counts clock cycles."); // LCOV_EXCL_START } // LCOV_EXCL_STOP @@ -486,6 +495,8 @@ static bool validateConfigKeys(const YAML::Node& cfg) { "learn_ineternal_relations", "allow_x_equality_in_internal_relations", "sec_reset", + "latch_support", + "sec_latch_events", "btor2_export", "btor2_export_path", "dump_only", @@ -507,6 +518,7 @@ static bool validateConfigKeys(const YAML::Node& cfg) { "dump_cnf_path", "compact_mode", "report_skipped_pos", + "error_on_opaque", "solver", "sv_design1_flist", "sv_design2_flist", @@ -1240,6 +1252,7 @@ static int KeplerFormalMainImpl( KEPLER_FORMAL::SEC::SecEncoding::DualRailSteady; KEPLER_FORMAL::SEC::SecResetSpec secResetSpec; KEPLER_FORMAL::Btor2ExportConfig btor2ExportConfig; + KEPLER_FORMAL::LatchEventConfig latchEventConfig; bool secEngineExplicit = false; bool secEncodingExplicit = false; KEPLER_FORMAL::SEC::InternalRelationOptions internalRelationOptions; @@ -1270,11 +1283,13 @@ static int KeplerFormalMainImpl( bool compactMode = false; bool allowBoundaryMismatch = false; bool reportSkippedPOs = false; + bool errorOnOpaque = false; bool verilogPreprocessing = false; std::string dumpCnfPath; std::string dumpPoCnfPath; KEPLER_FORMAL::Config::setReportSkippedPOs(false); + KEPLER_FORMAL::Config::setErrorOnOpaque(false); for (int i = 1; i < argc; ++i) { std::string a = argv[i]; @@ -1428,6 +1443,10 @@ static int KeplerFormalMainImpl( SPDLOG_CRITICAL("Invalid BTOR2 export config: {}", btor2ExportError); return EXIT_FAILURE; } + if (!latchEventConfig.parseYaml(cfg, btor2ExportError)) { + SPDLOG_CRITICAL("Invalid latch event config: {}", btor2ExportError); + return EXIT_FAILURE; + } // input_paths if (cfg["input_paths"]) { @@ -1520,6 +1539,14 @@ static int KeplerFormalMainImpl( allowBoundaryMismatch = cfg["allow-boundary-mismatch"].as(); } + if (cfg["error_on_opaque"]) { + if (!cfg["error_on_opaque"].IsScalar()) { + SPDLOG_CRITICAL("error_on_opaque must be a boolean scalar"); + return EXIT_FAILURE; + } + errorOnOpaque = cfg["error_on_opaque"].as(); + } + // report_skipped_pos if (cfg["report_skipped_pos"] && cfg["report_skipped_pos"].IsScalar()) { // LCOV_EXCL_START @@ -1590,6 +1617,21 @@ static int KeplerFormalMainImpl( int parseStart = 1; while (parseStart < argc) { std::string arg = argv[parseStart]; + std::string latchError; + const auto latchArgument = latchEventConfig.parseArgument(argc, argv, parseStart, latchError); + if (latchArgument == KEPLER_FORMAL::LatchEventConfig::ArgumentResult::Error) { + SPDLOG_CRITICAL("{}", latchError); + return EXIT_FAILURE; + } + if (latchArgument == KEPLER_FORMAL::LatchEventConfig::ArgumentResult::Parsed) { + ++parseStart; + continue; + } + if (arg == "--error-on-opaque") { + errorOnOpaque = true; + ++parseStart; + continue; + } std::string btor2ExportError; const auto exportArgument = btor2ExportConfig.parseArgument( argc, argv, parseStart, btor2ExportError); @@ -1803,6 +1845,13 @@ static int KeplerFormalMainImpl( // LCOV_EXCL_START for (int i = parseStart; i < argc; ++i) { std::string arg = argv[i]; + std::string latchError; + const auto latchArgument = latchEventConfig.parseArgument(argc, argv, i, latchError); + if (latchArgument == KEPLER_FORMAL::LatchEventConfig::ArgumentResult::Error) { + SPDLOG_CRITICAL("{}", latchError); + return EXIT_FAILURE; + } + if (latchArgument == KEPLER_FORMAL::LatchEventConfig::ArgumentResult::Parsed) continue; std::string btor2ExportError; const auto exportArgument = btor2ExportConfig.parseArgument( argc, argv, i, btor2ExportError); @@ -1988,6 +2037,10 @@ static int KeplerFormalMainImpl( continue; // LCOV_EXCL_STOP } + if (arg == "--error-on-opaque") { + errorOnOpaque = true; + continue; + } // LCOV_EXCL_START if (arg == "--sv_design1_flist" || arg == "--sv_design2_flist" || arg == "--verilog_design1_top" || arg == "--verilog_design2_top" || @@ -2165,6 +2218,12 @@ static int KeplerFormalMainImpl( SPDLOG_CRITICAL("Invalid BTOR2 export options: {}", btor2ExportError); return EXIT_FAILURE; } + if (!latchEventConfig.validate(verificationMode == VerificationMode::SEC, + secResetExplicit, !boundaryPairs.empty(), btor2ExportError)) { + SPDLOG_CRITICAL("Invalid latch event options: {}", btor2ExportError); + return EXIT_FAILURE; + } + KEPLER_FORMAL::SEC::LATCH::ScopedSupportOptions latchEventScope(latchEventConfig.options()); if (verificationMode == VerificationMode::LEC && secMaxKExplicit) { // LCOV_EXCL_START SPDLOG_CRITICAL("max_k/-k is only supported with SEC verification"); @@ -2187,6 +2246,10 @@ static int KeplerFormalMainImpl( SPDLOG_CRITICAL("Internal relation options are only supported with SEC verification"); return EXIT_FAILURE; } + if (verificationMode == VerificationMode::LEC && errorOnOpaque) { + SPDLOG_CRITICAL("error_on_opaque/--error-on-opaque is only supported with SEC verification"); + return EXIT_FAILURE; + } if (verificationMode == VerificationMode::LEC && secResetExplicit) { SPDLOG_CRITICAL("sec_reset/--sec-reset-* is only supported with SEC verification"); return EXIT_FAILURE; @@ -2274,6 +2337,7 @@ static int KeplerFormalMainImpl( auto solverType = KEPLER_FORMAL::Config::getSolverType(); KEPLER_FORMAL::Config::setReportSkippedPOs(reportSkippedPOs); + KEPLER_FORMAL::Config::setErrorOnOpaque(errorOnOpaque); const char* solverName = solverType == KEPLER_FORMAL::Config::SolverType::KISSAT ? "KISSAT" @@ -2291,6 +2355,12 @@ static int KeplerFormalMainImpl( SPDLOG_INFO("SEC internal relations: learn={} allow_x_equality={}", internalRelationOptions.learnInternalRelations, internalRelationOptions.allowXEqualityInInternalRelations); + if (const auto& latch = latchEventConfig.options(); latch.hasEventContract()) { + SPDLOG_INFO("SEC latch_support: enabled; Boolean external events={}; initial_inputs={}; initial_storage={}; bounds count events, not clock cycles", + latch.singleInputChange ? "single" : "any", + latch.initialInputs ? (*latch.initialInputs ? "1" : "0") : "unspecified", + latch.initialStorage ? (*latch.initialStorage ? "1" : "0") : "unspecified"); + } if (secResetSpec.enabled()) { SPDLOG_INFO("SEC reset bootstrap: {} cycle(s)", secResetSpec.cycles); for (const auto& port : secResetSpec.ports) { @@ -2530,7 +2600,9 @@ static int KeplerFormalMainImpl( std::filesystem::path libraryPath(libraryFile); SPDLOG_INFO("Loading library file: {}", libraryFile); SNLLibertyConstructor constructor(primitivesLibrary); - constructor.construct(libraryPath); + if (verificationMode == VerificationMode::SEC && latchEventConfig.options().enabled) + KEPLER_FORMAL::constructLibertyWithLatchModels(primitivesLibrary, libraryPath); + else constructor.construct(libraryPath); } for (const auto& pythonFile : pythonFiles) { // LCOV_EXCL_START @@ -3343,6 +3415,7 @@ int runKeplerFormal(int argc, char** argv, RunResult& result, const auto previousSolver = Config::getSolverType(); const bool previousReportSkippedPOs = Config::getReportSkippedPOs(); + const bool previousErrorOnOpaque = Config::getErrorOnOpaque(); const auto previousDefaultLogger = spdlog::default_logger(); const auto previousNamedLogger = spdlog::get("kepler_formal_main_logger"); const auto previousMiterLogger = spdlog::get("miter_logger"); @@ -3351,6 +3424,7 @@ int runKeplerFormal(int argc, char** argv, RunResult& result, struct RunStateGuard { Config::SolverType solver; bool reportSkippedPOs; + bool errorOnOpaque; std::shared_ptr defaultLogger; std::shared_ptr namedLogger; std::shared_ptr miterLogger; @@ -3359,6 +3433,7 @@ int runKeplerFormal(int argc, char** argv, RunResult& result, cleanupKeplerFormalState(); Config::setSolverType(solver); Config::setReportSkippedPOs(reportSkippedPOs); + Config::setErrorOnOpaque(errorOnOpaque); const auto restoreNamedLogger = []( const char* name, const std::shared_ptr& @@ -3378,6 +3453,7 @@ int runKeplerFormal(int argc, char** argv, RunResult& result, } stateGuard{ previousSolver, previousReportSkippedPOs, + previousErrorOnOpaque, previousDefaultLogger, previousNamedLogger, previousMiterLogger, @@ -3385,6 +3461,7 @@ int runKeplerFormal(int argc, char** argv, RunResult& result, Config::setSolverType(Config::SolverType::KISSAT); Config::setReportSkippedPOs(false); + Config::setErrorOnOpaque(false); const int rc = runKeplerFormalWorkflow(argc, argv, result, primitiveLoader); result.exitCode = rc; if (rc != EXIT_SUCCESS && result.status == RunStatus::Error && diff --git a/src/bin/LatchEventConfig.cpp b/src/bin/LatchEventConfig.cpp new file mode 100644 index 00000000..3d8808f3 --- /dev/null +++ b/src/bin/LatchEventConfig.cpp @@ -0,0 +1,120 @@ +// Copyright 2026 keplertech.io +// SPDX-License-Identifier: Apache-2.0 +#include "LatchEventConfig.h" +#include +#include +#include +#include +#include + +namespace KEPLER_FORMAL { +namespace { +bool number(const std::string& text, size_t& value) { + if (text.empty()) return false; + const auto parsed = std::from_chars(text.data(), text.data() + text.size(), value); + return parsed.ec == std::errc{} && parsed.ptr == text.data() + text.size(); +} +} +bool LatchEventConfig::set(const std::string& key, const std::string& value, std::string& error) { + options_.explicitConfiguration = true; + if (key == "input_changes") { + if (value == "any" || value == "single") { + options_.singleInputChange = value == "single"; + return true; + } + error = "sec_latch_events.input_changes must be any or single"; + return false; + } + if (key == "initial_inputs" || key == "initial_storage") { + if (value != "0" && value != "1") { + error = "sec_latch_events." + key + " must explicitly be Boolean 0 or 1"; + return false; + } + (key == "initial_inputs" ? options_.initialInputs : options_.initialStorage) = value == "1"; + return true; + } + size_t count = 0; + if (!number(value, count)) { + error = "sec_latch_events." + key + " must be a nonnegative integer"; + return false; + } + if (key == "workers" && count <= size_t(std::numeric_limits::max())) options_.workers = count; + else if (key == "max_waves" && count) options_.limits.maxWaves = count; + else if (key == "max_states" && count) options_.limits.maxBoundaryStates = count; + else if (key == "max_transactions" && count) options_.limits.maxTransactions = count; + else if (key == "max_symbolic_nodes" && count) options_.maxSymbolicNodes = count; + else if (key == "max_sat_conflicts" && count && count <= std::numeric_limits::max()) options_.maxSatConflicts = count; + else if (key == "max_sat_decisions" && count && count <= std::numeric_limits::max()) options_.maxSatDecisions = count; + else { + error = "unknown or invalid sec_latch_events option: " + key; + return false; + } + return true; +} + +bool LatchEventConfig::parseYaml(const YAML::Node& config, std::string& error) { + if (const auto gate = config["latch_support"]) { + if (!gate.IsScalar() || + (gate.as() != "true" && gate.as() != "false")) { + error = "latch_support must be true or false"; + return false; + } + options_.enabled = gate.as() == "true"; + } + const auto node = config["sec_latch_events"]; + if (!node) return true; + options_.explicitConfiguration = true; + if (!node.IsMap()) { error = "sec_latch_events must be a map"; return false; } + for (auto item : node) { + if (!item.first.IsScalar() || !item.second.IsScalar()) { + error = "sec_latch_events entries must be scalar key/value pairs"; + return false; + } + if (!set(item.first.as(), item.second.as(), error)) return false; + } + return true; +} + +LatchEventConfig::ArgumentResult LatchEventConfig::parseArgument( + int argc, char** argv, int& index, std::string& error) { + const std::string_view argument(argv[index]); + if (argument == "--latch_support" || argument == "--no-latch_support") { + options_.enabled = argument == "--latch_support"; + return ArgumentResult::Parsed; + } + static const std::map names{ + {"--sec-latch-events", "input_changes"}, + {"--sec-latch-initial-inputs", "initial_inputs"}, + {"--sec-latch-initial-storage", "initial_storage"}, + {"--sec-latch-workers", "workers"}, + {"--sec-latch-max-waves", "max_waves"}, + {"--sec-latch-max-states", "max_states"}, + {"--sec-latch-max-transactions", "max_transactions"}, + {"--sec-latch-max-nodes", "max_symbolic_nodes"}, + {"--sec-latch-sat-conflicts", "max_sat_conflicts"}, + {"--sec-latch-sat-decisions", "max_sat_decisions"}}; + const auto option = names.find(argv[index]); + if (option == names.end()) return ArgumentResult::NotHandled; + if (index + 1 == argc) { error = option->first + " requires a value"; return ArgumentResult::Error; } + return set(option->second, argv[++index], error) ? ArgumentResult::Parsed : ArgumentResult::Error; +} + +bool LatchEventConfig::validate(bool isSec, bool hasResetCycles, bool hasLeafBoundaries, + std::string& error) const { + (void)hasResetCycles; // Clock discovery and cycle expansion need the extracted models. + if (!options_.enabled) { + if (!options_.explicitConfiguration) return true; + error = "latch event tuning requires latch_support: true or --latch_support"; + return false; + } + // No tuning is required: unrestricted input changes and symbolic initial + // values are the default. LEC and selected-leaf workflows stay unchanged + // unless the user explicitly supplies SEC event options. + if (!options_.explicitConfiguration) return true; + if (!isSec) error = "latch event options require SEC verification"; + else if (hasLeafBoundaries) + error = "latch events currently require the complete top interface, not selected leaf boundaries"; + else return true; + return false; +} +} // namespace KEPLER_FORMAL diff --git a/src/bin/LatchEventConfig.h b/src/bin/LatchEventConfig.h new file mode 100644 index 00000000..ae9e4133 --- /dev/null +++ b/src/bin/LatchEventConfig.h @@ -0,0 +1,19 @@ +// Copyright 2026 keplertech.io +// SPDX-License-Identifier: Apache-2.0 +#pragma once +#include +#include "latch/LatchSupportOptions.h" +namespace YAML { class Node; } +namespace KEPLER_FORMAL { +class LatchEventConfig { + public: + enum class ArgumentResult { NotHandled, Parsed, Error }; + bool parseYaml(const YAML::Node& config, std::string& error); + ArgumentResult parseArgument(int argc, char** argv, int& index, std::string& error); + bool validate(bool isSec, bool hasResetCycles, bool hasLeafBoundaries, std::string& error) const; + const SEC::LATCH::SupportOptions& options() const { return options_; } + private: + SEC::LATCH::SupportOptions options_; + bool set(const std::string& key, const std::string& value, std::string& error); +}; +} // namespace KEPLER_FORMAL diff --git a/src/bin/LibertyLatchModels.cpp b/src/bin/LibertyLatchModels.cpp new file mode 100644 index 00000000..4b76a430 --- /dev/null +++ b/src/bin/LibertyLatchModels.cpp @@ -0,0 +1,273 @@ +// Copyright 2024-2026 keplertech.io +// SPDX-License-Identifier: Apache-2.0 + +#include "LibertyLatchModels.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "NLLibrary.h" +#include "SNLBooleanTree.h" +#include "SNLDesign.h" +#include "SNLDesignModeling.h" +#include "SNLLibertyConstructor.h" +#include "SNLScalarTerm.h" +#include "YosysLibertyParser.h" + +namespace KEPLER_FORMAL { +namespace { + +using namespace naja::NL; +using Ast = Yosys::LibertyAst; +using Modeling = SNLDesignModeling; +using Expression = Modeling::BooleanExpression; +using Conflict = Modeling::SequentialState::ClearPresetValue; + +const Ast* child(const Ast* parent, const std::string& name) { + const Ast* found = nullptr; + for (const auto* candidate : parent->children) { + if (candidate->id != name) continue; + if (found) throw std::runtime_error("duplicate `" + name + "` attribute"); + found = candidate; + } + return found; +} + +bool contains(const Ast* parent, const std::string& name) { + if (parent->id == name) return true; + return std::any_of(parent->children.begin(), parent->children.end(), + [&](const Ast* candidate) { return contains(candidate, name); }); +} + +const Ast* requireChild(const Ast* parent, const std::string& name) { + const auto* result = child(parent, name); + if (!result || result->value.empty()) { + throw std::runtime_error("missing `" + name + "` expression"); + } + return result; +} + +Conflict conflictValue(const Ast* latch, const std::string& name) { + const auto* value = child(latch, name); + if (!value) return Conflict::Unknown; + if (value->value == "L") return Conflict::Zero; + if (value->value == "H") return Conflict::One; + if (value->value == "N") return Conflict::Hold; + if (value->value == "T") return Conflict::Toggle; + if (value->value == "X") return Conflict::Unknown; + throw std::runtime_error("unsupported `" + name + "` value"); +} + +Conflict complement(Conflict value) { + if (value == Conflict::Zero) return Conflict::One; + if (value == Conflict::One) return Conflict::Zero; + return value; +} + +Expression expression(SNLDesign* primitive, const std::string& text, + const SNLBooleanTree::StateIdentifiers& states) { + SNLBooleanTree tree; + tree.parse(primitive, text, states); + auto result = tree.getBooleanExpression(); + if (!result.isValid()) throw std::runtime_error("invalid Boolean expression"); + for (const auto& node : result.nodes) { + if (node.operation == Expression::Operator::Term && + (!node.term || node.term->getDirection() != SNLTerm::Direction::Input)) { + throw std::runtime_error("expression refers to a non-input pin"); + } + } + return result; +} + +std::set terms(const Expression& expression) { + std::set result; + for (const auto& node : expression.nodes) { + if (node.operation == Expression::Operator::Term) result.insert(node.term); + } + return result; +} + +void addArcs(const Modeling::SequentialModel& model) { + std::set updateInputs; + for (const auto& state : model.states) { + for (const auto* expression : {&state.nextState, + state.clear ? &*state.clear : nullptr, + state.preset ? &*state.preset : nullptr}) { + if (expression) { + const auto inputs = terms(*expression); + updateInputs.insert(inputs.begin(), inputs.end()); + } + } + } + for (auto* enable : terms(model.clockedOn)) { + Modeling::setTermRole(enable, Modeling::SNLTermRole::Clock); + for (auto* input : updateInputs) { + const auto existing = Modeling::getInputRelatedClocks(input); + if (std::find(existing.begin(), existing.end(), enable) == existing.end()) { + Modeling::addInputsToClockArcs({input}, enable); + } + } + for (const auto& output : model.outputs) { + Modeling::setTermRole(output.term, Modeling::SNLTermRole::DataOutput); + const auto existing = Modeling::getOutputRelatedClocks(output.term); + if (std::find(existing.begin(), existing.end(), enable) == existing.end()) { + Modeling::addClockToOutputsArcs(enable, {output.term}); + } + } + } +} + +void populate(SNLDesign* primitive, const Ast* cell) { + for (const auto* forbidden : {"ff", "ff_bank", "latch_bank", "memory", + "statetable", "state_function", "bus", "bundle", + "power_down_function"}) { + if (contains(cell, forbidden)) { + throw std::runtime_error(std::string("unsupported latch cell containing `") + + forbidden + "`"); + } + } + std::vector latches; + SNLBooleanTree::StateIdentifiers identifiers; + std::string sharedEnable; + for (const auto* group : cell->children) { + if (group->id != "latch") continue; + for (const auto* attribute : group->children) { + if (attribute->id != "enable" && attribute->id != "data_in" && + attribute->id != "clear" && attribute->id != "preset" && + attribute->id != "clear_preset_var1" && attribute->id != "clear_preset_var2") { + throw std::runtime_error("unsupported latch attribute `" + attribute->id + "`"); + } + } + if (group->args.empty() || group->args.size() > 2) { + throw std::runtime_error("latch requires one or two state identifiers"); + } + if (group->args.size() == 1 && child(group, "clear_preset_var2")) { + throw std::runtime_error("clear_preset_var2 requires a second state identifier"); + } + const auto& enable = requireChild(group, "enable")->value; + requireChild(group, "data_in"); + if (!latches.empty() && enable != sharedEnable) { + throw std::runtime_error("multiple latch groups require identical enable expressions"); + } + sharedEnable = enable; + for (size_t i = 0; i < group->args.size(); ++i) { + if (group->args[i].empty() || primitive->getScalarTerm(NLName(group->args[i])) || + !identifiers.emplace(group->args[i], + SNLBooleanTree::StateIdentifier{latches.size(), i != 0}).second) { + throw std::runtime_error("ambiguous or duplicate latch state identifier"); + } + } + latches.push_back(group); + } + if (latches.empty()) return; + + Modeling::SequentialModel model; + model.kind = Modeling::SequentialModel::Kind::Latch; + model.clockedOn = expression(primitive, sharedEnable, identifiers); + for (const auto* latch : latches) { + Modeling::SequentialState state; + state.nextState = expression(primitive, requireChild(latch, "data_in")->value, identifiers); + if (const auto* clear = child(latch, "clear")) { + state.clear = expression(primitive, clear->value, identifiers); + } + if (const auto* preset = child(latch, "preset")) { + state.preset = expression(primitive, preset->value, identifiers); + } + state.clearPresetValue = conflictValue(latch, "clear_preset_var1"); + if (latch->args.size() == 2 && + conflictValue(latch, "clear_preset_var2") != complement(state.clearPresetValue)) { + // SequentialModel represents the second variable as the complement of + // the first; Liberty can instead specify independently forced values. + throw std::runtime_error("clear/preset conflict does not preserve complementary state outputs"); + } + model.states.push_back(std::move(state)); + } + for (auto* term : primitive->getBitTerms()) { + if (!dynamic_cast(term) || term->getDirection() == SNLTerm::Direction::InOut) { + throw std::runtime_error("only scalar input/output latch pins are supported"); + } + if (term->getDirection() != SNLTerm::Direction::Output) continue; + const Ast* pin = nullptr; + for (const auto* candidate : cell->children) { + if (candidate->id == "pin" && candidate->args.size() == 1 && + candidate->args[0] == term->getName().getString()) { + if (pin) throw std::runtime_error("duplicate output pin definition"); + pin = candidate; + } + } + if (!pin || child(pin, "three_state")) { + throw std::runtime_error("missing output pin or unsupported tri-state output"); + } + model.outputs.push_back({term, + expression(primitive, requireChild(pin, "function")->value, identifiers)}); + } + if (!model.isValid()) throw std::runtime_error("incomplete latch model"); + Modeling::setSequentialModel(primitive, model); + addArcs(model); +} + +std::string readText(const std::filesystem::path& path) { + // gzopen accepts uncompressed streams as well, so gzip and ordinary files + // follow the same parser without choosing behavior from the filename. + const auto close = [](gzFile file) { if (file) gzclose(file); }; + std::unique_ptr input(gzopen(path.string().c_str(), "rb"), close); + if (!input) throw std::runtime_error("cannot open Liberty input"); + std::string contents; + std::array buffer; + int count; + while ((count = gzread(input.get(), buffer.data(), buffer.size())) > 0) { + contents.append(buffer.data(), static_cast(count)); + } + if (count < 0) throw std::runtime_error("cannot decompress Liberty input"); + if (contents.starts_with("PK\003\004")) { + throw std::runtime_error("supplemental latch models do not support ZIP archives"); + } + return contents; +} + +} // namespace + +void constructLibertyWithLatchModels(NLLibrary* library, + const std::filesystem::path& path) { + if (!library) throw std::invalid_argument("Liberty latch loading requires a library"); + std::unordered_set seen; + for (const auto* design : library->getSNLDesigns()) { + seen.insert(design->getName().getString()); + } + SNLLibertyConstructor(library).construct(path); + try { + std::istringstream input(readText(path)); + // Full parsing retains clear_preset_var2 and unsupported behavior markers + // which the frontend's structural parse may omit. + Yosys::LibertyParser parser(input); + if (!parser.ast || parser.ast->id != "library") { + throw std::runtime_error("expected a Liberty library group"); + } + for (const auto* cell : parser.ast->children) { + if (cell->id != "cell" || cell->args.empty() || + !seen.insert(cell->args.front()).second) continue; + auto* primitive = library->getSNLDesign(NLName(cell->args.front())); + if (!primitive || !contains(cell, "latch") || Modeling::hasSequentialModel(primitive)) continue; + try { + populate(primitive, cell); + } catch (const std::exception& error) { + SPDLOG_WARN("Liberty latch cell `{}` in {} remains opaque: {}", + cell->args.front(), path.string(), error.what()); + } + } + } catch (const std::exception& error) { + SPDLOG_WARN("Supplemental Liberty latch modeling unavailable for {}: {}", + path.string(), error.what()); + } +} + +} // namespace KEPLER_FORMAL diff --git a/src/bin/LibertyLatchModels.h b/src/bin/LibertyLatchModels.h new file mode 100644 index 00000000..bc83a716 --- /dev/null +++ b/src/bin/LibertyLatchModels.h @@ -0,0 +1,21 @@ +// Copyright 2024-2026 keplertech.io +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include + +namespace naja::NL { +class NLLibrary; +} + +namespace KEPLER_FORMAL { + +// Ordinary Liberty loading plus explicit scalar latch semantics not yet +// supplied by the Naja frontend. Intended only for the opt-in latch-event path. +// Unsupported definitions remain unmodeled, with a warning; existing cells +// retain the ordinary constructor's first-definition-wins behavior. +void constructLibertyWithLatchModels(naja::NL::NLLibrary* library, + const std::filesystem::path& path); + +} // namespace KEPLER_FORMAL diff --git a/src/config/Config.h b/src/config/Config.h index e62f46af..775265c9 100644 --- a/src/config/Config.h +++ b/src/config/Config.h @@ -69,12 +69,16 @@ class Config { return reportSkippedPOs_; } + static void setErrorOnOpaque(bool enabled) { errorOnOpaque_ = enabled; } + static bool getErrorOnOpaque() { return errorOnOpaque_; } + private: Config() = default; ~Config() = default; inline static SolverType solverType_ = KISSAT; inline static bool reportSkippedPOs_ = false; + inline static bool errorOnOpaque_ = false; inline static std::atomic nextVerificationGeneration_{1}; inline static std::atomic verificationGeneration_{0}; }; diff --git a/src/python/BorrowedLatchOptions.cpp b/src/python/BorrowedLatchOptions.cpp new file mode 100644 index 00000000..5dde8481 --- /dev/null +++ b/src/python/BorrowedLatchOptions.cpp @@ -0,0 +1,61 @@ +// Copyright 2026 keplertech.io +// SPDX-License-Identifier: Apache-2.0 +#include "BorrowedLatchOptions.h" + +#include +#include + +namespace KEPLER_FORMAL { + +SEC::LATCH::SupportOptions BorrowedLatchOptions::validated( + bool isSec, bool hasLeafBoundaries) const { + SEC::LATCH::SupportOptions result; + result.enabled = enabled; + const bool hasTuning = inputChanges || initialInputs || initialStorage || workers || maxWaves || + maxStates || maxTransactions || maxSymbolicNodes || maxSatConflicts || maxSatDecisions; + result.explicitConfiguration = hasTuning; + if (!enabled) { + if (hasTuning) { + throw std::invalid_argument("latch event tuning requires latch_support=true"); + } + return result; + } + if (!isSec) { + if (hasTuning) throw std::invalid_argument("latch_support is only supported for SEC"); + result.enabled = false; + return result; + } + if (inputChanges && *inputChanges != LatchInputChanges::Any && *inputChanges != LatchInputChanges::Single) { + throw std::invalid_argument("latch_input_changes must be any or single"); + } + if (hasTuning && hasLeafBoundaries) { + throw std::invalid_argument( + "latch_support requires the complete top interface, not selected leaf boundaries"); + } + if (workers && *workers > size_t(std::numeric_limits::max())) { + throw std::invalid_argument("latch_workers must fit a nonnegative int"); + } + if ((maxWaves && !*maxWaves) || (maxStates && !*maxStates) || + (maxTransactions && !*maxTransactions) || (maxSymbolicNodes && !*maxSymbolicNodes) || + (maxSatConflicts && !*maxSatConflicts) || (maxSatDecisions && !*maxSatDecisions)) { + throw std::invalid_argument("latch resource limits must be positive integers"); + } + if ((maxSatConflicts && *maxSatConflicts > std::numeric_limits::max()) || + (maxSatDecisions && *maxSatDecisions > std::numeric_limits::max())) { + throw std::invalid_argument("latch SAT limits must fit a positive unsigned int"); + } + result.enabled = true; + result.singleInputChange = inputChanges && *inputChanges == LatchInputChanges::Single; + result.initialInputs = initialInputs; + result.initialStorage = initialStorage; + if (workers) result.workers = *workers; + if (maxWaves) result.limits.maxWaves = *maxWaves; + if (maxStates) result.limits.maxBoundaryStates = *maxStates; + if (maxTransactions) result.limits.maxTransactions = *maxTransactions; + if (maxSymbolicNodes) result.maxSymbolicNodes = *maxSymbolicNodes; + if (maxSatConflicts) result.maxSatConflicts = static_cast(*maxSatConflicts); + if (maxSatDecisions) result.maxSatDecisions = static_cast(*maxSatDecisions); + return result; +} + +} // namespace KEPLER_FORMAL diff --git a/src/python/BorrowedLatchOptions.h b/src/python/BorrowedLatchOptions.h new file mode 100644 index 00000000..a93d93ff --- /dev/null +++ b/src/python/BorrowedLatchOptions.h @@ -0,0 +1,33 @@ +// Copyright 2026 keplertech.io +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include + +#include "latch/LatchSupportOptions.h" + +namespace KEPLER_FORMAL { + +enum class LatchInputChanges { Any, Single }; + +// Optional members distinguish an explicitly stated event contract from an +// omitted setting. Omitted initialization stays symbolic; omitted inputChanges +// permits any input changes. Overrides never become implicit assumptions. +struct BorrowedLatchOptions { + bool enabled = true; + std::optional inputChanges; + std::optional initialInputs; + std::optional initialStorage; + std::optional workers; + std::optional maxWaves; + std::optional maxStates; + std::optional maxTransactions; + std::optional maxSymbolicNodes; + std::optional maxSatConflicts; + std::optional maxSatDecisions; + + // Throws invalid_argument without changing any process or netlist state. + SEC::LATCH::SupportOptions validated(bool isSec, bool hasLeafBoundaries) const; +}; + +} // namespace KEPLER_FORMAL diff --git a/src/python/CMakeLists.txt b/src/python/CMakeLists.txt index de189dc5..56602304 100644 --- a/src/python/CMakeLists.txt +++ b/src/python/CMakeLists.txt @@ -12,7 +12,8 @@ if(NOT KEPLER_FORMAL_GIT_HASH) set(KEPLER_FORMAL_GIT_HASH "unknown") endif() -add_library(kepler_borrowed_designs STATIC KeplerBorrowedDesigns.cpp KeplerNajaState.cpp) +add_library(kepler_borrowed_designs STATIC KeplerBorrowedDesigns.cpp KeplerNajaState.cpp + BorrowedLatchOptions.cpp) target_include_directories(kepler_borrowed_designs PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(kepler_borrowed_designs PUBLIC kepler_verification_support) @@ -22,7 +23,7 @@ if(KEPLER_USE_PUBLISHED_NAJAEDA) else() set(kepler_runtime_source KeplerNajaRuntime.cpp) endif() -Python3_add_library(kepler_formal_native MODULE WITH_SOABI PyKeplerFormal.cpp ${kepler_runtime_source}) +Python3_add_library(kepler_formal_native MODULE WITH_SOABI PyKeplerFormal.cpp PyLatchOptions.cpp ${kepler_runtime_source}) set_target_properties(kepler_formal_native PROPERTIES OUTPUT_NAME _native CXX_VISIBILITY_PRESET hidden diff --git a/src/python/KeplerBorrowedDesigns.cpp b/src/python/KeplerBorrowedDesigns.cpp index 52aa2d70..ddf41a33 100644 --- a/src/python/KeplerBorrowedDesigns.cpp +++ b/src/python/KeplerBorrowedDesigns.cpp @@ -22,6 +22,7 @@ #include "SNLDesign.h" #include "SNLInstance.h" #include "Tree2BoolExpr.h" +#include "latch/LatchSupportOptions.h" #include #include @@ -111,6 +112,7 @@ class BorrowedRunState { BorrowedRunState() : solver_(Config::getSolverType()), reportSkipped_(Config::getReportSkippedPOs()), + errorOnOpaque_(Config::getErrorOnOpaque()), defaultLogger_(spdlog::default_logger()), borrowedLogger_(spdlog::get("kepler_formal_borrowed_logger")), miterLogger_(spdlog::get("miter_logger")), @@ -122,6 +124,7 @@ class BorrowedRunState { MiterStrategy::logFileName_ = std::move(miterLogFile_); Config::setSolverType(solver_); Config::setReportSkippedPOs(reportSkipped_); + Config::setErrorOnOpaque(errorOnOpaque_); restoreLogger("miter_logger", miterLogger_); restoreLogger("miter_logger_fallback", fallbackLogger_); // set_default_logger also registers its logger by name. Restoring only @@ -143,6 +146,7 @@ class BorrowedRunState { Config::SolverType solver_; bool reportSkipped_; + bool errorOnOpaque_; std::shared_ptr defaultLogger_; std::shared_ptr borrowedLogger_; std::shared_ptr miterLogger_; @@ -237,6 +241,8 @@ int verifyBorrowedDesigns(naja::NL::SNLDesign* design0, std::lock_guard lock(mutex); try { + const auto latchOptions = options.latchSupport.validated( + options.mode == BorrowedVerificationMode::SEC, !options.setAsBoundary.empty()); auto* universe = naja::NL::NLUniverse::get(); if (!universe || !design0 || !design1) { throw std::invalid_argument("Borrowed designs need a live Naja universe and two live designs"); @@ -250,8 +256,14 @@ int verifyBorrowedDesigns(naja::NL::SNLDesign* design0, Config::ScopedVerificationContext verificationContext; BorrowedExpressionState expressionState; BorrowedRunState runState; + // Both designs share this explicit contract; never inherit ambient options. + SEC::LATCH::ScopedSupportOptions eventOptions(latchOptions); Config::setSolverType(options.solver); Config::setReportSkippedPOs(options.reportSkippedOutputs); + Config::setErrorOnOpaque(options.errorOnOpaque); + if (options.errorOnOpaque && options.mode != BorrowedVerificationMode::SEC) { + throw std::invalid_argument("error_on_opaque is only supported for SEC"); + } configureLogger(options, result); if (options.mode == BorrowedVerificationMode::SEC) { SEC::SequentialEquivalenceStrategy strategy( diff --git a/src/python/KeplerBorrowedDesigns.h b/src/python/KeplerBorrowedDesigns.h index 06606e79..ac656b04 100644 --- a/src/python/KeplerBorrowedDesigns.h +++ b/src/python/KeplerBorrowedDesigns.h @@ -4,7 +4,8 @@ #pragma once #include "RunResult.h" -#include "Config.h" +#include "../config/Config.h" +#include "BorrowedLatchOptions.h" #include "DesignBoundary.h" #include "strategy/SequentialEquivalenceStrategy.h" @@ -23,6 +24,8 @@ struct BorrowedDesignOptions { SEC::InternalRelationOptions internalRelationOptions; bool allowBoundaryMismatch = false; bool reportSkippedOutputs = false; + bool errorOnOpaque = false; + BorrowedLatchOptions latchSupport; std::string logFile; std::string logLevel; BoundaryPairs setAsBoundary; diff --git a/src/python/PyKeplerFormal.cpp b/src/python/PyKeplerFormal.cpp index 18d6a9e2..673658c1 100644 --- a/src/python/PyKeplerFormal.cpp +++ b/src/python/PyKeplerFormal.cpp @@ -15,6 +15,7 @@ #include "KeplerBorrowedDesigns.h" #include "KeplerNajaRuntime.h" +#include "PyLatchOptions.h" #include "SNLDesign.h" #ifndef KEPLER_FORMAL_VERSION @@ -351,7 +352,7 @@ bool parseBorrowedOptions(PyObject *object, static const std::unordered_set allowedKeys = { "mode", "solver", "max_k", "sec_engine", "sec_encoding", "allow_boundary_mismatch", - "set_as_boundary", "report_skipped_outputs", "log_file", "log_level", + "set_as_boundary", "report_skipped_outputs", "error_on_opaque", "log_file", "log_level", "learn_internal_relations", "allow_x_equality_in_internal_relations"}; Py_ssize_t position = 0; PyObject *key = nullptr; @@ -362,11 +363,13 @@ bool parseBorrowedOptions(PyObject *object, "verify_designs() option names must be strings"); return false; } - const char *name = PyUnicode_AsUTF8(key); + Py_ssize_t nameLength = 0; + const char *name = PyUnicode_AsUTF8AndSize(key, &nameLength); if (name == nullptr) { return false; } - if (!allowedKeys.contains(name)) { + const std::string_view optionName(name, size_t(nameLength)); + if (!allowedKeys.contains(optionName) && !KEPLER_FORMAL::isBorrowedLatchOption(optionName)) { PyErr_Format(PyExc_TypeError, "unknown verify_designs() native option: %s", name); return false; @@ -393,12 +396,17 @@ bool parseBorrowedOptions(PyObject *object, !dictionaryBoolean(object, "allow_x_equality_in_internal_relations", options.internalRelationOptions.allowXEqualityInInternalRelations) || !dictionaryBoolean(object, "report_skipped_outputs", - options.reportSkippedOutputs)) { + options.reportSkippedOutputs) || + !dictionaryBoolean(object, "error_on_opaque", options.errorOnOpaque)) { return false; } if (mode == "lec") { options.mode = KEPLER_FORMAL::BorrowedVerificationMode::LEC; + if (options.errorOnOpaque) { + PyErr_SetString(PyExc_ValueError, "error_on_opaque is only supported for SEC"); + return false; + } if (!options.internalRelationOptions.learnInternalRelations || !options.internalRelationOptions.allowXEqualityInInternalRelations) { PyErr_SetString(PyExc_ValueError, @@ -449,7 +457,9 @@ bool parseBorrowedOptions(PyObject *object, "log_level must be 'debug', 'info', or None"); return false; } - return true; + return KEPLER_FORMAL::parseBorrowedLatchOptions( + object, options.latchSupport, options.mode == KEPLER_FORMAL::BorrowedVerificationMode::SEC, + !options.setAsBoundary.empty()); } PyObject *fromNajaeda(PyObject *, PyObject *args) { diff --git a/src/python/PyLatchOptions.cpp b/src/python/PyLatchOptions.cpp new file mode 100644 index 00000000..d372691b --- /dev/null +++ b/src/python/PyLatchOptions.cpp @@ -0,0 +1,91 @@ +// Copyright 2026 keplertech.io +// SPDX-License-Identifier: Apache-2.0 +#include "PyLatchOptions.h" + +#include +#include +#include + +namespace KEPLER_FORMAL { +namespace { +constexpr std::array keys{ + "latch_support", "latch_input_changes", "latch_initial_inputs", "latch_initial_storage", + "latch_workers", "latch_max_waves", "latch_max_states", "latch_max_transactions", + "latch_max_symbolic_nodes", "latch_max_sat_conflicts", "latch_max_sat_decisions"}; + +bool number(PyObject* dictionary, const char* key, std::optional& value) { + auto* item = PyDict_GetItemString(dictionary, key); + if (!item || item == Py_None) return true; + if (!PyLong_Check(item) || PyBool_Check(item)) { + PyErr_Format(PyExc_TypeError, "%s must be an integer or None", key); + return false; + } + const auto parsed = PyLong_AsSize_t(item); + if (PyErr_Occurred()) return false; + value = parsed; + return true; +} + +bool initialBit(PyObject* dictionary, const char* key, std::optional& value) { + std::optional parsed; + if (!number(dictionary, key, parsed)) return false; + if (!parsed) return true; + if (*parsed > 1) { + PyErr_Format(PyExc_ValueError, "%s must explicitly be Boolean 0 or 1", key); + return false; + } + value = *parsed != 0; + return true; +} +} // namespace + +bool isBorrowedLatchOption(std::string_view key) { + for (auto allowed : keys) if (key == allowed) return true; + return false; +} + +bool parseBorrowedLatchOptions(PyObject* dictionary, BorrowedLatchOptions& options, + bool isSec, bool hasLeafBoundaries) { + if (auto* enabled = PyDict_GetItemString(dictionary, "latch_support")) { + if (!PyBool_Check(enabled)) { + PyErr_SetString(PyExc_TypeError, "latch_support must be a bool"); + return false; + } + options.enabled = enabled == Py_True; + } + auto* changes = PyDict_GetItemString(dictionary, "latch_input_changes"); + if (changes && changes != Py_None) { + if (!PyUnicode_Check(changes)) { + PyErr_SetString(PyExc_TypeError, "latch_input_changes must be a string or None"); + return false; + } + Py_ssize_t length = 0; + const char* text = PyUnicode_AsUTF8AndSize(changes, &length); + if (!text) return false; + const std::string_view value(text, size_t(length)); + if (value == "any") options.inputChanges = LatchInputChanges::Any; + else if (value == "single") options.inputChanges = LatchInputChanges::Single; + else { + PyErr_SetString(PyExc_ValueError, "latch_input_changes must be any or single"); + return false; + } + } + if (!initialBit(dictionary, "latch_initial_inputs", options.initialInputs) || + !initialBit(dictionary, "latch_initial_storage", options.initialStorage) || + !number(dictionary, "latch_workers", options.workers) || + !number(dictionary, "latch_max_waves", options.maxWaves) || + !number(dictionary, "latch_max_states", options.maxStates) || + !number(dictionary, "latch_max_transactions", options.maxTransactions) || + !number(dictionary, "latch_max_symbolic_nodes", options.maxSymbolicNodes) || + !number(dictionary, "latch_max_sat_conflicts", options.maxSatConflicts) || + !number(dictionary, "latch_max_sat_decisions", options.maxSatDecisions)) return false; + try { + (void)options.validated(isSec, hasLeafBoundaries); + return true; + } catch (const std::exception& error) { + PyErr_SetString(PyExc_ValueError, error.what()); + return false; + } +} + +} // namespace KEPLER_FORMAL diff --git a/src/python/PyLatchOptions.h b/src/python/PyLatchOptions.h new file mode 100644 index 00000000..42255da8 --- /dev/null +++ b/src/python/PyLatchOptions.h @@ -0,0 +1,16 @@ +// Copyright 2026 keplertech.io +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include +#include + +#include "BorrowedLatchOptions.h" + +namespace KEPLER_FORMAL { + +bool isBorrowedLatchOption(std::string_view key); +bool parseBorrowedLatchOptions(PyObject* dictionary, BorrowedLatchOptions& options, + bool isSec, bool hasLeafBoundaries); + +} // namespace KEPLER_FORMAL diff --git a/src/python/kepler_formal/_latch_options.py b/src/python/kepler_formal/_latch_options.py new file mode 100644 index 00000000..1a970958 --- /dev/null +++ b/src/python/kepler_formal/_latch_options.py @@ -0,0 +1,52 @@ +# Copyright 2026 keplertech.io +# SPDX-License-Identifier: Apache-2.0 + +"""Validation of default-on latch support and optional event assumptions.""" + +import ctypes + + +def build_latch_options(settings, *, mode: str, has_boundaries: bool) -> dict: + enabled = settings.latch_support + if not isinstance(enabled, bool): + raise TypeError("latch_support must be a bool") + names = ( + "latch_input_changes", "latch_initial_inputs", "latch_initial_storage", + "latch_workers", "latch_max_waves", "latch_max_states", "latch_max_transactions", + "latch_max_symbolic_nodes", "latch_max_sat_conflicts", "latch_max_sat_decisions", + ) + values = {name: getattr(settings, name) for name in names} + changes = values["latch_input_changes"] + if changes is not None: + if not isinstance(changes, str): + raise TypeError("latch_input_changes must be a string or None") + if changes not in ("any", "single"): + raise ValueError("latch_input_changes must be any or single") + size_max = (1 << (8 * ctypes.sizeof(ctypes.c_size_t))) - 1 + int_max = (1 << (8 * ctypes.sizeof(ctypes.c_int) - 1)) - 1 + unsigned_max = (1 << (8 * ctypes.sizeof(ctypes.c_uint))) - 1 + for name in names[1:]: + value = values[name] + if value is None: + continue + if isinstance(value, bool) or not isinstance(value, int): + raise TypeError(f"{name} must be an integer or None") + if value < 0 or value > size_max: + raise ValueError(f"{name} must fit a nonnegative size_t") + if name in ("latch_initial_inputs", "latch_initial_storage") and value > 1: + raise ValueError(f"{name} must explicitly be Boolean 0 or 1") + if name == "latch_workers" and value > int_max: + raise ValueError("latch_workers must fit a nonnegative int") + if name.startswith("latch_max_") and value == 0: + raise ValueError("latch resource limits must be positive integers") + if name.startswith("latch_max_sat_") and value > unsigned_max: + raise ValueError("latch SAT limits must fit a positive unsigned int") + has_tuning = any(value is not None for value in values.values()) + if not enabled: + if has_tuning: + raise ValueError("latch event tuning requires latch_support=True") + elif has_tuning and mode != "sec": + raise ValueError("latch_support is only supported for SEC") + elif has_tuning and has_boundaries: + raise ValueError("latch_support requires the complete top interface, not selected leaf boundaries") + return {"latch_support": enabled, **values} diff --git a/src/python/kepler_formal/api.py b/src/python/kepler_formal/api.py index 5291d48b..058b313d 100644 --- a/src/python/kepler_formal/api.py +++ b/src/python/kepler_formal/api.py @@ -14,6 +14,7 @@ import najaeda as _najaeda from . import _native +from ._latch_options import build_latch_options from .result import VerificationResult PathLike = str | os.PathLike[str] @@ -49,6 +50,15 @@ class VerificationOptions: ``set_as_boundary`` pairs top-relative paths to leaf instances, whose models have no child instances. Hierarchical paths to leaves are valid; selecting a nonleaf instance is rejected. + + ``latch_support`` is enabled by default for SEC and has no effect on LEC + without event options. Input changes default to unrestricted ``"any"``; + ``"single"`` explicitly restricts each transaction to one changing input. + Initial inputs/storage remain symbolic unless their independent options + are explicitly set to integer 0 or 1; reset is not required. In latch + event mode each SEC step represents an external transaction followed by + settling, not a clock cycle. It consumes declared Naja models; it never + infers cells by name. """ mode: VerificationMode | str = VerificationMode.LEC @@ -65,6 +75,18 @@ class VerificationOptions: ) = () learn_internal_relations: bool = True allow_x_equality_in_internal_relations: bool = True + error_on_opaque: bool = False + latch_support: bool = True + latch_input_changes: str | None = None + latch_initial_inputs: int | None = None + latch_initial_storage: int | None = None + latch_workers: int | None = None + latch_max_waves: int | None = None + latch_max_states: int | None = None + latch_max_transactions: int | None = None + latch_max_symbolic_nodes: int | None = None + latch_max_sat_conflicts: int | None = None + latch_max_sat_decisions: int | None = None NativeDesign = _native.NativeDesign @@ -167,7 +189,11 @@ def _build_native_design_options( report_skipped_outputs = _boolean( settings.report_skipped_outputs, "report_skipped_outputs" ) + error_on_opaque = _boolean(settings.error_on_opaque, "error_on_opaque") + if mode == VerificationMode.LEC.value and error_on_opaque: + raise ValueError("error_on_opaque is only supported for SEC") set_as_boundary = _boundary_pairs(settings.set_as_boundary) + latch_options = build_latch_options(settings, mode=mode, has_boundaries=bool(set_as_boundary)) if settings.max_k is not None: if isinstance(settings.max_k, bool) or not isinstance(settings.max_k, int): raise TypeError("max_k must be an integer") @@ -202,6 +228,7 @@ def _build_native_design_options( raise ValueError("log_level must be 'debug', 'info', or None") return { + **latch_options, "mode": mode, "solver": solver, "max_k": 32 if settings.max_k is None else settings.max_k, @@ -215,6 +242,7 @@ def _build_native_design_options( "allow_boundary_mismatch": allow_boundary_mismatch, "set_as_boundary": set_as_boundary, "report_skipped_outputs": report_skipped_outputs, + "error_on_opaque": error_on_opaque, "log_file": log_file, "log_level": log_level, } diff --git a/src/sec/BUILD.bazel b/src/sec/BUILD.bazel index b8edd183..044f088d 100644 --- a/src/sec/BUILD.bazel +++ b/src/sec/BUILD.bazel @@ -31,6 +31,20 @@ cc_library( "kinduction/KInductionEngine.cpp", "kinduction/OutputBatching.cpp", "kinduction/SatEncoding.cpp", + "latch/LatchBoundaryEncoding.cpp", + "latch/LatchEventModel.cpp", + "latch/LatchDependencyGraph.cpp", + "latch/LatchNetlistAdapter.cpp", + "latch/LatchSettlingCompiler.cpp", + "latch/LatchSupportOptions.cpp", + "latch/LatchInitialState.cpp", + "latch/LatchSymbolicCompiler.cpp", + "latch/LatchSymbolicEncoding.cpp", + "latch/LatchSymbolicModel.cpp", + "latch/LatchResetAdapter.cpp", + "latch/LatchResetClock.cpp", + "latch/NajaEventPrimitive.cpp", + "model/OpaquePolicy.cpp", "model/SecNetlistChecks.cpp", "model/SequentialDesignModel.cpp", "pdr/PDREngine.cpp", @@ -57,6 +71,24 @@ cc_library( "kinduction/KInductionProblem.h", "kinduction/OutputBatching.h", "kinduction/SatEncoding.h", + "latch/LatchBoundaryEncoding.h", + "latch/LatchConstantNet.h", + "latch/LatchEventContract.h", + "latch/LatchEventModel.h", + "latch/LatchDependencyGraph.h", + "latch/LatchNetlistAdapter.h", + "latch/LatchSettlingCompiler.h", + "latch/LatchSupportOptions.h", + "latch/LatchInitialState.h", + "latch/LatchSymbolicCompiler.h", + "latch/LatchSymbolicEncoding.h", + "latch/LatchSymbolicModel.h", + "latch/LatchInputHistory.h", + "latch/LatchResetAdapter.h", + "latch/LatchResetClock.h", + "latch/NajaEventPrimitive.h", + "latch/NajaSymbolicLogic.h", + "model/OpaquePolicy.h", "model/SecNetlistChecks.h", "model/SequentialDesignModel.h", "pdr/PDREngine.h", diff --git a/src/sec/CMakeLists.txt b/src/sec/CMakeLists.txt index 8aa1f398..c6cc5000 100644 --- a/src/sec/CMakeLists.txt +++ b/src/sec/CMakeLists.txt @@ -11,6 +11,20 @@ add_library(kepler_sec STATIC kinduction/KInductionEngine.cpp kinduction/OutputBatching.cpp model/SecNetlistChecks.cpp + model/OpaquePolicy.cpp + latch/LatchEventModel.cpp + latch/LatchDependencyGraph.cpp + latch/LatchSettlingCompiler.cpp + latch/LatchSymbolicModel.cpp + latch/LatchSymbolicCompiler.cpp + latch/LatchSymbolicEncoding.cpp + latch/LatchResetAdapter.cpp + latch/LatchResetClock.cpp + latch/LatchBoundaryEncoding.cpp + latch/LatchSupportOptions.cpp + latch/LatchInitialState.cpp + latch/NajaEventPrimitive.cpp + latch/LatchNetlistAdapter.cpp model/SequentialDesignModel.cpp pdr/PDREngine.cpp proof/DualRailEncoding.cpp diff --git a/src/sec/export/SecBtor2Exporter.cpp b/src/sec/export/SecBtor2Exporter.cpp index 82590222..8cf78e11 100644 --- a/src/sec/export/SecBtor2Exporter.cpp +++ b/src/sec/export/SecBtor2Exporter.cpp @@ -133,6 +133,7 @@ void exportSecBtor2(const KInductionProblem& problem, writer.comment("encoding=" + std::string(problem.usesDualRailStateEncoding ? "dual_rail_steady" : "binary")); writer.comment("startup=SEC concrete base-case observation semantics"); + if (!metadata.stepSemantics.empty()) writer.comment("step_semantics=" + metadata.stepSemantics); writer.comment("covered_outputs=" + std::to_string(problem.observedOutputNames.size()) + " total_outputs=" + std::to_string(metadata.totalOutputCount != 0 ? metadata.totalOutputCount : problem.observedOutputNames.size())); @@ -247,7 +248,8 @@ void exportSecBtor2(const KInductionProblem& problem, const bool initialize = resetFrames != 0 ? problem.usesDualRailStateEncoding : problem.hasSequentialState() && problem.hasExplicitInitialState(); - if (initialize && (resetFrames != 0 || problem.initialCondition != nullptr)) { + if (initialize && (resetFrames != 0 || problem.initialCondition != nullptr || + problem.hasExactRelationalInitialState)) { if (!problem.initialStateAssignments.empty()) { std::map values; for (const auto& [symbol, value] : problem.initialStateAssignments) { @@ -259,7 +261,10 @@ void exportSecBtor2(const KInductionProblem& problem, for (const auto& [symbol, value] : values) { writer.init(nodeForState(symbol), writer.constant(value)); } - } else if (resetFrames == 0 && problem.initialCondition != BoolExpr::createTrue()) { + } + if ((problem.initialStateAssignments.empty() || problem.hasExactRelationalInitialState) && + resetFrames == 0 && problem.initialCondition != nullptr && + problem.initialCondition != BoolExpr::createTrue()) { constrainWhen(writer, timeline.firstFrame(), writer.expression(problem.initialCondition)); } } diff --git a/src/sec/export/SecBtor2Exporter.h b/src/sec/export/SecBtor2Exporter.h index efcd45b4..af6e8c37 100644 --- a/src/sec/export/SecBtor2Exporter.h +++ b/src/sec/export/SecBtor2Exporter.h @@ -15,6 +15,7 @@ struct KInductionProblem; struct SecBtor2Metadata { size_t totalOutputCount = 0; std::vector skippedOutputs; + std::string stepSemantics; }; // Export the prepared SEC obligation with the concrete base-case startup and diff --git a/src/sec/imc/CraigInterpolatingModelChecker.cpp b/src/sec/imc/CraigInterpolatingModelChecker.cpp index ac9b97b8..e1292415 100644 --- a/src/sec/imc/CraigInterpolatingModelChecker.cpp +++ b/src/sec/imc/CraigInterpolatingModelChecker.cpp @@ -649,9 +649,11 @@ int instantiateRegionLiteral( std::unordered_set stateSymbolSet( const KInductionProblem& problem) { std::unordered_set states; - states.reserve(problem.state0Symbols.size() + problem.state1Symbols.size()); + states.reserve(problem.state0Symbols.size() + problem.state1Symbols.size() + + problem.auxiliaryStateSymbols.size()); states.insert(problem.state0Symbols.begin(), problem.state0Symbols.end()); states.insert(problem.state1Symbols.begin(), problem.state1Symbols.end()); + states.insert(problem.auxiliaryStateSymbols.begin(), problem.auxiliaryStateSymbols.end()); return states; } @@ -2778,6 +2780,11 @@ void addInitialFrontierConstraint( std::unordered_map leaves) { BoolExpr* initial = BoolExpr::createTrue(); bool hasInitialConstraint = false; + if (problem.hasExactRelationalInitialState) { + initial = problem.initialCondition != nullptr + ? problem.initialCondition : BoolExpr::createTrue(); + hasInitialConstraint = true; + } for (const auto& [symbol, value] : problem.initialStateAssignments) { initial = BoolExpr::And( initial, @@ -3534,6 +3541,11 @@ FrontierResult deriveBoundedFrontierRegion( " elapsed_ms=", elapsedMilliseconds(buildStart)); if (!startsAtConcreteBootstrapFrontier) { + if (problem.hasExactRelationalInitialState && depth != 0) { + // The exact BOOT relation is needed at frame zero at every lookahead, + // including dependencies outside the tracked output-state projection. + addInitialFrontierConstraint(solver, problem, frameLits[0]); + } addStateAssignments( solver, frameLits[0], problem.initialStateAssignments); } diff --git a/src/sec/imc/IMCEngine.cpp b/src/sec/imc/IMCEngine.cpp index 893f402f..cbe9b492 100644 --- a/src/sec/imc/IMCEngine.cpp +++ b/src/sec/imc/IMCEngine.cpp @@ -533,6 +533,12 @@ void addTransitionRelation(SATSolverWrapper& solver, variables.getLiteral(stateSymbol, frame + 1), encoder.encode(expr)); } + for (const auto& [stateSymbol, expr] : problem.auxiliaryTransitions) { + addLiteralEquivalence( + solver, + variables.getLiteral(stateSymbol, frame + 1), + encoder.encode(expr)); + } } BoolExpr* buildStateAssignmentCube(const std::vector& symbols, size_t assignment) { @@ -879,9 +885,11 @@ std::optional findLargeDualRailCounterexampleUpTo( size_t skippedStateDependent = 0; size_t skippedProjectionSupport = 0; std::unordered_set stateSymbols; - stateSymbols.reserve(problem.state0Symbols.size() + problem.state1Symbols.size()); + stateSymbols.reserve(problem.state0Symbols.size() + problem.state1Symbols.size() + + problem.auxiliaryStateSymbols.size()); stateSymbols.insert(problem.state0Symbols.begin(), problem.state0Symbols.end()); stateSymbols.insert(problem.state1Symbols.begin(), problem.state1Symbols.end()); + stateSymbols.insert(problem.auxiliaryStateSymbols.begin(), problem.auxiliaryStateSymbols.end()); const size_t stateCount = problem.effectiveTotalStateCount(); for (size_t output = 0; output < problem.observedOutputExprs0.size(); ++output) { const std::unordered_set support = diff --git a/src/sec/kinduction/BaseCaseSolver.cpp b/src/sec/kinduction/BaseCaseSolver.cpp index 6a410fdf..7c62d20a 100644 --- a/src/sec/kinduction/BaseCaseSolver.cpp +++ b/src/sec/kinduction/BaseCaseSolver.cpp @@ -347,9 +347,11 @@ struct ResetFrontierReachabilityContextData { std::unordered_set buildStateSymbolSet(const KInductionProblem& problem) { std::unordered_set stateSymbols; - stateSymbols.reserve(problem.state0Symbols.size() + problem.state1Symbols.size()); + stateSymbols.reserve(problem.state0Symbols.size() + problem.state1Symbols.size() + + problem.auxiliaryStateSymbols.size()); stateSymbols.insert(problem.state0Symbols.begin(), problem.state0Symbols.end()); stateSymbols.insert(problem.state1Symbols.begin(), problem.state1Symbols.end()); + stateSymbols.insert(problem.auxiliaryStateSymbols.begin(), problem.auxiliaryStateSymbols.end()); return stateSymbols; } @@ -461,7 +463,8 @@ void addFormulaSupport(BoolExpr* formula, std::unordered_set& output) { } bool hasStructuredInitialAssignments(const KInductionProblem& problem) { - return !problem.initialStateAssignments.empty(); + return !problem.hasExactRelationalInitialState && + !problem.initialStateAssignments.empty(); } bool isKInductionCoiDiagEnabled() { @@ -976,10 +979,11 @@ void addInitialConstraints(SATSolverWrapper& solver, if ((mode == InitialConstraintMode::CompleteInit || mode == InitialConstraintMode::PartialInit) && - problem.initialCondition != nullptr) { - if (hasStructuredInitialAssignments(problem)) { + (problem.initialCondition != nullptr || problem.hasExactRelationalInitialState)) { + if (hasStructuredInitialAssignments(problem) || problem.hasExactRelationalInitialState) { addInitialStateAssignments(solver, variables, problem, solverSymbols); - } else { + } + if (!hasStructuredInitialAssignments(problem) && problem.initialCondition != nullptr) { FrameFormulaEncoder encoder( solver, variables.makeLeafLits( diff --git a/src/sec/kinduction/InductionStepSolver.cpp b/src/sec/kinduction/InductionStepSolver.cpp index a075cb97..a8fa306a 100644 --- a/src/sec/kinduction/InductionStepSolver.cpp +++ b/src/sec/kinduction/InductionStepSolver.cpp @@ -296,9 +296,11 @@ bool shouldAddSimplePathConstraint(const KInductionProblem& problem, std::unordered_set buildStateSymbolSet(const KInductionProblem& problem) { std::unordered_set stateSymbols; - stateSymbols.reserve(problem.state0Symbols.size() + problem.state1Symbols.size()); + stateSymbols.reserve(problem.state0Symbols.size() + problem.state1Symbols.size() + + problem.auxiliaryStateSymbols.size()); stateSymbols.insert(problem.state0Symbols.begin(), problem.state0Symbols.end()); stateSymbols.insert(problem.state1Symbols.begin(), problem.state1Symbols.end()); + stateSymbols.insert(problem.auxiliaryStateSymbols.begin(), problem.auxiliaryStateSymbols.end()); return stateSymbols; } diff --git a/src/sec/kinduction/KInductionProblem.h b/src/sec/kinduction/KInductionProblem.h index 7b566183..32659930 100644 --- a/src/sec/kinduction/KInductionProblem.h +++ b/src/sec/kinduction/KInductionProblem.h @@ -226,6 +226,11 @@ struct KInductionProblem { std::vector> auxiliaryTransitions; std::shared_ptr lazyTransitions; BoolExpr* initialCondition = nullptr; + // An exact Boolean initial-state relation may describe many states without + // assigning individual bits. It is conjoined with initialStateAssignments, + // checked from frame zero, and must not assume the observation property. + // False preserves the legacy partial/ternary initialization conventions. + bool hasExactRelationalInitialState = false; size_t initializedStateCount = 0; size_t totalStateCount = 0; BoolExpr* property = nullptr; @@ -257,11 +262,12 @@ struct KInductionProblem { } bool hasExplicitInitialState() const { - return initializedStateCount != 0; + return hasExactRelationalInitialState || initializedStateCount != 0; } bool hasCompleteInitialState() const { - return initializedStateCount != 0 && initializedStateCount == totalStateCount; + return hasExactRelationalInitialState || + (initializedStateCount != 0 && initializedStateCount == totalStateCount); } bool hasResetBootstrap() const { diff --git a/src/sec/latch/LatchBoundaryEncoding.cpp b/src/sec/latch/LatchBoundaryEncoding.cpp new file mode 100644 index 00000000..6135492d --- /dev/null +++ b/src/sec/latch/LatchBoundaryEncoding.cpp @@ -0,0 +1,93 @@ +// Copyright 2026 keplertech.io +// SPDX-License-Identifier: Apache-2.0 +#include "latch/LatchBoundaryEncoding.h" +#include +#include +#include + +namespace KEPLER_FORMAL::SEC::LATCH { +namespace { +BoolExpr* equals(const std::vector& bits, size_t value) { + BoolExpr* result = BoolExpr::createTrue(); + for (size_t i = 0; i < bits.size(); ++i) + result = BoolExpr::And(result, (value >> i) & 1 ? bits[i] : BoolExpr::Not(bits[i])); + return result; +} +BoolExpr* literal(BoolExpr* expression, bool value) { + return value ? expression : BoolExpr::Not(expression); +} +} // namespace + +size_t boundaryEncodingBits(size_t states) { + if (!states) throw std::invalid_argument("empty boundary table"); + size_t bits = 1; + for (size_t remaining = states - 1; remaining >>= 1;) ++bits; + return bits; +} + +BoundaryEncoding encodeBoundaryTable( + const TransitionTable& table, const Network& network, + const std::vector& state, const std::vector& inputs, + const std::vector& selector, BoolExpr* eventValue, + const std::vector& globalInputIndices) { + if (state.size() != boundaryEncodingBits(table.boundaries.size()) || + inputs.size() != network.externalInputs.size() || + (table.singleExternalInputChange && + (!eventValue || globalInputIndices.size() != inputs.size()))) + throw std::invalid_argument("boundary encoding interface mismatch"); + if (selector.size() >= std::numeric_limits::digits) + throw std::invalid_argument("event selector exceeds index width"); + std::set uniqueIndices; + for (auto index : globalInputIndices) + if (index >= (size_t(1) << selector.size()) || !uniqueIndices.insert(index).second) + throw std::invalid_argument("aliased or out-of-range event selector index"); + BoundaryEncoding encoded; + encoded.nextState.assign(state.size(), BoolExpr::createFalse()); + encoded.observedNets.assign(network.netCount, BoolExpr::createFalse()); + std::vector fromConditions; + for (size_t i = 0; i < table.boundaries.size(); ++i) + fromConditions.push_back(equals(state, i)); + std::vector selected; + BoolExpr* selectsThisComponent = BoolExpr::createFalse(); + if (table.singleExternalInputChange) { + for (auto global : globalInputIndices) { + auto* condition = equals(selector, global); + selected.push_back(condition); + selectsThisComponent = BoolExpr::Or(selectsThisComponent, condition); + } + } + for (const auto& row : table.rows) { + if (row.from >= table.boundaries.size() || row.to >= table.boundaries.size() || + row.input.size() != inputs.size()) + throw std::invalid_argument("invalid certified boundary row"); + BoolExpr* inputCondition = BoolExpr::createTrue(); + if (!table.singleExternalInputChange) { + for (size_t i = 0; i < inputs.size(); ++i) + inputCondition = BoolExpr::And(inputCondition, literal(inputs[i], row.input[i])); + } else { + inputCondition = BoolExpr::Not(selectsThisComponent); + size_t differences = 0; + BoolExpr* changed = nullptr; + for (size_t i = 0; i < inputs.size(); ++i) { + const bool old = table.boundaries[row.from].current.at(network.externalInputs[i]); + auto* choose = BoolExpr::And(selected[i], literal(eventValue, row.input[i])); + if (row.input[i] != old) { ++differences; changed = choose; } + else inputCondition = BoolExpr::Or(inputCondition, choose); + } + if (differences > 1) throw std::invalid_argument("non-single-input certified row"); + if (differences == 1) inputCondition = changed; + } + auto* condition = BoolExpr::And(fromConditions[row.from], inputCondition); + for (size_t i = 0; i < state.size(); ++i) + if ((row.to >> i) & 1) + encoded.nextState[i] = BoolExpr::Or(encoded.nextState[i], condition); + const auto& next = table.boundaries[row.to]; + for (size_t i = 0; i < network.netCount; ++i) + if (next.current.at(i)) + encoded.observedNets[i] = BoolExpr::Or(encoded.observedNets[i], condition); + } + // Out-of-range IDs are unreachable from the explicitly initialized state and + // closed certified table. Their total encoding is zero, never an assumption. + return encoded; +} +} // namespace KEPLER_FORMAL::SEC::LATCH diff --git a/src/sec/latch/LatchBoundaryEncoding.h b/src/sec/latch/LatchBoundaryEncoding.h new file mode 100644 index 00000000..0ac47e06 --- /dev/null +++ b/src/sec/latch/LatchBoundaryEncoding.h @@ -0,0 +1,29 @@ +// Copyright 2026 keplertech.io +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include "BoolExpr.h" +#include "latch/LatchSettlingCompiler.h" + +namespace KEPLER_FORMAL::SEC::LATCH { +struct BoundaryEncoding { + std::vector nextState; + // Value at the completed observation of the incoming external transaction. + std::vector observedNets; +}; + +size_t boundaryEncodingBits(size_t states); + +// The finite table is the exact certified macro relation, encoded injectively. +// State IDs retain the entire quiescent state (table is its reconstruction). +// In single-change mode selectors name global external pins; other selector +// codes mean a stutter for this component. No input/scheduler path is assumed +// away. The shared decoder generates precisely the declared environment. +BoundaryEncoding encodeBoundaryTable( + const TransitionTable& table, const Network& network, + const std::vector& state, + const std::vector& inputs, + const std::vector& selector = {}, + BoolExpr* eventValue = nullptr, + const std::vector& globalInputIndices = {}); +} // namespace KEPLER_FORMAL::SEC::LATCH diff --git a/src/sec/latch/LatchConstantNet.h b/src/sec/latch/LatchConstantNet.h new file mode 100644 index 00000000..3cdfd57d --- /dev/null +++ b/src/sec/latch/LatchConstantNet.h @@ -0,0 +1,47 @@ +// Copyright 2026 keplertech.io +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include + +#include "DNL.h" + +namespace KEPLER_FORMAL::SEC::LATCH { + +struct DriverlessConstant { + std::optional value; + bool conflictingOrUnknown = false; +}; + +// Some driverless terminals have no flattened iso even when connected to an +// explicitly constant net. Walk the entire connected hierarchy, not just its +// local net annotation: a second annotation can conflict with that constant. +// Scratch connectivity and default no-op callbacks leave the caller's DNL and +// source models unchanged. Ordinary driven/floating nets are never constants. +class DriverlessConstantResolver { + public: + explicit DriverlessConstantResolver(const naja::DNL::DNLFull& dnl) + : builder_(scratch_, dnl) {} + DriverlessConstant resolve(const naja::DNL::DNLTerminalFull& term) { + naja::DNL::DNLComplexIso connected; + builder_.treatDriver(term, connected, visited_); + if (!connected.getDrivers().empty()) return {}; + DriverlessConstant result; + for (const auto* net : connected.getNets()) { + if (net->isConstantX() || net->isConstantZ()) return {{}, true}; + if (!net->isConstant0() && !net->isConstant1()) continue; + const bool value = net->isConstant1(); + if (result.value && *result.value != value) return {{}, true}; + result.value = value; + } + return result; + } + private: + naja::DNL::DNLIsoDB scratch_; + naja::DNL::DNLIsoDBBuilder builder_; + // Reuse the traversal bitmap instead of allocating one + // graph-sized visited set for each missing-iso terminal. + naja::DNL::visited visited_; +}; + +} // namespace KEPLER_FORMAL::SEC::LATCH diff --git a/src/sec/latch/LatchDependencyGraph.cpp b/src/sec/latch/LatchDependencyGraph.cpp new file mode 100644 index 00000000..2a62ca0b --- /dev/null +++ b/src/sec/latch/LatchDependencyGraph.cpp @@ -0,0 +1,143 @@ +// Copyright 2026 keplertech.io +// SPDX-License-Identifier: Apache-2.0 +#include "latch/LatchDependencyGraph.h" + +#include +#include +#include +#include + +namespace KEPLER_FORMAL::SEC::LATCH { +namespace { + +class DisjointSets { + public: + explicit DisjointSets(size_t size) : parent_(size), size_(size, 1) { + std::iota(parent_.begin(), parent_.end(), 0); + } + size_t root(size_t member) { + size_t result = member; + while (result != parent_[result]) result = parent_[result]; + while (member != result) { + const auto next = parent_[member]; + parent_[member] = result; + member = next; + } + return result; + } + void join(size_t a, size_t b) { + a = root(a); + b = root(b); + if (a == b) return; + if (size_[a] < size_[b] || (size_[a] == size_[b] && a > b)) std::swap(a, b); + parent_[b] = a; + size_[a] += size_[b]; + } + private: + std::vector parent_, size_; +}; + +void sortGroups(std::vector>& groups) { + for (auto& group : groups) std::sort(group.begin(), group.end()); + std::sort(groups.begin(), groups.end(), [](const auto& a, const auto& b) { + return a.front() < b.front(); + }); +} + +} // namespace + +DependencyGraph analyzeDependencies(const Network& network) { + const auto validNet = [&](size_t net) { + if (net >= network.netCount) throw std::invalid_argument("Dependency graph net index out of range"); + }; + for (auto net : network.externalInputs) validNet(net); + const size_t count = network.primitives.size(); + std::vector> writers(network.netCount), outgoing(count), incoming(count); + for (size_t i = 0; i < count; ++i) { + for (auto net : network.primitives[i].inputs) validNet(net); + for (auto net : network.primitives[i].outputs) { + validNet(net); + writers[net].push_back(i); + } + } + DisjointSets islands(count); + for (auto& producers : writers) { + // The input traversal visits cells in index order; repeated output pins can + // repeat a writer, but must not create duplicate arcs or artificial loops. + producers.erase(std::unique(producers.begin(), producers.end()), producers.end()); + for (size_t i = 1; i < producers.size(); ++i) islands.join(producers[0], producers[i]); + } + for (size_t consumer = 0; consumer < count; ++consumer) { + for (auto net : network.primitives[consumer].inputs) { + const auto& producers = writers[net]; + if (!producers.empty()) islands.join(producers[0], consumer); + for (auto producer : producers) outgoing[producer].push_back(consumer); + } + } + for (size_t source = 0; source < count; ++source) { + auto& targets = outgoing[source]; + std::sort(targets.begin(), targets.end()); + targets.erase(std::unique(targets.begin(), targets.end()), targets.end()); + for (auto target : targets) incoming[target].push_back(source); + } + + DependencyGraph result; + std::map> groups; + for (size_t i = 0; i < count; ++i) groups[islands.root(i)].push_back(i); + for (auto& [root, members] : groups) result.components.push_back(std::move(members)); + sortGroups(result.components); + result.componentOf.resize(count); + for (size_t i = 0; i < result.components.size(); ++i) + for (auto member : result.components[i]) result.componentOf[member] = i; + + // Iterative Kosaraju: first collect DFS exit order, then traverse reverse arcs. + // Explicit child cursors preserve exit order without recursive call frames. + std::vector visited(count, false); + std::vector finished; + finished.reserve(count); + std::vector> stack; + for (size_t start = 0; start < count; ++start) { + if (visited[start]) continue; + visited[start] = true; + stack.emplace_back(start, 0); + while (!stack.empty()) { + auto& [node, child] = stack.back(); + if (child == outgoing[node].size()) { + finished.push_back(node); + stack.pop_back(); + } else { + const auto target = outgoing[node][child++]; + if (!visited[target]) { + visited[target] = true; + stack.emplace_back(target, 0); + } + } + } + } + std::fill(visited.begin(), visited.end(), false); + std::vector pending; + for (auto item = finished.rbegin(); item != finished.rend(); ++item) { + if (visited[*item]) continue; + std::vector members; + pending.push_back(*item); + visited[*item] = true; + while (!pending.empty()) { + const auto node = pending.back(); + pending.pop_back(); + members.push_back(node); + for (auto source : incoming[node]) { + if (!visited[source]) { + visited[source] = true; + pending.push_back(source); + } + } + } + if (members.size() > 1 || std::binary_search(outgoing[members[0]].begin(), + outgoing[members[0]].end(), members[0])) + result.feedbackComponents.push_back(std::move(members)); + } + sortGroups(result.feedbackComponents); + return result; +} + +} // namespace KEPLER_FORMAL::SEC::LATCH diff --git a/src/sec/latch/LatchDependencyGraph.h b/src/sec/latch/LatchDependencyGraph.h new file mode 100644 index 00000000..4d3f8a86 --- /dev/null +++ b/src/sec/latch/LatchDependencyGraph.h @@ -0,0 +1,26 @@ +// Copyright 2026 keplertech.io +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include "latch/LatchEventModel.h" + +namespace KEPLER_FORMAL::SEC::LATCH { + +struct DependencyGraph { + // Event-connected scheduling islands. All producer/consumer arcs count, + // including clocks, enables and asynchronous controls; no implicit FF cut. + // Multiple writers share an island so an invalid driver cannot be isolated + // from another writer or its consumers. Read-only shared PIs are not edges. + std::vector> components; + // Directed strongly connected components with a cycle, including self-loops. + // A structural cycle is not evidence of an active loop or a settling proof. + std::vector> feedbackComponents; + std::vector componentOf; +}; + +// Indices refer to Network::primitives. Members and groups are ordered by their +// smallest primitive index. Analysis is iterative (including both SCC passes), +// invokes no primitive callbacks, and throws invalid_argument for bad net IDs. +DependencyGraph analyzeDependencies(const Network& network); + +} // namespace KEPLER_FORMAL::SEC::LATCH diff --git a/src/sec/latch/LatchEventContract.h b/src/sec/latch/LatchEventContract.h new file mode 100644 index 00000000..bb3adddd --- /dev/null +++ b/src/sec/latch/LatchEventContract.h @@ -0,0 +1,18 @@ +// Copyright 2026 keplertech.io +// SPDX-License-Identifier: Apache-2.0 +#pragma once +#include +#include + +namespace KEPLER_FORMAL::SEC::LATCH { +// Also used for already-extracted/borrowed models: CLI validation alone cannot +// protect those paths from mixing cycles, events, or initial-state contracts. +inline std::optional eventContractError( + const std::string& left, const std::string& right, bool resetCycles) { + if (left != right) + return "SEC models use different clock/event or Boolean initialization contracts"; + if (!left.empty() && resetCycles) + return "SEC external-event models cannot use clock-cycle reset bootstrap"; + return {}; +} +} // namespace KEPLER_FORMAL::SEC::LATCH diff --git a/src/sec/latch/LatchEventModel.cpp b/src/sec/latch/LatchEventModel.cpp new file mode 100644 index 00000000..f06622b4 --- /dev/null +++ b/src/sec/latch/LatchEventModel.cpp @@ -0,0 +1,396 @@ +// Copyright 2026 keplertech.io +// SPDX-License-Identifier: Apache-2.0 + +#include "LatchEventModel.h" + +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace KEPLER_FORMAL::SEC::LATCH { +namespace { + +bool binary(const Bits& bits) { + return std::all_of(bits.begin(), bits.end(), [](uint8_t bit) { return bit <= 1; }); +} + +Bits gather(const Bits& bits, const std::vector& indices) { + Bits result; + result.reserve(indices.size()); + for (size_t index : indices) result.push_back(bits[index]); + return result; +} + +State failure(State state, std::string reason) { + state.error = true; + state.errorReason = std::move(reason); + return state; +} + +void appendSize(std::string& key, size_t value) { + const auto wide = static_cast(value); + for (size_t i = 0; i < sizeof(wide); ++i) { + key.push_back(static_cast((wide >> (i * 8)) & 0xff)); + } +} + +void appendBits(std::string& key, const Bits& bits) { + appendSize(key, bits.size()); + for (auto bit : bits) key.push_back(static_cast(bit)); +} + +auto reactionTuple(const Reaction& reaction) { + return std::tie(reaction.storage, reaction.outputs, reaction.error, reaction.reason); +} + +} // namespace + +bool State::operator==(const State& other) const { + return std::tie(current, previous, storage, active, bootstrap, error, errorReason) == + std::tie(other.current, other.previous, other.storage, other.active, + other.bootstrap, other.error, other.errorReason); +} + +bool State::operator<(const State& other) const { + return std::tie(current, previous, storage, active, bootstrap, error, errorReason) < + std::tie(other.current, other.previous, other.storage, other.active, + other.bootstrap, other.error, other.errorReason); +} + +std::string State::key() const { + std::string result; + appendBits(result, current); + appendBits(result, previous); + appendSize(result, storage.size()); + for (const auto& bits : storage) appendBits(result, bits); + appendBits(result, active); + result.push_back(static_cast(bootstrap)); + result.push_back(static_cast(error)); + appendSize(result, errorReason.size()); + result += errorReason; + return result; +} + +EventModel::EventModel(Network network, Limits limits, size_t workerCount) + : network_(std::move(network)), limits_(limits), workerCount_(workerCount), + consumers_(network_.netCount) { + if (!limits_.maxPinOrderings || !limits_.maxSuccessors) { + throw std::invalid_argument("Latch event resource limits must be positive"); + } + if (workerCount_ > static_cast(std::numeric_limits::max())) { + throw std::invalid_argument("Latch event worker count is too large"); + } + if (network_.constantByNet.empty()) network_.constantByNet.resize(network_.netCount); + if (network_.constantByNet.size() != network_.netCount) { + throw std::invalid_argument("Latch event constant vector has the wrong width"); + } + Bits driven(network_.netCount, 0); + const auto writer = [&](size_t net) { + if (net >= network_.netCount) throw std::invalid_argument("Latch event net out of range"); + if (driven[net]) throw std::invalid_argument("Latch event net has multiple drivers"); + driven[net] = 1; + }; + for (size_t net : network_.externalInputs) writer(net); + for (size_t net = 0; net < network_.netCount; ++net) { + if (network_.constantByNet[net].has_value()) writer(net); + } + for (size_t i = 0; i < network_.primitives.size(); ++i) { + const auto& primitive = network_.primitives[i]; + for (size_t net : primitive.outputs) writer(net); + for (size_t net : primitive.inputs) { + if (net >= network_.netCount) throw std::invalid_argument("Latch event input out of range"); + consumers_[net].push_back(i); + } + } + if (std::find(driven.begin(), driven.end(), 0) != driven.end()) { + throw std::invalid_argument("Latch event net has no driver"); + } + for (auto& fanout : consumers_) { + std::sort(fanout.begin(), fanout.end()); + fanout.erase(std::unique(fanout.begin(), fanout.end()), fanout.end()); + } +} + +void EventModel::validateState(const State& state) const { + if (state.current.size() != network_.netCount || + state.previous.size() != network_.netCount || !binary(state.current) || + !binary(state.previous) || state.storage.size() != network_.primitives.size() || + state.active.size() != network_.primitives.size() || !binary(state.active)) { + throw std::invalid_argument("Malformed Boolean latch event state"); + } + for (size_t i = 0; i < state.storage.size(); ++i) { + if (state.storage[i].size() != network_.primitives[i].storageBits || + !binary(state.storage[i])) { + throw std::invalid_argument("Malformed latch event storage"); + } + } + for (size_t i = 0; i < network_.netCount; ++i) { + if (network_.constantByNet[i] && + (state.current[i] != *network_.constantByNet[i] || + state.previous[i] != *network_.constantByNet[i])) { + throw std::invalid_argument("Latch event state changed a constant net"); + } + } +} + +void EventModel::normalizeBoundary(State& state) const { + if (!state.bootstrap && !state.error && + std::none_of(state.active.begin(), state.active.end(), [](auto bit) { return bit; })) { + state.previous = state.current; + } +} + +bool EventModel::stable(const State& state) const { + validateState(state); + return !state.bootstrap && !state.error && state.previous == state.current && + std::none_of(state.active.begin(), state.active.end(), [](auto bit) { return bit; }); +} + +State EventModel::bootstrap(const Bits& inputs, const std::vector& initialStorage, + const Bits& internalSeeds) const { + if (inputs.size() != network_.externalInputs.size() || !binary(inputs) || + internalSeeds.size() != network_.netCount || !binary(internalSeeds) || + initialStorage.size() != network_.primitives.size()) { + throw std::invalid_argument("Malformed latch event bootstrap parameters"); + } + State state; + state.current = internalSeeds; + state.storage = initialStorage; + state.active.assign(network_.primitives.size(), 1); + state.bootstrap = true; + for (size_t i = 0; i < inputs.size(); ++i) state.current[network_.externalInputs[i]] = inputs[i]; + for (size_t i = 0; i < network_.netCount; ++i) { + if (network_.constantByNet[i]) state.current[i] = *network_.constantByNet[i]; + } + const Bits projectionSnapshot = state.current; + for (size_t i = 0; i < network_.primitives.size(); ++i) { + const auto& primitive = network_.primitives[i]; + const auto& storage = initialStorage[i]; + if (storage.size() != primitive.storageBits || !binary(storage)) { + throw std::invalid_argument("Malformed latch event bootstrap storage"); + } + if (!primitive.storageBits) continue; + Bits outputs; + try { + if (primitive.initialOutputValues) { + outputs = primitive.initialOutputValues(storage, gather(projectionSnapshot, primitive.inputs)); + } else if (primitive.initialOutputs) outputs = primitive.initialOutputs(storage); + else if (primitive.outputs.size() == storage.size()) outputs = storage; + else { + state = failure(std::move(state), primitive.name + ": missing initial output mapping"); + continue; + } + } catch (const Limit&) { + throw; + } catch (const std::bad_alloc&) { + throw; + } catch (const std::exception& exception) { + state = failure(std::move(state), primitive.name + ": initial output mapping: " + exception.what()); + continue; + } + if (outputs.size() != primitive.outputs.size() || !binary(outputs)) { + state = failure(std::move(state), primitive.name + ": invalid initial output mapping"); + continue; + } + for (size_t j = 0; j < outputs.size(); ++j) state.current[primitive.outputs[j]] = outputs[j]; + } + state.previous = state.current; // In particular, no invented external clock edge. + return state; +} + +State EventModel::admit(const State& quiescent, const Bits& inputs) const { + validateState(quiescent); + if (quiescent.error) return quiescent; + if (!stable(quiescent)) return failure(quiescent, "External transaction before quiescence"); + if (inputs.size() != network_.externalInputs.size() || !binary(inputs)) { + return failure(quiescent, "Invalid external Boolean transaction"); + } + State state = quiescent; + state.previous = quiescent.current; + state.active.assign(network_.primitives.size(), 0); + for (size_t i = 0; i < inputs.size(); ++i) { + const size_t net = network_.externalInputs[i]; + state.current[net] = inputs[i]; + if (inputs[i] != state.previous[net]) { + for (size_t consumer : consumers_[net]) state.active[consumer] = 1; + } + } + normalizeBoundary(state); + return state; +} + +std::vector EventModel::evaluate(size_t index, const State& state) const { + const auto& primitive = network_.primitives[index]; + const auto oldOutputs = gather(state.current, primitive.outputs); + const auto oldStorage = state.storage[index]; + if (!state.active[index]) return {{oldStorage, oldOutputs}}; + const auto current = gather(state.current, primitive.inputs); + const auto previous = gather(state.previous, primitive.inputs); + const auto invoke = [&](const Bits& storage, const Bits& before, const Bits& pins, + std::optional changedPin) -> Reaction { + auto invalid = [&](const std::string& reason) { + return Reaction{storage, oldOutputs, true, primitive.name + ": " + reason}; + }; + if (!primitive.react) return invalid("missing primitive reaction"); + Reaction result; + try { + result = primitive.react(storage, before, pins, changedPin, state.bootstrap); + } catch (const Limit&) { + throw; + } catch (const std::bad_alloc&) { + throw; + } catch (const std::exception& exception) { + return invalid(std::string("primitive reaction: ") + exception.what()); + } + if (result.error) return invalid(result.reason.empty() ? "unsupported primitive reaction" : result.reason); + if (result.storage.size() != primitive.storageBits || !binary(result.storage) || + result.outputs.size() != primitive.outputs.size() || !binary(result.outputs)) { + return invalid("invalid primitive reaction shape or Boolean value"); + } + return result; + }; + if (state.bootstrap || primitive.storageBits == 0) { + return {invoke(oldStorage, previous, current, std::nullopt)}; + } + std::vector changed; + for (size_t pin = 0; pin < current.size(); ++pin) { + if (current[pin] != previous[pin]) changed.push_back(pin); + } + size_t orderingCount = 1; + for (size_t i = 2; i <= changed.size(); ++i) { + if (orderingCount > limits_.maxPinOrderings / i) { + throw Limit(primitive.name + ": changed-pin ordering limit exceeded"); + } + orderingCount *= i; + } + if (changed.empty()) return {invoke(oldStorage, previous, current, std::nullopt)}; + std::vector results; + do { + Bits pins = previous; + Reaction result{oldStorage, oldOutputs}; + for (size_t pin : changed) { + const Bits before = pins; + pins[pin] = current[pin]; + result = invoke(result.storage, before, pins, pin); + if (result.error) break; + } + results.push_back(std::move(result)); + } while (std::next_permutation(changed.begin(), changed.end())); + std::sort(results.begin(), results.end(), [](const auto& a, const auto& b) { + return reactionTuple(a) < reactionTuple(b); + }); + results.erase(std::unique(results.begin(), results.end(), [](const auto& a, const auto& b) { + return reactionTuple(a) == reactionTuple(b); + }), results.end()); + return results; +} + +std::vector EventModel::successors(const State& state) const { + validateState(state); + if (state.error || stable(state)) return {state}; + std::vector> choices(network_.primitives.size()); + const auto evaluateAll = [&] { + tbb::parallel_for(tbb::blocked_range(0, choices.size()), [&](const auto& range) { + for (size_t i = range.begin(); i != range.end(); ++i) choices[i] = evaluate(i, state); + }); + }; + tbb::task_arena arena(workerCount_ ? static_cast(workerCount_) : tbb::task_arena::automatic); + arena.execute(evaluateAll); + + size_t count = 1; + for (const auto& alternatives : choices) { + if (count > limits_.maxSuccessors / alternatives.size()) { + throw Limit("Latch event successor limit exceeded"); + } + count *= alternatives.size(); + } + State base = state; + base.bootstrap = false; + base.previous = state.current; + base.active.assign(network_.primitives.size(), 0); + std::vector results{std::move(base)}; + for (size_t i = 0; i < choices.size(); ++i) { + std::vector expanded; + expanded.reserve(results.size() * choices[i].size()); + for (const auto& partial : results) { + for (const auto& reaction : choices[i]) { + State next = partial; + next.storage[i] = reaction.storage; + for (size_t j = 0; j < reaction.outputs.size(); ++j) { + next.current[network_.primitives[i].outputs[j]] = reaction.outputs[j]; + } + if (reaction.error && !next.error) { + next.error = true; + next.errorReason = reaction.reason; + } + expanded.push_back(std::move(next)); + } + } + results = std::move(expanded); + } + for (auto& next : results) { + for (size_t net = 0; net < network_.netCount; ++net) { + if (next.current[net] != state.current[net]) { + for (size_t consumer : consumers_[net]) next.active[consumer] = 1; + } + } + normalizeBoundary(next); + } + std::sort(results.begin(), results.end()); + results.erase(std::unique(results.begin(), results.end()), results.end()); + return results; +} + +Primitive combinational(std::string name, std::vector inputs, + std::vector outputs, + std::function function) { + Primitive primitive; + primitive.name = std::move(name); + primitive.inputs = std::move(inputs); + primitive.outputs = std::move(outputs); + if (function) { + primitive.react = [function = std::move(function)](const Bits&, const Bits&, const Bits& pins, + std::optional, bool) { + return Reaction{{}, function(pins)}; + }; + } + return primitive; +} + +Primitive latch(std::string name, size_t data, size_t enable, size_t output, bool activeHigh) { + Primitive primitive; + primitive.name = std::move(name); + primitive.inputs = {data, enable}; + primitive.outputs = {output}; + primitive.storageBits = 1; + primitive.react = [activeHigh](const Bits& storage, const Bits&, const Bits& pins, + std::optional, bool) { + const Bits value{pins[1] == activeHigh ? pins[0] : storage[0]}; + return Reaction{value, value}; + }; + return primitive; +} + +Primitive flipFlop(std::string name, size_t data, size_t clock, size_t output, bool risingEdge) { + Primitive primitive; + primitive.name = std::move(name); + primitive.inputs = {data, clock}; + primitive.outputs = {output}; + primitive.storageBits = 1; + primitive.react = [risingEdge](const Bits& storage, const Bits& before, const Bits& pins, + std::optional changedPin, bool bootstrap) { + const bool edge = !bootstrap && changedPin == std::optional{1} && + before[1] != pins[1] && pins[1] == risingEdge; + const Bits value{edge ? pins[0] : storage[0]}; + return Reaction{value, value}; + }; + return primitive; +} + +} // namespace KEPLER_FORMAL::SEC::LATCH diff --git a/src/sec/latch/LatchEventModel.h b/src/sec/latch/LatchEventModel.h new file mode 100644 index 00000000..d1bf7347 --- /dev/null +++ b/src/sec/latch/LatchEventModel.h @@ -0,0 +1,121 @@ +// Copyright 2026 keplertech.io +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +namespace KEPLER_FORMAL::SEC::LATCH { + +using Bits = std::vector; + +struct Reaction { + Bits storage; + Bits outputs; + bool error = false; + std::string reason{}; +}; + +struct Primitive { + std::string name; + std::vector inputs; + std::vector outputs; + size_t storageBits = 0; + // Callbacks must be pure and thread-safe. A sequential reaction consumes only + // changedPin's event; bootstrap has no edge. Zero-storage cells evaluate once + // per activation, with changedPin unset and the complete current pin vector. + std::function changedPin, bool bootstrap)> react; + // Maps remembered storage to physical outputs before forced bootstrap. If + // omitted, identity is permitted only when the widths match. Gates use seeds. + std::function initialOutputs; + // Optional input-dependent physical projection (for example, an integrated + // clock gate). All such projections read one frozen seed/input snapshot and + // commit together. Auxiliary seeds still require bootstrap certification. + std::function initialOutputValues; +}; + +struct Network { + size_t netCount = 0; + std::vector externalInputs; + std::vector primitives; + // Empty means no constants; otherwise exactly netCount entries. + std::vector> constantByNet{}; +}; + +struct State { + Bits current; + Bits previous; + std::vector storage; + Bits active; + bool bootstrap = false; + bool error = false; + std::string errorReason; + + bool operator==(const State& other) const; + bool operator!=(const State& other) const { return !(*this == other); } + bool operator<(const State& other) const; + // Complete, unambiguous serialization suitable for hash-map keys. + std::string key() const; +}; + +struct Limits { + size_t maxPinOrderings = 100000; + size_t maxSuccessors = 100000; +}; + +// Resource exhaustion never silently truncates the reference relation. +class Limit : public std::runtime_error { + public: + using std::runtime_error::runtime_error; +}; + +// Finite Boolean evaluate/update semantics. Every wave is an epoch barrier: +// primitive evaluations may run concurrently but all read the same snapshot. +// Simultaneous changed pin positions retain ALL orders and producer choices are +// committed once, shared by every fanout. Worker count cannot change behavior. +class EventModel { + public: + explicit EventModel(Network network, Limits limits = {}, size_t workerCount = 0); + + const Network& network() const { return network_; } + // inputs follows externalInputs order. initialStorage has one entry per cell, + // including empty entries for gates. Seeds has netCount bits. External and + // constant positions are overwritten. Physical storage outputs are projected + // before BOOT; with input-dependent projections their original seeds can + // affect other projections and must also be quantified by the certifier. + State bootstrap(const Bits& inputs, const std::vector& initialStorage, + const Bits& internalSeeds) const; + // Invalid admission is an explicit absorbing, nonstable error state. + State admit(const State& quiescent, const Bits& inputs) const; + std::vector successors(const State& state) const; + bool stable(const State& state) const; + + private: + void validateState(const State& state) const; + std::vector evaluate(size_t primitive, const State& state) const; + void normalizeBoundary(State& state) const; + + Network network_; + Limits limits_; + size_t workerCount_; + std::vector> consumers_; +}; + +// Convenience primitives for tests and explicitly modeled Boolean cells. More +// elaborate reset priorities/output mappings use the Primitive reaction API. +Primitive combinational(std::string name, std::vector inputs, + std::vector outputs, + std::function function); +Primitive latch(std::string name, size_t data, size_t enable, size_t output, + bool activeHigh = true); +Primitive flipFlop(std::string name, size_t data, size_t clock, size_t output, + bool risingEdge = true); + +} // namespace KEPLER_FORMAL::SEC::LATCH diff --git a/src/sec/latch/LatchInitialState.cpp b/src/sec/latch/LatchInitialState.cpp new file mode 100644 index 00000000..6cbd552f --- /dev/null +++ b/src/sec/latch/LatchInitialState.cpp @@ -0,0 +1,106 @@ +// Copyright 2026 keplertech.io +// SPDX-License-Identifier: Apache-2.0 +#include "latch/LatchInitialState.h" + +#include +#include +#include "common/BoolExprUtils.h" + +namespace KEPLER_FORMAL::SEC::LATCH { +namespace { +void append(KInductionProblem& problem, BoolExpr* expression) { + problem.initialCondition = BoolExpr::And( + problem.initialCondition ? problem.initialCondition : BoolExpr::createTrue(), + expression); +} + +void addRelation(const SequentialDesignModel& model, + const std::unordered_map& symbols, + KInductionProblem& problem) { + if (!model.initialCondition) return; + for (auto symbol : model.initialCondition->getSupportVars()) + if (symbol >= 2 && !symbols.count(symbol)) + throw std::runtime_error("Event initial relation references an undeclared symbol"); + append(problem, remapBoolExprVariables(model.initialCondition, symbols)); +} +} // namespace + +void reuseInitialInputHistory(SequentialDesignModel& model, + const std::vector& inputs, const std::vector& history) { + std::unordered_map replacements; + for (size_t i = 0; i < inputs.size(); ++i) { + const auto origin = model.initialInputStateKeyByInputKey.find(inputs[i]); + if (origin == model.initialInputStateKeyByInputKey.end()) continue; + if (!history.at(i) || history[i]->getOp() != Op::VAR || history[i]->getId() < 2) + throw std::runtime_error("Symbolic initial input lacks a remembered state bit"); + const auto originalKey = origin->second; + const auto id = model.inputVarByKey.at(originalKey); + const auto current = std::find_if(model.stateBits.begin(), model.stateBits.end(), + [&](const auto& key) { return model.inputVarByKey.at(key) == history[i]->getId(); }); + if (current == model.stateBits.end() || history[i]->getId() == id) + throw std::runtime_error("Symbolic input history is not a distinct state bit"); + origin->second = *current; + replacements.emplace(id, history[i]->getId()); + std::erase(model.stateBits, originalKey); + model.nextStateExprByStateKey.erase(originalKey); + model.inputVarByKey.erase(originalKey); + model.displayNameByKey.erase(originalKey); + } + if (!replacements.empty()) { + for (auto symbol : model.initialCondition->getSupportVars()) + if (symbol >= 2) replacements.try_emplace(symbol, symbol); + model.initialCondition = remapBoolExprVariables(model.initialCondition, replacements); + } + // A BOOT origin erased by transparency/reset and absent from the entire + // initial relation is existentially irrelevant. Macro transitions/outputs + // contain only boundary state and event inputs, never BOOT parameters. + const auto initialSupport = model.initialCondition->getSupportVars(); + std::erase_if(model.stateBits, [&](const auto& key) { + if (key.first.size() != 3 || key.first[0] != (uint64_t(1) << 61) || key.first[1] != 8 || + initialSupport.count(model.inputVarByKey.at(key))) return false; + model.nextStateExprByStateKey.erase(key); + model.inputVarByKey.erase(key); + model.displayNameByKey.erase(key); + return true; + }); +} + +void integrateEventInitialState( + const SequentialDesignModel& first, const SequentialDesignModel& second, + const AlignedSignals& inputs, + const std::unordered_map& symbols0, + const std::unordered_map& symbols1, + KInductionProblem& problem) { + if (!first.initialCondition && !second.initialCondition) return; + problem.hasExactRelationalInitialState = true; + addRelation(first, symbols0, problem); + addRelation(second, symbols1, problem); + for (const auto& [symbol, value] : problem.initialStateAssignments) + append(problem, value ? BoolExpr::Var(symbol) : BoolExpr::Not(BoolExpr::Var(symbol))); + for (size_t i = 0; i < inputs.keys0.size(); ++i) { + const auto lhs = first.initialInputStateKeyByInputKey.find(inputs.keys0[i]); + const auto rhs = second.initialInputStateKeyByInputKey.find(inputs.keys1[i]); + if (lhs == first.initialInputStateKeyByInputKey.end() || + rhs == second.initialInputStateKeyByInputKey.end()) continue; + append(problem, makeEqualityExpr( + BoolExpr::Var(symbols0.at(first.inputVarByKey.at(lhs->second))), + BoolExpr::Var(symbols1.at(second.inputVarByKey.at(rhs->second))))); + } +} + +void constrainEventInitialRails( + const SequentialDesignModel& model, + const std::unordered_map& rails, + KInductionProblem& problem, bool secondDesign) { + if (!model.initialCondition) return; + auto& complements = secondDesign ? problem.complementedStatePairs1 : problem.complementedStatePairs0; + for (const auto& [symbol, pair] : rails) { + append(problem, BoolExpr::Xor(BoolExpr::Var(pair.mayBeOne), + BoolExpr::Var(pair.mayBeZero))); + // BOOT is Boolean and every event next-state expression is Boolean over + // Boolean state/inputs. Dual-rail lifting therefore preserves complements + // inductively. This excludes unreachable X states, not genuine starts. + complements.emplace_back(pair.mayBeOne, pair.mayBeZero); + } +} +} // namespace KEPLER_FORMAL::SEC::LATCH diff --git a/src/sec/latch/LatchInitialState.h b/src/sec/latch/LatchInitialState.h new file mode 100644 index 00000000..557f5303 --- /dev/null +++ b/src/sec/latch/LatchInitialState.h @@ -0,0 +1,32 @@ +// Copyright 2026 keplertech.io +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include "common/AlignedSignals.h" +#include "kinduction/KInductionProblem.h" +#include "model/SequentialDesignModel.h" + +namespace KEPLER_FORMAL::SEC::LATCH { + +// At BOOT, an input's remembered level is its origin. Existentially eliminate +// the redundant fixed-once origin, without identifying future input levels. +void reuseInitialInputHistory(SequentialDesignModel& model, + const std::vector& inputs, const std::vector& history); + +// Preserve exact BOOT relations while sharing only initial external levels, +// never unrelated hardware storage, between the two designs. +void integrateEventInitialState( + const SequentialDesignModel& first, const SequentialDesignModel& second, + const AlignedSignals& inputs, + const std::unordered_map& symbols0, + const std::unordered_map& symbols1, + KInductionProblem& problem); + +// Event certification is Boolean. An unspecified Boolean origin is either 0 +// or 1, not the ternary value X (both rails asserted). +void constrainEventInitialRails( + const SequentialDesignModel& model, + const std::unordered_map& rails, + KInductionProblem& problem, bool secondDesign = false); + +} // namespace KEPLER_FORMAL::SEC::LATCH diff --git a/src/sec/latch/LatchInputHistory.h b/src/sec/latch/LatchInputHistory.h new file mode 100644 index 00000000..83c27895 --- /dev/null +++ b/src/sec/latch/LatchInputHistory.h @@ -0,0 +1,22 @@ +// Copyright 2026 keplertech.io +// SPDX-License-Identifier: Apache-2.0 +#pragma once +#include "latch/LatchSymbolicModel.h" +#include "latch/LatchSettlingCompiler.h" + +namespace KEPLER_FORMAL::SEC::LATCH { +// The finite compiler's state ID encodes the complete boundary. Decode a +// remembered level, not the post-transaction observation, for reset composition. +inline BoolExpr* finiteInputHistory(const TransitionTable& table, + const SymbolicBits& state, size_t net) { + auto* result = BoolExpr::createFalse(); + for (size_t row = 0; row < table.boundaries.size(); ++row) { + if (!table.boundaries[row].current.at(net)) continue; + auto* match = BoolExpr::createTrue(); + for (size_t bit = 0; bit < state.size(); ++bit) + match = BoolExpr::And(match, (row >> bit) & 1 ? state[bit] : BoolExpr::Not(state[bit])); + result = BoolExpr::Or(result, match); + } + return result; +} +} // namespace KEPLER_FORMAL::SEC::LATCH diff --git a/src/sec/latch/LatchNetlistAdapter.cpp b/src/sec/latch/LatchNetlistAdapter.cpp new file mode 100644 index 00000000..ea0cf5ad --- /dev/null +++ b/src/sec/latch/LatchNetlistAdapter.cpp @@ -0,0 +1,519 @@ +// Copyright 2026 keplertech.io +// SPDX-License-Identifier: Apache-2.0 +#include "latch/LatchNetlistAdapter.h" + +#include +#include +#include +#include +#include +#include "DNL.h" +#include "NLDB.h" +#include "NLName.h" +#include "NLUniverse.h" +#include "SNLDesign.h" +#include "SNLDesignModeling.h" +#include "SNLInstance.h" +#include "SNLPath.h" +#include "latch/LatchBoundaryEncoding.h" +#include "latch/LatchConstantNet.h" +#include "latch/LatchDependencyGraph.h" +#include "latch/LatchInputHistory.h" +#include "latch/LatchInitialState.h" +#include "latch/LatchResetAdapter.h" +#include "latch/LatchResetClock.h" +#include "latch/LatchSupportOptions.h" +#include "latch/NajaEventPrimitive.h" +#include "latch/LatchSymbolicEncoding.h" +#include "model/OpaquePolicy.h" + +namespace KEPLER_FORMAL::SEC::LATCH { +namespace { +using Term = naja::DNL::DNLTerminalFull; +using Direction = naja::NL::SNLBitTerm::Direction; +constexpr size_t absent = size_t(-1); + +SignalKey key(const Term& term) { + SignalKey result; + for (const auto& name : term.getDNLInstance().getPath().getPathNames()) + result.first.push_back(name.getID()); + result.first.push_back(term.getSnlBitTerm()->getName().getID()); + result.second.push_back(term.getSnlBitTerm()->getBit()); + return result; +} +SignalKey syntheticKey(size_t category, size_t object, size_t bit = 0) { + return {{uint64_t(1) << 61, category, object}, + {static_cast(bit)}}; +} +std::string name(const Term& term) { + std::string result; + for (const auto& part : term.getDNLInstance().getPath().getPathNames()) + result += part.getString() + "."; + return result + term.getSnlBitTerm()->getName().getString() + "[" + + std::to_string(term.getSnlBitTerm()->getBit()) + "]"; +} +struct Port { SignalKey key; std::string name; size_t net; }; +struct CellInfo { SignalKey key; std::string name, error; }; + +bool containsModeledLatch(naja::NL::SNLDesign* top) { + using Modeling = naja::NL::SNLDesignModeling; + std::set visited; + std::vector pending{top}; + while (!pending.empty()) { + auto* design = pending.back(); + pending.pop_back(); + if (!visited.insert(design).second) continue; + if (Modeling::hasSequentialModel(design) && + Modeling::getSequentialModel(design).kind == Modeling::SequentialModel::Kind::Latch) + return true; + for (auto* instance : design->getInstances()) pending.push_back(instance->getModel()); + } + return false; +} + +// Keep the caller's flattened graph and selected top intact (including borrowed +// designs). The compiled result owns no pointers into this temporary graph. +struct DnlScope { + naja::NL::NLUniverse* universe = naja::NL::NLUniverse::get(); + naja::NL::NLDB* previousDb = universe->getTopDB(); + naja::NL::NLDB* selectedDb; + naja::NL::SNLDesign* previousSelectedTop; + naja::DNL::DNLFull* previousDnl = nullptr; + std::vector> termOrders; + std::vector> instanceOrders; + explicit DnlScope(naja::NL::SNLDesign* top) + : selectedDb(top->getDB()), previousSelectedTop(selectedDb->getTopDesign()) { + // Flattening assigns ordering IDs on shared source models. Restoring only + // the graph pointer would leave a caller's cached graph with stale IDs. + std::set visited; + std::vector pending{top}; + while (!pending.empty()) { + auto* design = pending.back(); + pending.pop_back(); + if (!visited.insert(design).second) continue; + for (auto* term : design->getBitTerms()) termOrders.emplace_back(term, term->getOrderID()); + for (auto* instance : design->getInstances()) { + instanceOrders.emplace_back(instance, instance->getOrderID()); + pending.push_back(instance->getModel()); + } + } + previousDnl = naja::DNL::exchange(nullptr); + universe->setTopDesign(top); + } + ~DnlScope() { + naja::DNL::destroy(); + for (const auto& [term, order] : termOrders) term->setOrderID(order); + for (const auto& [instance, order] : instanceOrders) instance->setOrderID(order); + selectedDb->setTopDesign(previousSelectedTop); + universe->setTopDB(previousDb); + naja::DNL::exchange(previousDnl); + } +}; + +void opaque(SequentialDesignModel& model, const SignalKey& key, const std::string& name, + const std::string& reason) { + model.displayNameByKey.insert_or_assign(key, name); + model.connectivitySkipInfoByKey.insert_or_assign(key, + ConnectivitySkipInfo{ConnectivitySkipOrigin::OpaqueInternal, reason}); +} +BoolExpr* variable(SequentialDesignModel& model, const SignalKey& key, + const std::string& name, bool state, size_t& nextVar) { + model.displayNameByKey.emplace(key, name); + model.inputVarByKey.emplace(key, nextVar); + (state ? model.stateBits : model.environmentInputs).push_back(key); + return BoolExpr::Var(nextVar++); +} + +SequentialDesignModel extract(naja::NL::SNLDesign* top, size_t side) { + const auto options = supportOptions(); + SequentialDesignModel model; + model.eventContract = std::string("boolean-epochs-v1;") + + (options.singleInputChange ? "single;" : "any;") + + "initial_inputs=" + (options.initialInputs ? std::to_string(*options.initialInputs) : "symbolic") + + ";initial_storage=" + (options.initialStorage ? std::to_string(*options.initialStorage) : "symbolic"); + DnlScope scope(top); + const auto* dnl = naja::DNL::get(); + DriverlessConstantResolver driverlessConstants(*dnl); + Network network; + std::map byIso; + std::vector netErrors; + auto net = [&](const Term& term) { + const auto isoID = term.getIsoID(); + if (isoID != naja::DNL::DNLID_MAX) { + if (const auto found = byIso.find(isoID); found != byIso.end()) return found->second; + } + const size_t index = network.netCount++; + network.constantByNet.push_back({}); + netErrors.emplace_back(); + if (isoID == naja::DNL::DNLID_MAX) { + const auto constant = driverlessConstants.resolve(term); + network.constantByNet.back() = constant.value; + if (constant.conflictingOrUnknown) + netErrors.back() = "X/Z or conflicting constant net in Boolean event model: " + name(term); + else if (!constant.value) + netErrors.back() = "unconnected signal " + name(term); + return index; + } + byIso.emplace(isoID, index); + const auto& iso = dnl->getDNLIsoDB().getIsoFromIsoIDconst(isoID); + if (iso.isConstant0() || iso.isConstant1()) network.constantByNet.back() = iso.isConstant1(); + else if (iso.isConstantX() || iso.isConstantZ() || iso.getType() == naja::DNL::DNLIso::AMBIGUOUS) + netErrors.back() = "X/Z or conflicting constant net in Boolean event model: " + name(term); + else if (iso.getDrivers().size() != 1) netErrors.back() = "missing or multiple signal drivers: " + name(term); + return index; + }; + std::vector inputs, outputs; + const auto& topInstance = dnl->getTop(); + for (auto* bit : top->getBitTerms()) { + const auto& term = topInstance.getTerminalFromBitTerm(bit); + Port port{key(term), name(term), net(term)}; + model.displayNameByKey.emplace(port.key, port.name); + if (bit->getDirection() == Direction::Input) { + inputs.push_back(port); model.topInputKeys.push_back(port.key); + } else if (bit->getDirection() == Direction::Output) { + outputs.push_back(port); model.topOutputKeys.push_back(port.key); + model.allObservedOutputs.push_back(port.key); + } else model.unsupportedReasons.push_back("Boolean event model requires unidirectional top ports"); + } + std::sort(inputs.begin(), inputs.end(), [](const auto& a, const auto& b) { return a.name < b.name; }); + std::set external; + for (const auto& input : inputs) { + if (!external.insert(input.net).second || network.constantByNet[input.net]) + model.unsupportedReasons.push_back("aliased/constant external input in event contract: " + input.name); + network.externalInputs.push_back(input.net); + } + std::vector cells; + std::vector symbolicPrimitives; + std::vector resetClocks; + for (auto leaf : dnl->getLeaves()) { + const auto& instance = dnl->getDNLInstanceFromID(leaf); + if (instance.isTop()) continue; + std::map pins; + Primitive fallback; + fallback.name = instance.getFullPath(); + CellInfo info{syntheticKey(3, cells.size()), fallback.name, {}}; + for (auto* bit : instance.getSNLModel()->getBitTerms()) { + const auto& term = instance.getTerminalFromBitTerm(bit); + const size_t index = net(term); + pins.emplace(bit, index); + if (bit->getDirection() == Direction::Input) fallback.inputs.push_back(index); + else if (bit->getDirection() == Direction::Output) { + fallback.outputs.push_back(index); info.key = key(term); info.name = name(term); + } else info.error = "bidirectional primitive pin: " + name(term); + } + SymbolicPrimitive symbolic; + ResetClockPrimitive resetClock; + try { + auto primitive = makeNajaEventPrimitive(instance.getSNLInstance(), fallback.name, pins, &symbolic, &resetClock); + // Constant equipotentials already have an authoritative source. A cell + // tied into one is not silently treated as a second varying writer. + for (auto output : primitive.outputs) + if (network.constantByNet[output]) throw std::runtime_error("primitive output connected to a constant net"); + network.primitives.push_back(std::move(primitive)); + } catch (const std::exception& error) { + info.error = fallback.name + ": " + error.what(); + resetClock = {}; + network.primitives.push_back(std::move(fallback)); + } + symbolicPrimitives.push_back(std::move(symbolic)); + resetClocks.push_back(std::move(resetClock)); + cells.push_back(std::move(info)); + } + // A top wire observes a stored external level in selector mode. An observer + // buffer has no consumers, so this extra observation wave cannot affect any + // storage or capture event in the source circuit. + for (auto& output : outputs) { + if (!external.count(output.net) || network.constantByNet[output.net]) continue; + const size_t observed = network.netCount++; + network.constantByNet.push_back({}); netErrors.emplace_back(); + network.primitives.push_back(combinational("", {output.net}, {observed}, + [](const Bits& value) { return value; })); + SymbolicPrimitive observer; + observer.react = [](const SymbolicBits&, const SymbolicBits&, const SymbolicBits& value, + std::optional, bool) { return SymbolicReaction{{}, value}; }; + symbolicPrimitives.push_back(std::move(observer)); + ResetClockPrimitive observationClock; + observationClock.kind = ResetClockPrimitive::Kind::Combinational; + observationClock.outputs = {BoolExpr::Var(2)}; + resetClocks.push_back(std::move(observationClock)); + cells.push_back({syntheticKey(3, cells.size()), output.name, {}}); + output.net = observed; + } + if (!model.unsupportedReasons.empty()) return model; + + // Independent components are closed under ALL primitive input/output arcs, + // including clock/enable/asynchronous controls. Shared read-only PIs are not + // edges. This deliberately over-groups rather than dropping cross-island + // pulses; waves inside a component still execute in parallel. + const auto graph = analyzeDependencies(network); + std::vector owner(network.netCount, absent); + for (size_t i = 0; i < network.primitives.size(); ++i) { + for (auto output : network.primitives[i].outputs) { + if (owner[output] != absent) { + netErrors[output] = "multiple primitive writers"; + } else owner[output] = i; + } + } + + size_t nextVar = 2; + auto resetInterface = std::make_shared(); + resetInterface->singleInputChange = options.singleInputChange; + resetInterface->maxCompositionNodes = options.maxSymbolicNodes; + for (const auto& input : inputs) { + resetInterface->inputKeys.push_back(input.key); + resetInterface->inputNames.push_back(input.name); + } + resetInterface->currentInputs.resize(inputs.size()); + std::vector inputExpressions, selector; + BoolExpr* eventValue = nullptr; + if (!options.singleInputChange) { + for (const auto& input : inputs) + inputExpressions.push_back(variable(model, input.key, input.name, false, nextVar)); + } else { + // SEC aligns keys, not labels. Retain the exact original input keys as + // unused interface sentinels so selector positions cannot silently denote + // different pins on the two sides. Only selector/value drive transactions. + for (const auto& input : inputs) + variable(model, input.key, "$event.interface." + input.name, false, nextVar); + const size_t bits = boundaryEncodingBits(inputs.size() + 1); + for (size_t i = 0; i < bits; ++i) + selector.push_back(variable(model, syntheticKey(1, i + 1), "$event.select[" + std::to_string(i) + "]", false, nextVar)); + eventValue = variable(model, syntheticKey(1, bits + 1), "$event.value", false, nextVar); + for (auto* bit : selector) resetInterface->selectorSymbols.push_back(bit->getId()); + resetInterface->valueSymbol = eventValue->getId(); + inputExpressions.assign(inputs.size(), BoolExpr::createFalse()); + } + + model.initialCondition = BoolExpr::createTrue(); + const auto initialize = [&](const SignalKey& stateKey, BoolExpr* initial) { + auto* state = BoolExpr::Var(model.inputVarByKey.at(stateKey)); + model.initialCondition = BoolExpr::And(model.initialCondition, + BoolExpr::Not(BoolExpr::Xor(state, initial))); + if (initial->getOp() == Op::VAR && initial->getId() < 2) + model.initialStateValueByKey.emplace(stateKey, initial->getId() != 0); + }; + std::vector inputOrigins(inputs.size()); + for (size_t i = 0; i < inputs.size(); ++i) { + if (options.initialInputs) inputOrigins[i] = BoolExpr::Var(*options.initialInputs); + else { + // Categories 4 and 5 are reserved for reset counter/order symbols. + const auto originKey = syntheticKey(7, i); + auto* origin = variable(model, originKey, "$event.initial.input." + inputs[i].name, true, nextVar); + model.nextStateExprByStateKey.emplace(originKey, origin); + model.initialInputStateKeyByInputKey.emplace(inputs[i].key, originKey); + inputOrigins[i] = origin; + } + } + + for (size_t componentID = 0; componentID < graph.components.size(); ++componentID) { + const auto& members = graph.components[componentID]; + std::set used; + std::string failure; + for (auto member : members) { + const auto& primitive = network.primitives[member]; + used.insert(primitive.inputs.begin(), primitive.inputs.end()); + used.insert(primitive.outputs.begin(), primitive.outputs.end()); + if (failure.empty()) failure = cells[member].error; + } + for (auto index : used) + if (failure.empty() && !netErrors[index].empty()) failure = netErrors[index]; + Network local; + std::map localNet; + for (auto index : used) { + localNet.emplace(index, local.netCount++); + local.constantByNet.push_back(network.constantByNet[index]); + } + std::vector globalInputIndices; + std::vector localInputs; + for (size_t i = 0; i < inputs.size(); ++i) { + if (!used.count(inputs[i].net)) continue; + globalInputIndices.push_back(i); + local.externalInputs.push_back(localNet.at(inputs[i].net)); + localInputs.push_back(inputExpressions[i]); + } + std::vector localSymbolic; + for (auto member : members) { + auto primitive = network.primitives[member]; + for (auto& index : primitive.inputs) index = localNet.at(index); + for (auto& index : primitive.outputs) index = localNet.at(index); + local.primitives.push_back(std::move(primitive)); + localSymbolic.push_back(symbolicPrimitives[member]); + } + std::optional symbolic; + std::string symbolicFailure; + if (failure.empty()) { + SymbolicCompileOptions compile; + compile.initialInputValues.assign(local.externalInputs.size(), + options.initialInputs ? std::optional(*options.initialInputs) : std::nullopt); + compile.singleExternalInputChange = options.singleInputChange; + compile.maxWaves = options.limits.maxWaves; + compile.maxNodes = options.maxSymbolicNodes; + compile.maxSatConflicts = options.maxSatConflicts; + compile.maxSatDecisions = options.maxSatDecisions; + compile.workers = options.workers; + for (const auto& primitive : local.primitives) + compile.initialStorageValues.emplace_back(primitive.storageBits, + options.initialStorage ? std::optional(*options.initialStorage) : std::nullopt); + auto result = compileSymbolicNetwork({local, std::move(localSymbolic)}, compile); + if (result.certified()) symbolic = std::move(result.model); + else symbolicFailure = result.detail; + } + std::optional table; + const bool concreteInitials = (options.initialInputs || local.externalInputs.empty()) && + (options.initialStorage || std::all_of(local.primitives.begin(), local.primitives.end(), + [](const auto& primitive) { return !primitive.storageBits; })); + if (failure.empty() && !symbolic && !concreteInitials) + failure = "symbolic initialization has no certified macro: " + symbolicFailure; + if (failure.empty() && !symbolic) { + try { + EventModel reference(local, {}, options.workers); + CompileOptions compile; + // Missing options occur here only for zero-width input/storage vectors; + // a symbolic origin is never replaced by a finite compiler's zero bit. + compile.initialInputs.assign(local.externalInputs.size(), options.initialInputs.value_or(false)); + compile.singleExternalInputChange = options.singleInputChange; + compile.limits = options.limits; + for (const auto& primitive : local.primitives) + compile.initialStorage.emplace_back(primitive.storageBits, uint8_t(options.initialStorage.value_or(false))); + auto result = compileTransitionTable(reference, compile); + if (result.certified()) table = std::move(result.table); + else failure = std::string(certificationStatusName(result.status)) + ": " + result.detail + + "; symbolic certificate: " + symbolicFailure; + } catch (const std::exception& error) { failure = error.what(); } + } + if (table && (table->initials.size() != 1 || table->initials[0].boundary >= table->boundaries.size())) + failure = "event adapter requires a single explicitly initialized boundary"; + if (!failure.empty()) { + const std::string reason = "event component " + cells[members.front()].name + ": " + failure; + for (auto member : members) opaque(model, cells[member].key, cells[member].name, reason); + for (const auto& output : outputs) + if (owner[output.net] != absent && graph.componentOf[owner[output.net]] == componentID) { + model.skippedObservedOutputs.push_back(output.key); + opaque(model, output.key, output.name, reason); + } + continue; + } + std::vector state; + std::vector stateKeys; + const size_t stateWidth = symbolic ? symbolic->stateSymbols.size() : boundaryEncodingBits(table->boundaries.size()); + for (size_t bit = 0; bit < stateWidth; ++bit) { + const auto stateKey = syntheticKey(2, componentID, bit); + stateKeys.push_back(stateKey); + state.push_back(variable(model, stateKey, "$event.component[" + std::to_string(componentID) + "].state[" + std::to_string(bit) + "]", true, nextVar)); + if (!symbolic) initialize(stateKey, BoolExpr::Var((table->initials[0].boundary >> bit) & 1)); + } + if (symbolic) { + // Each symbolic BOOT parameter becomes a fixed-once state variable. PI + // origins are shared across islands; storage origins remain design-local. + std::unordered_map sharedOrigins; + for (size_t i = 0; i < globalInputIndices.size(); ++i) + if (const auto parameter = symbolic->initialInputSymbols.at(i)) + sharedOrigins.emplace(*parameter, inputOrigins.at(globalInputIndices[i])); + SymbolicBits parameters; + std::unordered_map directProjections; + for (size_t bit = 0; bit < symbolic->initialStateExpressions.size(); ++bit) { + auto* expression = symbolic->initialStateExpressions[bit]; + if (expression->getOp() == Op::VAR && expression->getId() >= 2) + directProjections.try_emplace(expression->getId(), bit); + } + for (size_t i = 0; i < symbolic->initialParameterSymbols.size(); ++i) { + const auto found = sharedOrigins.find(symbolic->initialParameterSymbols[i]); + if (found != sharedOrigins.end()) parameters.push_back(found->second); + else { + // If BOOT retains this origin verbatim in a boundary bit, that bit + // is an exact existential representative. No fixed-once copy is + // needed; the equality is used only in the initial relation. + const auto projection = directProjections.find(symbolic->initialParameterSymbols[i]); + if (projection != directProjections.end()) { + parameters.push_back(state.at(projection->second)); + continue; + } + const auto originKey = syntheticKey(8, componentID, i); + auto* origin = variable(model, originKey, "$event.initial.component[" + + std::to_string(componentID) + "].storage[" + std::to_string(i) + "]", true, nextVar); + model.nextStateExprByStateKey.emplace(originKey, origin); + parameters.push_back(origin); + } + } + model.initialCondition = BoolExpr::And(model.initialCondition, + encodeSymbolicInitialRelation(*symbolic, state, parameters)); + const auto initial = encodeSymbolicInitialState(*symbolic, parameters); + for (size_t bit = 0; bit < state.size(); ++bit) + if (initial[bit]->getOp() == Op::VAR && initial[bit]->getId() < 2) + model.initialStateValueByKey.emplace(stateKeys[bit], initial[bit]->getId() != 0); + } + const auto encoded = symbolic + ? encodeSymbolicMacro(*symbolic, local, state, localInputs, options.singleInputChange, selector, eventValue, globalInputIndices) + : encodeBoundaryTable(*table, local, state, localInputs, selector, eventValue, globalInputIndices); + for (size_t bit = 0; bit < state.size(); ++bit) + model.nextStateExprByStateKey.emplace(stateKeys[bit], encoded.nextState[bit]); + for (size_t i = 0; i < globalInputIndices.size(); ++i) + resetInterface->currentInputs[globalInputIndices[i]] = symbolic ? state.at(local.externalInputs[i]) + : finiteInputHistory(*table, state, local.externalInputs[i]); + for (const auto& output : outputs) { + if (owner[output.net] == absent || graph.componentOf[owner[output.net]] != componentID) continue; + model.observedOutputs.push_back(output.key); + model.observedOutputExprByKey.emplace(output.key, encoded.observedNets.at(localNet.at(output.net))); + } + } + // Reuse certified islands' input history rather than duplicating state. Only + // inputs outside those islands need separate remembered levels for reset. + for (size_t i = 0; i < inputs.size(); ++i) { + if (resetInterface->currentInputs[i]) continue; + const auto historyKey = syntheticKey(6, i); + auto* current = variable(model, historyKey, "$event.history." + inputs[i].name, true, nextVar); + resetInterface->currentInputs[i] = current; + initialize(historyKey, inputOrigins[i]); + auto* next = inputExpressions[i]; + if (options.singleInputChange) { + auto* selected = BoolExpr::createTrue(); + for (size_t bit = 0; bit < selector.size(); ++bit) + selected = BoolExpr::And(selected, (i >> bit) & 1 ? selector[bit] : BoolExpr::Not(selector[bit])); + next = symbolicMux(selected, eventValue, current); + } + model.nextStateExprByStateKey.emplace(historyKey, next); + } + for (const auto& output : outputs) { + if (owner[output.net] != absent) continue; + if (network.constantByNet[output.net]) { + model.observedOutputs.push_back(output.key); + model.observedOutputExprByKey.emplace(output.key, + *network.constantByNet[output.net] ? BoolExpr::createTrue() : BoolExpr::createFalse()); + } else { + model.skippedObservedOutputs.push_back(output.key); + opaque(model, output.key, output.name, netErrors[output.net].empty() ? "event output has no driver" : netErrors[output.net]); + } + } + recordOpaqueOutputlessCells(model, *dnl); + // Discovery failure limits automatic reset-cycle expansion only, not ordinary + // event semantics. No clock is guessed from a latch enable or a signal name. + const auto clock = discoverResetClock(network, resetClocks); + resetInterface->clockInputIndex = clock.externalInputIndex; + if (!clock.resolved()) resetInterface->clockError = clock.detail; + reuseInitialInputHistory(model, resetInterface->inputKeys, resetInterface->currentInputs); + model.eventResetInterface = std::move(resetInterface); + // Preserve the existing unit-fact fast path for genuinely concrete BOOTs. + // Only a set-valued start needs the general relation in the proof engines. + if (model.initialStateValueByKey.size() == model.stateBits.size()) + model.initialCondition = nullptr; + applyOpaquePolicy(model, top->getName().getString(), side); + return model; +} +} // namespace + +std::optional extractEventDesign( + naja::NL::SNLDesign* top, const BoundaryPairs& pairs, size_t side) { + const auto& options = supportOptions(); + if (!options.enabled) return {}; + // Automatic support must not turn an ordinary flop-only SEC run into an + // event protocol. Explicit low-level overrides also count as a request. + if (!options.explicitConfiguration && !options.initialInputs && !options.initialStorage && + !options.singleInputChange && !containsModeledLatch(top)) return {}; + if (!pairs.empty()) { + SequentialDesignModel result; + result.unsupportedReasons.push_back("event semantics do not yet support selected leaf boundaries"); + return result; + } + return extract(top, side); +} +} // namespace KEPLER_FORMAL::SEC::LATCH diff --git a/src/sec/latch/LatchNetlistAdapter.h b/src/sec/latch/LatchNetlistAdapter.h new file mode 100644 index 00000000..765d6a14 --- /dev/null +++ b/src/sec/latch/LatchNetlistAdapter.h @@ -0,0 +1,13 @@ +// Copyright 2026 keplertech.io +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include +#include "model/SequentialDesignModel.h" + +namespace KEPLER_FORMAL::SEC::LATCH { +// Empty when disabled, or when a latch-free design has no explicitly requested +// event configuration. Omitted BOOT values stay symbolic, never default to zero. +std::optional extractEventDesign( + naja::NL::SNLDesign* top, const BoundaryPairs& pairs, size_t side); +} // namespace KEPLER_FORMAL::SEC::LATCH diff --git a/src/sec/latch/LatchResetAdapter.cpp b/src/sec/latch/LatchResetAdapter.cpp new file mode 100644 index 00000000..45c9f4ea --- /dev/null +++ b/src/sec/latch/LatchResetAdapter.cpp @@ -0,0 +1,429 @@ +// Copyright 2026 keplertech.io +// SPDX-License-Identifier: Apache-2.0 + +#include "latch/LatchResetAdapter.h" + +#include +#include +#include +#include +#include +#include + +#include "strategy/SequentialEquivalenceStrategy.h" + +namespace KEPLER_FORMAL::SEC::LATCH { +namespace { +using Expressions = std::unordered_map; + +// A cumulative reachable-DAG work budget, not a process peak-memory limit. +class CompositionBudget { + public: + explicit CompositionBudget(size_t limit) : limit_(limit) { + if (!limit) throw std::invalid_argument("reset composition node budget must be positive"); + } + void account(BoolExpr* root) { + std::vector pending{root}; + while (!pending.empty()) { + auto* node = pending.back(); + pending.pop_back(); + if (!node || !seen_.insert(node).second) continue; + if (seen_.size() > limit_) + throw std::runtime_error("reset composition node budget exceeded"); + if (node->getLeft()) pending.push_back(node->getLeft()); + if (node->getRight()) pending.push_back(node->getRight()); + } + } + private: + size_t limit_; + std::unordered_set seen_; +}; + +BoolExpr* constant(bool value) { return value ? BoolExpr::createTrue() : BoolExpr::createFalse(); } +BoolExpr* mux(BoolExpr* select, BoolExpr* yes, BoolExpr* no) { + if (yes == no) return yes; + return BoolExpr::Or(BoolExpr::And(select, yes), BoolExpr::And(BoolExpr::Not(select), no)); +} + +// Composition needs accurate input history even for a pin unused by every +// certified island. Once the reset interface is consumed, a self-only history +// register is unobservable and can be projected out exactly. Do not remove an +// origin or a state participating in an initial relation/cross-design pairing. +void removeDeadInputHistory(SequentialDesignModel& model) { + std::unordered_set retained; + const auto retain = [&](BoolExpr* expression) { + if (expression) + for (auto id : expression->getSupportVars()) retained.insert(id); + }; + retain(model.initialCondition); + for (const auto& [key, expression] : model.observedOutputExprByKey) retain(expression); + for (const auto& [input, origin] : model.initialInputStateKeyByInputKey) + retained.insert(model.inputVarByKey.at(origin)); + std::unordered_map readers; + for (const auto& [key, expression] : model.nextStateExprByStateKey) + for (auto id : expression->getSupportVars()) ++readers[id]; + std::unordered_set discarded; + for (const auto& key : model.stateBits) { + if (key.first.size() != 3 || key.first[0] != (uint64_t(1) << 61) || key.first[1] != 6) + continue; + const auto id = model.inputVarByKey.at(key); + if (retained.count(id)) continue; + const auto ownSupport = model.nextStateExprByStateKey.at(key)->getSupportVars(); + if (readers[id] == ownSupport.count(id)) discarded.insert(key); + } + model.stateBits.erase(std::remove_if(model.stateBits.begin(), model.stateBits.end(), + [&](const auto& key) { return discarded.count(key); }), model.stateBits.end()); + for (const auto& key : discarded) { + model.nextStateExprByStateKey.erase(key); + model.initialStateValueByKey.erase(key); + model.inputVarByKey.erase(key); + model.displayNameByKey.erase(key); + } +} + +// Simultaneous substitution: replacement expressions are not themselves +// substituted. One memo is shared across every output and state in a stage. +class Substitute { + public: + Substitute(const Expressions& replacements, CompositionBudget& budget) + : replacements_(replacements), budget_(budget) {} + BoolExpr* operator()(BoolExpr* root) { + std::vector> pending{{root, false}}; + while (!pending.empty()) { + const auto [node, ready] = pending.back(); + pending.pop_back(); + if (!node || !node->isValid()) throw std::invalid_argument("invalid event expression"); + if (memo_.count(node)) continue; + if (node->getOp() == Op::VAR) { + const auto found = replacements_.find(node->getId()); + memo_[node] = found == replacements_.end() ? node : found->second; + } else if (!ready) { + pending.emplace_back(node, true); + if (node->getRight()) pending.emplace_back(node->getRight(), false); + pending.emplace_back(node->getLeft(), false); + } else { + auto* left = memo_.at(node->getLeft()); + switch (node->getOp()) { + case Op::NOT: memo_[node] = BoolExpr::Not(left); break; + case Op::AND: memo_[node] = BoolExpr::And(left, memo_.at(node->getRight())); break; + case Op::OR: memo_[node] = BoolExpr::Or(left, memo_.at(node->getRight())); break; + case Op::XOR: memo_[node] = BoolExpr::Xor(left, memo_.at(node->getRight())); break; + default: throw std::invalid_argument("unsupported event expression operation"); + } + } + if (memo_.count(node)) budget_.account(memo_.at(node)); + } + return memo_.at(root); + } + private: + const Expressions& replacements_; + CompositionBudget& budget_; + std::unordered_map memo_; +}; + +class Composer { + public: + Composer(const SequentialDesignModel& model, const EventResetInterface& interface, + CompositionBudget& budget) + : model_(model), interface_(interface), budget_(budget) {} + + Expressions initial() const { + Expressions state; + for (const auto& key : model_.stateBits) { + const auto id = model_.inputVarByKey.at(key); + state.emplace(id, BoolExpr::Var(id)); + } + return state; + } + + Expressions force(const Expressions& state, size_t index, bool value) const { + Expressions environment; + if (interface_.singleInputChange) { + select(environment, index, constant(value)); + } else { + Substitute previous(state, budget_); + for (size_t i = 0; i < interface_.inputKeys.size(); ++i) + environment.emplace(model_.inputVarByKey.at(interface_.inputKeys[i]), + i == index ? constant(value) : previous(interface_.currentInputs[i])); + } + return transition(state, environment); + } + + Expressions external(const Expressions& state, size_t resetIndex, bool active, + std::optional heldClock) const { + return transition(state, externalEnvironment(resetIndex, active, heldClock)); + } + + Expressions sampleAll(Expressions state, const std::vector& pins, + const std::vector>& ordering) const { + std::vector used(pins.size(), BoolExpr::createFalse()); + for (size_t stage = 0; stage < pins.size(); ++stage) { + std::vector requested; + auto* valid = BoolExpr::createFalse(); + for (size_t candidate = 0; candidate < pins.size(); ++candidate) { + auto* match = BoolExpr::Not(used[candidate]); + for (size_t bit = 0; bit < ordering[stage].size(); ++bit) + match = BoolExpr::And(match, ((candidate >> bit) & 1) ? ordering[stage][bit] + : BoolExpr::Not(ordering[stage][bit])); + requested.push_back(match); + valid = BoolExpr::Or(valid, match); + } + std::vector selected; + auto* earlierUsed = BoolExpr::createTrue(); + for (size_t candidate = 0; candidate < pins.size(); ++candidate) { + auto* fallback = BoolExpr::And(earlierUsed, BoolExpr::Not(used[candidate])); + selected.push_back(BoolExpr::Or(requested[candidate], + BoolExpr::And(BoolExpr::Not(valid), fallback))); + earlierUsed = BoolExpr::And(earlierUsed, used[candidate]); + } + Expressions environment; + for (size_t bit = 0; bit < interface_.selectorSymbols.size(); ++bit) { + auto* value = BoolExpr::createFalse(); + for (size_t candidate = 0; candidate < pins.size(); ++candidate) + if ((pins[candidate] >> bit) & 1) value = BoolExpr::Or(value, selected[candidate]); + environment.emplace(interface_.selectorSymbols[bit], value); + } + auto* value = BoolExpr::createFalse(); + for (size_t candidate = 0; candidate < pins.size(); ++candidate) { + value = BoolExpr::Or(value, BoolExpr::And(selected[candidate], + BoolExpr::Var(model_.inputVarByKey.at(interface_.inputKeys[pins[candidate]])))); + used[candidate] = BoolExpr::Or(used[candidate], selected[candidate]); + } + environment.emplace(*interface_.valueSymbol, value); + state = transition(state, environment); + } + return state; + } + + Expressions externalEnvironment(size_t resetIndex, bool active, + std::optional heldClock) const { + Expressions environment; + if (interface_.singleInputChange) { + auto* blocked = selected(resetIndex); + if (heldClock) blocked = BoolExpr::Or(blocked, selected(*heldClock)); + const size_t stutter = interface_.inputKeys.size(); + for (size_t bit = 0; bit < interface_.selectorSymbols.size(); ++bit) { + const auto id = interface_.selectorSymbols[bit]; + environment.emplace(id, mux(blocked, constant((stutter >> bit) & 1), BoolExpr::Var(id))); + } + environment.emplace(*interface_.valueSymbol, BoolExpr::Var(*interface_.valueSymbol)); + } else { + for (size_t index = 0; index < interface_.inputKeys.size(); ++index) { + const auto id = model_.inputVarByKey.at(interface_.inputKeys[index]); + environment.emplace(id, index == resetIndex ? constant(active) : + heldClock && index == *heldClock ? constant(false) : BoolExpr::Var(id)); + } + } + return environment; + } + + private: + BoolExpr* selected(size_t index) const { + auto* result = BoolExpr::createTrue(); + for (size_t bit = 0; bit < interface_.selectorSymbols.size(); ++bit) { + auto* variable = BoolExpr::Var(interface_.selectorSymbols[bit]); + result = BoolExpr::And(result, ((index >> bit) & 1) ? variable : BoolExpr::Not(variable)); + } + return result; + } + void select(Expressions& environment, size_t index, BoolExpr* value) const { + for (size_t bit = 0; bit < interface_.selectorSymbols.size(); ++bit) + environment.emplace(interface_.selectorSymbols[bit], constant((index >> bit) & 1)); + environment.emplace(*interface_.valueSymbol, value); + } + Expressions transition(const Expressions& state, const Expressions& environment) const { + Expressions replacements = state; + for (const auto& [id, expression] : environment) { + budget_.account(expression); + replacements.insert_or_assign(id, expression); + } + Substitute substitute(replacements, budget_); + Expressions next; + for (const auto& key : model_.stateBits) + next.emplace(model_.inputVarByKey.at(key), substitute(model_.nextStateExprByStateKey.at(key))); + return next; + } + const SequentialDesignModel& model_; + const EventResetInterface& interface_; + CompositionBudget& budget_; +}; + +std::string validate(const SequentialDesignModel& model, const SecResetSpec& reset, + size_t& resetIndex) { + if (model.eventContract.empty() || !model.eventResetInterface) + return "Latch reset cycles require retained event reset interface metadata"; + if (reset.cycles == 0) return "Latch reset cycles must be positive"; + if (reset.cycles == std::numeric_limits::max()) + return "Latch reset cycle count is too large"; + if (reset.ports.size() != 1) + return "Latch reset cycles currently require exactly one reset port"; + const auto& interface = *model.eventResetInterface; + if (!interface.clockError.empty()) return interface.clockError; + const size_t inputs = interface.inputKeys.size(); + if (!interface.clockInputIndex || *interface.clockInputIndex >= inputs) + return "Latch reset cycles require one unambiguous external clock carrier"; + if (interface.inputNames.size() != inputs || interface.currentInputs.size() != inputs) + return "Latch reset interface has inconsistent input metadata"; + const auto& requested = reset.ports.front().name; + std::vector exact, bits; + for (size_t i = 0; i < inputs; ++i) { + const auto& name = interface.inputNames[i]; + if (name == requested) exact.push_back(i); + else if (requested.find('[') == std::string::npos && name.size() > requested.size() + 2 && + name.compare(0, requested.size(), requested) == 0 && name[requested.size()] == '[' && name.back() == ']') + bits.push_back(i); + } + if (exact.size() == 1) resetIndex = exact.front(); + else if (exact.empty() && bits.size() == 1 && interface.inputNames[bits.front()] == requested + "[0]") + resetIndex = bits.front(); + else return "Latch reset port `" + requested + "` is missing or ambiguous; name a bus bit explicitly"; + if (resetIndex == *interface.clockInputIndex) + return "Latch reset port cannot also be the cycle clock carrier"; + std::set stateSymbols, environmentSymbols; + for (const auto& key : model.stateBits) { + const auto found = model.inputVarByKey.find(key); + if (found == model.inputVarByKey.end() || found->second < 2 || + !stateSymbols.insert(found->second).second || + !model.nextStateExprByStateKey.count(key) || + (!model.initialCondition && !model.initialStateValueByKey.count(key))) + return "Latch reset cycles require a valid event initial relation and transitions"; + } + for (const auto& key : model.environmentInputs) { + const auto found = model.inputVarByKey.find(key); + if (found == model.inputVarByKey.end() || found->second < 2 || + stateSymbols.count(found->second) || !environmentSymbols.insert(found->second).second) + return "Latch reset interface has invalid environment symbols"; + } + std::set inputSymbols; + for (size_t i = 0; i < inputs; ++i) { + if (!interface.currentInputs[i] || !interface.currentInputs[i]->isValid()) + return "Latch reset interface lacks remembered external input levels"; + for (auto id : interface.currentInputs[i]->getSupportVars()) + if (id >= 2 && !stateSymbols.count(id)) + return "Latch reset remembered input levels must depend only on state"; + if (!model.inputVarByKey.count(interface.inputKeys[i]) || + !environmentSymbols.count(model.inputVarByKey.at(interface.inputKeys[i])) || + !inputSymbols.insert(model.inputVarByKey.at(interface.inputKeys[i])).second) + return "Latch reset interface input is not an environment variable"; + } + if (interface.singleInputChange) { + if (!interface.valueSymbol || !environmentSymbols.count(*interface.valueSymbol) || + interface.selectorSymbols.empty() || + interface.selectorSymbols.size() >= std::numeric_limits::digits || + (size_t(1) << interface.selectorSymbols.size()) <= inputs) + return "Latch reset interface has invalid selector encoding"; + std::set selectors{*interface.valueSymbol}; + for (auto id : interface.selectorSymbols) + if (!environmentSymbols.count(id) || !selectors.insert(id).second) + return "Latch reset interface has invalid selector symbols"; + } + return {}; +} +} // namespace + +ResetCycleAdaptation adaptResetCycles(const SequentialDesignModel& model, const SecResetSpec& reset) { + size_t resetIndex = 0; + if (auto error = validate(model, reset, resetIndex); !error.empty()) return {{}, std::move(error)}; + try { + const auto& interface = *model.eventResetInterface; + const size_t clock = *interface.clockInputIndex; + const bool asserted = reset.ports.front().activeValue; + CompositionBudget budget(interface.maxCompositionNodes); + for (const auto& [key, expression] : model.nextStateExprByStateKey) budget.account(expression); + for (const auto& [key, expression] : model.observedOutputExprByKey) budget.account(expression); + for (auto* expression : interface.currentInputs) budget.account(expression); + SequentialDesignModel adapted = model; + size_t nextId = 2; + for (const auto& [key, id] : model.inputVarByKey) { + if (id >= std::numeric_limits::max() - std::numeric_limits::digits - 1) + return {{}, "Latch reset adapter has no free state symbol range"}; + nextId = std::max(nextId, id + 1); + } + std::vector sampledPins; + std::vector> ordering; + if (interface.singleInputChange) { + for (size_t i = 0; i < interface.inputKeys.size(); ++i) + if (i != resetIndex && i != clock) sampledPins.push_back(i); + if (!sampledPins.empty() && sampledPins.size() > interface.maxCompositionNodes / sampledPins.size()) + return {{}, "Latch reset ordering exceeds the composition node budget"}; + size_t width = 0; + for (size_t count = sampledPins.empty() ? 0 : sampledPins.size() - 1; count; count >>= 1) ++width; + ordering.resize(sampledPins.size()); + for (size_t stage = 0; stage < sampledPins.size(); ++stage) { + for (size_t bit = 0; bit < width; ++bit) { + if (nextId >= std::numeric_limits::max() - std::numeric_limits::digits - 1) + return {{}, "Latch reset adapter has no free ordering symbol range"}; + SignalKey key{{uint64_t(1) << 61, 5, stage, bit}, {0}}; + if (adapted.inputVarByKey.count(key)) return {{}, "Latch reset ordering key collides with existing input"}; + ordering[stage].push_back(BoolExpr::Var(nextId)); + budget.account(ordering[stage].back()); + adapted.environmentInputs.push_back(key); + adapted.inputVarByKey.emplace(key, nextId++); + adapted.displayNameByKey.emplace(key, "$event.reset.order[" + std::to_string(stage) + + "][" + std::to_string(bit) + "]"); + } + } + } + Composer composer(model, interface, budget); + const auto original = composer.initial(); + auto boot = composer.force(original, resetIndex, asserted); + boot = composer.force(boot, clock, false); + boot = interface.singleInputChange ? composer.sampleAll(std::move(boot), sampledPins, ordering) + : composer.external(boot, resetIndex, asserted, clock); + boot = composer.force(boot, clock, true); + boot = composer.force(boot, clock, false); + const auto released = composer.force(boot, resetIndex, !asserted); + const auto normal = composer.external(original, resetIndex, !asserted, {}); + std::vector count; + std::vector countKeys; + for (size_t value = reset.cycles, bit = 0; value != 0; value >>= 1, ++bit) { + SignalKey key{{uint64_t(1) << 61, 4, bit}, {0}}; + if (adapted.inputVarByKey.count(key)) return {{}, "Latch reset counter key collides with existing state"}; + countKeys.push_back(key); + count.push_back(BoolExpr::Var(nextId)); + adapted.inputVarByKey.emplace(key, nextId++); + adapted.stateBits.push_back(key); + adapted.initialStateValueByKey.emplace(key, (reset.cycles >> bit) & 1); + adapted.displayNameByKey.emplace(key, "$event.reset.remaining[" + std::to_string(bit) + "]"); + } + auto* active = BoolExpr::createFalse(); + auto* last = count.front(); + for (size_t bit = 0; bit < count.size(); ++bit) { + active = BoolExpr::Or(active, count[bit]); + if (bit) last = BoolExpr::And(last, BoolExpr::Not(count[bit])); + } + auto* borrow = BoolExpr::createTrue(); + for (size_t bit = 0; bit < count.size(); ++bit) { + adapted.nextStateExprByStateKey.emplace(countKeys[bit], + BoolExpr::And(active, BoolExpr::Xor(count[bit], borrow))); + budget.account(adapted.nextStateExprByStateKey.at(countKeys[bit])); + borrow = BoolExpr::And(borrow, BoolExpr::Not(count[bit])); + } + for (const auto& key : model.stateBits) { + const auto id = model.inputVarByKey.at(key); + adapted.nextStateExprByStateKey[key] = mux(active, + mux(last, released.at(id), boot.at(id)), normal.at(id)); + budget.account(adapted.nextStateExprByStateKey.at(key)); + } + const auto environment = composer.externalEnvironment(resetIndex, !asserted, {}); + Substitute observations(environment, budget); + for (const auto& key : model.observedOutputs) { + adapted.observedOutputExprByKey[key] = BoolExpr::And(BoolExpr::Not(active), + observations(model.observedOutputExprByKey.at(key))); + budget.account(adapted.observedOutputExprByKey.at(key)); + } + adapted.eventContract += ";reset_cycles=" + std::to_string(reset.cycles) + + ";reset_port=" + interface.inputNames[resetIndex] + + ";reset_active=" + std::to_string(asserted) + + ";reset_clock=" + interface.inputNames[clock] + ";reset_protocol=low-sample-high-low-release" + + (interface.singleInputChange ? ";reset_sampling=all-levels-all-arrival-orders" : ";reset_sampling=atomic-level-vector"); + adapted.eventResetCycles = reset.cycles; + // A second adaptation must not accidentally apply another prefix. + adapted.eventResetInterface.reset(); + removeDeadInputHistory(adapted); + return {std::move(adapted), {}}; + } catch (const std::exception& error) { + return {{}, std::string("Cannot compose latch reset cycles: ") + error.what()}; + } +} +} // namespace KEPLER_FORMAL::SEC::LATCH diff --git a/src/sec/latch/LatchResetAdapter.h b/src/sec/latch/LatchResetAdapter.h new file mode 100644 index 00000000..bc92c796 --- /dev/null +++ b/src/sec/latch/LatchResetAdapter.h @@ -0,0 +1,40 @@ +// Copyright 2026 keplertech.io +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include +#include +#include + +#include "model/SequentialDesignModel.h" + +namespace KEPLER_FORMAL::SEC { +struct SecResetSpec; +namespace LATCH { + +// Owned expressions survive compact extraction. currentInputs describes the +// remembered external levels at a settled boundary, never fresh environment PIs. +struct EventResetInterface { + std::vector inputKeys; + std::vector inputNames; + std::vector currentInputs; + bool singleInputChange = false; + std::vector selectorSymbols; + std::optional valueSymbol; + std::optional clockInputIndex; + std::string clockError; + size_t maxCompositionNodes = 2000000; +}; + +struct ResetCycleAdaptation { + std::optional model; + std::string error; +}; + +// Compile a finite reset-cycle prefix by composing already-certified event +// transitions. Original model and ambient configuration remain unchanged. +ResetCycleAdaptation adaptResetCycles(const SequentialDesignModel& model, + const SecResetSpec& reset); + +} // namespace LATCH +} // namespace KEPLER_FORMAL::SEC diff --git a/src/sec/latch/LatchResetClock.cpp b/src/sec/latch/LatchResetClock.cpp new file mode 100644 index 00000000..63ab4702 --- /dev/null +++ b/src/sec/latch/LatchResetClock.cpp @@ -0,0 +1,183 @@ +// Copyright 2026 keplertech.io +// SPDX-License-Identifier: Apache-2.0 +#include "latch/LatchResetClock.h" + +#include +#include +#include + +namespace KEPLER_FORMAL::SEC::LATCH { +namespace { +struct Route { + enum class Kind { Unknown, Constant, Literal }; + Kind kind = Kind::Unknown; + size_t input = 0; + // A constant value, or the inversion of the external input literal. + bool bit = false; +}; + +Route constant(bool bit) { return {Route::Kind::Constant, 0, bit}; } +Route invert(Route value) { + if (value.kind != Route::Kind::Unknown) value.bit = !value.bit; + return value; +} +bool isConstant(const Route& value, bool bit) { + return value.kind == Route::Kind::Constant && value.bit == bit; +} +Route combine(Op op, Route a, Route b) { + if (op == Op::AND) { + if (isConstant(a, false) || isConstant(b, false)) return constant(false); + if (isConstant(a, true)) return b; + if (isConstant(b, true)) return a; + } else if (op == Op::OR) { + if (isConstant(a, true) || isConstant(b, true)) return constant(true); + if (isConstant(a, false)) return b; + if (isConstant(b, false)) return a; + } else if (op == Op::XOR) { + if (a.kind == Route::Kind::Constant) return a.bit ? invert(b) : b; + if (b.kind == Route::Kind::Constant) return b.bit ? invert(a) : a; + } else throw std::invalid_argument("invalid clock routing operator"); + if (a.kind == Route::Kind::Literal && b.kind == Route::Kind::Literal && + a.input == b.input) { + if (op == Op::XOR) return constant(a.bit != b.bit); + if (a.bit == b.bit) return a; + return constant(op == Op::OR); + } + return {}; +} + +Route expressionRoute(BoolExpr* root, const Primitive& primitive, + const std::vector& routes) { + std::unordered_map memo; + std::vector> pending{{root, false}}; + while (!pending.empty()) { + const auto [node, ready] = pending.back(); + pending.pop_back(); + if (!node || !node->isValid()) throw std::invalid_argument("missing or invalid clock routing expression"); + if (memo.count(node)) continue; + if (node->getOp() == Op::VAR) { + if (node->getId() < 2) memo.emplace(node, constant(node->getId() != 0)); + else { + const size_t pin = node->getId() - 2; + if (pin >= primitive.inputs.size()) + throw std::invalid_argument("clock routing expression references a non-input symbol"); + memo.emplace(node, routes.at(primitive.inputs[pin])); + } + continue; + } + const auto op = node->getOp(); + if (op != Op::NOT && op != Op::AND && op != Op::OR && op != Op::XOR) + throw std::invalid_argument("invalid clock routing operator"); + if (!ready) { + pending.emplace_back(node, true); + if (op != Op::NOT) pending.emplace_back(node->getRight(), false); + pending.emplace_back(node->getLeft(), false); + continue; + } + const auto left = memo.at(node->getLeft()); + memo.emplace(node, op == Op::NOT ? invert(left) : + combine(op, left, memo.at(node->getRight()))); + } + return memo.at(root); +} + +ResetClockDiscovery unsupported(std::string detail) { + return {ResetClockDiscovery::Status::Unsupported, {}, {}, std::move(detail)}; +} +} // namespace + +ResetClockDiscovery discoverResetClock( + const Network& network, const std::vector& metadata) { + try { + if (metadata.size() != network.primitives.size()) + return unsupported("automatic reset clock discovery requires metadata for every primitive"); + if (!network.constantByNet.empty() && network.constantByNet.size() != network.netCount) + return unsupported("invalid constant net table during automatic reset clock discovery"); + std::vector routes(network.netCount); + std::vector hasSource(network.netCount, false); + std::vector> consumers(network.netCount); + for (size_t i = 0; i < network.externalInputs.size(); ++i) { + const size_t net = network.externalInputs[i]; + if (net >= network.netCount || hasSource[net]) + return unsupported("invalid or duplicate external net during automatic reset clock discovery"); + hasSource[net] = true; + routes[net] = {Route::Kind::Literal, i, false}; + } + for (size_t net = 0; net < network.constantByNet.size(); ++net) { + if (!network.constantByNet[net].has_value()) continue; + if (hasSource[net]) return unsupported("external reset clock input is also a constant net"); + hasSource[net] = true; + routes[net] = constant(*network.constantByNet[net]); + } + size_t edgeCells = 0; + std::deque work; + std::vector queued(network.primitives.size(), false); + for (size_t cell = 0; cell < network.primitives.size(); ++cell) { + const auto& primitive = network.primitives[cell]; + const auto& meta = metadata[cell]; + if (meta.kind == ResetClockPrimitive::Kind::Unsupported) + return unsupported("unclassified primitive prevents automatic reset clock discovery: " + primitive.name); + if (meta.kind == ResetClockPrimitive::Kind::FlipFlop) ++edgeCells; + if (meta.kind == ResetClockPrimitive::Kind::Combinational && + (primitive.storageBits != 0 || meta.outputs.size() != primitive.outputs.size())) + return unsupported("incomplete combinational clock routing metadata: " + primitive.name); + for (size_t net : primitive.inputs) { + if (net >= network.netCount) + return unsupported("out-of-range input net during automatic reset clock discovery"); + if (meta.kind == ResetClockPrimitive::Kind::Combinational) consumers[net].push_back(cell); + } + for (size_t net : primitive.outputs) { + if (net >= network.netCount || hasSource[net]) + return unsupported("multiple drivers or invalid output net during automatic reset clock discovery"); + hasSource[net] = true; + } + if (meta.kind == ResetClockPrimitive::Kind::Combinational) { + queued[cell] = true; + work.push_back(cell); + } + } + if (edgeCells == 0) + return {ResetClockDiscovery::Status::NoEdgeClock, {}, {}, + "reset cycles require an explicit flip-flop clock; latch enables do not define a clock cycle"}; + + // Route facts only become more precise (unknown -> constant/literal). Gates + // revisit only when an input fact is discovered, so routing cycles terminate + // without choosing an arbitrary clock or recursing through the netlist. + while (!work.empty()) { + const size_t cell = work.front(); + work.pop_front(); + queued[cell] = false; + const auto& primitive = network.primitives[cell]; + for (size_t pin = 0; pin < primitive.outputs.size(); ++pin) { + const size_t net = primitive.outputs[pin]; + if (routes[net].kind != Route::Kind::Unknown) continue; + const auto route = expressionRoute(metadata[cell].outputs[pin], primitive, routes); + if (route.kind == Route::Kind::Unknown) continue; + routes[net] = route; + for (size_t consumer : consumers[net]) if (!queued[consumer]) { + queued[consumer] = true; + work.push_back(consumer); + } + } + } + + std::optional carrier; + for (size_t cell = 0; cell < network.primitives.size(); ++cell) { + if (metadata[cell].kind != ResetClockPrimitive::Kind::FlipFlop) continue; + const auto& primitive = network.primitives[cell]; + const auto clock = expressionRoute(metadata[cell].clock, primitive, routes); + if (clock.kind == Route::Kind::Constant) + return unsupported("flip-flop clock is constant, so automatic reset cycles cannot clock it: " + primitive.name); + if (clock.kind != Route::Kind::Literal) + return unsupported("flip-flop clock is gated, state-generated, cyclic, or has no unique external carrier: " + primitive.name); + if (carrier && *carrier != clock.input) + return unsupported("multiple independent flip-flop clock roots require an explicit reset clock protocol"); + carrier = clock.input; + } + return {ResetClockDiscovery::Status::Resolved, network.externalInputs.at(*carrier), carrier, + "one external reset clock carrier resolved from every explicit flip-flop clock"}; + } catch (const std::exception& error) { + return unsupported(std::string("automatic reset clock discovery failed: ") + error.what()); + } +} +} // namespace KEPLER_FORMAL::SEC::LATCH diff --git a/src/sec/latch/LatchResetClock.h b/src/sec/latch/LatchResetClock.h new file mode 100644 index 00000000..ff9aa723 --- /dev/null +++ b/src/sec/latch/LatchResetClock.h @@ -0,0 +1,43 @@ +// Copyright 2026 keplertech.io +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include +#include +#include +#include "BoolExpr.h" +#include "latch/LatchEventModel.h" + +namespace KEPLER_FORMAL::SEC::LATCH { + +// Copied semantic metadata, parallel to Network::primitives. Expressions use +// local input symbols pinIndex + 2; symbols 0 and 1 are Boolean constants. +// Latch enables are deliberately not clock roots. This discovery schedules +// reset cycles; it does not classify or replace latch event semantics. +struct ResetClockPrimitive { + enum class Kind { Combinational, Latch, FlipFlop, Unsupported }; + Kind kind = Kind::Unsupported; + BoolExpr* clock = nullptr; + std::vector outputs; +}; + +struct ResetClockDiscovery { + enum class Status { Resolved, NoEdgeClock, Unsupported }; + Status status = Status::Unsupported; + std::optional rootNet; + std::optional externalInputIndex; + std::string detail; + + bool resolved() const { return status == Status::Resolved; } +}; + +// Finds one common external carrier for EVERY explicit flip-flop clock. Only +// exact constant/literal reductions through combinational gates are accepted. +// An arbitrary multi-input gate, state-generated clock, or second root cannot +// be guessed away. Both edge polarities on the same root are supported: the +// reset sequencer emits complete source-clock cycles, settling after each edge. +// The implementation is iterative, including for cyclic or very deep routing. +ResetClockDiscovery discoverResetClock( + const Network& network, const std::vector& metadata); + +} // namespace KEPLER_FORMAL::SEC::LATCH diff --git a/src/sec/latch/LatchSettlingCompiler.cpp b/src/sec/latch/LatchSettlingCompiler.cpp new file mode 100644 index 00000000..ced37549 --- /dev/null +++ b/src/sec/latch/LatchSettlingCompiler.cpp @@ -0,0 +1,449 @@ +// Copyright 2026 keplertech.io +// SPDX-License-Identifier: Apache-2.0 + +#include "LatchSettlingCompiler.h" + +#include +#include +#include +#include +#include +#include + +namespace KEPLER_FORMAL::SEC::LATCH { +namespace { + +struct Failure { + CertificationStatus status; + std::string detail; +}; + +[[noreturn]] void invalid(const std::string& detail) { + throw Failure{CertificationStatus::Invalid, detail}; +} + +[[noreturn]] void resource(const std::string& detail) { + throw Failure{CertificationStatus::ResourceLimit, detail}; +} + +void addWidth(size_t& total, size_t width, size_t maximum) { + if (total > maximum || width > maximum - total) { + resource("Complete reference state exceeds the state-bit limit"); + } + total += width; +} + +void checkBits(const Bits& bits) { + if (std::any_of(bits.begin(), bits.end(), [](uint8_t bit) { return bit > 1; })) { + invalid("A reference value is not Boolean"); + } +} + +void checkState(const State& state, const State& shape, + const CompilerLimits& limits) { + if (state.current.size() != shape.current.size() || + state.previous.size() != state.current.size() || + state.storage.size() != shape.storage.size() || + state.active.size() != shape.active.size() || + state.active.size() != state.storage.size()) { + invalid("Reference transitions changed the complete-state layout"); + } + size_t bits = 0; + addWidth(bits, 2, limits.maxStateBits); // BOOT and sticky error. + addWidth(bits, state.current.size(), limits.maxStateBits); + addWidth(bits, state.previous.size(), limits.maxStateBits); + addWidth(bits, state.active.size(), limits.maxStateBits); + checkBits(state.current); + checkBits(state.previous); + checkBits(state.active); + for (size_t primitive = 0; primitive < state.storage.size(); ++primitive) { + if (state.storage[primitive].size() != shape.storage[primitive].size()) { + invalid("Reference transitions changed the primitive storage layout"); + } + addWidth(bits, state.storage[primitive].size(), limits.maxStateBits); + checkBits(state.storage[primitive]); + } +} + +void checkNetworkSize(const Network& network, const CompilerLimits& limits) { + size_t bits = 0; + addWidth(bits, 2, limits.maxStateBits); + addWidth(bits, network.netCount, limits.maxStateBits); + addWidth(bits, network.netCount, limits.maxStateBits); + addWidth(bits, network.primitives.size(), limits.maxStateBits); + for (const auto& primitive : network.primitives) { + addWidth(bits, primitive.storageBits, limits.maxStateBits); + } +} + +size_t valuationCount(size_t bits, size_t bitLimit, const char* description) { + if (bits > bitLimit || bits >= std::numeric_limits::digits) { + resource(std::string(description) + " exceeds the Boolean-enumeration limit"); + } + return size_t{1} << bits; +} + +Bits valuation(size_t code, size_t width) { + Bits result(width); + for (size_t bit = 0; bit < width; ++bit) { + result[bit] = static_cast((code >> bit) & size_t{1}); + } + return result; +} + +std::vector bootstrapSeedNets(const Network& network) { + std::vector result; + const bool inputDependentProjection = std::any_of( + network.primitives.begin(), network.primitives.end(), + [](const Primitive& primitive) { return bool(primitive.initialOutputValues); }); + for (const auto& primitive : network.primitives) { + // Input-dependent BOOT projections read one frozen preprojection snapshot. + // A storage output's seed can therefore influence another cell even though + // that output is overwritten by its own storage projection afterward. + if (primitive.storageBits == 0 || inputDependentProjection) { + result.insert(result.end(), primitive.outputs.begin(), primitive.outputs.end()); + } + } + // The EventModel constructor enforces unique ownership; sorting also makes + // enumeration independent of how the caller listed the primitives. + std::sort(result.begin(), result.end()); + result.erase(std::unique(result.begin(), result.end()), result.end()); + return result; +} + +struct GraphNode { + State state; + std::vector next; + bool stable = false; +}; + +void exploreEpisode(const std::vector& entries, + const EpisodeRelation& relation, + const CompilerLimits& limits, EpisodeResult& result) { + if (entries.empty()) { + invalid("An empty episode-entry relation cannot establish a certificate"); + } + if (!relation.stable || !relation.successors) { + invalid("The episode reference relation is missing a required callback"); + } + std::vector nodes; + std::unordered_map byState; + std::vector roots; + const auto insert = [&](const State& state) { + checkState(state, entries.front(), limits); + auto key = state.key(); + const auto found = byState.find(key); + if (found != byState.end()) { + return found->second; + } + if (nodes.size() >= limits.maxEpisodeStates) { + resource("Episode graph exceeds the complete-state limit"); + } + const size_t index = nodes.size(); + byState.emplace(std::move(key), index); + nodes.push_back({state, {}, false}); + result.exploredStates = nodes.size(); + return index; + }; + for (const auto& entry : entries) { + roots.push_back(insert(entry)); + } + + for (size_t index = 0; index < nodes.size(); ++index) { + // Insertion below can reallocate nodes; never keep a reference into it. + const State state = nodes[index].state; + if (state.error) { + invalid("Reachable reference error: " + state.errorReason); + } + const bool stable = relation.stable(state); + nodes[index].stable = stable; + const auto successors = relation.successors(state); + if (successors.empty()) { + invalid("A reachable reference state has no successor (missing totality)"); + } + if (successors.size() > + limits.maxEpisodeTransitions - result.exploredTransitions) { + resource("Episode graph exceeds the transition limit"); + } + result.exploredTransitions += successors.size(); + if (stable) { + for (const auto& successor : successors) { + if (successor != state) { + invalid("Stable reference states must have only identity successors"); + } + } + result.stableStates.push_back(state); + continue; // Identity padding does not count toward the settling depth. + } + std::vector next; + for (const auto& successor : successors) { + next.push_back(insert(successor)); + } + std::sort(next.begin(), next.end()); + next.erase(std::unique(next.begin(), next.end()), next.end()); + nodes[index].next = std::move(next); + } + + // Remove stable identity edges and topologically sort the remaining graph. + // A cycle is an allowed infinite execution, even if other branches settle. + std::vector incoming(nodes.size(), 0); + for (const auto& node : nodes) { + for (const size_t next : node.next) { + ++incoming[next]; + } + } + std::vector order; + for (size_t index = 0; index < nodes.size(); ++index) { + if (incoming[index] == 0) { + order.push_back(index); + } + } + for (size_t cursor = 0; cursor < order.size(); ++cursor) { + for (const size_t next : nodes[order[cursor]].next) { + if (--incoming[next] == 0) { + order.push_back(next); + } + } + } + if (order.size() != nodes.size()) { + throw Failure{CertificationStatus::NonSettling, + "A reachable nonstable cycle admits an infinite episode"}; + } + + std::vector depth(nodes.size(), 0); + for (auto position = order.rbegin(); position != order.rend(); ++position) { + for (const size_t next : nodes[*position].next) { + depth[*position] = std::max(depth[*position], depth[next] + 1); + } + } + for (const size_t root : roots) { + result.maxWaves = std::max(result.maxWaves, depth[root]); + } + if (result.stableStates.empty()) { + invalid("The reference graph has no stable result"); + } + std::sort(result.stableStates.begin(), result.stableStates.end()); + if (result.stableStates.size() != 1) { + throw Failure{CertificationStatus::OrderDependent, + "Allowed executions reach different complete boundary states"}; + } + if (result.maxWaves > limits.maxWaves) { + throw Failure{CertificationStatus::UnprovedBound, + "The exact settling depth exceeds the permitted unfolding bound"}; + } + result.status = CertificationStatus::Certified; +} + +EpisodeResult bootstrapImpl(const EventModel& model, const Bits& inputs, + const std::vector& initialStorage, + const CompilerLimits& limits) { + checkNetworkSize(model.network(), limits); + const auto seedNets = bootstrapSeedNets(model.network()); + const size_t count = valuationCount(seedNets.size(), limits.maxBootstrapSeedBits, + "Auxiliary bootstrap seed space"); + if (count > limits.maxInitialConfigurations || count > limits.maxEpisodeStates) { + resource("Bootstrap seed space exceeds the initial-configuration limit"); + } + std::vector entries; + entries.reserve(count); + for (size_t code = 0; code < count; ++code) { + Bits seeds(model.network().netCount, 0); + for (size_t bit = 0; bit < seedNets.size(); ++bit) { + seeds[seedNets[bit]] = static_cast((code >> bit) & size_t{1}); + } + entries.push_back(model.bootstrap(inputs, initialStorage, seeds)); + } + return certifyEpisode(model, entries, limits); +} + +template +Result guarded(Work&& work) { + Result result; + try { + work(result); + } catch (const Failure& failure) { + result.status = failure.status; + result.detail = failure.detail; + } catch (const Limit& limit) { + result.status = CertificationStatus::ResourceLimit; + result.detail = limit.what(); + } catch (const std::bad_alloc&) { + result.status = CertificationStatus::ResourceLimit; + result.detail = "Memory exhausted while constructing the complete certificate"; + } catch (const std::exception& error) { + result.status = CertificationStatus::Invalid; + result.detail = error.what(); + } + return result; +} + +void requireCertificate(const EpisodeResult& episode, const std::string& context) { + if (!episode.certified()) { + throw Failure{episode.status, context + ": " + episode.detail}; + } +} + +} // namespace + +const char* certificationStatusName(CertificationStatus status) { + switch (status) { + case CertificationStatus::Certified: + return "certified"; + case CertificationStatus::NonSettling: + return "non-settling"; + case CertificationStatus::OrderDependent: + return "order-dependent"; + case CertificationStatus::Invalid: + return "invalid-reference"; + case CertificationStatus::ResourceLimit: + return "resource-limit"; + case CertificationStatus::UnprovedBound: + return "unproved-bound"; + } + return "invalid-status"; +} + +EpisodeResult certifyEpisode(const std::vector& entries, + const EpisodeRelation& relation, + const CompilerLimits& limits) { + return guarded([&](EpisodeResult& result) { + exploreEpisode(entries, relation, limits, result); + }); +} + +EpisodeResult certifyEpisode(const EventModel& model, + const std::vector& entries, + const CompilerLimits& limits) { + return certifyEpisode(entries, + {[&](const State& state) { return model.stable(state); }, + [&](const State& state) { return model.successors(state); }}, + limits); +} + +EpisodeResult certifyBootstrap(const EventModel& model, const Bits& inputs, + const std::vector& initialStorage, + const CompilerLimits& limits) { + return guarded([&](EpisodeResult& result) { + result = bootstrapImpl(model, inputs, initialStorage, limits); + }); +} + +CompileResult compileTransitionTable(const EventModel& model, + const CompileOptions& options) { + return guarded([&](CompileResult& result) { + const auto& network = model.network(); + const auto& limits = options.limits; + checkNetworkSize(network, limits); + checkBits(options.initialInputs); + if (options.initialInputs.size() != network.externalInputs.size()) { + invalid("Initial external valuation has the wrong width"); + } + const size_t inputCount = valuationCount(network.externalInputs.size(), + limits.maxExternalBits, + "External input space"); + if (!options.initialStorage.empty() && + options.initialStorage.size() != network.primitives.size()) { + invalid("Initial storage relation has the wrong primitive count"); + } + std::vector baseStorage; + std::vector> unknown; + for (size_t primitive = 0; primitive < network.primitives.size(); ++primitive) { + const size_t width = network.primitives[primitive].storageBits; + baseStorage.emplace_back(width, 0); + if (!options.initialStorage.empty() && + options.initialStorage[primitive].size() != width) { + invalid("Initial storage relation has the wrong primitive width"); + } + for (size_t bit = 0; bit < width; ++bit) { + const auto value = options.initialStorage.empty() + ? std::optional{} + : options.initialStorage[primitive][bit]; + if (!value) { + unknown.emplace_back(primitive, bit); + } else if (*value > 1) { + invalid("Initial storage relation contains a non-Boolean bit"); + } else { + baseStorage.back()[bit] = *value; + } + } + } + const size_t initialCount = valuationCount(unknown.size(), + limits.maxInitialStorageBits, + "Initial storage space"); + const size_t seedCount = valuationCount(bootstrapSeedNets(network).size(), + limits.maxBootstrapSeedBits, + "Auxiliary bootstrap seed space"); + if (initialCount > limits.maxInitialConfigurations || + seedCount > limits.maxInitialConfigurations / initialCount) { + resource("Complete initialization relation exceeds the configuration limit"); + } + + TransitionTable table; + table.initialInputs = options.initialInputs; + table.singleExternalInputChange = options.singleExternalInputChange; + std::unordered_map byBoundary; + const auto insertBoundary = [&](const State& boundary) { + if (!model.stable(boundary)) { + invalid("Only complete stable states may become macro boundaries"); + } + auto key = boundary.key(); + const auto found = byBoundary.find(key); + if (found != byBoundary.end()) { + return found->second; + } + if (table.boundaries.size() >= limits.maxBoundaryStates) { + resource("Reachable complete-boundary set exceeds its state limit"); + } + const size_t index = table.boundaries.size(); + byBoundary.emplace(std::move(key), index); + table.boundaries.push_back(boundary); + return index; + }; + + for (size_t code = 0; code < initialCount; ++code) { + auto storage = baseStorage; + for (size_t bit = 0; bit < unknown.size(); ++bit) { + const auto [primitive, position] = unknown[bit]; + storage[primitive][position] = + static_cast((code >> bit) & size_t{1}); + } + const auto episode = certifyBootstrap(model, options.initialInputs, storage, limits); + requireCertificate(episode, "Bootstrap origin " + std::to_string(code)); + table.maxWaves = std::max(table.maxWaves, episode.maxWaves); + const size_t boundary = insertBoundary(episode.stableStates.front()); + table.initials.push_back({std::move(storage), boundary}); + } + + // BFS closure is the boundary invariant. No externally allowed input may be + // skipped because it fails to settle or yields an inconvenient next state. + for (size_t from = 0; from < table.boundaries.size(); ++from) { + const State boundary = table.boundaries[from]; + for (size_t code = 0; code < inputCount; ++code) { + Bits input = valuation(code, network.externalInputs.size()); + if (options.singleExternalInputChange) { + size_t changes = 0; + for (size_t bit = 0; bit < input.size(); ++bit) { + changes += input[bit] != boundary.current[network.externalInputs[bit]]; + } + if (changes > 1) { + continue; + } + } + if (table.rows.size() >= limits.maxTransactions) { + resource("Reachable macro transition table exceeds its transaction limit"); + } + const State entry = model.admit(boundary, input); + const auto episode = certifyEpisode(model, {entry}, limits); + requireCertificate(episode, "Boundary " + std::to_string(from) + + ", external valuation " + std::to_string(code)); + table.maxWaves = std::max(table.maxWaves, episode.maxWaves); + const size_t to = insertBoundary(episode.stableStates.front()); + table.rows.push_back({from, std::move(input), to}); + } + } + result.status = CertificationStatus::Certified; + result.table = std::move(table); + }); +} + +} // namespace KEPLER_FORMAL::SEC::LATCH diff --git a/src/sec/latch/LatchSettlingCompiler.h b/src/sec/latch/LatchSettlingCompiler.h new file mode 100644 index 00000000..a351da96 --- /dev/null +++ b/src/sec/latch/LatchSettlingCompiler.h @@ -0,0 +1,130 @@ +// Copyright 2026 keplertech.io +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include +#include +#include +#include +#include + +#include "LatchEventModel.h" + +namespace KEPLER_FORMAL::SEC::LATCH { + +enum class CertificationStatus { + Certified, + NonSettling, + OrderDependent, + Invalid, + ResourceLimit, + UnprovedBound, +}; + +const char* certificationStatusName(CertificationStatus status); + +// Every limit is a rejection threshold, never an assumption removing executions. +// maxWaves limits compilation depth; cycle detection explores complete states +// independently of that candidate depth, within the graph resource limits. +struct CompilerLimits { + size_t maxStateBits = 512; + size_t maxEpisodeStates = 65536; + size_t maxEpisodeTransitions = 262144; + size_t maxBoundaryStates = 4096; + size_t maxTransactions = 65536; + size_t maxInitialConfigurations = 4096; + size_t maxExternalBits = 12; + size_t maxInitialStorageBits = 12; + size_t maxBootstrapSeedBits = 12; + size_t maxWaves = 256; +}; + +// The generic interface also permits testing totality, absorbing stability, and +// complete-state uniqueness independently of primitive-specific construction. +// Callbacks describe the entire relation, not one sampled execution. +struct EpisodeRelation { + std::function stable; + std::function(const State&)> successors; +}; + +struct EpisodeResult { + CertificationStatus status = CertificationStatus::Invalid; + std::string detail; + // Complete stable states, not merely equal present outputs. Certified implies + // exactly one result. On failure this vector is diagnostic, not a certificate. + std::vector stableStates; + size_t maxWaves = 0; + size_t exploredStates = 0; + size_t exploredTransitions = 0; + + bool certified() const { return status == CertificationStatus::Certified; } +}; + +// Explore the complete reachable episode graph. A nonstable reachable cycle is +// NonSettling, a missing/error transition Invalid, and distinct full boundaries +// OrderDependent. Acyclic graphs give an exact longest wave count. Stable states +// must have identity successors; they are not permitted to vanish from a bound. +EpisodeResult certifyEpisode(const std::vector& entries, + const EpisodeRelation& relation, + const CompilerLimits& limits = {}); +EpisodeResult certifyEpisode(const EventModel& model, + const std::vector& entries, + const CompilerLimits& limits = {}); + +// Fix intended storage and external inputs, independently enumerate ALL Boolean +// seeds of combinational outputs, and certify their joint set of BOOT entries. +// If a storage-output BOOT projection reads pins, enumerate every internal output +// seed: another storage output's overwritten seed can affect that projection. +EpisodeResult certifyBootstrap(const EventModel& model, const Bits& inputs, + const std::vector& initialStorage, + const CompilerLimits& limits = {}); + +struct CompileOptions { + Bits initialInputs; + // One vector per primitive, including empty vectors for zero-storage gates. + // nullopt means a fixed-once, universally included initial Boolean choice. + // An empty outer vector means every primitive's storage bit is unspecified. + std::vector>> initialStorage; + // An explicit environment restriction, OFF by default. It constrains only + // external transactions, never simultaneous internally generated pin changes. + bool singleExternalInputChange = false; + CompilerLimits limits; +}; + +struct BoundaryTransition { + size_t from = 0; + Bits input; + size_t to = 0; +}; + +struct InitialBoundary { + // Retain every prescribed origin, even if different storage choices settle + // to the same boundary. Never cherry-pick a convenient initialization pair. + std::vector storage; + size_t boundary = 0; +}; + +struct TransitionTable { + std::vector boundaries; + std::vector rows; + std::vector initials; + Bits initialInputs; + bool singleExternalInputChange = false; + size_t maxWaves = 0; +}; + +struct CompileResult { + CertificationStatus status = CertificationStatus::Invalid; + std::string detail; + // Present ONLY after all initial origins and every permitted transaction from + // every reachable complete boundary have passed progress and uniqueness. + std::optional table; + + bool certified() const { return status == CertificationStatus::Certified; } +}; + +CompileResult compileTransitionTable(const EventModel& model, + const CompileOptions& options); + +} // namespace KEPLER_FORMAL::SEC::LATCH diff --git a/src/sec/latch/LatchSupportOptions.cpp b/src/sec/latch/LatchSupportOptions.cpp new file mode 100644 index 00000000..2343c4ba --- /dev/null +++ b/src/sec/latch/LatchSupportOptions.cpp @@ -0,0 +1,12 @@ +// Copyright 2026 keplertech.io +// SPDX-License-Identifier: Apache-2.0 +#include "latch/LatchSupportOptions.h" +#include + +namespace KEPLER_FORMAL::SEC::LATCH { +namespace { thread_local SupportOptions currentOptions; } +const SupportOptions& supportOptions() { return currentOptions; } +ScopedSupportOptions::ScopedSupportOptions(SupportOptions options) + : previous_(currentOptions) { currentOptions = std::move(options); } +ScopedSupportOptions::~ScopedSupportOptions() { currentOptions = std::move(previous_); } +} // namespace KEPLER_FORMAL::SEC::LATCH diff --git a/src/sec/latch/LatchSupportOptions.h b/src/sec/latch/LatchSupportOptions.h new file mode 100644 index 00000000..4d94561c --- /dev/null +++ b/src/sec/latch/LatchSupportOptions.h @@ -0,0 +1,47 @@ +// Copyright 2026 keplertech.io +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include +#include + +#include "latch/LatchSettlingCompiler.h" + +namespace KEPLER_FORMAL::SEC::LATCH { + +// This is an explicit semantic contract, not an inference from cell names or +// clock carriers. A step is one external transaction followed by full settling. +struct SupportOptions { + bool enabled = true; + // Explicit event tuning can request event semantics even without latches. + bool explicitConfiguration = false; + bool singleInputChange = false; + std::optional initialInputs; + std::optional initialStorage; + size_t workers = 0; + CompilerLimits limits; + size_t maxSymbolicNodes = 2000000; + unsigned maxSatConflicts = 500000; + unsigned maxSatDecisions = 5000000; + + // Initial values are optional restrictions, never an admission requirement. + bool hasEventContract() const { + return enabled; + } +}; + +const SupportOptions& supportOptions(); + +// Extraction is serial; workers receive immutable copies, never this context. +// A scoped option also keeps embedded invocations from leaking semantics. +class ScopedSupportOptions { + public: + explicit ScopedSupportOptions(SupportOptions options); + ~ScopedSupportOptions(); + ScopedSupportOptions(const ScopedSupportOptions&) = delete; + ScopedSupportOptions& operator=(const ScopedSupportOptions&) = delete; + private: + SupportOptions previous_; +}; + +} // namespace KEPLER_FORMAL::SEC::LATCH diff --git a/src/sec/latch/LatchSymbolicCompiler.cpp b/src/sec/latch/LatchSymbolicCompiler.cpp new file mode 100644 index 00000000..2a8edff8 --- /dev/null +++ b/src/sec/latch/LatchSymbolicCompiler.cpp @@ -0,0 +1,397 @@ +// Copyright 2026 keplertech.io +// SPDX-License-Identifier: Apache-2.0 + +#include "latch/LatchSymbolicCompiler.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "kinduction/SatEncoding.h" + +namespace KEPLER_FORMAL::SEC::LATCH { +namespace { + +struct Failure { + CertificationStatus status; + std::string detail; +}; + +[[noreturn]] void unproved(const std::string& detail) { + throw Failure{CertificationStatus::UnprovedBound, detail}; +} + +[[noreturn]] void resource(const std::string& detail) { + throw Failure{CertificationStatus::ResourceLimit, detail}; +} + +// Count the cumulative DAG footprint, including proof-only copies. Traversal is +// iterative and shared nodes are visited once, so deeply unfolded paths do not +// consume the C++ stack. All leaves must belong to this compilation's allocator. +class DagBudget { + public: + DagBudget(size_t limit, const size_t& nextSymbol) + : limit_(limit), nextSymbol_(nextSymbol) {} + + void inspect(const SymbolicBits& roots) { + std::vector pending(roots.begin(), roots.end()); + while (!pending.empty()) { + auto* node = pending.back(); + pending.pop_back(); + if (!node || !node->isValid()) { + throw std::invalid_argument("Invalid Boolean DAG in symbolic latch reference"); + } + if (!seen_.insert(node).second) continue; + if (seen_.size() > limit_) resource("Symbolic latch DAG exceeds maxNodes"); + if (node->getOp() == Op::VAR) { + if (node->getId() >= nextSymbol_) { + throw std::invalid_argument("Symbolic primitive introduced an unallocated leaf"); + } + } else { + pending.push_back(node->getLeft()); + if (node->getRight()) pending.push_back(node->getRight()); + } + } + } + + void inspect(BoolExpr* root) { inspect(SymbolicBits{root}); } + + void inspect(const SymbolicState& state) { + inspect(state.current); + inspect(state.previous); + for (const auto& storage : state.storage) inspect(storage); + inspect(state.active); + inspect(state.bootstrap); + inspect(state.error); + } + + private: + size_t limit_; + const size_t& nextSymbol_; + std::unordered_set seen_; +}; + +BoolExpr* differs(const SymbolicBits& left, const SymbolicBits& right) { + if (left.size() != right.size()) { + throw std::invalid_argument("Symbolic boundary layout changed during propagation"); + } + auto* difference = BoolExpr::createFalse(); + for (size_t i = 0; i < left.size(); ++i) { + difference = BoolExpr::Or(difference, BoolExpr::Xor(left[i], right[i])); + } + return difference; +} + +BoolExpr* allowedInput(const Network& network, const SymbolicState& boundary, + const SymbolicBits& inputs, bool singleChange) { + if (!singleChange) return BoolExpr::createTrue(); + // At-most-one changed original external bit, in linear-size prefix form. + // The condition concerns admission, never the internal pin-change schedule. + auto* seenChange = BoolExpr::createFalse(); + auto* multiple = BoolExpr::createFalse(); + for (size_t i = 0; i < inputs.size(); ++i) { + auto* change = BoolExpr::Xor(boundary.current.at(network.externalInputs[i]), inputs[i]); + multiple = BoolExpr::Or(multiple, BoolExpr::And(seenChange, change)); + seenChange = BoolExpr::Or(seenChange, change); + } + return BoolExpr::Not(multiple); +} + +// Evaluate ground BOOT expressions without recursive BoolExpr::evaluate. +Bits groundValues(const SymbolicBits& roots) { + std::unordered_map values; + std::vector> pending; + for (auto* root : roots) { + pending.emplace_back(root, false); + while (!pending.empty()) { + const auto [node, visited] = pending.back(); + pending.pop_back(); + if (values.contains(node)) continue; + if (node->getOp() == Op::VAR) { + if (node->getId() > 1) { + throw std::invalid_argument("Canonical symbolic BOOT still contains a free input"); + } + values.emplace(node, node->getId() == 1); + } else if (!visited) { + pending.emplace_back(node, true); + if (node->getRight()) pending.emplace_back(node->getRight(), false); + pending.emplace_back(node->getLeft(), false); + } else { + const bool left = values.at(node->getLeft()); + switch (node->getOp()) { + case Op::NOT: values.emplace(node, !left); break; + case Op::AND: values.emplace(node, left && values.at(node->getRight())); break; + case Op::OR: values.emplace(node, left || values.at(node->getRight())); break; + case Op::XOR: values.emplace(node, left != values.at(node->getRight())); break; + default: throw std::invalid_argument("Unsupported symbolic Boolean operator"); + } + } + } + } + Bits result; + for (auto* root : roots) result.push_back(values.at(root)); + return result; +} + +bool checkFinalLeaves(const SymbolicBits& roots, const std::unordered_set& retained) { + bool ground = true; + std::unordered_set seen; + std::vector pending(roots.begin(), roots.end()); + while (!pending.empty()) { + auto* node = pending.back(); + pending.pop_back(); + if (!seen.insert(node).second) continue; + if (node->getOp() == Op::VAR) { + if (node->getId() > 1) ground = false; + if (node->getId() > 1 && !retained.contains(node->getId())) { + throw std::invalid_argument("Uneliminated seed or ordering choice in compiled macrostate"); + } + } else { + pending.push_back(node->getLeft()); + if (node->getRight()) pending.push_back(node->getRight()); + } + } + return ground; +} + +class Compiler { + public: + Compiler(const SymbolicNetwork& network, const SymbolicCompileOptions& options) + : network_(network), options_(options), model_(network, {}, options.workers), + dag_(options.maxNodes, nextSymbol_), + satBudget_(options.maxSatConflicts, options.maxSatDecisions, + std::numeric_limits::max()) {} + + SymbolicMacro run() { + if (!options_.maxNodes || !options_.maxSatConflicts || !options_.maxSatDecisions) { + resource("Symbolic certification requires a nonzero DAG and SAT work budget"); + } + const auto& reference = network_.reference; + SymbolicMacro result; + const auto bootInputs = initialBits(reference.externalInputs.size(), options_.initialInputs, + options_.initialInputValues, result.initialInputSymbols, result.initialParameterSymbols); + if ((!options_.initialStorage.empty() && !options_.initialStorageValues.empty()) || + (!options_.initialStorage.empty() && options_.initialStorage.size() != reference.primitives.size()) || + (!options_.initialStorageValues.empty() && options_.initialStorageValues.size() != reference.primitives.size())) { + throw std::invalid_argument("Symbolic storage initialization has conflicting forms or wrong shape"); + } + std::vector bootStorage; + result.initialStorageSymbols.resize(reference.primitives.size()); + for (size_t i = 0; i < reference.primitives.size(); ++i) { + const auto width = reference.primitives[i].storageBits; + // A supplied outer vector describes every primitive, including explicit + // empty entries for combinational cells; missing storage widths are not + // silently treated as unspecified. + if ((!options_.initialStorage.empty() && options_.initialStorage[i].size() != width) || + (!options_.initialStorageValues.empty() && options_.initialStorageValues[i].size() != width)) + throw std::invalid_argument("Symbolic primitive storage has the wrong width"); + bootStorage.push_back(initialBits(width, + options_.initialStorage.empty() ? Bits{} : options_.initialStorage[i], + options_.initialStorageValues.empty() ? std::vector>{} : options_.initialStorageValues[i], + result.initialStorageSymbols[i], result.initialParameterSymbols)); + } + result.singleExternalInputChange = options_.singleExternalInputChange; + result.externalInputNets = reference.externalInputs; + SymbolicBits current = variables(reference.netCount, &result.stateSymbols); + std::vector storage; + for (const auto& primitive : reference.primitives) { + storage.push_back(variables(primitive.storageBits, &result.stateSymbols)); + } + const auto input = variables(reference.externalInputs.size(), &result.inputSymbols); + const auto boundary = model_.boundary(current, storage); + dag_.inspect(boundary); + auto* invariant = model_.boundaryInvariant(boundary); + dag_.inspect(invariant); + + // Intended BOOT inputs/storage are shared, auxiliary net seeds and all wave + // choices are independent. The second copy is not tied to the first entry. + auto bootA = model_.bootstrap(bootInputs, bootStorage, variables(reference.netCount)); + auto bootB = model_.bootstrap(bootInputs, bootStorage, variables(reference.netCount)); + result.bootstrapWaves = settle(bootA, BoolExpr::createTrue(), "BOOT progress"); + advance(bootB, result.bootstrapWaves, false); + requireUnsat(BoolExpr::Not(model_.stable(bootB)), "independent BOOT progress"); + requireUnsat(differs(flattenSymbolicBoundary(bootA), flattenSymbolicBoundary(bootB)), + "BOOT seed/order independence", CertificationStatus::OrderDependent); + requireUnsat(BoolExpr::Not(model_.boundaryInvariant(bootA)), + "BOOT does not establish the candidate boundary invariant"); + + auto* assumption = BoolExpr::And(invariant, allowedInput( + reference, boundary, input, options_.singleExternalInputChange)); + dag_.inspect(assumption); + // Admission is a total functional construction, including invalid/error + // handling. Both copies share q/u, but never any internal ordering choices. + auto nextA = model_.admit(boundary, input); + auto nextB = model_.admit(boundary, input); + result.transitionWaves = settle(nextA, assumption, "boundary episode progress"); + advance(nextB, result.transitionWaves, false); + requireUnsat(BoolExpr::And(assumption, BoolExpr::Not(model_.stable(nextB))), + "independent boundary episode progress"); + requireUnsat(BoolExpr::And(assumption, + differs(flattenSymbolicBoundary(nextA), flattenSymbolicBoundary(nextB))), + "boundary outcome uniqueness over the candidate invariant"); + requireUnsat(BoolExpr::And(assumption, BoolExpr::Not(model_.boundaryInvariant(nextA))), + "boundary invariant closure"); + + // A legal canonical pin ordering is selected only after universal progress, + // uniqueness and inductive closure succeeded. Unused choice codes are legal + // in the reference; assigning zero therefore removes no possible behavior. + auto canonicalBoot = model_.bootstrap(bootInputs, bootStorage, + SymbolicBits(reference.netCount, BoolExpr::createFalse())); + advance(canonicalBoot, result.bootstrapWaves, true); + result.initialStateExpressions = flattenSymbolicBoundary(canonicalBoot); + const std::unordered_set initialParameters(result.initialParameterSymbols.begin(), + result.initialParameterSymbols.end()); + if (checkFinalLeaves(result.initialStateExpressions, initialParameters)) + result.initialState = groundValues(result.initialStateExpressions); + auto canonicalNext = model_.admit(boundary, input); + advance(canonicalNext, result.transitionWaves, true); + result.nextState = flattenSymbolicBoundary(canonicalNext); + result.observedNets = canonicalNext.current; + if (result.initialStateExpressions.size() != result.stateSymbols.size() || + result.nextState.size() != result.stateSymbols.size()) { + throw std::invalid_argument("Symbolic macro layout does not preserve the complete boundary"); + } + dag_.inspect(result.nextState); + std::unordered_set retained(result.stateSymbols.begin(), result.stateSymbols.end()); + retained.insert(result.inputSymbols.begin(), result.inputSymbols.end()); + checkFinalLeaves(result.nextState, retained); + return result; + } + + private: + SymbolicBits initialBits(size_t count, const Bits& concrete, + const std::vector>& restrictions, + std::vector>& symbols, std::vector& parameters) { + if ((!concrete.empty() && !restrictions.empty()) || + (!concrete.empty() && concrete.size() != count) || + (!restrictions.empty() && restrictions.size() != count)) + throw std::invalid_argument("Symbolic initialization has conflicting forms or wrong width"); + if (count > options_.maxNodes) resource("Symbolic initial interface exceeds the DAG budget"); + SymbolicBits result; + symbols.resize(count); + for (size_t i = 0; i < count; ++i) { + const auto value = !concrete.empty() ? std::optional{concrete[i]} + : !restrictions.empty() ? restrictions[i] : std::nullopt; + if (value) { + if (*value > 1) throw std::invalid_argument("Non-Boolean symbolic initialization"); + result.push_back(BoolExpr::Var(*value)); + } else { + auto* origin = fresh(); + symbols[i] = origin->getId(); + parameters.push_back(origin->getId()); + result.push_back(origin); + } + } + return result; + } + + BoolExpr* fresh() { + if (nextSymbol_ == std::numeric_limits::max()) { + resource("Symbolic variable identifier space exhausted"); + } + auto* symbol = BoolExpr::Var(nextSymbol_++); + dag_.inspect(symbol); + return symbol; + } + + SymbolicBits variables(size_t count, std::vector* ids = nullptr) { + if (count > options_.maxNodes) resource("Symbolic interface exceeds the DAG budget"); + SymbolicBits result; + result.reserve(count); + for (size_t i = 0; i < count; ++i) { + auto* variable = fresh(); + result.push_back(variable); + if (ids) ids->push_back(variable->getId()); + } + return result; + } + + SATSolverWrapper::SolveStatus solve(BoolExpr* formula) { + dag_.inspect(formula); + if (formula == BoolExpr::createFalse()) return SATSolverWrapper::SolveStatus::Unsat; + if (formula == BoolExpr::createTrue()) return SATSolverWrapper::SolveStatus::Sat; + if (satBudget_.exhausted()) resource("Symbolic settling cumulative SAT budget exhausted"); + SATSolverWrapper::ScopedCadicalWorkBudget scope(satBudget_); + SATSolverWrapper solver(Config::SolverType::CADICAL); + FrameFormulaEncoder encoder(solver, {}, true); + solver.addClause({encoder.encode(formula)}); + const auto answer = solver.solveWithResourceLimits( + options_.maxSatConflicts, options_.maxSatDecisions); + if (answer == SATSolverWrapper::SolveStatus::Unknown) { + resource("Symbolic settling SAT obligation returned UNKNOWN under its work budget"); + } + return answer; + } + + void requireUnsat(BoolExpr* failure, const std::string& obligation, + CertificationStatus status = CertificationStatus::UnprovedBound) { + if (solve(failure) != SATSolverWrapper::SolveStatus::Unsat) { + throw Failure{status, obligation + + "; certificate not established (candidate boundary states need not be reachable)"}; + } + } + + void advance(SymbolicState& state, size_t waves, bool canonical) { + dag_.inspect(state); + for (size_t wave = 0; wave < waves; ++wave) { + state = model_.wave(state, canonical + ? FreshSymbol([] { return BoolExpr::createFalse(); }) + : FreshSymbol([this] { return fresh(); })); + dag_.inspect(state); + } + } + + size_t settle(SymbolicState& state, BoolExpr* assumption, const std::string& obligation) { + size_t depth = 0; + dag_.inspect(state); + for (;;) { + auto* failure = BoolExpr::And(assumption, BoolExpr::Not(model_.stable(state))); + if (solve(failure) == SATSolverWrapper::SolveStatus::Unsat) return depth; + if (depth == options_.maxWaves) { + unproved(obligation + " unproved through maxWaves; a larger bound or stronger " + "inductive invariant may be needed, or behavior may not settle"); + } + const size_t next = depth == 0 ? 1 : + depth > options_.maxWaves / 2 ? options_.maxWaves : depth * 2; + advance(state, next - depth, false); + depth = next; + } + } + + const SymbolicNetwork& network_; + const SymbolicCompileOptions& options_; + SymbolicEventModel model_; + size_t nextSymbol_ = 2; + DagBudget dag_; + SATSolverWrapper::CadicalWorkBudget satBudget_; +}; + +} // namespace + +SymbolicCompileResult compileSymbolicNetwork(const SymbolicNetwork& network, + const SymbolicCompileOptions& options) { + SymbolicCompileResult result; + try { + result.model = Compiler(network, options).run(); + result.status = CertificationStatus::Certified; + } catch (const Failure& failure) { + result.status = failure.status; + result.detail = failure.detail; + } catch (const Limit& limit) { + result.status = CertificationStatus::ResourceLimit; + result.detail = limit.what(); + } catch (const std::bad_alloc&) { + result.status = CertificationStatus::ResourceLimit; + result.detail = "Memory exhausted during symbolic settling certification"; + } catch (const std::exception& error) { + result.status = CertificationStatus::Invalid; + result.detail = error.what(); + } + return result; +} + +} // namespace KEPLER_FORMAL::SEC::LATCH diff --git a/src/sec/latch/LatchSymbolicCompiler.h b/src/sec/latch/LatchSymbolicCompiler.h new file mode 100644 index 00000000..92fde3b7 --- /dev/null +++ b/src/sec/latch/LatchSymbolicCompiler.h @@ -0,0 +1,62 @@ +// Copyright 2026 keplertech.io +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include "latch/LatchSymbolicModel.h" +#include "latch/LatchSettlingCompiler.h" + +namespace KEPLER_FORMAL::SEC::LATCH { + +struct SymbolicCompileOptions { + // Compatibility form: a nonempty vector fixes every bit. Empty means that + // every intended initial bit is an independent, fixed-once Boolean origin. + Bits initialInputs; + std::vector initialStorage; + // Optional per-bit restrictions, mutually exclusive with the corresponding + // concrete form above. nullopt retains both Boolean starts; it is not X. + std::vector> initialInputValues; + std::vector>> initialStorageValues; + bool singleExternalInputChange = false; + size_t maxWaves = 256; + size_t maxNodes = 2000000; + unsigned maxSatConflicts = 500000; + unsigned maxSatDecisions = 5000000; + size_t workers = 0; +}; + +struct SymbolicMacro { + // Full boundary layout: current nets, then each primitive's storage. At a + // boundary previous=current, active=BOOT=error=0, reconstructing full history. + std::vector stateSymbols, inputSymbols; + // Available only if initialStateExpressions is ground. Never select one + // convenient valuation when intended initial parameters remain symbolic. + Bits initialState; + SymbolicBits initialStateExpressions; + std::vector initialParameterSymbols; + // These projections let the caller share external levels across components + // and designs without incorrectly identifying independent storage origins. + std::vector> initialInputSymbols; + std::vector>> initialStorageSymbols; + SymbolicBits nextState, observedNets; + size_t bootstrapWaves = 0, transitionWaves = 0; + // Retain the proved admission contract and its current-net projection. An + // encoder must not broaden the environment or relabel remembered inputs. + bool singleExternalInputChange = false; + std::vector externalInputNets; +}; + +struct SymbolicCompileResult { + CertificationStatus status = CertificationStatus::Invalid; + std::string detail; + std::optional model; + bool certified() const { return status == CertificationStatus::Certified && model.has_value(); } +}; + +// Prove universal progress and complete-boundary uniqueness with independent +// choice/seed copies, plus initialization and inductive boundary closure. Only +// then eliminate choices and retain K unfolded waves as deterministic logic. +// SAT/UNKNOWN over the candidate invariant is unproved, not a reachable defect. +SymbolicCompileResult compileSymbolicNetwork(const SymbolicNetwork& network, + const SymbolicCompileOptions& options); + +} // namespace KEPLER_FORMAL::SEC::LATCH diff --git a/src/sec/latch/LatchSymbolicEncoding.cpp b/src/sec/latch/LatchSymbolicEncoding.cpp new file mode 100644 index 00000000..0897a545 --- /dev/null +++ b/src/sec/latch/LatchSymbolicEncoding.cpp @@ -0,0 +1,143 @@ +// Copyright 2026 keplertech.io +// SPDX-License-Identifier: Apache-2.0 +#include "latch/LatchSymbolicEncoding.h" +#include +#include +#include + +namespace KEPLER_FORMAL::SEC::LATCH { +namespace { +BoolExpr* selected(const SymbolicBits& selector, size_t value) { + auto* result = BoolExpr::createTrue(); + for (size_t i = 0; i < selector.size(); ++i) + result = BoolExpr::And(result, (value >> i) & 1 ? selector[i] : BoolExpr::Not(selector[i])); + return result; +} +// Iterative, shared-DAG traversal: long unfolded latch chains must not consume +// the native stack, and substituting a variable is simultaneous, not recursive +// rewriting of its replacement (the destination may reuse a numeric symbol). +SymbolicBits rewrite(const SymbolicBits& roots, const std::unordered_map& replacements) { + std::unordered_map memo; + SymbolicBits result; + for (auto* root : roots) { + std::vector> pending{{root, false}}; + while (!pending.empty()) { + const auto [node, ready] = pending.back(); + pending.pop_back(); + if (!node || !node->isValid()) throw std::invalid_argument("invalid symbolic macro expression"); + if (memo.count(node)) continue; + if (node->getOp() == Op::VAR) { + if (node->getId() < 2) memo.emplace(node, node); + else { + const auto found = replacements.find(node->getId()); + if (found == replacements.end()) throw std::invalid_argument("uncertified auxiliary symbol in macro"); + memo.emplace(node, found->second); + } + continue; + } + if (!ready) { + pending.emplace_back(node, true); + if (node->getRight()) pending.emplace_back(node->getRight(), false); + pending.emplace_back(node->getLeft(), false); + continue; + } + auto* left = memo.at(node->getLeft()); + auto* right = node->getRight() ? memo.at(node->getRight()) : nullptr; + switch (node->getOp()) { + case Op::NOT: memo.emplace(node, BoolExpr::Not(left)); break; + case Op::AND: memo.emplace(node, BoolExpr::And(left, right)); break; + case Op::OR: memo.emplace(node, BoolExpr::Or(left, right)); break; + case Op::XOR: memo.emplace(node, BoolExpr::Xor(left, right)); break; + default: throw std::invalid_argument("unsupported symbolic macro expression"); + } + } + result.push_back(memo.at(root)); + } + return result; +} +} // namespace + +SymbolicBits encodeSymbolicInitialState(const SymbolicMacro& macro, + const SymbolicBits& parameters) { + if (parameters.size() != macro.initialParameterSymbols.size() || + (!macro.initialState.empty() && macro.initialState.size() != macro.stateSymbols.size()) || + (!macro.initialStateExpressions.empty() && macro.initialStateExpressions.size() != macro.stateSymbols.size())) + throw std::invalid_argument("symbolic initial interface mismatch"); + if (macro.initialStateExpressions.empty()) { + if (macro.initialState.size() != macro.stateSymbols.size() || !parameters.empty()) + throw std::invalid_argument("symbolic macro lacks its initialization relation"); + SymbolicBits result; + for (auto bit : macro.initialState) { + if (bit > 1) throw std::invalid_argument("non-Boolean initial state"); + result.push_back(BoolExpr::Var(bit)); + } + return result; + } + std::set ordinary(macro.stateSymbols.begin(), macro.stateSymbols.end()); + ordinary.insert(macro.inputSymbols.begin(), macro.inputSymbols.end()); + std::unordered_map replacements; + for (size_t i = 0; i < parameters.size(); ++i) { + const size_t id = macro.initialParameterSymbols[i]; + if (id < 2 || ordinary.count(id) || !parameters[i] || !parameters[i]->isValid() || + !replacements.emplace(id, parameters[i]).second) + throw std::invalid_argument("invalid or duplicate symbolic initial parameter"); + } + return rewrite(macro.initialStateExpressions, replacements); +} + +BoolExpr* encodeSymbolicInitialRelation(const SymbolicMacro& macro, + const SymbolicBits& state, const SymbolicBits& parameters) { + if (state.size() != macro.stateSymbols.size()) + throw std::invalid_argument("symbolic initial state width mismatch"); + const auto initial = encodeSymbolicInitialState(macro, parameters); + auto* relation = BoolExpr::createTrue(); + for (size_t i = 0; i < state.size(); ++i) { + if (!state[i] || !state[i]->isValid()) throw std::invalid_argument("invalid symbolic initial state"); + relation = BoolExpr::And(relation, BoolExpr::Not(BoolExpr::Xor(state[i], initial[i]))); + } + return relation; +} + +BoundaryEncoding encodeSymbolicMacro(const SymbolicMacro& macro, const Network& network, + const SymbolicBits& state, const SymbolicBits& inputs, bool singleInputChange, + const SymbolicBits& selector, BoolExpr* eventValue, + const std::vector& globalInputIndices) { + if (state.size() != macro.stateSymbols.size() || state.size() < network.netCount || + macro.nextState.size() != state.size() || + (macro.initialState.size() != state.size() && macro.initialStateExpressions.size() != state.size()) || + inputs.size() != macro.inputSymbols.size() || inputs.size() != network.externalInputs.size() || + macro.observedNets.size() != network.netCount || + macro.singleExternalInputChange != singleInputChange || + macro.externalInputNets != network.externalInputs || + selector.size() >= std::numeric_limits::digits || + (singleInputChange && (!eventValue || globalInputIndices.size() != inputs.size()))) + throw std::invalid_argument("symbolic macro interface mismatch"); + for (auto* bit : selector) + if (!bit || !bit->isValid()) throw std::invalid_argument("invalid symbolic event selector"); + if (singleInputChange && !eventValue->isValid()) + throw std::invalid_argument("invalid symbolic event value"); + std::unordered_map replacements; + for (size_t i = 0; i < state.size(); ++i) + if (macro.stateSymbols[i] < 2 || !state[i] || !state[i]->isValid() || + !replacements.emplace(macro.stateSymbols[i], state[i]).second) + throw std::invalid_argument("invalid or duplicate symbolic state"); + std::set indices; + for (size_t i = 0; i < inputs.size(); ++i) { + auto* input = inputs[i]; + if (singleInputChange) { + const size_t global = globalInputIndices[i]; + if (global >= (size_t(1) << selector.size()) || !indices.insert(global).second) + throw std::invalid_argument("invalid symbolic event selector index"); + input = symbolicMux(selected(selector, global), eventValue, state.at(network.externalInputs[i])); + } + if (macro.inputSymbols[i] < 2 || !input || !input->isValid() || + !replacements.emplace(macro.inputSymbols[i], input).second) + throw std::invalid_argument("invalid or duplicate symbolic input"); + } + auto roots = macro.nextState; + roots.insert(roots.end(), macro.observedNets.begin(), macro.observedNets.end()); + const auto encoded = rewrite(roots, replacements); + return {{encoded.begin(), encoded.begin() + state.size()}, + {encoded.begin() + state.size(), encoded.end()}}; +} +} // namespace KEPLER_FORMAL::SEC::LATCH diff --git a/src/sec/latch/LatchSymbolicEncoding.h b/src/sec/latch/LatchSymbolicEncoding.h new file mode 100644 index 00000000..d7025e1a --- /dev/null +++ b/src/sec/latch/LatchSymbolicEncoding.h @@ -0,0 +1,22 @@ +// Copyright 2026 keplertech.io +// SPDX-License-Identifier: Apache-2.0 +#pragma once +#include "latch/LatchBoundaryEncoding.h" +#include "latch/LatchSymbolicCompiler.h" + +namespace KEPLER_FORMAL::SEC::LATCH { +// Fixed-once origins are substituted in initialParameterSymbols order. Shared +// input levels must be aligned by the caller; storage origins are not implicitly +// coupled across designs. No parameter appears in the ordinary transition map. +SymbolicBits encodeSymbolicInitialState(const SymbolicMacro& macro, + const SymbolicBits& parameters); +BoolExpr* encodeSymbolicInitialRelation(const SymbolicMacro& macro, + const SymbolicBits& state, const SymbolicBits& parameters); + +// Map certified local symbols to SEC variables. In single-event mode raw new +// input levels are decoded from one globally aligned selector/value pair. +BoundaryEncoding encodeSymbolicMacro(const SymbolicMacro& macro, const Network& network, + const SymbolicBits& state, const SymbolicBits& inputs, bool singleInputChange, + const SymbolicBits& selector = {}, BoolExpr* eventValue = nullptr, + const std::vector& globalInputIndices = {}); +} // namespace KEPLER_FORMAL::SEC::LATCH diff --git a/src/sec/latch/LatchSymbolicModel.cpp b/src/sec/latch/LatchSymbolicModel.cpp new file mode 100644 index 00000000..173b3a80 --- /dev/null +++ b/src/sec/latch/LatchSymbolicModel.cpp @@ -0,0 +1,419 @@ +// Copyright 2026 keplertech.io +// SPDX-License-Identifier: Apache-2.0 +#include "latch/LatchSymbolicModel.h" + +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace KEPLER_FORMAL::SEC::LATCH { +namespace { +BoolExpr* zero() { return BoolExpr::createFalse(); } +BoolExpr* one() { return BoolExpr::createTrue(); } +BoolExpr* negate(BoolExpr* value) { return BoolExpr::Not(value); } +BoolExpr* conjunction(BoolExpr* a, BoolExpr* b) { return BoolExpr::And(a, b); } +BoolExpr* disjunction(BoolExpr* a, BoolExpr* b) { return BoolExpr::Or(a, b); } +BoolExpr* different(BoolExpr* a, BoolExpr* b) { return BoolExpr::Xor(a, b); } + +void validate(const SymbolicBits& bits, size_t size) { + if (bits.size() != size || std::any_of(bits.begin(), bits.end(), [](auto* bit) { + return !bit || !bit->isValid(); + })) throw std::invalid_argument("Malformed symbolic Boolean vector"); +} +void validate(const Network& network, const SymbolicState& state) { + validate(state.current, network.netCount); + validate(state.previous, network.netCount); + validate(state.active, network.primitives.size()); + validate({state.bootstrap, state.error}, 2); + if (state.storage.size() != network.primitives.size()) + throw std::invalid_argument("Malformed symbolic primitive storage"); + for (size_t i = 0; i < state.storage.size(); ++i) + validate(state.storage[i], network.primitives[i].storageBits); +} +SymbolicBits gather(const SymbolicBits& bits, const std::vector& indices) { + SymbolicBits result; + for (auto index : indices) result.push_back(bits.at(index)); + return result; +} +SymbolicBits choose(BoolExpr* condition, const SymbolicBits& yes, const SymbolicBits& no) { + if (yes.size() != no.size()) throw std::invalid_argument("Symbolic mux width mismatch"); + SymbolicBits result; + for (size_t i = 0; i < yes.size(); ++i) result.push_back(symbolicMux(condition, yes[i], no[i])); + return result; +} +BoolExpr* equal(const SymbolicBits& a, const SymbolicBits& b) { + if (a.size() != b.size()) throw std::invalid_argument("Symbolic equality width mismatch"); + auto* result = one(); + for (size_t i = 0; i < a.size(); ++i) result = conjunction(result, negate(different(a[i], b[i]))); + return result; +} +BoolExpr* inactive(const SymbolicState& state) { + auto* result = conjunction(negate(state.bootstrap), negate(state.error)); + for (auto* active : state.active) result = conjunction(result, negate(active)); + return result; +} +void normalizeCompletedActivation(SymbolicState& state, + const std::vector>& consumers) { + // Only call after admission/wave computed the COMPLETE fanout of changes + // against previous. For a consumed net, inactivity already implies that net + // did not change, so its history needs no mux. Sink nets still need global + // normalization. This keeps a global guard out of every primitive's inputs. + auto* settled = inactive(state); + for (size_t net = 0; net < state.current.size(); ++net) + if (consumers[net].empty()) + state.previous[net] = symbolicMux(settled, state.current[net], state.previous[net]); +} +SymbolicState choose(BoolExpr* condition, const SymbolicState& yes, const SymbolicState& no) { + SymbolicState result; + result.current = choose(condition, yes.current, no.current); + result.previous = choose(condition, yes.previous, no.previous); + result.active = choose(condition, yes.active, no.active); + for (size_t i = 0; i < yes.storage.size(); ++i) + result.storage.push_back(choose(condition, yes.storage[i], no.storage[i])); + result.bootstrap = symbolicMux(condition, yes.bootstrap, no.bootstrap); + result.error = symbolicMux(condition, yes.error, no.error); + return result; +} +SymbolicReaction choose(BoolExpr* condition, const SymbolicReaction& yes, const SymbolicReaction& no) { + return {choose(condition, yes.storage, no.storage), choose(condition, yes.outputs, no.outputs), + symbolicMux(condition, yes.error, no.error)}; +} + +SymbolicReaction invoke(const Primitive& primitive, const SymbolicPrimitive& callback, + const SymbolicBits& storage, const SymbolicBits& before, const SymbolicBits& current, + const SymbolicBits& oldOutputs, std::optional changed, bool bootstrap) { + SymbolicReaction result{storage, oldOutputs, one()}; + if (!callback.react) return result; + try { + result = callback.react(storage, before, current, changed, bootstrap); + validate(result.storage, primitive.storageBits); + validate(result.outputs, primitive.outputs.size()); + validate({result.error}, 1); + } catch (const Limit&) { throw; + } catch (const std::bad_alloc&) { throw; + } catch (const std::exception&) { + return {storage, oldOutputs, one()}; + } + // Concrete invalid/error reactions keep the incoming storage and original + // physical outputs, even after earlier successful visits in a permutation. + result.storage = choose(result.error, storage, result.storage); + result.outputs = choose(result.error, oldOutputs, result.outputs); + return result; +} + +size_t permutationCount(size_t pins, size_t limit) { + size_t count = 1; + for (size_t i = 2; i <= pins; ++i) { + if (count > limit / i) throw Limit("symbolic changed-pin ordering limit exceeded"); + count *= i; + } + return count; +} +BoolExpr* code(const SymbolicBits& bits, size_t value) { + auto* result = one(); + for (size_t i = 0; i < bits.size(); ++i) + result = conjunction(result, (value >> i) & 1 ? bits[i] : negate(bits[i])); + return result; +} + +bool binary(const Bits& bits) { + return std::all_of(bits.begin(), bits.end(), [](auto bit) { return bit <= 1; }); +} +size_t domainSize(size_t storage, size_t pins, size_t copies, size_t maximum) { + if (storage > maximum || pins > (maximum - storage) / copies) + throw Limit("local primitive truth-table lifting limit exceeded"); + const size_t bits = storage + copies * pins; + if (bits >= std::numeric_limits::digits) + throw Limit("local primitive truth-table lifting index overflow"); + return bits; +} +BoolExpr* minterm(const SymbolicBits& expressions, size_t value) { + return code(expressions, value); +} +Bits unpack(size_t value, size_t offset, size_t size) { + Bits result(size); + for (size_t i = 0; i < size; ++i) result[i] = (value >> (offset + i)) & 1; + return result; +} +} // namespace + +BoolExpr* symbolicMux(BoolExpr* condition, BoolExpr* yes, BoolExpr* no) { + if (yes == no) return yes; + if (condition == one()) return yes; + if (condition == zero()) return no; + return disjunction(conjunction(condition, yes), conjunction(negate(condition), no)); +} + +SymbolicBits flattenSymbolicBoundary(const SymbolicState& state) { + auto result = state.current; + for (const auto& storage : state.storage) result.insert(result.end(), storage.begin(), storage.end()); + return result; +} + +SymbolicEventModel::SymbolicEventModel(SymbolicNetwork network, Limits limits, size_t workers) + : network_(std::move(network)), limits_(limits), workers_(workers) { + // Reuse all concrete structural/single-writer validation, not its callbacks. + EventModel validated(network_.reference, limits, workers); + network_.reference = validated.network(); + if (network_.primitives.size() != network_.reference.primitives.size()) + throw std::invalid_argument("Symbolic primitive count mismatch"); + consumers_.resize(network_.reference.netCount); + for (size_t i = 0; i < network_.reference.primitives.size(); ++i) + for (auto net : network_.reference.primitives[i].inputs) consumers_[net].push_back(i); + for (auto& consumers : consumers_) { + std::sort(consumers.begin(), consumers.end()); + consumers.erase(std::unique(consumers.begin(), consumers.end()), consumers.end()); + } +} + +SymbolicState SymbolicEventModel::boundary(const SymbolicBits& current, + const std::vector& storage) const { + SymbolicState result; + result.current = result.previous = current; + result.storage = storage; + result.active.assign(network_.primitives.size(), zero()); + validate(network_.reference, result); + return result; +} + +SymbolicState SymbolicEventModel::bootstrap(const SymbolicBits& inputs, + const std::vector& storage, const SymbolicBits& seeds) const { + const auto& reference = network_.reference; + validate(inputs, reference.externalInputs.size()); + auto result = boundary(seeds, storage); + result.bootstrap = one(); + result.active.assign(network_.primitives.size(), one()); + for (size_t i = 0; i < inputs.size(); ++i) result.current[reference.externalInputs[i]] = inputs[i]; + for (size_t i = 0; i < reference.netCount; ++i) + if (reference.constantByNet[i]) result.current[i] = *reference.constantByNet[i] ? one() : zero(); + const auto snapshot = result.current; + for (size_t i = 0; i < reference.primitives.size(); ++i) { + const auto& primitive = reference.primitives[i]; + if (!primitive.storageBits) continue; + SymbolicBits outputs; + try { + if (network_.primitives[i].initialOutputs) + outputs = network_.primitives[i].initialOutputs(storage[i], gather(snapshot, primitive.inputs)); + else if (!primitive.initialOutputs && !primitive.initialOutputValues && + primitive.outputs.size() == primitive.storageBits) outputs = storage[i]; + else throw std::invalid_argument("Missing symbolic initial output projection"); + validate(outputs, primitive.outputs.size()); + } catch (const Limit&) { throw; + } catch (const std::bad_alloc&) { throw; + } catch (const std::exception&) { + result.error = one(); + continue; + } + for (size_t bit = 0; bit < outputs.size(); ++bit) result.current[primitive.outputs[bit]] = outputs[bit]; + } + result.previous = result.current; + return result; +} + +BoolExpr* SymbolicEventModel::stable(const SymbolicState& state) const { + validate(network_.reference, state); + return conjunction(inactive(state), equal(state.current, state.previous)); +} + +SymbolicState SymbolicEventModel::admit(const SymbolicState& state, const SymbolicBits& inputs) const { + validate(network_.reference, state); + if (inputs.size() != network_.reference.externalInputs.size()) { + auto result = state; + result.error = one(); + return result; + } + validate(inputs, inputs.size()); + auto result = state; + result.previous = state.current; + result.active.assign(network_.primitives.size(), zero()); + for (size_t i = 0; i < inputs.size(); ++i) { + const auto net = network_.reference.externalInputs[i]; + result.current[net] = inputs[i]; + auto* changed = different(inputs[i], state.current[net]); + for (auto consumer : consumers_[net]) result.active[consumer] = disjunction(result.active[consumer], changed); + } + normalizeCompletedActivation(result, consumers_); + auto invalid = state; + invalid.error = one(); + return choose(state.error, state, choose(stable(state), result, invalid)); +} + +SymbolicState SymbolicEventModel::wave(const SymbolicState& state, const FreshSymbol& fresh) const { + const auto& reference = network_.reference; + validate(reference, state); + if (state.error == one() || stable(state) == one()) return state; + // Allocate every nondeterministic symbol before entering parallel workers. + std::vector selections(reference.primitives.size()); + std::set allocated; + for (size_t i = 0; i < selections.size(); ++i) { + const auto& primitive = reference.primitives[i]; + if (!primitive.storageBits || state.bootstrap == one() || state.active[i] == zero()) continue; + const auto count = permutationCount(primitive.inputs.size(), limits_.maxPinOrderings); + for (size_t remaining = count - 1; remaining; remaining >>= 1) { + if (!fresh) throw std::invalid_argument("Missing symbolic choice allocator"); + auto* symbol = fresh(); + validate({symbol}, 1); + if (symbol->getOp() != Op::VAR || + (symbol->getId() >= 2 && !allocated.insert(symbol->getId()).second)) + throw std::invalid_argument("Symbolic choice allocator must return distinct variables"); + selections[i].push_back(symbol); + } + } + std::vector results(reference.primitives.size()); + const auto evaluate = [&](size_t index) { + const auto& primitive = reference.primitives[index]; + const auto& callback = network_.primitives[index]; + const auto& storage = state.storage[index]; + const auto previous = gather(state.previous, primitive.inputs); + const auto current = gather(state.current, primitive.inputs); + const auto outputs = gather(state.current, primitive.outputs); + SymbolicReaction held{storage, outputs, zero()}; + if (state.active[index] == zero()) { results[index] = held; return; } + auto boot = invoke(primitive, callback, storage, previous, current, outputs, {}, true); + if (state.bootstrap == one()) { + results[index] = choose(state.active[index], boot, held); + return; + } + SymbolicReaction ordinary; + if (!primitive.storageBits) { + ordinary = invoke(primitive, callback, storage, previous, current, outputs, {}, false); + } else { + auto* noChanges = one(); + SymbolicBits changed; + for (size_t pin = 0; pin < current.size(); ++pin) { + changed.push_back(different(current[pin], previous[pin])); + noChanges = conjunction(noChanges, negate(changed.back())); + } + const auto fallback = invoke(primitive, callback, storage, previous, current, outputs, {}, false); + std::vector permutation(current.size()); + std::iota(permutation.begin(), permutation.end(), 0); + size_t ordinal = 0; + do { + auto pins = previous; + auto result = held; + for (auto pin : permutation) { + const auto before = pins; + pins[pin] = current[pin]; + auto reaction = invoke(primitive, callback, result.storage, before, pins, outputs, pin, false); + auto* visit = conjunction(changed[pin], negate(result.error)); + result = choose(visit, reaction, result); + } + result = choose(noChanges, fallback, result); + // All unused binary rank encodings select the canonical first order. + ordinary = ordinal == 0 ? result : choose(code(selections[index], ordinal), result, ordinary); + ++ordinal; + } while (std::next_permutation(permutation.begin(), permutation.end())); + } + results[index] = choose(state.active[index], choose(state.bootstrap, boot, ordinary), held); + }; + tbb::task_arena arena(workers_ ? static_cast(workers_) : tbb::task_arena::automatic); + arena.execute([&] { + tbb::parallel_for(tbb::blocked_range(0, results.size()), [&](const auto& range) { + for (size_t i = range.begin(); i != range.end(); ++i) evaluate(i); + }); + }); + auto result = state; + result.bootstrap = zero(); + result.previous = state.current; + result.active.assign(results.size(), zero()); + for (size_t i = 0; i < results.size(); ++i) { + result.storage[i] = results[i].storage; + result.error = disjunction(result.error, results[i].error); + for (size_t bit = 0; bit < results[i].outputs.size(); ++bit) + result.current[reference.primitives[i].outputs[bit]] = results[i].outputs[bit]; + } + for (size_t net = 0; net < reference.netCount; ++net) { + auto* changed = different(result.current[net], state.current[net]); + for (auto consumer : consumers_[net]) result.active[consumer] = disjunction(result.active[consumer], changed); + } + normalizeCompletedActivation(result, consumers_); + // Stable identity needs no global mux: every inactive primitive already + // holds, BOOT is false and previous=current, so every field above is unchanged + // on a stable valuation. Only errors require explicit absorbing padding. + return choose(state.error, state, result); +} + +BoolExpr* SymbolicEventModel::boundaryInvariant(const SymbolicState& state) const { + auto* result = stable(state); + const auto& reference = network_.reference; + for (size_t net = 0; net < reference.netCount; ++net) + if (reference.constantByNet[net]) + result = conjunction(result, *reference.constantByNet[net] ? state.current[net] : negate(state.current[net])); + for (size_t i = 0; i < reference.primitives.size(); ++i) { + const auto& primitive = reference.primitives[i]; + const auto pins = gather(state.current, primitive.inputs); + const auto outputs = gather(state.current, primitive.outputs); + const auto reaction = invoke(primitive, network_.primitives[i], state.storage[i], pins, pins, outputs, {}, true); + result = conjunction(result, negate(reaction.error)); + result = conjunction(result, equal(state.storage[i], reaction.storage)); + result = conjunction(result, equal(outputs, reaction.outputs)); + } + return result; +} + +SymbolicPrimitive liftPrimitive(const Primitive& primitive, size_t maxLocalBits) { + const auto storageBits = primitive.storageBits; + const auto pins = primitive.inputs.size(); + const auto total = domainSize(storageBits, pins, 2, maxLocalBits); + SymbolicPrimitive result; + result.react = [primitive, storageBits, pins, total](const SymbolicBits& storage, + const SymbolicBits& before, const SymbolicBits& current, + std::optional changed, bool bootstrap) { + validate(storage, storageBits); validate(before, pins); validate(current, pins); + SymbolicBits arguments = storage; + arguments.insert(arguments.end(), before.begin(), before.end()); + arguments.insert(arguments.end(), current.begin(), current.end()); + SymbolicReaction lifted{SymbolicBits(storageBits, zero()), SymbolicBits(primitive.outputs.size(), zero()), zero()}; + for (size_t assignment = 0; assignment < (size_t{1} << total); ++assignment) { + auto* condition = minterm(arguments, assignment); + if (condition == zero()) continue; + Reaction concrete; + try { + if (!primitive.react) throw std::invalid_argument("missing reaction"); + concrete = primitive.react(unpack(assignment, 0, storageBits), + unpack(assignment, storageBits, pins), unpack(assignment, storageBits + pins, pins), changed, bootstrap); + if (concrete.storage.size() != storageBits || concrete.outputs.size() != primitive.outputs.size() || + !binary(concrete.storage) || !binary(concrete.outputs)) concrete.error = true; + } catch (const Limit&) { throw; + } catch (const std::bad_alloc&) { throw; + } catch (const std::exception&) { concrete.error = true; } + if (concrete.error) { lifted.error = disjunction(lifted.error, condition); continue; } + for (size_t i = 0; i < storageBits; ++i) + if (concrete.storage[i]) lifted.storage[i] = disjunction(lifted.storage[i], condition); + for (size_t i = 0; i < concrete.outputs.size(); ++i) + if (concrete.outputs[i]) lifted.outputs[i] = disjunction(lifted.outputs[i], condition); + } + return lifted; + }; + const auto initialBits = domainSize(storageBits, pins, 1, maxLocalBits); + result.initialOutputs = [primitive, storageBits, pins, initialBits](const SymbolicBits& storage, + const SymbolicBits& inputs) { + validate(storage, storageBits); validate(inputs, pins); + SymbolicBits arguments = storage; + arguments.insert(arguments.end(), inputs.begin(), inputs.end()); + SymbolicBits outputs(primitive.outputs.size(), zero()); + for (size_t assignment = 0; assignment < (size_t{1} << initialBits); ++assignment) { + auto* condition = minterm(arguments, assignment); + if (condition == zero()) continue; + const auto bits = unpack(assignment, 0, storageBits); + Bits values; + if (primitive.initialOutputValues) values = primitive.initialOutputValues(bits, unpack(assignment, storageBits, pins)); + else if (primitive.initialOutputs) values = primitive.initialOutputs(bits); + else if (primitive.outputs.size() == storageBits) values = bits; + else throw std::invalid_argument("missing initial output mapping"); + if (values.size() != outputs.size() || !binary(values)) + throw std::invalid_argument("invalid initial output mapping"); + for (size_t i = 0; i < values.size(); ++i) + if (values[i]) outputs[i] = disjunction(outputs[i], condition); + } + return outputs; + }; + return result; +} +} // namespace KEPLER_FORMAL::SEC::LATCH diff --git a/src/sec/latch/LatchSymbolicModel.h b/src/sec/latch/LatchSymbolicModel.h new file mode 100644 index 00000000..b151cfc7 --- /dev/null +++ b/src/sec/latch/LatchSymbolicModel.h @@ -0,0 +1,75 @@ +// Copyright 2026 keplertech.io +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include "BoolExpr.h" +#include "latch/LatchEventModel.h" + +namespace KEPLER_FORMAL::SEC::LATCH { + +using SymbolicBits = std::vector; +using FreshSymbol = std::function; + +struct SymbolicReaction { + SymbolicBits storage, outputs; + BoolExpr* error = BoolExpr::createFalse(); +}; + +// These callbacks have the same pin-local contract as Primitive, but construct +// Boolean DAGs. They must be pure and safe for concurrent evaluation. +struct SymbolicPrimitive { + std::function, bool)> react; + std::function initialOutputs; +}; + +struct SymbolicNetwork { + Network reference; + std::vector primitives; +}; + +struct SymbolicState { + SymbolicBits current, previous; + std::vector storage; + SymbolicBits active; + BoolExpr* bootstrap = BoolExpr::createFalse(); + BoolExpr* error = BoolExpr::createFalse(); +}; + +BoolExpr* symbolicMux(BoolExpr* condition, BoolExpr* yes, BoolExpr* no); +SymbolicBits flattenSymbolicBoundary(const SymbolicState& state); + +// Total symbolic counterpart of EventModel. Choice encodings must cover every +// pin ordering, with unused codes selecting a legal ordering (not pruning). +// Fresh symbols are requested serially in stable primitive order, before any +// parallel work. Wave boundaries retain shared producer choices across fanout. +class SymbolicEventModel { + public: + explicit SymbolicEventModel(SymbolicNetwork network, Limits limits = {}, size_t workers = 0); + const SymbolicNetwork& network() const { return network_; } + SymbolicState boundary(const SymbolicBits& current, + const std::vector& storage) const; + SymbolicState bootstrap(const SymbolicBits& inputs, + const std::vector& storage, const SymbolicBits& seeds) const; + SymbolicState admit(const SymbolicState& boundary, const SymbolicBits& inputs) const; + // For reference/proof construction fresh must allocate globally fresh Boolean + // variables. After independent choice-independence certification, constants + // may instead select one legal ordering for a deterministic compiled result. + SymbolicState wave(const SymbolicState& state, const FreshSymbol& fresh) const; + BoolExpr* stable(const SymbolicState& state) const; + // A candidate boundary invariant: all primitive output and level/async rules + // are consistent. The compiler must prove BOOT establishes it and episodes + // preserve it; it is not an assumed reachability restriction. + BoolExpr* boundaryInvariant(const SymbolicState& state) const; + private: + SymbolicNetwork network_; + Limits limits_; + size_t workers_; + std::vector> consumers_; +}; + +// Exact truth-table lifting for small test/reference primitives, not the +// production frontend for wide gates. The latter supplies native DAG callbacks. +SymbolicPrimitive liftPrimitive(const Primitive& primitive, size_t maxLocalBits = 12); + +} // namespace KEPLER_FORMAL::SEC::LATCH diff --git a/src/sec/latch/NajaEventPrimitive.cpp b/src/sec/latch/NajaEventPrimitive.cpp new file mode 100644 index 00000000..42bf3eb0 --- /dev/null +++ b/src/sec/latch/NajaEventPrimitive.cpp @@ -0,0 +1,281 @@ +// Copyright 2026 keplertech.io +// SPDX-License-Identifier: Apache-2.0 +#include "latch/NajaEventPrimitive.h" + +#include +#include +#include +#include +#include +#include "NLBitDependencies.h" +#include "SNLDesign.h" +#include "SNLDesignModeling.h" +#include "SNLInstance.h" +#include "latch/NajaSymbolicLogic.h" +#include "latch/LatchResetClock.h" + +namespace KEPLER_FORMAL::SEC::LATCH { +namespace { +using Modeling = naja::NL::SNLDesignModeling; +using Expression = Modeling::BooleanExpression; +using Operator = Expression::Operator; +using Term = naja::NL::SNLBitTerm; + +// Copy the expression and resolve pin identities once. No Naja objects are read +// by worker callbacks, so compact extraction can release the source netlist. +struct Formula { + Expression expression; + std::vector pins; + BoolExpr* operator()(const SymbolicBits& input, const SymbolicBits& storage) const { + return detail::symbolicFormula(expression, pins, input, storage); + } + bool operator()(const Bits& input, const Bits& storage) const { + Bits values(expression.nodes.size()); + for (size_t i = 0; i < values.size(); ++i) { + const auto& node = expression.nodes[i]; + switch (node.operation) { + case Operator::Constant: values[i] = node.constant; break; + case Operator::Term: values[i] = input.at(pins[i]); break; + case Operator::State: values[i] = storage.at(node.state); break; + case Operator::Not: values[i] = !values.at(node.operands[0]); break; + case Operator::And: case Operator::Or: case Operator::Xor: { + bool value = node.operation == Operator::And; + for (auto operand : node.operands) { + if (node.operation == Operator::And) value &= values[operand]; + else if (node.operation == Operator::Or) value |= values[operand]; + else value ^= values[operand]; + } + values[i] = value; + break; + } + } + } + return values.at(expression.root); + } +}; + +Formula formula(const Expression& source, const std::map& pins, + size_t storageBits, bool allowState = true) { + if (!source.isValid()) throw std::runtime_error("missing sequential expression"); + Formula result{source, std::vector(source.nodes.size())}; + for (size_t i = 0; i < source.nodes.size(); ++i) { + const auto& node = source.nodes[i]; + if (node.operation == Operator::Term) { + const auto pin = pins.find(node.term); + if (pin == pins.end()) throw std::runtime_error("expression references a non-input pin"); + result.pins[i] = pin->second; + } else if (node.operation == Operator::State && + (!allowState || node.state >= storageBits)) { + throw std::runtime_error("invalid state reference in sequential expression"); + } + if ((node.operation == Operator::Not && node.operands.size() != 1) || + ((node.operation == Operator::And || node.operation == Operator::Or || + node.operation == Operator::Xor) && node.operands.empty())) + throw std::runtime_error("invalid sequential expression arity"); + for (auto child : node.operands) + if (child >= i) throw std::runtime_error("cyclic or unordered sequential expression"); + } + // The copied expression no longer needs pointers into the source netlist. + for (auto& node : result.expression.nodes) node.term = nullptr; + return result; +} + +struct StoredRule { + Formula data; + std::optional clear, preset; + Modeling::SequentialState::ClearPresetValue conflict; +}; + +struct SequentialRule { + bool latch = false; + Formula control; + std::vector states; + std::vector outputs; + + Bits output(const Bits& state, const Bits& pins) const { + Bits result; + for (const auto& expression : outputs) result.push_back(expression(pins, state)); + return result; + } + Reaction react(const Bits& old, const Bits& before, const Bits& now, + std::optional changed, bool bootstrap) const { + Reaction result; + result.storage = old; + const bool open = control(now, old); + const bool capture = latch ? open : + (!bootstrap && changed.has_value() && !control(before, old) && open); + for (size_t i = 0; i < states.size(); ++i) { + const auto& rule = states[i]; + const bool clear = rule.clear && (*rule.clear)(now, old); + const bool preset = rule.preset && (*rule.preset)(now, old); + if (clear && preset) { + using Value = Modeling::SequentialState::ClearPresetValue; + switch (rule.conflict) { + case Value::Zero: result.storage[i] = 0; break; + case Value::One: result.storage[i] = 1; break; + case Value::Hold: break; + case Value::Toggle: + result.error = true; + result.reason = "state-dependent simultaneous clear/preset toggle is unsupported"; + break; + case Value::Unknown: + result.error = true; + result.reason = "undefined simultaneous clear/preset"; + break; + } + } else if (clear) result.storage[i] = 0; + else if (preset) result.storage[i] = 1; + else if (capture) result.storage[i] = rule.data(now, old); + } + result.outputs = output(result.storage, now); + return result; + } +}; + +struct Table { + naja::NL::SNLTruthTable truth; + std::vector pins; + bool operator()(const Bits& input) const { + using Type = naja::NL::SNLTruthTable::GenericType; + if (!truth.isGeneric()) { + size_t index = 0; + for (size_t i = 0; i < pins.size(); ++i) index |= size_t(input[pins[i]]) << i; + return truth.bits().bit(index); + } + const auto type = truth.getGenericType(); + bool value = type == Type::AND || type == Type::NAND; + for (size_t pin : pins) { + if (type == Type::AND || type == Type::NAND) value &= input[pin]; + else if (type == Type::OR || type == Type::NOR) value |= input[pin]; + else value ^= input[pin]; + } + return (type == Type::NAND || type == Type::NOR || type == Type::XNOR) ? !value : value; + } +}; +} // namespace + +Primitive makeNajaEventPrimitive(naja::NL::SNLInstance* instance, std::string path, + const std::map& nets, + SymbolicPrimitive* symbolic, ResetClockPrimitive* resetClock) { + if (!instance) throw std::runtime_error("missing leaf instance"); + Primitive primitive; + primitive.name = std::move(path); + std::map pins; + std::map pinByOrder; + std::vector outputs; + for (auto* term : instance->getModel()->getBitTerms()) { + if (term->getDirection() == Term::Direction::Input) { + pins.emplace(term, primitive.inputs.size()); + pinByOrder.emplace(term->getOrderID(), primitive.inputs.size()); + primitive.inputs.push_back(nets.at(term)); + } else if (term->getDirection() == Term::Direction::Output) { + primitive.outputs.push_back(nets.at(term)); + outputs.push_back(term); + } else throw std::runtime_error("bidirectional or undefined primitive pin"); + } + if (outputs.empty()) throw std::runtime_error("outputless primitive has no supported Boolean event model"); + if (Modeling::hasSequentialModel(instance->getModel())) { + const auto& model = Modeling::getSequentialModel(instance->getModel()); + if (!model.isValid()) throw std::runtime_error("invalid explicit sequential model"); + if (model.kind != Modeling::SequentialModel::Kind::Latch && + model.kind != Modeling::SequentialModel::Kind::FlipFlop) + throw std::runtime_error("unknown sequential element kind"); + std::set declaredOutputs; + for (const auto& output : model.outputs) + if (!output.term || output.term->getDesign() != instance->getModel() || + output.term->getDirection() != Term::Direction::Output || + !declaredOutputs.insert(output.term).second) + throw std::runtime_error("invalid or duplicate sequential output association"); + primitive.storageBits = model.states.size(); + auto rule = std::make_shared(); + rule->latch = model.kind == Modeling::SequentialModel::Kind::Latch; + rule->control = formula(model.clockedOn, pins, model.states.size(), false); + for (const auto& state : model.states) { + // Internal state references are not pin events. Latch data feedback and + // state-dependent async controls need additional activation semantics; + // never certify them from the pin-only graph as if they were quiescent. + StoredRule item{formula(state.nextState, pins, model.states.size(), !rule->latch), {}, {}, state.clearPresetValue}; + if (state.clear) item.clear = formula(*state.clear, pins, model.states.size(), false); + if (state.preset) item.preset = formula(*state.preset, pins, model.states.size(), false); + rule->states.push_back(std::move(item)); + } + for (auto* term : outputs) { + const auto it = std::find_if(model.outputs.begin(), model.outputs.end(), + [term](const auto& output) { return output.term == term; }); + if (it == model.outputs.end()) throw std::runtime_error("missing physical sequential output"); + rule->outputs.push_back(formula(it->function, pins, model.states.size())); + } + primitive.react = [rule](const Bits& state, const Bits& before, const Bits& now, + std::optional changed, bool bootstrap) { + return rule->react(state, before, now, changed, bootstrap); + }; + primitive.initialOutputValues = [rule](const Bits& state, const Bits& pins) { + return rule->output(state, pins); + }; + if (symbolic) { + symbolic->react = [rule](const SymbolicBits& state, const SymbolicBits& before, + const SymbolicBits& now, std::optional changed, bool bootstrap) { + return detail::symbolicSequentialReaction(*rule, state, before, now, changed, bootstrap); + }; + symbolic->initialOutputs = [rule](const SymbolicBits& state, const SymbolicBits& pins) { + return detail::symbolicSequentialOutputs(*rule, state, pins); + }; + } + if (resetClock) { + resetClock->kind = rule->latch ? ResetClockPrimitive::Kind::Latch : ResetClockPrimitive::Kind::FlipFlop; + if (!rule->latch) { + SymbolicBits pins; + for (size_t i = 0; i < primitive.inputs.size(); ++i) pins.push_back(BoolExpr::Var(i + 2)); + resetClock->clock = rule->control(pins, SymbolicBits{}); + } + } + } else { + std::vector tables; + for (auto* output : outputs) { + Table table{Modeling::getTruthTable(instance, output->getOrderID()), {}}; + if (!table.truth.isInitialized()) throw std::runtime_error("no explicit sequential model or truth table"); + using Type = naja::NL::SNLTruthTable::GenericType; + const auto type = table.truth.getGenericType(); + if (type == Type::TABLE_SELECT || type == Type::DIVMOD) + throw std::runtime_error("generic arithmetic/table-select primitive not supported by Boolean event adapter"); + for (auto dependency : naja::NL::NLBitDependencies::decodeBits(table.truth.getDependencies())) { + const auto pin = pinByOrder.find(dependency); + if (pin == pinByOrder.end()) throw std::runtime_error("truth table references a non-input pin"); + table.pins.push_back(pin->second); + } + if (table.pins.size() != table.truth.size()) throw std::runtime_error("truth table dependency arity mismatch"); + if (!table.truth.isGeneric() && table.pins.size() >= sizeof(size_t) * 8) + throw std::runtime_error("truth table exceeds event index width"); + tables.push_back(std::move(table)); + } + const auto sharedTables = std::make_shared>(std::move(tables)); + primitive.react = [sharedTables](const Bits&, const Bits&, const Bits& input, + std::optional, bool) { + Reaction result; + for (const auto& table : *sharedTables) result.outputs.push_back(table(input)); + return result; + }; + if (symbolic) symbolic->react = [sharedTables](const SymbolicBits&, const SymbolicBits&, + const SymbolicBits& input, std::optional, bool) { + SymbolicReaction result; + for (const auto& table : *sharedTables) + result.outputs.push_back(detail::symbolicTruthTable(table.truth, table.pins, input)); + return result; + }; + if (resetClock) { + try { + SymbolicBits pins; + for (size_t i = 0; i < primitive.inputs.size(); ++i) pins.push_back(BoolExpr::Var(i + 2)); + resetClock->kind = ResetClockPrimitive::Kind::Combinational; + for (const auto& table : *sharedTables) + resetClock->outputs.push_back(detail::symbolicTruthTable(table.truth, table.pins, pins)); + } catch (const Limit&) { + // Clock discovery is optional. Its symbolic routing budget must not + // disable a primitive that the exact finite event path can still model. + *resetClock = {}; + } + } + } + return primitive; +} +} // namespace KEPLER_FORMAL::SEC::LATCH diff --git a/src/sec/latch/NajaEventPrimitive.h b/src/sec/latch/NajaEventPrimitive.h new file mode 100644 index 00000000..d4ee6278 --- /dev/null +++ b/src/sec/latch/NajaEventPrimitive.h @@ -0,0 +1,17 @@ +// Copyright 2026 keplertech.io +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include +#include "latch/LatchEventModel.h" + +namespace naja::NL { class SNLInstance; class SNLBitTerm; } +namespace KEPLER_FORMAL::SEC::LATCH { +struct SymbolicPrimitive; +struct ResetClockPrimitive; +// No cell-name heuristics: only explicit sequential expressions/truth tables. +Primitive makeNajaEventPrimitive( + naja::NL::SNLInstance* instance, std::string path, + const std::map& nets, + SymbolicPrimitive* symbolic = nullptr, ResetClockPrimitive* resetClock = nullptr); +} // namespace KEPLER_FORMAL::SEC::LATCH diff --git a/src/sec/latch/NajaSymbolicLogic.h b/src/sec/latch/NajaSymbolicLogic.h new file mode 100644 index 00000000..0bf1e1a3 --- /dev/null +++ b/src/sec/latch/NajaSymbolicLogic.h @@ -0,0 +1,112 @@ +// Copyright 2026 keplertech.io +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include "SNLDesignModeling.h" +#include "latch/LatchSymbolicModel.h" + +namespace KEPLER_FORMAL::SEC::LATCH::detail { + +// The concrete and symbolic callbacks share the same copied, validated Naja +// bytecode and pin mapping. No source-netlist pointers are accessed here. +inline BoolExpr* symbolicFormula( + const naja::NL::SNLDesignModeling::BooleanExpression& expression, + const std::vector& pins, const SymbolicBits& input, + const SymbolicBits& storage) { + using Operator = naja::NL::SNLDesignModeling::BooleanExpression::Operator; + SymbolicBits values(expression.nodes.size()); + for (size_t i = 0; i < values.size(); ++i) { + const auto& node = expression.nodes[i]; + switch (node.operation) { + case Operator::Constant: values[i] = BoolExpr::Var(node.constant ? 1 : 0); break; + case Operator::Term: values[i] = input.at(pins.at(i)); break; + case Operator::State: values[i] = storage.at(node.state); break; + case Operator::Not: values[i] = BoolExpr::Not(values.at(node.operands.at(0))); break; + case Operator::And: case Operator::Or: case Operator::Xor: { + auto* value = BoolExpr::Var(node.operation == Operator::And ? 1 : 0); + for (auto operand : node.operands) { + if (node.operation == Operator::And) value = BoolExpr::And(value, values.at(operand)); + else if (node.operation == Operator::Or) value = BoolExpr::Or(value, values.at(operand)); + else value = BoolExpr::Xor(value, values.at(operand)); + } + values[i] = value; + break; + } + default: throw std::runtime_error("unknown sequential expression operator"); + } + } + return values.at(expression.root); +} + +template +SymbolicBits symbolicSequentialOutputs(const Rule& rule, const SymbolicBits& state, + const SymbolicBits& pins) { + SymbolicBits result; + for (const auto& expression : rule.outputs) result.push_back(expression(pins, state)); + return result; +} + +template +SymbolicReaction symbolicSequentialReaction(const Rule& cell, const SymbolicBits& old, + const SymbolicBits& before, const SymbolicBits& now, + std::optional changed, bool bootstrap) { + using Value = naja::NL::SNLDesignModeling::SequentialState::ClearPresetValue; + SymbolicReaction result; + auto* zero = BoolExpr::createFalse(); + auto* one = BoolExpr::createTrue(); + auto* open = cell.control(now, old); + auto* capture = cell.latch ? open : (!bootstrap && changed.has_value() + ? BoolExpr::And(BoolExpr::Not(cell.control(before, old)), open) : zero); + for (size_t i = 0; i < cell.states.size(); ++i) { + const auto& rule = cell.states[i]; + auto* clear = rule.clear ? (*rule.clear)(now, old) : zero; + auto* preset = rule.preset ? (*rule.preset)(now, old) : zero; + auto* both = BoolExpr::And(clear, preset); + auto* conflict = old.at(i); + switch (rule.conflict) { + case Value::Zero: conflict = zero; break; + case Value::One: conflict = one; break; + case Value::Hold: break; + case Value::Toggle: case Value::Unknown: + result.error = BoolExpr::Or(result.error, both); + break; + default: throw std::runtime_error("unknown asynchronous conflict behavior"); + } + auto* normal = symbolicMux(capture, rule.data(now, old), old.at(i)); + result.storage.push_back(symbolicMux(both, conflict, + symbolicMux(clear, zero, symbolicMux(preset, one, normal)))); + } + result.outputs = symbolicSequentialOutputs(cell, result.storage, now); + return result; +} + +inline BoolExpr* symbolicTruthTable(const naja::NL::SNLTruthTable& truth, + const std::vector& pins, const SymbolicBits& input) { + using Type = naja::NL::SNLTruthTable::GenericType; + if (!truth.isGeneric()) { + // This is expansion of the provided local cell table, never a circuit's + // input/state space. Refuse pathological tables before allocating a DAG. + if (pins.size() > 16) throw Limit("symbolic local truth table exceeds 16 inputs"); + SymbolicBits layer; + const size_t width = size_t(1) << pins.size(); + layer.reserve(width); + for (size_t i = 0; i < width; ++i) layer.push_back(BoolExpr::Var(truth.bits().bit(i) ? 1 : 0)); + for (size_t pin = 0; pin < pins.size(); ++pin) { + for (size_t i = 0; i < layer.size() / 2; ++i) + layer[i] = symbolicMux(input.at(pins[pin]), layer[2*i + 1], layer[2*i]); + layer.resize(layer.size() / 2); + } + return layer.front(); + } + const auto type = truth.getGenericType(); + auto* value = BoolExpr::Var(type == Type::AND || type == Type::NAND ? 1 : 0); + for (size_t pin : pins) { + if (type == Type::AND || type == Type::NAND) value = BoolExpr::And(value, input.at(pin)); + else if (type == Type::OR || type == Type::NOR) value = BoolExpr::Or(value, input.at(pin)); + else if (type == Type::XOR || type == Type::XNOR) value = BoolExpr::Xor(value, input.at(pin)); + else throw std::runtime_error("unsupported symbolic generic truth table"); + } + return type == Type::NAND || type == Type::NOR || type == Type::XNOR ? BoolExpr::Not(value) : value; +} + +} // namespace KEPLER_FORMAL::SEC::LATCH::detail diff --git a/src/sec/model/OpaquePolicy.cpp b/src/sec/model/OpaquePolicy.cpp new file mode 100644 index 00000000..ada31730 --- /dev/null +++ b/src/sec/model/OpaquePolicy.cpp @@ -0,0 +1,79 @@ +// Copyright 2024-2026 keplertech.io +// SPDX-License-Identifier: Apache-2.0 + +#include "model/OpaquePolicy.h" + +#include +#include + +#include "../../config/Config.h" +#include "../../utils/DesignBoundary.h" +#include "SNLDesign.h" +#include "SNLPath.h" +#include "common/SignalKey.h" +#include "model/SequentialDesignModel.h" + +namespace KEPLER_FORMAL::SEC { + +void recordOpaqueOutputlessCells(SequentialDesignModel& model, + const naja::DNL::DNLFull& dnl, + const LeafBoundary* boundary) { + if (!Config::getErrorOnOpaque()) return; + for (const auto leafID : dnl.getLeaves()) { + const auto& instance = dnl.getDNLInstanceFromID(leafID); + if (instance.isTop() || (boundary && boundary->containsInstance(leafID))) continue; + const auto* cell = instance.getSNLModel(); + if (!cell || !cell->isLeaf()) continue; + bool hasOutput = false; + for (auto* term : cell->getBitTerms()) { + if (term->getDirection() != naja::NL::SNLTerm::Direction::Input) { + hasOutput = true; + break; + } + } + if (hasOutput) continue; + SignalKey key; + for (const auto& part : instance.getPath().getPathNames()) { + key.first.push_back(part.getID()); + } + key.first.push_back(uint64_t{1} << 60); + key.second.push_back(0); + const auto path = instance.getFullPath(); + model.displayNameByKey.insert_or_assign(key, path); + model.connectivitySkipInfoByKey.insert_or_assign(key, + ConnectivitySkipInfo{ConnectivitySkipOrigin::OpaqueInternal, + "opaque outputless cell `" + path + "` (model `" + + cell->getName().getString() + "`): no usable SEC output model"}); + } +} + +void applyOpaquePolicy(SequentialDesignModel& model, + const std::string& topName, size_t side) { + if (!Config::getErrorOnOpaque()) { + return; + } + std::optional first; + for (const auto& [key, info] : model.connectivitySkipInfoByKey) { + if (info.origin != ConnectivitySkipOrigin::OpaqueInternal) { + continue; + } + const auto name = model.displayNameByKey.find(key); + const auto signal = name == model.displayNameByKey.end() + ? signalKeyToString(key) : name->second; + const std::string diagnostic = + "SEC error-on-opaque: design " + std::to_string(side + 1) + + " (`" + topName + "`), signal `" + signal + "`: " + info.detail; + // Hash-map traversal and extraction worker ordering must not determine + // which diagnostic the user sees first. + if (!first || diagnostic < *first) { + first = diagnostic; + } + } + if (first && std::find(model.unsupportedReasons.begin(), + model.unsupportedReasons.end(), *first) == + model.unsupportedReasons.end()) { + model.unsupportedReasons.push_back(*first); + } +} + +} // namespace KEPLER_FORMAL::SEC diff --git a/src/sec/model/OpaquePolicy.h b/src/sec/model/OpaquePolicy.h new file mode 100644 index 00000000..f4e332c9 --- /dev/null +++ b/src/sec/model/OpaquePolicy.h @@ -0,0 +1,32 @@ +// Copyright 2024-2026 keplertech.io +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include +#include +#include "DNL.h" + +namespace KEPLER_FORMAL { +class LeafBoundary; +} + +namespace KEPLER_FORMAL::SEC { + +struct SequentialDesignModel; + +// Outputless primitive/black-box instances have no output key for the normal +// boundary collector. Scan the explicitly supplied graph only in strict mode. +// Top/ordinary empty hierarchy and explicitly selected boundaries are not +// implicitly reclassified as opaque cells. +void recordOpaqueOutputlessCells(SequentialDesignModel& model, + const naja::DNL::DNLFull& dnl, + const LeafBoundary* boundary = nullptr); + +// Promote already-classified opacity to a hard unsupported result only when +// the opt-in policy is enabled. Call before pruning and after late extraction +// classifications: disconnected cells must not escape the policy. +void applyOpaquePolicy(SequentialDesignModel& model, + const std::string& topName, size_t side); + +} // namespace KEPLER_FORMAL::SEC diff --git a/src/sec/model/SequentialDesignModel.cpp b/src/sec/model/SequentialDesignModel.cpp index 2b7a4d47..f3d65e6e 100644 --- a/src/sec/model/SequentialDesignModel.cpp +++ b/src/sec/model/SequentialDesignModel.cpp @@ -33,6 +33,8 @@ #include "../../clauses/Tree2BoolExpr.h" #include "common/BoolExprUtils.h" #include "model/SecNetlistChecks.h" +#include "model/OpaquePolicy.h" +#include "latch/LatchNetlistAdapter.h" #include "../../strategies/miter/BuildPrimaryOutputClauses.h" namespace KEPLER_FORMAL::SEC { @@ -4857,6 +4859,10 @@ SequentialDesignModel SequentialDesignModel::extract( throw std::runtime_error("SequentialDesignModel::extract: NLUniverse not created"); } + if (auto eventModel = LATCH::extractEventDesign(top, pairs, side)) { + return std::move(*eventModel); + } + SequentialDesignModel model; ExtractContext ctx{ // LCOV_EXCL_START @@ -4877,6 +4883,8 @@ SequentialDesignModel SequentialDesignModel::extract( collectTopInterfaceTerms(ctx, model); classifyBuilderBoundaryTerms(ctx, model); collectSequentialTransitions(ctx, model); + recordOpaqueOutputlessCells(model, *ctx.dnl, ctx.boundary); + applyOpaquePolicy(model, ctx.topName, side); if (model.hasUnsupportedFeatures()) { // Primitive-modeling issues are structural, not proof-related. Report them @@ -5006,6 +5014,7 @@ SequentialDesignModel SequentialDesignModel::extract( // Phase 6: make sure the remaining covered interface is complete before SEC // hands this model to the proof engines. + applyOpaquePolicy(model, ctx.topName, side); validateExtractedModel(model); // Restore the original top design for callers that keep using the universe. diff --git a/src/sec/model/SequentialDesignModel.h b/src/sec/model/SequentialDesignModel.h index 876a4860..5a8d44ed 100644 --- a/src/sec/model/SequentialDesignModel.h +++ b/src/sec/model/SequentialDesignModel.h @@ -4,6 +4,7 @@ #pragma once #include +#include #include #include @@ -17,6 +18,7 @@ class SNLDesign; } namespace KEPLER_FORMAL::SEC { +namespace LATCH { struct EventResetInterface; } struct ComplementedStateRelation { // LCOV_EXCL_LINE SignalKey primaryKey; @@ -54,6 +56,12 @@ struct SequentialDesignModel { // LCOV_EXCL_LINE std::unordered_map observedOutputExprByKey; std::unordered_map nextStateExprByStateKey; std::unordered_map initialStateValueByKey; + // An exact initial relation can represent unspecified, independent Boolean + // storage and correlated settled event signals without fixing their values. + // It is applied only at frame zero, in addition to any unit initial facts. + BoolExpr* initialCondition = nullptr; + std::unordered_map + initialInputStateKeyByInputKey; // Variables proven during extraction to be pure routed clock carriers. // Downstream SEC matching can classify them with the top clock without // making any name-based assumption about internal sequential state. @@ -64,6 +72,13 @@ struct SequentialDesignModel { // LCOV_EXCL_LINE connectivitySkipInfoByKey; std::vector complementedStateRelations; std::vector unsupportedReasons; + // Empty for legacy clock-cycle extraction; event models retain their contract + // after compact mode releases the netlists. + std::string eventContract; + // Copied input-level/clock metadata for reset-cycle expansion after compact + // extraction; never retains pointers into the source netlist. + std::shared_ptr eventResetInterface; + size_t eventResetCycles = 0; // Extract the model from the given top design. Opaque per-output cones are // skipped; globally unsupported structures are recorded in unsupportedReasons. diff --git a/src/sec/pdr/PDREngine.cpp b/src/sec/pdr/PDREngine.cpp index 840179bf..79d2851a 100644 --- a/src/sec/pdr/PDREngine.cpp +++ b/src/sec/pdr/PDREngine.cpp @@ -1399,6 +1399,8 @@ struct PDRExactInitCache::Impl { sourceProblem->resetBootstrapInputs == candidate.resetBootstrapInputs && sourceProblem->initialStateAssignments == candidate.initialStateAssignments && + sourceProblem->hasExactRelationalInitialState == + candidate.hasExactRelationalInitialState && sourceProblem->bootstrapStateAssignments == candidate.bootstrapStateAssignments && sourceProblem->state0Symbols == candidate.state0Symbols && @@ -5356,7 +5358,10 @@ class PdrTernaryModelReducer { } for (auto& [symbolMap, dependencies] : memoDependenciesBySymbolMap_) { - (void)symbolMap; + // Later roots can extend the shared DAG under a different symbol map. + // Parent propagation visits those new nodes even for an earlier map, + // so every memo must cover the final DAG before generation checks. + dependencies.memo = &supportCache_->ternaryEvaluationMemo(symbolMap); for (auto& [mappedSymbol, localSymbols] : dependencies.localSymbolsByMappedSymbol) { (void)mappedSymbol; diff --git a/src/sec/proof/ProofEngineShared.cpp b/src/sec/proof/ProofEngineShared.cpp index 67a40ce2..4de45917 100644 --- a/src/sec/proof/ProofEngineShared.cpp +++ b/src/sec/proof/ProofEngineShared.cpp @@ -330,7 +330,13 @@ BoolExpr* buildProofInitFormula(const KInductionProblem& problem) { hasConstraint = true; } } else { - if (problem.initialCondition == BoolExpr::createTrue() && + if (problem.hasExactRelationalInitialState) { + init = problem.initialCondition != nullptr + ? problem.initialCondition : BoolExpr::createTrue(); + hasConstraint = true; + init = appendStructuredAssignmentFacts( + init, problem.initialStateAssignments, hasConstraint); + } else if (problem.initialCondition == BoolExpr::createTrue() && !problem.initialStateAssignments.empty()) { // Dual-rail SEC keeps the boot rails as structured unit facts so PDR and // k-induction can encode only the local COI. Formula-based callers such diff --git a/src/sec/strategy/SequentialEquivalenceStrategy.cpp b/src/sec/strategy/SequentialEquivalenceStrategy.cpp index 82956fb4..e8752b8a 100644 --- a/src/sec/strategy/SequentialEquivalenceStrategy.cpp +++ b/src/sec/strategy/SequentialEquivalenceStrategy.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -40,6 +41,9 @@ #include "kinduction/OutputBatching.h" #include "kinduction/SatEncoding.h" #include "model/SequentialDesignModel.h" +#include "latch/LatchEventContract.h" +#include "latch/LatchInitialState.h" +#include "latch/LatchResetAdapter.h" #include "pdr/PDREngine.h" #include "proof/DualRailEncoding.h" #include "proof/TransitionExprResolver.h" @@ -830,7 +834,9 @@ std::string formatConeTraceback(const KInductionResult::CounterexampleWitness& w std::ostringstream oss; oss << "Traceback for first differing point `" << differencePoint.signal - << "` at cycle " << witness.badFrame << ":\n"; + << "` at " << (model0.eventContract.empty() ? "cycle " : + model0.eventResetCycles ? "reset/event step " : "event transaction ") + << witness.badFrame << ":\n"; // LCOV_EXCL_STOP @@ -894,8 +900,13 @@ std::string formatCounterexampleWitness(const KInductionResult& result, } const auto& witness = *result.witness; + const char* step = model0.eventContract.empty() ? "cycle " : + model0.eventResetCycles ? "reset/event step " : "event transaction "; std::ostringstream oss; - oss << "Counterexample reaches the first bad frame at cycle " + if (!model0.eventContract.empty()) oss << "Event contract: " << model0.eventContract << ".\n"; + if (model0.eventResetCycles) oss << "The first " << model0.eventResetCycles + << " steps are reset clock cycles; subsequent steps are external event transactions.\n"; + oss << "Counterexample reaches the first bad frame at " << step << witness.badFrame << ".\n"; if (witness.inputTrace.empty()) { @@ -903,7 +914,7 @@ std::string formatCounterexampleWitness(const KInductionResult& result, } else { // LCOV_EXCL_LINE oss << "Input trace:\n"; for (const auto& frame : witness.inputTrace) { - oss << " cycle " << frame.frame << ": "; + oss << " " << step << frame.frame << ": "; if (frame.assignments.empty()) { oss << ""; // LCOV_EXCL_LINE } else { // LCOV_EXCL_LINE @@ -922,7 +933,7 @@ std::string formatCounterexampleWitness(const KInductionResult& result, // LCOV_EXCL_STOP if (!witness.outputMismatches.empty()) { - oss << "Observed output mismatches at cycle " << witness.badFrame << ":\n"; + oss << "Observed output mismatches at " << step << witness.badFrame << ":\n"; // LCOV_EXCL_START for (const auto& mismatch : witness.outputMismatches) { oss << " " << mismatch.signal << ": design0=" @@ -2521,6 +2532,7 @@ void addDualRailInitialAssignments( for (const auto& key : model.stateBits) { const auto rails = railsByKey.at(key); const auto value = lookupStateValue(model.initialStateValueByKey, key); + if (model.initialCondition && !value.has_value()) continue; addDualRailStateAssignment(problem.initialStateAssignments, rails, value); problem.initializedStateCount += 2; } @@ -2877,6 +2889,19 @@ KInductionProblem buildDualRailSecProblem( // materializing a huge duplicate conjunction over every rail. problem.initialCondition = BoolExpr::createTrue(); + if (model0.initialCondition || model1.initialCondition) { + auto symbols0 = symbolSpace.localToCombined0; + auto symbols1 = symbolSpace.localToCombined1; + for (const auto& [local, rails] : railMaps.localState0BySymbol) + symbols0[local] = rails.mayBeOne; + for (const auto& [local, rails] : railMaps.localState1BySymbol) + symbols1[local] = rails.mayBeOne; + LATCH::integrateEventInitialState(model0, model1, alignedInputs, + symbols0, symbols1, problem); + LATCH::constrainEventInitialRails(model0, railMaps.localState0BySymbol, problem); + LATCH::constrainEventInitialRails(model1, railMaps.localState1BySymbol, problem, true); + } + // LCOV_DISABLED_START SecDualRailVariableMapper mapper0( @@ -3737,6 +3762,27 @@ SequentialEquivalenceResult SequentialEquivalenceStrategy::runExtractedModels( // Phase 2: align the externally visible SEC interface, then drop any outputs // whose cones were already classified as skipped by extraction. // Internal names are candidate hints only; relations are certified below. + if (!model0.eventContract.empty() && resetSpec_.enabled()) { + auto failure = [&](const std::string& reason) { + return makeSecResult(SequentialEquivalenceStatus::Unsupported, 0, reason, + OutputCoverageSelection{}, extractedBoundaryReports); + }; + if (auto error = LATCH::eventContractError(model0.eventContract, model1.eventContract, false)) + return failure(*error); + if (resetSpec_.cycles > std::numeric_limits::max() - maxK) + return failure("reset cycle count overflows the SEC bound"); + auto first = LATCH::adaptResetCycles(model0, resetSpec_); + if (!first.model) return failure("design0: " + first.error); + auto second = LATCH::adaptResetCycles(model1, resetSpec_); + if (!second.model) return failure("design1: " + second.error); + auto adapted = *this; + adapted.resetSpec_ = {}; + return adapted.runExtractedModels(*first.model, *second.model, maxK + resetSpec_.cycles); + } + if (auto error = LATCH::eventContractError(model0.eventContract, model1.eventContract, resetSpec_.enabled())) { + return makeSecResult(SequentialEquivalenceStatus::Unsupported, 0, *error, + OutputCoverageSelection{}, extractedBoundaryReports); + } AlignedSecInterface aligned = alignSecInterface( model0, model1, @@ -3772,6 +3818,8 @@ SequentialEquivalenceResult SequentialEquivalenceStrategy::runExtractedModels( symbolSpace.state0Symbols, symbolSpace.state1Symbols, symbolSpace.problem); + LATCH::integrateEventInitialState(model0, model1, aligned.inputs, + symbolSpace.localToCombined0, symbolSpace.localToCombined1, symbolSpace.problem); if (auto resetError = applyResetBootstrapSpec( resetSpec_, aligned.inputs, symbolSpace.problem, secDiagEnabled)) { return makeSecResult( @@ -3782,7 +3830,8 @@ SequentialEquivalenceResult SequentialEquivalenceStrategy::runExtractedModels( extractedBoundaryReports); } if (encoding_ == SecEncoding::Binary) { - if (symbolSpace.problem.hasResetBootstrap()) { + if (symbolSpace.problem.hasResetBootstrap() || + symbolSpace.problem.hasExactRelationalInitialState) { logSecDiagLine( secDiagEnabled, "SEC diag: reset bootstrap keeps reset-unanchored outputs in the " @@ -3868,7 +3917,7 @@ SequentialEquivalenceResult SequentialEquivalenceStrategy::runExtractedModels( if (exportOptions_.enabled()) { exportSecBtor2File(proofProblem, exportOptions_.path, - {aligned.outputCoverage.totalOutputs, aligned.outputCoverage.skippedOutputs}); + {aligned.outputCoverage.totalOutputs, aligned.outputCoverage.skippedOutputs, model0.eventContract}); if (exportOptions_.dumpOnly) { return makeSecResult(SequentialEquivalenceStatus::Exported, 0, "BTOR2 exported to " + exportOptions_.path + "; proof not run", diff --git a/test/python/CMakeLists.txt b/test/python/CMakeLists.txt index 6c4d7d48..f544fec8 100644 --- a/test/python/CMakeLists.txt +++ b/test/python/CMakeLists.txt @@ -34,3 +34,24 @@ add_test(NAME kepler-formal-borrowed-native-tests WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} ) set_tests_properties(kepler-formal-borrowed-native-tests PROPERTIES TIMEOUT 120) + +add_executable(kepler-borrowed-policy-tests borrowed_policy_tests.cpp) +target_link_libraries(kepler-borrowed-policy-tests PRIVATE kepler_borrowed_designs Python3::Python) +najaeda_fixup_consumer(kepler-borrowed-policy-tests) +add_test(NAME kepler-formal-borrowed-policy-tests COMMAND kepler-borrowed-policy-tests) +set_tests_properties(kepler-formal-borrowed-policy-tests PROPERTIES TIMEOUT 120) + +add_executable(kepler-borrowed-latch-options-tests borrowed_latch_options_tests.cpp + ${PROJECT_SOURCE_DIR}/src/python/BorrowedLatchOptions.cpp + ${PROJECT_SOURCE_DIR}/src/python/PyLatchOptions.cpp) +target_include_directories(kepler-borrowed-latch-options-tests PRIVATE + ${PROJECT_SOURCE_DIR}/src/python ${PROJECT_SOURCE_DIR}/src/sec) +target_link_libraries(kepler-borrowed-latch-options-tests PRIVATE Python3::Python) +add_test(NAME kepler-formal-borrowed-latch-options-tests COMMAND kepler-borrowed-latch-options-tests) + +add_executable(kepler-borrowed-latch-tests borrowed_latch_tests.cpp) +target_link_libraries(kepler-borrowed-latch-tests PRIVATE kepler_borrowed_designs Python3::Python) +najaeda_fixup_consumer(kepler-borrowed-latch-tests) +add_test(NAME kepler-formal-borrowed-latch-tests COMMAND kepler-borrowed-latch-tests) +set_tests_properties(kepler-formal-borrowed-latch-options-tests + kepler-formal-borrowed-latch-tests PROPERTIES TIMEOUT 120) diff --git a/test/python/borrowed_latch_options_tests.cpp b/test/python/borrowed_latch_options_tests.cpp new file mode 100644 index 00000000..d1c29a3d --- /dev/null +++ b/test/python/borrowed_latch_options_tests.cpp @@ -0,0 +1,212 @@ +// Copyright 2026 keplertech.io +// SPDX-License-Identifier: Apache-2.0 +// Exercises the actual native Python parser without requiring a Naja runtime. +#include "PyLatchOptions.h" + +#include +#include +#include +#include + +namespace { +using namespace KEPLER_FORMAL; +size_t checks = 0; +void check(bool value, const std::string& detail) { + ++checks; + if (!value) throw std::runtime_error(detail); +} + +BorrowedLatchOptions contract() { + BorrowedLatchOptions options; + options.inputChanges = LatchInputChanges::Single; + options.initialInputs = false; + options.initialStorage = false; + return options; +} + +void invalid(const BorrowedLatchOptions& options, const char* message, + bool sec = true, bool boundaries = false) { + try { (void)options.validated(sec, boundaries); } + catch (const std::invalid_argument& error) { + check(std::string(error.what()).find(message) != std::string::npos, error.what()); + return; + } + check(false, "invalid C++ options accepted"); +} + +void cppOptions() { + for (bool sec : {false, true}) { + for (bool boundaries : {false, true}) { + auto defaults = BorrowedLatchOptions{}.validated(sec, boundaries); + check(defaults.enabled == sec && !defaults.initialInputs && !defaults.initialStorage && + !defaults.singleInputChange && !defaults.explicitConfiguration, + "default-on support invented assumptions or enabled LEC event semantics"); + BorrowedLatchOptions disabled; + disabled.enabled = false; + check(!disabled.validated(sec, boundaries).enabled, "explicit false ignored"); + } + } + auto options = contract(); + auto result = options.validated(true, false); + check(result.enabled && result.singleInputChange && result.initialInputs == false && + result.initialStorage == false, "explicit zeros did not survive"); + options.inputChanges = LatchInputChanges::Any; + options.initialInputs = true; + options.initialStorage = true; + options.workers = 0; + options.maxWaves = 17; + options.maxStates = 23; + options.maxTransactions = 31; + options.maxSymbolicNodes = 1000; + options.maxSatConflicts = 123; + options.maxSatDecisions = 456; + result = options.validated(true, false); + check(!result.singleInputChange && result.initialInputs == true && result.initialStorage == true && + result.workers == 0 && result.limits.maxWaves == 17 && result.limits.maxBoundaryStates == 23 && + result.limits.maxTransactions == 31 && result.maxSymbolicNodes == 1000 && + result.maxSatConflicts == 123 && result.maxSatDecisions == 456, "explicit tuning lost"); + invalid(options, "only supported for SEC", false); + invalid(options, "complete top interface", true, true); + options.enabled = false; + invalid(options, "requires latch_support"); + for (int field = 0; field < 3; ++field) { + options = contract(); + if (field == 0) options.inputChanges.reset(); + if (field == 1) options.initialInputs.reset(); + if (field == 2) options.initialStorage.reset(); + const auto partial = options.validated(true, false); + check(partial.enabled && partial.initialInputs == options.initialInputs && + partial.initialStorage == options.initialStorage && + partial.singleInputChange == (options.inputChanges == LatchInputChanges::Single) && + partial.explicitConfiguration, "optional semantic field was required or defaulted"); + } + options = contract(); + options.inputChanges = static_cast(999); + invalid(options, "any or single"); + options = contract(); + options.workers = size_t(std::numeric_limits::max()) + 1; + invalid(options, "nonnegative int"); + for (int field = 0; field < 3; ++field) { + options = contract(); + if (field == 0) options.maxWaves = 0; + if (field == 1) options.maxStates = 0; + if (field == 2) options.maxTransactions = 0; + invalid(options, "positive integers"); + } + options = {}; + options.workers = 0; + result = options.validated(true, false); + check(result.enabled && result.explicitConfiguration && !result.singleInputChange && + !result.initialInputs && !result.initialStorage && result.workers == 0, + "resource-only tuning invented initialization"); + options.enabled = false; + invalid(options, "requires latch_support"); + for (int field = 0; field < 3; ++field) { + options = contract(); + auto& limit = field == 0 ? options.maxSymbolicNodes : field == 1 ? options.maxSatConflicts : options.maxSatDecisions; + limit = 0; + invalid(options, "positive integers"); + if (field) { + limit = size_t(std::numeric_limits::max()) + 1; + invalid(options, "unsigned int"); + } + limit = 1; + options.enabled = false; + invalid(options, "requires latch_support"); + } + check(isBorrowedLatchOption("latch_support") && + isBorrowedLatchOption("latch_max_transactions") && + !isBorrowedLatchOption(std::string_view("latch_support\0suffix", 20)) && + !isBorrowedLatchOption("latch_unknown"), "option allowlist incorrect"); +} + +void parsed(const std::string& expression, bool success, PyObject* exception = nullptr, + bool sec = true, bool boundaries = false, bool expectedEnabled = true) { + PyObject* scope = PyDict_New(); + PyDict_SetItemString(scope, "__builtins__", PyEval_GetBuiltins()); + PyObject* dictionary = PyRun_String(expression.c_str(), Py_eval_input, scope, scope); + Py_DECREF(scope); + check(dictionary && PyDict_Check(dictionary), "bad Python fixture: " + expression); + BorrowedLatchOptions options; + const bool accepted = parseBorrowedLatchOptions(dictionary, options, sec, boundaries); + Py_DECREF(dictionary); + check(accepted == success, "unexpected parser verdict: " + expression); + if (!accepted) { + check(PyErr_Occurred() && (!exception || PyErr_ExceptionMatches(exception)), + "wrong parser exception: " + expression); + PyErr_Clear(); + } else { + check(!PyErr_Occurred(), "success retained Python exception"); + check(options.enabled == expectedEnabled, "parser changed the master gate"); + const auto validated = options.validated(sec, boundaries); + check(validated.initialInputs == options.initialInputs && + validated.initialStorage == options.initialStorage && + validated.singleInputChange == (options.inputChanges == LatchInputChanges::Single), + "parser invented an omitted semantic setting"); + } +} + +void pythonOptions() { + const std::string good = "{'latch_input_changes': 'single', " + "'latch_initial_inputs': 0, 'latch_initial_storage': 0}"; + parsed("{}", true, nullptr, false, true); + parsed("{}", true); + parsed("{'latch_support': False}", true, nullptr, true, false, false); + parsed("{'latch_support': True}", true); + parsed(good, true); + parsed(good + " | {'latch_support': True}", true); + parsed(good + " | {'latch_support': False}", false, PyExc_ValueError); + parsed(good + " | {'latch_input_changes': 'any', 'latch_initial_inputs': 1, 'latch_initial_storage': 1}", true); + parsed(good + " | {'latch_workers': 0, 'latch_max_waves': 8, 'latch_max_states': 16, 'latch_max_transactions': 32}", true); + parsed(good, false, PyExc_ValueError, false); + parsed(good, false, PyExc_ValueError, true, true); + for (const char* value : {"None", "0", "1", "'true'", "[]"}) + parsed(good + " | {'latch_support': " + value + "}", false, PyExc_TypeError); + for (const char* key : {"latch_input_changes", "latch_initial_inputs", "latch_initial_storage"}) + parsed(good + " | {'" + key + "': None}", true); + for (const char* value : {"''", "'ANY'", "'single\\x00tail'"}) + parsed(good + " | {'latch_input_changes': " + value + "}", false, PyExc_ValueError); + for (const char* value : {"True", "1", "[]"}) + parsed(good + " | {'latch_input_changes': " + value + "}", false, PyExc_TypeError); + for (const char* key : {"latch_initial_inputs", "latch_initial_storage"}) { + for (const char* value : {"True", "False", "'0'", "0.0"}) + parsed(good + " | {'" + key + "': " + value + "}", false, PyExc_TypeError); + parsed(good + " | {'" + key + "': 2}", false, PyExc_ValueError); + parsed(good + " | {'" + key + "': -1}", false, PyExc_OverflowError); + } + for (const char* key : {"latch_workers", "latch_max_waves", "latch_max_states", "latch_max_transactions", + "latch_max_symbolic_nodes", "latch_max_sat_conflicts", "latch_max_sat_decisions"}) { + for (const char* value : {"True", "'1'", "1.0"}) + parsed(good + " | {'" + key + "': " + value + "}", false, PyExc_TypeError); + parsed(good + " | {'" + key + "': 2**128}", false, PyExc_OverflowError); + parsed(good + " | {'" + key + "': -1}", false, PyExc_OverflowError); + parsed("{'" + std::string(key) + "': 1}", true); + parsed("{'latch_support': False, '" + std::string(key) + "': 1}", false, PyExc_ValueError); + } + parsed(good + " | {'latch_workers': 2**31}", false, PyExc_ValueError); + for (const char* key : {"latch_max_waves", "latch_max_states", "latch_max_transactions", + "latch_max_symbolic_nodes", "latch_max_sat_conflicts", "latch_max_sat_decisions"}) + parsed(good + " | {'" + key + "': 0}", false, PyExc_ValueError); + for (const char* key : {"latch_max_sat_conflicts", "latch_max_sat_decisions"}) + parsed(good + " | {'" + key + "': 2**32}", false, PyExc_ValueError); + parsed("{'latch_input_changes': 'any'}", true); + parsed("{'latch_initial_inputs': 0}", true); + parsed("{'latch_initial_storage': 0}", true); +} +} // namespace + +int main() { + Py_Initialize(); + try { + cppOptions(); + pythonOptions(); + Py_Finalize(); + std::cout << "Borrowed latch options: " << checks << " checks passed\n"; + return 0; + } catch (const std::exception& error) { + if (PyErr_Occurred()) PyErr_Print(); + Py_Finalize(); + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/test/python/borrowed_latch_tests.cpp b/test/python/borrowed_latch_tests.cpp new file mode 100644 index 00000000..740c94d8 --- /dev/null +++ b/test/python/borrowed_latch_tests.cpp @@ -0,0 +1,196 @@ +// Copyright 2026 keplertech.io +// SPDX-License-Identifier: Apache-2.0 + +#include +#include +#include +#include +#include + +#include "KeplerBorrowedDesigns.h" +#include "DNL.h" +#include "NLUniverse.h" +#include "NLLibrary.h" +#include "SNLDesign.h" +#include "SNLDesignModeling.h" +#include "SNLInstance.h" +#include "SNLInstTerm.h" +#include "SNLScalarNet.h" +#include "SNLScalarTerm.h" + +namespace { +using namespace KEPLER_FORMAL; +using namespace naja::NL; +size_t checks = 0; +void check(bool value, const std::string& detail) { + ++checks; + if (!value) throw std::runtime_error(detail); +} + +SNLDesign* latchModel(NLLibrary* primitives, const char* name, bool invert) { + auto* cell = SNLDesign::create(primitives, SNLDesign::Type::Primitive, NLName(name)); + auto* data = SNLScalarTerm::create(cell, SNLTerm::Direction::Input, NLName("D")); + auto* enable = SNLScalarTerm::create(cell, SNLTerm::Direction::Input, NLName("E")); + auto* output = SNLScalarTerm::create(cell, SNLTerm::Direction::Output, NLName("Q")); + SNLDesignModeling::SequentialModel model; + model.kind = SNLDesignModeling::SequentialModel::Kind::Latch; + model.clockedOn.root = model.clockedOn.addTerm(enable); + SNLDesignModeling::SequentialState storage; + storage.nextState.root = storage.nextState.addTerm(data); + model.states.push_back(storage); + SNLDesignModeling::BooleanExpression visible; + visible.root = visible.addState(0); + if (invert) visible.root = visible.addOperation( + SNLDesignModeling::BooleanExpression::Operator::Not, {visible.root}); + model.outputs.push_back({output, visible}); + SNLDesignModeling::setSequentialModel(cell, model); + return cell; +} + +SNLDesign* top(NLLibrary* library, const char* name, SNLDesign* cell) { + auto* design = SNLDesign::create(library, NLName(name)); + auto* instance = SNLInstance::create(design, cell, NLName("latch")); + for (auto* pin : cell->getScalarTerms()) { + auto* net = SNLScalarNet::create(design, pin->getName()); + SNLScalarTerm::create(design, pin->getDirection(), pin->getName())->setNet(net); + instance->getInstTerm(pin)->setNet(net); + } + return design; +} + +void run(const std::filesystem::path& directory) { + auto* universe = NLUniverse::create(); + auto* database = NLDB::create(universe); + auto* designs = NLLibrary::create(database, NLName("designs")); + auto* primitives = NLLibrary::create(database, NLLibrary::Type::Primitives, NLName("primitives")); + auto* positive = latchModel(primitives, "explicit_latch", false); + auto* negative = latchModel(primitives, "explicit_inverted_latch", true); + auto* first = top(designs, "first", positive); + auto* second = top(designs, "second", positive); + auto* different = top(designs, "different", negative); + universe->setTopDesign(first); + auto* callerGraph = naja::DNL::get(); + const auto firstReference = first->getReference(); + const auto secondReference = second->getReference(); + SEC::LATCH::SupportOptions ambient; + ambient.enabled = true; + ambient.workers = 7; + SEC::LATCH::ScopedSupportOptions ambientScope(ambient); + + BorrowedDesignOptions options; + options.mode = BorrowedVerificationMode::SEC; + options.logFile = (directory / "latches.log").string(); + RunResult result; + const auto unchanged = [&] { + check(universe->getTopDesign() == first && naja::DNL::get() == callerGraph, + "borrowed run changed the caller's selected design or DNL"); + check(universe->getSNLDesign(firstReference) == first && + universe->getSNLDesign(secondReference) == second && + first->getInstances().size() == 1 && second->getInstances().size() == 1, + "borrowed run modified or deleted source designs"); + check(SEC::LATCH::supportOptions().enabled && SEC::LATCH::supportOptions().workers == 7 && + !SEC::LATCH::supportOptions().initialInputs, + "borrowed run leaked its event contract"); + }; + verifyBorrowedDesigns(first, second, options, result); + check(result.status == RunStatus::Unsupported && result.coveredOutputs == 0, + "default any-change mode certified an ambiguous latch race"); + unchanged(); + + check(options.latchSupport.enabled, "latch support is not enabled by default"); + options.latchSupport.enabled = false; + verifyBorrowedDesigns(first, second, options, result); + check(result.status == RunStatus::Unsupported && result.coveredOutputs == 0, + "explicit false did not preserve opaque-latch behavior"); + unchanged(); + options.latchSupport = {}; + options.latchSupport.inputChanges = LatchInputChanges::Single; + options.latchSupport.initialInputs = false; + options.latchSupport.initialStorage = false; + options.latchSupport.workers = 2; + for (auto engine : {SEC::SecEngine::Pdr, SEC::SecEngine::KInduction, SEC::SecEngine::Imc}) { + for (auto encoding : {SEC::SecEncoding::Binary, SEC::SecEncoding::DualRailSteady}) { + options.secEngine = engine; + options.secEncoding = encoding; + check(verifyBorrowedDesigns(first, second, options, result) == 0 && + result.status == RunStatus::Equivalent && result.coveredOutputs == 1 && result.totalOutputs == 1, + "explicit borrowed latch contract failed self equivalence: " + result.reason); + unchanged(); + } + } + check(verifyBorrowedDesigns(first, different, options, result) != 0 && + result.status == RunStatus::Different, "different latch outputs incorrectly proved equivalent"); + unchanged(); + options.latchSupport.inputChanges = LatchInputChanges::Any; + verifyBorrowedDesigns(first, second, options, result); + check(result.coveredOutputs == 0, "order-dependent any-change latch should remain opaque"); + unchanged(); + options.latchSupport.inputChanges = LatchInputChanges::Single; + options.latchSupport.initialStorage.reset(); + for (auto engine : {SEC::SecEngine::Pdr, SEC::SecEngine::KInduction, SEC::SecEngine::Imc}) { + for (auto encoding : {SEC::SecEncoding::Binary, SEC::SecEncoding::DualRailSteady}) { + options.secEngine = engine; + options.secEncoding = encoding; + check(verifyBorrowedDesigns(first, second, options, result) != 0 && + result.status == RunStatus::Different && result.coveredOutputs == 1, + "independent unknown storage was initialized or paired without justification: " + result.reason); + unchanged(); + } + } + options.secEngine = SEC::SecEngine::KInduction; + options.secEncoding = SEC::SecEncoding::Binary; + options.latchSupport.initialStorage = false; + options.latchSupport.initialInputs.reset(); + check(verifyBorrowedDesigns(first, second, options, result) == 0 && + result.status == RunStatus::Equivalent && result.coveredOutputs == 1, + "shared unspecified initial input levels were not correlated: " + result.reason); + unchanged(); + options.latchSupport.initialStorage.reset(); + check(verifyBorrowedDesigns(first, second, options, result) != 0 && + result.status == RunStatus::Different && result.coveredOutputs == 1, + "omitting both initial restrictions fixed an unknown storage value: " + result.reason); + unchanged(); + options.latchSupport.initialInputs = false; + options.latchSupport.initialStorage = false; + options.latchSupport.enabled = false; + check(verifyBorrowedDesigns(first, second, options, result) != 0 && + result.status == RunStatus::Error && result.reason.find("requires latch_support") != std::string::npos, + "tuning enabled the master gate"); + unchanged(); + options.latchSupport.enabled = true; + options.mode = BorrowedVerificationMode::LEC; + check(verifyBorrowedDesigns(first, second, options, result) != 0 && result.status == RunStatus::Error, + "LEC accepted latch event semantics"); + unchanged(); + options.mode = BorrowedVerificationMode::SEC; + options.setAsBoundary = {{"latch", "latch"}}; + check(verifyBorrowedDesigns(first, second, options, result) != 0 && result.status == RunStatus::Error && + result.reason.find("complete top interface") != std::string::npos, "event mode accepted leaf boundaries"); + unchanged(); + options.setAsBoundary.clear(); + options.latchSupport = {}; + verifyBorrowedDesigns(first, second, options, result); + check(result.coveredOutputs == 0, "enabled call contaminated the following default call"); + unchanged(); + naja::DNL::destroy(); + universe->destroy(); +} +} // namespace + +int main() { + const auto directory = std::filesystem::temp_directory_path() / + ("kepler_borrowed_latches_" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); + std::filesystem::create_directories(directory); + try { + run(directory); + std::filesystem::remove_all(directory); + std::cout << "Borrowed latch integration: " << checks << " checks passed\n"; + return 0; + } catch (const std::exception& error) { + naja::DNL::destroy(); + if (auto* universe = NLUniverse::get()) universe->destroy(); + std::filesystem::remove_all(directory); + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/test/python/borrowed_policy_tests.cpp b/test/python/borrowed_policy_tests.cpp new file mode 100644 index 00000000..3f18dc2e --- /dev/null +++ b/test/python/borrowed_policy_tests.cpp @@ -0,0 +1,103 @@ +// Copyright 2026 keplertech.io +// SPDX-License-Identifier: Apache-2.0 + +#include +#include +#include +#include + +#include "KeplerBorrowedDesigns.h" +#include "NLUniverse.h" +#include "NLLibrary.h" +#include "SNLDesign.h" +#include "SNLInstance.h" +#include "SNLInstTerm.h" +#include "SNLScalarNet.h" +#include "SNLScalarTerm.h" +#include "latch/LatchSupportOptions.h" + +namespace { +using namespace KEPLER_FORMAL; +using namespace naja::NL; + +void check(bool value, const char* detail) { + if (!value) throw std::runtime_error(detail); +} + +void runTests(const std::filesystem::path& directory) { + auto* universe = NLUniverse::create(); + auto* database = NLDB::create(universe); + auto* library = NLLibrary::create(database, NLName("designs")); + auto* top = SNLDesign::create(library, NLName("top")); + auto* net = SNLScalarNet::create(top, NLName("net")); + SNLScalarTerm::create(top, SNLTerm::Direction::Input, NLName("a"))->setNet(net); + SNLScalarTerm::create(top, SNLTerm::Direction::Output, NLName("y"))->setNet(net); + BorrowedDesignOptions options; + options.mode = BorrowedVerificationMode::SEC; + options.secEncoding = SEC::SecEncoding::Binary; + options.secEngine = SEC::SecEngine::KInduction; + options.logFile = (directory / "run.log").string(); + RunResult result; + + // An incomplete ambient event contract would make any extraction unsupported + // if borrowed verification accidentally inherited it. + SEC::LATCH::SupportOptions ambient; + ambient.enabled = true; + ambient.workers = 3; + SEC::LATCH::ScopedSupportOptions scope(ambient); + check(verifyBorrowedDesigns(top, top, options, result) == 0 && + result.status == RunStatus::Equivalent, + "borrowed verification inherited ambient event semantics"); + check(SEC::LATCH::supportOptions().enabled && + !SEC::LATCH::supportOptions().initialInputs.has_value() && + SEC::LATCH::supportOptions().workers == 3, + "borrowed verification failed to restore ambient event contract"); + + auto* primitives = NLLibrary::create(database, NLLibrary::Type::Primitives, NLName("prims")); + auto* unknown = SNLDesign::create(primitives, SNLDesign::Type::Primitive, NLName("OPAQUE")); + auto* d = SNLScalarTerm::create(unknown, SNLTerm::Direction::Input, NLName("D")); + auto* q = SNLScalarTerm::create(unknown, SNLTerm::Direction::Output, NLName("Q")); + auto* instance = SNLInstance::create(top, unknown, NLName("unused")); + instance->getInstTerm(d)->setNet(net); + instance->getInstTerm(q)->setNet(SNLScalarNet::create(top, NLName("unused_net"))); + Config::setErrorOnOpaque(true); + check(verifyBorrowedDesigns(top, top, options, result) == 0 && + result.status == RunStatus::Equivalent, + "borrowed default inherited caller's strict opaque policy"); + check(Config::getErrorOnOpaque(), "borrowed default did not restore strict opaque policy"); + Config::setErrorOnOpaque(false); + options.errorOnOpaque = true; + check(verifyBorrowedDesigns(top, top, options, result) != 0 && + result.status == RunStatus::Unsupported && + result.reason.find("unused") != std::string::npos, + "borrowed strict policy ignored disconnected opacity"); + check(!Config::getErrorOnOpaque(), "borrowed strict policy leaked to caller"); + check(SEC::LATCH::supportOptions().enabled, + "unsupported borrowed run failed to restore ambient event scope"); + options.mode = BorrowedVerificationMode::LEC; + check(verifyBorrowedDesigns(top, top, options, result) != 0 && + result.status == RunStatus::Error, + "borrowed LEC accepted SEC-only strict opaque policy"); + check(!Config::getErrorOnOpaque() && SEC::LATCH::supportOptions().enabled, + "error path leaked borrowed policy state"); + universe->destroy(); +} +} // namespace + +int main() { + const auto directory = std::filesystem::temp_directory_path() / + ("kepler_borrowed_policy_" + std::to_string( + std::chrono::steady_clock::now().time_since_epoch().count())); + std::filesystem::create_directories(directory); + try { + runTests(directory); + std::filesystem::remove_all(directory); + std::cout << "Borrowed policy isolation tests passed\n"; + return 0; + } catch (const std::exception& error) { + if (auto* universe = naja::NL::NLUniverse::get()) universe->destroy(); + std::filesystem::remove_all(directory); + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/test/python/test_latch_api.py b/test/python/test_latch_api.py new file mode 100644 index 00000000..7a706cb8 --- /dev/null +++ b/test/python/test_latch_api.py @@ -0,0 +1,168 @@ +# Copyright 2026 keplertech.io +# SPDX-License-Identifier: Apache-2.0 + +"""End-to-end borrowed latch verification using explicitly modeled Naja cells.""" + +import tempfile +import unittest +from pathlib import Path + +from najaeda import naja, netlist +from kepler_formal import VerificationOptions, VerificationStatus, from_najaeda, verify_designs + + +class BorrowedLatchApiTest(unittest.TestCase): + def setUp(self): + netlist.reset() + self.temporary = tempfile.TemporaryDirectory(prefix="kepler_latch_api_") + self.universe = naja.NLUniverse.create() + self.database = naja.NLDB.create(self.universe) + self.designs = naja.NLLibrary.create(self.database, "designs") + self.primitives = naja.NLLibrary.createPrimitives(self.database, "primitives") + self.model = self.make_model("declared_latch") + self.first = self.make_top("first", self.model) + self.second = self.make_top("second", self.model) + self.universe.setTopDesign(self.first) + + def tearDown(self): + netlist.reset() + self.temporary.cleanup() + + def make_model(self, name, invert=False): + model = naja.SNLDesign.createPrimitive(self.primitives, name) + for pin in ("D", "E"): + naja.SNLScalarTerm.create(model, naja.SNLTerm.Direction.Input, pin) + output = naja.SNLScalarTerm.create(model, naja.SNLTerm.Direction.Output, "Q") + model.setSequentialModel(clocked_on="E", kind="latch", + states=[{"name": "H", "next_state": "D"}], + outputs=[(output, "!H" if invert else "H")]) + return model + + def make_top(self, name, model): + top = naja.SNLDesign.create(self.designs, name) + cell = naja.SNLInstance.create(top, model, "latch") + for pin in model.getScalarTerms(): + net = naja.SNLScalarNet.create(top, pin.getName()) + naja.SNLScalarTerm.create(top, pin.getDirection(), pin.getName()).setNet(net) + cell.getInstTerm(pin).setNet(net) + return top + + def options(self, with_contract=True, **changes): + values = dict(mode="sec", sec_engine="k_induction", sec_encoding="binary", + log_file=Path(self.temporary.name) / "verification.log") + if with_contract: + values.update(latch_input_changes="single", + latch_initial_inputs=0, latch_initial_storage=0) + return VerificationOptions(**(values | changes)) + + def unchanged(self): + self.assertIs(naja.NLUniverse.get(), self.universe) + self.assertIs(self.universe.getTopDesign(), self.first) + self.assertEqual("first", self.first.getName()) + self.assertEqual("second", self.second.getName()) + self.assertEqual(1, len(list(self.first.getInstances()))) + self.assertEqual(1, len(list(self.second.getInstances()))) + self.assertTrue(self.model.hasSequentialModel()) + + def test_default_any_race_then_single_then_default_do_not_leak(self): + default = verify_designs(self.first, self.second, options=self.options(False)) + self.assertEqual(VerificationStatus.UNSUPPORTED, default.status) + self.assertEqual(0, default.covered_outputs) + self.unchanged() + enabled = verify_designs(self.first, self.second, options=self.options()) + self.assertEqual(VerificationStatus.EQUIVALENT, enabled.status) + self.assertEqual(1, enabled.covered_outputs) + self.assertEqual(1, enabled.total_outputs) + self.unchanged() + repeated = verify_designs(self.first, self.second, options=self.options(False)) + self.assertEqual(VerificationStatus.UNSUPPORTED, repeated.status) + self.assertEqual(0, repeated.covered_outputs) + self.unchanged() + + def test_explicit_false_keeps_latches_opaque(self): + result = verify_designs(self.first, self.second, + options=self.options(False, latch_support=False)) + self.assertEqual(VerificationStatus.UNSUPPORTED, result.status) + self.assertEqual(0, result.covered_outputs) + self.unchanged() + + def test_handles_and_raw_designs_share_the_event_contract(self): + for left, right in ((from_najaeda(self.first), from_najaeda(self.second)), + (self.first, self.second), (self.second, self.first)): + result = verify_designs(left, right, options=self.options()) + self.assertEqual(VerificationStatus.EQUIVALENT, result.status) + self.assertEqual(1, result.covered_outputs) + self.unchanged() + + def test_inverted_latch_is_different_on_either_side(self): + different = self.make_top("different", self.make_model("inverted", invert=True)) + for left, right in ((self.first, different), (different, self.first)): + result = verify_designs(left, right, options=self.options()) + self.assertEqual(VerificationStatus.DIFFERENT, result.status) + self.unchanged() + + def test_any_change_race_remains_opaque(self): + result = verify_designs(self.first, self.second, options=self.options(latch_input_changes="any")) + self.assertEqual(0, result.covered_outputs) + self.unchanged() + strict = verify_designs(self.first, self.second, + options=self.options(latch_input_changes="any", error_on_opaque=True)) + self.assertEqual(VerificationStatus.UNSUPPORTED, strict.status) + self.unchanged() + + def test_explicit_one_initialization_is_supported(self): + result = verify_designs(self.first, self.second, + options=self.options(latch_initial_inputs=1, latch_initial_storage=1)) + self.assertEqual(VerificationStatus.EQUIVALENT, result.status) + self.assertEqual(1, result.covered_outputs) + self.unchanged() + + def test_unspecified_storage_is_independent_and_not_an_implicit_reset(self): + for engine in ("k_induction", "imc", "pdr"): + for encoding in ("binary", "dual_rail_steady"): + with self.subTest(engine=engine, encoding=encoding): + result = verify_designs(self.first, self.second, options=self.options( + latch_initial_storage=None, sec_engine=engine, sec_encoding=encoding)) + self.assertEqual(VerificationStatus.DIFFERENT, result.status) + self.assertEqual(1, result.covered_outputs) + self.unchanged() + + def test_unspecified_initial_inputs_are_shared_across_designs(self): + result = verify_designs(self.first, self.second, + options=self.options(latch_initial_inputs=None)) + self.assertEqual(VerificationStatus.EQUIVALENT, result.status) + self.assertEqual(1, result.covered_outputs) + self.unchanged() + + def test_both_initial_restrictions_can_be_omitted(self): + result = verify_designs(self.first, self.second, options=self.options( + latch_initial_inputs=None, latch_initial_storage=None)) + self.assertEqual(VerificationStatus.DIFFERENT, result.status) + self.assertEqual(1, result.covered_outputs) + self.unchanged() + + def test_invalid_contract_preserves_live_designs(self): + for changes in (dict(latch_initial_storage=2), dict(latch_support=False), + dict(mode="lec", sec_engine=None, sec_encoding=None), + dict(set_as_boundary=(("latch", "latch"),))): + with self.subTest(changes=changes), self.assertRaises(ValueError): + verify_designs(self.first, self.second, options=self.options(**changes)) + self.unchanged() + + def test_strict_opaque_policy_checks_unused_cells_on_either_side(self): + unknown = naja.SNLDesign.createPrimitive(self.primitives, "unknown") + output = naja.SNLScalarTerm.create(unknown, naja.SNLTerm.Direction.Output, "Y") + cell = naja.SNLInstance.create(self.second, unknown, "unused_opaque") + cell.getInstTerm(output).setNet(naja.SNLScalarNet.create(self.second, "unused_net")) + for left, right in ((self.first, self.second), (self.second, self.first)): + result = verify_designs(left, right, options=self.options(error_on_opaque=True)) + self.assertEqual(VerificationStatus.UNSUPPORTED, result.status) + self.assertIn("unused_opaque", result.reason) + relaxed = verify_designs(self.first, self.second, options=self.options()) + self.assertEqual(VerificationStatus.EQUIVALENT, relaxed.status) + self.assertEqual(1, relaxed.covered_outputs) + self.assertEqual(2, len(list(self.second.getInstances()))) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/python/test_latch_native.py b/test/python/test_latch_native.py new file mode 100644 index 00000000..ab5d2aaf --- /dev/null +++ b/test/python/test_latch_native.py @@ -0,0 +1,85 @@ +# Copyright 2026 keplertech.io +# SPDX-License-Identifier: Apache-2.0 + +"""Exercise native option validation directly, bypassing the Python wrapper.""" + +import unittest + +from kepler_formal import _native + + +class NativeLatchOptionsTest(unittest.TestCase): + def contract(self, **changes): + return dict(mode="sec", latch_input_changes="single", + latch_initial_inputs=0, latch_initial_storage=0) | changes + + def rejected(self, options, error, message): + with self.assertRaisesRegex(error, message): + _native.verify_designs(None, None, options) + + def test_valid_contract_reaches_normal_design_validation(self): + for changes in ("single", "any"): + self.rejected(self.contract(latch_input_changes=changes), TypeError, "design1 must be a NativeDesign") + + def test_no_event_settings_preserve_legacy_with_default_or_explicit_gate(self): + for options in ({}, {"latch_support": False}, {"latch_support": True}, {"mode": "sec"}): + self.rejected(options, TypeError, "design1 must be a NativeDesign") + + def test_native_optional_settings_cannot_override_explicit_false(self): + for key, value in (("latch_input_changes", "any"), ("latch_initial_inputs", 0), + ("latch_initial_storage", 0), ("latch_workers", 0), + ("latch_max_waves", 1), ("latch_max_states", 1), + ("latch_max_transactions", 1)): + with self.subTest(key=key): + self.rejected({"mode": "sec", key: value}, TypeError, "design1 must be a NativeDesign") + self.rejected({"mode": "sec", "latch_support": False, key: value}, + ValueError, "requires latch_support") + + def test_native_semantic_fields_can_be_omitted_independently(self): + for key in ("latch_input_changes", "latch_initial_inputs", "latch_initial_storage"): + for remove in (False, True): + options = self.contract(**{key: None}) + if remove: + options.pop(key) + with self.subTest(key=key, remove=remove): + self.rejected(options, TypeError, "design1 must be a NativeDesign") + + def test_native_requires_real_boolean_gate(self): + for value in (None, 0, 1, "true", []): + with self.subTest(value=value): + self.rejected(self.contract(latch_support=value), TypeError, "latch_support must be a bool") + + def test_native_rejects_nonbinary_initialization(self): + for key in ("latch_initial_inputs", "latch_initial_storage"): + for value, error in ((-1, OverflowError), (2, ValueError), (True, TypeError), + (False, TypeError), ("0", TypeError), (0.0, TypeError)): + with self.subTest(key=key, value=value): + self.rejected(self.contract(**{key: value}), error, ".+") + + def test_native_rejects_unknown_and_embedded_nul_keys(self): + for key in ("latch_unknown", "latch_support\0suffix", "latch_input_changes\0"): + with self.subTest(key=key): + self.rejected(self.contract(**{key: True}), TypeError, "unknown.*option") + + def test_native_change_modes_are_exact(self): + for value in ("", "ANY", "single\0suffix"): + with self.subTest(value=value): + self.rejected(self.contract(latch_input_changes=value), ValueError, "any or single") + + def test_native_rejects_lec_and_leaf_boundary_semantics(self): + self.rejected(self.contract(mode="lec"), ValueError, "only supported for SEC") + self.rejected(self.contract(set_as_boundary=(("a", "b"),)), ValueError, "complete top interface") + + def test_native_resource_limits_are_checked(self): + for key in ("latch_max_waves", "latch_max_states", "latch_max_transactions", + "latch_max_symbolic_nodes", "latch_max_sat_conflicts", "latch_max_sat_decisions"): + for value, error in ((0, ValueError), (-1, OverflowError), (2**128, OverflowError), (True, TypeError)): + with self.subTest(key=key, value=value): + self.rejected(self.contract(**{key: value}), error, ".+") + self.rejected(self.contract(latch_workers=2**31), ValueError, "nonnegative int") + for key in ("latch_max_sat_conflicts", "latch_max_sat_decisions"): + self.rejected(self.contract(**{key: 2**32}), ValueError, "unsigned int") + + +if __name__ == "__main__": + unittest.main() diff --git a/test/python/test_latch_options.py b/test/python/test_latch_options.py new file mode 100644 index 00000000..ed941c17 --- /dev/null +++ b/test/python/test_latch_options.py @@ -0,0 +1,160 @@ +# Copyright 2026 keplertech.io +# SPDX-License-Identifier: Apache-2.0 + +import ctypes +import dataclasses +import unittest + +from kepler_formal import VerificationOptions +from kepler_formal.api import _build_native_design_options + + +class LatchOptionsTest(unittest.TestCase): + def contract(self, **changes): + values = dict(mode="sec", latch_input_changes="single", + latch_initial_inputs=0, latch_initial_storage=0) + return VerificationOptions(**(values | changes)) + + def test_default_is_enabled_without_an_invented_contract(self): + for options in (None, VerificationOptions(), VerificationOptions(mode="sec"), + VerificationOptions(latch_support=True)): + result = _build_native_design_options(options) + self.assertIs(result["latch_support"], True) + for key in ("latch_input_changes", "latch_initial_inputs", "latch_initial_storage"): + self.assertIsNone(result[key]) + + def test_explicit_false_preserves_legacy(self): + self.assertFalse(_build_native_design_options(VerificationOptions(latch_support=False))["latch_support"]) + + def test_any_and_single_and_all_boolean_initial_values(self): + for mode in ("any", "single"): + for inputs in (0, 1): + for storage in (0, 1): + with self.subTest(mode=mode, inputs=inputs, storage=storage): + result = _build_native_design_options(self.contract( + latch_input_changes=mode, latch_initial_inputs=inputs, + latch_initial_storage=storage)) + self.assertTrue(result["latch_support"]) + self.assertEqual(mode, result["latch_input_changes"]) + self.assertEqual(inputs, result["latch_initial_inputs"]) + self.assertEqual(storage, result["latch_initial_storage"]) + + def test_optional_settings_do_not_invent_initialization_or_override_explicit_false(self): + for key, value in (("latch_input_changes", "any"), ("latch_initial_inputs", 0), + ("latch_initial_storage", 1), ("latch_workers", 0), + ("latch_max_waves", 1), ("latch_max_states", 1), + ("latch_max_transactions", 1)): + with self.subTest(key=key): + result = _build_native_design_options(VerificationOptions(mode="sec", **{key: value})) + self.assertEqual(value, result[key]) + for initial in ("latch_initial_inputs", "latch_initial_storage"): + if initial != key: + self.assertIsNone(result[initial]) + with self.assertRaisesRegex(ValueError, "requires latch_support"): + _build_native_design_options(VerificationOptions( + mode="sec", latch_support=False, **{key: value})) + + def test_each_semantic_field_can_be_omitted_independently(self): + for key in ("latch_input_changes", "latch_initial_inputs", "latch_initial_storage"): + with self.subTest(key=key): + result = _build_native_design_options(self.contract(**{key: None})) + self.assertIsNone(result[key]) + + def test_enabled_lec_is_rejected(self): + with self.assertRaisesRegex(ValueError, "only supported for SEC"): + _build_native_design_options(self.contract(mode="lec")) + + def test_boundaries_rejected_only_with_event_contract(self): + boundaries = (("left/cell", "right/cell"),) + self.assertEqual(list(boundaries), _build_native_design_options( + VerificationOptions(mode="sec", set_as_boundary=boundaries))["set_as_boundary"]) + with self.assertRaisesRegex(ValueError, "complete top interface"): + _build_native_design_options(self.contract(set_as_boundary=boundaries)) + + def test_master_gate_requires_bool(self): + for value in (None, 0, 1, "true", [], {}): + with self.subTest(value=value), self.assertRaises(TypeError): + _build_native_design_options(self.contract(latch_support=value)) + + def test_input_change_names_are_exact(self): + for value in ("", "ANY", "Single", "any\0", "any ", "unknown"): + with self.subTest(value=value), self.assertRaises(ValueError): + _build_native_design_options(self.contract(latch_input_changes=value)) + for value in (0, True, [], {}): + with self.subTest(value=value), self.assertRaises(TypeError): + _build_native_design_options(self.contract(latch_input_changes=value)) + + def test_initialization_rejects_nonbinary(self): + for key in ("latch_initial_inputs", "latch_initial_storage"): + for value in (-1, 2, 256): + with self.subTest(key=key, value=value), self.assertRaises(ValueError): + _build_native_design_options(self.contract(**{key: value})) + + def test_initialization_rejects_bool_float_and_string(self): + for key in ("latch_initial_inputs", "latch_initial_storage"): + for value in (True, False, "0", 0.0, []): + with self.subTest(key=key, value=value), self.assertRaises(TypeError): + _build_native_design_options(self.contract(**{key: value})) + + def test_resource_values_forwarded_and_zero_workers_is_auto(self): + values = dict(latch_workers=0, latch_max_waves=12, latch_max_states=34, latch_max_transactions=56) + result = _build_native_design_options(self.contract(**values)) + for key, value in values.items(): + self.assertEqual(value, result[key]) + + def test_resource_limits_positive_workers_nonnegative(self): + for key in ("latch_max_waves", "latch_max_states", "latch_max_transactions"): + for value in (0, -1): + with self.subTest(key=key, value=value), self.assertRaises(ValueError): + _build_native_design_options(self.contract(**{key: value})) + with self.assertRaises(ValueError): + _build_native_design_options(self.contract(latch_workers=-1)) + + def test_resource_limits_reject_wrong_types(self): + for key in ("latch_workers", "latch_max_waves", "latch_max_states", "latch_max_transactions"): + for value in (True, False, "1", 1.0, []): + with self.subTest(key=key, value=value), self.assertRaises(TypeError): + _build_native_design_options(self.contract(**{key: value})) + + def test_integer_overflow_rejected(self): + for key in ("latch_max_waves", "latch_max_states", "latch_max_transactions"): + with self.subTest(key=key), self.assertRaises(ValueError): + _build_native_design_options(self.contract(**{key: 1 << (8 * ctypes.sizeof(ctypes.c_size_t))})) + with self.assertRaises(ValueError): + _build_native_design_options(self.contract(latch_workers=1 << (8 * ctypes.sizeof(ctypes.c_int) - 1))) + + def test_symbolic_budgets_are_explicit_bounded_tuning(self): + for key in ("latch_max_symbolic_nodes", "latch_max_sat_conflicts", "latch_max_sat_decisions"): + with self.subTest(key=key): + self.assertEqual(123, _build_native_design_options(self.contract(**{key: 123}))[key]) + result = _build_native_design_options(VerificationOptions(mode="sec", **{key: 123})) + self.assertEqual(123, result[key]) + self.assertIsNone(result["latch_initial_inputs"]) + self.assertIsNone(result["latch_initial_storage"]) + with self.assertRaisesRegex(ValueError, "requires latch_support"): + _build_native_design_options(VerificationOptions( + mode="sec", latch_support=False, **{key: 123})) + for value, error in ((0, ValueError), (-1, ValueError), (True, TypeError), + (1.0, TypeError), ("1", TypeError), (2**128, ValueError)): + with self.subTest(value=value), self.assertRaises(error): + _build_native_design_options(self.contract(**{key: value})) + for key in ("latch_max_sat_conflicts", "latch_max_sat_decisions"): + with self.subTest(key=key), self.assertRaisesRegex(ValueError, "unsigned int"): + _build_native_design_options(self.contract(**{key: 1 << (8 * ctypes.sizeof(ctypes.c_uint))})) + + def test_opaque_policy_is_independent(self): + self.assertTrue(_build_native_design_options( + VerificationOptions(mode="sec", error_on_opaque=True))["error_on_opaque"]) + for strict in (False, True): + result = _build_native_design_options(self.contract(error_on_opaque=strict)) + self.assertEqual(strict, result["error_on_opaque"]) + self.assertTrue(result["latch_support"]) + + def test_options_remain_immutable(self): + options = self.contract() + with self.assertRaises(dataclasses.FrozenInstanceError): + options.latch_support = False + + +if __name__ == "__main__": + unittest.main() diff --git a/test/python/test_opaque_policy.py b/test/python/test_opaque_policy.py new file mode 100644 index 00000000..1612ce0c --- /dev/null +++ b/test/python/test_opaque_policy.py @@ -0,0 +1,30 @@ +# Copyright 2024-2026 keplertech.io +# SPDX-License-Identifier: Apache-2.0 + +import unittest + +from kepler_formal import VerificationOptions +from kepler_formal.api import _build_native_design_options + + +class OpaquePolicyOptionsTest(unittest.TestCase): + def test_default_is_disabled(self): + self.assertFalse(_build_native_design_options(None)["error_on_opaque"]) + + def test_sec_accepts_explicit_enablement(self): + settings = VerificationOptions(mode="sec", error_on_opaque=True) + self.assertTrue(_build_native_design_options(settings)["error_on_opaque"]) + + def test_lec_rejects_enablement(self): + with self.assertRaisesRegex(ValueError, "only supported for SEC"): + _build_native_design_options(VerificationOptions(error_on_opaque=True)) + + def test_non_boolean_is_rejected(self): + for value in (None, 1, "true", []): + with self.subTest(value=value), self.assertRaises(TypeError): + _build_native_design_options( + VerificationOptions(mode="sec", error_on_opaque=value)) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/sec/BUILD.bazel b/test/sec/BUILD.bazel index 26983f4c..8d830fd1 100644 --- a/test/sec/BUILD.bazel +++ b/test/sec/BUILD.bazel @@ -28,6 +28,22 @@ cc_test( cc_test( name = "SequentialEquivalenceStrategyTests", srcs = [ + "RelationalInitializationTests.cpp", + "PdrTernaryMemoTests.cpp", + "LatchBoundaryEncodingTests.cpp", + "LatchConstantNetTests.cpp", + "LatchEventModelTests.cpp", + "LatchDependencyGraphTests.cpp", + "LatchNetlistAdapterTests.cpp", + "LatchSettlingCompilerTests.cpp", + "LatchSymbolicCompilerTests.cpp", + "LatchSymbolicIntegrationTests.cpp", + "LatchSymbolicModelTests.cpp", + "LatchEventExportTests.cpp", + "LatchResetClockTests.cpp", + "LatchResetAdapterTests.cpp", + "LatchResetIntegrationTests.cpp", + "OpaquePolicyTests.cpp", "SequentialEquivalenceStrategyTests.cpp", "InternalRelationsTests.cpp", "Btor2ExportStrategyTests.cpp", diff --git a/test/sec/CMakeLists.txt b/test/sec/CMakeLists.txt index 861264ab..f783de12 100644 --- a/test/sec/CMakeLists.txt +++ b/test/sec/CMakeLists.txt @@ -2,6 +2,22 @@ # SPDX-License-Identifier: Apache-2.0 add_executable(secStrategyTests + RelationalInitializationTests.cpp + PdrTernaryMemoTests.cpp + OpaquePolicyTests.cpp + LatchEventModelTests.cpp + LatchConstantNetTests.cpp + LatchDependencyGraphTests.cpp + LatchSettlingCompilerTests.cpp + LatchSymbolicModelTests.cpp + LatchSymbolicCompilerTests.cpp + LatchSymbolicIntegrationTests.cpp + LatchEventExportTests.cpp + LatchResetClockTests.cpp + LatchResetAdapterTests.cpp + LatchResetIntegrationTests.cpp + LatchBoundaryEncodingTests.cpp + LatchNetlistAdapterTests.cpp InternalRelationsTests.cpp SequentialEquivalenceStrategyTests.cpp Btor2ExportStrategyTests.cpp) diff --git a/test/sec/LatchBoundaryEncodingTests.cpp b/test/sec/LatchBoundaryEncodingTests.cpp new file mode 100644 index 00000000..b60c84b0 --- /dev/null +++ b/test/sec/LatchBoundaryEncodingTests.cpp @@ -0,0 +1,224 @@ +// Copyright 2026 keplertech.io +// SPDX-License-Identifier: Apache-2.0 + +#include + +#include +#include + +#include "BoolExprCache.h" +#include "latch/LatchBoundaryEncoding.h" + +namespace KEPLER_FORMAL::SEC::LATCH { +namespace { + +class LatchBoundaryEncodingTests : public ::testing::Test { + protected: + void TearDown() override { BoolExprCache::destroy(); } + + TransitionTable compile(const Network& network, bool singleChange) { + CompileOptions options; + options.initialInputs.assign(network.externalInputs.size(), 0); + options.singleExternalInputChange = singleChange; + for (const auto& primitive : network.primitives) + options.initialStorage.emplace_back(primitive.storageBits, uint8_t{0}); + auto result = compileTransitionTable(EventModel(network), options); + EXPECT_TRUE(result.certified()) << result.detail; + return result.table.value(); + } + + std::vector variables(size_t count, size_t base) { + std::vector result; + for (size_t i = 0; i < count; ++i) result.push_back(BoolExpr::Var(base + i)); + return result; + } + + void assign(std::unordered_map& environment, + const std::vector& variables, size_t value) { + for (size_t i = 0; i < variables.size(); ++i) + environment[variables[i]->getId()] = (value >> i) & 1; + } + + size_t evaluateId(const BoundaryEncoding& encoding, + const std::unordered_map& environment) { + size_t result = 0; + for (size_t i = 0; i < encoding.nextState.size(); ++i) + result |= size_t{encoding.nextState[i]->evaluate(environment)} << i; + return result; + } + + void expectRow(const BoundaryEncoding& encoding, const TransitionTable& table, + size_t target, const std::unordered_map& environment) { + EXPECT_EQ(evaluateId(encoding, environment), target); + ASSERT_EQ(encoding.observedNets.size(), table.boundaries[target].current.size()); + for (size_t net = 0; net < encoding.observedNets.size(); ++net) + EXPECT_EQ(encoding.observedNets[net]->evaluate(environment), + table.boundaries[target].current[net] != 0) << "net=" << net; + } +}; + +TEST_F(LatchBoundaryEncodingTests, StateWidthCoversEveryIdentifierIncludingSingleton) { + EXPECT_THROW(boundaryEncodingBits(0), std::invalid_argument); + EXPECT_EQ(boundaryEncodingBits(1), 1u); + EXPECT_EQ(boundaryEncodingBits(2), 1u); + EXPECT_EQ(boundaryEncodingBits(3), 2u); + EXPECT_EQ(boundaryEncodingBits(4), 2u); + EXPECT_EQ(boundaryEncodingBits(5), 3u); + EXPECT_EQ(boundaryEncodingBits(256), 8u); + EXPECT_EQ(boundaryEncodingBits(257), 9u); +} + +TEST_F(LatchBoundaryEncodingTests, EncodesEveryAllChangeTruthTableRowExactly) { + Network network{3, {0, 1}, {combinational("xor", {0, 1}, {2}, [](const Bits& pins) { + return Bits{static_cast(pins[0] ^ pins[1])}; + })}}; + const auto table = compile(network, false); + const auto state = variables(boundaryEncodingBits(table.boundaries.size()), 2); + const auto input = variables(2, 20); + const auto encoding = encodeBoundaryTable(table, network, state, input); + for (const auto& row : table.rows) { + std::unordered_map environment; + assign(environment, state, row.from); + for (size_t i = 0; i < row.input.size(); ++i) environment[input[i]->getId()] = row.input[i]; + expectRow(encoding, table, row.to, environment); + } +} + +TEST_F(LatchBoundaryEncodingTests, SharedSelectorChangesOnlyItsNamedGlobalInput) { + Network network{3, {0, 1}, {latch("l", 0, 1, 2)}}; + const auto table = compile(network, true); + const auto state = variables(boundaryEncodingBits(table.boundaries.size()), 2); + const auto inputs = variables(2, 20); + const auto selector = variables(3, 30); + auto* value = BoolExpr::Var(40); + const std::vector global{2, 5}; + const auto encoding = encodeBoundaryTable(table, network, state, inputs, selector, value, global); + for (size_t from = 0; from < table.boundaries.size(); ++from) { + for (size_t selected = 0; selected < 8; ++selected) { + for (size_t event = 0; event < 2; ++event) { + Bits expectedInput; + for (size_t i = 0; i < global.size(); ++i) + expectedInput.push_back(global[i] == selected ? event : + table.boundaries[from].current[network.externalInputs[i]]); + const auto row = std::find_if(table.rows.begin(), table.rows.end(), [&](const auto& row) { + return row.from == from && row.input == expectedInput; + }); + ASSERT_NE(row, table.rows.end()); + std::unordered_map environment; + assign(environment, state, from); + assign(environment, selector, selected); + environment[value->getId()] = event; + // Original full-valued input variables must not constrain selector mode. + environment[inputs[0]->getId()] = !expectedInput[0]; + environment[inputs[1]->getId()] = !expectedInput[1]; + expectRow(encoding, table, row->to, environment); + } + } + } +} + +TEST_F(LatchBoundaryEncodingTests, SharedSelectorMaintainsIndependentComponents) { + const Network network{2, {0}, {combinational("buffer", {0}, {1}, [](const Bits& pins) { return pins; })}}; + const auto table = compile(network, true); + const auto selector = variables(2, 30); + auto* value = BoolExpr::Var(40); + const auto firstState = variables(boundaryEncodingBits(table.boundaries.size()), 2); + const auto secondState = variables(boundaryEncodingBits(table.boundaries.size()), 10); + const auto first = encodeBoundaryTable(table, network, firstState, {BoolExpr::createFalse()}, selector, value, {0}); + const auto second = encodeBoundaryTable(table, network, secondState, {BoolExpr::createFalse()}, selector, value, {1}); + std::unordered_map environment; + assign(environment, firstState, table.initials.front().boundary); + assign(environment, secondState, table.initials.front().boundary); + assign(environment, selector, 0); + environment[40] = true; + EXPECT_TRUE(first.observedNets[1]->evaluate(environment)); + EXPECT_FALSE(second.observedNets[1]->evaluate(environment)); + assign(environment, selector, 1); + EXPECT_FALSE(first.observedNets[1]->evaluate(environment)); + EXPECT_TRUE(second.observedNets[1]->evaluate(environment)); +} + +TEST_F(LatchBoundaryEncodingTests, StateIdsPreserveHiddenInputHistoryNotOnlyOutput) { + const Network network{3, {0, 1}, {latch("l", 0, 1, 2)}}; + const auto table = compile(network, true); + size_t closedZero = table.boundaries.size(), closedOne = table.boundaries.size(); + for (size_t i = 0; i < table.boundaries.size(); ++i) { + if (table.boundaries[i].current == Bits{0, 0, 0}) closedZero = i; + if (table.boundaries[i].current == Bits{1, 0, 0}) closedOne = i; + } + ASSERT_LT(closedZero, table.boundaries.size()); + ASSERT_LT(closedOne, table.boundaries.size()); + ASSERT_NE(closedZero, closedOne); + const auto state = variables(boundaryEncodingBits(table.boundaries.size()), 2); + const auto selector = variables(2, 20); + const auto encoding = encodeBoundaryTable(table, network, state, + {BoolExpr::createFalse(), BoolExpr::createFalse()}, selector, BoolExpr::Var(30), {0, 1}); + std::unordered_map environment{{30, true}}; + assign(environment, selector, 1); // Open without changing remembered data input. + assign(environment, state, closedZero); + EXPECT_FALSE(encoding.observedNets[2]->evaluate(environment)); + assign(environment, state, closedOne); + EXPECT_TRUE(encoding.observedNets[2]->evaluate(environment)); +} + +TEST_F(LatchBoundaryEncodingTests, UnreachableStateIdsHaveTotalZeroEncoding) { + Network network{3, {0, 1}, {latch("l", 0, 1, 2)}}; + const auto table = compile(network, true); + const auto state = variables(boundaryEncodingBits(table.boundaries.size()), 2); + const auto selector = variables(2, 20); + const auto encoding = encodeBoundaryTable(table, network, state, + {BoolExpr::createFalse(), BoolExpr::createFalse()}, selector, BoolExpr::Var(30), {0, 1}); + ASSERT_LT(table.boundaries.size(), size_t{1} << state.size()); + for (size_t id = table.boundaries.size(); id < (size_t{1} << state.size()); ++id) { + std::unordered_map environment{{30, true}}; + assign(environment, state, id); + assign(environment, selector, 1); + EXPECT_EQ(evaluateId(encoding, environment), 0u); + for (auto* expression : encoding.observedNets) EXPECT_FALSE(expression->evaluate(environment)); + } +} + +TEST_F(LatchBoundaryEncodingTests, RejectsMismatchedInterfaces) { + const Network network{3, {0, 1}, {latch("l", 0, 1, 2)}}; + const auto table = compile(network, true); + const auto state = variables(boundaryEncodingBits(table.boundaries.size()), 2); + const auto inputs = variables(2, 20); + const auto selector = variables(2, 30); + auto* value = BoolExpr::Var(40); + EXPECT_THROW(encodeBoundaryTable(table, network, {}, inputs, selector, value, {0, 1}), std::invalid_argument); + EXPECT_THROW(encodeBoundaryTable(table, network, state, {}, selector, value, {}), std::invalid_argument); + EXPECT_THROW(encodeBoundaryTable(table, network, state, inputs, selector, nullptr, {0, 1}), std::invalid_argument); + EXPECT_THROW(encodeBoundaryTable(table, network, state, inputs, selector, value, {0}), std::invalid_argument); +} + +TEST_F(LatchBoundaryEncodingTests, RejectsAliasingGlobalSelectorIndices) { + const Network network{3, {0, 1}, {latch("l", 0, 1, 2)}}; + const auto table = compile(network, true); + const auto state = variables(boundaryEncodingBits(table.boundaries.size()), 2); + const auto inputs = variables(2, 20); + EXPECT_THROW(encodeBoundaryTable(table, network, state, inputs, variables(2, 30), + BoolExpr::Var(40), {1, 1}), std::invalid_argument); + EXPECT_THROW(encodeBoundaryTable(table, network, state, inputs, variables(1, 30), + BoolExpr::Var(40), {0, 2}), std::invalid_argument); +} + +TEST_F(LatchBoundaryEncodingTests, RejectsMalformedCertifiedRows) { + const Network network{3, {0, 1}, {latch("l", 0, 1, 2)}}; + const auto table = compile(network, true); + const auto state = variables(boundaryEncodingBits(table.boundaries.size()), 2); + const auto inputs = variables(2, 20); + const auto selector = variables(2, 30); + auto bad = table; + bad.rows.front().to = table.boundaries.size(); + EXPECT_THROW(encodeBoundaryTable(bad, network, state, inputs, selector, BoolExpr::Var(40), {0, 1}), std::invalid_argument); + bad = table; + bad.rows.front().input.clear(); + EXPECT_THROW(encodeBoundaryTable(bad, network, state, inputs, selector, BoolExpr::Var(40), {0, 1}), std::invalid_argument); + bad = table; + const auto& source = bad.boundaries[bad.rows.front().from]; + bad.rows.front().input = {static_cast(!source.current[0]), static_cast(!source.current[1])}; + EXPECT_THROW(encodeBoundaryTable(bad, network, state, inputs, selector, BoolExpr::Var(40), {0, 1}), std::invalid_argument); +} + +} // namespace +} // namespace KEPLER_FORMAL::SEC::LATCH diff --git a/test/sec/LatchConstantNetTests.cpp b/test/sec/LatchConstantNetTests.cpp new file mode 100644 index 00000000..e6d250a8 --- /dev/null +++ b/test/sec/LatchConstantNetTests.cpp @@ -0,0 +1,295 @@ +// Copyright 2026 keplertech.io +// SPDX-License-Identifier: Apache-2.0 + +#include + +#include +#include +#include + +#include "BoolExprCache.h" +#include "DNL.h" +#include "NLDB.h" +#include "NLDB0.h" +#include "NLLibrary.h" +#include "NLName.h" +#include "NLUniverse.h" +#include "SNLDesign.h" +#include "SNLDesignModeling.h" +#include "SNLInstance.h" +#include "SNLScalarNet.h" +#include "SNLScalarTerm.h" +#include "latch/LatchConstantNet.h" +#include "latch/LatchSupportOptions.h" +#include "model/SequentialDesignModel.h" + +namespace KEPLER_FORMAL::SEC::LATCH { +namespace { +using namespace naja::NL; + +class LatchConstantNetTests : public ::testing::Test { + protected: + void SetUp() override { + NLUniverse::create(); + auto* db = NLDB::create(NLUniverse::get()); + designs_ = NLLibrary::create(db, NLLibrary::Type::Standard, NLName("designs")); + } + void TearDown() override { + naja::DNL::destroy(); + if (auto* universe = NLUniverse::get()) universe->destroy(); + BoolExprCache::destroy(); + } + SNLDesign* design(const std::string& name) { + return SNLDesign::create(designs_, SNLDesign::Type::Standard, NLName(name)); + } + SNLScalarNet* output(SNLDesign* top, const std::string& name, SNLNet::Type type) { + auto* term = SNLScalarTerm::create(top, SNLTerm::Direction::Output, NLName(name)); + auto* net = SNLScalarNet::create(top, NLName(name)); + net->setType(type); + term->setNet(net); + return net; + } + SequentialDesignModel extract(SNLDesign* top) { + SupportOptions options; + options.enabled = true; + options.singleInputChange = true; + options.initialInputs = false; + options.initialStorage = false; + options.workers = 2; + ScopedSupportOptions scope(options); + return SequentialDesignModel::extract(top); + } + void expectConstant(const SequentialDesignModel& model, bool expected) { + EXPECT_FALSE(model.hasUnsupportedFeatures()); + ASSERT_EQ(model.allObservedOutputs.size(), 1u); + ASSERT_EQ(model.observedOutputs.size(), 1u); + EXPECT_TRUE(model.skippedObservedOutputs.empty()); + std::unordered_map environment; + for (const auto& key : model.stateBits) + environment[model.inputVarByKey.at(key)] = model.initialStateValueByKey.at(key); + for (const auto& key : model.environmentInputs) + environment[model.inputVarByKey.at(key)] = false; + EXPECT_EQ(model.observedOutputExprByKey.at(model.observedOutputs.front())->evaluate(environment), expected); + } + void expectOpaque(const SequentialDesignModel& model) { + EXPECT_TRUE(model.observedOutputs.empty()); + ASSERT_EQ(model.allObservedOutputs.size(), 1u); + ASSERT_EQ(model.skippedObservedOutputs.size(), 1u); + const auto& reason = model.connectivitySkipInfoByKey.at(model.skippedObservedOutputs.front()).detail; + EXPECT_FALSE(reason.empty()); + } + SNLInstance* constantLatch(SNLDesign* top, SNLScalarNet* source, SNLScalarNet* target) { + auto* primitive = NLDB0::getDLatch(); + auto* cell = SNLInstance::create(top, primitive, NLName("latch")); + cell->getInstTerm(primitive->getScalarTerm(NLName("D")))->setNet(source); + cell->getInstTerm(primitive->getScalarTerm(NLName("E")))->setNet(source); + cell->getInstTerm(primitive->getScalarTerm(NLName("Q")))->setNet(target); + return cell; + } + SNLDesign* constantChild(const std::string& name, SNLNet::Type type) { + auto* child = design(name); + auto* constant = output(child, "out", type); + auto* held = SNLScalarNet::create(child, NLName("held")); + // A real nested cell makes this a hierarchical design, while no primitive + // drives the annotated output. The annotation also feeds its D/E pins. + constantLatch(child, constant, held); + return child; + } + SNLDesign* wrapper(const std::string& name, SNLDesign* child, SNLNet::Type type) { + auto* top = design(name); + auto* wire = output(top, "out", type); + auto* cell = SNLInstance::create(top, child, NLName("nested")); + cell->getInstTerm(child->getScalarTerm(NLName("out")))->setNet(wire); + return top; + } + NLLibrary* designs_ = nullptr; +}; + +TEST_F(LatchConstantNetTests, DriverlessTopZeroAndOneAreObservedConstants) { + for (bool value : {false, true}) { + auto* top = design(value ? "one" : "zero"); + output(top, "out", value ? SNLNet::Type::Assign1 : SNLNet::Type::Assign0); + expectConstant(extract(top), value); + } +} + +TEST_F(LatchConstantNetTests, DisconnectedOutputIsNotInventedAsZero) { + auto* top = design("disconnected"); + SNLScalarTerm::create(top, SNLTerm::Direction::Output, NLName("out")); + expectOpaque(extract(top)); +} + +TEST_F(LatchConstantNetTests, ConnectedButFloatingWireIsNotInventedAsZero) { + auto* top = design("floating"); + output(top, "out", SNLNet::Type::Standard); + expectOpaque(extract(top)); +} + +TEST_F(LatchConstantNetTests, DriverlessUnknownAndHighImpedanceRemainOpaque) { + for (const auto type : {SNLNet::Type::AssignX, SNLNet::Type::AssignZ}) { + auto* top = design(type == SNLNet::Type::AssignX ? "unknown" : "high_impedance"); + output(top, "out", type); + expectOpaque(extract(top)); + } +} + +TEST_F(LatchConstantNetTests, MatchingHierarchicalConstantsRemainKnown) { + for (bool value : {false, true}) { + const auto type = value ? SNLNet::Type::Assign1 : SNLNet::Type::Assign0; + const std::string suffix = value ? "one" : "zero"; + auto* child = constantChild("child_" + suffix, type); + auto* middle = wrapper("middle_" + suffix, child, type); + auto* top = wrapper("top_" + suffix, middle, type); + expectConstant(extract(top), value); + } +} + +TEST_F(LatchConstantNetTests, HierarchicalConstantFlowsThroughUnannotatedParentNets) { + auto* child = constantChild("child", SNLNet::Type::Assign1); + auto* middle = wrapper("middle", child, SNLNet::Type::Standard); + auto* top = wrapper("top", middle, SNLNet::Type::Standard); + expectConstant(extract(top), true); +} + +TEST_F(LatchConstantNetTests, ConflictingDriverlessHierarchicalAnnotationsRemainOpaque) { + for (bool parentValue : {false, true}) { + const std::string suffix = parentValue ? "one" : "zero"; + auto* child = constantChild("child_" + suffix, + parentValue ? SNLNet::Type::Assign0 : SNLNet::Type::Assign1); + auto* middle = wrapper("middle_" + suffix, child, SNLNet::Type::Standard); + auto* top = wrapper("top_" + suffix, middle, + parentValue ? SNLNet::Type::Assign1 : SNLNet::Type::Assign0); + expectOpaque(extract(top)); + } +} + +TEST_F(LatchConstantNetTests, HierarchicalUnknownAnnotationCannotBeOverriddenByBooleanParent) { + for (const auto type : {SNLNet::Type::AssignX, SNLNet::Type::AssignZ}) { + const std::string suffix = type == SNLNet::Type::AssignX ? "x" : "z"; + auto* child = constantChild("child_" + suffix, type); + auto* top = wrapper("top_" + suffix, child, SNLNet::Type::Assign0); + expectOpaque(extract(top)); + } +} + +TEST_F(LatchConstantNetTests, DriverlessConstantOnLatchPinsIsResolvedWithoutDriverCell) { + auto* top = design("top"); + auto* one = SNLScalarNet::create(top, NLName("one")); + one->setType(SNLNet::Type::Assign1); + auto* out = output(top, "out", SNLNet::Type::Standard); + constantLatch(top, one, out); + // Explicit storage initialization is zero; transparent D=E=1 must settle to + // one. Neither a dangling-input default nor the stored value is correct. + expectConstant(extract(top), true); +} + +TEST_F(LatchConstantNetTests, SharedHierarchyAndPrimitiveRemainUnchangedAcrossExtraction) { + auto* child = constantChild("shared_child", SNLNet::Type::Assign1); + auto* first = wrapper("first", child, SNLNet::Type::Standard); + auto* second = wrapper("second", child, SNLNet::Type::Assign1); + auto* childNet = child->getScalarNet(NLName("out")); + auto* cell = child->getInstance(NLName("latch")); + auto* primitive = cell->getModel(); + auto* data = primitive->getScalarTerm(NLName("D")); + auto* enable = primitive->getScalarTerm(NLName("E")); + auto* outputTerm = child->getScalarTerm(NLName("out")); + NLUniverse::get()->setTopDesign(first); + auto* originalDnl = naja::DNL::get(); + const auto dataOrder = data->getOrderID(); + const auto cellOrder = cell->getOrderID(); + const auto outputOrder = outputTerm->getOrderID(); + const auto childNetCount = child->getNets().size(); + const auto primitiveTermCount = primitive->getBitTerms().size(); + + expectConstant(extract(second), true); + expectConstant(extract(first), true); + expectConstant(extract(second), true); + + EXPECT_EQ(naja::DNL::get(), originalDnl); + EXPECT_EQ(NLUniverse::get()->getTopDesign(), first); + EXPECT_EQ(cell->getModel(), primitive); + EXPECT_EQ(child->getInstances().size(), 1u); + EXPECT_EQ(child->getNets().size(), childNetCount); + EXPECT_EQ(primitive->getBitTerms().size(), primitiveTermCount); + EXPECT_EQ(childNet->getType(), SNLNet::Type::Assign1); + EXPECT_EQ(outputTerm->getNet(), childNet); + EXPECT_EQ(cell->getInstTerm(data)->getNet(), childNet); + EXPECT_EQ(cell->getInstTerm(enable)->getNet(), childNet); + EXPECT_EQ(data->getOrderID(), dataOrder); + EXPECT_EQ(cell->getOrderID(), cellOrder); + EXPECT_EQ(outputTerm->getOrderID(), outputOrder); + EXPECT_EQ(SNLDesignModeling::getSequentialModel(primitive).kind, + SNLDesignModeling::SequentialModel::Kind::Latch); +} + +TEST_F(LatchConstantNetTests, ResolverTraversesAllHierarchicalAnnotationsWithoutChangingIsoIds) { + auto* top = design("top"); + const std::vector> annotations{ + {SNLNet::Type::Assign0, SNLNet::Type::Assign0}, + {SNLNet::Type::Assign1, SNLNet::Type::Assign1}, + {SNLNet::Type::Assign0, SNLNet::Type::Assign1}, + {SNLNet::Type::Assign1, SNLNet::Type::Assign0}, + {SNLNet::Type::Assign0, SNLNet::Type::AssignX}, + {SNLNet::Type::Assign0, SNLNet::Type::AssignZ}}; + std::vector terms; + for (size_t i = 0; i < annotations.size(); ++i) { + const auto suffix = std::to_string(i); + auto* child = constantChild("child" + suffix, annotations[i].second); + auto* wire = output(top, "out" + suffix, annotations[i].first); + auto* cell = SNLInstance::create(top, child, NLName("child" + suffix)); + cell->getInstTerm(child->getScalarTerm(NLName("out")))->setNet(wire); + terms.push_back(top->getScalarTerm(NLName("out" + suffix))); + } + NLUniverse::get()->setTopDesign(top); + const auto* dnl = naja::DNL::get(); + std::vector originalIds; + for (const auto& term : dnl->getDNLTerms()) originalIds.push_back(term.getIsoID()); + DriverlessConstantResolver resolver(*dnl); + // Call the resolver directly even where DNL already supplies a valid iso: + // these assertions exercise its traversal, not the adapter's normal path. + for (size_t repetition = 0; repetition < 3; ++repetition) { + for (size_t i = 0; i < terms.size(); ++i) { + const auto& terminal = dnl->getTop().getTerminalFromBitTerm(terms[i]); + if (i < 2) EXPECT_NE(terminal.getIsoID(), naja::DNL::DNLID_MAX); + const auto result = resolver.resolve(terminal); + if (i < 2) { + ASSERT_TRUE(result.value.has_value()); + EXPECT_EQ(*result.value, i == 1); + EXPECT_FALSE(result.conflictingOrUnknown); + } else { + EXPECT_FALSE(result.value.has_value()); + EXPECT_TRUE(result.conflictingOrUnknown); + } + } + } + ASSERT_EQ(dnl->getDNLTerms().size(), originalIds.size()); + for (size_t i = 0; i < originalIds.size(); ++i) + EXPECT_EQ(dnl->getDNLTerms()[i].getIsoID(), originalIds[i]); + EXPECT_EQ(naja::DNL::get(), dnl); + EXPECT_EQ(NLUniverse::get()->getTopDesign(), top); +} + +TEST_F(LatchConstantNetTests, ResolverRejectsConstantAnnotatedNetWithActualDriver) { + auto* top = design("top"); + auto* wire = output(top, "out", SNLNet::Type::Assign0); + auto* driver = SNLScalarTerm::create(top, SNLTerm::Direction::Input, NLName("driver")); + driver->setNet(wire); + NLUniverse::get()->setTopDesign(top); + const auto* dnl = naja::DNL::get(); + const auto& terminal = dnl->getTop().getTerminalFromBitTerm(top->getScalarTerm(NLName("out"))); + const auto originalId = terminal.getIsoID(); + ASSERT_NE(originalId, naja::DNL::DNLID_MAX); + ASSERT_FALSE(dnl->getDNLIsoDB().getIsoFromIsoIDconst(originalId).getDrivers().empty()); + DriverlessConstantResolver resolver(*dnl); + for (size_t repetition = 0; repetition < 3; ++repetition) { + const auto result = resolver.resolve(terminal); + EXPECT_FALSE(result.value.has_value()); + EXPECT_FALSE(result.conflictingOrUnknown); + EXPECT_EQ(terminal.getIsoID(), originalId); + } + EXPECT_EQ(wire->getType(), SNLNet::Type::Assign0); + EXPECT_EQ(driver->getNet(), wire); +} + +} // namespace +} // namespace KEPLER_FORMAL::SEC::LATCH diff --git a/test/sec/LatchDependencyGraphTests.cpp b/test/sec/LatchDependencyGraphTests.cpp new file mode 100644 index 00000000..d1a537fe --- /dev/null +++ b/test/sec/LatchDependencyGraphTests.cpp @@ -0,0 +1,177 @@ +// Copyright 2026 keplertech.io +// SPDX-License-Identifier: Apache-2.0 + +#include + +#include +#include "latch/LatchDependencyGraph.h" + +namespace KEPLER_FORMAL::SEC::LATCH { +namespace { + +Primitive cell(std::vector inputs, std::vector outputs) { + Primitive result; + result.inputs = std::move(inputs); + result.outputs = std::move(outputs); + return result; +} + +TEST(LatchDependencyGraphTests, EmptyNetworkHasNoComponents) { + const auto graph = analyzeDependencies({}); + EXPECT_TRUE(graph.components.empty()); + EXPECT_TRUE(graph.feedbackComponents.empty()); + EXPECT_TRUE(graph.componentOf.empty()); +} + +TEST(LatchDependencyGraphTests, AcyclicChainIsOneSchedulingIslandWithoutFeedback) { + const auto graph = analyzeDependencies({4, {0}, + {cell({0}, {1}), cell({1}, {2}), cell({2}, {3})}}); + EXPECT_EQ(graph.components, (std::vector>{{0, 1, 2}})); + EXPECT_TRUE(graph.feedbackComponents.empty()); + EXPECT_EQ(graph.componentOf, (std::vector{0, 0, 0})); +} + +TEST(LatchDependencyGraphTests, InterconnectedLoopsRemainDistinctInsideOneIsland) { + const auto graph = analyzeDependencies({5, {}, {cell({1}, {0}), cell({0}, {1}), + cell({1, 3}, {2}), cell({2}, {3}), cell({3}, {4})}}); + EXPECT_EQ(graph.components, (std::vector>{{0, 1, 2, 3, 4}})); + EXPECT_EQ(graph.feedbackComponents, (std::vector>{{0, 1}, {2, 3}})); +} + +TEST(LatchDependencyGraphTests, SelfFeedbackIsAnExplicitFeedbackComponent) { + const auto graph = analyzeDependencies({2, {}, {cell({0}, {0}), cell({}, {1})}}); + EXPECT_EQ(graph.components, (std::vector>{{0}, {1}})); + EXPECT_EQ(graph.feedbackComponents, (std::vector>{{0}})); +} + +TEST(LatchDependencyGraphTests, EveryInputIncludingControlClosesTheGraph) { + // The second pin may be a clock, enable or async reset. Its role is irrelevant + // to event connectivity; cutting it would lose transient control propagation. + const auto graph = analyzeDependencies({4, {0, 1}, + {cell({3}, {2}), cell({0, 2, 1}, {3})}}); + EXPECT_EQ(graph.components, (std::vector>{{0, 1}})); + EXPECT_EQ(graph.feedbackComponents, (std::vector>{{0, 1}})); +} + +TEST(LatchDependencyGraphTests, SequentialStorageIsNotAnImplicitGraphCut) { + auto first = cell({1}, {0}); + auto second = cell({0}, {1}); + first.storageBits = second.storageBits = 1; + const auto graph = analyzeDependencies({2, {}, {first, second}}); + EXPECT_EQ(graph.feedbackComponents, (std::vector>{{0, 1}})); +} + +TEST(LatchDependencyGraphTests, SharedReadOnlyInputsDoNotJoinIndependentIslands) { + const auto graph = analyzeDependencies({4, {0, 1}, + {cell({0, 1}, {2}), cell({0, 1}, {3})}}); + EXPECT_EQ(graph.components, (std::vector>{{0}, {1}})); + EXPECT_TRUE(graph.feedbackComponents.empty()); + EXPECT_EQ(graph.componentOf, (std::vector{0, 1})); +} + +TEST(LatchDependencyGraphTests, MultipleWritersJoinWithoutInventingDirectedFeedback) { + const auto graph = analyzeDependencies({4, {0, 1}, + {cell({0}, {2}), cell({1}, {2}), cell({2}, {3})}}); + EXPECT_EQ(graph.components, (std::vector>{{0, 1, 2}})); + EXPECT_TRUE(graph.feedbackComponents.empty()); +} + +TEST(LatchDependencyGraphTests, MultipleUnconsumedWritersStillShareAnIsland) { + const auto graph = analyzeDependencies({3, {0, 1}, + {cell({0}, {2}), cell({1}, {2})}}); + EXPECT_EQ(graph.components, (std::vector>{{0, 1}})); + EXPECT_TRUE(graph.feedbackComponents.empty()); +} + +TEST(LatchDependencyGraphTests, DuplicatePinArcsDoNotChangeComponents) { + const auto graph = analyzeDependencies({2, {}, + {cell({1, 1}, {0, 0}), cell({0, 0}, {1, 1})}}); + EXPECT_EQ(graph.components, (std::vector>{{0, 1}})); + EXPECT_EQ(graph.feedbackComponents, (std::vector>{{0, 1}})); +} + +TEST(LatchDependencyGraphTests, IsolatedAndOutputlessCellsAreRetained) { + const auto graph = analyzeDependencies({2, {0}, + {cell({}, {}), cell({0}, {1}), cell({1}, {})}}); + EXPECT_EQ(graph.components, (std::vector>{{0}, {1, 2}})); + EXPECT_EQ(graph.componentOf, (std::vector{0, 1, 1})); +} + +TEST(LatchDependencyGraphTests, ComponentsAreSortedByLowestPrimitiveIndex) { + const auto graph = analyzeDependencies({5, {}, {cell({4}, {0}), cell({3}, {1}), + cell({}, {2}), cell({1}, {3}), cell({0}, {4})}}); + EXPECT_EQ(graph.components, (std::vector>{{0, 4}, {1, 3}, {2}})); + EXPECT_EQ(graph.feedbackComponents, (std::vector>{{0, 4}, {1, 3}})); + EXPECT_EQ(graph.componentOf, (std::vector{0, 1, 2, 1, 0})); +} + +TEST(LatchDependencyGraphTests, InvalidNetIndicesAreRejected) { + EXPECT_THROW(analyzeDependencies(Network{1, {1}, {}}), std::invalid_argument); + EXPECT_THROW(analyzeDependencies(Network{1, {}, {cell({1}, {})}}), std::invalid_argument); + EXPECT_THROW(analyzeDependencies(Network{1, {}, {cell({}, {1})}}), std::invalid_argument); +} + +TEST(LatchDependencyGraphTests, TenThousandCellChainUsesNoRecursiveTraversal) { + constexpr size_t length = 10000; + Network network; + network.netCount = length + 1; + network.externalInputs = {0}; + for (size_t i = 0; i < length; ++i) network.primitives.push_back(cell({i}, {i + 1})); + auto graph = analyzeDependencies(network); + ASSERT_EQ(graph.components.size(), 1); + EXPECT_EQ(graph.components.front().size(), length); + EXPECT_TRUE(graph.feedbackComponents.empty()); + // Close the same long chain to exercise the reverse SCC traversal as well. + network.primitives.front().inputs = {length}; + graph = analyzeDependencies(network); + ASSERT_EQ(graph.feedbackComponents.size(), 1); + EXPECT_EQ(graph.feedbackComponents.front().size(), length); +} + +TEST(LatchDependencyGraphTests, AllThreeNodeDirectedGraphsMatchTransitiveClosure) { + constexpr size_t size = 3; + for (size_t mask = 0; mask < (size_t{1} << (size * size)); ++mask) { + SCOPED_TRACE(mask); + Network network; + network.netCount = size; + bool directed[size][size] = {}, weak[size][size] = {}; + for (size_t target = 0; target < size; ++target) { + auto primitive = cell({}, {target}); + for (size_t source = 0; source < size; ++source) { + if (!(mask & (size_t{1} << (source * size + target)))) continue; + primitive.inputs.push_back(source); + directed[source][target] = true; + weak[source][target] = weak[target][source] = true; + } + network.primitives.push_back(std::move(primitive)); + } + for (size_t k = 0; k < size; ++k) + for (size_t i = 0; i < size; ++i) + for (size_t j = 0; j < size; ++j) { + directed[i][j] |= directed[i][k] && directed[k][j]; + weak[i][j] |= weak[i][k] && weak[k][j]; + } + std::vector> expectedFeedback, expectedIslands; + bool assignedFeedback[size] = {}, assignedIsland[size] = {}; + for (size_t i = 0; i < size; ++i) { + if (!assignedIsland[i]) { + std::vector group; + for (size_t j = i; j < size; ++j) + if (i == j || weak[i][j]) { group.push_back(j); assignedIsland[j] = true; } + expectedIslands.push_back(std::move(group)); + } + if (directed[i][i] && !assignedFeedback[i]) { + std::vector group; + for (size_t j = i; j < size; ++j) + if (directed[i][j] && directed[j][i]) { group.push_back(j); assignedFeedback[j] = true; } + expectedFeedback.push_back(std::move(group)); + } + } + const auto graph = analyzeDependencies(network); + EXPECT_EQ(graph.components, expectedIslands); + EXPECT_EQ(graph.feedbackComponents, expectedFeedback); + } +} + +} // namespace +} // namespace KEPLER_FORMAL::SEC::LATCH diff --git a/test/sec/LatchEventExportTests.cpp b/test/sec/LatchEventExportTests.cpp new file mode 100644 index 00000000..d0844d75 --- /dev/null +++ b/test/sec/LatchEventExportTests.cpp @@ -0,0 +1,401 @@ +// Copyright 2026 keplertech.io +// SPDX-License-Identifier: Apache-2.0 + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "BoolExprCache.h" +#include "DNL.h" +#include "NLDB.h" +#include "NLDB0.h" +#include "NLLibrary.h" +#include "NLName.h" +#include "NLUniverse.h" +#include "SNLDesign.h" +#include "SNLDesignModeling.h" +#include "SNLInstance.h" +#include "SNLScalarNet.h" +#include "SNLScalarTerm.h" +#include "latch/LatchSupportOptions.h" +#include "latch/LatchResetAdapter.h" +#include "model/SequentialDesignModel.h" +#include "strategy/SequentialEquivalenceStrategy.h" + +namespace KEPLER_FORMAL::SEC::LATCH { +namespace { +using namespace naja::NL; + +struct Event { + size_t selector; + bool value; + bool sampleData = false; + bool sampleEnable = false; + size_t order = 0; +}; + +bool eventInput(const std::string& name, Event event) { + if (name.find("$event.value") != std::string::npos) return event.value; + const std::string orderPrefix = "$event.reset.order["; + if (const auto position = name.find(orderPrefix); position != std::string::npos) { + const auto stage = std::stoul(name.substr(position + orderPrefix.size())); + return (event.order >> stage) & 1; // These circuits have exactly two sampled pins. + } + const std::string prefix = "$event.select["; + if (const auto position = name.find(prefix); position != std::string::npos) { + return (event.selector >> std::stoul(name.substr(position + prefix.size()))) & 1; + } + // Original PI sentinels supply desired levels during reset, unused afterward. + if (name.ends_with("data[0]")) return event.sampleData; + if (name.ends_with("enable[0]")) return event.sampleEnable; + if (name.ends_with("clock[0]") || name.ends_with("reset[0]")) return false; + throw std::runtime_error("Unexpected event input: " + name); +} + +// A trace interpreter, independent of BoolExpr and the SEC transition builder. +// It interprets the one-bit BTOR2 operations emitted by Btor2Writer, including +// simultaneous state updates and constraints. No state-space-size limit or +// assumption about exported state numbering is needed for these concrete traces. +class BtorTrace { + public: + explicit BtorTrace(const std::filesystem::path& path) { + std::ifstream file(path); + if (!file) throw std::runtime_error("Cannot read exported BTOR2"); + std::string line; + while (std::getline(file, line)) { + std::istringstream fields(line); + size_t id = 0; + if (!(fields >> id)) continue; + Node node; + fields >> node.operation; + std::string field; + while (fields >> field) node.arguments.push_back(field); + if (!nodes_.emplace(id, std::move(node)).second) + throw std::runtime_error("Duplicate BTOR2 node"); + } + for (const auto& [id, node] : nodes_) { + if (node.operation == "state") state_[id] = false; + else if (node.operation == "init") + initial_.emplace(number(node, 1), number(node, 2)); + else if (node.operation == "next") + next_.emplace(number(node, 1), number(node, 2)); + } + if (initial_.size() != state_.size() || next_.size() != state_.size()) + throw std::runtime_error("Event export must initialize and update every state"); + const auto values = evaluate({0, false}); + for (auto& [id, value] : state_) value = values.at(initial_.at(id)); + } + + bool step(Event event) { + const auto values = evaluate(event); + bool bad = false; + size_t badCount = 0; + for (const auto& [id, node] : nodes_) { + if (node.operation == "constraint" && !values.at(number(node, 0))) + throw std::runtime_error("Export excluded an allowed concrete event trace"); + if (node.operation == "bad") { + bad |= values.at(number(node, 0)); + ++badCount; + } + } + if (badCount != 1) throw std::runtime_error("Expected one SEC bad property"); + for (auto& [id, value] : state_) value = values.at(next_.at(id)); + return bad; + } + + private: + struct Node { + std::string operation; + std::vector arguments; + }; + static size_t number(const Node& node, size_t index) { + return std::stoull(node.arguments.at(index)); + } + std::map evaluate(Event event) const { + std::map values; + for (const auto& [id, node] : nodes_) { + const auto& op = node.operation; + const auto operand = [&](size_t index) { return values.at(number(node, index)); }; + if (op == "sort") { + if (node.arguments != std::vector{"bitvec", "1"}) + throw std::runtime_error("Expected one-bit BTOR2 sort"); + } else if (op == "input") values[id] = eventInput(node.arguments.at(1), event); + else if (op == "state") values[id] = state_.at(id); + else if (op == "const") { + if (node.arguments.at(1) != "0" && node.arguments.at(1) != "1") + throw std::runtime_error("Expected Boolean BTOR2 constant"); + values[id] = node.arguments.at(1) == "1"; + } else if (op == "not") values[id] = !operand(1); + else if (op == "and") values[id] = operand(1) && operand(2); + else if (op == "or") values[id] = operand(1) || operand(2); + else if (op == "xor") values[id] = operand(1) != operand(2); + else if (op == "eq") values[id] = operand(1) == operand(2); + else if (op != "init" && op != "next" && op != "constraint" && + op != "bad" && op != "output") + throw std::runtime_error("Unhandled BTOR2 operation: " + op); + } + return values; + } + std::map nodes_; + std::map state_; + std::map initial_, next_; +}; + +struct TemporaryDirectory { + std::filesystem::path path; + TemporaryDirectory() { + for (size_t attempt = 0; attempt < 32; ++attempt) { + path = std::filesystem::temp_directory_path() / + ("kf-latch-export-" + std::to_string(std::random_device{}()) + "-" + std::to_string(attempt)); + if (std::filesystem::create_directory(path)) return; + } + throw std::runtime_error("Cannot create latch export test directory"); + } + ~TemporaryDirectory() { + std::error_code ignored; + std::filesystem::remove_all(path, ignored); + } +}; + +using State = std::unordered_map; +State initialState(const SequentialDesignModel& model) { + State result; + for (const auto& key : model.stateBits) + result[model.inputVarByKey.at(key)] = model.initialStateValueByKey.at(key); + return result; +} + +bool nativeStep(const SequentialDesignModel& model, State& state, Event event) { + auto values = state; + for (const auto& key : model.environmentInputs) + values[model.inputVarByKey.at(key)] = eventInput(model.displayNameByKey.at(key), event); + const bool output = model.observedOutputExprByKey.at(model.observedOutputs.at(0))->evaluate(values); + for (const auto& key : model.stateBits) + state[model.inputVarByKey.at(key)] = model.nextStateExprByStateKey.at(key)->evaluate(values); + return output; +} + +class LatchEventExportTests : public ::testing::Test { + protected: + void SetUp() override { + NLUniverse::create(); + auto* db = NLDB::create(NLUniverse::get()); + designs_ = NLLibrary::create(db, NLLibrary::Type::Standard, NLName("designs")); + primitives_ = NLLibrary::create(db, NLLibrary::Type::Primitives, NLName("primitives")); + } + void TearDown() override { + naja::DNL::destroy(); + if (auto* universe = NLUniverse::get()) universe->destroy(); + BoolExprCache::destroy(); + } + + SNLDesign* chain(const char* name, size_t length, bool resetFlop = false) { + auto* top = SNLDesign::create(designs_, SNLDesign::Type::Standard, NLName(name)); + const auto input = [&](const char* portName) { + auto* term = SNLScalarTerm::create(top, SNLTerm::Direction::Input, NLName(portName)); + auto* net = SNLScalarNet::create(top, NLName(portName)); + term->setNet(net); + return net; + }; + auto* data = input("data"); + auto* enable = input("enable"); + if (resetFlop) { + auto* clock = input("clock"); + auto* reset = input("reset"); + auto* primitive = SNLDesign::create(primitives_, SNLDesign::Type::Primitive, NLName(std::string(name) + "_ff")); + auto* d = SNLScalarTerm::create(primitive, SNLTerm::Direction::Input, NLName("D")); + auto* c = SNLScalarTerm::create(primitive, SNLTerm::Direction::Input, NLName("C")); + auto* r = SNLScalarTerm::create(primitive, SNLTerm::Direction::Input, NLName("R")); + auto* q = SNLScalarTerm::create(primitive, SNLTerm::Direction::Output, NLName("Q")); + using Modeling = SNLDesignModeling; + using Operator = Modeling::BooleanExpression::Operator; + Modeling::SequentialModel sequential; + sequential.kind = Modeling::SequentialModel::Kind::FlipFlop; + sequential.clockedOn.root = sequential.clockedOn.addTerm(c); + Modeling::SequentialState storage; + auto& expression = storage.nextState; + const auto dNode = expression.addTerm(d); + const auto notReset = expression.addOperation(Operator::Not, {expression.addTerm(r)}); + expression.root = expression.addOperation(Operator::And, {dNode, notReset}); + sequential.states.push_back(storage); + Modeling::BooleanExpression output; + output.root = output.addState(0); + sequential.outputs.push_back({q, output}); + Modeling::setSequentialModel(primitive, sequential); + auto* ff = SNLInstance::create(top, primitive, NLName("reset_ff")); + auto* captured = SNLScalarNet::create(top, NLName("captured")); + ff->getInstTerm(d)->setNet(data); + ff->getInstTerm(c)->setNet(clock); + ff->getInstTerm(r)->setNet(reset); + ff->getInstTerm(q)->setNet(captured); + data = captured; + } + for (size_t index = 0; index < length; ++index) { + auto* latch = SNLInstance::create(top, NLDB0::getDLatch(), NLName("latch" + std::to_string(index))); + auto* output = SNLScalarNet::create(top, NLName("q" + std::to_string(index))); + latch->getInstTerm(NLDB0::getDLatchData())->setNet(data); + latch->getInstTerm(NLDB0::getDLatchEnable())->setNet(enable); + latch->getInstTerm(NLDB0::getDLatchOutput())->setNet(output); + data = output; + } + SNLScalarTerm::create(top, SNLTerm::Direction::Output, NLName("out"))->setNet(data); + return top; + } + + void compareResetTrace(bool rightIsWire) { + auto* leftTop = chain("reset_single", 1, true); + auto* rightTop = chain("reset_reference", rightIsWire ? 0 : 4, true); + SupportOptions options; + options.enabled = true; + options.singleInputChange = true; + options.initialInputs = false; + options.initialStorage = true; + options.workers = 2; + ScopedSupportOptions scope(options); + const auto left = SequentialDesignModel::extract(leftTop); + const auto right = SequentialDesignModel::extract(rightTop); + ASSERT_FALSE(left.hasUnsupportedFeatures()); + ASSERT_FALSE(right.hasUnsupportedFeatures()); + ASSERT_EQ(left.observedOutputs.size(), 1u); + ASSERT_EQ(right.observedOutputs.size(), 1u); + const SecResetSpec reset{2, {{"reset", true}}}; + const auto adaptedLeft = adaptResetCycles(left, reset); + const auto adaptedRight = adaptResetCycles(right, reset); + ASSERT_TRUE(adaptedLeft.model) << adaptedLeft.error; + ASSERT_TRUE(adaptedRight.model) << adaptedRight.error; + TemporaryDirectory directory; + for (const auto engine : {SecEngine::KInduction, SecEngine::Imc, SecEngine::Pdr}) { + for (const auto encoding : {SecEncoding::Binary, SecEncoding::DualRailSteady}) { + SCOPED_TRACE(::testing::Message() << "engine=" << static_cast(engine) + << " encoding=" << static_cast(encoding)); + const auto path = directory.path / "reset.btor2"; + SequentialEquivalenceStrategy strategy(nullptr, nullptr, Config::SolverType::KISSAT, + engine, encoding, reset, Btor2ExportOptions{path.string(), true}); + const auto result = strategy.runExtractedModels(left, right, 16); + ASSERT_EQ(result.status, SequentialEquivalenceStatus::Exported) << result.reason; + BtorTrace exported(path); + auto leftState = initialState(*adaptedLeft.model); + auto rightState = initialState(*adaptedRight.model); + bool data = false, enable = false, clock = false, captured = true, held = true; + // Sorted input positions: clock=0,data=1,enable=2,reset=3; 4 is stutter. + const std::vector events = { + {0, true, true, false, 0}, {3, false, false, false, 3}, // two reset clock cycles + {4, false}, {2, true}, {1, true}, {0, true}, {2, false}, + {1, false}, {0, false}, {0, true}, {3, true}, {4, false}, {2, true}}; + for (size_t frame = 0; frame < events.size(); ++frame) { + SCOPED_TRACE(::testing::Message() << "reset/event step=" << frame); + const auto event = events[frame]; + if (frame < reset.cycles) { + data = event.sampleData; + enable = event.sampleEnable; + captured = false; // asserted synchronous reset sees a complete source-clock pulse + clock = false; + if (enable) held = captured; + } else { + const bool previousClock = clock; + if (event.selector == 0) clock = event.value; + if (event.selector == 1) data = event.value; + if (event.selector == 2) enable = event.value; + // A request to reassert reset is a stutter after release. + if (!previousClock && clock) captured = data; + if (enable) held = captured; + } + const auto leftOutput = nativeStep(*adaptedLeft.model, leftState, event); + const auto rightOutput = nativeStep(*adaptedRight.model, rightState, event); + ASSERT_EQ(leftOutput, frame < reset.cycles ? false : held); + ASSERT_EQ(rightOutput, frame < reset.cycles ? false : rightIsWire ? captured : held); + const auto bad = exported.step(event); + EXPECT_EQ(bad, leftOutput != rightOutput); + EXPECT_EQ(bad, frame >= reset.cycles && rightIsWire && held != captured); + } + } + } + } + + void compareTrace(bool rightIsWire) { + auto* leftTop = chain("single_latch", 1); + auto* rightTop = chain("reference", rightIsWire ? 0 : 4); + TemporaryDirectory directory; + for (const bool initial : {false, true}) { + SupportOptions options; + options.enabled = true; + options.singleInputChange = true; + options.initialInputs = initial; + options.initialStorage = initial; + options.workers = 2; + ScopedSupportOptions scope(options); + const auto left = SequentialDesignModel::extract(leftTop); + const auto right = SequentialDesignModel::extract(rightTop); + ASSERT_FALSE(left.hasUnsupportedFeatures()); + ASSERT_FALSE(right.hasUnsupportedFeatures()); + ASSERT_EQ(left.observedOutputs.size(), 1u); + ASSERT_EQ(right.observedOutputs.size(), 1u); + ASSERT_TRUE(left.skippedObservedOutputs.empty()); + ASSERT_TRUE(right.skippedObservedOutputs.empty()); + for (const auto engine : {SecEngine::KInduction, SecEngine::Imc, SecEngine::Pdr}) { + for (const auto encoding : {SecEncoding::Binary, SecEncoding::DualRailSteady}) { + SCOPED_TRACE(::testing::Message() << "initial=" << initial << " engine=" + << static_cast(engine) << " encoding=" << static_cast(encoding)); + const auto path = directory.path / "trace.btor2"; + SequentialEquivalenceStrategy strategy(nullptr, nullptr, Config::SolverType::KISSAT, + engine, encoding, {}, Btor2ExportOptions{path.string(), true}); + const auto result = strategy.runExtractedModels(left, right, 16); + ASSERT_EQ(result.status, SequentialEquivalenceStatus::Exported) << result.reason; + BtorTrace exported(path); + auto leftState = initialState(left); + auto rightState = initialState(right); + bool data = initial, enable = initial, held = initial; + // Includes first-frame mismatch (no phantom bootstrap suppression), + // hold, opening, transparent updates, closing, and reserved-selector stutter. + const std::vector events = {{0, !initial}, {1, true}, {0, false}, + {0, true}, {1, false}, {0, false}, {3, true}, {1, true}, + {1, false}, {0, true}, {3, false}, {1, true}}; + size_t frame = 0; + for (const auto event : events) { + SCOPED_TRACE(::testing::Message() << "event=" << frame++); + if (event.selector == 0) data = event.value; + if (event.selector == 1) enable = event.value; + if (enable) held = data; + const auto leftOutput = nativeStep(left, leftState, event); + const auto rightOutput = nativeStep(right, rightState, event); + ASSERT_EQ(leftOutput, held); + ASSERT_EQ(rightOutput, rightIsWire ? data : held); + const bool exportedBad = exported.step(event); + EXPECT_EQ(exportedBad, leftOutput != rightOutput); + EXPECT_EQ(exportedBad, rightIsWire && held != data); + } + } + } + } + } + private: + NLLibrary* designs_ = nullptr; + NLLibrary* primitives_ = nullptr; +}; + +TEST_F(LatchEventExportTests, BadPropertyTracksHoldOpenAndCaptureWithoutHiddenFrames) { + compareTrace(true); +} + +TEST_F(LatchEventExportTests, DifferentInternalSettlingDepthsShareExternalObservations) { + compareTrace(false); +} + +TEST_F(LatchEventExportTests, ResetPrefixMasksOnlyResetCyclesAndRetainsPostreleaseLatchMismatch) { + compareResetTrace(true); +} + +TEST_F(LatchEventExportTests, ResetPrefixPreservesEquivalentChainsWithDifferentSettlingDepths) { + compareResetTrace(false); +} + +} // namespace +} // namespace KEPLER_FORMAL::SEC::LATCH diff --git a/test/sec/LatchEventModelTests.cpp b/test/sec/LatchEventModelTests.cpp new file mode 100644 index 00000000..7b4339fe --- /dev/null +++ b/test/sec/LatchEventModelTests.cpp @@ -0,0 +1,551 @@ +// Copyright 2026 keplertech.io +// SPDX-License-Identifier: Apache-2.0 + +#include + +#include +#include + +#include "latch/LatchEventModel.h" + +namespace KEPLER_FORMAL::SEC::LATCH { +namespace { + +Primitive buffer(size_t input, size_t output) { + return combinational("buffer", {input}, {output}, [](const Bits& bits) { return bits; }); +} + +Primitive inverter(size_t input, size_t output) { + return combinational("inverter", {input}, {output}, [](const Bits& bits) { + return Bits{static_cast(!bits[0])}; + }); +} + +std::vector settle(const EventModel& model, State state, size_t bound = 32) { + std::vector states{std::move(state)}; + for (size_t wave = 0; wave <= bound; ++wave) { + if (std::all_of(states.begin(), states.end(), [&](const auto& item) { return model.stable(item); })) { + return states; + } + std::set next; + for (const auto& item : states) { + const auto successors = model.successors(item); + next.insert(successors.begin(), successors.end()); + } + states.assign(next.begin(), next.end()); + } + ADD_FAILURE() << "Expected bounded settling"; + return states; +} + +State simpleBoundary(const EventModel& model, Bits inputs, std::vector storage) { + const auto states = settle(model, model.bootstrap(inputs, storage, Bits(model.network().netCount))); + EXPECT_EQ(states.size(), 1); + return states.front(); +} + +TEST(LatchEventModelTests, EmptyNetworkHasOneForcedBootstrapWave) { + EventModel model(Network{}); + const auto initial = model.bootstrap({}, {}, {}); + EXPECT_FALSE(model.stable(initial)); + const auto next = model.successors(initial); + ASSERT_EQ(next.size(), 1); + EXPECT_TRUE(model.stable(next.front())); + EXPECT_EQ(model.successors(next.front()), next); +} + +TEST(LatchEventModelTests, BootstrapCopiesInitialStorageBeforeForcedEvaluation) { + EventModel model({3, {0, 1}, {latch("l", 0, 1, 2)}}); + auto initial = model.bootstrap({0, 0}, {{1}}, {1, 1, 0}); + EXPECT_EQ(initial.current, (Bits{0, 0, 1})); + EXPECT_EQ(initial.previous, initial.current); + EXPECT_EQ(settle(model, initial).front().storage[0], Bits{1}); +} + +TEST(LatchEventModelTests, LatchFollowsDataWhileOpenAndHoldsWhenClosed) { + EventModel model({3, {0, 1}, {latch("l", 0, 1, 2)}}); + auto state = simpleBoundary(model, {0, 1}, {{0}}); + state = settle(model, model.admit(state, {1, 1})).front(); + EXPECT_EQ(state.current[2], 1); + state = settle(model, model.admit(state, {1, 0})).front(); + state = settle(model, model.admit(state, {0, 0})).front(); + EXPECT_EQ(state.current[2], 1); +} + +TEST(LatchEventModelTests, ActiveLowLatchUsesExplicitPolarity) { + EventModel model({3, {0, 1}, {latch("l", 0, 1, 2, false)}}); + auto state = simpleBoundary(model, {1, 0}, {{0}}); + EXPECT_EQ(state.storage[0], Bits{1}); + state = settle(model, model.admit(state, {1, 1})).front(); + state = settle(model, model.admit(state, {0, 1})).front(); + EXPECT_EQ(state.storage[0], Bits{1}); +} + +TEST(LatchEventModelTests, FourOpenLatchesPropagateInOneExternalEpisode) { + EventModel model({6, {0, 1}, {latch("l0", 0, 1, 2), latch("l1", 2, 1, 3), + latch("l2", 3, 1, 4), latch("l3", 4, 1, 5)}}); + auto state = simpleBoundary(model, {0, 1}, {{0}, {0}, {0}, {0}}); + state = model.admit(state, {1, 1}); + for (size_t wave = 0; wave < 4; ++wave) { + ASSERT_FALSE(model.stable(state)); + auto next = model.successors(state); + ASSERT_EQ(next.size(), 1); + state = next.front(); + for (size_t output = 0; output < 4; ++output) { + EXPECT_EQ(state.current[output + 2], output <= wave); + } + } + EXPECT_TRUE(model.stable(state)); +} + +TEST(LatchEventModelTests, SimultaneousCloseAndDataPreservesBothPinOrders) { + EventModel model({3, {0, 1}, {latch("l", 0, 1, 2)}}); + const auto state = simpleBoundary(model, {0, 1}, {{0}}); + const auto results = settle(model, model.admit(state, {1, 0})); + ASSERT_EQ(results.size(), 2); + EXPECT_EQ(results[0].current[2], 0); + EXPECT_EQ(results[1].current[2], 1); +} + +TEST(LatchEventModelTests, OpeningWithDataChangeDeduplicatesIdenticalFinalOutcomes) { + EventModel model({3, {0, 1}, {latch("l", 0, 1, 2)}}); + const auto state = simpleBoundary(model, {0, 0}, {{0}}); + const auto results = settle(model, model.admit(state, {1, 1})); + ASSERT_EQ(results.size(), 1); + EXPECT_EQ(results.front().current[2], 1); +} + +TEST(LatchEventModelTests, BootstrapDoesNotInventClockEdge) { + EventModel model({3, {0, 1}, {flipFlop("f", 0, 1, 2)}}); + const auto state = simpleBoundary(model, {1, 1}, {{0}}); + EXPECT_EQ(state.storage[0], Bits{0}); +} + +TEST(LatchEventModelTests, FlipFlopCapturesOnlySpecifiedEdge) { + EventModel model({3, {0, 1}, {flipFlop("f", 0, 1, 2)}}); + auto state = simpleBoundary(model, {1, 0}, {{0}}); + state = settle(model, model.admit(state, {1, 1})).front(); + EXPECT_EQ(state.current[2], 1); + state = settle(model, model.admit(state, {0, 1})).front(); + state = settle(model, model.admit(state, {0, 0})).front(); + EXPECT_EQ(state.current[2], 1); +} + +TEST(LatchEventModelTests, FallingEdgeFlipFlopUsesExplicitPolarity) { + EventModel model({3, {0, 1}, {flipFlop("f", 0, 1, 2, false)}}); + auto state = simpleBoundary(model, {1, 1}, {{0}}); + state = settle(model, model.admit(state, {1, 0})).front(); + EXPECT_EQ(state.current[2], 1); +} + +TEST(LatchEventModelTests, ClockDataRaceDoesNotConsumeClockEdgeTwice) { + EventModel model({3, {0, 1}, {flipFlop("f", 0, 1, 2)}}); + const auto state = simpleBoundary(model, {0, 0}, {{0}}); + const auto results = settle(model, model.admit(state, {1, 1})); + // Clock first keeps old data; a subsequent data-pin visit MUST NOT re-use it. + ASSERT_EQ(results.size(), 2); + EXPECT_EQ(results[0].current[2], 0); + EXPECT_EQ(results[1].current[2], 1); +} + +TEST(LatchEventModelTests, GeneratedClockAfterBootstrapCommitIsRealEvent) { + EventModel model({4, {0, 1}, {buffer(1, 2), flipFlop("f", 0, 2, 3)}}); + auto states = settle(model, model.bootstrap({1, 1}, {{}, {0}}, {0, 0, 0, 0})); + ASSERT_EQ(states.size(), 1); + EXPECT_EQ(states[0].current[3], 1); +} + +TEST(LatchEventModelTests, GeneratedClockBootstrapSeedsCanChangeRememberedOutcome) { + EventModel model({4, {0, 1}, {buffer(1, 2), flipFlop("f", 0, 2, 3)}}); + const auto lowSeed = settle(model, model.bootstrap({1, 1}, {{}, {0}}, {0, 0, 0, 0})); + const auto highSeed = settle(model, model.bootstrap({1, 1}, {{}, {0}}, {0, 0, 1, 0})); + EXPECT_EQ(lowSeed.front().current[3], 1); + EXPECT_EQ(highSeed.front().current[3], 0); +} + +TEST(LatchEventModelTests, SelfFeedbackRetainsHistoryInsteadOfChoosingFixedPoint) { + EventModel model({2, {0}, {latch("self", 1, 0, 1)}}); + for (uint8_t value : {0, 1}) { + const auto state = simpleBoundary(model, {1}, {{value}}); + EXPECT_EQ(state.current[1], value); + EXPECT_EQ(model.successors(state), std::vector{state}); + } +} + +TEST(LatchEventModelTests, InvertingTransparentFeedbackKeepsExecuting) { + EventModel model({3, {0}, {inverter(2, 1), latch("loop", 1, 0, 2)}}); + auto state = model.bootstrap({1}, {{}, {0}}, {0, 1, 0}); + std::set seen; + bool repeated = false; + for (size_t i = 0; i < 20; ++i) { + EXPECT_FALSE(model.stable(state)); + if (!seen.insert(state).second) repeated = true; + auto next = model.successors(state); + ASSERT_EQ(next.size(), 1); + state = next.front(); + } + EXPECT_TRUE(repeated); +} + +TEST(LatchEventModelTests, ClosedLatchBreaksInvertingFeedback) { + EventModel model({3, {0}, {inverter(2, 1), latch("loop", 1, 0, 2)}}); + const auto state = simpleBoundary(model, {0}, {{}, {0}}); + EXPECT_EQ(state.current, (Bits{0, 1, 0})); +} + +TEST(LatchEventModelTests, IntermediateEnablePulseIsNotLostAtRegionBoundary) { + Network network{6, {0}, {buffer(0, 1), buffer(1, 2), + combinational("xor", {0, 2}, {3}, [](const Bits& bits) { + return Bits{static_cast(bits[0] ^ bits[1])}; + }), latch("capture", 4, 3, 5)}}; + network.constantByNet.resize(6); + network.constantByNet[4] = true; + EventModel model(network); + auto state = simpleBoundary(model, {0}, {{}, {}, {}, {0}}); + state = model.admit(state, {1}); + bool sawPulse = false; + for (size_t i = 0; !model.stable(state) && i < 16; ++i) { + state = model.successors(state).front(); + sawPulse |= state.current[3] != 0; + } + EXPECT_TRUE(sawPulse); + EXPECT_TRUE(model.stable(state)); + EXPECT_EQ(state.current[3], 0); + EXPECT_EQ(state.current[5], 1); +} + +TEST(LatchEventModelTests, EpochBarrierDoesNotInventReconvergentXorPulse) { + Network network{6, {0}, {buffer(0, 1), buffer(0, 2), + combinational("xor", {1, 2}, {3}, [](const Bits& bits) { + return Bits{static_cast(bits[0] ^ bits[1])}; + }), latch("capture", 4, 3, 5)}}; + network.constantByNet.resize(6); + network.constantByNet[4] = true; + for (size_t workers : {1, 2, 4}) { + EventModel model(network, {}, workers); + auto state = simpleBoundary(model, {0}, {{}, {}, {}, {0}}); + state = model.admit(state, {1}); + while (!model.stable(state)) { + state = model.successors(state).front(); + EXPECT_EQ(state.current[3], 0); + EXPECT_EQ(state.current[5], 0); + } + } +} + +TEST(LatchEventModelTests, NondeterministicProducerChoiceIsSharedByAllFanout) { + EventModel model({5, {0, 1}, {flipFlop("f", 0, 1, 2), buffer(2, 3), buffer(2, 4)}}); + auto state = simpleBoundary(model, {0, 0}, {{0}, {}, {}}); + const auto results = settle(model, model.admit(state, {1, 1})); + ASSERT_EQ(results.size(), 2); + for (const auto& result : results) { + EXPECT_EQ(result.current[2], result.current[3]); + EXPECT_EQ(result.current[3], result.current[4]); + } +} + +TEST(LatchEventModelTests, IndependentPrimitiveRacesKeepCartesianProduct) { + Network network{4, {0, 1}, {flipFlop("f0", 0, 1, 2), flipFlop("f1", 0, 1, 3)}}; + std::vector expected; + for (size_t workers : {1, 2, 4}) { + EventModel model(network, {}, workers); + const auto state = simpleBoundary(model, {0, 0}, {{0}, {0}}); + const auto results = settle(model, model.admit(state, {1, 1})); + ASSERT_EQ(results.size(), 4); + if (expected.empty()) expected = results; + EXPECT_EQ(results, expected); + } +} + +TEST(LatchEventModelTests, DuplicateNetPinsAreDistinctVisitedPositions) { + auto primitive = latch("duplicated", 0, 0, 1); + EventModel model({2, {0}, {primitive}}); + auto state = simpleBoundary(model, {1}, {{1}}); + const auto results = settle(model, model.admit(state, {0})); + // Both positions change: enable first holds 1, data first captures 0. + ASSERT_EQ(results.size(), 2); + EXPECT_EQ(results[0].current[1], 0); + EXPECT_EQ(results[1].current[1], 1); +} + +TEST(LatchEventModelTests, EnumeratesAllSixOrdersOfThreeChangedPinPositions) { + Primitive primitive; + primitive.name = "order_recorder"; + primitive.inputs = {0, 1, 2}; + primitive.outputs = {3, 4, 5, 6, 7, 8}; + primitive.storageBits = 6; + primitive.react = [](const Bits& storage, const Bits&, const Bits&, + std::optional changedPin, bool boot) { + if (boot || !changedPin) return Reaction{storage, storage}; + size_t code = 0; + for (size_t bit = 0; bit < storage.size(); ++bit) code |= size_t{storage[bit]} << bit; + code = 4 * code + *changedPin + 1; + Bits next(6); + for (size_t bit = 0; bit < next.size(); ++bit) next[bit] = (code >> bit) & 1; + return Reaction{next, next}; + }; + EventModel model({9, {0, 1, 2}, {primitive}}); + const auto state = simpleBoundary(model, {0, 0, 0}, {Bits(6)}); + const auto results = settle(model, model.admit(state, {1, 1, 1})); + EXPECT_EQ(results.size(), 6); + std::set codes; + for (const auto& result : results) { + size_t code = 0; + for (size_t bit = 0; bit < 6; ++bit) code |= size_t{result.storage[0][bit]} << bit; + codes.insert(code); + } + EXPECT_EQ(codes, (std::set{27, 30, 39, 45, 54, 57})); +} + +TEST(LatchEventModelTests, AsyncClearWorksWithoutClockEdgeIncludingBootstrap) { + auto primitive = flipFlop("clear_ff", 0, 1, 3); + const auto baseReaction = primitive.react; + primitive.inputs.push_back(2); + primitive.react = [baseReaction](const Bits& storage, const Bits& before, const Bits& pins, + std::optional changedPin, bool boot) { + if (pins[2]) return Reaction{{0}, {0}}; + return baseReaction(storage, before, pins, changedPin, boot); + }; + EventModel model({4, {0, 1, 2}, {primitive}}); + auto state = simpleBoundary(model, {1, 0, 0}, {{1}}); + state = settle(model, model.admit(state, {1, 0, 1})).front(); + EXPECT_EQ(state.current[3], 0); + EXPECT_EQ(simpleBoundary(model, {1, 1, 1}, {{1}}).current[3], 0); +} + +TEST(LatchEventModelTests, ConflictingControlsProduceStickyNonstableError) { + auto primitive = latch("controls", 0, 1, 4); + const auto baseReaction = primitive.react; + primitive.inputs.insert(primitive.inputs.end(), {2, 3}); + primitive.react = [baseReaction](const Bits& storage, const Bits& before, const Bits& pins, + std::optional changedPin, bool boot) { + if (pins[2] && pins[3]) return Reaction{{}, {}, true, "clear and preset conflict"}; + if (pins[2]) return Reaction{{0}, {0}}; + if (pins[3]) return Reaction{{1}, {1}}; + return baseReaction(storage, before, pins, changedPin, boot); + }; + EventModel model({5, {0, 1, 2, 3}, {primitive}}); + auto state = model.bootstrap({0, 0, 1, 1}, {{0}}, Bits(5)); + state = model.successors(state).front(); + EXPECT_TRUE(state.error); + EXPECT_FALSE(model.stable(state)); + EXPECT_NE(state.errorReason.find("conflict"), std::string::npos); + EXPECT_EQ(model.successors(state), std::vector{state}); +} + +TEST(LatchEventModelTests, OneFailingPinOrderCannotVanishAmongSuccessfulOrders) { + Primitive primitive; + primitive.name = "clear_preset"; + primitive.inputs = {0, 1}; + primitive.outputs = {2}; + primitive.storageBits = 1; + primitive.react = [](const Bits& storage, const Bits&, const Bits& pins, + std::optional, bool) -> Reaction { + if (pins[0] && pins[1]) return {{}, {}, true, "conflicting controls"}; + const Bits value{pins[0] ? uint8_t{0} : pins[1] ? uint8_t{1} : storage[0]}; + return {value, value}; + }; + EventModel model({3, {0, 1}, {primitive}}); + const auto state = simpleBoundary(model, {1, 0}, {{0}}); + const auto outcomes = model.successors(model.admit(state, {0, 1})); + ASSERT_EQ(outcomes.size(), 2); + size_t failures = 0; + size_t successes = 0; + for (const auto& outcome : outcomes) { + if (outcome.error) { + ++failures; + EXPECT_FALSE(model.stable(outcome)); + EXPECT_EQ(model.successors(outcome), std::vector{outcome}); + } else { + ++successes; + EXPECT_EQ(outcome.current[2], 1); + } + } + EXPECT_EQ(failures, 1); + EXPECT_EQ(successes, 1); +} + +TEST(LatchEventModelTests, MultiOutputTupleAndComplementStorageMappingAreAtomic) { + auto primitive = latch("pair", 0, 1, 2); + primitive.outputs.push_back(3); + primitive.initialOutputs = [](const Bits& storage) { + return Bits{storage[0], static_cast(!storage[0])}; + }; + const auto baseReaction = primitive.react; + primitive.react = [baseReaction](const Bits& storage, const Bits& before, const Bits& pins, + std::optional changedPin, bool boot) { + auto result = baseReaction(storage, before, pins, changedPin, boot); + result.outputs.push_back(static_cast(!result.outputs[0])); + return result; + }; + EventModel model({4, {0, 1}, {primitive}}); + const auto initial = model.bootstrap({0, 0}, {{1}}, Bits(4)); + EXPECT_EQ(initial.current, (Bits{0, 0, 1, 0})); + auto state = settle(model, initial).front(); + state = settle(model, model.admit(state, {0, 1})).front(); + EXPECT_EQ(state.current, (Bits{0, 1, 0, 1})); +} + +TEST(LatchEventModelTests, InputDependentInitialProjectionsReadOneFrozenSnapshot) { + auto first = latch("first", 1, 0, 2); + auto second = latch("second", 2, 0, 3); + first.initialOutputValues = second.initialOutputValues = [](const Bits& storage, const Bits& pins) { + return Bits{static_cast(storage[0] & pins[0])}; + }; + // First's projected output becomes 1, but second must read the original seed 0. + // Swapping primitive order cannot change this simultaneous projection. + EventModel forward({4, {0, 1}, {first, second}}); + EventModel reversed({4, {0, 1}, {second, first}}); + const auto a = forward.bootstrap({0, 1}, {{1}, {1}}, {0, 0, 0, 0}); + const auto b = reversed.bootstrap({0, 1}, {{1}, {1}}, {0, 0, 0, 0}); + EXPECT_EQ(a.current, (Bits{0, 1, 1, 0})); + EXPECT_EQ(a.current, b.current); + EXPECT_EQ(a.previous, a.current); + EXPECT_EQ(b.previous, b.current); +} + +TEST(LatchEventModelTests, AdmissionActivatesExactlyChangedConsumersAndReplacesOldWork) { + EventModel model({4, {0, 1}, {buffer(0, 2), buffer(1, 3)}}); + auto state = simpleBoundary(model, {0, 0}, {{}, {}}); + state = model.admit(state, {1, 0}); + EXPECT_EQ(state.active, (Bits{1, 0})); + state = model.successors(state).front(); + EXPECT_EQ(state.active, (Bits{0, 0})); + EXPECT_EQ(state.previous, state.current); + EXPECT_TRUE(model.stable(state)); + EXPECT_EQ(model.admit(state, {1, 0}), state); +} + +TEST(LatchEventModelTests, UnconsumedExternalChangeStillNormalizesHistory) { + EventModel model({1, {0}, {}}); + auto state = simpleBoundary(model, {0}, {}); + state = model.admit(state, {1}); + EXPECT_TRUE(model.stable(state)); + EXPECT_EQ(state.previous, state.current); +} + +TEST(LatchEventModelTests, InvalidAdmissionDoesNotRemoveTransaction) { + EventModel model({3, {0, 1}, {latch("l", 0, 1, 2)}}); + const auto initial = model.bootstrap({0, 0}, {{0}}, Bits(3)); + EXPECT_TRUE(model.admit(initial, {1, 0}).error); + const auto state = settle(model, initial).front(); + EXPECT_TRUE(model.admit(state, {}).error); + EXPECT_TRUE(model.admit(state, {2, 0}).error); + const auto bad = model.admit(state, {}); + EXPECT_EQ(model.admit(bad, {0, 0}), bad); + EXPECT_FALSE(model.stable(bad)); +} + +TEST(LatchEventModelTests, MissingReactionIsExplicitAbsorbingError) { + auto primitive = latch("missing", 0, 1, 2); + primitive.react = {}; + EventModel model({3, {0, 1}, {primitive}}); + const auto next = model.successors(model.bootstrap({0, 0}, {{0}}, Bits(3))); + ASSERT_EQ(next.size(), 1); + EXPECT_TRUE(next[0].error); + EXPECT_FALSE(model.stable(next[0])); + EXPECT_EQ(model.successors(next[0]), next); +} + +TEST(LatchEventModelTests, MalformedAndThrowingReactionRemainVisibleErrors) { + for (int variant = 0; variant < 4; ++variant) { + auto primitive = latch("bad", 0, 1, 2); + primitive.react = [variant](const Bits&, const Bits&, const Bits&, std::optional, bool) -> Reaction { + if (variant == 0) return {{}, {0}}; + if (variant == 1) return {{0}, {}}; + if (variant == 2) return {{0}, {2}}; + throw std::runtime_error("undefined case"); + }; + EventModel model({3, {0, 1}, {primitive}}); + const auto next = model.successors(model.bootstrap({0, 0}, {{0}}, Bits(3))); + ASSERT_EQ(next.size(), 1); + EXPECT_TRUE(next[0].error); + EXPECT_FALSE(model.stable(next[0])); + } +} + +TEST(LatchEventModelTests, MissingOrMalformedInitialOutputMappingIsError) { + auto primitive = latch("map", 0, 1, 2); + primitive.outputs.push_back(3); + EventModel missing({4, {0, 1}, {primitive}}); + EXPECT_TRUE(missing.bootstrap({0, 0}, {{0}}, Bits(4)).error); + primitive.initialOutputs = [](const Bits&) { return Bits{0, 2}; }; + EventModel malformed({4, {0, 1}, {primitive}}); + EXPECT_TRUE(malformed.bootstrap({0, 0}, {{0}}, Bits(4)).error); +} + +TEST(LatchEventModelTests, ConstantsAreFixedAcrossBootstrapAndTransactions) { + Network network{3, {0}, {latch("constant_enable", 0, 1, 2)}}; + network.constantByNet.resize(3); + network.constantByNet[1] = true; + EventModel model(network); + auto state = simpleBoundary(model, {0}, {{0}}); + state = settle(model, model.admit(state, {1})).front(); + EXPECT_EQ(state.current, (Bits{1, 1, 1})); + state.current[1] = 0; + EXPECT_THROW(model.successors(state), std::invalid_argument); +} + +TEST(LatchEventModelTests, RejectsMultipleDriversAndUndrivenOrOutOfRangeNets) { + EXPECT_THROW(EventModel((Network{2, {0}, {buffer(0, 0)}})), std::invalid_argument); + EXPECT_THROW(EventModel((Network{2, {0}, {buffer(2, 1)}})), std::invalid_argument); + EXPECT_THROW(EventModel((Network{2, {0}, {buffer(0, 2)}})), std::invalid_argument); + EXPECT_THROW(EventModel((Network{2, {0}, {}})), std::invalid_argument); + EXPECT_THROW(EventModel((Network{1, {0, 0}, {}})), std::invalid_argument); + Network constantConflict{1, {0}, {}, {true}}; + EXPECT_THROW(EventModel{constantConflict}, std::invalid_argument); + Network badConstants{1, {0}, {}, {true, false}}; + EXPECT_THROW(EventModel{badConstants}, std::invalid_argument); +} + +TEST(LatchEventModelTests, RejectsMalformedBootstrapAndStateShapes) { + EventModel model({3, {0, 1}, {latch("l", 0, 1, 2)}}); + EXPECT_THROW(model.bootstrap({}, {{0}}, Bits(3)), std::invalid_argument); + EXPECT_THROW(model.bootstrap({2, 0}, {{0}}, Bits(3)), std::invalid_argument); + EXPECT_THROW(model.bootstrap({0, 0}, {}, Bits(3)), std::invalid_argument); + EXPECT_THROW(model.bootstrap({0, 0}, {{2}}, Bits(3)), std::invalid_argument); + EXPECT_THROW(model.bootstrap({0, 0}, {{0}}, Bits(2)), std::invalid_argument); + auto state = model.bootstrap({0, 0}, {{0}}, Bits(3)); + state.active.push_back(0); + EXPECT_THROW(model.successors(state), std::invalid_argument); +} + +TEST(LatchEventModelTests, PinOrderingResourceLimitThrowsWithoutTruncatingOutcomes) { + EventModel model({3, {0, 1}, {flipFlop("f", 0, 1, 2)}}, {1, 100}); + auto state = simpleBoundary(model, {0, 0}, {{0}}); + EXPECT_THROW(model.successors(model.admit(state, {1, 1})), Limit); +} + +TEST(LatchEventModelTests, SuccessorResourceLimitThrowsWithoutTruncatingOutcomes) { + EventModel model({4, {0, 1}, {flipFlop("f0", 0, 1, 2), flipFlop("f1", 0, 1, 3)}}, {100, 3}); + auto state = simpleBoundary(model, {0, 0}, {{0}, {0}}); + EXPECT_THROW(model.successors(model.admit(state, {1, 1})), Limit); +} + +TEST(LatchEventModelTests, RejectsZeroResourceLimit) { + EXPECT_THROW(EventModel(Network{}, Limits{0, 1}), std::invalid_argument); + EXPECT_THROW(EventModel(Network{}, Limits{1, 0}), std::invalid_argument); +} + +TEST(LatchEventModelTests, StateKeysIncludeHistoryWorkStorageBootstrapAndErrors) { + State state{{0}, {0}, {{0}}, {0}, false, false, {}}; + std::set states{state}; + std::set keys{state.key()}; + const auto insert = [&](State different) { + EXPECT_NE(different, state); + states.insert(different); + keys.insert(different.key()); + }; + auto different = state; different.current[0] = 1; insert(different); + different = state; different.previous[0] = 1; insert(different); + different = state; different.storage[0][0] = 1; insert(different); + different = state; different.active[0] = 1; insert(different); + different = state; different.bootstrap = true; insert(different); + different = state; different.error = true; insert(different); + different = state; different.errorReason = std::string("a\0b", 3); insert(different); + EXPECT_EQ(states.size(), 8); + EXPECT_EQ(keys.size(), 8); +} + +} // namespace +} // namespace KEPLER_FORMAL::SEC::LATCH diff --git a/test/sec/LatchNetlistAdapterTests.cpp b/test/sec/LatchNetlistAdapterTests.cpp new file mode 100644 index 00000000..d327c2cc --- /dev/null +++ b/test/sec/LatchNetlistAdapterTests.cpp @@ -0,0 +1,801 @@ +// Copyright 2026 keplertech.io +// SPDX-License-Identifier: Apache-2.0 + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "BoolExprCache.h" +#include "DNL.h" +#include "NLDB.h" +#include "NLDB0.h" +#include "NLLibrary.h" +#include "NLName.h" +#include "NLUniverse.h" +#include "SNLDesign.h" +#include "SNLDesignModeling.h" +#include "SNLInstance.h" +#include "SNLScalarNet.h" +#include "SNLScalarTerm.h" +#include "latch/LatchNetlistAdapter.h" +#include "latch/LatchSupportOptions.h" +#include "latch/NajaEventPrimitive.h" +#include "model/SequentialDesignModel.h" +#include "strategy/SequentialEquivalenceStrategy.h" + +namespace KEPLER_FORMAL::SEC::LATCH { +namespace { +using namespace naja::NL; +using Modeling = SNLDesignModeling; +using Expression = Modeling::BooleanExpression; +using Operator = Expression::Operator; + +Expression termExpression(SNLBitTerm* term) { + Expression expression; + expression.root = expression.addTerm(term); + return expression; +} + +Expression stateExpression(bool invert = false) { + Expression expression; + auto root = expression.addState(0); + if (invert) root = expression.addOperation(Operator::Not, {root}); + expression.root = root; + return expression; +} + +class LatchNetlistAdapterTests : public ::testing::Test { + protected: + void SetUp() override { + NLUniverse::create(); + auto* db = NLDB::create(NLUniverse::get()); + designs = NLLibrary::create(db, NLLibrary::Type::Standard, NLName("designs")); + primitives = NLLibrary::create(db, NLLibrary::Type::Primitives, NLName("primitives")); + } + void TearDown() override { + naja::DNL::destroy(); + if (auto* universe = NLUniverse::get()) universe->destroy(); + BoolExprCache::destroy(); + } + + SupportOptions options(bool single = true) { + SupportOptions result; + result.enabled = true; + result.singleInputChange = single; + result.initialInputs = false; + result.initialStorage = false; + result.workers = 2; + return result; + } + + SNLScalarNet* port(SNLDesign* top, const char* name, SNLTerm::Direction direction) { + auto* term = SNLScalarTerm::create(top, direction, NLName(name)); + auto* net = SNLScalarNet::create(top, NLName(name)); + term->setNet(net); + return net; + } + + SNLDesign* directTop(const char* name, SNLDesign* primitive, const char* control = "E") { + auto* top = SNLDesign::create(designs, SNLDesign::Type::Standard, NLName(name)); + auto* data = port(top, "data", SNLTerm::Direction::Input); + auto* enable = port(top, "enable", SNLTerm::Direction::Input); + auto* cell = SNLInstance::create(top, primitive, NLName("cell")); + cell->getInstTerm(primitive->getScalarTerm(NLName("D")))->setNet(data); + cell->getInstTerm(primitive->getScalarTerm(NLName(control)))->setNet(enable); + size_t outputIndex = 0; + for (auto* term : primitive->getBitTerms()) { + if (term->getDirection() == SNLTerm::Direction::Output) { + auto* output = port(top, outputIndex++ ? "out_n" : "out", SNLTerm::Direction::Output); + cell->getInstTerm(term)->setNet(output); + } + } + return top; + } + + SNLDesign* explicitLatch(const char* name, bool inverted = false, bool pair = false, + bool gatedOutput = false) { + auto* cell = SNLDesign::create(primitives, SNLDesign::Type::Primitive, NLName(name)); + auto* data = SNLScalarTerm::create(cell, SNLTerm::Direction::Input, NLName("D")); + auto* enable = SNLScalarTerm::create(cell, SNLTerm::Direction::Input, NLName("E")); + auto* output = SNLScalarTerm::create(cell, SNLTerm::Direction::Output, NLName("Q")); + Modeling::SequentialModel model; + model.kind = Modeling::SequentialModel::Kind::Latch; + model.clockedOn = termExpression(enable); + if (gatedOutput) { + model.clockedOn.root = model.clockedOn.addOperation(Operator::Not, {model.clockedOn.root}); + } + Modeling::SequentialState state; + state.nextState = termExpression(data); + model.states.push_back(state); + auto outputExpression = stateExpression(inverted); + if (gatedOutput) { + const auto phase = outputExpression.addTerm(enable); + outputExpression.root = outputExpression.addOperation(Operator::And, {outputExpression.root, phase}); + } + model.outputs.push_back({output, outputExpression}); + if (pair) { + auto* complement = SNLScalarTerm::create(cell, SNLTerm::Direction::Output, NLName("QN")); + model.outputs.push_back({complement, stateExpression(!inverted)}); + } + Modeling::setSequentialModel(cell, model); + return cell; + } + + SNLDesign* inverterModel() { + auto* cell = SNLDesign::create(primitives, SNLDesign::Type::Primitive, NLName("inverter")); + auto* input = SNLScalarTerm::create(cell, SNLTerm::Direction::Input, NLName("A")); + auto* output = SNLScalarTerm::create(cell, SNLTerm::Direction::Output, NLName("Y")); + Modeling::addCombinatorialArcs({input}, {output}); + Modeling::setTruthTable(cell, SNLTruthTable(1, 1, SNLTruthTable::fullDependencies(1))); + return cell; + } + + std::unordered_map initialState(const SequentialDesignModel& model) { + std::unordered_map result; + for (const auto& key : model.stateBits) { + EXPECT_EQ(model.initialStateValueByKey.count(key), 1u); + result[model.inputVarByKey.at(key)] = model.initialStateValueByKey.at(key); + } + return result; + } + + std::vector> initialStates(const SequentialDesignModel& model) { + // Small fixtures only: enumerate the actual initial relation, not just its + // unit facts, so lost correlations and accidentally fixed origins fail. + EXPECT_LT(model.stateBits.size(), 20u); + if (model.stateBits.size() >= 20) return {}; + std::vector> result; + for (size_t code = 0; code < (size_t(1) << model.stateBits.size()); ++code) { + std::unordered_map state; + bool matches = true; + for (size_t i = 0; i < model.stateBits.size(); ++i) { + const auto& key = model.stateBits[i]; + const bool value = (code >> i) & 1; + state.emplace(model.inputVarByKey.at(key), value); + const auto fixed = model.initialStateValueByKey.find(key); + if (fixed != model.initialStateValueByKey.end() && fixed->second != value) matches = false; + } + if (matches && (!model.initialCondition || model.initialCondition->evaluate(state))) + result.push_back(std::move(state)); + } + return result; + } + + bool inputOrigin(const SequentialDesignModel& model, + const std::unordered_map& state, const std::string& name) { + for (const auto& [input, origin] : model.initialInputStateKeyByInputKey) { + const auto& display = model.displayNameByKey.at(input); + if (display == name || display == "$event.interface." + name) + return state.at(model.inputVarByKey.at(origin)); + } + ADD_FAILURE() << "Missing shared initial input origin: " << name; + return false; + } + + std::map step(const SequentialDesignModel& model, + std::unordered_map& state, size_t selector, bool value) { + auto environment = state; + for (const auto& key : model.environmentInputs) { + const auto& name = model.displayNameByKey.at(key); + bool bit = false; + if (name == "$event.value") bit = value; + else if (name.find("$event.select[") == 0) { + const auto index = std::stoul(name.substr(std::string("$event.select[").size())); + bit = (selector >> index) & 1; + } + environment[model.inputVarByKey.at(key)] = bit; + } + std::map outputs; + for (const auto& key : model.observedOutputs) + outputs[model.displayNameByKey.at(key)] = model.observedOutputExprByKey.at(key)->evaluate(environment); + for (const auto& key : model.stateBits) + state[model.inputVarByKey.at(key)] = model.nextStateExprByStateKey.at(key)->evaluate(environment); + return outputs; + } + + void expectPublishedSupport(const SequentialDesignModel& model) { + std::set variables; + for (const auto& [key, variable] : model.inputVarByKey) variables.insert(variable); + const auto check = [&](BoolExpr* expression) { + for (auto variable : expression->getSupportVars()) { + if (variable > 1) EXPECT_TRUE(variables.count(variable)) << variable; + } + }; + for (const auto& [key, expression] : model.nextStateExprByStateKey) check(expression); + for (const auto& [key, expression] : model.observedOutputExprByKey) check(expression); + if (model.initialCondition) check(model.initialCondition); + } + + NLLibrary* designs = nullptr; + NLLibrary* primitives = nullptr; +}; + +TEST_F(LatchNetlistAdapterTests, DefaultAnyChangeAttemptKeepsUnprovedRaceOpaque) { + EXPECT_TRUE(supportOptions().enabled); + EXPECT_TRUE(supportOptions().hasEventContract()); + EXPECT_FALSE(supportOptions().initialInputs); + EXPECT_FALSE(supportOptions().initialStorage); + auto* top = directTop("top", NLDB0::getDLatch()); + EXPECT_TRUE(extractEventDesign(top, {}, 0).has_value()); + const auto model = SequentialDesignModel::extract(top); + EXPECT_TRUE(model.observedOutputs.empty()); + EXPECT_EQ(model.skippedObservedOutputs.size(), 1u); +} + +TEST_F(LatchNetlistAdapterTests, UnspecifiedInitializationRetainsEveryStableLatchStart) { + auto setting = options(); + setting.initialInputs.reset(); + setting.initialStorage.reset(); + ScopedSupportOptions scope(setting); + const auto model = SequentialDesignModel::extract(directTop("top", NLDB0::getDLatch())); + ASSERT_FALSE(model.hasUnsupportedFeatures()); + ASSERT_EQ(model.observedOutputs.size(), 1u); + ASSERT_NE(model.initialCondition, nullptr); + expectPublishedSupport(model); + const auto starts = initialStates(model); + ASSERT_EQ(starts.size(), 8u); // Two inputs and an independent stored origin. + std::set> observations; + for (auto state : starts) { + const bool data = inputOrigin(model, state, "data[0]"); + const bool enable = inputOrigin(model, state, "enable[0]"); + const bool output = step(model, state, 3, false).at("out[0]"); + if (enable) EXPECT_EQ(output, data); + observations.emplace(data, enable, output); + } + EXPECT_EQ(observations.size(), 6u); // Closed holds 0 or 1; open follows D. +} + +TEST_F(LatchNetlistAdapterTests, ScopedOptionsRestorePreviousSemanticContract) { + EXPECT_TRUE(supportOptions().enabled); + EXPECT_TRUE(supportOptions().hasEventContract()); + { + ScopedSupportOptions outer(options()); + EXPECT_TRUE(supportOptions().enabled); + { + auto disabled = options(); + disabled.enabled = false; + ScopedSupportOptions inner(disabled); + EXPECT_FALSE(supportOptions().enabled); + EXPECT_FALSE(supportOptions().hasEventContract()); + } + EXPECT_TRUE(supportOptions().enabled); + EXPECT_TRUE(supportOptions().singleInputChange); + } + EXPECT_TRUE(supportOptions().enabled); + EXPECT_TRUE(supportOptions().hasEventContract()); +} + +TEST_F(LatchNetlistAdapterTests, OptionalInputRestrictionDoesNotInitializeStorage) { + auto setting = options(); + setting.initialStorage.reset(); + ScopedSupportOptions scope(setting); + const auto model = SequentialDesignModel::extract(directTop("top", NLDB0::getDLatch())); + ASSERT_EQ(model.observedOutputs.size(), 1u); + const auto starts = initialStates(model); + ASSERT_EQ(starts.size(), 2u); + std::set outputValues; + for (auto state : starts) outputValues.insert(step(model, state, 3, false).at("out[0]")); + EXPECT_EQ(outputValues, (std::set{false, true})); +} + +TEST_F(LatchNetlistAdapterTests, IndependentComponentsShareInitialInputLevels) { + auto* top = directTop("top", NLDB0::getDLatch()); + auto* second = SNLInstance::create(top, NLDB0::getDLatch(), NLName("second")); + second->getInstTerm(NLDB0::getDLatchData())->setNet(top->getScalarNet(NLName("data"))); + second->getInstTerm(NLDB0::getDLatchEnable())->setNet(top->getScalarNet(NLName("enable"))); + second->getInstTerm(NLDB0::getDLatchOutput())->setNet(port(top, "other", SNLTerm::Direction::Output)); + auto setting = options(); + setting.initialInputs.reset(); + ScopedSupportOptions scope(setting); + const auto model = SequentialDesignModel::extract(top); + ASSERT_EQ(model.observedOutputs.size(), 2u); + EXPECT_EQ(model.initialInputStateKeyByInputKey.size(), 2u); + const auto starts = initialStates(model); + ASSERT_EQ(starts.size(), 4u); // Shared D/E, not four unrelated component inputs. + for (auto state : starts) { + const bool data = inputOrigin(model, state, "data[0]"); + const bool enable = inputOrigin(model, state, "enable[0]"); + const auto outputs = step(model, state, 3, false); + EXPECT_EQ(outputs.at("out[0]"), data && enable); + EXPECT_EQ(outputs.at("other[0]"), outputs.at("out[0]")); + } +} + +TEST_F(LatchNetlistAdapterTests, UnknownInitialClockDoesNotCaptureWithoutAnEdge) { + auto setting = options(); + setting.initialInputs.reset(); + setting.initialStorage.reset(); + ScopedSupportOptions scope(setting); + const auto model = SequentialDesignModel::extract(directTop("top", NLDB0::getDFF(), "C")); + ASSERT_EQ(model.observedOutputs.size(), 1u); + const auto starts = initialStates(model); + ASSERT_EQ(starts.size(), 8u); + std::set> observations; + for (auto state : starts) { + const bool data = inputOrigin(model, state, "data[0]"); + const bool clock = inputOrigin(model, state, "enable[0]"); + const bool stored = step(model, state, 3, false).at("out[0]"); + observations.emplace(data, clock, stored); + EXPECT_EQ(step(model, state, 1, !clock).at("out[0]"), clock ? stored : data); + } + EXPECT_EQ(observations.size(), 8u); // Initial high clock did not reset/capture. +} + +TEST_F(LatchNetlistAdapterTests, Db0LatchFollowsOpenDataAndRetainsClosingValue) { + ScopedSupportOptions scope(options()); + const auto model = SequentialDesignModel::extract(directTop("top", NLDB0::getDLatch())); + ASSERT_FALSE(model.hasUnsupportedFeatures()); + ASSERT_EQ(model.observedOutputs.size(), 1u); + EXPECT_TRUE(model.skippedObservedOutputs.empty()); + EXPECT_FALSE(model.stateBits.empty()); + expectPublishedSupport(model); + auto state = initialState(model); + EXPECT_FALSE(step(model, state, 0, true).at("out[0]")); // Closed. + EXPECT_TRUE(step(model, state, 1, true).at("out[0]")); // Open. + EXPECT_FALSE(step(model, state, 0, false).at("out[0]")); + EXPECT_TRUE(step(model, state, 0, true).at("out[0]")); + EXPECT_TRUE(step(model, state, 1, false).at("out[0]")); // Close. + EXPECT_TRUE(step(model, state, 0, false).at("out[0]")); // Hold. +} + +TEST_F(LatchNetlistAdapterTests, AnyChangeLatchRaceRemainsOpaqueRatherThanSelectingOrder) { + ScopedSupportOptions scope(options(false)); + const auto model = SequentialDesignModel::extract(directTop("top", NLDB0::getDLatch())); + ASSERT_FALSE(model.hasUnsupportedFeatures()); + EXPECT_TRUE(model.observedOutputs.empty()); + ASSERT_EQ(model.skippedObservedOutputs.size(), 1u); + const auto& detail = model.connectivitySkipInfoByKey.at(model.skippedObservedOutputs.front()).detail; + EXPECT_NE(detail.find("order"), std::string::npos); +} + +TEST_F(LatchNetlistAdapterTests, Db0FlipFlopConsumesClockEventOnlyOnce) { + ScopedSupportOptions scope(options()); + const auto model = SequentialDesignModel::extract(directTop("top", NLDB0::getDFF(), "C")); + ASSERT_EQ(model.observedOutputs.size(), 1u); + auto state = initialState(model); + EXPECT_FALSE(step(model, state, 0, true).at("out[0]")); + EXPECT_TRUE(step(model, state, 1, true).at("out[0]")); + EXPECT_TRUE(step(model, state, 0, false).at("out[0]")); + EXPECT_TRUE(step(model, state, 1, false).at("out[0]")); + EXPECT_FALSE(step(model, state, 1, true).at("out[0]")); +} + +TEST_F(LatchNetlistAdapterTests, InvertedPhysicalOutputDoesNotInvertRememberedStorage) { + ScopedSupportOptions scope(options()); + const auto model = SequentialDesignModel::extract(directTop("top", explicitLatch("inverse", true))); + ASSERT_EQ(model.observedOutputs.size(), 1u); + auto state = initialState(model); + EXPECT_TRUE(step(model, state, 3, false).at("out[0]")); + EXPECT_TRUE(step(model, state, 0, true).at("out[0]")); + EXPECT_FALSE(step(model, state, 1, true).at("out[0]")); + EXPECT_FALSE(step(model, state, 1, false).at("out[0]")); + EXPECT_FALSE(step(model, state, 0, false).at("out[0]")); +} + +TEST_F(LatchNetlistAdapterTests, ComplementaryOutputsCommitConsistentlyAndHavePublishedSymbols) { + ScopedSupportOptions scope(options()); + const auto model = SequentialDesignModel::extract(directTop("top", explicitLatch("pair", false, true))); + ASSERT_EQ(model.observedOutputs.size(), 2u); + expectPublishedSupport(model); + auto state = initialState(model); + for (const auto event : {std::pair{3, false}, {0, true}, {1, true}, {1, false}, {0, false}}) { + const auto outputs = step(model, state, event.first, event.second); + EXPECT_NE(outputs.at("out[0]"), outputs.at("out_n[0]")); + } +} + +TEST_F(LatchNetlistAdapterTests, InputDependentPhysicalOutputTracksClockGatePhase) { + ScopedSupportOptions scope(options()); + const auto model = SequentialDesignModel::extract(directTop("top", explicitLatch("icg", false, false, true))); + ASSERT_EQ(model.observedOutputs.size(), 1u); + auto state = initialState(model); + EXPECT_FALSE(step(model, state, 0, true).at("out[0]")); // Enable remembered during low carrier. + EXPECT_TRUE(step(model, state, 1, true).at("out[0]")); + EXPECT_TRUE(step(model, state, 0, false).at("out[0]")); // Held during high carrier. + EXPECT_FALSE(step(model, state, 1, false).at("out[0]")); + EXPECT_FALSE(step(model, state, 1, true).at("out[0]")); +} + +TEST_F(LatchNetlistAdapterTests, LatchGeneratedClockDrivesFlipFlopThroughInternalEvents) { + auto* top = SNLDesign::create(designs, SNLDesign::Type::Standard, NLName("clock_gated")); + auto* clock = port(top, "clock", SNLTerm::Direction::Input); + auto* data = port(top, "data", SNLTerm::Direction::Input); + auto* gate = port(top, "gate", SNLTerm::Direction::Input); + auto* output = port(top, "out", SNLTerm::Direction::Output); + auto* generatedClock = SNLScalarNet::create(top, NLName("generated_clock")); + auto* gateModel = explicitLatch("clock_gate", false, false, true); + auto* gateCell = SNLInstance::create(top, gateModel, NLName("gate_cell")); + gateCell->getInstTerm(gateModel->getScalarTerm(NLName("D")))->setNet(gate); + gateCell->getInstTerm(gateModel->getScalarTerm(NLName("E")))->setNet(clock); + gateCell->getInstTerm(gateModel->getScalarTerm(NLName("Q")))->setNet(generatedClock); + auto* flop = SNLInstance::create(top, NLDB0::getDFF(), NLName("flop")); + flop->getInstTerm(NLDB0::getDFFData())->setNet(data); + flop->getInstTerm(NLDB0::getDFFClock())->setNet(generatedClock); + flop->getInstTerm(NLDB0::getDFFOutput())->setNet(output); + + ScopedSupportOptions scope(options()); + const auto model = SequentialDesignModel::extract(top); + ASSERT_FALSE(model.hasUnsupportedFeatures()); + ASSERT_EQ(model.observedOutputs.size(), 1u); + EXPECT_TRUE(model.skippedObservedOutputs.empty()); + expectPublishedSupport(model); + auto state = initialState(model); + // Selector order follows the common alphabetical interface: clock, data, gate. + EXPECT_FALSE(step(model, state, 1, true).at("out[0]")); + EXPECT_FALSE(step(model, state, 2, true).at("out[0]")); + EXPECT_TRUE(step(model, state, 0, true).at("out[0]")); // Generated rising edge captures 1. + EXPECT_TRUE(step(model, state, 1, false).at("out[0]")); // Data change is not another edge. + EXPECT_TRUE(step(model, state, 2, false).at("out[0]")); + EXPECT_TRUE(step(model, state, 0, false).at("out[0]")); + EXPECT_TRUE(step(model, state, 0, true).at("out[0]")); // Gate disabled: no generated edge. + EXPECT_TRUE(step(model, state, 2, true).at("out[0]")); // High-phase gate change stays held. + EXPECT_TRUE(step(model, state, 0, false).at("out[0]")); + EXPECT_FALSE(step(model, state, 0, true).at("out[0]")); // Next generated edge captures 0. +} + +TEST_F(LatchNetlistAdapterTests, StateDependentLatchDataIsOpaqueNotFalseSettling) { + auto* primitive = explicitLatch("hidden_feedback"); + auto sequential = Modeling::getSequentialModel(primitive); + sequential.clockedOn = Expression{}; + sequential.clockedOn.root = sequential.clockedOn.addConstant(true); + sequential.states.front().nextState = stateExpression(true); + Modeling::setSequentialModel(primitive, sequential); + ScopedSupportOptions scope(options()); + const auto model = SequentialDesignModel::extract(directTop("top", primitive)); + EXPECT_TRUE(model.observedOutputs.empty()); + EXPECT_EQ(model.skippedObservedOutputs.size(), 1u); +} + +TEST_F(LatchNetlistAdapterTests, StateDependentAsyncControlIsOpaqueForLatchAndFlop) { + for (bool flop : {false, true}) { + auto* primitive = explicitLatch(flop ? "hidden_ff_clear" : "hidden_latch_clear"); + auto sequential = Modeling::getSequentialModel(primitive); + if (flop) sequential.kind = Modeling::SequentialModel::Kind::FlipFlop; + sequential.states.front().clear = stateExpression(true); + Modeling::setSequentialModel(primitive, sequential); + ScopedSupportOptions scope(options()); + const auto model = SequentialDesignModel::extract(directTop(flop ? "ff_top" : "latch_top", primitive)); + EXPECT_TRUE(model.observedOutputs.empty()); + EXPECT_EQ(model.skippedObservedOutputs.size(), 1u); + } +} + +TEST_F(LatchNetlistAdapterTests, UnsupportedLoopDoesNotDiscardIndependentSupportedOutput) { + auto* top = directTop("top", NLDB0::getDLatch()); + auto* enable = top->getScalarNet(NLName("enable")); + auto* loop = SNLScalarNet::create(top, NLName("loop")); + auto* inverted = SNLScalarNet::create(top, NLName("inverted")); + auto* bad = port(top, "bad", SNLTerm::Direction::Output); + auto* invModel = inverterModel(); + auto* inv = SNLInstance::create(top, invModel, NLName("inv")); + inv->getInstTerm(invModel->getScalarTerm(NLName("A")))->setNet(loop); + inv->getInstTerm(invModel->getScalarTerm(NLName("Y")))->setNet(inverted); + auto* latch = SNLInstance::create(top, NLDB0::getDLatch(), NLName("bad_latch")); + latch->getInstTerm(NLDB0::getDLatchEnable())->setNet(enable); + latch->getInstTerm(NLDB0::getDLatchData())->setNet(inverted); + latch->getInstTerm(NLDB0::getDLatchOutput())->setNet(loop); + top->getScalarTerm(NLName("bad"))->setNet(loop); + (void)bad; + ScopedSupportOptions scope(options()); + const auto model = SequentialDesignModel::extract(top); + ASSERT_FALSE(model.hasUnsupportedFeatures()); + ASSERT_EQ(model.observedOutputs.size(), 1u); + ASSERT_EQ(model.skippedObservedOutputs.size(), 1u); + EXPECT_EQ(model.displayNameByKey.at(model.observedOutputs.front()), "out[0]"); + EXPECT_EQ(model.displayNameByKey.at(model.skippedObservedOutputs.front()), "bad[0]"); +} + +TEST_F(LatchNetlistAdapterTests, UnsupportedControlSourceMakesDependentComponentOpaque) { + auto* unknown = SNLDesign::create(primitives, SNLDesign::Type::Primitive, NLName("opaque_source")); + auto* sourceData = SNLScalarTerm::create(unknown, SNLTerm::Direction::Input, NLName("D")); + auto* sourceOut = SNLScalarTerm::create(unknown, SNLTerm::Direction::Output, NLName("Q")); + auto* top = directTop("top", NLDB0::getDLatch()); + auto* net = SNLScalarNet::create(top, NLName("control")); + auto* source = SNLInstance::create(top, unknown, NLName("source")); + source->getInstTerm(sourceData)->setNet(top->getScalarNet(NLName("enable"))); + source->getInstTerm(sourceOut)->setNet(net); + top->getInstance(NLName("cell"))->getInstTerm(NLDB0::getDLatchEnable())->setNet(net); + ScopedSupportOptions scope(options()); + const auto model = SequentialDesignModel::extract(top); + EXPECT_TRUE(model.observedOutputs.empty()); + ASSERT_EQ(model.skippedObservedOutputs.size(), 1u); + EXPECT_NE(model.connectivitySkipInfoByKey.at(model.skippedObservedOutputs[0]).detail.find("source"), std::string::npos); +} + +TEST_F(LatchNetlistAdapterTests, ExtractionRestoresBorrowedTopAndFlattenedGraph) { + auto* saved = directTop("saved", NLDB0::getDLatch()); + auto* target = directTop("target", NLDB0::getDLatch()); + NLUniverse::get()->setTopDesign(saved); + auto* dnl = naja::DNL::get(); + ScopedSupportOptions scope(options()); + const auto model = SequentialDesignModel::extract(target); + EXPECT_EQ(model.observedOutputs.size(), 1u); + EXPECT_EQ(NLUniverse::get()->getTopDesign(), saved); + EXPECT_EQ(naja::DNL::get(), dnl); +} + +TEST_F(LatchNetlistAdapterTests, ExtractionRestoresNondefaultSourceOrderingIds) { + auto* primitive = explicitLatch("ordering"); + auto* top = directTop("top", primitive); + std::vector> orders; + NLID::DesignObjectID order = 51; + for (auto* design : {top, primitive}) { + for (auto* term : design->getBitTerms()) { + term->setOrderID(order++); + orders.emplace_back(term, term->getOrderID()); + } + } + auto* instance = top->getInstance(NLName("cell")); + instance->setOrderID(77); + const auto* previousDb = NLUniverse::get()->getTopDB(); + const auto* previousTop = top->getDB()->getTopDesign(); + ScopedSupportOptions scope(options()); + const auto model = extractEventDesign(top, {}, 0); + ASSERT_TRUE(model.has_value()); + EXPECT_EQ(model->observedOutputs.size(), 1u); + for (const auto& [term, savedOrder] : orders) EXPECT_EQ(term->getOrderID(), savedOrder); + EXPECT_EQ(instance->getOrderID(), 77u); + EXPECT_EQ(NLUniverse::get()->getTopDB(), previousDb); + EXPECT_EQ(top->getDB()->getTopDesign(), previousTop); +} + +TEST_F(LatchNetlistAdapterTests, LatchFreeSideUsesSameEventEnvironmentForComparison) { + auto* top = SNLDesign::create(designs, SNLDesign::Type::Standard, NLName("wire")); + auto* data = port(top, "data", SNLTerm::Direction::Input); + port(top, "enable", SNLTerm::Direction::Input); + auto* output = SNLScalarTerm::create(top, SNLTerm::Direction::Output, NLName("out")); + output->setNet(data); + ScopedSupportOptions scope(options()); + const auto model = SequentialDesignModel::extract(top); + ASSERT_EQ(model.observedOutputs.size(), 1u); + EXPECT_FALSE(model.stateBits.empty()); + auto state = initialState(model); + EXPECT_TRUE(step(model, state, 0, true).at("out[0]")); + EXPECT_TRUE(step(model, state, 1, true).at("out[0]")); +} + +TEST_F(LatchNetlistAdapterTests, ProofEnginesAcceptSelfEquivalentLatchInBothEncodings) { + auto* first = directTop("first", NLDB0::getDLatch()); + auto* second = directTop("second", NLDB0::getDLatch()); + ScopedSupportOptions scope(options()); + for (auto engine : {SecEngine::KInduction, SecEngine::Imc, SecEngine::Pdr}) { + for (auto encoding : {SecEncoding::Binary, SecEncoding::DualRailSteady}) { + SequentialEquivalenceStrategy strategy(first, second, Config::SolverType::KISSAT, engine, encoding); + const auto result = strategy.run(16); + EXPECT_EQ(result.status, SequentialEquivalenceStatus::Equivalent) << result.reason; + EXPECT_EQ(result.coveredOutputs, 1u); + } + } +} + +TEST_F(LatchNetlistAdapterTests, ProofEnginesShareUnknownInitialInputsButNotIndependentStorage) { + auto* first = directTop("first", NLDB0::getDLatch()); + auto* second = directTop("second", NLDB0::getDLatch()); + for (bool unknownStorage : {false, true}) { + auto setting = options(); + if (unknownStorage) setting.initialStorage.reset(); + else setting.initialInputs.reset(); + ScopedSupportOptions scope(setting); + for (auto engine : {SecEngine::KInduction, SecEngine::Imc, SecEngine::Pdr}) { + for (auto encoding : {SecEncoding::Binary, SecEncoding::DualRailSteady}) { + SCOPED_TRACE(::testing::Message() << "unknownStorage=" << unknownStorage + << " engine=" << int(engine) << " encoding=" << int(encoding)); + SequentialEquivalenceStrategy strategy(first, second, Config::SolverType::KISSAT, engine, encoding); + const auto result = strategy.run(16); + // Fixed storage with shared arbitrary D/E is equivalent. Independently + // uninitialized, closed latches can differ before any capture/reset. + EXPECT_EQ(result.status, unknownStorage ? SequentialEquivalenceStatus::Different + : SequentialEquivalenceStatus::Equivalent) << result.reason; + EXPECT_EQ(result.coveredOutputs, 1u); + } + } + } +} + +TEST_F(LatchNetlistAdapterTests, ProofEnginesFindDifferentPhysicalLatchOutputs) { + auto* first = directTop("first", NLDB0::getDLatch()); + auto* second = directTop("second", explicitLatch("inverted", true)); + ScopedSupportOptions scope(options()); + for (auto engine : {SecEngine::KInduction, SecEngine::Pdr}) { + SequentialEquivalenceStrategy strategy(first, second, Config::SolverType::KISSAT, engine, SecEncoding::Binary); + const auto result = strategy.run(8); + EXPECT_EQ(result.status, SequentialEquivalenceStatus::Different) << result.reason; + EXPECT_NE(result.reason.find("event transaction"), std::string::npos); + EXPECT_NE(result.reason.find("Event contract: boolean-epochs-v1"), std::string::npos); + EXPECT_EQ(result.reason.find("at cycle"), std::string::npos); + } +} + +TEST_F(LatchNetlistAdapterTests, SameSizeDifferentNamedInterfacesCannotShareSelectorAccidentally) { + auto* first = directTop("first", NLDB0::getDLatch()); + auto* second = directTop("second", NLDB0::getDLatch()); + second->getScalarTerm(NLName("data"))->setName(NLName("different_data")); + ScopedSupportOptions scope(options()); + SequentialEquivalenceStrategy strategy(first, second, Config::SolverType::KISSAT, + SecEngine::KInduction, SecEncoding::Binary); + EXPECT_THROW(strategy.run(8), std::runtime_error); +} + +TEST_F(LatchNetlistAdapterTests, CopiedPrimitiveReactionsSurviveReleaseOfNajaNetlist) { + auto* top = directTop("top", explicitLatch("copied", false, true)); + auto* instance = top->getInstance(NLName("cell")); + std::map netByTerm; + Network network; + for (auto* term : instance->getModel()->getBitTerms()) { + netByTerm.emplace(term, network.netCount); + if (term->getDirection() == SNLTerm::Direction::Input) + network.externalInputs.push_back(network.netCount); + ++network.netCount; + } + network.primitives.push_back(makeNajaEventPrimitive(instance, "cell", netByTerm)); + NLUniverse::get()->destroy(); + EventModel model(network, {}, 2); + auto state = model.bootstrap({1, 1}, {{0}}, Bits(network.netCount)); + for (size_t wave = 0; !model.stable(state) && wave < 16; ++wave) + state = model.successors(state).front(); + ASSERT_TRUE(model.stable(state)); + const auto& outputs = network.primitives.front().outputs; + EXPECT_EQ(state.current[outputs[0]], 1); + EXPECT_EQ(state.current[outputs[1]], 0); +} + +TEST_F(LatchNetlistAdapterTests, ConflictingConstantAnnotationsStayOpaqueWithExactlyOneDriver) { + auto* top = directTop("top", NLDB0::getDLatch()); + auto* data = top->getScalarNet(NLName("data")); + data->setType(SNLNet::Type::Assign0); + auto* child = SNLDesign::create(designs, SNLDesign::Type::Standard, NLName("annotation")); + auto* input = SNLScalarTerm::create(child, SNLTerm::Direction::Input, NLName("i")); + auto* childNet = SNLScalarNet::create(child, NLName("conflicting_one")); + childNet->setType(SNLNet::Type::Assign1); + input->setNet(childNet); + auto* instance = SNLInstance::create(top, child, NLName("annotation")); + instance->getInstTerm(input)->setNet(data); + + NLUniverse::get()->setTopDesign(top); + const auto* dnl = naja::DNL::get(); + const auto& terminal = dnl->getTop().getTerminalFromBitTerm(top->getScalarTerm(NLName("data"))); + const auto& iso = dnl->getDNLIsoDB().getIsoFromIsoIDconst(terminal.getIsoID()); + ASSERT_EQ(iso.getType(), naja::DNL::DNLIso::AMBIGUOUS); + ASSERT_EQ(iso.getDrivers().size(), 1u); // Driver-count validation alone misses this case. + + ScopedSupportOptions scope(options()); + const auto model = SequentialDesignModel::extract(top); + EXPECT_TRUE(model.observedOutputs.empty()); + ASSERT_EQ(model.skippedObservedOutputs.size(), 1u); + EXPECT_NE(model.connectivitySkipInfoByKey.at(model.skippedObservedOutputs.front()).detail.find( + "conflicting constant"), std::string::npos); +} + +TEST_F(LatchNetlistAdapterTests, DuplicateSequentialOutputAssociationsAreNotChosenArbitrarily) { + auto* primitive = explicitLatch("duplicate_outputs"); + auto sequential = Modeling::getSequentialModel(primitive); + sequential.outputs.push_back({sequential.outputs.front().term, stateExpression(true)}); + Modeling::setSequentialModel(primitive, sequential); + ScopedSupportOptions scope(options()); + const auto model = SequentialDesignModel::extract(directTop("top", primitive)); + EXPECT_TRUE(model.observedOutputs.empty()); + ASSERT_EQ(model.skippedObservedOutputs.size(), 1u); + EXPECT_NE(model.connectivitySkipInfoByKey.at(model.skippedObservedOutputs.front()).detail.find( + "duplicate sequential output"), std::string::npos); +} + +TEST_F(LatchNetlistAdapterTests, ReachableClearPresetToggleConflictIsOpaqueNotFalseSettling) { + for (bool flop : {false, true}) { + auto* primitive = explicitLatch(flop ? "toggle_flop" : "toggle_latch"); + auto sequential = Modeling::getSequentialModel(primitive); + if (flop) sequential.kind = Modeling::SequentialModel::Kind::FlipFlop; + sequential.states.front().clear = termExpression(primitive->getScalarTerm(NLName("D"))); + sequential.states.front().preset = termExpression(primitive->getScalarTerm(NLName("E"))); + sequential.states.front().clearPresetValue = Modeling::SequentialState::ClearPresetValue::Toggle; + Modeling::setSequentialModel(primitive, sequential); + ScopedSupportOptions scope(options()); + const auto model = SequentialDesignModel::extract(directTop(flop ? "ff_top" : "latch_top", primitive)); + EXPECT_TRUE(model.observedOutputs.empty()); + ASSERT_EQ(model.skippedObservedOutputs.size(), 1u); + EXPECT_NE(model.connectivitySkipInfoByKey.at(model.skippedObservedOutputs.front()).detail.find( + "toggle"), std::string::npos); + } +} + +TEST_F(LatchNetlistAdapterTests, UnreachableToggleConflictDoesNotRejectUsableCell) { + auto* primitive = explicitLatch("never_conflicting"); + auto sequential = Modeling::getSequentialModel(primitive); + sequential.states.front().clear = termExpression(primitive->getScalarTerm(NLName("D"))); + auto preset = *sequential.states.front().clear; + preset.root = preset.addOperation(Operator::Not, {preset.root}); + sequential.states.front().preset = preset; + sequential.states.front().clearPresetValue = Modeling::SequentialState::ClearPresetValue::Toggle; + Modeling::setSequentialModel(primitive, sequential); + ScopedSupportOptions scope(options()); + const auto model = SequentialDesignModel::extract(directTop("top", primitive)); + ASSERT_EQ(model.observedOutputs.size(), 1u); + EXPECT_TRUE(model.skippedObservedOutputs.empty()); + auto state = initialState(model); + EXPECT_TRUE(step(model, state, 3, false).at("out[0]")); + EXPECT_FALSE(step(model, state, 0, true).at("out[0]")); +} + +TEST_F(LatchNetlistAdapterTests, ExtractedModelsCannotMixEventAndCycleContracts) { + ScopedSupportOptions scope(options()); + const auto eventModel = SequentialDesignModel::extract(directTop("top", NLDB0::getDLatch())); + ASSERT_FALSE(eventModel.eventContract.empty()); + auto legacyModel = eventModel; + legacyModel.eventContract.clear(); + SequentialEquivalenceStrategy strategy(nullptr, nullptr, Config::SolverType::KISSAT, + SecEngine::KInduction, SecEncoding::Binary); + const auto result = strategy.runExtractedModels(eventModel, legacyModel, 8); + EXPECT_EQ(result.status, SequentialEquivalenceStatus::Unsupported); + EXPECT_NE(result.reason.find("different clock/event"), std::string::npos); +} + +TEST_F(LatchNetlistAdapterTests, ExtractedModelsCannotMixBooleanInitializationContracts) { + auto* top = directTop("top", NLDB0::getDLatch()); + SequentialDesignModel zero, one; + { + ScopedSupportOptions scope(options()); + zero = SequentialDesignModel::extract(top); + } + { + auto setting = options(); + setting.initialStorage = true; + ScopedSupportOptions scope(setting); + one = SequentialDesignModel::extract(top); + } + SequentialEquivalenceStrategy strategy(nullptr, nullptr, Config::SolverType::KISSAT, + SecEngine::KInduction, SecEncoding::Binary); + const auto result = strategy.runExtractedModels(zero, one, 8); + EXPECT_EQ(result.status, SequentialEquivalenceStatus::Unsupported); + EXPECT_NE(result.reason.find("initialization contracts"), std::string::npos); +} + +TEST_F(LatchNetlistAdapterTests, ResetCyclesRequireDiscoverableClockNotAnArbitraryLatchEnable) { + ScopedSupportOptions scope(options()); + const auto model = SequentialDesignModel::extract(directTop("top", NLDB0::getDLatch())); + SequentialEquivalenceStrategy strategy(nullptr, nullptr, Config::SolverType::KISSAT, + SecEngine::KInduction, SecEncoding::Binary, SecResetSpec{1, {{"data", false}}}); + const auto result = strategy.runExtractedModels(model, model, 8); + EXPECT_EQ(result.status, SequentialEquivalenceStatus::Unsupported); + EXPECT_NE(result.reason.find("clock"), std::string::npos); +} + +TEST_F(LatchNetlistAdapterTests, Btor2ExportRetainsEventStepAndInitializationMetadata) { + struct TemporaryDirectory { + std::filesystem::path path; + TemporaryDirectory() { + for (size_t attempt = 0; attempt < 32; ++attempt) { + path = std::filesystem::temp_directory_path() / + ("kf-event-metadata-" + std::to_string(std::random_device{}()) + "-" + std::to_string(attempt)); + if (std::filesystem::create_directory(path)) return; + } + throw std::runtime_error("Cannot create event export test directory"); + } + ~TemporaryDirectory() { + std::error_code ignored; + std::filesystem::remove_all(path, ignored); + } + } directory; + ScopedSupportOptions scope(options()); + const auto model = SequentialDesignModel::extract(directTop("top", NLDB0::getDLatch())); + const auto output = directory.path / "events.btor2"; + SequentialEquivalenceStrategy strategy(nullptr, nullptr, Config::SolverType::KISSAT, + SecEngine::KInduction, SecEncoding::Binary, {}, Btor2ExportOptions{output.string(), true}); + const auto result = strategy.runExtractedModels(model, model, 8); + ASSERT_EQ(result.status, SequentialEquivalenceStatus::Exported) << result.reason; + std::ifstream file(output); + ASSERT_TRUE(file.good()); + std::ostringstream contents; + contents << file.rdbuf(); + EXPECT_NE(contents.str().find("step_semantics=" + model.eventContract), std::string::npos); + EXPECT_NE(contents.str().find("initial_inputs=0;initial_storage=0"), std::string::npos); +} + +} // namespace +} // namespace KEPLER_FORMAL::SEC::LATCH diff --git a/test/sec/LatchResetAdapterTests.cpp b/test/sec/LatchResetAdapterTests.cpp new file mode 100644 index 00000000..0786facd --- /dev/null +++ b/test/sec/LatchResetAdapterTests.cpp @@ -0,0 +1,430 @@ +// Copyright 2026 keplertech.io +// SPDX-License-Identifier: Apache-2.0 + +#include + +#include +#include +#include + +#include "BoolExprCache.h" +#include "latch/LatchResetAdapter.h" +#include "strategy/SequentialEquivalenceStrategy.h" + +namespace KEPLER_FORMAL::SEC::LATCH { +namespace { +using Values = std::unordered_map; + +BoolExpr* choose(BoolExpr* condition, BoolExpr* yes, BoolExpr* no) { + return BoolExpr::Or(BoolExpr::And(condition, yes), BoolExpr::And(BoolExpr::Not(condition), no)); +} +SignalKey key(size_t group, size_t index) { return {{group, index}, {0}}; } + +// Four remembered PIs plus resettable FF, non-resettable FF and open latch. +// This direct event relation makes the reset wrapper independently testable, +// without relying on Naja extraction or the settling compiler to build its oracle. +SequentialDesignModel synthetic(bool single, bool activeHigh = true, bool falling = false) { + SequentialDesignModel model; + model.eventContract = single ? "test-events;single" : "test-events;any"; + auto interface = std::make_shared(); + interface->singleInputChange = single; + interface->clockInputIndex = 0; + interface->inputNames = {"clock[0]", "data[0]", "enable[0]", "reset[0]"}; + for (size_t i = 0; i < 4; ++i) { + const auto inputKey = key(0, i); + model.environmentInputs.push_back(inputKey); + model.inputVarByKey.emplace(inputKey, i + 2); + model.displayNameByKey.emplace(inputKey, interface->inputNames[i]); + interface->inputKeys.push_back(inputKey); + interface->currentInputs.push_back(BoolExpr::Var(20 + i)); + } + if (single) { + for (size_t i = 0; i < 4; ++i) { + const auto inputKey = key(1, i); + model.environmentInputs.push_back(inputKey); + model.inputVarByKey.emplace(inputKey, 6 + i); + model.displayNameByKey.emplace(inputKey, "selector" + std::to_string(i)); + if (i < 3) interface->selectorSymbols.push_back(6 + i); + else interface->valueSymbol = 9; + } + } + std::vector inputs; + for (size_t i = 0; i < 4; ++i) { + auto* value = BoolExpr::Var(2 + i); + if (single) { + auto* selected = BoolExpr::createTrue(); + for (size_t bit = 0; bit < 3; ++bit) { + auto* variable = BoolExpr::Var(6 + bit); + selected = BoolExpr::And(selected, ((i >> bit) & 1) ? variable : BoolExpr::Not(variable)); + } + value = choose(selected, BoolExpr::Var(9), BoolExpr::Var(20 + i)); + } + inputs.push_back(value); + } + auto* edge = falling ? BoolExpr::And(BoolExpr::Var(20), BoolExpr::Not(inputs[0])) + : BoolExpr::And(BoolExpr::Not(BoolExpr::Var(20)), inputs[0]); + auto* reset = activeHigh ? inputs[3] : BoolExpr::Not(inputs[3]); + inputs.push_back(choose(edge, choose(reset, BoolExpr::createFalse(), inputs[1]), BoolExpr::Var(24))); + inputs.push_back(choose(edge, inputs[1], BoolExpr::Var(25))); + inputs.push_back(choose(inputs[2], inputs[1], BoolExpr::Var(26))); + for (size_t i = 0; i < inputs.size(); ++i) { + const auto stateKey = key(2, i); + model.stateBits.push_back(stateKey); + model.inputVarByKey.emplace(stateKey, 20 + i); + model.nextStateExprByStateKey.emplace(stateKey, inputs[i]); + model.initialStateValueByKey.emplace(stateKey, i >= 4); + model.displayNameByKey.emplace(stateKey, "state" + std::to_string(i)); + } + for (size_t i = 0; i < 3; ++i) { + const auto outputKey = key(3, i); + model.observedOutputs.push_back(outputKey); + model.allObservedOutputs.push_back(outputKey); + model.observedOutputExprByKey.emplace(outputKey, inputs[4 + i]); + model.displayNameByKey.emplace(outputKey, "out" + std::to_string(i)); + } + model.eventResetInterface = std::move(interface); + return model; +} + +Values initial(const SequentialDesignModel& model) { + Values result; + for (const auto& state : model.stateBits) + result.emplace(model.inputVarByKey.at(state), model.initialStateValueByKey.at(state)); + return result; +} + +Values transaction(bool single, size_t selected, bool value, bool clock = false, + bool data = false, bool enable = false, bool reset = false) { + Values result{{2, clock}, {3, data}, {4, enable}, {5, reset}}; + if (single) { + for (size_t bit = 0; bit < 3; ++bit) result.emplace(6 + bit, (selected >> bit) & 1); + result.emplace(9, value); + } + return result; +} + +std::vector step(const SequentialDesignModel& model, Values& state, const Values& input) { + auto environment = state; + environment.insert(input.begin(), input.end()); + // New reset-order inputs default to code zero unless a test selects an order. + for (const auto& key : model.environmentInputs) + environment.try_emplace(model.inputVarByKey.at(key), false); + std::vector outputs; + for (const auto& key : model.observedOutputs) + outputs.push_back(model.observedOutputExprByKey.at(key)->evaluate(environment)); + for (const auto& key : model.stateBits) + state[model.inputVarByKey.at(key)] = model.nextStateExprByStateKey.at(key)->evaluate(environment); + return outputs; +} + +size_t remaining(const SequentialDesignModel& model, const Values& state) { + size_t result = 0; + for (const auto& key : model.stateBits) { + const auto& name = model.displayNameByKey.at(key); + const std::string prefix = "$event.reset.remaining["; + if (name.find(prefix) == 0 && state.at(model.inputVarByKey.at(key))) + result |= size_t(1) << std::stoul(name.substr(prefix.size())); + } + return result; +} + +class LatchResetAdapterTests : public ::testing::Test { + protected: + void TearDown() override { BoolExprCache::destroy(); } +}; + +TEST_F(LatchResetAdapterTests, PrefixRunsExactCyclesSamplesSymbolicDataAndReleasesReset) { + for (const bool single : {false, true}) { + for (const bool activeHigh : {false, true}) { + for (const bool falling : {false, true}) { + SCOPED_TRACE(::testing::Message() << "single=" << single << " active=" << activeHigh + << " falling=" << falling); + const auto original = synthetic(single, activeHigh, falling); + const auto adapted = adaptResetCycles(original, {3, {{"reset", activeHigh}}}); + ASSERT_TRUE(adapted.model) << adapted.error; + const auto& model = *adapted.model; + EXPECT_EQ(model.eventResetCycles, 3u); + EXPECT_NE(model.eventContract.find("reset_protocol=low-sample-high-low-release"), std::string::npos); + EXPECT_EQ(original.stateBits.size(), 7u); + EXPECT_EQ(original.eventResetCycles, 0u); + EXPECT_TRUE(original.eventResetInterface); + auto state = initial(model); + for (size_t cycle = 0; cycle < 3; ++cycle) { + const bool data = cycle != 1; + EXPECT_EQ(remaining(model, state), 3u - cycle); + EXPECT_EQ(step(model, state, transaction(single, 1, data, true, data, false, !activeHigh)), + (std::vector{false, false, false})); + EXPECT_FALSE(state.at(20)); // full cycle ends at low carrier + EXPECT_EQ(state.at(21), data); // arbitrary sample was retained + EXPECT_EQ(state.at(23), cycle == 2 ? !activeHigh : activeHigh); + EXPECT_FALSE(state.at(24)); // synchronous reset observed a real edge + EXPECT_EQ(state.at(25), data); // non-reset FF captures the same free sample + } + EXPECT_EQ(remaining(model, state), 0u); + const auto outputs = step(model, state, transaction(single, 4, false, false, true, false, activeHigh)); + EXPECT_EQ(outputs, (std::vector{false, true, true})); + EXPECT_EQ(remaining(model, state), 0u); + EXPECT_EQ(state.at(23), !activeHigh); + } + } + } +} + +TEST_F(LatchResetAdapterTests, ResetAndClockSelectorsCannotOverridePrefixAndResetStaysInactiveAfterward) { + const auto result = adaptResetCycles(synthetic(true), {2, {{"reset", true}}}); + ASSERT_TRUE(result.model) << result.error; + auto state = initial(*result.model); + step(*result.model, state, transaction(true, 0, true)); // blocked during sample + EXPECT_FALSE(state.at(20)); + EXPECT_TRUE(state.at(23)); + step(*result.model, state, transaction(true, 3, false)); + EXPECT_FALSE(state.at(20)); + EXPECT_FALSE(state.at(23)); + step(*result.model, state, transaction(true, 3, true)); // reset permanently inactive + EXPECT_FALSE(state.at(23)); + step(*result.model, state, transaction(true, 1, true)); + const auto output = step(*result.model, state, transaction(true, 0, true)); + EXPECT_TRUE(output[0]); // post-prefix real clock events are no longer blocked + EXPECT_TRUE(output[1]); + EXPECT_TRUE(state.at(20)); +} + +TEST_F(LatchResetAdapterTests, LatchTransparencyContinuesDuringPrefixAndHoldAfterRelease) { + for (const bool single : {false, true}) { + const auto result = adaptResetCycles(synthetic(single), {3, {{"reset[0]", true}}}); + ASSERT_TRUE(result.model) << result.error; + auto state = initial(*result.model); + step(*result.model, state, transaction(single, 2, true, false, false, true)); + EXPECT_FALSE(state.at(26)); // open while D=0 + step(*result.model, state, transaction(single, 1, true, false, true, true)); + EXPECT_TRUE(state.at(26)); + step(*result.model, state, transaction(single, 2, false, false, true, false)); + EXPECT_TRUE(state.at(26)); + const auto output = step(*result.model, state, transaction(single, 1, false, false, false, false)); + EXPECT_TRUE(output[2]); + EXPECT_TRUE(state.at(26)); + } +} + +TEST_F(LatchResetAdapterTests, FirstNormalObservationIsNotMaskedAfterOneCycle) { + const auto result = adaptResetCycles(synthetic(false), {1, {{"reset", true}}}); + ASSERT_TRUE(result.model) << result.error; + auto state = initial(*result.model); + EXPECT_EQ(step(*result.model, state, transaction(false, 0, false, false, false, false)), + (std::vector{false, false, false})); + EXPECT_EQ(step(*result.model, state, transaction(false, 0, false, true, true, true, true)), + (std::vector{true, true, true})); +} + +TEST_F(LatchResetAdapterTests, ComposedRelationMatchesExplicitCertifiedEventSequenceForEverySample) { + for (const bool single : {false, true}) { + for (const bool activeHigh : {false, true}) { + for (const bool falling : {false, true}) { + const auto source = synthetic(single, activeHigh, falling); + const auto result = adaptResetCycles(source, {2, {{"reset", activeHigh}}}); + ASSERT_TRUE(result.model) << result.error; + // Enumerate all data/enable levels and all reset-order input encodings. + // Then compare ALL retained source state, not merely observed outputs. + for (size_t sample = 0; sample < (single ? 16u : 4u); ++sample) { + auto actual = initial(*result.model); + auto reference = initial(source); + for (size_t cycle = 0; cycle < 2; ++cycle) { + const auto force = [&](size_t pin, bool value) { + auto input = transaction(single, pin, value, reference.at(20), + reference.at(21), reference.at(22), reference.at(23)); + if (!single) input[2 + pin] = value; + step(source, reference, input); + }; + force(3, activeHigh); + force(0, false); + auto input = transaction(single, 0, false, true, sample & 1, sample & 2, !activeHigh); + auto sampled = input; + if (single) { + for (const auto& key : result.model->environmentInputs) { + const auto& name = result.model->displayNameByKey.at(key); + if (name == "$event.reset.order[0][0]") input[result.model->inputVarByKey.at(key)] = sample & 4; + if (name == "$event.reset.order[1][0]") input[result.model->inputVarByKey.at(key)] = sample & 8; + } + const size_t first = sample & 4 ? 2 : 1; + const size_t second = first == 1 ? 2 : 1; + force(first, input.at(2 + first)); + force(second, input.at(2 + second)); + } else { + sampled[2] = false; + sampled[5] = activeHigh; + step(source, reference, sampled); + } + force(0, true); + force(0, false); + if (cycle == 1) force(3, !activeHigh); + EXPECT_EQ(step(*result.model, actual, input), (std::vector{false, false, false})); + for (const auto& key : source.stateBits) { + const auto id = source.inputVarByKey.at(key); + EXPECT_EQ(actual.at(id), reference.at(id)) + << "single=" << single << " polarity=" << activeHigh << " falling=" << falling + << " sample=" << sample << " cycle=" << cycle << " variable=" << id; + } + } + } + } + } + } +} + +TEST_F(LatchResetAdapterTests, AllNoncontrolLevelsAreFreeBeforeFirstResetClock) { + auto source = synthetic(true); + auto* nextClock = source.nextStateExprByStateKey.at(key(2, 0)); + auto* edge = BoolExpr::And(BoolExpr::Not(BoolExpr::Var(20)), nextClock); + auto* bothInputs = BoolExpr::And(source.nextStateExprByStateKey.at(key(2, 1)), + source.nextStateExprByStateKey.at(key(2, 2))); + auto* captured = choose(edge, bothInputs, BoolExpr::Var(25)); + source.nextStateExprByStateKey[key(2, 5)] = captured; + source.observedOutputExprByKey[key(3, 1)] = captured; + source.initialStateValueByKey[key(2, 5)] = false; + const auto result = adaptResetCycles(source, {1, {{"reset", true}}}); + ASSERT_TRUE(result.model) << result.error; + auto state = initial(*result.model); + step(*result.model, state, transaction(true, 4, false, false, true, true)); + EXPECT_TRUE(state.at(21)); + EXPECT_TRUE(state.at(22)); + EXPECT_TRUE(state.at(25)); // Both initially-low free pins became high before capture. +} + +TEST_F(LatchResetAdapterTests, BothArrivalOrdersRemainSharedEnvironmentChoices) { + auto source = synthetic(true); + source.initialStateValueByKey[key(2, 1)] = true; + source.initialStateValueByKey[key(2, 2)] = true; + const auto result = adaptResetCycles(source, {1, {{"reset", true}}}); + ASSERT_TRUE(result.model) << result.error; + for (const bool enableFirst : {false, true}) { + auto state = initial(*result.model); + auto input = transaction(true, 4, false, false, false, false); + for (const auto& key : result.model->environmentInputs) + if (result.model->displayNameByKey.at(key) == "$event.reset.order[0][0]") + input[result.model->inputVarByKey.at(key)] = enableFirst; + step(*result.model, state, input); + EXPECT_FALSE(state.at(21)); + EXPECT_FALSE(state.at(22)); + EXPECT_EQ(state.at(26), enableFirst); // E closes first: hold 1; D falls first: capture 0. + } +} + +TEST_F(LatchResetAdapterTests, CounterWidthAndSaturationDoNotAddOrDropCycles) { + for (const size_t cycles : {size_t(1), size_t(2), size_t(3), size_t(4), size_t(7), size_t(8), size_t(16)}) { + const auto result = adaptResetCycles(synthetic(true), {cycles, {{"reset", true}}}); + ASSERT_TRUE(result.model) << result.error; + auto state = initial(*result.model); + for (size_t i = 0; i < cycles + 2; ++i) { + EXPECT_EQ(remaining(*result.model, state), i < cycles ? cycles - i : 0u); + step(*result.model, state, transaction(true, 4, false)); + } + } +} + +TEST_F(LatchResetAdapterTests, RejectsMissingOrAmbiguousContractRatherThanGuessingClock) { + const SecResetSpec reset{2, {{"reset", true}}}; + auto model = synthetic(true); + model.eventResetInterface.reset(); + EXPECT_NE(adaptResetCycles(model, reset).error.find("metadata"), std::string::npos); + model = synthetic(true); + auto interface = std::make_shared(*model.eventResetInterface); + model.eventResetInterface = interface; + interface->clockInputIndex.reset(); + EXPECT_NE(adaptResetCycles(model, reset).error.find("unambiguous"), std::string::npos); + interface->clockError = "multiple clock carriers"; + EXPECT_EQ(adaptResetCycles(model, reset).error, "multiple clock carriers"); + EXPECT_FALSE(adaptResetCycles(synthetic(true), {0, {{"reset", true}}}).model); + EXPECT_FALSE(adaptResetCycles(synthetic(true), {std::numeric_limits::max(), {{"reset", true}}}).model); + EXPECT_FALSE(adaptResetCycles(synthetic(true), {1, {}}).model); + EXPECT_FALSE(adaptResetCycles(synthetic(true), {1, {{"reset", true}, {"enable", true}}}).model); + EXPECT_FALSE(adaptResetCycles(synthetic(true), {1, {{"missing", true}}}).model); + EXPECT_FALSE(adaptResetCycles(synthetic(true), {1, {{"clock", true}}}).model); +} + +TEST_F(LatchResetAdapterTests, RejectsMalformedRememberedInputsAndSelectorMetadata) { + const auto check = [](auto mutate) { + auto model = synthetic(true); + auto interface = std::make_shared(*model.eventResetInterface); + model.eventResetInterface = interface; + mutate(model, *interface); + const auto result = adaptResetCycles(model, {1, {{"reset", true}}}); + EXPECT_FALSE(result.model); + EXPECT_FALSE(result.error.empty()); + }; + check([](auto&, auto& i) { i.currentInputs.pop_back(); }); + check([](auto&, auto& i) { i.currentInputs[0] = nullptr; }); + check([](auto&, auto& i) { i.currentInputs[0] = BoolExpr::Var(6); }); + check([](auto&, auto& i) { i.selectorSymbols = {6}; }); + check([](auto&, auto& i) { i.selectorSymbols = {6, 6, 8}; }); + check([](auto&, auto& i) { i.valueSymbol.reset(); }); + check([](auto&, auto& i) { i.inputKeys[1] = i.inputKeys[2]; }); + check([](auto& m, auto&) { m.initialStateValueByKey.clear(); }); + check([](auto& m, auto&) { m.nextStateExprByStateKey.clear(); }); +} + +TEST_F(LatchResetAdapterTests, BareBusResetNameCannotSilentlySelectBitZero) { + auto source = synthetic(true); + auto interface = std::make_shared(*source.eventResetInterface); + interface->inputNames[2] = "reset[1]"; + source.eventResetInterface = interface; + const auto ambiguous = adaptResetCycles(source, {1, {{"reset", true}}}); + EXPECT_FALSE(ambiguous.model); + EXPECT_NE(ambiguous.error.find("bus bit explicitly"), std::string::npos); + EXPECT_TRUE(adaptResetCycles(source, {1, {{"reset[0]", true}}}).model); +} + +TEST_F(LatchResetAdapterTests, AdaptationIsIdempotenceGuardedAndOriginalRemainsUnmodified) { + const auto source = synthetic(true); + const auto result = adaptResetCycles(source, {2, {{"reset", true}}}); + ASSERT_TRUE(result.model) << result.error; + EXPECT_FALSE(result.model->eventResetInterface); + EXPECT_FALSE(adaptResetCycles(*result.model, {2, {{"reset", true}}}).model); + EXPECT_EQ(source.eventContract, "test-events;single"); + EXPECT_EQ(source.stateBits.size(), 7u); + EXPECT_TRUE(source.eventResetInterface); + EXPECT_TRUE(adaptResetCycles(source, {2, {{"reset", true}}}).model); +} + +TEST_F(LatchResetAdapterTests, CompositionBudgetFailsWithoutPublishingPartialModel) { + for (const size_t limit : {size_t(0), size_t(1), size_t(40)}) { + auto source = synthetic(true); + auto interface = std::make_shared(*source.eventResetInterface); + interface->maxCompositionNodes = limit; + source.eventResetInterface = interface; + const auto result = adaptResetCycles(source, {1, {{"reset", true}}}); + EXPECT_FALSE(result.model); + EXPECT_NE(result.error.find("budget"), std::string::npos); + EXPECT_EQ(source.stateBits.size(), 7u); + EXPECT_EQ(source.environmentInputs.size(), 8u); + } +} + +TEST_F(LatchResetAdapterTests, OnlyUnobservableUnconstrainedSyntheticHistoryIsRemoved) { + const SignalKey history{{uint64_t(1) << 61, 6, 99}, {0}}; + for (size_t reader = 0; reader < 5; ++reader) { + SCOPED_TRACE(reader); + auto source = synthetic(true); + source.stateBits.push_back(history); + source.inputVarByKey.emplace(history, 40); + source.displayNameByKey.emplace(history, "$event.history.unused"); + source.initialStateValueByKey.emplace(history, false); + source.nextStateExprByStateKey.emplace(history, BoolExpr::Var(40)); + if (reader == 1) + source.observedOutputExprByKey.at(source.observedOutputs[0]) = BoolExpr::Var(40); + else if (reader == 2) + source.nextStateExprByStateKey.at(source.stateBits[4]) = BoolExpr::Var(40); + else if (reader == 3) + source.initialCondition = BoolExpr::Not(BoolExpr::Xor(BoolExpr::Var(40), BoolExpr::Var(20))); + else if (reader == 4) + source.initialInputStateKeyByInputKey.emplace(source.eventResetInterface->inputKeys[1], history); + const auto result = adaptResetCycles(source, {1, {{"reset", true}}}); + ASSERT_TRUE(result.model) << result.error; + EXPECT_EQ(result.model->inputVarByKey.count(history), reader ? 1u : 0u); + EXPECT_EQ(result.model->nextStateExprByStateKey.count(history), reader ? 1u : 0u); + EXPECT_EQ(result.model->initialStateValueByKey.count(history), reader ? 1u : 0u); + EXPECT_EQ(source.inputVarByKey.count(history), 1u); // Input model stays complete. + } +} + +} // namespace +} // namespace KEPLER_FORMAL::SEC::LATCH diff --git a/test/sec/LatchResetClockTests.cpp b/test/sec/LatchResetClockTests.cpp new file mode 100644 index 00000000..a6ce33ea --- /dev/null +++ b/test/sec/LatchResetClockTests.cpp @@ -0,0 +1,264 @@ +// Copyright 2026 keplertech.io +// SPDX-License-Identifier: Apache-2.0 +#include +#include +#include "BoolExprCache.h" +#include "latch/LatchResetClock.h" + +namespace KEPLER_FORMAL::SEC::LATCH { +namespace { +class LatchResetClockTests : public ::testing::Test { + protected: + void TearDown() override { BoolExprCache::destroy(); } + size_t external() { + const size_t net = network.netCount++; + network.externalInputs.push_back(net); + return net; + } + size_t constantNet(bool value) { + const size_t net = network.netCount++; + network.constantByNet.resize(network.netCount); + network.constantByNet[net] = value; + return net; + } + size_t add(std::vector inputs, ResetClockPrimitive meta) { + const size_t output = network.netCount++; + Primitive primitive; + primitive.name = "cell" + std::to_string(network.primitives.size()); + primitive.inputs = std::move(inputs); + primitive.outputs = {output}; + primitive.storageBits = meta.kind == ResetClockPrimitive::Kind::Combinational ? 0 : 1; + network.primitives.push_back(std::move(primitive)); + metadata.push_back(std::move(meta)); + if (!network.constantByNet.empty()) network.constantByNet.resize(network.netCount); + return output; + } + size_t ff(size_t clock, bool inverse = false) { + auto* expression = BoolExpr::Var(2); + if (inverse) expression = BoolExpr::Not(expression); + return add({clock}, {ResetClockPrimitive::Kind::FlipFlop, expression, {}}); + } + size_t gate(std::vector inputs, BoolExpr* output) { + return add(std::move(inputs), {ResetClockPrimitive::Kind::Combinational, nullptr, {output}}); + } + ResetClockDiscovery discover() { return discoverResetClock(network, metadata); } + void expectUnsupported(const std::string& fragment) { + const auto result = discover(); + EXPECT_EQ(result.status, ResetClockDiscovery::Status::Unsupported) << result.detail; + EXPECT_FALSE(result.rootNet); + EXPECT_FALSE(result.externalInputIndex); + EXPECT_NE(result.detail.find(fragment), std::string::npos) << result.detail; + } + Network network; + std::vector metadata; +}; + +TEST_F(LatchResetClockTests, DirectPositiveClockKeepsExternalInputIdentity) { + external(); + const auto clock = external(); + ff(clock); + const auto result = discover(); + ASSERT_TRUE(result.resolved()) << result.detail; + EXPECT_EQ(result.rootNet, clock); + EXPECT_EQ(result.externalInputIndex, 1u); +} + +TEST_F(LatchResetClockTests, NegativeEdgeAndMixedEdgesUseOneRoot) { + const auto clock = external(); + ff(clock, true); + ff(clock, false); + const auto result = discover(); + ASSERT_TRUE(result.resolved()) << result.detail; + EXPECT_EQ(result.rootNet, clock); +} + +TEST_F(LatchResetClockTests, BuffersAndInvertersPreserveClockRoot) { + const auto clock = external(); + const auto inverted = gate({clock}, BoolExpr::Not(BoolExpr::Var(2))); + const auto buffered = gate({inverted}, BoolExpr::Var(2)); + ff(buffered); + ff(clock); + const auto result = discover(); + ASSERT_TRUE(result.resolved()) << result.detail; + EXPECT_EQ(result.rootNet, clock); +} + +TEST_F(LatchResetClockTests, ConstantGateInputsAreFoldedExactly) { + const auto clock = external(); + const auto one = constantNet(true); + const auto zero = constantNet(false); + const auto enabled = gate({clock, one}, BoolExpr::And(BoolExpr::Var(2), BoolExpr::Var(3))); + const auto routed = gate({enabled, zero}, BoolExpr::Or(BoolExpr::Var(2), BoolExpr::Var(3))); + const auto inverted = gate({routed, one}, BoolExpr::Xor(BoolExpr::Var(2), BoolExpr::Var(3))); + ff(inverted); + const auto result = discover(); + ASSERT_TRUE(result.resolved()) << result.detail; + EXPECT_EQ(result.rootNet, clock); +} + +TEST_F(LatchResetClockTests, AliasedPinsCollapseToTheSameCarrier) { + const auto clock = external(); + const auto copy = gate({clock}, BoolExpr::Var(2)); + const auto repeated = gate({copy, clock}, BoolExpr::And(BoolExpr::Var(2), BoolExpr::Var(3))); + ff(repeated); + EXPECT_TRUE(discover().resolved()); +} + +TEST_F(LatchResetClockTests, LatchEnableDoesNotBecomeAnExtraClockDomain) { + const auto clock = external(); + const auto enable = external(); + add({enable}, {ResetClockPrimitive::Kind::Latch, BoolExpr::Var(2), {}}); + ff(clock); + const auto result = discover(); + ASSERT_TRUE(result.resolved()) << result.detail; + EXPECT_EQ(result.rootNet, clock); +} + +TEST_F(LatchResetClockTests, LatchOnlyCircuitHasNoInferredClock) { + const auto enable = external(); + add({enable}, {ResetClockPrimitive::Kind::Latch, BoolExpr::Var(2), {}}); + network.primitives.back().name = "CLOCK_GATE"; + const auto result = discover(); + EXPECT_EQ(result.status, ResetClockDiscovery::Status::NoEdgeClock); + EXPECT_FALSE(result.rootNet); + EXPECT_NE(result.detail.find("latch enables"), std::string::npos); +} + +TEST_F(LatchResetClockTests, EmptyCircuitCannotDefineClockCycles) { + EXPECT_EQ(discover().status, ResetClockDiscovery::Status::NoEdgeClock); +} + +TEST_F(LatchResetClockTests, IndependentClockRootsRequireProtocol) { + ff(external()); + ff(external()); + expectUnsupported("multiple independent"); +} + +TEST_F(LatchResetClockTests, ArbitraryClockGateDoesNotGuessCarrierFromNames) { + const auto clock = external(); + const auto gateInput = external(); + const auto gated = gate({clock, gateInput}, BoolExpr::And(BoolExpr::Var(2), BoolExpr::Var(3))); + network.primitives.back().name = "CLOCK_GATE"; + ff(gated); + expectUnsupported("no unique external carrier"); +} + +TEST_F(LatchResetClockTests, ValidDomainDoesNotHideAnotherUnresolvedClock) { + const auto clock = external(); + const auto enable = external(); + ff(clock); + const auto gated = gate({clock, enable}, BoolExpr::And(BoolExpr::Var(2), BoolExpr::Var(3))); + ff(gated); + expectUnsupported("no unique external carrier"); +} + +TEST_F(LatchResetClockTests, MultiPinClockExpressionCannotGuessRoot) { + const auto a = external(), b = external(); + add({a, b}, {ResetClockPrimitive::Kind::FlipFlop, + BoolExpr::Or(BoolExpr::Var(2), BoolExpr::Var(3)), {}}); + expectUnsupported("no unique external carrier"); +} + +TEST_F(LatchResetClockTests, StateGeneratedClockIsNotExternalCarrier) { + const auto clock = external(); + const auto divided = ff(clock); + ff(divided); + expectUnsupported("state-generated"); +} + +TEST_F(LatchResetClockTests, ConstantClockCannotCountResetEdges) { + ff(constantNet(false)); + expectUnsupported("clock is constant"); +} + +TEST_F(LatchResetClockTests, DisabledClockGateIsRejectedEvenWithKnownRoot) { + const auto clock = external(); + const auto zero = constantNet(false); + ff(gate({clock, zero}, BoolExpr::And(BoolExpr::Var(2), BoolExpr::Var(3)))); + expectUnsupported("clock is constant"); +} + +TEST_F(LatchResetClockTests, UnrelatedCombinationalCycleDoesNotChangeKnownClock) { + const auto clock = external(); + ff(clock); + const size_t first = network.netCount, second = first + 1; + gate({second}, BoolExpr::Var(2)); + gate({first}, BoolExpr::Var(2)); + EXPECT_TRUE(discover().resolved()); +} + +TEST_F(LatchResetClockTests, CyclicClockRoutingTerminatesWithoutGuessing) { + const size_t first = network.netCount, second = first + 1; + gate({second}, BoolExpr::Var(2)); + gate({first}, BoolExpr::Var(2)); + ff(first); + expectUnsupported("cyclic"); +} + +TEST_F(LatchResetClockTests, ReverseOrderedDeepRoutingDoesNotRecurse) { + const auto clock = external(); + size_t routed = clock; + for (size_t i = 0; i < 4096; ++i) routed = gate({routed}, BoolExpr::Not(BoolExpr::Var(2))); + std::reverse(network.primitives.begin(), network.primitives.end()); + std::reverse(metadata.begin(), metadata.end()); + ff(routed); + const auto result = discover(); + ASSERT_TRUE(result.resolved()) << result.detail; + EXPECT_EQ(result.rootNet, clock); +} + +TEST_F(LatchResetClockTests, MissingMetadataDoesNotSilentlyDropAClockDomain) { + ff(external()); + metadata.clear(); + expectUnsupported("metadata for every primitive"); +} + +TEST_F(LatchResetClockTests, UnknownPrimitiveCannotBeAssumedCombinational) { + ff(external()); + add({}, {}); + expectUnsupported("unclassified primitive"); +} + +TEST_F(LatchResetClockTests, MissingClockExpressionIsUnsupported) { + ff(external()); + metadata.back().clock = nullptr; + expectUnsupported("invalid clock routing expression"); +} + +TEST_F(LatchResetClockTests, ClockExpressionMayNotReferenceUndeclaredPins) { + ff(external()); + metadata.back().clock = BoolExpr::Var(3); + expectUnsupported("non-input symbol"); +} + +TEST_F(LatchResetClockTests, IncompleteGateMetadataIsRejected) { + const auto clock = external(); + const auto buffered = gate({clock}, BoolExpr::Var(2)); + metadata.back().outputs.clear(); + ff(buffered); + expectUnsupported("incomplete combinational"); +} + +TEST_F(LatchResetClockTests, DuplicateOrDrivenExternalInputsAreRejected) { + const auto clock = external(); + ff(clock); + network.externalInputs.push_back(clock); + expectUnsupported("duplicate external"); + network.externalInputs.pop_back(); + network.primitives[0].outputs[0] = clock; + expectUnsupported("multiple drivers"); +} + +TEST_F(LatchResetClockTests, InvalidConstantsAndPinRangesAreRejected) { + const auto clock = external(); + ff(clock); + network.constantByNet = {false}; + expectUnsupported("invalid constant net table"); + network.constantByNet.resize(network.netCount); + expectUnsupported("also a constant"); + network.constantByNet.clear(); + network.primitives[0].inputs[0] = network.netCount; + expectUnsupported("out-of-range input"); +} +} // namespace +} // namespace KEPLER_FORMAL::SEC::LATCH diff --git a/test/sec/LatchResetIntegrationTests.cpp b/test/sec/LatchResetIntegrationTests.cpp new file mode 100644 index 00000000..a00ae8ba --- /dev/null +++ b/test/sec/LatchResetIntegrationTests.cpp @@ -0,0 +1,413 @@ +// Copyright 2026 keplertech.io +// SPDX-License-Identifier: Apache-2.0 + +#include + +#include +#include +#include "BoolExprCache.h" +#include "DNL.h" +#include "NLDB.h" +#include "NLDB0.h" +#include "NLLibrary.h" +#include "NLName.h" +#include "NLUniverse.h" +#include "SNLDesign.h" +#include "SNLDesignModeling.h" +#include "SNLInstance.h" +#include "SNLScalarNet.h" +#include "SNLScalarTerm.h" +#include "latch/LatchResetAdapter.h" +#include "latch/LatchSupportOptions.h" +#include "model/SequentialDesignModel.h" +#include "strategy/SequentialEquivalenceStrategy.h" + +namespace KEPLER_FORMAL::SEC::LATCH { +namespace { +using namespace naja::NL; +using Modeling = SNLDesignModeling; +using Expression = Modeling::BooleanExpression; +using Operator = Expression::Operator; + +Expression termExpression(SNLBitTerm* term) { + Expression expression; + expression.root = expression.addTerm(term); + return expression; +} + +enum class ClockRoute { Direct, Buffered, Inverted, Constant, Gated, Multiple }; +struct CircuitOptions { + ClockRoute clock = ClockRoute::Direct; + bool latchChain = true; + bool mixedEdges = false; + bool asyncThroughLatch = false; + bool invertData = false; + bool invertOnlyDuringReset = false; + bool ignoreReset = false; + bool andDataEnable = false; + bool constantData = false; +}; + +class LatchResetIntegrationTests : public ::testing::Test { + protected: + void SetUp() override { + NLUniverse::create(); + auto* db = NLDB::create(NLUniverse::get()); + designs_ = NLLibrary::create(db, NLLibrary::Type::Standard, NLName("designs")); + primitives_ = NLLibrary::create(db, NLLibrary::Type::Primitives, NLName("primitives")); + } + void TearDown() override { + naja::DNL::destroy(); + if (auto* universe = NLUniverse::get()) universe->destroy(); + BoolExprCache::destroy(); + } + SupportOptions settings(bool initialStorage = false) { + SupportOptions options; + options.enabled = true; + options.singleInputChange = true; + options.initialInputs = false; + options.initialStorage = initialStorage; + options.workers = 2; + return options; + } + SNLScalarNet* port(SNLDesign* top, const std::string& name, SNLTerm::Direction direction) { + auto* term = SNLScalarTerm::create(top, direction, NLName(name)); + auto* net = SNLScalarNet::create(top, NLName(name)); + term->setNet(net); + return net; + } + void instance(SNLDesign* top, SNLDesign* primitive, const std::string& name, + const std::vector>& pins) { + auto* cell = SNLInstance::create(top, primitive, NLName(name)); + for (const auto& [pin, net] : pins) + cell->getInstTerm(primitive->getScalarTerm(NLName(pin)))->setNet(net); + } + SNLDesign* flop(const std::string& name, const CircuitOptions& options, bool falling = false) { + auto* cell = SNLDesign::create(primitives_, SNLDesign::Type::Primitive, NLName(name)); + auto* data = options.constantData ? nullptr + : SNLScalarTerm::create(cell, SNLTerm::Direction::Input, NLName("D")); + auto* clock = SNLScalarTerm::create(cell, SNLTerm::Direction::Input, NLName("C")); + auto* reset = SNLScalarTerm::create(cell, SNLTerm::Direction::Input, NLName("R")); + auto* output = SNLScalarTerm::create(cell, SNLTerm::Direction::Output, NLName("Q")); + Modeling::SequentialModel model; + model.kind = Modeling::SequentialModel::Kind::FlipFlop; + model.clockedOn = termExpression(clock); + if (falling) model.clockedOn.root = model.clockedOn.addOperation(Operator::Not, {model.clockedOn.root}); + Modeling::SequentialState state; + if (options.constantData) state.nextState.root = state.nextState.addConstant(false); + else state.nextState = termExpression(data); + if (options.invertData) + state.nextState.root = state.nextState.addOperation(Operator::Not, {state.nextState.root}); + if (options.asyncThroughLatch) state.clear = termExpression(reset); + else if (!options.ignoreReset) { + auto inactiveReset = state.nextState.addTerm(reset); + inactiveReset = state.nextState.addOperation(Operator::Not, {inactiveReset}); + state.nextState.root = state.nextState.addOperation(Operator::And, {state.nextState.root, inactiveReset}); + } + model.states.push_back(state); + Expression physical; + physical.root = physical.addState(0); + if (options.invertOnlyDuringReset) { + const auto asserted = physical.addTerm(reset); + physical.root = physical.addOperation(Operator::Xor, {physical.root, asserted}); + } + model.outputs.push_back({output, physical}); + Modeling::setSequentialModel(cell, model); + return cell; + } + SNLDesign* routeCell(const std::string& name, ClockRoute route) { + auto* cell = SNLDesign::create(primitives_, SNLDesign::Type::Primitive, NLName(name)); + auto* a = SNLScalarTerm::create(cell, SNLTerm::Direction::Input, NLName("A")); + Modeling::BitTerms inputs{a}; + if (route == ClockRoute::Gated) + inputs.push_back(SNLScalarTerm::create(cell, SNLTerm::Direction::Input, NLName("B"))); + auto* y = SNLScalarTerm::create(cell, SNLTerm::Direction::Output, NLName("Y")); + Modeling::addCombinatorialArcs(inputs, {y}); + const auto arity = inputs.size(); + Modeling::setTruthTable(cell, SNLTruthTable(arity, + route == ClockRoute::Gated ? 8 : route == ClockRoute::Inverted ? 1 : 2, + SNLTruthTable::fullDependencies(arity))); + return cell; + } + SNLDesign* circuit(const std::string& name, CircuitOptions options = {}) { + auto* top = SNLDesign::create(designs_, SNLDesign::Type::Standard, NLName(name)); + auto* clock = port(top, "clock", SNLTerm::Direction::Input); + auto* data = port(top, "data", SNLTerm::Direction::Input); + auto* enable = port(top, "enable", SNLTerm::Direction::Input); + auto* reset = port(top, "reset", SNLTerm::Direction::Input); + auto* output = port(top, "out", SNLTerm::Direction::Output); + auto* localClock = clock; + if (options.clock == ClockRoute::Constant) { + localClock = SNLScalarNet::create(top, NLName("constant_clock")); + localClock->setType(SNLNet::Type::Assign0); + } else if (options.clock != ClockRoute::Direct && options.clock != ClockRoute::Multiple) { + localClock = SNLScalarNet::create(top, NLName("routed_clock")); + auto* primitive = routeCell(name + "_route", options.clock); + std::vector> pins{{"A", clock}, {"Y", localClock}}; + if (options.clock == ClockRoute::Gated) pins.emplace_back("B", enable); + instance(top, primitive, "clock_route", pins); + } + auto* localReset = reset; + if (options.asyncThroughLatch) { + localReset = SNLScalarNet::create(top, NLName("latched_reset")); + instance(top, NLDB0::getDLatch(), "reset_latch", {{"D", reset}, {"E", enable}, {"Q", localReset}}); + } + auto* held = options.latchChain || options.mixedEdges + ? SNLScalarNet::create(top, NLName("held")) : output; + auto* localData = data; + if (options.andDataEnable) { + localData = SNLScalarNet::create(top, NLName("qualified_data")); + instance(top, routeCell(name + "_data_gate", ClockRoute::Gated), "data_gate", + {{"A", data}, {"B", enable}, {"Y", localData}}); + } + std::vector> firstPins{ + {"C", localClock}, {"R", localReset}, {"Q", held}}; + if (!options.constantData) firstPins.emplace_back("D", localData); + instance(top, flop(name + "_ff", options), "ff", firstPins); + if (options.mixedEdges) { + auto* falling = options.latchChain ? SNLScalarNet::create(top, NLName("falling")) : output; + std::vector> fallingPins{ + {"C", localClock}, {"R", localReset}, {"Q", falling}}; + if (!options.constantData) fallingPins.emplace_back("D", held); + instance(top, flop(name + "_falling", options, true), "falling_ff", fallingPins); + held = falling; + } + if (options.latchChain) { + auto* middle = SNLScalarNet::create(top, NLName("middle")); + instance(top, NLDB0::getDLatch(), "first_latch", {{"D", held}, {"E", enable}, {"Q", middle}}); + instance(top, NLDB0::getDLatch(), "second_latch", {{"D", middle}, {"E", enable}, {"Q", output}}); + } + if (options.clock == ClockRoute::Multiple) { + auto* otherClock = port(top, "other_clock", SNLTerm::Direction::Input); + auto* otherOutput = port(top, "other_out", SNLTerm::Direction::Output); + instance(top, flop(name + "_other", options), "other_ff", + {{"D", data}, {"C", otherClock}, {"R", reset}, {"Q", otherOutput}}); + } + return top; + } + SNLDesign* resetObservationCircuit(const std::string& name, bool exposeReset) { + auto* top = SNLDesign::create(designs_, SNLDesign::Type::Standard, NLName(name)); + auto* clock = port(top, "clock", SNLTerm::Direction::Input); + auto* reset = port(top, "reset", SNLTerm::Direction::Input); + auto* output = port(top, "out", SNLTerm::Direction::Output); + if (exposeReset) top->getScalarTerm(NLName("out"))->setNet(reset); + else output->setType(SNLNet::Type::Assign0); + auto* hidden = SNLScalarNet::create(top, NLName("hidden")); + CircuitOptions options; + options.constantData = true; + // A real sequential cell supplies clock discovery, but its data state is + // irrelevant to the output-mask and permanent-reset-clamp property. + instance(top, flop(name + "_ff", options), "ff", + {{"C", clock}, {"R", reset}, {"Q", hidden}}); + return top; + } + SequentialEquivalenceResult compare(SNLDesign* first, SNLDesign* second, + SecEngine engine = SecEngine::KInduction, SecEncoding encoding = SecEncoding::Binary, + SecResetSpec reset = {2, {{"reset", true}}}) { + ScopedSupportOptions scope(settings()); + const auto left = SequentialDesignModel::extract(first); + const auto right = SequentialDesignModel::extract(second); + EXPECT_FALSE(left.hasUnsupportedFeatures()); + EXPECT_FALSE(right.hasUnsupportedFeatures()); + for (const auto* extracted : {&left, &right}) { + for (const auto& skipped : extracted->skippedObservedOutputs) + ADD_FAILURE() << (extracted == &left ? "first: " : "second: ") + << extracted->connectivitySkipInfoByKey.at(skipped).detail; + } + SequentialEquivalenceStrategy strategy(nullptr, nullptr, Config::SolverType::KISSAT, + engine, encoding, reset); + return strategy.runExtractedModels(left, right, 48); + } + NLLibrary* designs_ = nullptr; + NLLibrary* primitives_ = nullptr; +}; + +class LatchResetEngineTests : public LatchResetIntegrationTests, + public ::testing::WithParamInterface> {}; + +TEST_P(LatchResetEngineTests, SynchronousResetThenIndependentEnableLatchChain) { + auto* first = circuit("first"); + auto* second = circuit("second"); + const auto [engine, encoding] = GetParam(); + const auto result = compare(first, second, engine, encoding); + EXPECT_EQ(result.status, SequentialEquivalenceStatus::Equivalent) << result.reason; + EXPECT_EQ(result.coveredOutputs, 1u); +} + +TEST_P(LatchResetEngineTests, RealPostResetMismatchIsNotMaskedForever) { + auto* first = circuit("first"); + CircuitOptions options; + options.invertData = true; + auto* second = circuit("second", options); + const auto [engine, encoding] = GetParam(); + const auto result = compare(first, second, engine, encoding); + EXPECT_EQ(result.status, SequentialEquivalenceStatus::Different) << result.reason; + EXPECT_EQ(result.coveredOutputs, 1u); +} + +TEST_P(LatchResetEngineTests, ResetOnlyOutputDifferenceIsMaskedAndResetStaysInactiveAfterRelease) { + auto* first = resetObservationCircuit("first", false); + auto* second = resetObservationCircuit("second", true); + const auto [engine, encoding] = GetParam(); + // Two cycles expose active reset at the first prefix boundary: missing + // masking would fail there. After release, every selector/value sequence is + // checked, including attempts to reassert reset. The irrelevant FF data + // state is intentionally outside this focused property's observable cone. + const auto result = compare(first, second, engine, encoding); + EXPECT_EQ(result.status, SequentialEquivalenceStatus::Equivalent) << result.reason; + EXPECT_EQ(result.coveredOutputs, 1u); +} + +TEST_F(LatchResetIntegrationTests, ResetSensitivePhysicalSequentialOutputIsMasked) { + CircuitOptions options; + options.latchChain = false; + options.constantData = true; + auto* first = circuit("first", options); + options.invertOnlyDuringReset = true; + auto* second = circuit("second", options); + // This richer output depends on both hidden storage and current reset. The + // existing dual-rail IMC Craig interpolant-growth budget cannot prove this + // fixture; the smaller reset-observation property above is required to pass + // all six engine/encoding combinations. Do not turn a resource-limited + // result into an expected equivalence or silently relax that assertion. + for (auto engine : {SecEngine::KInduction, SecEngine::Imc, SecEngine::Pdr}) { + for (auto encoding : {SecEncoding::Binary, SecEncoding::DualRailSteady}) { + if (engine == SecEngine::Imc && encoding == SecEncoding::DualRailSteady) continue; + SCOPED_TRACE(::testing::Message() << "engine=" << int(engine) << " encoding=" << int(encoding)); + const auto result = compare(first, second, engine, encoding, {1, {{"reset", true}}}); + EXPECT_EQ(result.status, SequentialEquivalenceStatus::Equivalent) << result.reason; + EXPECT_EQ(result.coveredOutputs, 1u); + } + } +} + +INSTANTIATE_TEST_SUITE_P(AllEnginesAndEncodings, LatchResetEngineTests, + ::testing::Combine(::testing::Values(SecEngine::KInduction, SecEngine::Imc, SecEngine::Pdr), + ::testing::Values(SecEncoding::Binary, SecEncoding::DualRailSteady))); + +TEST_F(LatchResetIntegrationTests, BufferedAndInvertedClockRoutesSupportFullCycles) { + size_t index = 0; + for (auto route : {ClockRoute::Buffered, ClockRoute::Inverted}) { + CircuitOptions options; + options.clock = route; + auto* first = circuit("first" + std::to_string(index), options); + auto* second = circuit("second" + std::to_string(index++), options); + const auto result = compare(first, second); + EXPECT_EQ(result.status, SequentialEquivalenceStatus::Equivalent) << result.reason; + EXPECT_EQ(result.coveredOutputs, 1u); + } +} + +TEST_F(LatchResetIntegrationTests, MixedEdgeFlopsUseTheSameExternalCarrier) { + CircuitOptions options; + options.mixedEdges = true; + const auto result = compare(circuit("first", options), circuit("second", options)); + EXPECT_EQ(result.status, SequentialEquivalenceStatus::Equivalent) << result.reason; + EXPECT_EQ(result.coveredOutputs, 1u); +} + +TEST_F(LatchResetIntegrationTests, OneResetCycleActuallyVisitsBothEdgesAndClearsStoredOnes) { + CircuitOptions options; + options.latchChain = false; + options.mixedEdges = true; + auto* top = circuit("top", options); + ScopedSupportOptions scope(settings(true)); + const auto original = SequentialDesignModel::extract(top); + ASSERT_EQ(original.observedOutputs.size(), 1u); + ASSERT_TRUE(original.skippedObservedOutputs.empty()); + const auto wrapped = adaptResetCycles(original, {1, {{"reset", true}}}); + ASSERT_TRUE(wrapped.model.has_value()) << wrapped.error; + const auto& model = *wrapped.model; + std::unordered_map state; + for (const auto& key : model.stateBits) + state[model.inputVarByKey.at(key)] = model.initialStateValueByKey.at(key); + const auto step = [&] { + auto environment = state; + // Selector zero and value zero request a clock-low stutter. The prefix + // itself must supply both edges; the environment provides no clock tick. + for (const auto& key : model.environmentInputs) + environment[model.inputVarByKey.at(key)] = false; + const bool output = model.observedOutputExprByKey.at(model.observedOutputs.front())->evaluate(environment); + for (const auto& key : model.stateBits) + state[model.inputVarByKey.at(key)] = model.nextStateExprByStateKey.at(key)->evaluate(environment); + return output; + }; + EXPECT_FALSE(step()); // Reset cycle is masked. + EXPECT_FALSE(step()); // Normal observation: falling-edge state really cleared. + EXPECT_FALSE(step()); +} + +TEST_F(LatchResetIntegrationTests, AsynchronousResetCanPropagateThroughALatch) { + CircuitOptions options; + options.asyncThroughLatch = true; + const auto result = compare(circuit("first", options), circuit("second", options)); + EXPECT_EQ(result.status, SequentialEquivalenceStatus::Equivalent) << result.reason; + EXPECT_EQ(result.coveredOutputs, 1u); +} + +TEST_F(LatchResetIntegrationTests, ResetCycleSamplesAllFreeInputsRatherThanOnlyOneInput) { + CircuitOptions options; + options.latchChain = false; + options.ignoreReset = true; + options.andDataEnable = true; + auto* top = circuit("top", options); + ScopedSupportOptions scope(settings()); + const auto original = SequentialDesignModel::extract(top); + ASSERT_EQ(original.observedOutputs.size(), 1u); + ASSERT_TRUE(original.eventResetInterface); + const auto wrapped = adaptResetCycles(original, {1, {{"reset", true}}}); + ASSERT_TRUE(wrapped.model) << wrapped.error; + const auto& model = *wrapped.model; + std::unordered_map state; + for (const auto& key : model.stateBits) + state[model.inputVarByKey.at(key)] = model.initialStateValueByKey.at(key); + const auto step = [&] { + auto environment = state; + for (const auto& key : model.environmentInputs) + environment[model.inputVarByKey.at(key)] = false; + const auto& interface = *original.eventResetInterface; + for (size_t i = 0; i < interface.inputNames.size(); ++i) { + if (interface.inputNames[i] == "data[0]" || interface.inputNames[i] == "enable[0]") + environment[model.inputVarByKey.at(interface.inputKeys[i])] = true; + } + const bool output = model.observedOutputExprByKey.at(model.observedOutputs.front())->evaluate(environment); + for (const auto& key : model.stateBits) + state[model.inputVarByKey.at(key)] = model.nextStateExprByStateKey.at(key)->evaluate(environment); + return output; + }; + EXPECT_FALSE(step()); // Masked reset cycle, with both free inputs driven high. + EXPECT_TRUE(step()); // AND(data,enable) was captured on the forced rising edge. +} + +TEST_F(LatchResetIntegrationTests, MultipleIndependentClockRootsHaveASpecificDiagnostic) { + CircuitOptions options; + options.clock = ClockRoute::Multiple; + const auto result = compare(circuit("first", options), circuit("second", options)); + EXPECT_EQ(result.status, SequentialEquivalenceStatus::Unsupported) << result.reason; + EXPECT_NE(result.reason.find("multiple independent"), std::string::npos) << result.reason; +} + +TEST_F(LatchResetIntegrationTests, ConstantAndGatedClocksAreNotInventedAsPeriodicCarriers) { + size_t index = 0; + for (auto route : {ClockRoute::Constant, ClockRoute::Gated}) { + CircuitOptions options; + options.clock = route; + const auto suffix = std::to_string(index++); + const auto result = compare(circuit("first" + suffix, options), + circuit("second" + suffix, options)); + EXPECT_EQ(result.status, SequentialEquivalenceStatus::Unsupported) << result.reason; + EXPECT_NE(result.reason.find(route == ClockRoute::Constant ? "constant" : "carrier"), + std::string::npos) << result.reason; + } +} + +TEST_F(LatchResetIntegrationTests, MultipleResetPortsAreExplicitlyUnsupported) { + const auto result = compare(circuit("first"), circuit("second"), SecEngine::KInduction, + SecEncoding::Binary, {2, {{"reset", true}, {"enable", false}}}); + EXPECT_EQ(result.status, SequentialEquivalenceStatus::Unsupported) << result.reason; + EXPECT_NE(result.reason.find("reset"), std::string::npos) << result.reason; + EXPECT_EQ(result.reason.find("cannot use clock-cycle"), std::string::npos) << result.reason; +} + +} // namespace +} // namespace KEPLER_FORMAL::SEC::LATCH diff --git a/test/sec/LatchSettlingCompilerTests.cpp b/test/sec/LatchSettlingCompilerTests.cpp new file mode 100644 index 00000000..59922814 --- /dev/null +++ b/test/sec/LatchSettlingCompilerTests.cpp @@ -0,0 +1,591 @@ +// Copyright 2026 keplertech.io +// SPDX-License-Identifier: Apache-2.0 + +#include + +#include +#include +#include +#include +#include + +#include "latch/LatchSettlingCompiler.h" + +namespace KEPLER_FORMAL::SEC::LATCH { +namespace { + +State graphState(size_t id, bool stable = false) { + State state; + for (size_t bit = 0; bit < 4; ++bit) { + state.current.push_back(static_cast((id >> bit) & 1)); + } + state.previous = state.current; + state.storage = {{0}}; + state.active = {static_cast(!stable)}; + return state; +} + +EpisodeRelation graphRelation(std::vector states, + std::vector> edges) { + return { + [](const State& state) { return state.active == Bits{0} && !state.error; }, + [states = std::move(states), edges = std::move(edges)](const State& state) { + const auto found = std::find(states.begin(), states.end(), state); + if (found == states.end()) { + throw std::invalid_argument("Unknown synthetic reference state"); + } + std::vector result; + for (const auto next : edges[static_cast(found - states.begin())]) { + result.push_back(states[next]); + } + return result; + }}; +} + +Network simpleLatchNetwork() { + Network network; + network.netCount = 3; + network.externalInputs = {0, 1}; + network.primitives = {latch("latch", 0, 1, 2)}; + return network; +} + +CompileOptions simpleLatchOptions() { + CompileOptions options; + options.initialInputs = {0, 0}; + options.initialStorage = {{uint8_t{0}}}; + options.singleExternalInputChange = true; + return options; +} + +Network selfLatchNetwork(bool inverted) { + Network network; + network.netCount = inverted ? 3 : 2; + network.constantByNet.resize(network.netCount); + network.constantByNet[1] = true; + network.primitives.push_back(latch("self", inverted ? 2 : 0, 1, 0)); + if (inverted) { + network.primitives.push_back(combinational( + "invert", {0}, {2}, [](const Bits& inputs) { return Bits{uint8_t(!inputs[0])}; })); + } + return network; +} + +TEST(LatchSettlingCompilerTests, EmptyEntryRelationIsNotAVacuousCertificate) { + const auto result = certifyEpisode({}, {}); + EXPECT_EQ(result.status, CertificationStatus::Invalid); + EXPECT_NE(result.detail.find("empty"), std::string::npos); +} + +TEST(LatchSettlingCompilerTests, MissingRelationCallbacksAreInvalid) { + EXPECT_EQ(certifyEpisode({graphState(0)}, {}).status, CertificationStatus::Invalid); +} + +TEST(LatchSettlingCompilerTests, StableEntryNeedsZeroWavesButIdentitySuccessor) { + const auto stable = graphState(0, true); + CompilerLimits limits; + limits.maxWaves = 0; + const auto result = certifyEpisode({stable}, graphRelation({stable}, {{0}}), limits); + ASSERT_TRUE(result.certified()) << result.detail; + EXPECT_EQ(result.maxWaves, 0); + EXPECT_EQ(result.stableStates, (std::vector{stable})); +} + +TEST(LatchSettlingCompilerTests, MissingNonstableSuccessorCannotDisappear) { + const auto state = graphState(0); + EXPECT_EQ(certifyEpisode({state}, graphRelation({state}, {{}})).status, + CertificationStatus::Invalid); +} + +TEST(LatchSettlingCompilerTests, MissingStableSuccessorCannotDisappear) { + const auto state = graphState(0, true); + EXPECT_EQ(certifyEpisode({state}, graphRelation({state}, {{}})).status, + CertificationStatus::Invalid); +} + +TEST(LatchSettlingCompilerTests, StableSuccessorMustPreserveTheEntireState) { + const auto first = graphState(0, true); + auto changed = first; + changed.storage[0][0] = 1; + EXPECT_EQ(certifyEpisode({first}, graphRelation({first, changed}, {{1}, {1}})).status, + CertificationStatus::Invalid); +} + +TEST(LatchSettlingCompilerTests, ExplicitErrorRejectsEvenIfAnotherBranchSettles) { + const auto entry = graphState(0); + const auto stable = graphState(1, true); + auto error = graphState(2); + error.error = true; + error.errorReason = "invalid clear/preset combination"; + const auto result = certifyEpisode( + {entry}, graphRelation({entry, stable, error}, {{1, 2}, {1}, {2}})); + EXPECT_EQ(result.status, CertificationStatus::Invalid); + EXPECT_NE(result.detail.find("clear/preset"), std::string::npos); +} + +TEST(LatchSettlingCompilerTests, ErrorCannotBeRelabeledStable) { + auto error = graphState(0, true); + error.error = true; + const EpisodeRelation relation{ + [](const State&) { return true; }, + [](const State& state) { return std::vector{state}; }}; + EXPECT_EQ(certifyEpisode({error}, relation).status, CertificationStatus::Invalid); +} + +TEST(LatchSettlingCompilerTests, LongestPathNotShortestDistanceDefinesTheBound) { + const auto entry = graphState(0); + const auto middle = graphState(1); + const auto stable = graphState(2, true); + const auto result = certifyEpisode( + {entry}, graphRelation({entry, middle, stable}, {{2, 1}, {2}, {2}})); + ASSERT_TRUE(result.certified()) << result.detail; + EXPECT_EQ(result.maxWaves, 2); + EXPECT_EQ(result.exploredStates, 3); + EXPECT_EQ(result.exploredTransitions, 4); +} + +TEST(LatchSettlingCompilerTests, AllEntryStatesContributeToTheUniformBound) { + const auto first = graphState(0); + const auto second = graphState(1); + const auto stable = graphState(2, true); + const auto result = certifyEpisode( + {stable, second, first}, graphRelation({first, second, stable}, {{1}, {2}, {2}})); + ASSERT_TRUE(result.certified()) << result.detail; + EXPECT_EQ(result.maxWaves, 2); +} + +TEST(LatchSettlingCompilerTests, NonstableCycleIsNotASufficientlyLargeBound) { + const auto first = graphState(0); + const auto second = graphState(1); + const auto result = certifyEpisode( + {first}, graphRelation({first, second}, {{1}, {0}})); + EXPECT_EQ(result.status, CertificationStatus::NonSettling); +} + +TEST(LatchSettlingCompilerTests, CycleWithAnExitStillFailsUniversalSettling) { + const auto entry = graphState(0); + const auto stable = graphState(1, true); + const auto result = certifyEpisode( + {entry}, graphRelation({entry, stable}, {{0, 1}, {1}})); + EXPECT_EQ(result.status, CertificationStatus::NonSettling); +} + +TEST(LatchSettlingCompilerTests, TwoStableEntriesAreNotCherryPicked) { + const auto first = graphState(0, true); + const auto second = graphState(1, true); + const auto result = certifyEpisode( + {first, second}, graphRelation({first, second}, {{0}, {1}})); + EXPECT_EQ(result.status, CertificationStatus::OrderDependent); + EXPECT_EQ(result.stableStates.size(), 2); +} + +TEST(LatchSettlingCompilerTests, EqualOutputsDoNotHideDifferentRetainedStorage) { + const auto entry = graphState(0); + auto first = graphState(1, true); + auto second = first; + second.storage[0][0] = 1; + const auto result = certifyEpisode( + {entry}, graphRelation({entry, first, second}, {{1, 2}, {1}, {2}})); + EXPECT_EQ(result.status, CertificationStatus::OrderDependent); + ASSERT_EQ(result.stableStates.size(), 2); + EXPECT_EQ(result.stableStates[0].current, result.stableStates[1].current); +} + +TEST(LatchSettlingCompilerTests, HistoryIsPartOfBoundaryUniqueness) { + const auto entry = graphState(0); + auto first = graphState(1, true); + auto second = first; + second.previous[0] = 0; + EXPECT_EQ(certifyEpisode({entry}, graphRelation( + {entry, first, second}, {{1, 2}, {1}, {2}})).status, + CertificationStatus::OrderDependent); +} + +TEST(LatchSettlingCompilerTests, EquivalentDuplicateSuccessorsDoNotMakeACycle) { + const auto entry = graphState(0); + const auto stable = graphState(1, true); + const auto result = certifyEpisode( + {entry, entry}, graphRelation({entry, stable}, {{1, 1}, {1, 1}})); + ASSERT_TRUE(result.certified()) << result.detail; + EXPECT_EQ(result.maxWaves, 1); +} + +TEST(LatchSettlingCompilerTests, ExhaustiveSmallGraphsMatchBoundedUniversalSemantics) { + const std::vector states = { + graphState(0), graphState(1), graphState(2, true), graphState(3, true)}; + // Every nonempty successor subset for each of two nonstable states. The two + // stable states pad identically. Four waves suffice for any acyclic graph on + // these four states; surviving nonstable paths therefore witness a cycle. + for (size_t firstMask = 1; firstMask < 16; ++firstMask) { + for (size_t secondMask = 1; secondMask < 16; ++secondMask) { + SCOPED_TRACE("masks " + std::to_string(firstMask) + "," + + std::to_string(secondMask)); + std::vector> edges(4); + for (size_t state = 0; state < 4; ++state) { + if (firstMask & (size_t{1} << state)) edges[0].push_back(state); + if (secondMask & (size_t{1} << state)) edges[1].push_back(state); + } + edges[2] = {2}; + edges[3] = {3}; + size_t frontier = 1; + size_t expectedDepth = 0; + for (size_t depth = 1; depth <= 4; ++depth) { + size_t next = 0; + for (size_t state = 0; state < 4; ++state) { + if (frontier & (size_t{1} << state)) { + for (const auto successor : edges[state]) next |= size_t{1} << successor; + } + } + frontier = next; + if ((frontier & 3) == 0 && expectedDepth == 0) expectedDepth = depth; + } + const auto expected = (frontier & 3) + ? CertificationStatus::NonSettling + : frontier == 12 ? CertificationStatus::OrderDependent + : CertificationStatus::Certified; + const auto result = certifyEpisode({states[0]}, graphRelation(states, edges)); + EXPECT_EQ(result.status, expected) << result.detail; + if (expected != CertificationStatus::NonSettling) { + EXPECT_EQ(result.maxWaves, expectedDepth); + } + } + } +} + +TEST(LatchSettlingCompilerTests, StateLayoutCannotChangeDuringPropagation) { + const auto entry = graphState(0); + auto malformed = graphState(1, true); + malformed.current.push_back(0); + EXPECT_EQ(certifyEpisode({entry}, graphRelation( + {entry, malformed}, {{1}, {1}})).status, + CertificationStatus::Invalid); +} + +TEST(LatchSettlingCompilerTests, NonBooleanReferenceValuesAreUnsupported) { + auto malformed = graphState(0, true); + malformed.storage[0][0] = 2; + EXPECT_EQ(certifyEpisode({malformed}, graphRelation({malformed}, {{0}})).status, + CertificationStatus::Invalid); +} + +TEST(LatchSettlingCompilerTests, InsufficientWaveBudgetIsNotNonSettling) { + const auto entry = graphState(0); + const auto stable = graphState(1, true); + CompilerLimits limits; + limits.maxWaves = 0; + const auto result = certifyEpisode( + {entry}, graphRelation({entry, stable}, {{1}, {1}}), limits); + EXPECT_EQ(result.status, CertificationStatus::UnprovedBound); + EXPECT_EQ(result.maxWaves, 1); +} + +TEST(LatchSettlingCompilerTests, GraphLimitsNeverTruncateToASuccessfulProof) { + const auto entry = graphState(0); + const auto stable = graphState(1, true); + const auto relation = graphRelation({entry, stable}, {{1}, {1}}); + CompilerLimits limits; + limits.maxEpisodeStates = 1; + EXPECT_EQ(certifyEpisode({entry}, relation, limits).status, + CertificationStatus::ResourceLimit); + limits = {}; + limits.maxEpisodeTransitions = 1; + EXPECT_EQ(certifyEpisode({entry}, relation, limits).status, + CertificationStatus::ResourceLimit); + limits = {}; + limits.maxStateBits = 1; + EXPECT_EQ(certifyEpisode({entry}, relation, limits).status, + CertificationStatus::ResourceLimit); +} + +TEST(LatchSettlingCompilerTests, PrimitiveEnumerationLimitIsNotASampledProof) { + const EpisodeRelation relation{ + [](const State&) { return false; }, + [](const State&) -> std::vector { throw Limit("pin order limit"); }}; + EXPECT_EQ(certifyEpisode({graphState(0)}, relation).status, + CertificationStatus::ResourceLimit); +} + +TEST(LatchSettlingCompilerTests, ReferenceExceptionCannotBecomeAnEmptyRelation) { + const EpisodeRelation relation{ + [](const State&) { return false; }, + [](const State&) -> std::vector { + throw std::invalid_argument("missing primitive rule"); + }}; + EXPECT_EQ(certifyEpisode({graphState(0)}, relation).status, + CertificationStatus::Invalid); +} + +TEST(LatchSettlingCompilerTests, OpenSelfLatchPreservesBothInitialHistories) { + EventModel model(selfLatchNetwork(false)); + CompileOptions options; + const auto result = compileTransitionTable(model, options); + ASSERT_TRUE(result.certified()) << result.detail; + ASSERT_TRUE(result.table); + EXPECT_EQ(result.table->initials.size(), 2); + EXPECT_EQ(result.table->boundaries.size(), 2); + EXPECT_EQ(result.table->rows.size(), 2); + for (const auto& origin : result.table->initials) { + EXPECT_EQ(origin.storage[0][0], result.table->boundaries[origin.boundary].current[0]); + } +} + +TEST(LatchSettlingCompilerTests, InvertingOpenLatchLoopCannotBeCompiled) { + EventModel model(selfLatchNetwork(true)); + CompileOptions options; + options.initialStorage = {{uint8_t{0}}, {}}; + const auto result = compileTransitionTable(model, options); + EXPECT_EQ(result.status, CertificationStatus::NonSettling) << result.detail; + EXPECT_FALSE(result.table); +} + +TEST(LatchSettlingCompilerTests, AllInitialOriginsRemainWhenTheyMerge) { + Network network; + network.netCount = 3; + network.constantByNet = {true, true, std::nullopt}; + network.primitives = {latch("capture_one", 0, 1, 2)}; + EventModel model(network); + const auto result = compileTransitionTable(model, {}); + ASSERT_TRUE(result.certified()) << result.detail; + ASSERT_TRUE(result.table); + ASSERT_EQ(result.table->initials.size(), 2); + EXPECT_EQ(result.table->boundaries.size(), 1); + EXPECT_EQ(result.table->initials[0].boundary, result.table->initials[1].boundary); + EXPECT_NE(result.table->initials[0].storage, result.table->initials[1].storage); +} + +TEST(LatchSettlingCompilerTests, DefaultContractRetainsConcurrentClosingDataRace) { + EventModel model(simpleLatchNetwork()); + auto options = simpleLatchOptions(); + options.singleExternalInputChange = false; + const auto result = compileTransitionTable(model, options); + EXPECT_EQ(result.status, CertificationStatus::OrderDependent) << result.detail; + EXPECT_FALSE(result.table); +} + +TEST(LatchSettlingCompilerTests, ExplicitSingleInputContractHasCompleteClosure) { + EventModel model(simpleLatchNetwork()); + const auto result = compileTransitionTable(model, simpleLatchOptions()); + ASSERT_TRUE(result.certified()) << result.detail; + ASSERT_TRUE(result.table); + EXPECT_EQ(result.table->boundaries.size(), 6); + EXPECT_EQ(result.table->rows.size(), 18); + for (size_t from = 0; from < result.table->boundaries.size(); ++from) { + size_t rowCount = 0; + size_t stutters = 0; + const auto& boundary = result.table->boundaries[from]; + for (const auto& row : result.table->rows) { + if (row.from != from) { + continue; + } + ++rowCount; + const size_t changes = (row.input[0] != boundary.current[0]) + + (row.input[1] != boundary.current[1]); + EXPECT_LE(changes, 1); + if (changes == 0) { + ++stutters; + EXPECT_EQ(row.to, from); + } + const auto replay = certifyEpisode(model, {model.admit(boundary, row.input)}); + ASSERT_TRUE(replay.certified()) << replay.detail; + EXPECT_EQ(replay.stableStates.front(), result.table->boundaries[row.to]); + } + EXPECT_EQ(rowCount, 3); + EXPECT_EQ(stutters, 1); + } +} + +TEST(LatchSettlingCompilerTests, WorkerScheduleDoesNotChangeCompiledTransitions) { + auto network = simpleLatchNetwork(); + network.netCount = 4; + network.primitives.push_back(latch("second_latch", 0, 1, 3)); + auto options = simpleLatchOptions(); + options.initialStorage.push_back({uint8_t{1}}); + const auto serial = compileTransitionTable(EventModel(network, {}, 1), options); + const auto parallel = compileTransitionTable(EventModel(network, {}, 4), options); + ASSERT_TRUE(serial.certified()) << serial.detail; + ASSERT_TRUE(parallel.certified()) << parallel.detail; + EXPECT_EQ(serial.table->boundaries, parallel.table->boundaries); + ASSERT_EQ(serial.table->rows.size(), parallel.table->rows.size()); + for (size_t index = 0; index < serial.table->rows.size(); ++index) { + EXPECT_EQ(serial.table->rows[index].from, parallel.table->rows[index].from); + EXPECT_EQ(serial.table->rows[index].input, parallel.table->rows[index].input); + EXPECT_EQ(serial.table->rows[index].to, parallel.table->rows[index].to); + } + EXPECT_EQ(serial.table->maxWaves, parallel.table->maxWaves); +} + +TEST(LatchSettlingCompilerTests, BootstrapUniversallyChecksAuxiliaryGateSeeds) { + Network network; + network.netCount = 4; + network.externalInputs = {0, 1}; + network.primitives.push_back(combinational( + "buffer", {0}, {2}, [](const Bits& inputs) { return inputs; })); + network.primitives.push_back(latch("held", 2, 1, 3)); + const EventModel model(network); + const auto result = certifyBootstrap(model, {1, 0}, {{}, {0}}); + ASSERT_TRUE(result.certified()) << result.detail; + ASSERT_EQ(result.stableStates.size(), 1); + EXPECT_EQ(result.stableStates.front().current, (Bits{1, 0, 1, 0})); +} + +TEST(LatchSettlingCompilerTests, GeneratedStartupClockCannotChooseConvenientSeed) { + Network network; + network.netCount = 4; + network.externalInputs = {0, 1}; + network.primitives.push_back(combinational( + "clock_buffer", {1}, {2}, [](const Bits& inputs) { return inputs; })); + network.primitives.push_back(flipFlop("capture", 0, 2, 3)); + const EventModel model(network); + const auto result = certifyBootstrap(model, {1, 1}, {{}, {0}}); + EXPECT_EQ(result.status, CertificationStatus::OrderDependent) << result.detail; + ASSERT_EQ(result.stableStates.size(), 2); + EXPECT_NE(result.stableStates[0].storage[1], result.stableStates[1].storage[1]); +} + +TEST(LatchSettlingCompilerTests, ProjectionReadsOverwrittenStorageSeedsUniversally) { + Network network; + network.netCount = 3; + network.externalInputs = {2}; + Primitive projected; + projected.name = "projected_clock"; + projected.inputs = {1}; + projected.outputs = {0}; + projected.storageBits = 1; + projected.initialOutputValues = [](const Bits&, const Bits& pins) { return pins; }; + projected.react = [](const Bits& storage, const Bits&, const Bits&, + std::optional, bool) { + return Reaction{storage, {0}, false, {}}; + }; + network.primitives = {projected, flipFlop("fall_capture", 2, 0, 1, false)}; + // The second output is initialized from stored zero, but its PRE-projection + // seed is read by the first output's projection. Seed one creates a falling + // clock event after BOOT; seed zero does not. Enumerating only gate outputs + // would miss this distinction because this network has no zero-storage gates. + const auto result = certifyBootstrap(EventModel(network), {1}, {{0}, {0}}); + EXPECT_EQ(result.status, CertificationStatus::OrderDependent) << result.detail; +} + +TEST(LatchSettlingCompilerTests, SingleExternalChangeDoesNotExcludeInternalRaces) { + Network network; + network.netCount = 4; + network.externalInputs = {0}; + network.primitives.push_back(combinational( + "data", {0}, {1}, [](const Bits& inputs) { return inputs; })); + network.primitives.push_back(combinational( + "enable", {0}, {2}, [](const Bits& inputs) { return Bits{uint8_t(!inputs[0])}; })); + network.primitives.push_back(latch("capture", 1, 2, 3)); + // BOOT at input zero ends with an open latch and known data zero, independently + // of auxiliary gate seeds. A single external rise then makes data rise and enable fall in + // the same internal wave. The external restriction must not discard either + // ordering at the latch pins. + CompileOptions options; + options.initialInputs = {0}; + options.initialStorage = {{}, {}, {uint8_t{0}}}; + options.singleExternalInputChange = true; + const auto result = compileTransitionTable(EventModel(network), options); + EXPECT_EQ(result.status, CertificationStatus::OrderDependent) << result.detail; + EXPECT_FALSE(result.table); +} + +TEST(LatchSettlingCompilerTests, BootstrapGateSeedLimitNeverMeansAllZeroSeeds) { + Network network; + network.netCount = 2; + network.externalInputs = {0}; + network.primitives.push_back(combinational( + "buffer", {0}, {1}, [](const Bits& inputs) { return inputs; })); + CompilerLimits limits; + limits.maxBootstrapSeedBits = 0; + const auto result = certifyBootstrap(EventModel(network), {0}, {{}}, limits); + EXPECT_EQ(result.status, CertificationStatus::ResourceLimit); +} + +TEST(LatchSettlingCompilerTests, ConstantStorageOutputDoesNotAddAuxiliarySeeds) { + CompilerLimits limits; + limits.maxBootstrapSeedBits = 0; + const auto result = certifyBootstrap(EventModel(selfLatchNetwork(false)), {}, {{1}}, limits); + ASSERT_TRUE(result.certified()) << result.detail; + EXPECT_EQ(result.stableStates.front().storage, (std::vector{{1}})); +} + +TEST(LatchSettlingCompilerTests, InvalidInitializationIsNotAnEmptyInitialRelation) { + const EventModel model(simpleLatchNetwork()); + auto options = simpleLatchOptions(); + options.initialInputs = {0}; + EXPECT_EQ(compileTransitionTable(model, options).status, CertificationStatus::Invalid); + options = simpleLatchOptions(); + options.initialStorage = {{uint8_t{2}}}; + EXPECT_EQ(compileTransitionTable(model, options).status, CertificationStatus::Invalid); + options.initialStorage = {{}}; + EXPECT_EQ(compileTransitionTable(model, options).status, CertificationStatus::Invalid); + options.initialStorage = {{uint8_t{0}}, {}}; + EXPECT_EQ(compileTransitionTable(model, options).status, CertificationStatus::Invalid); +} + +TEST(LatchSettlingCompilerTests, InitializationBudgetsDoNotSelectASubset) { + const EventModel model(selfLatchNetwork(false)); + CompileOptions options; + options.limits.maxInitialStorageBits = 0; + EXPECT_EQ(compileTransitionTable(model, options).status, CertificationStatus::ResourceLimit); + options = {}; + options.limits.maxInitialConfigurations = 1; + const auto result = compileTransitionTable(model, options); + EXPECT_EQ(result.status, CertificationStatus::ResourceLimit); + EXPECT_FALSE(result.table); +} + +TEST(LatchSettlingCompilerTests, InitializationLimitCoversStorageTimesAuxiliarySeeds) { + Network network; + network.netCount = 4; + network.externalInputs = {0, 1}; + network.primitives.push_back(combinational( + "buffer", {0}, {2}, [](const Bits& inputs) { return inputs; })); + network.primitives.push_back(latch("held", 2, 1, 3)); + CompileOptions options; + options.initialInputs = {0, 0}; + options.singleExternalInputChange = true; + options.limits.maxInitialConfigurations = 2; + // Two initial storage choices TIMES two independent auxiliary seed choices. + const auto result = compileTransitionTable(EventModel(network), options); + EXPECT_EQ(result.status, CertificationStatus::ResourceLimit); + EXPECT_FALSE(result.table); +} + +TEST(LatchSettlingCompilerTests, BoundaryTransactionAndInputLimitsRejectPartialTables) { + const EventModel model(simpleLatchNetwork()); + auto options = simpleLatchOptions(); + options.limits.maxBoundaryStates = 1; + auto result = compileTransitionTable(model, options); + EXPECT_EQ(result.status, CertificationStatus::ResourceLimit); + EXPECT_FALSE(result.table); + options = simpleLatchOptions(); + options.limits.maxTransactions = 1; + result = compileTransitionTable(model, options); + EXPECT_EQ(result.status, CertificationStatus::ResourceLimit); + EXPECT_FALSE(result.table); + options = simpleLatchOptions(); + options.limits.maxExternalBits = 1; + result = compileTransitionTable(model, options); + EXPECT_EQ(result.status, CertificationStatus::ResourceLimit); + EXPECT_FALSE(result.table); +} + +TEST(LatchSettlingCompilerTests, MissingBootstrapRuleCannotBeIgnored) { + auto network = simpleLatchNetwork(); + network.primitives[0].react = [](const Bits&, const Bits&, const Bits&, + std::optional, bool) { + return Reaction{{}, {}, true, "missing reaction"}; + }; + const auto result = compileTransitionTable(EventModel(network), simpleLatchOptions()); + EXPECT_EQ(result.status, CertificationStatus::Invalid); + EXPECT_FALSE(result.table); +} + +TEST(LatchSettlingCompilerTests, StatusNamesDistinguishUnknownFromNonsettling) { + EXPECT_STREQ(certificationStatusName(CertificationStatus::Certified), "certified"); + EXPECT_STREQ(certificationStatusName(CertificationStatus::NonSettling), "non-settling"); + EXPECT_STREQ(certificationStatusName(CertificationStatus::UnprovedBound), "unproved-bound"); + EXPECT_STREQ(certificationStatusName(CertificationStatus::ResourceLimit), "resource-limit"); +} + +} // namespace +} // namespace KEPLER_FORMAL::SEC::LATCH diff --git a/test/sec/LatchSymbolicCompilerTests.cpp b/test/sec/LatchSymbolicCompilerTests.cpp new file mode 100644 index 00000000..bdc4e85c --- /dev/null +++ b/test/sec/LatchSymbolicCompilerTests.cpp @@ -0,0 +1,485 @@ +// Copyright 2026 keplertech.io +// SPDX-License-Identifier: Apache-2.0 + +#include + +#include +#include +#include + +#include "BoolExprCache.h" +#include "latch/LatchSymbolicCompiler.h" + +namespace KEPLER_FORMAL::SEC::LATCH { +namespace { + +class LatchSymbolicCompilerTests : public ::testing::Test { + protected: + void TearDown() override { BoolExprCache::destroy(); } + + SymbolicNetwork lifted(Network reference) { + SymbolicNetwork result; + for (const auto& primitive : reference.primitives) { + result.primitives.push_back(liftPrimitive(primitive)); + } + result.reference = std::move(reference); + return result; + } + + SymbolicCompileOptions options(const Network& network, bool single = true) { + SymbolicCompileOptions result; + result.initialInputs.assign(network.externalInputs.size(), 0); + for (const auto& primitive : network.primitives) { + result.initialStorage.emplace_back(primitive.storageBits, 0); + } + result.singleExternalInputChange = single; + result.maxWaves = 32; + return result; + } + + Network latchNetwork() { + return Network{3, {0, 1}, {latch("latch", 0, 1, 2)}}; + } + + Bits flatten(const State& state) { + Bits result = state.current; + for (const auto& storage : state.storage) { + result.insert(result.end(), storage.begin(), storage.end()); + } + return result; + } + + Bits evaluate(const SymbolicBits& expressions, const SymbolicMacro& macro, + const Bits& state, const Bits& inputs) { + std::unordered_map values; + for (size_t i = 0; i < state.size(); ++i) values.emplace(macro.stateSymbols.at(i), state[i]); + for (size_t i = 0; i < inputs.size(); ++i) values.emplace(macro.inputSymbols.at(i), inputs[i]); + Bits result; + for (auto* expression : expressions) result.push_back(expression->evaluate(values)); + return result; + } + + Bits initialValues(const SymbolicMacro& macro, const Bits& parameters) { + std::unordered_map values; + for (size_t i = 0; i < parameters.size(); ++i) + values.emplace(macro.initialParameterSymbols.at(i), parameters[i]); + Bits result; + for (auto* expression : macro.initialStateExpressions) + result.push_back(expression->evaluate(values)); + return result; + } + + void compareWithFinite(const Network& network, bool single = true) { + const auto settings = options(network, single); + const auto symbolic = compileSymbolicNetwork(lifted(network), settings); + ASSERT_TRUE(symbolic.certified()) << symbolic.detail; + CompileOptions exactSettings; + exactSettings.initialInputs = settings.initialInputs; + exactSettings.singleExternalInputChange = single; + for (const auto& storage : settings.initialStorage) { + exactSettings.initialStorage.emplace_back(storage.begin(), storage.end()); + } + const auto exact = compileTransitionTable(EventModel(network), exactSettings); + ASSERT_TRUE(exact.certified()) << exact.detail; + const auto& macro = *symbolic.model; + EXPECT_EQ(macro.singleExternalInputChange, single); + EXPECT_EQ(macro.externalInputNets, network.externalInputs); + EXPECT_EQ(macro.initialState, flatten(exact.table->boundaries.at(exact.table->initials[0].boundary))); + for (const auto& row : exact.table->rows) { + const auto old = flatten(exact.table->boundaries[row.from]); + const auto expected = flatten(exact.table->boundaries[row.to]); + EXPECT_EQ(evaluate(macro.nextState, macro, old, row.input), expected); + EXPECT_EQ(evaluate(macro.observedNets, macro, old, row.input), + exact.table->boundaries[row.to].current); + } + } +}; + +TEST_F(LatchSymbolicCompilerTests, SingleInputLatchMatchesEveryExactTableRow) { + compareWithFinite(latchNetwork()); +} + +TEST_F(LatchSymbolicCompilerTests, IndependentFlopAndLatchMatchExactHistory) { + auto network = latchNetwork(); + network.netCount = 5; + network.externalInputs.push_back(3); + network.primitives.push_back(flipFlop("ff", 2, 3, 4)); + compareWithFinite(network); +} + +TEST_F(LatchSymbolicCompilerTests, AllChangeCombinationalModelMatchesExactTable) { + Network network{3, {0, 1}, {combinational("xor", {0, 1}, {2}, [](const Bits& pins) { + return Bits{uint8_t(pins[0] ^ pins[1])}; + })}}; + compareWithFinite(network, false); +} + +TEST_F(LatchSymbolicCompilerTests, StoredSelfFeedbackIsNotClassifiedAsOscillation) { + Network network{2, {}, {latch("self", 0, 1, 0)}}; + network.constantByNet = {std::nullopt, true}; + compareWithFinite(network, false); +} + +TEST_F(LatchSymbolicCompilerTests, CapturesNegativeClockEdgesExactly) { + Network network{3, {0, 1}, {flipFlop("falling", 0, 1, 2, false)}}; + compareWithFinite(network); +} + +TEST_F(LatchSymbolicCompilerTests, WideStateAndInputSpacesNeedNoValuationEnumeration) { + Network network; + constexpr size_t width = 16; + network.netCount = width * 3; + for (size_t input = 0; input < width * 2; ++input) network.externalInputs.push_back(input); + for (size_t bit = 0; bit < width; ++bit) { + network.primitives.push_back(latch("l" + std::to_string(bit), + bit * 2, bit * 2 + 1, width * 2 + bit)); + } + const auto result = compileSymbolicNetwork(lifted(network), options(network)); + ASSERT_TRUE(result.certified()) << result.detail; + EXPECT_EQ(result.model->inputSymbols.size(), 32); + EXPECT_EQ(result.model->stateSymbols.size(), 64); + Bits state = result.model->initialState; + Bits input(network.externalInputs.size(), 0); + input[0] = 1; + state = evaluate(result.model->nextState, *result.model, state, input); + EXPECT_EQ(state[32], 0); + input[1] = 1; + state = evaluate(result.model->nextState, *result.model, state, input); + EXPECT_EQ(state[32], 1); + EXPECT_EQ(state[48], 1); + EXPECT_EQ(state[33], 0); +} + +TEST_F(LatchSymbolicCompilerTests, GrowingBoundHandlesAChainBeyondTwelveStateBits) { + Network network; + constexpr size_t length = 13; + network.netCount = length + 2; + network.externalInputs = {0, 1}; + for (size_t stage = 0; stage < length; ++stage) { + network.primitives.push_back(latch("stage" + std::to_string(stage), + stage ? stage + 1 : 0, 1, stage + 2)); + } + auto settings = options(network); + settings.maxWaves = 16; + const auto result = compileSymbolicNetwork(lifted(network), settings); + ASSERT_TRUE(result.certified()) << result.detail; + EXPECT_GE(result.model->transitionWaves, length); + EXPECT_LE(result.model->transitionWaves, settings.maxWaves); + auto state = result.model->initialState; + state = evaluate(result.model->nextState, *result.model, state, {1, 0}); + state = evaluate(result.model->nextState, *result.model, state, {1, 1}); + EXPECT_EQ(state[length + 1], 1); +} + +TEST_F(LatchSymbolicCompilerTests, SimultaneousClosingDataRaceIsNotAProvenTransition) { + const auto network = latchNetwork(); + const auto result = compileSymbolicNetwork(lifted(network), options(network, false)); + EXPECT_EQ(result.status, CertificationStatus::UnprovedBound) << result.detail; + EXPECT_FALSE(result.model); +} + +TEST_F(LatchSymbolicCompilerTests, NonsettlingBootstrapIsUnprovedNotSilentlyExcluded) { + Network network{3, {}, {latch("self", 2, 1, 0), + combinational("invert", {0}, {2}, [](const Bits& pins) { return Bits{uint8_t(!pins[0])}; })}}; + network.constantByNet = {std::nullopt, true, std::nullopt}; + auto settings = options(network); + settings.maxWaves = 8; + const auto result = compileSymbolicNetwork(lifted(network), settings); + EXPECT_EQ(result.status, CertificationStatus::UnprovedBound) << result.detail; + EXPECT_FALSE(result.model); +} + +TEST_F(LatchSymbolicCompilerTests, ATooSmallCandidateDoesNotClaimConvergence) { + Network network{5, {0, 1}, {latch("first", 0, 1, 2), + latch("second", 2, 1, 3), latch("third", 3, 1, 4)}}; + auto settings = options(network); + settings.maxWaves = 1; + const auto result = compileSymbolicNetwork(lifted(network), settings); + EXPECT_EQ(result.status, CertificationStatus::UnprovedBound) << result.detail; + EXPECT_FALSE(result.model); +} + +TEST_F(LatchSymbolicCompilerTests, BootstrapErrorsAreNotPrunedOrAbsorbedAsSuccess) { + auto network = latchNetwork(); + auto symbolic = lifted(network); + // Reference callback error must remain represented through every wave. + symbolic.primitives[0].react = [](const SymbolicBits& storage, + const SymbolicBits&, const SymbolicBits&, std::optional, bool) { + return SymbolicReaction{storage, storage, BoolExpr::createTrue()}; + }; + auto settings = options(network); + settings.maxWaves = 2; + const auto result = compileSymbolicNetwork(symbolic, settings); + EXPECT_EQ(result.status, CertificationStatus::UnprovedBound) << result.detail; + EXPECT_FALSE(result.model); +} + +TEST_F(LatchSymbolicCompilerTests, CandidateInvariantFailureIsNotAReachableBugReport) { + Primitive cell; + cell.name = "unreachable_error"; + cell.inputs = {0}; + cell.outputs = {1}; + cell.storageBits = 1; + cell.react = [](const Bits& storage, const Bits&, const Bits& pins, + std::optional, bool) { + return Reaction{storage, storage, bool(storage[0] && pins[0])}; + }; + Network network{2, {0}, {cell}}; + const auto settings = options(network); + const auto symbolic = compileSymbolicNetwork(lifted(network), settings); + EXPECT_EQ(symbolic.status, CertificationStatus::UnprovedBound) << symbolic.detail; + EXPECT_FALSE(symbolic.model); + // The error is not reachable from intended storage=0. The generic invariant + // nevertheless includes storage=1/input=0, so rejecting the symbolic proof + // cannot be reported as a reachable circuit bug. The exact fallback succeeds. + CompileOptions exactSettings; + exactSettings.initialInputs = {0}; + exactSettings.initialStorage = {{false}}; + exactSettings.singleExternalInputChange = true; + EXPECT_TRUE(compileTransitionTable(EventModel(network), exactSettings).certified()); +} + +TEST_F(LatchSymbolicCompilerTests, UniquenessIncludesHiddenStorageNotOnlyOutputs) { + auto cell = latch("hidden", 0, 1, 2); + const auto reaction = cell.react; + cell.react = [reaction](const Bits& storage, const Bits& before, const Bits& after, + std::optional changedPin, bool bootstrap) { + auto result = reaction(storage, before, after, changedPin, bootstrap); + result.outputs = {0}; + return result; + }; + cell.initialOutputs = [](const Bits&) { return Bits{0}; }; + Network network{3, {0, 1}, {cell}}; + const auto result = compileSymbolicNetwork(lifted(network), options(network, false)); + EXPECT_EQ(result.status, CertificationStatus::UnprovedBound) << result.detail; + EXPECT_FALSE(result.model); +} + +TEST_F(LatchSymbolicCompilerTests, NonPowerOfTwoLimitIsCheckedWithoutOvershooting) { + Network network{5, {0, 1}, {latch("first", 0, 1, 2), + latch("second", 2, 1, 3), latch("third", 3, 1, 4)}}; + auto settings = options(network); + settings.maxWaves = 3; + const auto result = compileSymbolicNetwork(lifted(network), settings); + ASSERT_TRUE(result.certified()) << result.detail; + EXPECT_EQ(result.model->transitionWaves, 3); +} + +TEST_F(LatchSymbolicCompilerTests, ZeroWaveLimitDoesNotSkipBootstrap) { + const auto network = latchNetwork(); + auto settings = options(network); + settings.maxWaves = 0; + const auto result = compileSymbolicNetwork(lifted(network), settings); + EXPECT_EQ(result.status, CertificationStatus::UnprovedBound) << result.detail; + EXPECT_FALSE(result.model); +} + +TEST_F(LatchSymbolicCompilerTests, AuxiliaryBootstrapSeedsRemainIndependent) { + Network network{4, {0, 1}, {combinational("clock", {1}, {2}, + [](const Bits& pins) { return pins; }), flipFlop("capture", 0, 2, 3)}}; + auto settings = options(network); + settings.initialInputs = {1, 1}; + const auto result = compileSymbolicNetwork(lifted(network), settings); + EXPECT_EQ(result.status, CertificationStatus::OrderDependent) << result.detail; + EXPECT_FALSE(result.model); +} + +TEST_F(LatchSymbolicCompilerTests, InsufficientNodeAndSatBudgetsNeverReturnPartialModels) { + const auto network = latchNetwork(); + auto settings = options(network); + settings.maxNodes = 2; + auto result = compileSymbolicNetwork(lifted(network), settings); + EXPECT_EQ(result.status, CertificationStatus::ResourceLimit); + EXPECT_FALSE(result.model); + settings = options(network); + settings.maxSatConflicts = 0; + result = compileSymbolicNetwork(lifted(network), settings); + EXPECT_EQ(result.status, CertificationStatus::ResourceLimit); + EXPECT_FALSE(result.model); + settings = options(network); + settings.maxSatDecisions = 0; + result = compileSymbolicNetwork(lifted(network), settings); + EXPECT_EQ(result.status, CertificationStatus::ResourceLimit); + EXPECT_FALSE(result.model); +} + +TEST_F(LatchSymbolicCompilerTests, InvalidInitializationIsRejected) { + const auto network = latchNetwork(); + auto settings = options(network); + settings.initialInputs = {0}; + EXPECT_EQ(compileSymbolicNetwork(lifted(network), settings).status, CertificationStatus::Invalid); + settings = options(network); + settings.initialStorage = {{2}}; + EXPECT_EQ(compileSymbolicNetwork(lifted(network), settings).status, CertificationStatus::Invalid); + settings = options(network); + settings.initialStorage = {{}}; + EXPECT_EQ(compileSymbolicNetwork(lifted(network), settings).status, CertificationStatus::Invalid); +} + +TEST_F(LatchSymbolicCompilerTests, UnspecifiedInputsAndStorageRetainEveryBooleanBootstrapOrigin) { + const auto network = latchNetwork(); + SymbolicCompileOptions settings; + settings.singleExternalInputChange = true; + const auto compiled = compileSymbolicNetwork(lifted(network), settings); + ASSERT_TRUE(compiled.certified()) << compiled.detail; + const auto& macro = *compiled.model; + EXPECT_TRUE(macro.initialState.empty()); + ASSERT_EQ(macro.initialParameterSymbols.size(), 3u); + ASSERT_EQ(macro.initialInputSymbols.size(), 2u); + ASSERT_EQ(macro.initialStorageSymbols.size(), 1u); + EXPECT_EQ(macro.initialInputSymbols[0], macro.initialParameterSymbols[0]); + EXPECT_EQ(macro.initialInputSymbols[1], macro.initialParameterSymbols[1]); + EXPECT_EQ(macro.initialStorageSymbols[0][0], macro.initialParameterSymbols[2]); + for (size_t code = 0; code < 8; ++code) { + const Bits input{uint8_t(code & 1), uint8_t((code >> 1) & 1)}; + const Bits stored{uint8_t((code >> 2) & 1)}; + const auto expected = certifyBootstrap(EventModel(network), input, {stored}); + ASSERT_TRUE(expected.certified()) << expected.detail; + const auto state = initialValues(macro, {input[0], input[1], stored[0]}); + EXPECT_EQ(state, flatten(expected.stableStates.front())); + EXPECT_EQ(evaluate(macro.nextState, macro, state, input), state); + EXPECT_EQ(state[2], input[1] ? input[0] : stored[0]); + } +} + +TEST_F(LatchSymbolicCompilerTests, MixedPerBitRestrictionsDoNotFixUnspecifiedOrigins) { + SymbolicCompileOptions settings; + settings.singleExternalInputChange = true; + settings.initialInputValues = {std::nullopt, uint8_t(0)}; + settings.initialStorageValues = {{uint8_t(1)}}; + const auto compiled = compileSymbolicNetwork(lifted(latchNetwork()), settings); + ASSERT_TRUE(compiled.certified()) << compiled.detail; + const auto& macro = *compiled.model; + ASSERT_EQ(macro.initialParameterSymbols.size(), 1u); + EXPECT_TRUE(macro.initialInputSymbols[0]); + EXPECT_FALSE(macro.initialInputSymbols[1]); + EXPECT_FALSE(macro.initialStorageSymbols[0][0]); + EXPECT_EQ(initialValues(macro, {0}), (Bits{0, 0, 1, 1})); + EXPECT_EQ(initialValues(macro, {1}), (Bits{1, 0, 1, 1})); +} + +TEST_F(LatchSymbolicCompilerTests, UnknownSelfHeldStorageIsHistoryNotSchedulerNondeterminism) { + Network network{2, {}, {latch("self", 0, 1, 0)}}; + network.constantByNet = {std::nullopt, true}; + const auto compiled = compileSymbolicNetwork(lifted(network), {}); + ASSERT_TRUE(compiled.certified()) << compiled.detail; + const auto& macro = *compiled.model; + ASSERT_EQ(macro.initialParameterSymbols.size(), 1u); + EXPECT_TRUE(macro.initialInputSymbols.empty()); + for (bool value : {false, true}) { + const auto state = initialValues(macro, {uint8_t(value)}); + EXPECT_EQ(state, (Bits{uint8_t(value), 1, uint8_t(value)})); + EXPECT_EQ(evaluate(macro.nextState, macro, state, {}), state); + } +} + +TEST_F(LatchSymbolicCompilerTests, UnknownInitialClockDoesNotFabricateAnEdge) { + Network network{3, {0, 1}, {flipFlop("ff", 0, 1, 2)}}; + SymbolicCompileOptions settings; + settings.singleExternalInputChange = true; + const auto compiled = compileSymbolicNetwork(lifted(network), settings); + ASSERT_TRUE(compiled.certified()) << compiled.detail; + const auto& macro = *compiled.model; + for (size_t code = 0; code < 8; ++code) { + const uint8_t data = code & 1, clock = (code >> 1) & 1, stored = (code >> 2) & 1; + const auto state = initialValues(macro, {data, clock, stored}); + EXPECT_EQ(state, (Bits{data, clock, stored, stored})); + EXPECT_EQ(evaluate(macro.nextState, macro, state, {data, clock}), state); + const auto changed = evaluate(macro.nextState, macro, state, {data, uint8_t(!clock)}); + EXPECT_EQ(changed[2], clock ? stored : data); + } +} + +TEST_F(LatchSymbolicCompilerTests, BootstrapResetCanEliminateUnknownStorageWithoutChoosingItsValue) { + Primitive cell; + cell.name = "async_clear"; + cell.inputs = {0}; cell.outputs = {1}; cell.storageBits = 1; + cell.react = [](const Bits& storage, const Bits&, const Bits& pins, + std::optional, bool) { + const Bits next{uint8_t(pins[0] ? 0 : storage[0])}; + return Reaction{next, next}; + }; + Network network{2, {0}, {cell}}; + SymbolicCompileOptions settings; + settings.initialInputValues = {uint8_t(1)}; + const auto compiled = compileSymbolicNetwork(lifted(network), settings); + ASSERT_TRUE(compiled.certified()) << compiled.detail; + const auto& macro = *compiled.model; + ASSERT_EQ(macro.initialParameterSymbols.size(), 1u); + EXPECT_EQ(macro.initialState, (Bits{1, 0, 0})); + EXPECT_EQ(initialValues(macro, {0}), macro.initialState); + EXPECT_EQ(initialValues(macro, {1}), macro.initialState); +} + +TEST_F(LatchSymbolicCompilerTests, AnInvalidUnknownBootstrapOriginCannotDisappear) { + auto cell = latch("reject_one", 0, 1, 2); + const auto react = cell.react; + cell.react = [react](const Bits& storage, const Bits& before, const Bits& pins, + std::optional pin, bool boot) { + auto result = react(storage, before, pins, pin, boot); + result.error = boot && storage[0]; + return result; + }; + Network network{3, {0, 1}, {cell}}; + auto settings = options(network); + settings.initialStorage.clear(); + settings.maxWaves = 4; + const auto compiled = compileSymbolicNetwork(lifted(network), settings); + EXPECT_EQ(compiled.status, CertificationStatus::UnprovedBound) << compiled.detail; + EXPECT_FALSE(compiled.model); +} + +TEST_F(LatchSymbolicCompilerTests, OriginRestrictionShapesAndConflictingFormsAreRejected) { + const auto network = latchNetwork(); + auto settings = options(network); + settings.initialInputValues = {std::nullopt, std::nullopt}; + EXPECT_EQ(compileSymbolicNetwork(lifted(network), settings).status, CertificationStatus::Invalid); + settings = options(network); + settings.initialStorageValues = {{std::nullopt}}; + EXPECT_EQ(compileSymbolicNetwork(lifted(network), settings).status, CertificationStatus::Invalid); + settings = {}; + settings.initialInputValues = {std::nullopt}; + EXPECT_EQ(compileSymbolicNetwork(lifted(network), settings).status, CertificationStatus::Invalid); + settings.initialInputValues = {uint8_t(2), std::nullopt}; + EXPECT_EQ(compileSymbolicNetwork(lifted(network), settings).status, CertificationStatus::Invalid); + settings = {}; + settings.initialStorageValues = {{}}; + EXPECT_EQ(compileSymbolicNetwork(lifted(network), settings).status, CertificationStatus::Invalid); + settings.initialStorageValues = {{uint8_t(2)}}; + EXPECT_EQ(compileSymbolicNetwork(lifted(network), settings).status, CertificationStatus::Invalid); +} + +TEST_F(LatchSymbolicCompilerTests, NonzeroSatBudgetExhaustionDoesNotBecomeAProof) { + Network network{5, {0, 1}, {latch("first", 0, 1, 2), + latch("second", 2, 1, 3), latch("third", 3, 1, 4)}}; + auto settings = options(network); + settings.maxSatConflicts = 1; + settings.maxSatDecisions = 1; + const auto result = compileSymbolicNetwork(lifted(network), settings); + EXPECT_EQ(result.status, CertificationStatus::ResourceLimit) << result.detail; + EXPECT_FALSE(result.model); +} + +TEST_F(LatchSymbolicCompilerTests, ParallelWorkersPreserveTheCompiledFunction) { + Network network{4, {0, 1}, {latch("a", 0, 1, 2), latch("b", 0, 1, 3)}}; + auto settings = options(network); + settings.workers = 1; + const auto serial = compileSymbolicNetwork(lifted(network), settings); + settings.workers = 4; + const auto parallel = compileSymbolicNetwork(lifted(network), settings); + ASSERT_TRUE(serial.certified()) << serial.detail; + ASSERT_TRUE(parallel.certified()) << parallel.detail; + EXPECT_EQ(serial.model->initialState, parallel.model->initialState); + EXPECT_EQ(serial.model->stateSymbols, parallel.model->stateSymbols); + EXPECT_EQ(serial.model->inputSymbols, parallel.model->inputSymbols); + auto state = serial.model->initialState; + for (const auto& input : std::vector{{1, 0}, {1, 1}, {0, 1}, {0, 0}, {1, 0}}) { + const auto a = evaluate(serial.model->nextState, *serial.model, state, input); + const auto b = evaluate(parallel.model->nextState, *parallel.model, state, input); + EXPECT_EQ(a, b); + state = a; + } +} + +} // namespace +} // namespace KEPLER_FORMAL::SEC::LATCH diff --git a/test/sec/LatchSymbolicIntegrationTests.cpp b/test/sec/LatchSymbolicIntegrationTests.cpp new file mode 100644 index 00000000..31430528 --- /dev/null +++ b/test/sec/LatchSymbolicIntegrationTests.cpp @@ -0,0 +1,236 @@ +// Copyright 2026 keplertech.io +// SPDX-License-Identifier: Apache-2.0 +#include +#include +#include +#include +#include +#include "BoolExprCache.h" +#include "DNL.h" +#include "NLDB.h" +#include "NLDB0.h" +#include "NLLibrary.h" +#include "NLUniverse.h" +#include "SNLDesign.h" +#include "SNLBusTerm.h" +#include "SNLBusTermBit.h" +#include "SNLDesignModeling.h" +#include "SNLInstance.h" +#include "SNLScalarNet.h" +#include "SNLScalarTerm.h" +#include "latch/LatchSupportOptions.h" +#include "latch/LatchSymbolicEncoding.h" +#include "latch/NajaEventPrimitive.h" +#include "model/SequentialDesignModel.h" + +namespace KEPLER_FORMAL::SEC::LATCH { +namespace { +using namespace naja::NL; +class LatchSymbolicIntegrationTests : public ::testing::Test { + protected: + void SetUp() override { + auto* db = NLDB::create(NLUniverse::create()); + designs = NLLibrary::create(db, NLName("designs")); + primitives = NLLibrary::create(db, NLLibrary::Type::Primitives, NLName("primitives")); + } + void TearDown() override { + naja::DNL::destroy(); + if (auto* universe = NLUniverse::get()) universe->destroy(); + BoolExprCache::destroy(); + } + SNLScalarNet* port(SNLDesign* top, const std::string& name, SNLTerm::Direction dir) { + auto* net = SNLScalarNet::create(top, NLName(name)); + SNLScalarTerm::create(top, dir, NLName(name))->setNet(net); + return net; + } + SNLDesign* wideLatchCone(size_t width) { + auto* gate = NLDB0::getOrCreateNInputGate(NLDB0::GateType::And, width); + std::vector pins; + for (auto* bit : NLDB0::getGateNTerms(gate)->getBusBits()) pins.push_back(bit); + auto* result = NLDB0::getGateSingleTerm(gate); + auto* top = SNLDesign::create(designs, NLName("top")); + auto* enable = port(top, "enable", SNLTerm::Direction::Input); + auto* reduction = SNLInstance::create(top, gate, NLName("reduction")); + reduction->getInstTerm(result)->setNet(port(top, "out", SNLTerm::Direction::Output)); + for (size_t i = 0; i < width; ++i) { + std::ostringstream name; + name << "data" << std::setw(4) << std::setfill('0') << i; + auto* latch = SNLInstance::create(top, NLDB0::getDLatch(), NLName("latch" + std::to_string(i))); + latch->getInstTerm(NLDB0::getDLatchData())->setNet(port(top, name.str(), SNLTerm::Direction::Input)); + latch->getInstTerm(NLDB0::getDLatchEnable())->setNet(enable); + auto* q = SNLScalarNet::create(top, NLName("q" + std::to_string(i))); + latch->getInstTerm(NLDB0::getDLatchOutput())->setNet(q); + reduction->getInstTerm(pins[i])->setNet(q); + } + return top; + } + SupportOptions options() { + SupportOptions result; + result.enabled = true; result.singleInputChange = true; + result.initialInputs = false; result.initialStorage = false; result.workers = 2; + return result; + } + std::unordered_map initial(const SequentialDesignModel& model) { + std::unordered_map result; + for (const auto& key : model.stateBits) + result.emplace(model.inputVarByKey.at(key), model.initialStateValueByKey.at(key)); + return result; + } + bool step(const SequentialDesignModel& model, std::unordered_map& state, + size_t selector, bool value) { + auto environment = state; + for (const auto& key : model.environmentInputs) { + const auto& name = model.displayNameByKey.at(key); + bool bit = name == "$event.value" && value; + if (name.starts_with("$event.select[")) bit = (selector >> std::stoul(name.substr(14))) & 1; + environment[model.inputVarByKey.at(key)] = bit; + } + const auto output = model.observedOutputExprByKey.at(model.observedOutputs.at(0))->evaluate(environment); + for (const auto& key : model.stateBits) + state[model.inputVarByKey.at(key)] = model.nextStateExprByStateKey.at(key)->evaluate(environment); + return output; + } + NLLibrary* designs = nullptr; + NLLibrary* primitives = nullptr; +}; + +TEST_F(LatchSymbolicIntegrationTests, ModelsWideConnectedLatchConeBeyondFiniteEnumerationLimits) { + constexpr size_t width = 32; + auto* top = wideLatchCone(width); + auto config = options(); + // Neither a state/input table nor bootstrap seed enumeration may prove this. + config.limits.maxBoundaryStates = 1; + config.limits.maxExternalBits = 1; + config.limits.maxBootstrapSeedBits = 1; + ScopedSupportOptions scope(config); + const auto model = SequentialDesignModel::extract(top); + ASSERT_TRUE(model.unsupportedReasons.empty()); + ASSERT_EQ(model.observedOutputs.size(), 1u) << (model.connectivitySkipInfoByKey.empty() + ? "no skip diagnostic" : model.connectivitySkipInfoByKey.begin()->second.detail); + EXPECT_TRUE(model.skippedObservedOutputs.empty()); + EXPECT_GT(model.stateBits.size(), 64u); + auto state = initial(model); + EXPECT_FALSE(step(model, state, width, true)); + for (size_t i = 0; i < width; ++i) EXPECT_EQ(step(model, state, i, true), i + 1 == width); + EXPECT_TRUE(step(model, state, width, false)); + for (size_t i = 0; i < width; ++i) EXPECT_TRUE(step(model, state, i, false)); + EXPECT_FALSE(step(model, state, width, true)); +} + +TEST_F(LatchSymbolicIntegrationTests, ExhaustingBothCompilersLeavesConeOpaque) { + auto* top = wideLatchCone(16); + auto config = options(); + config.maxSymbolicNodes = 1; + config.limits.maxExternalBits = 1; + ScopedSupportOptions scope(config); + const auto model = SequentialDesignModel::extract(top); + EXPECT_TRUE(model.observedOutputs.empty()); + ASSERT_EQ(model.skippedObservedOutputs.size(), 1u); + const auto& reason = model.connectivitySkipInfoByKey.at(model.skippedObservedOutputs[0]).detail; + EXPECT_NE(reason.find("symbolic"), std::string::npos) << reason; +} + +TEST_F(LatchSymbolicIntegrationTests, NativeNajaSymbolicCallbacksMatchConcreteStorageAndOutputs) { + for (auto* cell : {NLDB0::getDLatch(), NLDB0::getDFF()}) { + auto* top = SNLDesign::create(designs); + auto* instance = SNLInstance::create(top, cell); + std::map nets; + for (auto* term : cell->getBitTerms()) nets.emplace(term, nets.size()); + SymbolicPrimitive symbolic; + const auto concrete = makeNajaEventPrimitive(instance, "cell", nets, &symbolic); + ASSERT_EQ(concrete.inputs.size(), 2u); + SymbolicBits old{BoolExpr::Var(2)}, before{BoolExpr::Var(3), BoolExpr::Var(4)}, now{BoolExpr::Var(5), BoolExpr::Var(6)}; + for (bool boot : {false, true}) for (int pin = -1; pin < 2; ++pin) { + const auto changed = pin < 0 ? std::optional{} : size_t(pin); + const auto reaction = symbolic.react(old, before, now, changed, boot); + for (size_t code = 0; code < 32; ++code) { + std::unordered_map environment; + for (size_t i = 0; i < 5; ++i) environment.emplace(i + 2, (code >> i) & 1); + const auto expected = concrete.react({uint8_t(code & 1)}, + {uint8_t((code >> 1) & 1), uint8_t((code >> 2) & 1)}, + {uint8_t((code >> 3) & 1), uint8_t((code >> 4) & 1)}, changed, boot); + EXPECT_EQ(reaction.error->evaluate(environment), expected.error); + EXPECT_EQ(reaction.storage[0]->evaluate(environment), expected.storage[0]); + EXPECT_EQ(reaction.outputs[0]->evaluate(environment), expected.outputs[0]); + } + } + } +} + +TEST_F(LatchSymbolicIntegrationTests, MacroEncodingSubstitutesSimultaneouslyAndRejectsHiddenChoices) { + Network network{1, {0}, {}, {}}; + SymbolicMacro macro; + macro.externalInputNets = network.externalInputs; + macro.stateSymbols = {2}; macro.inputSymbols = {3}; macro.initialState = {0}; + macro.nextState = {BoolExpr::Var(3)}; macro.observedNets = macro.nextState; + const auto encoded = encodeSymbolicMacro(macro, network, {BoolExpr::Var(3)}, {BoolExpr::Var(2)}, false); + EXPECT_EQ(encoded.nextState[0], BoolExpr::Var(2)); + macro.nextState[0] = BoolExpr::Var(99); + EXPECT_THROW(encodeSymbolicMacro(macro, network, {BoolExpr::Var(3)}, {BoolExpr::Var(2)}, false), std::invalid_argument); +} + +TEST_F(LatchSymbolicIntegrationTests, EncoderCannotBroadenCertificateOrRelabelInputs) { + Network network{2, {0}, {}, {}}; + SymbolicMacro macro; + macro.stateSymbols = {2, 3}; macro.inputSymbols = {4}; macro.initialState = {0, 0}; + macro.nextState = {BoolExpr::Var(4), BoolExpr::Var(3)}; + macro.observedNets = macro.nextState; + macro.externalInputNets = {0}; macro.singleExternalInputChange = true; + const SymbolicBits state{BoolExpr::Var(10), BoolExpr::Var(11)}, input{BoolExpr::Var(12)}; + EXPECT_THROW(encodeSymbolicMacro(macro, network, state, input, false), std::invalid_argument); + const SymbolicBits selector{BoolExpr::Var(13)}; + EXPECT_NO_THROW(encodeSymbolicMacro(macro, network, state, input, true, selector, BoolExpr::Var(14), {0})); + network.externalInputs = {1}; + EXPECT_THROW(encodeSymbolicMacro(macro, network, state, input, true, selector, BoolExpr::Var(14), {0}), std::invalid_argument); + network.externalInputs = {0}; + EXPECT_THROW(encodeSymbolicMacro(macro, network, state, input, true, {nullptr}, BoolExpr::Var(14), {0}), std::invalid_argument); +} + +TEST_F(LatchSymbolicIntegrationTests, InitialRelationPreservesAllOriginsAndSharedInputCorrelation) { + SymbolicMacro macro; + macro.stateSymbols = {2, 3}; + macro.inputSymbols = {4}; + macro.initialParameterSymbols = {5, 6}; + macro.initialStateExpressions = {BoolExpr::Var(5), BoolExpr::Xor(BoolExpr::Var(5), BoolExpr::Var(6))}; + const SymbolicBits state{BoolExpr::Var(10), BoolExpr::Var(11)}; + const SymbolicBits origins{BoolExpr::Var(12), BoolExpr::Var(13)}; + auto* relation = encodeSymbolicInitialRelation(macro, state, origins); + for (size_t code = 0; code < 16; ++code) { + const bool first = code & 1, second = (code >> 1) & 1; + const bool input = (code >> 2) & 1, stored = (code >> 3) & 1; + EXPECT_EQ(relation->evaluate({{10, first}, {11, second}, {12, input}, {13, stored}}), + first == input && second == (input != stored)); + } + // Two components can share input origin 12 while their storage origins 13/14 + // stay distinct; the compiler never imposes an unintended storage equality. + const auto other = encodeSymbolicInitialState(macro, {origins[0], BoolExpr::Var(14)}); + EXPECT_EQ(other[0], origins[0]); + EXPECT_EQ(other[1]->getSupportVars(), (std::set{12, 14})); + // Replacement IDs may collide with local IDs; substitution is simultaneous. + const auto swapped = encodeSymbolicInitialState(macro, {BoolExpr::Var(6), BoolExpr::Var(5)}); + EXPECT_EQ(swapped[0], BoolExpr::Var(6)); + EXPECT_EQ(swapped[1], BoolExpr::Xor(BoolExpr::Var(6), BoolExpr::Var(5))); +} + +TEST_F(LatchSymbolicIntegrationTests, InitialEncodingRejectsHiddenChoicesAndOrdinaryFrameInputs) { + SymbolicMacro macro; + macro.stateSymbols = {2}; macro.inputSymbols = {3}; + macro.initialParameterSymbols = {4}; + macro.initialStateExpressions = {BoolExpr::Var(4)}; + EXPECT_THROW(encodeSymbolicInitialState(macro, {}), std::invalid_argument); + EXPECT_THROW(encodeSymbolicInitialState(macro, {nullptr}), std::invalid_argument); + for (size_t illegal : {size_t(2), size_t(3), size_t(99)}) { + macro.initialStateExpressions[0] = BoolExpr::Var(illegal); + EXPECT_THROW(encodeSymbolicInitialState(macro, {BoolExpr::Var(20)}), std::invalid_argument); + } + macro.initialStateExpressions = {BoolExpr::Var(4)}; + macro.initialParameterSymbols = {2}; + EXPECT_THROW(encodeSymbolicInitialState(macro, {BoolExpr::Var(20)}), std::invalid_argument); + macro.initialParameterSymbols = {4, 4}; + EXPECT_THROW(encodeSymbolicInitialState(macro, {BoolExpr::Var(20), BoolExpr::Var(21)}), std::invalid_argument); + macro.initialParameterSymbols.clear(); macro.initialStateExpressions.clear(); + macro.initialState = {1}; + EXPECT_EQ(encodeSymbolicInitialState(macro, {}), (SymbolicBits{BoolExpr::createTrue()})); +} +} // namespace +} // namespace KEPLER_FORMAL::SEC::LATCH diff --git a/test/sec/LatchSymbolicModelTests.cpp b/test/sec/LatchSymbolicModelTests.cpp new file mode 100644 index 00000000..dc59e476 --- /dev/null +++ b/test/sec/LatchSymbolicModelTests.cpp @@ -0,0 +1,456 @@ +// Copyright 2026 keplertech.io +// SPDX-License-Identifier: Apache-2.0 +#include +#include +#include + +#include "BoolExprCache.h" +#include "latch/LatchSymbolicModel.h" + +namespace KEPLER_FORMAL::SEC::LATCH { +namespace { +using Environment = std::unordered_map; + +class LatchSymbolicModelTests : public ::testing::Test { + protected: + void TearDown() override { BoolExprCache::destroy(); } + SymbolicNetwork lifted(Network network) { + SymbolicNetwork result; + result.reference = std::move(network); + for (const auto& primitive : result.reference.primitives) result.primitives.push_back(liftPrimitive(primitive)); + return result; + } + SymbolicBits constants(const Bits& bits) { + SymbolicBits result; + for (auto bit : bits) result.push_back(bit ? BoolExpr::createTrue() : BoolExpr::createFalse()); + return result; + } + SymbolicState constants(const State& state) { + SymbolicState result; + result.current = constants(state.current); result.previous = constants(state.previous); + result.active = constants(state.active); + for (const auto& storage : state.storage) result.storage.push_back(constants(storage)); + result.bootstrap = BoolExpr::Var(state.bootstrap); result.error = BoolExpr::Var(state.error); + return result; + } + SymbolicBits variables(size_t count, size_t& next) { + SymbolicBits result; + for (size_t i = 0; i < count; ++i) result.push_back(BoolExpr::Var(next++)); + return result; + } + Bits evaluate(const SymbolicBits& bits, const Environment& env) { + Bits result; + for (auto* bit : bits) result.push_back(bit->evaluate(env)); + return result; + } + State evaluate(const SymbolicState& state, const Environment& env) { + State result; + result.current = evaluate(state.current, env); result.previous = evaluate(state.previous, env); + result.active = evaluate(state.active, env); + for (const auto& storage : state.storage) result.storage.push_back(evaluate(storage, env)); + result.bootstrap = state.bootstrap->evaluate(env); result.error = state.error->evaluate(env); + return result; + } + State withoutReason(State state) { state.errorReason.clear(); return state; } + Environment assignment(size_t first, size_t count, size_t value, Environment base = {}) { + for (size_t bit = 0; bit < count; ++bit) base[first + bit] = (value >> bit) & 1; + return base; + } + void expectSuccessors(const EventModel& concrete, const SymbolicEventModel& symbolic, const State& state) { + size_t next = 1000; + const auto formula = symbolic.wave(constants(state), [&] { return BoolExpr::Var(next++); }); + ASSERT_LT(next - 1000, 12u); + std::set actual, expected; + for (size_t value = 0; value < (size_t{1} << (next - 1000)); ++value) + actual.insert(evaluate(formula, assignment(1000, next - 1000, value))); + for (const auto& successor : concrete.successors(state)) expected.insert(withoutReason(successor)); + EXPECT_EQ(actual, expected); + } + State settle(const EventModel& model, State state) { + for (size_t i = 0; i < 32 && !model.stable(state); ++i) { + const auto next = model.successors(state); + EXPECT_EQ(next.size(), 1u); + state = next.front(); + } + EXPECT_TRUE(model.stable(state)); + return state; + } +}; + +TEST_F(LatchSymbolicModelTests, ExhaustiveLatchWaveMatchesEveryCompleteConcreteState) { + const auto network = lifted({3, {0, 1}, {latch("l", 0, 1, 2)}}); + EventModel concrete(network.reference); + SymbolicEventModel symbolic(network); + size_t next = 2; + SymbolicState state; + state.current = variables(3, next); state.previous = variables(3, next); + state.storage = {variables(1, next)}; state.active = variables(1, next); + state.bootstrap = variables(1, next).front(); state.error = variables(1, next).front(); + size_t choice = 1000; + const auto result = symbolic.wave(state, [&] { return BoolExpr::Var(choice++); }); + for (size_t value = 0; value < (size_t{1} << (next - 2)); ++value) { + const auto env = assignment(2, next - 2, value); + const auto concreteState = evaluate(state, env); + std::set actual, expected; + for (size_t order = 0; order < (size_t{1} << (choice - 1000)); ++order) + actual.insert(evaluate(result, assignment(1000, choice - 1000, order, env))); + for (const auto& successor : concrete.successors(concreteState)) expected.insert(withoutReason(successor)); + EXPECT_EQ(actual, expected) << "assignment=" << value; + } +} + +TEST_F(LatchSymbolicModelTests, ExhaustiveFlipFlopWaveConsumesClockOnlyOnClockVisit) { + const auto network = lifted({3, {0, 1}, {flipFlop("f", 0, 1, 2)}}); + EventModel concrete(network.reference); + SymbolicEventModel symbolic(network); + for (size_t value = 0; value < 128; ++value) { + State state; + state.current = {uint8_t(value & 1), uint8_t((value >> 1) & 1), uint8_t((value >> 2) & 1)}; + state.previous = {uint8_t((value >> 3) & 1), uint8_t((value >> 4) & 1), uint8_t((value >> 5) & 1)}; + state.storage = {{uint8_t((value >> 6) & 1)}}; + state.active = {1}; + expectSuccessors(concrete, symbolic, state); + } +} + +TEST_F(LatchSymbolicModelTests, BootstrapMatchesAllInitialInputsStorageAndSeeds) { + const auto network = lifted({3, {0, 1}, {latch("l", 0, 1, 2)}}); + EventModel concrete(network.reference); + SymbolicEventModel symbolic(network); + size_t next = 2; + const auto inputs = variables(2, next), storage = variables(1, next), seeds = variables(3, next); + const auto boot = symbolic.bootstrap(inputs, {storage}, seeds); + for (size_t value = 0; value < 64; ++value) { + const auto env = assignment(2, 6, value); + const auto expected = concrete.bootstrap(evaluate(inputs, env), {evaluate(storage, env)}, evaluate(seeds, env)); + EXPECT_EQ(evaluate(boot, env), withoutReason(expected)); + expectSuccessors(concrete, symbolic, expected); + } +} + +TEST_F(LatchSymbolicModelTests, InputDependentBootstrapUsesFrozenSeedSnapshot) { + auto first = latch("a", 0, 1, 2), second = latch("b", 2, 1, 3); + first.initialOutputValues = second.initialOutputValues = [](const Bits& storage, const Bits& pins) { + return Bits{static_cast(storage[0] & pins[0])}; + }; + const auto network = lifted({4, {0, 1}, {first, second}}); + EventModel concrete(network.reference); + SymbolicEventModel symbolic(network); + for (uint8_t seed : {0, 1}) { + const auto expected = concrete.bootstrap({1, 0}, {{1}, {1}}, {0, 0, seed, 0}); + const auto result = symbolic.bootstrap(constants({1, 0}), {constants({1}), constants({1})}, constants({0, 0, seed, 0})); + EXPECT_EQ(evaluate(result, {}), withoutReason(expected)); + EXPECT_EQ(expected.current[3], seed); + } +} + +TEST_F(LatchSymbolicModelTests, AdmissionMatchesAllTransactionsIncludingNonstableErrors) { + const auto network = lifted({3, {0, 1}, {latch("l", 0, 1, 2)}}); + EventModel concrete(network.reference); + SymbolicEventModel symbolic(network); + for (size_t value = 0; value < 8; ++value) { + auto state = settle(concrete, concrete.bootstrap({uint8_t(value & 1), uint8_t((value >> 1) & 1)}, + {{uint8_t((value >> 2) & 1)}}, Bits(3))); + for (size_t tx = 0; tx < 4; ++tx) { + const Bits inputs{uint8_t(tx & 1), uint8_t((tx >> 1) & 1)}; + EXPECT_EQ(evaluate(symbolic.admit(constants(state), constants(inputs)), {}), withoutReason(concrete.admit(state, inputs))); + } + state.active = {1}; + EXPECT_EQ(evaluate(symbolic.admit(constants(state), constants({0, 0})), {}), withoutReason(concrete.admit(state, {0, 0}))); + } +} + +TEST_F(LatchSymbolicModelTests, FourOpenLatchesAndSelfFeedbackPreserveHistory) { + const auto network = lifted({6, {0, 1}, {latch("a", 0, 1, 2), latch("b", 2, 1, 3), + latch("c", 3, 1, 4), latch("d", 4, 1, 5)}}); + EventModel concrete(network.reference); + SymbolicEventModel symbolic(network, {}, 4); + auto state = settle(concrete, concrete.bootstrap({0, 1}, {{0}, {0}, {0}, {0}}, Bits(6))); + state = concrete.admit(state, {1, 1}); + for (size_t i = 0; i < 5; ++i) { + expectSuccessors(concrete, symbolic, state); + state = concrete.successors(state).front(); + } + EXPECT_TRUE(concrete.stable(state)); + EXPECT_EQ(state.current[5], 1); + const auto self = lifted({2, {0}, {latch("self", 1, 0, 1)}}); + EventModel concreteSelf(self.reference); + SymbolicEventModel symbolicSelf(self); + for (uint8_t bit : {0, 1}) expectSuccessors(concreteSelf, symbolicSelf, concreteSelf.bootstrap({1}, {{bit}}, Bits(2))); +} + +TEST_F(LatchSymbolicModelTests, IntermediateEnablePulseSurvivesEveryWave) { + Network reference{6, {0}, { + combinational("a", {0}, {1}, [](const Bits& bits) { return bits; }), + combinational("b", {1}, {2}, [](const Bits& bits) { return bits; }), + combinational("xor", {0, 2}, {3}, [](const Bits& bits) { return Bits{uint8_t(bits[0] ^ bits[1])}; }), + latch("l", 4, 3, 5)}}; + reference.constantByNet.resize(6); reference.constantByNet[4] = true; + const auto network = lifted(reference); + EventModel concrete(reference); + SymbolicEventModel symbolic(network); + auto state = settle(concrete, concrete.bootstrap({0}, {{}, {}, {}, {0}}, Bits(6))); + state = concrete.admit(state, {1}); + bool pulse = false; + for (size_t i = 0; i < 8 && !concrete.stable(state); ++i) { + expectSuccessors(concrete, symbolic, state); + state = concrete.successors(state).front(); pulse |= state.current[3]; + } + EXPECT_TRUE(pulse); EXPECT_EQ(state.current[3], 0); EXPECT_EQ(state.current[5], 1); +} + +TEST_F(LatchSymbolicModelTests, SharedFanoutChoiceAndWorkerOrderingAreIdentical) { + const auto network = lifted({5, {0, 1}, {flipFlop("f", 0, 1, 2), + combinational("a", {2}, {3}, [](const Bits& bits) { return bits; }), + combinational("b", {2}, {4}, [](const Bits& bits) { return bits; })}}); + EventModel concrete(network.reference); + SymbolicEventModel serial(network, {}, 1), parallel(network, {}, 4); + auto initial = settle(concrete, concrete.bootstrap({0, 0}, {{0}, {}, {}}, Bits(5))); + auto state = constants(concrete.admit(initial, {1, 1})); + size_t first = 100, second = 100; + auto a = serial.wave(state, [&] { return BoolExpr::Var(first++); }); + auto b = parallel.wave(state, [&] { return BoolExpr::Var(second++); }); + EXPECT_EQ(first, second); + EXPECT_EQ(flattenSymbolicBoundary(a), flattenSymbolicBoundary(b)); + a = serial.wave(a, [&] { return BoolExpr::Var(first++); }); + b = parallel.wave(b, [&] { return BoolExpr::Var(second++); }); + EXPECT_EQ(flattenSymbolicBoundary(a), flattenSymbolicBoundary(b)); + for (size_t choice = 0; choice < (size_t{1} << (first - 100)); ++choice) { + const auto result = evaluate(a, assignment(100, first - 100, choice)); + EXPECT_EQ(result.current[2], result.current[3]); EXPECT_EQ(result.current[3], result.current[4]); + } +} + +TEST_F(LatchSymbolicModelTests, UnusedChoiceEncodingsSelectLegalOrders) { + auto primitive = flipFlop("f", 0, 1, 3); + primitive.inputs.push_back(2); // Six total permutations use three choice bits. + const auto network = lifted({4, {0, 1, 2}, {primitive}}); + EventModel concrete(network.reference); + SymbolicEventModel symbolic(network); + auto state = settle(concrete, concrete.bootstrap({0, 0, 0}, {{0}}, Bits(4))); + state = concrete.admit(state, {1, 1, 1}); + expectSuccessors(concrete, symbolic, state); +} + +TEST_F(LatchSymbolicModelTests, StickyErrorsRemainVisibleWithSuccessfulAlternative) { + auto primitive = latch("l", 0, 1, 2); + primitive.react = [](const Bits& storage, const Bits&, const Bits& pins, std::optional, bool) -> Reaction { + if (pins[0] && pins[1]) return {{}, {}, true, "conflict"}; + const Bits next{pins[0] ? uint8_t{0} : pins[1] ? uint8_t{1} : storage[0]}; + return {next, next}; + }; + const auto network = lifted({3, {0, 1}, {primitive}}); + EventModel concrete(network.reference); + SymbolicEventModel symbolic(network); + auto state = settle(concrete, concrete.bootstrap({1, 0}, {{0}}, Bits(3))); + state = concrete.admit(state, {0, 1}); + expectSuccessors(concrete, symbolic, state); + for (const auto& outcome : concrete.successors(state)) { + expectSuccessors(concrete, symbolic, outcome); + EXPECT_EQ(symbolic.stable(constants(outcome))->evaluate({}), concrete.stable(outcome)); + } +} + +TEST_F(LatchSymbolicModelTests, BoundaryInvariantChecksConstantsLevelsAndNoFakeFfCapture) { + const auto network = lifted({3, {0, 1}, {latch("l", 0, 1, 2)}}); + SymbolicEventModel latchModel(network); + EXPECT_TRUE(latchModel.boundaryInvariant(latchModel.boundary(constants({0, 0, 1}), {constants({1})}))->evaluate({})); + EXPECT_FALSE(latchModel.boundaryInvariant(latchModel.boundary(constants({0, 1, 1}), {constants({1})}))->evaluate({})); + EXPECT_FALSE(latchModel.boundaryInvariant(latchModel.boundary(constants({0, 0, 0}), {constants({1})}))->evaluate({})); + SymbolicEventModel flopModel(lifted({3, {0, 1}, {flipFlop("f", 0, 1, 2)}})); + EXPECT_TRUE(flopModel.boundaryInvariant(flopModel.boundary(constants({1, 1, 0}), {constants({0})}))->evaluate({})); + Network reference{1, {}, {}, {true}}; + SymbolicEventModel constantModel(lifted(reference)); + EXPECT_FALSE(constantModel.boundaryInvariant(constantModel.boundary(constants({0}), {}))->evaluate({})); + EXPECT_TRUE(constantModel.boundaryInvariant(constantModel.boundary(constants({1}), {}))->evaluate({})); +} + +TEST_F(LatchSymbolicModelTests, ResourceAndMalformedCallbackFailuresNeverPruneOutcomes) { + const auto network = lifted({3, {0, 1}, {flipFlop("f", 0, 1, 2)}}); + SymbolicEventModel limited(network, {1, 100}); + EventModel concrete(network.reference); + const auto state = concrete.admit(settle(concrete, concrete.bootstrap({0, 0}, {{0}}, Bits(3))), {1, 1}); + size_t next = 100; + EXPECT_THROW(limited.wave(constants(state), [&] { return BoolExpr::Var(next++); }), Limit); + EXPECT_THROW(liftPrimitive(network.reference.primitives[0], 1), Limit); + auto malformed = network; + malformed.primitives[0].react = [](const SymbolicBits&, const SymbolicBits&, const SymbolicBits&, + std::optional, bool) { return SymbolicReaction{}; }; + SymbolicEventModel bad(malformed); + const auto result = bad.wave(constants(state), [&] { return BoolExpr::Var(next++); }); + EXPECT_TRUE(result.error->evaluate({})); + EXPECT_FALSE(bad.stable(result)->evaluate({})); + EXPECT_EQ(evaluate(bad.wave(result, {}), {}), evaluate(result, {})); +} + +TEST_F(LatchSymbolicModelTests, FlattenedBoundaryIncludesAllNetAndStorageBitsInFixedOrder) { + SymbolicEventModel model(lifted({3, {0, 1}, {latch("l", 0, 1, 2)}})); + size_t next = 2; + const auto nets = variables(3, next), storage = variables(1, next); + const auto boundary = model.boundary(nets, {storage}); + EXPECT_EQ(flattenSymbolicBoundary(boundary), (SymbolicBits{nets[0], nets[1], nets[2], storage[0]})); + EXPECT_EQ(boundary.previous, nets); + EXPECT_TRUE(model.stable(boundary)->evaluate({})); +} + +TEST_F(LatchSymbolicModelTests, MissingExplicitInitialProjectionCannotFallBackToWrongIdentity) { + auto primitive = latch("complement", 0, 1, 2); + primitive.initialOutputs = [](const Bits& storage) { return Bits{uint8_t(!storage[0])}; }; + auto network = lifted({3, {0, 1}, {primitive}}); + network.primitives.front().initialOutputs = {}; + SymbolicEventModel model(network); + const auto state = model.bootstrap(constants({0, 0}), {constants({0})}, constants({0, 0, 0})); + EXPECT_TRUE(state.error->evaluate({})); + EXPECT_EQ(evaluate(model.wave(state, {}), {}), evaluate(state, {})); +} + +TEST_F(LatchSymbolicModelTests, SymbolicBootstrapDoesNotFabricateFlopStartupEdge) { + const auto network = lifted({3, {0, 1}, {flipFlop("f", 0, 1, 2)}}); + SymbolicEventModel model(network); + auto state = model.bootstrap(constants({1, 1}), {constants({0})}, constants({0, 0, 1})); + state = model.wave(state, {}); + EXPECT_TRUE(model.stable(state)->evaluate({})); + EXPECT_FALSE(state.current[2]->evaluate({})); +} + +TEST_F(LatchSymbolicModelTests, ZeroChangedActivePrimitiveUsesNullVisitFallback) { + auto primitive = latch("null_visit", 0, 1, 2); + primitive.react = [](const Bits& storage, const Bits&, const Bits&, + std::optional changed, bool bootstrap) { + const Bits next{uint8_t(!bootstrap && !changed ? !storage[0] : storage[0])}; + return Reaction{next, next}; + }; + const auto network = lifted({3, {0, 1}, {primitive}}); + EventModel concrete(network.reference); + SymbolicEventModel symbolic(network); + State state{{0, 0, 0}, {0, 0, 0}, {{0}}, {1}, false, false, {}}; + expectSuccessors(concrete, symbolic, state); +} + +TEST_F(LatchSymbolicModelTests, PureCombinationalSnapshotsNeverInventXorPulse) { + const auto network = lifted({4, {0}, { + combinational("a", {0}, {1}, [](const Bits& bits) { return bits; }), + combinational("b", {0}, {2}, [](const Bits& bits) { return bits; }), + combinational("xor", {1, 2}, {3}, [](const Bits& bits) { return Bits{uint8_t(bits[0] ^ bits[1])}; })}}); + EventModel concrete(network.reference); + SymbolicEventModel symbolic(network, {}, 4); + auto state = settle(concrete, concrete.bootstrap({0}, {{}, {}, {}}, Bits(4))); + state = concrete.admit(state, {1}); + for (size_t i = 0; i < 3; ++i) { + expectSuccessors(concrete, symbolic, state); + state = concrete.successors(state).front(); + EXPECT_EQ(state.current[3], 0); + } +} + +TEST_F(LatchSymbolicModelTests, IndependentPrimitiveChoicesAreAllocatedSeriallyAndRemainIndependent) { + const auto network = lifted({4, {0, 1}, {flipFlop("a", 0, 1, 2), flipFlop("b", 0, 1, 3)}}); + EventModel concrete(network.reference); + SymbolicEventModel serial(network, {}, 1), parallel(network, {}, 4); + const auto initial = settle(concrete, concrete.bootstrap({0, 0}, {{0}, {0}}, Bits(4))); + const auto state = concrete.admit(initial, {1, 1}); + size_t first = 100, second = 100; + const auto a = serial.wave(constants(state), [&] { return BoolExpr::Var(first++); }); + const auto b = parallel.wave(constants(state), [&] { return BoolExpr::Var(second++); }); + ASSERT_EQ(first, 102u); ASSERT_EQ(second, first); + EXPECT_EQ(flattenSymbolicBoundary(a), flattenSymbolicBoundary(b)); + std::set outcomes; + for (size_t bits = 0; bits < 4; ++bits) outcomes.insert(evaluate(a, assignment(100, 2, bits))); + EXPECT_EQ(outcomes.size(), 4u); + expectSuccessors(concrete, parallel, state); +} + +TEST_F(LatchSymbolicModelTests, FixedChoiceModeSelectsLegalConcreteOrderWithoutFreeSymbols) { + const auto network = lifted({3, {0, 1}, {flipFlop("f", 0, 1, 2)}}); + EventModel concrete(network.reference); + SymbolicEventModel symbolic(network); + const auto state = concrete.admit(settle(concrete, concrete.bootstrap({0, 0}, {{0}}, Bits(3))), {1, 1}); + std::set permitted; + for (auto successor : concrete.successors(state)) permitted.insert(withoutReason(successor)); + for (bool value : {false, true}) { + const auto fixed = symbolic.wave(constants(state), [value] { return BoolExpr::Var(value); }); + EXPECT_TRUE(permitted.count(evaluate(fixed, {}))); + } +} + +TEST_F(LatchSymbolicModelTests, SymbolicStableValuationsPadEveryStateFieldByIdentity) { + SymbolicEventModel model(lifted({4, {0, 1}, + {latch("l", 0, 1, 2), flipFlop("f", 0, 1, 3)}})); + size_t next = 2; + SymbolicState state; + state.current = variables(4, next); + state.previous = variables(4, next); + state.storage = {variables(1, next), variables(1, next)}; + state.active = variables(2, next); + state.bootstrap = variables(1, next).front(); + state.error = variables(1, next).front(); + ASSERT_NE(model.stable(state), BoolExpr::createTrue()); + size_t choice = 1000; + const auto result = model.wave(state, [&] { return BoolExpr::Var(choice++); }); + for (size_t value = 0; value < 64; ++value) { + Environment env; + for (size_t bit = 0; bit < 4; ++bit) { + env[state.current[bit]->getId()] = (value >> bit) & 1; + env[state.previous[bit]->getId()] = (value >> bit) & 1; + } + env[state.storage[0][0]->getId()] = (value >> 4) & 1; + env[state.storage[1][0]->getId()] = (value >> 5) & 1; + for (auto* active : state.active) env[active->getId()] = false; + env[state.bootstrap->getId()] = false; + env[state.error->getId()] = false; + ASSERT_TRUE(model.stable(state)->evaluate(env)); + for (size_t order = 0; order < (size_t{1} << (choice - 1000)); ++order) + EXPECT_EQ(evaluate(result, assignment(1000, choice - 1000, order, env)), evaluate(state, env)); + } +} + +TEST_F(LatchSymbolicModelTests, ConsumedAndSinkHistoryMatchesConcreteAdmissionIncludingErrors) { + const auto network = lifted({5, {0, 1, 2}, + {latch("first", 0, 1, 3), latch("second", 3, 1, 4)}}); + EventModel concrete(network.reference); + SymbolicEventModel symbolic(network); + size_t next = 2; + auto state = symbolic.boundary(variables(5, next), {variables(1, next), variables(1, next)}); + const auto input = variables(3, next); + state.error = variables(1, next).front(); + const auto admitted = symbolic.admit(state, input); + for (size_t value = 0; value < (size_t{1} << (next - 2)); ++value) { + const auto env = assignment(2, next - 2, value); + EXPECT_EQ(evaluate(admitted, env), withoutReason(concrete.admit(evaluate(state, env), evaluate(input, env)))); + } +} + +TEST_F(LatchSymbolicModelTests, ConsumedAndSinkHistoryMatchesConcreteWavesIncludingErrors) { + const auto network = lifted({5, {0, 1, 2}, + {latch("first", 0, 1, 3), latch("second", 3, 1, 4)}}); + EventModel concrete(network.reference); + SymbolicEventModel symbolic(network); + for (size_t value = 0; value < 512; ++value) { + State state; + for (size_t bit = 0; bit < 5; ++bit) state.current.push_back((value >> bit) & 1); + state.previous = state.current; + // Include stale sink history, changed consumed feedback and a changed + // unconsumed external input. Supplied activation need not be consistent: + // the optimization applies only to the newly computed activation set. + for (auto net : {2u, 3u, 4u}) state.previous[net] ^= 1; + state.storage = {{uint8_t((value >> 5) & 1)}, {uint8_t((value >> 6) & 1)}}; + state.active = {uint8_t((value >> 7) & 1), uint8_t((value >> 8) & 1)}; + expectSuccessors(concrete, symbolic, state); + state.error = true; + expectSuccessors(concrete, symbolic, state); + } +} + +TEST_F(LatchSymbolicModelTests, EntirelyUnconsumedNetworkStillNormalizesHistory) { + const auto network = lifted({1, {0}, {}}); + EventModel concrete(network.reference); + SymbolicEventModel symbolic(network); + auto state = settle(concrete, concrete.bootstrap({0}, {}, {0})); + const auto result = symbolic.admit(constants(state), constants({1})); + EXPECT_EQ(evaluate(result, {}), withoutReason(concrete.admit(state, {1}))); + EXPECT_EQ(evaluate(result.previous, {}), Bits{1}); + state.previous = {1}; + expectSuccessors(concrete, symbolic, state); +} +} // namespace +} // namespace KEPLER_FORMAL::SEC::LATCH diff --git a/test/sec/OpaquePolicyTests.cpp b/test/sec/OpaquePolicyTests.cpp new file mode 100644 index 00000000..f17dc992 --- /dev/null +++ b/test/sec/OpaquePolicyTests.cpp @@ -0,0 +1,242 @@ +// Copyright 2024-2026 keplertech.io +// SPDX-License-Identifier: Apache-2.0 + +#include + +#include "BoolExprCache.h" +#include "Config.h" +#include "DNL.h" +#include "NLDB0.h" +#include "NLUniverse.h" +#include "SNLDesign.h" +#include "SNLInstance.h" +#include "SNLScalarNet.h" +#include "SNLScalarTerm.h" +#include "Tree2BoolExpr.h" +#include "model/OpaquePolicy.h" +#include "model/SequentialDesignModel.h" +#include "latch/LatchSupportOptions.h" +#include "strategy/SequentialEquivalenceStrategy.h" + +namespace { + +using namespace naja::NL; +using namespace KEPLER_FORMAL::SEC; +using KEPLER_FORMAL::Config; + +class OpaquePolicyTests : public ::testing::Test { + protected: + void SetUp() override { + oldPolicy_ = Config::getErrorOnOpaque(); + Config::setErrorOnOpaque(false); + } + void TearDown() override { + Config::setErrorOnOpaque(oldPolicy_); + naja::DNL::destroy(); + if (auto* universe = NLUniverse::get()) universe->destroy(); + KEPLER_FORMAL::Tree2BoolExpr::iso2boolExpr_.clear(); + KEPLER_FORMAL::BoolExprCache::destroy(); + } + + SequentialDesignModel classifiedModel( + ConnectivitySkipOrigin origin = ConnectivitySkipOrigin::OpaqueInternal) { + SequentialDesignModel model; + const SignalKey key{{1}, {2}}; + model.displayNameByKey.emplace(key, "island.cell.Q[0]"); + model.connectivitySkipInfoByKey.emplace( + key, ConnectivitySkipInfo{origin, "missing primitive model"}); + return model; + } + + void createLibraries() { + auto* db = NLDB::create(NLUniverse::create()); + library_ = NLLibrary::create(db, NLLibrary::Type::Standard, NLName("designs")); + auto* primitives = + NLLibrary::create(db, NLLibrary::Type::Primitives, NLName("primitives")); + opaque_ = SNLDesign::create( + primitives, SNLDesign::Type::Primitive, NLName("UNMODELED")); + SNLScalarTerm::create(opaque_, SNLTerm::Direction::Input, NLName("D")); + SNLScalarTerm::create(opaque_, SNLTerm::Direction::Output, NLName("Q")); + } + + SNLDesign* top(const std::string& name, SNLDesign* cell = nullptr, + bool connected = false) { + auto* design = SNLDesign::create(library_, SNLDesign::Type::Standard, NLName(name)); + auto* data = SNLScalarNet::create(design, NLName("data")); + auto* enable = SNLScalarNet::create(design, NLName("enable")); + SNLScalarTerm::create(design, SNLTerm::Direction::Input, NLName("a"))->setNet(data); + SNLScalarTerm::create(design, SNLTerm::Direction::Input, NLName("e"))->setNet(enable); + SNLScalarTerm::create(design, SNLTerm::Direction::Output, NLName("good"))->setNet(data); + if (cell) { + auto* instance = SNLInstance::create(design, cell, NLName("unused_cell")); + instance->getInstTerm(cell->getScalarTerm(NLName("D")))->setNet(data); + if (auto* gate = cell->getScalarTerm(NLName("E"))) { + instance->getInstTerm(gate)->setNet(enable); + } + auto* output = SNLScalarNet::create(design, NLName("cell_output")); + instance->getInstTerm(cell->getScalarTerm(NLName("Q")))->setNet(output); + if (connected) { + SNLScalarTerm::create(design, SNLTerm::Direction::Output, NLName("bad"))->setNet(output); + } + } + return design; + } + + bool oldPolicy_ = false; + NLLibrary* library_ = nullptr; + SNLDesign* opaque_ = nullptr; +}; + +TEST_F(OpaquePolicyTests, DisabledPreservesClassificationAndReasons) { + auto model = classifiedModel(); + applyOpaquePolicy(model, "top", 0); + EXPECT_FALSE(model.hasUnsupportedFeatures()); + ASSERT_EQ(model.connectivitySkipInfoByKey.size(), 1u); + EXPECT_EQ(model.connectivitySkipInfoByKey.begin()->second.detail, + "missing primitive model"); +} + +TEST_F(OpaquePolicyTests, EnabledIdentifiesDesignSignalAndReason) { + Config::setErrorOnOpaque(true); + auto model = classifiedModel(); + applyOpaquePolicy(model, "candidate", 1); + ASSERT_EQ(model.unsupportedReasons.size(), 1u); + EXPECT_EQ(model.unsupportedReasons.front(), + "SEC error-on-opaque: design 2 (`candidate`), signal " + "`island.cell.Q[0]`: missing primitive model"); + applyOpaquePolicy(model, "candidate", 1); + EXPECT_EQ(model.unsupportedReasons.size(), 1u); +} + +TEST_F(OpaquePolicyTests, OtherConnectivitySkipKindsDoNotBecomeOpaque) { + Config::setErrorOnOpaque(true); + for (const auto origin : {ConnectivitySkipOrigin::NoDriver, + ConnectivitySkipOrigin::MultiDriver, + ConnectivitySkipOrigin::LogicalLoop, + ConnectivitySkipOrigin::MultiClockDomain, + ConnectivitySkipOrigin::UnknownConstant}) { + auto model = classifiedModel(origin); + applyOpaquePolicy(model, "top", 0); + EXPECT_FALSE(model.hasUnsupportedFeatures()); + } +} + +TEST_F(OpaquePolicyTests, DiagnosticOrderIsStableAndEarlierErrorsArePreserved) { + Config::setErrorOnOpaque(true); + auto model = classifiedModel(); + model.unsupportedReasons.push_back("earlier error"); + const SignalKey earlier{{3}, {4}}; + model.displayNameByKey.emplace(earlier, "a.Q[0]"); + model.connectivitySkipInfoByKey.emplace( + earlier, ConnectivitySkipInfo{ConnectivitySkipOrigin::OpaqueInternal, "other reason"}); + applyOpaquePolicy(model, "top", 0); + ASSERT_EQ(model.unsupportedReasons.size(), 2u); + EXPECT_EQ(model.unsupportedReasons.front(), "earlier error"); + EXPECT_NE(model.unsupportedReasons.back().find("a.Q[0]"), std::string::npos); +} + +TEST_F(OpaquePolicyTests, MissingDisplayNameHasSignalKeyFallback) { + Config::setErrorOnOpaque(true); + auto model = classifiedModel(); + model.displayNameByKey.clear(); + applyOpaquePolicy(model, "top", 0); + ASSERT_EQ(model.unsupportedReasons.size(), 1u); + EXPECT_NE(model.unsupportedReasons.front().find("signal `1.2.`"), std::string::npos); +} + +TEST_F(OpaquePolicyTests, DefaultExtractionStillChecksSupportedOutput) { + createLibraries(); + const auto model = SequentialDesignModel::extract(top("top", opaque_, true)); + EXPECT_FALSE(model.hasUnsupportedFeatures()); + EXPECT_EQ(model.coveredObservedOutputCount(), 1u); + EXPECT_EQ(model.totalObservedOutputCount(), 2u); +} + +TEST_F(OpaquePolicyTests, EnabledExtractionRejectsDisconnectedNonLatchCell) { + createLibraries(); + Config::setErrorOnOpaque(true); + const auto model = SequentialDesignModel::extract(top("candidate", opaque_)); + ASSERT_TRUE(model.hasUnsupportedFeatures()); + EXPECT_NE(model.unsupportedReasons.front().find("unused_cell"), std::string::npos); + EXPECT_NE(model.unsupportedReasons.front().find("UNMODELED"), std::string::npos); + EXPECT_FALSE(naja::DNL::isCreated()); +} + +TEST_F(OpaquePolicyTests, EnabledExtractionRejectsDisconnectedLatch) { + LATCH::SupportOptions legacy; + legacy.enabled = false; + LATCH::ScopedSupportOptions scope(legacy); + createLibraries(); + Config::setErrorOnOpaque(true); + const auto model = SequentialDesignModel::extract(top("candidate", NLDB0::getDLatch())); + ASSERT_TRUE(model.hasUnsupportedFeatures()); + EXPECT_NE(model.unsupportedReasons.front().find("latch"), std::string::npos); +} + +TEST_F(OpaquePolicyTests, SupportedDesignIsUnchangedWhenEnabled) { + createLibraries(); + Config::setErrorOnOpaque(true); + const auto model = SequentialDesignModel::extract(top("supported")); + EXPECT_FALSE(model.hasUnsupportedFeatures()); + EXPECT_EQ(model.coveredObservedOutputCount(), 1u); +} + +TEST_F(OpaquePolicyTests, RejectsOpacityInEitherDesign) { + createLibraries(); + Config::setErrorOnOpaque(true); + auto* supported = top("supported"); + auto* unsupported = top("unsupported", opaque_); + for (const bool first : {true, false}) { + const SequentialEquivalenceStrategy strategy( + first ? unsupported : supported, first ? supported : unsupported, + Config::KISSAT, SecEngine::Pdr, SecEncoding::Binary); + const auto result = strategy.run(1); + EXPECT_EQ(result.status, SequentialEquivalenceStatus::Unsupported); + EXPECT_NE(result.reason.find(first ? "design 1" : "design 2"), std::string::npos); + } +} + +TEST_F(OpaquePolicyTests, StrictModeDetectsInputOnlyOpaqueLeafCells) { + createLibraries(); + auto* sink = SNLDesign::create(opaque_->getLibrary(), SNLDesign::Type::Primitive, + NLName("OPAQUE_SINK")); + auto* input = SNLScalarTerm::create(sink, SNLTerm::Direction::Input, NLName("D")); + auto* design = top("with_sink"); + auto* instance = SNLInstance::create(design, sink, NLName("unknown_sink")); + instance->getInstTerm(input)->setNet(design->getScalarTerm(NLName("a"))->getNet()); + EXPECT_FALSE(SequentialDesignModel::extract(design).hasUnsupportedFeatures()); + Config::setErrorOnOpaque(true); + const auto model = SequentialDesignModel::extract(design); + ASSERT_TRUE(model.hasUnsupportedFeatures()); + EXPECT_NE(model.unsupportedReasons.front().find("unknown_sink"), std::string::npos); + EXPECT_NE(model.unsupportedReasons.front().find("outputless"), std::string::npos); +} + +TEST_F(OpaquePolicyTests, OutputlessScanDoesNotTreatEmptyStandardHierarchyAsOpaque) { + createLibraries(); + auto* design = top("empty_hierarchy"); + auto* empty = SNLDesign::create(library_, SNLDesign::Type::Standard, NLName("empty")); + SNLInstance::create(design, empty, NLName("empty_child")); + Config::setErrorOnOpaque(true); + const auto model = SequentialDesignModel::extract(design); + EXPECT_FALSE(model.hasUnsupportedFeatures()); + EXPECT_EQ(model.coveredObservedOutputCount(), 1u); +} + +TEST_F(OpaquePolicyTests, OutputlessScanHandlesBlackBoxesAndNoPortPrimitives) { + createLibraries(); + for (const auto type : {SNLDesign::Type::UserBlackBox, SNLDesign::Type::Primitive}) { + const auto suffix = type == SNLDesign::Type::Primitive ? "primitive" : "blackbox"; + auto* design = top(std::string("top_") + suffix); + auto* unknown = SNLDesign::create( + type == SNLDesign::Type::Primitive ? opaque_->getLibrary() : library_, + type, NLName(suffix)); + SNLInstance::create(design, unknown, NLName("unknown")); + Config::setErrorOnOpaque(true); + const auto model = SequentialDesignModel::extract(design); + ASSERT_TRUE(model.hasUnsupportedFeatures()); + EXPECT_NE(model.unsupportedReasons.front().find("unknown"), std::string::npos); + } +} + +} // namespace diff --git a/test/sec/PdrTernaryMemoTests.cpp b/test/sec/PdrTernaryMemoTests.cpp new file mode 100644 index 00000000..1589440f --- /dev/null +++ b/test/sec/PdrTernaryMemoTests.cpp @@ -0,0 +1,65 @@ +// Copyright 2026 keplertech.io +// SPDX-License-Identifier: Apache-2.0 + +#include + +#include "BoolExprCache.h" +#include "pdr/PDREngine.h" + +namespace KEPLER_FORMAL::SEC { +namespace { + +class PdrTernaryMemoTests : public ::testing::Test { + protected: + void TearDown() override { BoolExprCache::destroy(); } + + KInductionProblem twoMappedRoots(bool largerFirst) { + KInductionProblem problem; + problem.state0Symbols = {2, 3}; + problem.state1Symbols = {4, 5}; + problem.inputSymbols = {6, 7}; + problem.allSymbols = {2, 3, 4, 5, 6, 7}; + problem.initialStateAssignments = {{2, false}, {3, true}, {4, false}, {5, true}}; + problem.initialCondition = BoolExpr::createTrue(); + problem.initializedStateCount = problem.totalStateCount = 4; + problem.bad = BoolExpr::And(BoolExpr::Var(2), BoolExpr::Var(4)); + problem.property = BoolExpr::Not(problem.bad); + problem.lazyTransitions = std::make_shared(); + auto& lazy = *problem.lazyTransitions; + lazy.localToCombinedByDesign[0] = {{10, 3}, {11, 6}, {12, 7}}; + lazy.localToCombinedByDesign[1] = {{10, 5}, {11, 6}, {12, 7}}; + auto* small = BoolExpr::Or(BoolExpr::Var(10), BoolExpr::Var(11)); + auto* large = BoolExpr::Or(BoolExpr::Var(10), + BoolExpr::And(BoolExpr::Var(11), BoolExpr::Var(12))); + lazy.sourceByStateSymbol.emplace(2, LazyTransitionSource{ + 0, largerFirst ? large : small, LazyTransitionRail::Binary}); + lazy.sourceByStateSymbol.emplace(4, LazyTransitionSource{ + 1, largerFirst ? small : large, LazyTransitionRail::Binary}); + lazy.sourceByStateSymbol.emplace(3, LazyTransitionSource{ + 0, BoolExpr::Var(10), LazyTransitionRail::Binary}); + lazy.sourceByStateSymbol.emplace(5, LazyTransitionSource{ + 1, BoolExpr::Var(10), LazyTransitionRail::Binary}); + return problem; + } +}; + +TEST_F(PdrTernaryMemoTests, LaterMappedRootCanAddParentsBeyondEarlierMemoSize) { + // Both design-local DAGs share variable nodes but use different symbol maps. + // Compiling the second root adds parents to those shared nodes after the + // first map's memo was allocated. Probing a predecessor literal must never + // index that shorter memo with a later root's parent index (ASan regression). + for (bool largerFirst : {false, true}) { + SCOPED_TRACE(largerFirst); + const auto problem = twoMappedRoots(largerFirst); + auto cache = std::make_shared(problem, Config::SolverType::KISSAT); + PDREngine engine(problem, Config::SolverType::KISSAT, 0, cache); + for (size_t repetition = 0; repetition < 3; ++repetition) { + const auto result = engine.run(2); + EXPECT_EQ(result.status, PDRStatus::Different); + EXPECT_EQ(result.bound, 1u); + } + } +} + +} // namespace +} // namespace KEPLER_FORMAL::SEC diff --git a/test/sec/RelationalInitializationTests.cpp b/test/sec/RelationalInitializationTests.cpp new file mode 100644 index 00000000..d253566f --- /dev/null +++ b/test/sec/RelationalInitializationTests.cpp @@ -0,0 +1,286 @@ +// Copyright 2026 keplertech.io +// SPDX-License-Identifier: Apache-2.0 + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "BoolExprCache.h" +#include "export/SecBtor2Exporter.h" +#include "imc/CraigInterpolatingModelChecker.h" +#include "imc/IMCEngine.h" +#include "kinduction/KInductionEngine.h" +#include "pdr/PDREngine.h" +#include "proof/ProofEngineShared.h" + +namespace KEPLER_FORMAL::SEC { +namespace { + +BoolExpr* equal(size_t lhs, size_t rhs) { + return BoolExpr::Not(BoolExpr::Xor(BoolExpr::Var(lhs), BoolExpr::Var(rhs))); +} + +void setBad(KInductionProblem& problem, BoolExpr* bad) { + problem.bad = problem.inductionBad = bad; + problem.property = problem.inductionProperty = BoolExpr::Not(bad); + problem.observedOutputNames = {"result"}; + problem.observedOutputExprs0 = {bad}; + problem.observedOutputExprs1 = {BoolExpr::createFalse()}; +} + +KInductionProblem relationalProblem() { + KInductionProblem problem; + problem.state0Symbols = {2}; + problem.state1Symbols = {3}; + problem.auxiliaryStateSymbols = {4}; + problem.allSymbols = {2, 3, 4}; + problem.transitions0 = {{2, BoolExpr::Var(2)}}; + problem.transitions1 = {{3, BoolExpr::Var(3)}}; + problem.auxiliaryTransitions = {{4, BoolExpr::Var(4)}}; + // A unit fact must not replace the independently supplied relation. Counts + // describe known bits, not the completeness of this set-valued BOOT state. + problem.initialStateAssignments = {{4, false}}; + problem.initializedStateCount = 1; + problem.totalStateCount = 3; + problem.initialCondition = equal(2, 3); + problem.hasExactRelationalInitialState = true; + setBad(problem, BoolExpr::Or(BoolExpr::Xor(BoolExpr::Var(2), BoolExpr::Var(3)), + BoolExpr::Var(4))); + return problem; +} + +class RelationalInitializationTests : public ::testing::Test { + protected: + void TearDown() override { BoolExprCache::destroy(); } +}; + +class RelationalInitializationEngineTests : public RelationalInitializationTests, + public ::testing::WithParamInterface { + protected: + void check(const KInductionProblem& problem, bool different, size_t bound = 0) { + constexpr auto solver = Config::SolverType::KISSAT; + if (GetParam() == 0) { + const auto result = KInductionEngine(problem, solver).run(5); + EXPECT_EQ(result.status, different ? KInductionStatus::Different : KInductionStatus::Equivalent); + if (different) EXPECT_EQ(result.bound, bound); + } else if (GetParam() == 1) { + const auto result = PDREngine(problem, solver).run(5); + EXPECT_EQ(result.status, different ? PDRStatus::Different : PDRStatus::Equivalent); + if (different) EXPECT_EQ(result.bound, bound); + } else { + const auto result = IMCEngine(problem, solver).run(5); + EXPECT_EQ(result.status, different ? IMCStatus::Different : IMCStatus::Equivalent); + if (different) EXPECT_EQ(result.bound, bound); + } + } +}; + +TEST_P(RelationalInitializationEngineTests, RelationAndUnitFactsBothConstrainBoot) { + check(relationalProblem(), false); +} + +TEST_P(RelationalInitializationEngineTests, MismatchingBootIsNotAssumedAway) { + auto problem = relationalProblem(); + problem.initialCondition = BoolExpr::Xor(BoolExpr::Var(2), BoolExpr::Var(3)); + check(problem, true, 0); +} + +TEST_P(RelationalInitializationEngineTests, ExactUnconstrainedBootDoesNotAssumeOutputEquality) { + auto problem = relationalProblem(); + problem.initialCondition = BoolExpr::createTrue(); + problem.initialStateAssignments.clear(); + problem.initializedStateCount = 0; + check(problem, true, 0); +} + +TEST_P(RelationalInitializationEngineTests, InitializationRelationIsNotAnInvariant) { + auto problem = relationalProblem(); + problem.transitions1 = {{3, BoolExpr::Not(BoolExpr::Var(3))}}; + check(problem, true, 1); +} + +TEST_P(RelationalInitializationEngineTests, InitialPredicateSupportExtendsOutputCoi) { + auto problem = relationalProblem(); + // The output depends only on 2; the initial relation must bring 4 into the + // initial COI so its separate unit fact can constrain 2. + problem.initialCondition = equal(2, 4); + setBad(problem, BoolExpr::Var(2)); + check(problem, false); +} + +TEST_P(RelationalInitializationEngineTests, OutputSubsetsRetainBootRelation) { + auto problem = relationalProblem(); + auto* different = BoolExpr::Xor(BoolExpr::Var(2), BoolExpr::Var(3)); + problem.observedOutputNames = {"relation", "unit"}; + problem.observedOutputExprs0 = {different, BoolExpr::Var(4)}; + problem.observedOutputExprs1 = {BoolExpr::createFalse(), BoolExpr::createFalse()}; + check(problem, false); + problem.transitions1 = {{3, BoolExpr::Not(BoolExpr::Var(3))}}; + check(problem, true, 1); +} + +TEST_P(RelationalInitializationEngineTests, SharedInputOriginAndIndependentStorageOriginsDiffer) { + for (bool shared : {true, false}) { + SCOPED_TRACE(shared); + auto problem = relationalProblem(); + problem.auxiliaryStateSymbols = {4, 5, 6}; + problem.auxiliaryTransitions = {{4, BoolExpr::Var(4)}, {5, BoolExpr::Var(5)}, + {6, BoolExpr::Var(6)}}; + problem.allSymbols = {2, 3, 4, 5, 6}; + problem.totalStateCount = 5; + problem.initialStateAssignments = {{6, false}}; + problem.initialCondition = BoolExpr::And(equal(2, 4), equal(3, shared ? 4 : 5)); + setBad(problem, BoolExpr::Xor(BoolExpr::Var(2), BoolExpr::Var(3))); + check(problem, !shared, 0); + } +} + +INSTANTIATE_TEST_SUITE_P(KiPdrImc, RelationalInitializationEngineTests, + ::testing::Values(0, 1, 2)); + +TEST_F(RelationalInitializationTests, SharedProofInitKeepsUnitsWithoutInventingPointState) { + auto problem = relationalProblem(); + auto* initial = buildProofInitFormula(problem); + ASSERT_NE(initial, nullptr); + for (bool value : {false, true}) { + EXPECT_TRUE(initial->evaluate({{2, value}, {3, value}, {4, false}})); + EXPECT_FALSE(initial->evaluate({{2, value}, {3, !value}, {4, false}})); + EXPECT_FALSE(initial->evaluate({{2, value}, {3, value}, {4, true}})); + } +} + +TEST_F(RelationalInitializationTests, CraigProjectionPreservesRelationAndItsHiddenOrigin) { + auto problem = relationalProblem(); + problem.initialCondition = equal(2, 4); + setBad(problem, BoolExpr::Var(2)); + const auto result = CraigInterpolatingModelChecker(problem).run(4); + EXPECT_EQ(result.status, CraigImcStatus::Equivalent); +} + +TEST_F(RelationalInitializationTests, ImcCounterexampleStartsAtExactBootRelation) { + auto problem = relationalProblem(); + problem.initialCondition = BoolExpr::And(equal(2, 4), equal(3, 4)); + problem.transitions0 = {{2, BoolExpr::Var(3)}}; + problem.transitions1 = {{3, BoolExpr::createTrue()}}; + setBad(problem, BoolExpr::Var(2)); + const auto result = IMCEngine(problem, Config::SolverType::KISSAT).run(4); + EXPECT_EQ(result.status, IMCStatus::Different); + EXPECT_EQ(result.bound, 2); +} + +TEST_F(RelationalInitializationTests, ImcExactReachabilityIncludesAuxiliaryTransitions) { + auto problem = relationalProblem(); + // Three-bit rotating one-hot state. The public property alone is not + // inductive; neither is the one-step image. The exact reachability path must + // retain the auxiliary transition to close the three-state orbit. + problem.initialStateAssignments = {{2, true}, {3, false}, {4, false}}; + problem.initializedStateCount = 3; + problem.initialCondition = BoolExpr::createTrue(); + problem.transitions0 = {{2, BoolExpr::Var(3)}}; + problem.transitions1 = {{3, BoolExpr::Var(4)}}; + problem.auxiliaryTransitions = {{4, BoolExpr::Var(2)}}; + setBad(problem, BoolExpr::And(BoolExpr::Var(2), BoolExpr::Var(3))); + const auto result = IMCEngine(problem, Config::SolverType::KISSAT).run(4); + EXPECT_EQ(result.status, IMCStatus::Equivalent); +} + +TEST_F(RelationalInitializationTests, LargeDualRailImcKeepsBooleanRelations) { + auto problem = relationalProblem(); + problem.usesDualRailStateEncoding = true; + // Select Craig IMC's large-state path; the arbitrary origins are Boolean, + // not X=11 values or fabricated fixed initial assignments. + for (size_t symbol = 5; symbol != 18; ++symbol) { + problem.auxiliaryStateSymbols.push_back(symbol); + problem.auxiliaryTransitions.emplace_back(symbol, BoolExpr::Var(symbol)); + problem.allSymbols.push_back(symbol); + } + problem.totalStateCount = problem.combinedStateSymbols().size(); + const auto result = IMCEngine(problem, Config::SolverType::KISSAT).run(4); + EXPECT_EQ(result.status, IMCStatus::Equivalent); +} + +// Independent exhaustive Boolean BTOR2 execution for these tiny no-input +// systems. Checks semantics, including the exporter's first-frame monitor. +std::vector exportedBadFrames(const KInductionProblem& problem, size_t frames) { + std::ostringstream output; + exportSecBtor2(problem, output); + struct Node { size_t id; std::string op; std::vector args; }; + std::vector nodes; + std::map offsets, initial, next; + std::istringstream source(output.str()); + std::string line; + while (std::getline(source, line)) { + std::istringstream fields(line.substr(0, line.find(';'))); + Node node{}; + if (!(fields >> node.id >> node.op)) continue; + std::string argument; + while (fields >> argument) node.args.push_back(argument); + if (node.op == "state") offsets.emplace(node.id, offsets.size()); + if (node.op == "init" || node.op == "next") { + (node.op == "init" ? initial : next).emplace(std::stoul(node.args.at(1)), + std::stoul(node.args.at(2))); + } + nodes.push_back(std::move(node)); + } + if (offsets.size() > 10) throw std::runtime_error("Tiny BTOR2 test state limit"); + std::set reachable; + for (size_t value = 0; value != (size_t{1} << offsets.size()); ++value) reachable.insert(value); + std::vector result(frames, false); + for (size_t frame = 0; frame != frames; ++frame) { + std::set successors; + for (size_t state : reachable) { + std::map values; + bool legal = true, bad = false; + for (const auto& node : nodes) { + const auto operand = [&](size_t index) { return values.at(std::stoul(node.args.at(index))); }; + if (node.op == "state") values[node.id] = (state >> offsets.at(node.id)) & 1; + else if (node.op == "const") values[node.id] = node.args.at(1) == "1"; + else if (node.op == "zero") values[node.id] = false; + else if (node.op == "one" || node.op == "ones") values[node.id] = true; + else if (node.op == "not") values[node.id] = !operand(1); + else if (node.op == "and") values[node.id] = operand(1) && operand(2); + else if (node.op == "or") values[node.id] = operand(1) || operand(2); + else if (node.op == "xor") values[node.id] = operand(1) != operand(2); + else if (node.op == "eq" || node.op == "xnor") values[node.id] = operand(1) == operand(2); + else if (node.op == "ite") values[node.id] = operand(1) ? operand(2) : operand(3); + else if (node.op == "constraint") legal &= operand(0); + else if (node.op == "bad") bad |= operand(0); + else if (node.op != "sort" && node.op != "init" && node.op != "next" && node.op != "output") + throw std::runtime_error("Unexpected BTOR2 Boolean opcode: " + node.op); + } + if (frame == 0) for (const auto& [symbol, expression] : initial) + legal &= values.at(symbol) == values.at(expression); + if (!legal) continue; + result[frame] = result[frame] || bad; + size_t successor = 0; + for (const auto& [symbol, expression] : next) + successor |= size_t(values.at(expression)) << offsets.at(symbol); + successors.insert(successor); + } + EXPECT_FALSE(successors.empty()) << "Exporter removed all behavior at frame " << frame; + reachable = std::move(successors); + } + return result; +} + +TEST_F(RelationalInitializationTests, ExportPreservesRelationAlongsideUnitsAndOnlyAtBoot) { + auto problem = relationalProblem(); + EXPECT_EQ(exportedBadFrames(problem, 3), std::vector({false, false, false})); + problem.transitions1 = {{3, BoolExpr::Not(BoolExpr::Var(3))}}; + EXPECT_EQ(exportedBadFrames(problem, 3), std::vector({false, true, false})); +} + +TEST_F(RelationalInitializationTests, ExportKeepsInitialMismatchWithoutObservationMask) { + auto problem = relationalProblem(); + problem.initialCondition = BoolExpr::Xor(BoolExpr::Var(2), BoolExpr::Var(3)); + EXPECT_EQ(exportedBadFrames(problem, 2), std::vector({true, true})); +} + +} // namespace +} // namespace KEPLER_FORMAL::SEC diff --git a/test/sec/SequentialEquivalenceStrategyTests.cpp b/test/sec/SequentialEquivalenceStrategyTests.cpp index 08e4f6e9..c255fdac 100644 --- a/test/sec/SequentialEquivalenceStrategyTests.cpp +++ b/test/sec/SequentialEquivalenceStrategyTests.cpp @@ -52,6 +52,7 @@ #include "kinduction/BaseCaseSolver.h" #include "kinduction/SatEncoding.h" #include "kinduction/InductionStepSolver.h" +#include "latch/LatchSupportOptions.h" #include "model/SecNetlistChecks.h" #include "model/SequentialDesignModel.h" #include "proof/TransitionExprResolver.h" @@ -17318,6 +17319,9 @@ TEST_F(SequentialEquivalenceStrategyTests, TEST_F(SequentialEquivalenceStrategyTests, SequentialDesignModelExtractTreatsNajaLatchModelAsOpaque) { + LATCH::SupportOptions legacy; + legacy.enabled = false; + LATCH::ScopedSupportOptions scope(legacy); NLUniverse::create(); auto* db = NLDB::create(NLUniverse::get()); auto* library = @@ -17355,6 +17359,9 @@ TEST_F(SequentialEquivalenceStrategyTests, TEST_F(SequentialEquivalenceStrategyTests, SequentialDesignModelExtractTreatsModeledClockGateLatchAsOpaque) { + LATCH::SupportOptions legacy; + legacy.enabled = false; + LATCH::ScopedSupportOptions scope(legacy); NLUniverse::create(); auto* db = NLDB::create(NLUniverse::get()); auto* primitives = diff --git a/test/strategies/miter/BUILD.bazel b/test/strategies/miter/BUILD.bazel index 2fb4ed33..c8c652cc 100644 --- a/test/strategies/miter/BUILD.bazel +++ b/test/strategies/miter/BUILD.bazel @@ -32,3 +32,24 @@ cc_test( "@googletest//:gtest_main", ], ) + +cc_test( + name = "LatchEventCliTests", + srcs = [ + "LatchEventConfigTests.cpp", + "LatchResetCliTests.cpp", + "LibertyLatchModelsTests.cpp", + "OpaquePolicyCliTests.cpp", + ], + copts = NAJA_HEADER_COPTS, + deps = [ + "//src/bin:kepler_formal_test_driver", + "//src/config:kepler_config", + "//src/sec:kepler_sec", + "@naja", + "@naja//:naja_extra_headers", + "@yaml-cpp", + "@zlib//:zlib", + "@googletest//:gtest_main", + ], +) diff --git a/test/strategies/miter/CMakeLists.txt b/test/strategies/miter/CMakeLists.txt index 80390df8..253b2362 100644 --- a/test/strategies/miter/CMakeLists.txt +++ b/test/strategies/miter/CMakeLists.txt @@ -27,6 +27,10 @@ add_executable(keplerFormalCliTests Btor2ExportCliTests.cpp Btor2ExportDriverTests.cpp DriverSeparationTests.cpp + OpaquePolicyCliTests.cpp + LibertyLatchModelsTests.cpp + LatchEventConfigTests.cpp + LatchResetCliTests.cpp ) target_include_directories(keplerFormalCliTests PRIVATE ${CMAKE_SOURCE_DIR}/src/bin) diff --git a/test/strategies/miter/LatchEventConfigTests.cpp b/test/strategies/miter/LatchEventConfigTests.cpp new file mode 100644 index 00000000..a3270a90 --- /dev/null +++ b/test/strategies/miter/LatchEventConfigTests.cpp @@ -0,0 +1,469 @@ +// Copyright 2026 keplertech.io +// SPDX-License-Identifier: Apache-2.0 + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "KeplerFormalDriver.h" +#include "LatchEventConfig.h" +#include "latch/LatchSupportOptions.h" + +namespace { + +using KEPLER_FORMAL::LatchEventConfig; +using Result = LatchEventConfig::ArgumentResult; + +bool parseYaml(LatchEventConfig& config, const std::string& body, std::string& error) { + return config.parseYaml(YAML::Load("latch_support: true\nsec_latch_events: " + body), error); +} + +Result parseArgument(LatchEventConfig& config, std::vector args, + std::string& error) { + std::vector argv; + for (auto& arg : args) argv.push_back(arg.data()); + int index = 0; + return config.parseArgument(static_cast(argv.size()), argv.data(), index, error); +} + +constexpr auto complete = "{input_changes: single, initial_inputs: 0, initial_storage: 0}"; + +TEST(LatchEventConfigTests, EnabledByDefaultWithoutAnImplicitContract) { + LatchEventConfig config; + std::string error; + EXPECT_TRUE(config.parseYaml(YAML::Load("{}"), error)); + EXPECT_TRUE(config.options().enabled); + EXPECT_FALSE(config.options().initialInputs.has_value()); + EXPECT_FALSE(config.options().initialStorage.has_value()); + EXPECT_FALSE(config.options().singleInputChange); + EXPECT_FALSE(config.options().explicitConfiguration); + EXPECT_TRUE(config.validate(false, false, false, error)); + EXPECT_TRUE(config.validate(true, true, true, error)); + EXPECT_EQ(parseArgument(config, {"--unrelated"}, error), Result::NotHandled); +} + +TEST(LatchEventConfigTests, ExplicitMasterOffPreservesLegacyWithoutTuning) { + LatchEventConfig config; + std::string error; + ASSERT_TRUE(config.parseYaml(YAML::Load("latch_support: false"), error)); + EXPECT_FALSE(config.options().enabled); + EXPECT_TRUE(config.validate(false, true, true, error)); +} + +TEST(LatchEventConfigTests, CompleteEventContractUsesDefaultEnableUnlessExplicitlyDisabled) { + for (const bool disabled : {false, true}) { + LatchEventConfig config; + std::string error; + const std::string gate = disabled ? "latch_support: false\n" : ""; + ASSERT_TRUE(config.parseYaml(YAML::Load(gate + "sec_latch_events: " + complete), error)); + EXPECT_EQ(config.options().enabled, !disabled); + EXPECT_TRUE(config.options().explicitConfiguration); + EXPECT_EQ(config.validate(true, false, false, error), !disabled); + if (disabled) EXPECT_NE(error.find("latch_support"), std::string::npos); + } + LatchEventConfig config; + std::string error; + ASSERT_EQ(parseArgument(config, {"--sec-latch-events", "single"}, error), Result::Parsed); + EXPECT_TRUE(config.options().enabled); + EXPECT_TRUE(config.validate(true, false, false, error)); +} + +TEST(LatchEventConfigTests, DisableFlagPreservesLegacyAndCanBeOverridden) { + LatchEventConfig config; + std::string error; + ASSERT_EQ(parseArgument(config, {"--no-latch_support"}, error), Result::Parsed); + EXPECT_FALSE(config.options().enabled); + EXPECT_TRUE(config.validate(true, true, true, error)); + ASSERT_EQ(parseArgument(config, {"--latch_support"}, error), Result::Parsed); + EXPECT_TRUE(config.options().enabled); + EXPECT_FALSE(config.options().explicitConfiguration); +} + +TEST(LatchEventConfigTests, MasterGateRequiresExactBooleanYamlValues) { + for (const auto* value : {"0", "1", "yes", "off", "null", "[]", "{enabled: true}"}) { + LatchEventConfig config; + std::string error; + EXPECT_FALSE(config.parseYaml(YAML::Load(std::string("latch_support: ") + value), error)); + EXPECT_TRUE(config.options().enabled); + EXPECT_NE(error.find("latch_support"), std::string::npos); + } +} + +TEST(LatchEventConfigTests, MasterEnableAloneDoesNotInventAnEventContract) { + LatchEventConfig config; + std::string error; + ASSERT_EQ(parseArgument(config, {"--latch_support"}, error), Result::Parsed); + EXPECT_TRUE(config.options().enabled); + EXPECT_TRUE(config.validate(true, false, false, error)); + EXPECT_FALSE(config.options().explicitConfiguration); + ASSERT_EQ(parseArgument(config, {"--sec-latch-initial-inputs", "0"}, error), Result::Parsed); + EXPECT_TRUE(config.validate(true, false, false, error)); + EXPECT_FALSE(config.options().initialStorage.has_value()); + ASSERT_EQ(parseArgument(config, {"--sec-latch-initial-storage", "0"}, error), Result::Parsed); + EXPECT_TRUE(config.validate(true, false, false, error)); + EXPECT_FALSE(config.options().singleInputChange); + ASSERT_EQ(parseArgument(config, {"--sec-latch-events", "single"}, error), Result::Parsed); + EXPECT_TRUE(config.validate(true, false, false, error)); +} + +TEST(LatchEventConfigTests, MasterEnableCanFollowTuningFlags) { + LatchEventConfig config; + std::string error; + ASSERT_TRUE(config.parseYaml(YAML::Load(std::string("sec_latch_events: ") + complete), error)); + ASSERT_EQ(parseArgument(config, {"--latch_support"}, error), Result::Parsed); + EXPECT_TRUE(config.validate(true, false, false, error)); +} + +TEST(LatchEventConfigTests, EventModeAndInitializationOverridesAreIndependentlyOptional) { + for (const auto* body : {"{}", "{input_changes: single}", + "{initial_inputs: 0}", "{initial_storage: 1}", "{workers: 2}", + "{input_changes: single, initial_inputs: 0}", + "{input_changes: single, initial_storage: 0}", + "{initial_inputs: 0, initial_storage: 0}"}) { + SCOPED_TRACE(body); + LatchEventConfig config; + std::string error; + ASSERT_TRUE(parseYaml(config, body, error)); + EXPECT_TRUE(config.validate(true, false, false, error)) << error; + EXPECT_TRUE(config.validate(true, true, false, error)) << error; + EXPECT_TRUE(config.options().explicitConfiguration); + const auto supplied = YAML::Load(body); + EXPECT_EQ(config.options().initialInputs.has_value(), bool(supplied["initial_inputs"])); + EXPECT_EQ(config.options().initialStorage.has_value(), bool(supplied["initial_storage"])); + EXPECT_EQ(config.options().singleInputChange, + supplied["input_changes"] && supplied["input_changes"].as() == "single"); + } +} + +TEST(LatchEventConfigTests, BooleanZeroIsPresentRatherThanMissing) { + LatchEventConfig config; + std::string error; + ASSERT_TRUE(parseYaml(config, complete, error)); + EXPECT_TRUE(config.validate(true, false, false, error)); + EXPECT_TRUE(config.options().enabled); + EXPECT_TRUE(config.options().singleInputChange); + EXPECT_EQ(config.options().initialInputs, false); + EXPECT_EQ(config.options().initialStorage, false); +} + +TEST(LatchEventConfigTests, AnyChangesAndExplicitHighInitializationAreAccepted) { + LatchEventConfig config; + std::string error; + ASSERT_TRUE(parseYaml(config, + "{input_changes: any, initial_inputs: 1, initial_storage: 1, workers: 2, " + "max_waves: 7, max_states: 9, max_transactions: 11, max_symbolic_nodes: 123, " + "max_sat_conflicts: 45, max_sat_decisions: 67}", error)); + EXPECT_TRUE(config.validate(true, false, false, error)); + EXPECT_FALSE(config.options().singleInputChange); + EXPECT_EQ(config.options().initialInputs, true); + EXPECT_EQ(config.options().initialStorage, true); + EXPECT_EQ(config.options().workers, 2u); + EXPECT_EQ(config.options().limits.maxWaves, 7u); + EXPECT_EQ(config.options().limits.maxBoundaryStates, 9u); + EXPECT_EQ(config.options().limits.maxTransactions, 11u); + EXPECT_EQ(config.options().maxSymbolicNodes, 123u); + EXPECT_EQ(config.options().maxSatConflicts, 45u); + EXPECT_EQ(config.options().maxSatDecisions, 67u); +} + +TEST(LatchEventConfigTests, RejectsUnknownKeysAndMalformedYamlShapes) { + for (const auto* body : {"null", "true", "[]", "[single]", + "{unknown: 1}", "{workers: [1]}", "{input_changes: {mode: single}}"}) { + SCOPED_TRACE(body); + LatchEventConfig config; + std::string error; + EXPECT_FALSE(parseYaml(config, body, error)); + EXPECT_FALSE(error.empty()); + } +} + +TEST(LatchEventConfigTests, RejectsBadEnumsAndNonBinaryInitialization) { + for (const auto* argument : {"--sec-latch-events", "--sec-latch-initial-inputs", + "--sec-latch-initial-storage"}) { + for (const auto* value : {"", "maybe", "true", "-1", "2"}) { + SCOPED_TRACE(std::string(argument) + " " + value); + LatchEventConfig config; + std::string error; + EXPECT_EQ(parseArgument(config, {argument, value}, error), Result::Error); + } + } +} + +TEST(LatchEventConfigTests, RejectsNegativeOverflowAndZeroResourceLimits) { + for (const auto* argument : {"--sec-latch-max-waves", "--sec-latch-max-states", + "--sec-latch-max-transactions", "--sec-latch-max-nodes", + "--sec-latch-sat-conflicts", "--sec-latch-sat-decisions"}) { + for (const auto* value : {"0", "-1", "184467440737095516160", "1.2", "3junk"}) { + LatchEventConfig config; + std::string error; + EXPECT_EQ(parseArgument(config, {argument, value}, error), Result::Error); + } + } + LatchEventConfig config; + std::string error; + EXPECT_EQ(parseArgument(config, {"--sec-latch-workers", "-1"}, error), Result::Error); + EXPECT_EQ(parseArgument(config, {"--sec-latch-workers", "2147483648"}, error), Result::Error); + EXPECT_EQ(parseArgument(config, {"--sec-latch-workers", "0"}, error), Result::Parsed); +} + +TEST(LatchEventConfigTests, SymbolicBudgetsDoNotRequireOrInventInitialization) { + for (const auto* flag : {"--sec-latch-sat-conflicts", "--sec-latch-sat-decisions"}) { + LatchEventConfig config; + std::string error; + EXPECT_EQ(parseArgument(config, {flag, "4294967296"}, error), Result::Error); + EXPECT_EQ(parseArgument(config, {flag, "42"}, error), Result::Parsed); + EXPECT_TRUE(config.options().enabled); + EXPECT_FALSE(config.options().initialInputs.has_value()); + EXPECT_FALSE(config.options().initialStorage.has_value()); + EXPECT_TRUE(config.options().explicitConfiguration); + EXPECT_TRUE(config.validate(true, false, false, error)); + } + for (const auto* key : {"max_symbolic_nodes", "max_sat_conflicts", "max_sat_decisions"}) { + LatchEventConfig config; + std::string error; + EXPECT_FALSE(parseYaml(config, std::string("{") + key + ": 0}", error)); + } + LatchEventConfig config; + std::string error; + ASSERT_EQ(parseArgument(config, {"--sec-latch-max-nodes", "99"}, error), Result::Parsed); + EXPECT_EQ(config.options().maxSymbolicNodes, 99u); + EXPECT_TRUE(config.validate(true, false, false, error)); +} + +TEST(LatchEventConfigTests, RejectsMissingFlagValueAndIncompatibleWorkflows) { + LatchEventConfig config; + std::string error; + EXPECT_EQ(parseArgument(config, {"--sec-latch-events"}, error), Result::Error); + ASSERT_TRUE(parseYaml(config, complete, error)); + EXPECT_FALSE(config.validate(false, false, false, error)); + EXPECT_TRUE(config.validate(true, true, false, error)); + EXPECT_FALSE(config.validate(true, false, true, error)); + EXPECT_NE(error.find("leaf boundaries"), std::string::npos); +} + +class LatchEventCliTests : public ::testing::Test { + protected: + void SetUp() override { + oldDirectory_ = std::filesystem::current_path(); + directory_ = std::filesystem::temp_directory_path() / + ("kepler_latch_event_cli_" + std::to_string( + std::chrono::steady_clock::now().time_since_epoch().count())); + std::filesystem::create_directories(directory_); + std::filesystem::current_path(directory_); + library_ = "library(test) { cell(LATCH) { latch(IQ, IQN) { enable : E; data_in : D; }" + "pin(D) { direction : input; } pin(E) { direction : input; }" + "pin(Q) { direction : output; function : IQ; } } }"; + write("cells.lib", library_); + write("chain.v", "module top(input d, input e, output q); wire a,b,c;" + "LATCH l0(.D(d),.E(e),.Q(a)); LATCH l1(.D(a),.E(e),.Q(b));" + "LATCH l2(.D(b),.E(e),.Q(c)); LATCH l3(.D(c),.E(e),.Q(q)); endmodule\n"); + write("self.v", "module top(input e, output q); LATCH l0(.D(q),.E(e),.Q(q)); endmodule\n"); + write("race.v", "module top(input d, input e, output q, output good);" + "LATCH l0(.D(d),.E(e),.Q(q)); assign good = d; endmodule\n"); + } + void TearDown() override { + std::filesystem::current_path(oldDirectory_); + std::filesystem::remove_all(directory_); + } + void write(const std::string& name, const std::string& contents) { + std::ofstream(directory_ / name) << contents; + } + KEPLER_FORMAL::RunResult run(std::vector arguments) { + arguments.insert(arguments.begin(), "kepler-formal"); + std::vector argv; + for (auto& argument : arguments) argv.push_back(argument.data()); + KEPLER_FORMAL::RunResult result; + const int code = KEPLER_FORMAL::runKeplerFormal( + static_cast(argv.size()), argv.data(), result); + EXPECT_EQ(code, result.exitCode); + return result; + } + KEPLER_FORMAL::RunResult config(const std::string& design, const std::string& mode, + const std::string& extra = "", + const std::string& library = "cells.lib") { + write("run.yaml", "format: verilog\nverification: sec\nsec_encoding: binary\n" + "input_paths: [" + design + ", " + design + "]\nliberty_files: [" + library + "]\n" + "latch_support: true\nsec_latch_events: {input_changes: " + mode + + ", initial_inputs: 0, initial_storage: 0, workers: 2}\n" + extra); + return run({"--config", "run.yaml"}); + } + std::vector options() { + return {"--latch_support", "--sec-latch-events", "single", "--sec-latch-initial-inputs", "0", + "--sec-latch-initial-storage", "0"}; + } + std::filesystem::path oldDirectory_, directory_; + std::string library_; +}; + +TEST_F(LatchEventCliTests, YamlModelsFourTransparentLatchesInAChain) { + const auto result = config("chain.v", "single"); + EXPECT_EQ(result.status, KEPLER_FORMAL::RunStatus::Equivalent) << result.reason; + EXPECT_EQ(result.exitCode, 0); + EXPECT_EQ(result.coveredOutputs, 1u); +} + +TEST_F(LatchEventCliTests, ModelsWithDefaultEnableAndRejectsExplicitDisableWithTuning) { + auto arguments = options(); + arguments.erase(arguments.begin()); // The master switch now defaults on. + arguments.insert(arguments.end(), {"-verilog", "-v", "sec", "self.v", "self.v", "cells.lib"}); + const auto enabled = run(arguments); + EXPECT_EQ(enabled.status, KEPLER_FORMAL::RunStatus::Equivalent) << enabled.reason; + EXPECT_EQ(enabled.coveredOutputs, 1u); + arguments.push_back("--no-latch_support"); + EXPECT_NE(run(arguments).exitCode, 0); + write("enabled.yaml", "format: verilog\nverification: sec\n" + "input_paths: [self.v, self.v]\nliberty_files: [cells.lib]\n" + "sec_latch_events: " + std::string(complete) + "\n"); + const auto yamlEnabled = run({"--config", "enabled.yaml"}); + EXPECT_EQ(yamlEnabled.status, KEPLER_FORMAL::RunStatus::Equivalent) << yamlEnabled.reason; + EXPECT_EQ(yamlEnabled.coveredOutputs, 1u); + write("disabled.yaml", "format: verilog\nverification: sec\n" + "input_paths: [self.v, self.v]\nliberty_files: [cells.lib]\n" + "latch_support: false\nsec_latch_events: " + std::string(complete) + "\n"); + EXPECT_NE(run({"--config", "disabled.yaml"}).exitCode, 0); +} + +TEST_F(LatchEventCliTests, MasterDisabledRetainsOpaqueLatchesAndIndependentStrictPolicy) { + const std::string base = "format: verilog\nverification: sec\n" + "input_paths: [race.v, race.v]\nliberty_files: [cells.lib]\nlatch_support: false\n"; + write("disabled.yaml", base); + const auto legacy = run({"--config", "disabled.yaml"}); + EXPECT_EQ(legacy.coveredOutputs, 1u); + EXPECT_EQ(legacy.totalOutputs, 2u); + write("disabled.yaml", base + "error_on_opaque: true\n"); + const auto strict = run({"--config", "disabled.yaml"}); + EXPECT_NE(strict.exitCode, 0); + EXPECT_EQ(strict.status, KEPLER_FORMAL::RunStatus::Unsupported); + EXPECT_NE(strict.reason.find("error-on-opaque"), std::string::npos); +} + +TEST_F(LatchEventCliTests, DisableFlagBeforeAndAfterFormatKeepsUnsupportedRaceOpaque) { + const std::vector base{ + "-verilog", "-v", "sec", "race.v", "race.v", "cells.lib"}; + const auto implicit = run(base); + EXPECT_EQ(implicit.coveredOutputs, 1u); + EXPECT_EQ(implicit.totalOutputs, 2u); + for (const bool before : {true, false}) { + auto arguments = base; + arguments.insert(before ? arguments.begin() : arguments.end(), "--no-latch_support"); + const auto disabled = run(arguments); + EXPECT_EQ(disabled.status, implicit.status) << disabled.reason; + EXPECT_EQ(disabled.exitCode, implicit.exitCode); + EXPECT_EQ(disabled.coveredOutputs, implicit.coveredOutputs); + EXPECT_EQ(disabled.skippedObservedOutputs.size(), implicit.skippedObservedOutputs.size()); + ASSERT_FALSE(implicit.skippedObservedOutputs.empty()); + EXPECT_NE(implicit.skippedObservedOutputs.front().find("uniqueness"), std::string::npos); + } +} + +TEST_F(LatchEventCliTests, DefaultModelsTransparentLatchWithoutAnyInitializationSettings) { + write("open.v", "module top(input d, output q); LATCH l(.D(d),.E(1'b1),.Q(q)); endmodule\n"); + for (const auto* encoding : {"binary", "dual_rail_steady"}) { + const auto result = run({"-verilog", "-v", "sec", "--sec-encoding", encoding, + "open.v", "open.v", "cells.lib"}); + EXPECT_EQ(result.status, KEPLER_FORMAL::RunStatus::Equivalent) << result.reason; + EXPECT_EQ(result.coveredOutputs, 1u); + const auto disabled = run({"--no-latch_support", "-verilog", "-v", "sec", + "open.v", "open.v", "cells.lib"}); + EXPECT_EQ(disabled.coveredOutputs, 0u); + } +} + +TEST_F(LatchEventCliTests, UnspecifiedStorageDoesNotAcquireACommonPowerUpValue) { + for (const auto* encoding : {"binary", "dual_rail_steady"}) { + for (const auto* engine : {"k_induction", "pdr", "imc"}) { + const auto result = run({"-verilog", "-v", "sec", "--sec-engine", engine, + "--sec-encoding", encoding, "self.v", "self.v", "cells.lib"}); + EXPECT_EQ(result.status, KEPLER_FORMAL::RunStatus::Different) << result.reason; + EXPECT_EQ(result.coveredOutputs, 1u); + } + } + const auto explicitStorage = run({"--sec-latch-initial-storage", "0", "-verilog", + "-v", "sec", "self.v", "self.v", "cells.lib"}); + EXPECT_EQ(explicitStorage.status, KEPLER_FORMAL::RunStatus::Equivalent) << explicitStorage.reason; +} + +TEST_F(LatchEventCliTests, SelfFeedbackRetainsExplicitInitialStorage) { + const auto result = config("self.v", "any"); + EXPECT_EQ(result.status, KEPLER_FORMAL::RunStatus::Equivalent) << result.reason; + EXPECT_EQ(result.coveredOutputs, 1u); +} + +TEST_F(LatchEventCliTests, AnyInputChangesKeepRaceOpaqueWhileSingleChangesModelIt) { + const auto any = config("race.v", "any"); + EXPECT_EQ(any.coveredOutputs, 1u) << any.reason; + EXPECT_EQ(any.totalOutputs, 2u); + EXPECT_EQ(any.skippedObservedOutputs.size(), 1u); + const auto single = config("race.v", "single"); + EXPECT_EQ(single.status, KEPLER_FORMAL::RunStatus::Equivalent) << single.reason; + EXPECT_EQ(single.coveredOutputs, 2u); +} + +TEST_F(LatchEventCliTests, StrictOpacityPolicyStopsAnUncertifiedLatchComponent) { + const auto result = config("race.v", "any", "error_on_opaque: true\n"); + EXPECT_NE(result.exitCode, 0); + EXPECT_EQ(result.status, KEPLER_FORMAL::RunStatus::Unsupported); + EXPECT_NE(result.reason.find("error-on-opaque"), std::string::npos); +} + +TEST_F(LatchEventCliTests, FlagsAreAcceptedBeforeAndAfterFormat) { + const std::vector base{ + "-verilog", "-v", "sec", "--sec-encoding", "binary", "self.v", "self.v", "cells.lib"}; + for (const bool before : {true, false}) { + auto arguments = before ? options() : base; + const auto suffix = before ? base : options(); + arguments.insert(arguments.end(), suffix.begin(), suffix.end()); + const auto result = run(arguments); + EXPECT_EQ(result.status, KEPLER_FORMAL::RunStatus::Equivalent) << result.reason; + } +} + +TEST_F(LatchEventCliTests, RejectsLecClocklessResetCyclesAndSelectedLeafBoundaries) { + const std::vector> incompatible{ + {"-v", "lec"}, + {"-v", "sec", "--sec-reset-cycles", "1", "--sec-reset-port", "e=1"}, + {"-v", "sec", "--set-as-boundary", "l0", "l0"}}; + for (const auto& suffix : incompatible) { + auto arguments = options(); + arguments.insert(arguments.end(), {"-verilog", "self.v", "self.v", "cells.lib"}); + arguments.insert(arguments.end(), suffix.begin(), suffix.end()); + EXPECT_NE(run(arguments).exitCode, 0); + } +} + +TEST_F(LatchEventCliTests, GzipLibraryIsModeledInCompactMode) { + auto* file = gzopen((directory_ / "cells.lib.gz").string().c_str(), "wb"); + ASSERT_NE(file, nullptr); + ASSERT_EQ(gzwrite(file, library_.data(), library_.size()), library_.size()); + ASSERT_EQ(gzclose(file), Z_OK); + const auto result = config("chain.v", "single", "compact_mode: true\n", "cells.lib.gz"); + EXPECT_EQ(result.status, KEPLER_FORMAL::RunStatus::Equivalent) << result.reason; + EXPECT_EQ(result.coveredOutputs, 1u); +} + +TEST_F(LatchEventCliTests, WorkflowRestoresOuterEventContractWithoutInheritingIt) { + namespace Latch = KEPLER_FORMAL::SEC::LATCH; + Latch::SupportOptions original; + original.enabled = true; + original.initialInputs = true; + original.initialStorage = true; + original.workers = 3; + Latch::ScopedSupportOptions outer(original); + EXPECT_EQ(config("self.v", "single").status, KEPLER_FORMAL::RunStatus::Equivalent); + EXPECT_TRUE(Latch::supportOptions().enabled); + EXPECT_EQ(Latch::supportOptions().initialInputs, true); + EXPECT_EQ(Latch::supportOptions().workers, 3u); + const auto unspecified = run({"-verilog", "-v", "sec", "self.v", "self.v", "cells.lib"}); + EXPECT_EQ(unspecified.status, KEPLER_FORMAL::RunStatus::Different) << unspecified.reason; + EXPECT_EQ(unspecified.coveredOutputs, 1u); + EXPECT_TRUE(Latch::supportOptions().enabled); + EXPECT_EQ(Latch::supportOptions().initialStorage, true); +} + +} // namespace diff --git a/test/strategies/miter/LatchResetCliTests.cpp b/test/strategies/miter/LatchResetCliTests.cpp new file mode 100644 index 00000000..db9ca81b --- /dev/null +++ b/test/strategies/miter/LatchResetCliTests.cpp @@ -0,0 +1,259 @@ +// Copyright 2026 keplertech.io +// SPDX-License-Identifier: Apache-2.0 + +#include + +#include +#include +#include +#include +#include +#include "KeplerFormalDriver.h" +#include "latch/LatchSupportOptions.h" + +namespace { + +class LatchResetCliTests : public ::testing::Test { + protected: + void SetUp() override { + previousDirectory_ = std::filesystem::current_path(); + directory_ = std::filesystem::temp_directory_path() / + ("kepler_latch_reset_cli_" + std::to_string( + std::chrono::steady_clock::now().time_since_epoch().count())); + ASSERT_TRUE(std::filesystem::create_directory(directory_)); + std::filesystem::current_path(directory_); + write("cells.lib", R"( +library(test) { + cell(LATCH) { + latch(IQ, IQN) { enable : E; data_in : D; } + pin(D) { direction : input; } pin(E) { direction : input; } + pin(Q) { direction : output; function : IQ; } + } + cell(DFF) { + ff(IQ, IQN) { clocked_on : C; next_state : "D & !R"; } + pin(D) { direction : input; } pin(C) { direction : input; } pin(R) { direction : input; } + pin(Q) { direction : output; function : IQ; } + } + cell(DFFN) { + ff(IQ, IQN) { clocked_on : C; next_state : D; clear : "!R"; } + pin(D) { direction : input; } pin(C) { direction : input; } pin(R) { direction : input; } + pin(Q) { direction : output; function : IQ; } + } + cell(BUF) { pin(A) { direction : input; } pin(Y) { direction : output; function : A; } } + cell(INV) { pin(A) { direction : input; } pin(Y) { direction : output; function : "!A"; } } + cell(XOR2) { + pin(A) { direction : input; } pin(B) { direction : input; } + pin(Y) { direction : output; function : "A ^ B"; } + } +})"); + write("chain.v", chain("DFF", "data", "clock")); + write("different.v", chain("DFF", "inverted_data", "clock", + "wire inverted_data; INV invert_data(.A(data),.Y(inverted_data));")); + write("active_low.v", chain("DFFN", "data", "clock")); + write("buffered.v", chain("DFF", "data", "local_clock", + "wire local_clock; BUF route(.A(clock),.Y(local_clock));")); + write("inverted.v", chain("DFF", "data", "local_clock", + "wire local_clock; INV route(.A(clock),.Y(local_clock));")); + write("latch_only.v", "module top(input clock, data, enable, reset, output out);" + " LATCH l(.D(data),.E(enable),.Q(out)); endmodule\n"); + write("two_clocks.v", "module top(input clock, other_clock, data, enable, reset, output out, other_out);" + " DFF f(.D(data),.C(clock),.R(reset),.Q(out));" + " DFF g(.D(data),.C(other_clock),.R(reset),.Q(other_out)); endmodule\n"); + write("masked_left.v", "module top(input clock, data, enable, reset, output out);" + " DFF f(.D(data),.C(clock),.R(reset),.Q(out)); endmodule\n"); + write("masked_right.v", "module top(input clock, data, enable, reset, output out); wire q;" + " DFF f(.D(data),.C(clock),.R(reset),.Q(q)); XOR2 mask(.A(q),.B(reset),.Y(out)); endmodule\n"); + write("wire.v", "module top(input clock, data, enable, reset, output out);" + " assign out = data; endmodule\n"); + write("wire_different.v", "module top(input clock, data, enable, reset, output out);" + " INV invert_data(.A(data),.Y(out)); endmodule\n"); + } + void TearDown() override { + std::filesystem::current_path(previousDirectory_); + std::filesystem::remove_all(directory_); + } + std::string chain(const std::string& flop, const std::string& data, const std::string& clock, + const std::string& route = "") { + return "module top(input clock, data, enable, reset, output out); wire q, middle; " + route + + flop + " f(.D(" + data + "),.C(" + clock + "),.R(reset),.Q(q));" + " LATCH a(.D(q),.E(enable),.Q(middle)); LATCH b(.D(middle),.E(enable),.Q(out)); endmodule\n"; + } + void write(const std::string& file, const std::string& text) { std::ofstream(directory_ / file) << text; } + KEPLER_FORMAL::RunResult run(std::vector arguments) { + arguments.insert(arguments.begin(), "kepler-formal"); + std::vector argv; + for (auto& argument : arguments) argv.push_back(argument.data()); + KEPLER_FORMAL::RunResult result; + const int code = KEPLER_FORMAL::runKeplerFormal(static_cast(argv.size()), argv.data(), result); + EXPECT_EQ(code, result.exitCode); + return result; + } + KEPLER_FORMAL::RunResult yaml(const std::string& first, const std::string& second, + bool compact = false, bool activeHigh = true, const std::string& extraPorts = "") { + write("run.yaml", "format: verilog\nverification: sec\nsec_engine: pdr\nsec_encoding: binary\n" + "max_k: 48\ninput_paths: [" + first + ", " + second + "]\nliberty_files: [cells.lib]\n" + "compact_mode: " + (compact ? "true" : "false") + "\n" + "latch_support: true\n" + "sec_latch_events: {input_changes: single, initial_inputs: 0, initial_storage: 0, workers: 2}\n" + "sec_reset:\n cycles: 2\n ports:\n - name: reset\n active_value: " + + (activeHigh ? "1" : "0") + "\n" + extraPorts); + return run({"--config", "run.yaml"}); + } + std::filesystem::path previousDirectory_, directory_; +}; + +TEST_F(LatchResetCliTests, YamlResetCyclesWorkWithIndependentLatchEnableAndCompactExtraction) { + for (const bool compact : {false, true}) { + const auto result = yaml("chain.v", "chain.v", compact); + EXPECT_EQ(result.status, KEPLER_FORMAL::RunStatus::Equivalent) << result.reason; + EXPECT_EQ(result.exitCode, 0); + EXPECT_EQ(result.coveredOutputs, 1u); + } +} + +TEST_F(LatchResetCliTests, ResetFlagsComposeWithLatchSupportFlags) { + const auto result = run({"--latch_support", "--sec-latch-events", "single", + "--sec-latch-initial-inputs", "0", "--sec-latch-initial-storage", "0", + "--sec-reset-cycles", "2", "--sec-reset-port", "reset=1", + "-verilog", "-v", "sec", "--sec-encoding", "binary", "chain.v", "chain.v", "cells.lib"}); + EXPECT_EQ(result.status, KEPLER_FORMAL::RunStatus::Equivalent) << result.reason; + EXPECT_EQ(result.coveredOutputs, 1u); +} + +TEST_F(LatchResetCliTests, BufferedAndInvertedClockRoutesAreDiscovered) { + for (const auto* design : {"buffered.v", "inverted.v"}) { + const auto result = yaml(design, design); + EXPECT_EQ(result.status, KEPLER_FORMAL::RunStatus::Equivalent) << result.reason; + EXPECT_EQ(result.coveredOutputs, 1u); + } +} + +TEST_F(LatchResetCliTests, DifferentAfterResetRemainsDifferentInBothExtractionModes) { + for (const bool compact : {false, true}) { + const auto result = yaml("chain.v", "different.v", compact); + EXPECT_EQ(result.status, KEPLER_FORMAL::RunStatus::Different) << result.reason; + EXPECT_NE(result.exitCode, 0); + EXPECT_EQ(result.coveredOutputs, 1u); + } +} + +TEST_F(LatchResetCliTests, ResetOnlyMismatchIsNotComparedAndResetCannotBeReasserted) { + const auto result = yaml("masked_left.v", "masked_right.v", true); + EXPECT_EQ(result.status, KEPLER_FORMAL::RunStatus::Equivalent) << result.reason; + EXPECT_EQ(result.coveredOutputs, 1u); +} + +TEST_F(LatchResetCliTests, ActiveLowAsynchronousResetIsAccepted) { + const auto result = yaml("active_low.v", "active_low.v", true, false); + EXPECT_EQ(result.status, KEPLER_FORMAL::RunStatus::Equivalent) << result.reason; + EXPECT_EQ(result.coveredOutputs, 1u); +} + +TEST_F(LatchResetCliTests, MissingEdgeClockDoesNotTreatLatchEnableAsAClock) { + const auto result = yaml("latch_only.v", "latch_only.v"); + EXPECT_EQ(result.status, KEPLER_FORMAL::RunStatus::Unsupported) << result.reason; + EXPECT_NE(result.reason.find("flip-flop clock"), std::string::npos) << result.reason; + EXPECT_EQ(result.reason.find("cannot use clock-cycle"), std::string::npos) << result.reason; +} + +TEST_F(LatchResetCliTests, MultipleClockRootsHaveASpecificDiagnostic) { + const auto result = yaml("two_clocks.v", "two_clocks.v"); + EXPECT_EQ(result.status, KEPLER_FORMAL::RunStatus::Unsupported) << result.reason; + EXPECT_NE(result.reason.find("multiple independent"), std::string::npos) << result.reason; +} + +TEST_F(LatchResetCliTests, MultipleResetPortsAreRejectedExplicitly) { + const auto result = yaml("chain.v", "chain.v", false, true, + " - name: enable\n active_value: 0\n"); + EXPECT_EQ(result.status, KEPLER_FORMAL::RunStatus::Unsupported) << result.reason; + EXPECT_NE(result.reason.find("reset"), std::string::npos) << result.reason; + EXPECT_EQ(result.reason.find("cannot use clock-cycle"), std::string::npos) << result.reason; +} + +TEST_F(LatchResetCliTests, OffOnOffPreservesLegacyResetSemanticsForEveryEngine) { + const auto legacy = [&](const std::string& engine, const std::string& second, + bool explicitlyDisabled = true) { + // Legacy reset supports multiple reset ports and needs no discovered FF + // carrier. Either leaked event dispatch or leaked reset sampling rejects + // this deliberately clockless design instead of preserving that behavior. + write("legacy.yaml", "format: verilog\nverification: sec\nsec_engine: " + engine + + "\nsec_encoding: binary\nmax_k: 8\n" + + (explicitlyDisabled ? "latch_support: false\n" : "") + + "input_paths: [wire.v, " + second + "]\nliberty_files: [cells.lib]\n" + "sec_reset:\n cycles: 2\n ports:\n" + " - name: reset\n active_value: 1\n" + " - name: enable\n active_value: 0\n"); + return run({"--config", "legacy.yaml"}); + }; + for (const auto* engine : {"k_induction", "imc", "pdr"}) { + SCOPED_TRACE(engine); + for (const auto* second : {"wire.v", "wire_different.v"}) { + SCOPED_TRACE(second); + const auto before = legacy(engine, second); + const auto enabled = yaml("chain.v", "chain.v"); + EXPECT_EQ(enabled.status, KEPLER_FORMAL::RunStatus::Equivalent) << enabled.reason; + const auto after = legacy(engine, second); + EXPECT_EQ(before.status, std::string(second) == "wire.v" + ? KEPLER_FORMAL::RunStatus::Equivalent : KEPLER_FORMAL::RunStatus::Different) << before.reason; + EXPECT_EQ(before.coveredOutputs, 1u) << before.reason; + EXPECT_EQ(after.status, before.status) << after.reason; + EXPECT_EQ(after.exitCode, before.exitCode); + EXPECT_EQ(after.coveredOutputs, before.coveredOutputs); + EXPECT_EQ(after.totalOutputs, before.totalOutputs); + EXPECT_EQ(after.skippedObservedOutputs, before.skippedObservedOutputs); + EXPECT_EQ(after.reason, before.reason); + EXPECT_EQ(after.reason.find("Event contract"), std::string::npos); + EXPECT_EQ(after.reason.find("boolean-epochs"), std::string::npos); + EXPECT_EQ(after.reason.find("reset/event step"), std::string::npos); + const auto implicit = legacy(engine, second, false); + EXPECT_EQ(implicit.status, before.status) << implicit.reason; + EXPECT_EQ(implicit.exitCode, before.exitCode); + EXPECT_EQ(implicit.coveredOutputs, before.coveredOutputs); + EXPECT_EQ(implicit.totalOutputs, before.totalOutputs); + EXPECT_EQ(implicit.skippedObservedOutputs, before.skippedObservedOutputs); + EXPECT_EQ(implicit.reason, before.reason); + EXPECT_TRUE(KEPLER_FORMAL::SEC::LATCH::supportOptions().enabled); + EXPECT_FALSE(KEPLER_FORMAL::SEC::LATCH::supportOptions().explicitConfiguration); + EXPECT_FALSE(KEPLER_FORMAL::SEC::LATCH::supportOptions().initialInputs.has_value()); + } + } +} + +TEST_F(LatchResetCliTests, ResetCanFlushSymbolicStorageWithoutPowerUpSettings) { + // The open latch follows a synchronously reset flop. Unlike the independent + // enable fixture, this really does flush every state affecting the output. + write("open_reset.v", "module top(input clock, data, reset, output out); wire q;" + " DFF f(.D(data),.C(clock),.R(reset),.Q(q));" + " LATCH l(.D(q),.E(1'b1),.Q(out)); endmodule\n"); + // This positive end-to-end property exceeds IMC's exact-frontier threshold + // and its current interpolant budget. KI/PDR prove it in both encodings; + // cross-engine relational BOOT semantics have separate focused tests. + for (const auto* engine : {"k_induction", "pdr"}) { + for (const auto* encoding : {"binary", "dual_rail_steady"}) { + for (const bool compact : {false, true}) { + write("symbolic.yaml", std::string("format: verilog\nverification: sec\nsec_engine: ") + engine + + "\nsec_encoding: " + encoding + "\nmax_k: 12\n" + "input_paths: [open_reset.v, open_reset.v]\nliberty_files: [cells.lib]\n" + "compact_mode: " + (compact ? "true" : "false") + "\n" + "sec_latch_events: {input_changes: single}\n" + "sec_reset: {cycles: 2, ports: [{name: reset, active_value: 1}]}\n"); + const auto result = run({"--config", "symbolic.yaml"}); + EXPECT_EQ(result.status, KEPLER_FORMAL::RunStatus::Equivalent) << result.reason; + EXPECT_EQ(result.coveredOutputs, 1u); + } + } + } +} + +TEST_F(LatchResetCliTests, ResetDoesNotInitializeAClosedUnresetLatch) { + write("closed_reset.v", "module top(input clock, data, reset, output out); wire q;" + " DFF f(.D(data),.C(clock),.R(reset),.Q(q));" + " LATCH l(.D(q),.E(1'b0),.Q(out)); endmodule\n"); + const auto result = run({"--sec-latch-events", "single", "--sec-reset-cycles", "2", + "--sec-reset-port", "reset=1", "-verilog", "-v", "sec", "--sec-encoding", "binary", + "closed_reset.v", "closed_reset.v", "cells.lib"}); + EXPECT_EQ(result.status, KEPLER_FORMAL::RunStatus::Different) << result.reason; + EXPECT_EQ(result.coveredOutputs, 1u); +} + +} // namespace diff --git a/test/strategies/miter/LibertyLatchModelsTests.cpp b/test/strategies/miter/LibertyLatchModelsTests.cpp new file mode 100644 index 00000000..6f3125d4 --- /dev/null +++ b/test/strategies/miter/LibertyLatchModelsTests.cpp @@ -0,0 +1,261 @@ +// Copyright 2024-2026 keplertech.io +// SPDX-License-Identifier: Apache-2.0 + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "LibertyLatchModels.h" +#include "NLUniverse.h" +#include "NLLibrary.h" +#include "SNLDesign.h" +#include "SNLDesignModeling.h" +#include "SNLLibertyConstructor.h" +#include "SNLScalarTerm.h" + +namespace { + +using namespace naja::NL; +using Modeling = SNLDesignModeling; +using Expression = Modeling::BooleanExpression; +using Conflict = Modeling::SequentialState::ClearPresetValue; + +bool evaluate(const Expression& expression, + const std::map& inputs, + const std::vector& states = {}) { + const auto visit = [&](const auto& self, size_t id) -> bool { + const auto& node = expression.nodes.at(id); + switch (node.operation) { + case Expression::Operator::Constant: return node.constant; + case Expression::Operator::Term: return inputs.at(node.term->getName().getString()); + case Expression::Operator::State: return states.at(node.state); + case Expression::Operator::Not: return !self(self, node.operands.at(0)); + case Expression::Operator::And: { + bool value = true; + for (const auto operand : node.operands) value &= self(self, operand); + return value; + } + case Expression::Operator::Or: { + bool value = false; + for (const auto operand : node.operands) value |= self(self, operand); + return value; + } + case Expression::Operator::Xor: { + bool value = false; + for (const auto operand : node.operands) value ^= self(self, operand); + return value; + } + } + throw std::runtime_error("unknown expression operator"); + }; + return visit(visit, expression.root); +} + +class LibertyLatchModelsTests : public ::testing::Test { + protected: + void SetUp() override { + auto* db = NLDB::create(NLUniverse::create()); + library_ = NLLibrary::create(db, NLLibrary::Type::Primitives, NLName("cells")); + directory_ = std::filesystem::temp_directory_path() / + ("kepler_latch_liberty_" + std::to_string( + std::chrono::steady_clock::now().time_since_epoch().count())); + std::filesystem::create_directories(directory_); + } + void TearDown() override { + if (auto* universe = NLUniverse::get()) universe->destroy(); + std::filesystem::remove_all(directory_); + } + std::string cell(const std::string& behavior, + const std::string& function = "IQ", + const std::string& extra = "", + const std::string& name = "LATCH") { + return "cell(" + name + ") { " + behavior + + " pin(D) { direction : input; } pin(E) { direction : input; }" + " pin(CLK) { direction : input; } pin(R) { direction : input; }" + " pin(S) { direction : input; } pin(Q) { direction : output; function : \"" + + function + "\"; } " + extra + " } "; + } + SNLDesign* load(const std::string& cells, bool gzip = false) { + const auto path = directory_ / ("cells" + std::to_string(nextFile_++) + ".lib"); + const auto contents = "library(test) { " + cells + " }"; + if (gzip) { + auto* file = gzopen(path.string().c_str(), "wb"); + if (!file) throw std::runtime_error("cannot create gzip fixture"); + const auto count = gzwrite(file, contents.data(), contents.size()); + const auto status = gzclose(file); + if (count != contents.size() || status != Z_OK) throw std::runtime_error("gzip write failed"); + } else { + std::ofstream(path) << contents; + } + KEPLER_FORMAL::constructLibertyWithLatchModels(library_, path); + return library_->getSNLDesign(NLName("LATCH")); + } + const Modeling::SequentialModel& model(SNLDesign* design) { + return Modeling::getSequentialModel(design); + } + NLLibrary* library_ = nullptr; + std::filesystem::path directory_; + unsigned nextFile_ = 0; +}; + +TEST_F(LibertyLatchModelsTests, ParsesActiveLowLatchDataAndComplementedOutput) { + auto* design = load(cell("latch(IQ, IQN) { enable : \"!E\"; data_in : \"D\"; }", + "IQN")); + ASSERT_TRUE(Modeling::hasSequentialModel(design)); + const auto& latch = model(design); + EXPECT_EQ(latch.kind, Modeling::SequentialModel::Kind::Latch); + ASSERT_EQ(latch.states.size(), 1u); + EXPECT_TRUE(evaluate(latch.clockedOn, {{"E", false}})); + EXPECT_FALSE(evaluate(latch.clockedOn, {{"E", true}})); + EXPECT_TRUE(evaluate(latch.states[0].nextState, {{"D", true}})); + EXPECT_FALSE(evaluate(latch.outputs[0].function, {}, {true})); + EXPECT_FALSE(Modeling::getOutputRelatedClocks(design->getScalarTerm(NLName("Q"))).empty()); +} + +TEST_F(LibertyLatchModelsTests, ClockGateUsesActualLatchAndOutputExpressions) { + auto* design = load(cell("latch(IQ, IQN) { enable : \"!CLK\"; data_in : \"D | E\"; }", + "IQ & CLK", "clock_gating_integrated_cell : latch_posedge;")); + ASSERT_TRUE(Modeling::hasSequentialModel(design)); + const auto& latch = model(design); + EXPECT_TRUE(evaluate(latch.clockedOn, {{"CLK", false}})); + EXPECT_TRUE(evaluate(latch.states[0].nextState, {{"D", false}, {"E", true}})); + EXPECT_FALSE(evaluate(latch.outputs[0].function, {{"CLK", false}}, {true})); + EXPECT_TRUE(evaluate(latch.outputs[0].function, {{"CLK", true}}, {true})); +} + +TEST_F(LibertyLatchModelsTests, ResetPresetAndConflictModesAreRetained) { + const std::vector> cases = { + {"L", Conflict::Zero}, {"H", Conflict::One}, {"N", Conflict::Hold}, + {"T", Conflict::Toggle}, {"X", Conflict::Unknown}}; + for (size_t i = 0; i < cases.size(); ++i) { + const auto& [value, expected] = cases[i]; + const auto second = value == "L" ? "H" : value == "H" ? "L" : value; + const auto name = "LATCH" + std::to_string(i); + load(cell("latch(IQ, IQN) { enable : E; data_in : D; clear : R; preset : S; " + "clear_preset_var1 : " + value + "; clear_preset_var2 : " + second + "; }", + "IQ", "", name)); + auto* design = library_->getSNLDesign(NLName(name)); + ASSERT_TRUE(Modeling::hasSequentialModel(design)); + const auto& state = model(design).states[0]; + EXPECT_EQ(state.clearPresetValue, expected); + ASSERT_TRUE(state.clear.has_value()); + ASSERT_TRUE(state.preset.has_value()); + EXPECT_TRUE(evaluate(*state.clear, {{"R", true}})); + EXPECT_FALSE(evaluate(*state.preset, {{"S", false}})); + } +} + +TEST_F(LibertyLatchModelsTests, MultipleGroupsRequireSharedEnable) { + auto* design = load(cell("latch(IQ, IQN) { enable : E; data_in : D; }" + "latch(JQ, JQN) { enable : E; data_in : !D; }", "IQ ^ JQ")); + ASSERT_TRUE(Modeling::hasSequentialModel(design)); + ASSERT_EQ(model(design).states.size(), 2u); + EXPECT_TRUE(evaluate(model(design).outputs[0].function, {}, {true, false})); + EXPECT_FALSE(evaluate(model(design).states[1].nextState, {{"D", true}})); +} + +TEST_F(LibertyLatchModelsTests, UnsupportedLatchDescriptionsStayUnmodeled) { + const std::vector unsupported = { + "latch(IQ, IQN) { data_in : D; }", + "latch(IQ, IQN) { enable : E; }", + "latch(IQ, IQN) { enable : E; data_in : MISSING; }", + "latch(IQ, IQN) { enable : E; data_in : Q; }", + "latch(IQ, IQN) { enable : E; data_in : D; enable_also : CLK; }", + "latch(IQ) { enable : E; data_in : D; clear_preset_var2 : H; }", + "latch(IQ, IQN) { enable : E; data_in : D; clear : R; preset : S; " + "clear_preset_var1 : L; clear_preset_var2 : L; }", + "latch(IQ, IQN) { enable : E; data_in : D; clear : R; preset : S; " + "clear_preset_var1 : H; }", + "latch(IQ, IQN) { enable : E; data_in : D; }" + "latch(JQ, JQN) { enable : CLK; data_in : D; }", + "latch(IQ, IQN) { enable : E; data_in : D; }" + "latch(IQ, JQN) { enable : E; data_in : D; }", + "latch(D, IQN) { enable : E; data_in : D; }", + "latch(IQ, IQN) { enable : E; data_in : D; }" + "ff(FQ, FQN) { clocked_on : CLK; next_state : D; }", + "latch(IQ, IQN) { enable : E; data_in : D; }" + "statetable(\"D E\", \"IQ\") { table : \"- - : - : -\"; }", + "latch(IQ, IQN) { enable : E; data_in : D; } power_down_function : R;", + }; + for (size_t i = 0; i < unsupported.size(); ++i) { + const auto name = "UNSUPPORTED" + std::to_string(i); + SCOPED_TRACE(unsupported[i]); + load(cell(unsupported[i], "IQ", "", name)); + auto* design = library_->getSNLDesign(NLName(name)); + ASSERT_NE(design, nullptr); + EXPECT_FALSE(Modeling::hasSequentialModel(design)); + } +} + +TEST_F(LibertyLatchModelsTests, DoesNotInferLatchFromClockGateMetadataOrNames) { + auto* design = load(cell("", "D & CLK", + "clock_gating_integrated_cell : latch_posedge;")); + EXPECT_FALSE(Modeling::hasSequentialModel(design)); +} + +TEST_F(LibertyLatchModelsTests, UnsupportedOutputPinsAndBusRemainUnmodeled) { + const std::vector extraPins = { + "pin(Z) { direction : output; }", + "pin(Z) { direction : output; function : IQ; three_state : S; }", + "pin(Z) { direction : inout; }", + "bus(B) { direction : input; bus_type : two_bits; }", + }; + for (size_t i = 0; i < extraPins.size(); ++i) { + const auto name = "PINS" + std::to_string(i); + load("type(two_bits) { base_type : array; data_type : bit; bit_width : 2; " + "bit_from : 1; bit_to : 0; downto : true; } " + + cell("latch(IQ, IQN) { enable : E; data_in : D; }", "IQ", extraPins[i], name)); + auto* design = library_->getSNLDesign(NLName(name)); + ASSERT_NE(design, nullptr); + EXPECT_FALSE(Modeling::hasSequentialModel(design)); + } +} + +TEST_F(LibertyLatchModelsTests, ExistingDefinitionWinsAcrossFiles) { + auto* first = load(cell("latch(IQ, IQN) { enable : E; data_in : D; }")); + const auto before = model(first).clockedOn; + auto* second = load(cell("latch(IQ, IQN) { enable : !E; data_in : !D; }")); + EXPECT_EQ(first, second); + EXPECT_TRUE(evaluate(model(second).clockedOn, {{"E", true}})); + EXPECT_TRUE(evaluate(before, {{"E", true}})); +} + +TEST_F(LibertyLatchModelsTests, EarlierUnsupportedDuplicateIsNotUpgraded) { + auto* design = load(cell("latch(IQ, IQN) { enable : E; }") + + cell("latch(IQ, IQN) { enable : E; data_in : D; }")); + EXPECT_FALSE(Modeling::hasSequentialModel(design)); + load(cell("latch(IQ, IQN) { enable : E; data_in : D; }")); + EXPECT_FALSE(Modeling::hasSequentialModel(design)); +} + +TEST_F(LibertyLatchModelsTests, GzipSignatureIsSupportedWithoutGzipExtension) { + auto* design = load(cell("latch(IQ, IQN) { enable : E; data_in : D; }"), true); + EXPECT_TRUE(Modeling::hasSequentialModel(design)); +} + +TEST_F(LibertyLatchModelsTests, OrdinaryFrontendBehaviorRemainsUnchanged) { + const auto path = directory_ / "ordinary.lib"; + std::ofstream(path) << "library(test) { " + << cell("latch(IQ, IQN) { enable : E; data_in : D; }") << " }"; + SNLLibertyConstructor(library_).construct(path); + auto* design = library_->getSNLDesign(NLName("LATCH")); + EXPECT_FALSE(Modeling::hasSequentialModel(design)); + KEPLER_FORMAL::constructLibertyWithLatchModels(library_, path); + EXPECT_FALSE(Modeling::hasSequentialModel(design)); +} + +TEST_F(LibertyLatchModelsTests, RejectsMissingInputAndNullLibrary) { + EXPECT_THROW(KEPLER_FORMAL::constructLibertyWithLatchModels( + library_, directory_ / "missing.lib"), std::exception); + EXPECT_THROW(KEPLER_FORMAL::constructLibertyWithLatchModels( + nullptr, directory_ / "missing.lib"), std::invalid_argument); +} + +} // namespace diff --git a/test/strategies/miter/OpaquePolicyCliTests.cpp b/test/strategies/miter/OpaquePolicyCliTests.cpp new file mode 100644 index 00000000..b4e3b9ff --- /dev/null +++ b/test/strategies/miter/OpaquePolicyCliTests.cpp @@ -0,0 +1,117 @@ +// Copyright 2024-2026 keplertech.io +// SPDX-License-Identifier: Apache-2.0 + +#include + +#include +#include +#include +#include +#include + +#include "Config.h" +#include "KeplerFormalDriver.h" + +namespace { + +class OpaquePolicyCliTests : public ::testing::Test { + protected: + void SetUp() override { + oldDirectory_ = std::filesystem::current_path(); + oldPolicy_ = KEPLER_FORMAL::Config::getErrorOnOpaque(); + directory_ = std::filesystem::temp_directory_path() / + ("kepler_opaque_policy_" + std::to_string( + std::chrono::steady_clock::now().time_since_epoch().count())); + std::filesystem::create_directories(directory_); + std::filesystem::current_path(directory_); + write("supported.v", "module top(input a, output y); assign y = a; endmodule\n"); + write("opaque.v", "module top(input a, output y); wire unused; " + "UNMODELED hidden(.D(a), .Q(unused)); assign y = a; endmodule\n"); + write("cells.lib", "library(test) { cell(UNMODELED) { " + "pin(D) { direction : input; } pin(Q) { direction : output; } } }\n"); + } + void TearDown() override { + KEPLER_FORMAL::Config::setErrorOnOpaque(oldPolicy_); + std::filesystem::current_path(oldDirectory_); + std::filesystem::remove_all(directory_); + } + void write(const std::string& name, const std::string& contents) { + std::ofstream(directory_ / name) << contents; + } + KEPLER_FORMAL::RunResult run(std::vector arguments) { + arguments.insert(arguments.begin(), "kepler-formal"); + std::vector argv; + for (auto& argument : arguments) argv.push_back(argument.data()); + KEPLER_FORMAL::RunResult result; + const int code = KEPLER_FORMAL::runKeplerFormal( + static_cast(argv.size()), argv.data(), result); + EXPECT_EQ(code, result.exitCode); + return result; + } + KEPLER_FORMAL::RunResult config(const std::string& extra, + const std::string& mode = "sec") { + write("run.yaml", "format: verilog\nverification: " + mode + + "\ninput_paths: [supported.v, opaque.v]\n" + "liberty_files: [cells.lib]\n" + extra); + return run({"--config", "run.yaml"}); + } + std::filesystem::path oldDirectory_; + std::filesystem::path directory_; + bool oldPolicy_ = false; +}; + +TEST_F(OpaquePolicyCliTests, DefaultStillProvesSupportedOutputWithUnusedOpaqueCell) { + const auto result = config(""); + EXPECT_EQ(result.exitCode, 0); + EXPECT_EQ(result.status, KEPLER_FORMAL::RunStatus::Equivalent); + EXPECT_EQ(result.coveredOutputs, 1u); +} + +TEST_F(OpaquePolicyCliTests, YamlEnabledRejectsSecondDesignAndDisabledPreservesBehavior) { + const auto result = config("error_on_opaque: true\n"); + EXPECT_NE(result.exitCode, 0); + EXPECT_EQ(result.status, KEPLER_FORMAL::RunStatus::Unsupported); + EXPECT_NE(result.reason.find("design 2"), std::string::npos); + EXPECT_NE(result.reason.find("hidden"), std::string::npos); + EXPECT_EQ(config("error_on_opaque: false\n").status, + KEPLER_FORMAL::RunStatus::Equivalent); +} + +TEST_F(OpaquePolicyCliTests, CompactModeEnforcesPolicyInEitherDesign) { + for (const bool first : {true, false}) { + const auto result = run({"-verilog", "-v", "sec", "--compact", + "--error-on-opaque", first ? "opaque.v" : "supported.v", + first ? "supported.v" : "opaque.v", "cells.lib"}); + EXPECT_NE(result.exitCode, 0); + EXPECT_EQ(result.status, KEPLER_FORMAL::RunStatus::Unsupported); + EXPECT_NE(result.reason.find(first ? "design 1" : "design 2"), std::string::npos); + } +} + +TEST_F(OpaquePolicyCliTests, FlagBeforeFormatAndExplicitInputListsAreAccepted) { + const auto result = run({"--error-on-opaque", "-verilog", "-v", "sec", + "--design1", "opaque.v", "--design2", "supported.v", "--liberty", "cells.lib"}); + EXPECT_EQ(result.status, KEPLER_FORMAL::RunStatus::Unsupported); + EXPECT_NE(result.reason.find("error-on-opaque"), std::string::npos); +} + +TEST_F(OpaquePolicyCliTests, ScopedRunsResetAndRestorePolicy) { + KEPLER_FORMAL::Config::setErrorOnOpaque(true); + EXPECT_EQ(config("").status, KEPLER_FORMAL::RunStatus::Equivalent); + EXPECT_TRUE(KEPLER_FORMAL::Config::getErrorOnOpaque()); + KEPLER_FORMAL::Config::setErrorOnOpaque(false); + EXPECT_EQ(config("error_on_opaque: true\n").status, + KEPLER_FORMAL::RunStatus::Unsupported); + EXPECT_FALSE(KEPLER_FORMAL::Config::getErrorOnOpaque()); +} + +TEST_F(OpaquePolicyCliTests, RejectsInvalidConfigurationAndNonSecEnablement) { + for (const auto* value : {"null", "[true]", "{enabled: true}", "perhaps"}) { + EXPECT_NE(config(std::string("error_on_opaque: ") + value + "\n").exitCode, 0); + } + EXPECT_NE(config("error_on_opaque: true\n", "lec").exitCode, 0); + EXPECT_NE(run({"-verilog", "supported.v", "supported.v", + "--error-on-opaque"}).exitCode, 0); +} + +} // namespace