From 8d1f94a9ae21133e79c8db9583482faf3350b958 Mon Sep 17 00:00:00 2001 From: Noam Cohen Date: Fri, 25 Sep 2026 14:59:06 +0200 Subject: [PATCH 01/10] docs: describe SEC latch support approach --- docs/sec-latch-support.md | 953 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 953 insertions(+) create mode 100644 docs/sec-latch-support.md diff --git a/docs/sec-latch-support.md b/docs/sec-latch-support.md new file mode 100644 index 00000000..40391e47 --- /dev/null +++ b/docs/sec-latch-support.md @@ -0,0 +1,953 @@ +# Proposed SEC Latch Support + +Status: design proposal, not implemented behavior. This document records the +literature-backed approach discussed for level-sensitive latch support. It does +not enable latch extraction or change SEC results. The constructions and proof +sketches below close the identified specification gaps for a deliberately +restricted digital contract. They are not machine-checked proofs of an +implementation, or a claim to support every physical latch network. + +The central distinction is between **modeling behavior** and **optimizing that +model**. Latch storage, transparency, event ordering, and internal propagation +must be defined first. Loop unfolding, local scheduling, parallel evaluation, +and phase abstraction are subsequent transformations with separate correctness +conditions. No single referenced paper proves this complete Kepler architecture. + +## 1. Summary + +The proposed pipeline is: + +1. Extract explicit latch and flip-flop models, including asynchronous controls. +2. Build the finite, snapshot-based evaluate/update reference system in Section 5, + preserving history and events between flip-flop clock edges. +3. Analyze data and control dependencies. Identify feedback components and + candidate scheduling regions; these are different partitions. +4. Prove that each accepted settling episode terminates within a sufficient bound. +5. Compile supported internal propagation into bounded unfolded logic, preserving + all relevant intermediate effects. +6. Schedule regions using complete, wave-tagged inputs as specified in Section 8; + preserve intermediate events rather than publishing only settled endpoints. +7. Optionally apply periodic phase discovery and phase abstraction to the valid + model, with their own observation-preservation conditions. +8. Compare the two designs at corresponding external observation boundaries, + checking progress as well as output agreement. + +The first compiled path additionally requires a unique complete boundary state +for each admitted initial state and transaction. The conservative fallback is +the existing opaque behavior, with explicit skipped-output coverage, or the +opt-in error policy in Section 11. A backend retaining internal steps would be +a separate extension, not a capability assumed here. Never choose an arbitrary +fixed point, assume convergence, or discard a non-settling execution. + +```mermaid +flowchart TD + P[Explicit primitive models and initialization] --> R[Finite event reference model] + R --> C[Check admission, progress, invariant closure and boundary uniqueness] + C -->|All obligations established| U[Compile a complete episode with K waves] + U --> L[Schedule locally with complete wave-tagged inputs] + L --> S[SEC at corresponding external boundaries] + L -. Optional .-> A[Phase abstraction] + A --> S + C -->|Unsupported or unproved| O[Opaque with coverage report] + O -->|Error-on-opaque enabled| E[Stop with error] +``` + +## 2. Relationship to Current Kepler Behavior + +Current documented behavior is described in +[SEC Sequential Models](sec-sequential-models.md) and +[SEC Clock Handling](sec-clock-handling.md). Generic latch outputs are currently +opaque. Existing clock handling also has explicit limits on cross-domain cones. + +This proposal would extend those semantics; it is not merely an extraction +optimization. In particular, modeling independent latch enables requires more +than attaching another state-update mux to the existing abstract clock tick. +The current clock-domain restrictions must not be removed until the replacement +semantics and coverage rules have been implemented and verified. + +Naja's explicit sequential models should provide the element kind, data/state +expressions, enable or clock expression, clear/preset behavior, and physical +output mapping. Do not infer latch semantics from cell or pin names. + +This is a frontend prerequisite, not an assumption that the current Liberty +reader already supplies every such model. Its latch/state-table support must +be addressed separately. Existing clock-tree name exclusions do not establish +latch semantics. Reusing a clock-carrier classifier also does not prove gate +stability, transparency through chains, or absence of active feedback. + +## 3. Initial Semantic Contract + +The first target is a **zero-delay digital model with explicit internal +propagation steps**, not a physical timing model. + +- An external transaction supplies the next permitted input valuation or event. + Clocks, latch enables, data, and asynchronous controls can change independently + when allowed by the environment contract. +- New external stimulus is admitted only at a quiescent boundary. During the + resulting settling episode, external values are held fixed while internal + events continue to propagate. +- Quiescence means no remaining modeled work can change the state: pending + updates and event/edge history must be accounted for, not just latch outputs. +- A transaction supplies Boolean external values and any declared ordering + constraints. At a primitive activation, preserve every permitted ordering of + changed input pins, as specified in Section 5. Do not silently select one + order or impose a one-input-change restriction. +- The initial supported primitive set is Boolean, single-driver combinational + logic plus explicitly modeled latches and flip-flops. Cell-specific + clear/preset priority and invalid combinations must be defined. Unmodeled + X/Z, multiple drivers, and missing primitive rules are unsupported, not + don't-cares. Section 5 defines a distinct Boolean initialization contract. +- Observations initially occur at completed external transactions. Any internal + event that affects stored state remains relevant even if outputs are observed + only after settling. + +There is no inferred physical sampling interval. A formal internal step advances +the event semantics, not necessarily a circuit clock. An independent enable +pulse between flip-flop edges can be represented by external transactions for +its opening and closing. This does not establish fidelity for arbitrary physical +pulses, gate delays, setup/hold violations, or metastability. + +Allowing the external environment to interrupt an unfinished settling episode +would be a separate extension. The initial quiescent-environment contract must +be documented rather than mistaken for unrestricted asynchronous hardware. + +## 4. Basic Latch Building Block + +For a simple active-high latch, distinguish the remembered value H from the +visible output Q. The presentation [R1] gives the schematic construction: + + Q = enable ? data : H + next(H) = Q + +The register in this construction is mathematical storage. It is not a claim +that the physical latch samples on the main flip-flop clock. + +When the latch is closed, its output is its remembered value. When open, changes +at its data input can change its output and activate downstream logic without +waiting for a flip-flop edge. Reset and preset semantics must be incorporated +from the actual primitive model. + +These equations alone are not a complete network semantics. In a feedback +network, treating them as simultaneous unconstrained equations can lose history +or eliminate behavior. An open self-feedback latch can reduce to Q = Q, which +allows either value unless its previous state is preserved. Inverting feedback +can produce Q = not Q, which has no Boolean solution; excluding that case would +hide a problematic execution. + +The reference model must therefore start propagation from the actual preceding +state and apply defined event rules. Closing a latch and changing its data in +the same transaction must use those rules, not an assumed universal old-data +or new-data convention. + +Four open latches in a chain are not four clock cycles of delay. Their changes +propagate through internal evaluations within the same settling episode. The +eventually settled result is not a claim of zero physical propagation time. + +## 5. Reference Internal-Step Transition System + +The direct formal foundation is the VERICELL construction [R2], which models +primitive evaluation and updates and encodes them as a Boolean transition +system. Its published implementation makes restrictions that must not be +inherited implicitly; see the reference notes below. + +### 5.1 Complete finite state and primitive contract + +Select the following zero-delay barrier semantics as the reference. This is an +explicit digital contract derived from the operational structure in [R2], not +a claim of complete IEEE Verilog or physical-timing equivalence. + +The state contains primitive storage, current and previous net values, active +evaluations, staged updates, initialization status, sticky errors, and any finite +environment monitor needed for later transactions. Each net has one writer; +multi-output primitives own an output tuple committed together. A wave has at +most one pending output tuple per primitive, so no unbounded event queue or +absolute time counter is required. A reference step is one complete wave, +not a worker dispatch or an arbitrary scheduler delay. + +Every supported primitive supplies a complete finite reaction table: data and +control polarity, storage, clock edge, asynchronous clear/preset behavior, +conflicting-control behavior, and physical output mapping. An undefined or +unsupported case produces a sticky error, never an absent successor. Different +cell types need not share a universal reset priority. + +At a quiescent boundary, admission copies the old current values into history, +atomically replaces external input values with the transaction's values, and +activates exactly their changed-net consumers. Internal values and storage are +initially unchanged; staged updates are empty. Update the finite environment +monitor once for the transaction, not once per wave. Hold the supplied external +values throughout the episode. Changes intended to occur at distinct settled +moments are separate transactions. In the absence of an explicit constraint, +simultaneous changes leave every local pin order permitted. Invalid admission +must produce an error rather than remove that transaction from the relation. + +### 5.2 One ordinary evaluate/update wave + +1. Freeze the complete current/previous snapshot for this wave. Every activated + primitive reads that same snapshot; no worker reads another worker's staged + result. +2. A combinational primitive stages its Boolean function of the current inputs. + For a sequential primitive, begin with its remembered state and previous pin + values. Process the changed pin positions in every permitted order. For each + visited pin, apply its change once, then apply the primitive's reaction table + to the resulting local pin vector and remembered state. +3. For a simple latch reaction, apply the specified asynchronous controls first; + otherwise copy the local data value while enabled and retain storage while + disabled. A flip-flop captures only on a visit to its clock pin that carries + the specified edge, subject to its controls. Do not re-use that edge while + subsequently visiting a data or enable pin. +4. Stage only the final primitive state/output tuple from that evaluation. + Intermediate values inside its pin-processing sequence are private under + this contract. This does not permit hiding changes between network waves. +5. Commit all staged updates together, set previous net values to the pre-commit + current values, and clear the consumed activations and committed update + buffer. Replace the active set with exactly the consumers of changed nets, + including data, enable, clock, reset, and preset consumers. Inactive + primitives retain state. +6. Continue if work remains. Otherwise normalize previous values to current + values and mark a completed boundary, provided no error occurred. An idle + region alone does not establish whole-episode quiescence. + +The primitive-local recurrence is: + +```text +b := previous input-pin vector +z := current primitive storage +choose a permitted ordering pi of changed pin positions +for each j in pi: + before := b + b[j] := current value of input pin j + z := reaction(z, before, b, j) +stage z and its physical output tuple +``` + +The reference relation retains all permitted outcomes. Only after the +boundary-uniqueness check in Section 9 may a legal deterministic implementation +replace those alternatives. No pin-stability restriction may be inferred merely +from a primary input being held: internally generated changes remain possible. +Any such restriction must hold at the actual reference activations. + +This resolves simultaneous-event ambiguity explicitly. An open latch whose data +changes as it closes can retain old data or capture new data depending on the +permitted pin order. A clock/data race can similarly change a flip-flop capture. +The accepted deterministic path rejects unresolved outcome dependence; it does +not silently choose whichever outcome makes two designs agree. + +```mermaid +flowchart LR + Q[Complete quiescent state] --> J[Admit external transaction] + J --> V[Evaluate active primitives from one snapshot] + V --> C[Commit staged updates together] + C -->|Changed nets activate consumers| V + C -->|No work and no error| B[Next observation boundary] + V -->|Undefined reaction| E[Sticky nonstable error] +``` + +Do not freeze an internally generated control during settling. Conversely, +adding/removing gates or replacing a primitive with a decomposition can move +events between waves and change capture. Such rewrites need preservation +arguments; Boolean function equality alone is insufficient for event-sensitive +consumers. The primitive granularity is part of this reference contract. + +### 5.3 Initialization is an actual settling episode + +For the first proposed Boolean path, supply and hold an initial external input +valuation u0, and choose initial storage bits once under an explicit +initialization relation. Initialize their physical outputs consistently. Choose +declared Boolean seeds for the remaining internal nets, or quantify over all +such seeds; do not select a convenient stable solution. Set previous values +equal to current values for all initialized nets before the forced first +evaluation, including the external clocks. + +Run a dedicated first evaluation: combinational functions evaluate; latches +apply their level-sensitive and asynchronous rules; flip-flops apply asynchronous +controls but otherwise retain their initial storage, without an invented clock +edge. Commit together, then use ordinary waves. A generated clock change caused +by that commit is an actual event under the chosen model and is processed. + +Certify bootstrap progress and the resulting boundary invariant, just as for +later episodes. If only storage bits and initial external inputs are intended +initial parameters, compare bootstrap runs with those parameters shared but +auxiliary net seeds and permitted event choices independent. Require a unique +complete boundary result, or report unsupported. Otherwise arbitrary gate seeds +could silently decide which state is retained. + +This is a Kepler-specific Boolean bootstrap, not the all-X initialization in +[R2]. A symbolic but fixed initial Boolean bit is not HDL X and is never +resampled during propagation. A future multivalued path needs its own explicit +rules, including genuine startup transitions that its edge semantics recognize. +It must not silently inherit the Boolean no-startup-edge convention. Existing +X-handling policy outside this proposed path is not changed by this document. + +## 6. Feedback Components and Scheduling Islands + +### Feedback components + +Construct a dependency graph that includes both data and control paths. Strongly +connected components identify candidate feedback regions. A loop may contain +one latch, many latches, and intervening combinational logic. + +A closed latch can break a transparent dependency for a particular condition. +To classify an entire feedback region as inactive, prove that every directed +cycle is broken under every admitted condition. Structural cyclicity alone +does not establish oscillation. + +If controls change during the episode, acyclicity of an individual snapshot is +not by itself a termination proof. The complete event computation must still +satisfy the progress obligation; do not silently freeze changing controls when +using a structural shortcut. + +A flip-flop's data-to-state update is normally a sequential boundary, but its +clock and asynchronous controls cannot be treated as unconditional cuts: +internally generated events on those pins can change its state during settling. + +### Scheduling islands + +A scheduling island may contain several feedback components and the acyclic +logic or latch chains connecting them. It need not equal a component being +unfolded. DeVane [R5] supplies a concrete precedent for trigger-based regions, +not a rule to merge everything that is connected. + +Candidate grouping must account for enable, clock, reset, and data dependencies, +shared downstream storage, and pending-event effects. Two events that change +the same latch's data and enable are not independent simply because their +source logic belongs to separate graph components. Conversely, sharing a held, +read-only external input does not necessarily make two regions dependent. + +### Why final outputs alone are insufficient + +Suppose region A produces a temporary enable pulse for a latch in region B. +The enable starts and ends low, but the intervening high value lets B capture +data. Sending only A's final low value to B would lose that capture. + +Either preserve the relevant boundary event sequence, enlarge the modeled +region while retaining those internal events, or prove that the intermediate +events cannot affect any required behavior. Merely merging regions does not +justify deleting their internal propagation history. + +## 7. Certifying a Settling Bound + +Unfolding replaces repeated applications of internal transition logic with a +chain of copies. It is built once during model construction and reused for each +external transaction. It is exact only after a sufficient bound is justified. + +The finite-state eventuality result in [R3] provides the foundation: universal +eventual arrival at a target condition has a finite uniform bound, and checking +a candidate bound is a safety obligation. Finite state by itself does not imply +convergence; a reachable cycle can avoid stability forever. + +For one settling episode, define: + + q complete quiescent boundary state + B(q) admitted boundary invariant + Allowed(e, u) shared environment's allowed transaction, with monitor e + J(q, u, x0) admission of the transaction, retaining every allowed outcome + x complete internal state, including history and events + u held external stimulus + Entry(x, u) all admissible episode-entry configurations + T_hold(x, x', u) one permitted internal transition with stimulus held + Stable(x) full quiescence + +Entry describes the complete state immediately after admission of the new +external transaction, including its pending events or changed-value history. +The state and held stimulus must be consistent. Entry is not the quiescent +state before that transaction is applied. + +The finite environment monitor is part of the complete state; the same +environment contract supplies both designs. Do not intersect two different +implementation-specific admissibility conditions to hide a disagreement. + +Establish these obligations before using a bound: + +1. Bootstrap covers the prescribed initial conditions and establishes B. It must + not replace them with a convenient subset or an empty initial relation. +2. For every B(q) and Allowed(e, u), J has a successor. Unsupported admission + produces an explicit error, not an absent transition. Establish this totality + structurally or with a quantified/exhaustive check; it is not implied by the + bounded SAT query below. +3. Every such admission satisfies Entry(x0, u), including all admitted choices. +4. T_hold is total. Stable states have identity successors, preserving the + entire state, not merely the truth of Stable. +5. Every completed episode from B returns to B. Together with bootstrap, this + supplies induction over arbitrarily many external transactions. + +Make Stable absorbing within this episode. Totalize non-stable deadlocks and +modeled failures as absorbing error states for which Stable is false, or prove +separately that such states are unreachable. Short failing paths must survive +to depth K rather than disappear from the formula. For a candidate K, prove +the following formula unsatisfiable: + + Entry(x0, u) + AND T_hold(x0, x1, u) AND ... AND T_hold(x[K-1], x[K], u) + AND NOT Stable(x[K]) + +This is our proposed specialization of the published finite-state result, not +a latch-specific algorithm quoted from [R3]. It covers all represented entry +states and scheduling choices, not just states visited in a reset simulation. +An entry-state invariant or over-approximation must cover every reachable +episode entry; restrictions on impossible states require justification. + +After proving the obligation, K copies of the same transition logic implement +the complete settling episode. Early completion is padded with identity steps. +All intermediate capture/reset effects are still computed inside those copies. + +### Exact bounded-compilation theorem + +Initially retain the complete quiescent reference state as the macrostate. +Removing a field requires a reconstruction or future-behavior preservation +proof; being irrelevant to the current outputs is not sufficient. Define: + +```text +M(q, u, q_next) iff there exist x0, ..., xK such that + J(q, u, x0) + AND T_hold(x0, x1, u) AND ... AND T_hold(x[K-1], xK, u) + AND q_next = xK +``` + +**Claim, under the obligations above:** M equals the reference relation from one +completed external boundary to the next, for states in B and allowed inputs. + +**Proof sketch.** Every permitted reference episode reaches its first stable +state by K; identity padding extends it to length K without changing its result, +giving a witness for M. Conversely, every M witness reaches stability by K; +trimming its identity suffix yields a permitted reference episode with exactly +the same complete final state. Boundary-invariant closure permits concatenating +this argument, proving equality of boundary traces for any transaction sequence. +This is our relational-compilation argument using [R3, R4, R9], not a theorem +quoted verbatim from a latch-specific paper. Turning M into one next-state +function requires the separate uniqueness check in Section 9. + +### A finite decision procedure, not a guessed depth + +With frozen stimulus, explore the complete internal states reachable from the +episode entries. A reachable cycle entirely outside Stable gives a non-settling +execution. Otherwise the nonstable graph is acyclic, and its longest path to +stability supplies a sufficient K. The number of nonstable states is a coarse +upper bound, at most 2^b for a b-bit complete-state encoding. This is a finite +theoretical procedure, not a claim that exhaustive exploration is practical. + +An implementation can search candidate bounds with the safety query, use +symbolic cycle checks, or use ranking proofs. Resource exhaustion means an +unproved/unsupported case. A failure found only from an over-approximate B or +Entry must be concretized before being reported as a reachable design defect; +a successful universal certificate over that over-approximation remains sound. + +Alternatives include a decreasing ranking function or a liveness-to-safety +check for a non-quiescent repeating execution [R4]. Event history and pending +work matter when identifying repeated states. A proof of eventual convergence +does not by itself provide a convenient small numerical bound. + +Important limits: + +- A counterexample at depth K can mean only that more steps are needed. +- A timeout is an unproven bound, not a proof of convergence or oscillation. +- Fair eventual scheduling alone permits arbitrary postponement and does not + establish a uniform bound on scheduler steps. +- Counting latches, taking graph depth through a cycle, or using an ordinary + shortest-path reachability diameter is not a general bound on settling. +- A bound can be too large for useful unfolding. + +Initially, certify complete event-connected episodes. Local bounds require +contracts describing incoming events, not an assumption that neighboring +regions stay fixed. Do not combine component bounds by an unjustified maximum +or sum and assume that the complete design is covered. + +## 8. Safe Scheduling Reduction and Parallelism + +### 8.1 First construction: preserve reference-wave epochs + +Local scheduling need not mean changing the circuit's event order. Use the +following concrete construction, initially with the certified complete-episode +bound K from Section 7: + +1. Partition primitive evaluations into islands using the full data/control + dependency graph. SCCs may guide grouping but are not the correctness proof. +2. Associate every boundary value, relevant history, and activity indication + with its reference wave number, or epoch. At epoch k an island reads only the + complete epoch-k inputs and its epoch-k local state, producing epoch-(k+1) + updates. +3. Each predecessor supplies either its value/update or an explicit + unchanged/epoch-complete indication. Do not interpret silence as no change, + and never combine input values from different epochs. +4. Preserve every relevant boundary transition. A pulse is an opening and a + closing in their respective epochs, even when its final value equals its + initial value. Lossless compression of unchanged intervals is permitted. +5. Schedule a computation only after all its input epochs are complete. Include + shared error, quiescence, and environment monitors in this dependency rule; + their reductions may require a barrier. A locally idle island must still + receive later events and must not declare the episode globally stable. +6. Build or evaluate the finite epoch-indexed dependency graph through K. Every + task completes once; worker waiting is not an extra circuit step. Identity + padding is permitted only where it agrees with the reference, including its + global stable/error guards. + +For symbolic compilation these interfaces are epoch-indexed expressions and +activity guards, not necessarily runtime event queues inside the SEC machine. +Workers may evaluate independent tasks in parallel. Cross-island dependency +cycles are handled by successive epochs; they do not become same-epoch circular +equations. Computing each island's entire K-wave boundary trace in topological +order is another option only if the full inter-island dependency graph is +acyclic. Arbitrarily merging nonadjacent SCCs need not preserve that property. + +**Preservation claim.** This construction has exactly the reference's full state +at each epoch, or the same set of full-state traces when primitive choices remain +nondeterministic. + +**Proof sketch.** At entry, each island receives its exact projection of the +reference state. Assume equality at epoch k. The complete-input rule gives each +primitive the same activation, values, controls, and history as the reference; +it therefore has the same permitted staged updates. Unique output ownership and +the same monitor rules produce the same committed epoch-(k+1) state. Induction +proves equality through K. Nondeterministic choices retain their identities and +constraints across fanout; duplicating a producer must not create independent +copies of its choice. Matching choices gives both directions of trace inclusion. + +This is our direct scheduling proof for the selected reference. It closes the +local-scheduling gap without assuming an island can settle atomically or relying +on a general partial-order-reduction theorem whose hypotheses were not checked. + +**Why complete epochs matter.** Suppose two branches make a and b rise in the +same reference update, and their XOR enables a latch. The reference sees both +new values together, so the XOR stays low. A scheduler that reads one new value +and one old value invents an enable pulse and can change stored state. This +failure needs no feedback loop; finding SCCs alone does not prevent it. + +### 8.2 Optional stronger reductions + +Termination and order independence are separate properties. All schedules may +terminate yet produce different stored values. A single canonical schedule is +valid only if it is the declared reference semantics or is proven to represent +all permitted observable outcomes. Otherwise retain nondeterminism. + +Confluence and partial-order reduction results [R6, R7] provide sufficient +conditions for selected scheduling reductions. Applying them here requires a +mapping from latch events to their formal hypotheses; an SCC partition is not +that proof. + +For each proposed reordering, establish that it preserves activation conditions, +capture behavior, relevant observations, and progress. Read/write dependencies +must include control history, event enqueue/cancel effects, and shared monitors. +Only steps invisible under the declared observation contract may be hidden. +Matching final outputs alone is not enough to justify reordering or hiding. + +McDonald and Bryant [R8] demonstrate local event queues for symbolic timing +simulation. Their timing assumptions and event-cluster definition differ from +the proposed zero-delay latch islands. This is an optimization reference, not +the correctness basis for the complete SEC model. + +Practical parallelism can begin conservatively: + +- Parallel extraction and independent graph analyses with deterministic results. +- Parallel primitive evaluations within a reference evaluation wave, reading + the same immutable snapshot and staging updates for the same update barrier. +- Parallel proof jobs for independent regions with validated boundary contracts. + +Do not let worker completion order determine latch capture. The epoch-preserving +construction above may compute an island's complete trace locally, but must +still expose its relevant boundary trace. Publishing only a final settled value +needs an additional proof that discarded transitions cannot affect external +storage, control-event detection, errors, or required observations, and that +retained state preserves all future observations. Atomic summaries, shortcuts +across epochs, and alternative scheduling semantics remain separate extensions. + +## 9. Connection to SEC + +The internal-round abstraction and transparent-latch example in [R9] support +the idea of grouping propagation into observations. The stronger progress and +interface-preservation obligations below are part of our proposed adaptation. + +Supply both designs with the same allowed external transaction sequence. Each +design may require a different number of internal steps. Compare corresponding +completed observations, not identically numbered internal steps. + +In a reference paired model, a design that finishes early waits at the boundary +while the other finishes; no new external transaction is accepted prematurely. +With certified bounded compilation, each side can instead expose its complete +episode as a boundary-to-boundary transition. + +Two independent obligations are required: + +1. **Progress:** every accepted episode completes within its certified bound, + or an explicitly supported liveness analysis establishes completion. +2. **Agreement:** the required outputs agree at matched observation boundaries + under the specified initialization/reset relation. + +Only checking agreement when both designs finish can pass vacuously if a design +never finishes. A bound overflow must therefore be an error or unsupported +result, never an assumption excluding that execution. + +### 9.1 A sufficient determinism check + +For the first conventional SEC path, use a stronger, concrete condition than +same-episode output equality: prove uniqueness of the complete retained boundary +state. With the macro relation M from Section 7, require this query to be +unsatisfiable: + +```text +B(q) AND Allowed(e, u) + AND M(q, u, a) AND M(q, u, b) + AND a != b +``` + +The two copies share the **pre-admission** state q and external transaction u, +but independently choose admission outcomes and all permitted primitive orders. +Sharing a particular post-admission x0 could hide nondeterminism in J. Initial +storage bits already belong to q; they are not independently resampled for the +two executions. Bootstrap separately checks auxiliary-seed independence as +specified in Section 5.3. + +This test is conservative: two different internal states might still have the +same future observable behavior. Accepting them would require a proved +behavioral quotient or a separate all-future-observations check. Comparing only +the current output vector is insufficient; a later input may expose a hidden +latch-state difference. A failure on an over-approximate B is not automatically +a reachable design defect, but it prevents acceptance without refinement. + +As another sufficient route, primitive pin-order independence for every admitted +activation implies deterministic waves under the unique-writer barrier model. +The local commutation results in [R2] support such checks. They do not establish +network termination, bootstrap independence, or regional abstraction by +themselves. The complete-boundary test can also accept cases where local +ambiguity disappears before the full state settles. + +### 9.2 From a relation to ordinary sequential equivalence + +Admission totality, certified progress, and boundary uniqueness make M a total +function on B and the allowed transactions. A compiler can retain the relation +or implement it using a legal deterministic selection of its choices, after +showing that the selection realizes M. Uniqueness does not validate arbitrary +new event rules or an independently written scheduler. + +For two accepted designs, fix an explicit initial/reset relation R0 and a shared +environment transaction sequence. Each design uses its own compiled function +and its own certified K. An inductive paired-state invariant R must: + +1. Contain all initial pairs prescribed by R0, including the bootstrap results. +2. Be preserved by both compiled transitions under every shared allowed input. +3. Imply equality of the required observations at corresponding boundaries. + +Together with each design's independent progress certificate, these conditions +prove the stated SEC property by induction. They do not change the initial-state +quantification or establish equivalence under a different environment. Choosing +a favorable subset of initial pairs or implementation-specific input assumptions +would not prove the declared contract. + +If complete-boundary uniqueness is not established, the first path reports +unsupported/opaque under Section 11. It does not align two arbitrary scheduler +choices merely to obtain equal outputs. General nondeterministic trace-set +equivalence is a separate extension, not a prerequisite of this construction. + +Reset bootstrap and cycle counts must also distinguish external clock/reset +events from internal settling steps. An internal propagation round is not an +additional hardware reset cycle. + +The macrostep here is an admitted external transaction, not automatically the +existing SEC clock tick. Connecting this construction to existing clock-cycle +observations, reset counters, and exporters requires an explicit adapter. A +cycle-level reduction may hide transactions only after preserving their capture +effects and the required observation relation. Until that adapter is established, +the mathematical construction must not be advertised as existing-backend support. + +## 10. Optional Phase Optimization + +Phase discovery [R10] belongs after construction of a valid transition system. It +cannot repair missed enable pulses, unspecified event order, or unsafe loop +handling. + +The intended periodic-analysis layer starts from a normalized sequential +machine with an explicitly defined step, performs reset-based ternary analysis, +and looks for deterministic periodic state signals. It does not begin by +assigning a phase to each latch or grouping latches solely by syntactically equal +enables. A latch-specific adapter +can then interpret enable expressions relative to discovered carriers while +retaining residual local gating. + +Scheduler activity is not automatically a hardware phase: a period measured in +internal propagation steps must not be interpreted as a circuit clock period. +The relationship between the selected transition-system step and SEC observation +boundaries has to be preserved. + +Any subsequent phase abstraction needs its own observation-preservation +conditions. Failure to discover a periodic carrier is a valid outcome: retain +the unreduced model. Phase reduction is optional; generic latch semantics must +not depend on its success. + +## 11. Unsupported Cases and Diagnostics + +The proposed bounded-settling path cannot automatically accept: + +- **Non-settling feedback.** For example, a known Boolean value circulating + through an open latch and an inverter can alternate indefinitely under the + chosen propagation semantics. Unknown-value initialization must not be used + to disguise that case as a useful settled Boolean result. +- **Order-dependent boundary results.** This can occur with or without feedback. + The first path requires complete-boundary uniqueness; failure or inability to + prove it leaves the affected behavior unsupported, not arbitrarily resolved. +- **Unproven or impractical bounds.** A valid circuit may fall outside the + resource limits of this compiler. +- **Unmodeled timing or primitives.** Physical delay-sensitive behavior, + metastability, unresolved asynchronous-control rules, and unsupported + multi-driver/unknown-value semantics are outside the declared contract. + +A history-preserving self-feedback latch is not inherently unsupported. Several +stable values are also not inherently a problem when prior state and permitted +events determine which value is reached. + +Diagnostics should identify the affected region, relevant latch/control paths, +the failed obligation, and the impacted observed outputs. Distinguish a proven +non-settling trace from an unproven bound and from a resource limit. Preserve +explicit checked-output coverage rather than representing skipped behavior by +free shared symbols or claiming a complete SEC pass. + +### Default opaque behavior and optional error mode + +Keep the existing opaque-handling policy as the default. A latch must no longer +be classified as opaque merely because it is a latch: model its behavior when +the strategy's semantic and proof requirements are satisfied. Latch behavior +that cannot be modeled safely remains opaque, as do other unsupported cells +and signals. Ordinary propagation of opacity through dependent logic still +applies. + +By default, encountering opacity is not itself a fatal error. Preserve the +existing opaque diagnostics/reports and affected-output skipping behavior, +continue checking supported outputs, and report the resulting coverage. Skipped +outputs are not proved equivalent, and opaque signals must not be replaced by +unconstrained shared values to obtain a proof. + +Propagate opacity through data and event/control dependencies, not only data +cones. Certificates for remaining dependency-closed modeled cones make no claim +about progress or equivalence of the excluded behavior. Reporting an unsupported +region is not permission to drop one of its executions inside a purported proof +of that region. + +Add a separate opt-in error-on-opaque switch, **disabled by default**. When +enabled, encountering an opaque cell or signal during SEC model construction +for either design must stop the run with an error and a nonzero exit status, +rather than continuing with partial coverage. The diagnostic must identify the +design, hierarchical cell or signal, and reason for opacity. This policy applies +to opacity generally, not only to unsupported latches; it does not change which +behavior the modeling strategy supports. The switch's CLI spelling is not yet +specified, and this document does not implement it. + +## 12. Proposed Implementation and Validation Stages + +These are future tasks, not changes made by this document. + +1. Encode and review the explicit primitive tables, Boolean bootstrap, shared + environment, and observation contract specified here; supply missing frontend + models without inventing semantics from names. +2. Implement a small executable reference model and symbolic transition encoder + with the same evaluate/update and sticky-error rules. +3. Establish bootstrap progress, admission totality, and boundary invariants. + Support acyclic propagation and provably inactive loops before active-loop + certification; these still require correct data/control event handling. +4. Implement the bounded-progress and boundary-uniqueness checks, then exact + bounded compilation. Validate that compiled transitions realize the reference + macro relation, including all retained state and error effects. +5. Implement epoch-complete regional compilation/parallel scheduling and validate + the per-wave preservation argument. Stronger atomic summaries remain optional. +6. Integrate paired observations, the cycle/reset adapter, exporters, coverage, + and the default-off error-on-opaque policy with SEC. +7. Add optional phase abstraction or stronger scheduling reductions only with + their own observation-preservation conditions. + +Essential regressions include: + +- Open/closed latch behavior and a chain of four simultaneously open latches. +- Self-feedback retaining both possible previous Boolean values. +- Complementary-enable feedback with a provably closed path. +- A known-state inverting loop that cannot settle. +- A convergent example needing more than the first attempted bound. +- Simultaneous data/enable or data/clock changes under the explicit contract. +- A flip-flop clock edge consumed once, not reused on a later data-pin visit. +- Bootstrap with an initially open latch, independent auxiliary net seeds, and + generated clock events, without a fabricated initial flip-flop edge. +- Internally generated enable/reset/clock pulses that capture state, including + pulses crossing a proposed island boundary. +- Equal final boundary signals but different transiently captured state. +- Reconvergent same-wave changes without mixed-epoch enable pulses. +- Shared nondeterministic producer choices preserved across island fanout. +- Equal current outputs but hidden latch states distinguishable by a later input. +- Different internal settling depths on equivalent designs. +- One side failing to settle, preventing a vacuous equivalence result. +- Missing admission successors and short error paths that must not disappear + from the bounded query; independent admission choices in uniqueness checks. +- Explicit X/reset behavior, unproven-bound reporting, and partial coverage. +- Supported latches becoming modeled while unsupported latch behavior remains + opaque, with existing diagnostics and affected-output skipping by default. +- Error-on-opaque disabled by default, and explicit enablement producing an error + for an opaque cell or signal in either design, including non-latch opacity. +- Identical semantics and results under different worker counts. + +Differential simulation is useful for examples, but does not replace universal +bound, scheduling-preservation, and observation-correspondence obligations. + +## References and What They Establish + +### R1. Hjort: latch building block and modeling pitfalls + +Håkan Hjort, *On Applying Model Checking in Formal Verification*, FMCAD 2022 +tutorial. [Presentation](https://fmcad.org/FMCAD22/presentations/00%20-%20tutorials/02_hjort.pdf). +Printed slides 38-41 discuss the common discrete time base and sequential +elements; slide 41 gives the latch state/bypass construction; slides 44-50 +discuss feedback and delta propagation. This reference is the tutorial slide +deck, not the separate one-page published tutorial abstract. It motivates the +building block and its hazards, not a universal safe sampling interval or a +general settling bound. + +### R2. Raffelsieper, Roorda, Mousavi: explicit formal event semantics + +Matthias Raffelsieper, Jan-Willem Roorda, MohammadReza Mousavi, +*Model Checking Verilog Descriptions of Cell Libraries*, ACSD 2009, pp. 128-137. +[Publication record](https://research.tue.nl/en/publications/model-checking-verilog-descriptions-of-cell-libraries). +[DOI](https://doi.org/10.1109/ACSD.2009.18). +[Indexed author PDF](https://www.cs.le.ac.uk/people/mm789/pub/mousavi_acsd_2009.pdf). +The author PDF's indexed text was available during review, but direct download +returned an error; the publication record is included as a stable locator. + +An accessible later author treatment is Matthias Raffelsieper's 2011 TU Eindhoven +dissertation, *Cell Libraries and Verification*, Chapter 3, printed pp. 15-31, +explicitly based on the 2009 paper. +[University PDF](https://pure.tue.nl/ws/files/3499974/717717.pdf#page=24). +[Repository record](https://research.tue.nl/en/publications/cell-libraries-and-verification). +This is a later treatment, not the identical conference article. + +For the selected reference, the detailed anchors are Chapter 3, Section 3.1, +printed pp. 20-25 (pin-order evaluation and Table 3.2's operational rules), and +Section 3.2, printed p. 27 (the implementation's fixed-order restriction). +Chapter 4, Section 4.1 gives local order-independence/commutation checks. These +do not automatically prove settling or safe island abstraction. Chapter 5's +translation of timing restrictions through combinational input logic has +additional assumptions; such restrictions must be checked on the actual +activations of our wave model. The Boolean bootstrap and preservation proofs in +this proposal are explicitly our adaptations, not claims made by the thesis. + +Sections 2-3 of the conference paper define primitive/history semantics, +execute/update iteration, and a Boolean transition-system encoding. Section 4 +compares stable outputs and separately checks eventual stability. The published +encoding is zero-delay and +fixes a UDP input-processing order. Its example equivalence checks restrict +external changes and treat X outputs as don't-cares. Those choices are not +implicit Kepler defaults. Experiments concern cell libraries, not proof of +whole-design scalability. + +### R3. Claessen and Sörensson: proving finite bounds + +Koen Claessen and Niklas Sörensson, *A Liveness Checking Algorithm that Counts*, +FMCAD 2012, pp. 52-59. +[Proceedings, paper](https://www.cs.utexas.edu/~hunt/fmcad/FMCAD12/fmcad2012.pdf#page=59). +Section III-A, printed p. 53 (PDF p. 60), states the finite-state eventuality +bound; Section III-B develops the related k-liveness result. It supports +checking a proposed settling bound as a safety obligation. Applying it to a +complete, history-preserving latch episode is our specialization. It does not +provide a small bound from the number of latches. + +### R4. Schuppan and Biere: checking progress through safety + +Viktor Schuppan and Armin Biere, *Efficient Reduction of Finite State Model +Checking to Reachability Analysis*, STTT 5, 2004, pp. 185-204. +[Author preprint](https://www.schuppan.de/viktor/VSchuppanABiere-STTT-2004.pdf). +Sections 2 and 6 describe and justify state-recording reductions of liveness to +safety. This is the extended, corrected follow-up to *Liveness Checking as +Safety Checking* (Biere, Artho, Schuppan, 2002). It supplies a formal route for +detecting non-settling executions; it does not establish that a latch circuit +converges or that the proof is inexpensive. + +### R5. DeVane: trigger-based circuit regions + +Charles J. DeVane, *Efficient Circuit Partitioning to Extend Cycle Simulation +Beyond Synchronous Circuits*, ICCAD 1997, pp. 154-161. +[Paper](https://cecs.uci.edu/~papers/compendium94-03/papers/1997/iccad97/pdffiles/03a_1.pdf). +Sections 3.5-3.7 and 4 treat generated clocks, asynchronous controls, transparent +latches, and trigger-based partitioning. The principal algorithm assumes no +combinational feedback; separate handling is discussed. This is a scheduling +precedent, not a proof of bounded latch-loop unfolding or of our SEC adaptation. + +### R6. Lang and Mateescu: compositional scheduling reduction + +Frédéric Lang and Radu Mateescu, *Partial Order Reductions using Compositional +Confluence Detection*, FM 2009; full report INRIA RR-7078. +[Official publication page](https://cadp.inria.fr/publications/Lang-Mateescu-09.html). +The report gives conditions for lifting local confluence through composition +and prioritizing suitable acyclic invisible transitions while preserving +branching equivalence. These are conditional proof tools, not automatic +permission to serialize arbitrary latch regions. + +### R7. Neele, Valmari, Willemse: observation-preserving reduction + +Thomas Neele, Antti Valmari, Tim A. C. Willemse, *A Detailed Account of the +Inconsistent Labelling Problem of Stutter-Preserving Partial-Order Reduction*, +Logical Methods in Computer Science 17(3), 2021. +[Paper](https://lmcs.episciences.org/7709/pdf). +Section 5 gives corrected conditions and proofs for stutter-trace preservation. +Its counterexamples explain why endpoint agreement alone is insufficient when +intermediate state observations matter. Our observation mapping must satisfy +the selected reduction's conditions. + +### R8. McDonald and Bryant: local symbolic event queues + +Clayton B. McDonald and Randal E. Bryant, *Symbolic Timing Simulation Using +Cluster Scheduling*, DAC 2000, pp. 254-259. +[Author PDF](https://www.cs.cmu.edu/~bryant/pubdir/dac00a.pdf). +Section 3.2 describes local event queues and ordering safeguards. Event clusters +in this work are not synonymous with latch-loop components. Its symbolic +timing model is an optimization reference, not a direct correctness theorem for +our proposed zero-delay SEC frontend. + +### R9. Alur and Henzinger: internal rounds and round abstraction + +Rajeev Alur and Thomas A. Henzinger, *Reactive Modules*, Formal Methods in System +Design 15, 1999, pp. 7-48. +[Paper](https://www.cis.upenn.edu/~alur/FMSD99.pdf). +Section 6.2, printed pp. 37-38 (PDF pp. 31-32), Figure 13, explicitly treats +transparent-latch feedback and internal stabilization. Sections 6.1 and 6.3 +provide round abstraction and triggering. This supports combining internal +rounds under stated conditions; it does not prove universal bounded convergence +for arbitrary latch networks. In particular, its round-marker continuation +condition must not be substituted for our stronger all-executions progress +obligation. + +### R10. Bjesse and Kukula: later phase abstraction + +Per Bjesse and James Kukula, *Automatic Generalized Phase Abstraction for Formal +Verification*, ICCAD 2005. +[Author PDF](http://www.perbjesse.com/iccad05.pdf). +[DOI](https://doi.org/10.1109/ICCAD.2005.1560220). +Section II assumes clock/latch modeling and combinational-loop resolution have +already occurred. Its periodic analysis and abstraction operate on that +normalized machine. It supports the optional optimization layer, not the +construction of correct arbitrary-latch event semantics. + +## Resolved Algorithmic Choices and Remaining Work + +The previously open construction is now specialized as follows: + +- **Reference semantics:** finite, single-writer Boolean primitives; + snapshot-based waves; permitted pin-order alternatives retained; complete + cell-specific control rules; explicit Boolean bootstrap and sticky errors. +- **Compilation:** total admission and internal transitions, an inductive + boundary invariant, a certified all-executions bound, identity padding, and + retention of complete boundary state. Section 7 supplies the exactness proof. +- **Local scheduling:** complete epoch-tagged inputs, preserved boundary events + and choice correlations, with the per-wave induction in Section 8. Atomic + settle-and-publish islands are not assumed. +- **SEC:** complete-boundary uniqueness before deterministic compilation; + shared external transactions and explicit initial relation; independent + progress and paired-observation obligations, as specified in Section 9. +- **Fallback:** unsupported behavior stays opaque by default, with existing + reporting/skipping; error-on-opaque is a separate default-off switch. + +These close the logical construction under the stated hypotheses. They do not +constitute a tested implementation, machine-checked theorem, or a proof that +arbitrary asynchronous hardware satisfies the hypotheses. Before enabling the +feature, the implementation must instantiate and validate the primitive tables, +bootstrap and invariants, compiler and scheduler, proof certificates, frontend +models, resource limits, and the adapter to SEC's existing cycle/reset/export +conventions. The environment/initial-state contract must be explicit in the user +configuration; it must not be inferred to make a proof succeed. + +Extensions still requiring separate arguments include genuine multivalued +semantics, input changes before an episode settles, physical timing and +metastability, nondeterministic trace-set equivalence, atomic regional summaries, +and transformations that alter primitive granularity or internal event order. +The cited research supports the foundations; the short specialization proofs +here are our own and remain subject to implementation validation. From bda970385d6b4231d3f195a56f974c9b73c9cabe Mon Sep 17 00:00:00 2001 From: Noam Cohen Date: Fri, 25 Sep 2026 15:40:48 +0200 Subject: [PATCH 02/10] feat(sec): add opt-in bounded latch event support Gate the Boolean event model and supplemental Liberty latch loading behind default-off latch_support. Certify finite settling and complete-state uniqueness before compiling supported components into SEC. Preserve opaque fallback and add a separate default-off strict opacity policy. Keep algorithms and regressions in new files, with scoped integration and explicit event/init contracts. Add CLI, adapter, engine, export, scheduling, and certification coverage plus implementation documentation. --- docs/sec-latch-implementation.md | 334 +++++++++ docs/sec-latch-support.md | 9 +- src/bin/BUILD.bazel | 74 +- src/bin/CMakeLists.txt | 5 +- src/bin/KeplerFormal.cpp | 80 ++- src/bin/LatchEventConfig.cpp | 113 +++ src/bin/LatchEventConfig.h | 21 + src/bin/LibertyLatchModels.cpp | 273 ++++++++ src/bin/LibertyLatchModels.h | 21 + src/config/Config.h | 4 + src/python/KeplerBorrowedDesigns.cpp | 11 + src/python/KeplerBorrowedDesigns.h | 1 + src/python/PyKeplerFormal.cpp | 9 +- src/python/kepler_formal/api.py | 5 + src/sec/BUILD.bazel | 15 + src/sec/CMakeLists.txt | 7 + src/sec/export/SecBtor2Exporter.cpp | 1 + src/sec/export/SecBtor2Exporter.h | 1 + src/sec/latch/LatchBoundaryEncoding.cpp | 93 +++ src/sec/latch/LatchBoundaryEncoding.h | 29 + src/sec/latch/LatchEventContract.h | 18 + src/sec/latch/LatchEventModel.cpp | 396 +++++++++++ src/sec/latch/LatchEventModel.h | 121 ++++ src/sec/latch/LatchNetlistAdapter.cpp | 355 ++++++++++ src/sec/latch/LatchNetlistAdapter.h | 13 + src/sec/latch/LatchSettlingCompiler.cpp | 449 ++++++++++++ src/sec/latch/LatchSettlingCompiler.h | 130 ++++ src/sec/latch/LatchSupportOptions.cpp | 12 + src/sec/latch/LatchSupportOptions.h | 37 + src/sec/latch/NajaEventPrimitive.cpp | 237 +++++++ src/sec/latch/NajaEventPrimitive.h | 14 + src/sec/model/OpaquePolicy.cpp | 79 +++ src/sec/model/OpaquePolicy.h | 32 + src/sec/model/SequentialDesignModel.cpp | 9 + src/sec/model/SequentialDesignModel.h | 3 + .../SequentialEquivalenceStrategy.cpp | 7 +- test/python/CMakeLists.txt | 6 + test/python/borrowed_policy_tests.cpp | 103 +++ test/python/test_opaque_policy.py | 30 + test/sec/BUILD.bazel | 5 + test/sec/CMakeLists.txt | 5 + test/sec/LatchBoundaryEncodingTests.cpp | 224 ++++++ test/sec/LatchEventModelTests.cpp | 551 +++++++++++++++ test/sec/LatchNetlistAdapterTests.cpp | 659 ++++++++++++++++++ test/sec/LatchSettlingCompilerTests.cpp | 591 ++++++++++++++++ test/sec/OpaquePolicyTests.cpp | 238 +++++++ test/strategies/miter/BUILD.bazel | 20 + test/strategies/miter/CMakeLists.txt | 3 + .../miter/LatchEventConfigTests.cpp | 357 ++++++++++ .../miter/LibertyLatchModelsTests.cpp | 261 +++++++ .../strategies/miter/OpaquePolicyCliTests.cpp | 117 ++++ 51 files changed, 6153 insertions(+), 35 deletions(-) create mode 100644 docs/sec-latch-implementation.md create mode 100644 src/bin/LatchEventConfig.cpp create mode 100644 src/bin/LatchEventConfig.h create mode 100644 src/bin/LibertyLatchModels.cpp create mode 100644 src/bin/LibertyLatchModels.h create mode 100644 src/sec/latch/LatchBoundaryEncoding.cpp create mode 100644 src/sec/latch/LatchBoundaryEncoding.h create mode 100644 src/sec/latch/LatchEventContract.h create mode 100644 src/sec/latch/LatchEventModel.cpp create mode 100644 src/sec/latch/LatchEventModel.h create mode 100644 src/sec/latch/LatchNetlistAdapter.cpp create mode 100644 src/sec/latch/LatchNetlistAdapter.h create mode 100644 src/sec/latch/LatchSettlingCompiler.cpp create mode 100644 src/sec/latch/LatchSettlingCompiler.h create mode 100644 src/sec/latch/LatchSupportOptions.cpp create mode 100644 src/sec/latch/LatchSupportOptions.h create mode 100644 src/sec/latch/NajaEventPrimitive.cpp create mode 100644 src/sec/latch/NajaEventPrimitive.h create mode 100644 src/sec/model/OpaquePolicy.cpp create mode 100644 src/sec/model/OpaquePolicy.h create mode 100644 test/python/borrowed_policy_tests.cpp create mode 100644 test/python/test_opaque_policy.py create mode 100644 test/sec/LatchBoundaryEncodingTests.cpp create mode 100644 test/sec/LatchEventModelTests.cpp create mode 100644 test/sec/LatchNetlistAdapterTests.cpp create mode 100644 test/sec/LatchSettlingCompilerTests.cpp create mode 100644 test/sec/OpaquePolicyTests.cpp create mode 100644 test/strategies/miter/LatchEventConfigTests.cpp create mode 100644 test/strategies/miter/LibertyLatchModelsTests.cpp create mode 100644 test/strategies/miter/OpaquePolicyCliTests.cpp diff --git a/docs/sec-latch-implementation.md b/docs/sec-latch-implementation.md new file mode 100644 index 00000000..88301681 --- /dev/null +++ b/docs/sec-latch-implementation.md @@ -0,0 +1,334 @@ +# SEC Latch Event Implementation + +This document describes the first implementation of the +[latch-support design](sec-latch-support.md). It is an **opt-in, finite Boolean +event model with exhaustive certification and table-based compilation**. It is +not an implementation of every proposed optimization, a scalable symbolic +settling prover, or a timing-accurate model of arbitrary asynchronous circuits. + +The master switch is `latch_support: true` in YAML or `--latch_support` on the +command line. It is **off by default**. New latch extraction and supplemental +Liberty latch modeling are behind this switch; leaving it off preserves the +existing SEC path and its opaque-latch behavior. + +## 1. Enabling the model requires an explicit contract + +The master switch does not select an initialization or input-event assumption. +An enabled run must also provide all three fields below: + +```yaml +format: verilog +verification: sec +input_paths: [reference.v, implementation.v] +liberty_files: [cells.lib] + +latch_support: true +sec_latch_events: + input_changes: any + initial_inputs: 0 + initial_storage: 0 +``` + +`initial_inputs` sets **every external input** to the specified Boolean value +before initialization. `initial_storage` sets **every modeled primitive storage +bit** to its specified value. Each accepts only `0` or `1`; these are separate +choices, not per-port mappings. They describe a particular initial-state +contract, not a proof that arbitrary power-up state reaches reset. + +| Purpose | YAML | Command-line option | +| --- | --- | --- | +| Master enable, default off | `latch_support: true` | `--latch_support` | +| Allowed external transactions | `sec_latch_events.input_changes: any` or `single` | `--sec-latch-events any` or `single` | +| Initial value of all external inputs | `sec_latch_events.initial_inputs: 0` or `1` | `--sec-latch-initial-inputs 0` or `1` | +| Initial value of all primitive storage bits | `sec_latch_events.initial_storage: 0` or `1` | `--sec-latch-initial-storage 0` or `1` | +| Optional strict opacity policy, default off | `error_on_opaque: true` | `--error-on-opaque` | + +The `sec_latch_events` fields and `--sec-latch-*` tuning flags do **not** enable +latch support by themselves. Providing tuning while the master switch is off +is rejected, as is an enabled run with an incomplete contract. The strict +opaque policy is independent of the master latch switch: +it can also be used with ordinary SEC. + +### `any` and `single` are different verification assumptions + +- `any` permits every Boolean valuation of a component's external inputs at + each transaction, including several simultaneous changes and no change. +- `single` permits at most one original top-level input bit to change per + transaction, including no change. It is an explicit restriction on the + environment, not an inferred property of the circuit. + +With `any`, a data change concurrent with a latch closing can produce two +different retained values. Such a component remains opaque if the complete +boundary result is not unique. Selecting `single` is legitimate only when that +restricted environment is the intended proof contract; it is not a sound way +to waive races in a design that must tolerate simultaneous external changes. + +Even in `single` mode, one external change can generate several simultaneous +internal changes. All permitted changed-pin orders inside the component remain +part of certification. The setting does not impose a one-pin-change assumption +on latch data, enables, internal clocks, or asynchronous controls. + +## 2. What one SEC step means + +One step supplies a permitted external transaction, holds those external values +fixed, and completes all internal propagation before the next observation. +The environment cannot interrupt an unfinished settling episode. + +```mermaid +flowchart LR + B[Previous complete boundary] --> I[Admit external transaction] + I --> W[Evaluate and commit an internal wave] + W -->|Changed nets activate consumers| W + W -->|Quiescent and error-free| N[Next complete boundary] + W -->|Error, nonsettling, or certification limit| R[Reject component certificate] + N --> O[Observe outputs and permit next transaction] +``` + +Consequently, `max_k` and counterexample steps count **external transactions**, +not clock cycles or internal waves. An independent enable can open and close +between flip-flop edges through separate transactions. Data changes while a +latch is open also propagate without requiring a flip-flop edge. + +The current event path rejects clock-cycle reset bootstrap and selected leaf +boundaries. Asynchronous reset/set pins still participate in the event model +as ordinary external or internally generated controls; the existing +`sec_reset.cycles` mechanism is not reinterpreted as an event sequence. BTOR2 export +uses the compiled transaction transition system and records the event contract; +its steps must not be interpreted as hardware clock ticks. + +Both comparison designs use the same contract, even if one contains no latches. +Contract metadata prevents mixing ordinary clock-cycle models, event models, +or incompatible Boolean initialization contracts. Original top-input identities +remain part of interface alignment, so a selector cannot silently refer to +different pins on the two sides. + +This is zero-delay Boolean behavior under the declared primitive granularity. +It does not cover propagation delays, setup/hold violations, metastability, +HDL X/Z behavior, multiple drivers, or all IEEE Verilog scheduling semantics. +Replacing one modeled primitive with a gate decomposition may move events +between waves and therefore needs more than a Boolean-function equivalence +argument. + +## 3. Extraction and primitive modeling + +The ordinary Liberty reader is augmented, only in the enabled file-based path, +with explicit scalar `latch` groups. The supplemental reader preserves data, +enable, asynchronous clear/preset, conflict behavior, and physical output +expressions. An integrated clock gate is supported through its actual latch +and output expressions, such as a stored enable ANDed with a clock; neither +cell names nor `clock_gating_integrated_cell` metadata alone establish those +semantics. Unsupported library descriptions remain unmodeled. + +The Naja adapter copies explicit sequential expressions and supported Boolean +truth tables into pure primitive callbacks. Worker evaluations do not access +the original Naja objects. This also allows compact extraction to release the +source design after constructing its SEC model. + +The accepted subset is intentionally conservative: + +- Physical output mappings and clear/preset behavior must be defined. Undefined + simultaneous controls become errors if reached, not don't-cares. A reached + simultaneous clear/preset `Toggle` rule is also unsupported: its implicit + state-dependent asynchronous activity is not represented by pin events alone. +- Latch data and asynchronous control expressions may not depend directly on + internal state variables in a way requiring unmodeled internal feedback. + Such definitions are rejected by the adapter. Feedback through actual net + connections is handled by the event model and certification. +- Flip-flop next-state expressions may use stored state, because their update + is explicitly edge-triggered. Clock/enable expressions cannot depend on + internal state variables. +- Unmodeled primitives, unsupported generic arithmetic/table-select models, + unsupported state-table descriptions, invalid pin mappings, and missing or + multiple drivers do not silently acquire new semantics. + +Each component contains every primitive connected through internally driven +data **or control** nets, including enable, clock, and asynchronous controls. +This deliberately groups more than just feedback strongly connected components. +Shared read-only primary inputs do not by themselves join otherwise independent +components. A component can contain latch chains, feedback loops, combinational +logic, and flip-flops. + +This conservative closure prevents a component's temporary pulse from being +discarded at a neighboring storage element. It is not an implementation of +arbitrary independently settling local islands or final-output-only summaries. + +## 4. Initialization and internal waves + +Initialization is itself a checked settling episode: + +1. Install the explicit initial external values and primitive storage values. +2. Initialize physical storage outputs from their model; enumerate auxiliary + internal net seeds rather than choosing convenient values. +3. Set previous values equal to initial current values, so merely beginning + with a high external clock does not invent a rising edge. +4. Force a BOOT evaluation. Gates evaluate, latches apply level-sensitive and + asynchronous rules, and flip-flops apply asynchronous rules without an + invented edge. Generated clock changes from that update remain real modeled + events in subsequent waves. +5. Certify that all auxiliary seed choices and permitted event orders settle + to the same complete boundary for the prescribed intended initialization. + +When a physical output's initial projection reads input pins, certification +also enumerates seeds of other storage-output nets that the projection can +read before they are overwritten. Otherwise those hidden seed choices could +determine the retained result. + +The standalone compiler can enumerate unspecified initial storage and retain +every origin mapping. The first integrated CLI path instead requires explicit +fixed Boolean input/storage settings and one initialized boundary per component; +it does not select a favorable member of an unspecified initial relation. + +Within an ordinary wave, activated primitives read the same frozen snapshot. +A sequential primitive processes every permitted ordering of changed input +positions, applying each change once. A flip-flop edge is not reused when a +later data pin is visited. The final primitive storage/output tuple is staged; +all staged tuples commit at the wave boundary. Changed nets activate all their +consumers for the next wave. Primitive-local intermediate pin-processing values +are private under this contract, but transitions between network waves are not +discarded. + +Independent primitive evaluations within a wave run through TBB. They read +immutable inputs and stage separate results, so worker completion order cannot +choose a latch capture. A producer's chosen result is shared across its fanout, +not resampled independently for each consumer. The complete-wave update barrier +preserves the reference epochs. + +## 5. Exact finite certification and compilation + +For each component the compiler performs exhaustive finite-state analysis, +subject to explicit resource limits: + +1. Certify the initialization episodes and retain their complete stable states. +2. From each reachable complete boundary, enumerate every permitted external + transaction, including stutters, and admit it into the event model. +3. Explore every reachable internal state and successor. The retained state + includes current/previous net values, primitive storage, activations, BOOT, + and errors; equal visible outputs do not imply equal state. +4. Require totality. Missing successors and reachable errors fail certification. + Stable states must have identity successors, so early completion can be + padded without changing history or hiding a failure. +5. Detect reachable nonstable cycles. A cycle with an exit still permits an + infinite execution and therefore fails universal settling. +6. For an acyclic episode graph, calculate the longest path to stability. It is + an exact sufficient wave bound, not a guessed latch count or shortest path. +7. Require one **complete** stable boundary for the transaction. Different + retained storage or history is rejected even if present outputs agree. +8. Add the boundary-to-boundary row and continue until the reachable boundary + set is closed under every permitted transaction. + +No partial table is returned as certified. Resource exhaustion, an excessive +wave bound, a race, or a nonsettling execution cannot be turned into an +assumption that removes the troublesome input from the proof. + +The accepted table is encoded into Boolean next-state and observation formulas +for the existing SEC engines. Its boundary IDs are injective identifiers of +complete states, including remembered input values and history. Output formulas +describe the completed observation of the incoming transaction. Unused IDs have +a total encoding but are unreachable from the explicitly initialized, closed +table. + +In `single` mode, both designs share selector bits and one value bit. The selector +names an original top input; that input is assigned the value. Selecting an +unrelated component's input, selecting a reserved/out-of-range code, or assigning +the existing value produces the appropriate local stutter. The input interface +is aligned before verification. Counterexample inputs therefore include these +synthetic selector/value signals; they are not a complete ordinary input-vector +sample at each step. + +For those witnesses, `$event.select[i]` contributes bit `i` of the zero-based +selector, with bit zero least significant. Original top input names, including +their bit indices, are sorted lexicographically to define that selector order. +`$event.value` supplies the assigned Boolean value. The retained +`$event.interface.*` variables are alignment sentinels only; their witness values +do not drive the modeled input levels. + +This implementation does **not** yet generate a scalable symbolic `K`-copy +unfolding of arbitrary latch networks. It uses the finite decision procedure +described in the design document to certify and explicitly compile small +event-connected components. Both state exploration and table formulas can grow +exponentially. Larger symbolic certificates, finer island scheduling, stronger +reductions, and phase abstraction remain subsequent work. + +## 6. Resource controls + +The first three limits below and the worker count can be tuned through +`sec_latch_events` or the corresponding CLI flags: + +| Setting | Default | Scope | +| --- | ---: | --- | +| `max_waves` / `--sec-latch-max-waves` | 256 | Maximum accepted settling depth of an episode | +| `max_states` / `--sec-latch-max-states` | 4,096 | Reachable complete boundaries per component | +| `max_transactions` / `--sec-latch-max-transactions` | 65,536 | Compiled boundary/input rows per component | +| `workers` / `--sec-latch-workers` | 0 | Automatic TBB worker selection; `1` selects serial evaluation | + +Other conservative limits currently live in the standalone compiler/reference +API, not in additional YAML keys: + +| Resource | Default | +| --- | ---: | +| Complete reference-state Boolean bits | 512 | +| Reachable internal states per episode | 65,536 | +| Successor transitions explored per episode | 262,144 | +| Enumerated external input bits per component | 12 | +| Unspecified initial-storage bits | 12 | +| Auxiliary bootstrap seed bits | 12 | +| Initial storage/seed configurations | 4,096 | +| Changed-pin permutations per primitive activation | 100,000 | +| Combined successor alternatives per wave | 100,000 | + +`max_states` is not the internal episode-state limit. Raising one setting does +not disable the others or guarantee that a component becomes tractable. +These limits reject unproved cases; they do not truncate the relation while +claiming a successful certificate. + +## 7. Opacity, errors, and coverage + +With latch support disabled, the existing extraction path remains in use. +With it enabled, a certified component is modeled; an unsupported or uncertified +component remains opaque, with reasons and affected output skipping. Independent +supported outputs can still be checked. Skipped outputs are not proved, and a +partial result is not a claim that an excluded nonsettling component terminates. + +Diagnostics distinguish a proven nonsettling cycle, distinct complete boundary +results, an invalid reference/primitive case, exhausted resources, and a settling +depth beyond the accepted bound. A resource limit is not evidence of oscillation. + +`error_on_opaque: true` or `--error-on-opaque` changes the policy to a hard +unsupported result with a nonzero exit and an identified design/signal/reason. +It is default-off, applies to general opaque cells/signals rather than just +latches, and is checked in either design, including disconnected opaque cells. +It does not reclassify every unrelated connectivity skip as an opaque cell. + +Invalid global configuration, incompatible contracts, and unsupported whole-run +interfaces are rejected rather than represented as a successfully modeled event +run. The borrowed-design/Python API currently does **not** enable this event +path and explicitly prevents inheriting an ambient event-model scope. Its +independent `error_on_opaque` setting is supported for SEC. + +## 8. Implementation map and verification + +| File | Responsibility | +| --- | --- | +| `src/bin/LatchEventConfig.*` | Master enable, explicit contract, tuning, and CLI/YAML validation | +| `src/bin/LibertyLatchModels.*` | Opt-in supplemental scalar Liberty latch descriptions | +| `src/sec/latch/NajaEventPrimitive.*` | Copy supported Naja primitive expressions into immutable callbacks | +| `src/sec/latch/LatchEventModel.*` | Boolean BOOT/admission/wave semantics, complete state, parallel primitive evaluation | +| `src/sec/latch/LatchSettlingCompiler.*` | Exhaustive settling/uniqueness checks and reachable macro-transition table | +| `src/sec/latch/LatchBoundaryEncoding.*` | Injective boundary-state encoding and shared event-input decoding | +| `src/sec/latch/LatchNetlistAdapter.*` | Full data/control component closure, extraction, opacity, and SEC integration | +| `src/sec/latch/LatchSupportOptions.*`, `LatchEventContract.h` | Scoped options and protection against incompatible step/initialization contracts | +| `src/sec/model/OpaquePolicy.*` | Independent default-off error-on-opaque policy | + +New tests cover the reference waves, latch chains and feedback, initialization +seed dependence, all permitted pin orders, transient controls, serial/parallel +agreement, exhaustive small-graph certification, resource failures, table +encoding, original-input alignment, Naja/Liberty adapters, and configuration and +opacity policies. Test names are in the new `Latch*Tests.cpp` and +`OpaquePolicy*Tests.cpp` suites. This document intentionally does not claim a +fixed passing-test count or completion of the entire long-term architecture. + +For the sources, conditional proof arguments, and remaining theoretical +extensions, see [the design and literature references](sec-latch-support.md). +Those references motivate the construction; the exact Boolean initialization, +barrier semantics, compiler, and adapter are Kepler implementation choices whose +correctness must be checked against the stated contract. diff --git a/docs/sec-latch-support.md b/docs/sec-latch-support.md index 40391e47..2ccdeea4 100644 --- a/docs/sec-latch-support.md +++ b/docs/sec-latch-support.md @@ -1,9 +1,12 @@ # Proposed SEC Latch Support -Status: design proposal, not implemented behavior. This document records the +Status: architectural design and conditional correctness arguments. The initial +opt-in, resource-bounded Boolean event implementation is documented separately +in [SEC Latch Event Implementation](sec-latch-implementation.md); it does not +implement every optimization proposed here. This document records the literature-backed approach discussed for level-sensitive latch support. It does -not enable latch extraction or change SEC results. The constructions and proof -sketches below close the identified specification gaps for a deliberately +not itself enable latch extraction or change SEC results. The constructions and +proof sketches below close the identified specification gaps for a deliberately restricted digital contract. They are not machine-checked proofs of an implementation, or a claim to support every physical latch network. 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..1414d716 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,20 @@ 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("Boolean event SEC (off by default): --latch_support --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 ]. " + "Steps are settled external events, not clock/reset cycles."); // LCOV_EXCL_START } // LCOV_EXCL_STOP @@ -486,6 +493,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 +516,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 +1250,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 +1281,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 +1441,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 +1537,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 +1615,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 +1843,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 +2035,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 +2216,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 +2244,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 +2335,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 +2353,11 @@ static int KeplerFormalMainImpl( SPDLOG_INFO("SEC internal relations: learn={} allow_x_equality={}", internalRelationOptions.learnInternalRelations, internalRelationOptions.allowXEqualityInInternalRelations); + if (const auto& latch = latchEventConfig.options(); latch.enabled) { + 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 ? 1 : 0, *latch.initialStorage ? 1 : 0); + } if (secResetSpec.enabled()) { SPDLOG_INFO("SEC reset bootstrap: {} cycle(s)", secResetSpec.cycles); for (const auto& port : secResetSpec.ports) { @@ -2530,7 +2597,9 @@ static int KeplerFormalMainImpl( std::filesystem::path libraryPath(libraryFile); SPDLOG_INFO("Loading library file: {}", libraryFile); SNLLibertyConstructor constructor(primitivesLibrary); - constructor.construct(libraryPath); + if (latchEventConfig.options().enabled) + KEPLER_FORMAL::constructLibertyWithLatchModels(primitivesLibrary, libraryPath); + else constructor.construct(libraryPath); } for (const auto& pythonFile : pythonFiles) { // LCOV_EXCL_START @@ -3343,6 +3412,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 +3421,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 +3430,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 +3450,7 @@ int runKeplerFormal(int argc, char** argv, RunResult& result, } stateGuard{ previousSolver, previousReportSkippedPOs, + previousErrorOnOpaque, previousDefaultLogger, previousNamedLogger, previousMiterLogger, @@ -3385,6 +3458,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..7df48509 --- /dev/null +++ b/src/bin/LatchEventConfig.cpp @@ -0,0 +1,113 @@ +// 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) { + explicitTuning_ = true; + if (key == "input_changes") { + if (value == "any" || value == "single") { + inputChangesExplicit_ = true; + 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 { + 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; + explicitTuning_ = 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) { + if (std::string_view(argv[index]) == "--latch_support") { + options_.enabled = true; + 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"}}; + 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 { + if (!options_.enabled) { + if (!explicitTuning_) return true; + error = "latch event tuning requires latch_support: true or --latch_support"; + return false; + } + if (!isSec) error = "latch event options require SEC verification"; + else if (!inputChangesExplicit_ || !options_.initialInputs || !options_.initialStorage) + error = "latch events require explicit input_changes, initial_inputs and initial_storage"; + else if (hasResetCycles) + error = "latch event steps are external transactions, not reset cycles; drive resets as external events"; + 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..04ea02cc --- /dev/null +++ b/src/bin/LatchEventConfig.h @@ -0,0 +1,21 @@ +// 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 explicitTuning_ = false; + bool inputChangesExplicit_ = false; + 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/KeplerBorrowedDesigns.cpp b/src/python/KeplerBorrowedDesigns.cpp index 52aa2d70..5f2feec6 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_; @@ -250,8 +254,15 @@ int verifyBorrowedDesigns(naja::NL::SNLDesign* design0, Config::ScopedVerificationContext verificationContext; BorrowedExpressionState expressionState; BorrowedRunState runState; + // BorrowedDesignOptions has no event contract: never inherit ambient + // thread-local event semantics from a caller's direct extraction scope. + SEC::LATCH::ScopedSupportOptions eventOptions({}); 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..9d47ee47 100644 --- a/src/python/KeplerBorrowedDesigns.h +++ b/src/python/KeplerBorrowedDesigns.h @@ -23,6 +23,7 @@ struct BorrowedDesignOptions { SEC::InternalRelationOptions internalRelationOptions; bool allowBoundaryMismatch = false; bool reportSkippedOutputs = false; + bool errorOnOpaque = false; std::string logFile; std::string logLevel; BoundaryPairs setAsBoundary; diff --git a/src/python/PyKeplerFormal.cpp b/src/python/PyKeplerFormal.cpp index 18d6a9e2..33afad6e 100644 --- a/src/python/PyKeplerFormal.cpp +++ b/src/python/PyKeplerFormal.cpp @@ -351,7 +351,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; @@ -393,12 +393,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, diff --git a/src/python/kepler_formal/api.py b/src/python/kepler_formal/api.py index 5291d48b..ab880604 100644 --- a/src/python/kepler_formal/api.py +++ b/src/python/kepler_formal/api.py @@ -65,6 +65,7 @@ class VerificationOptions: ) = () learn_internal_relations: bool = True allow_x_equality_in_internal_relations: bool = True + error_on_opaque: bool = False NativeDesign = _native.NativeDesign @@ -167,6 +168,9 @@ 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) if settings.max_k is not None: if isinstance(settings.max_k, bool) or not isinstance(settings.max_k, int): @@ -215,6 +219,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..fdca9561 100644 --- a/src/sec/BUILD.bazel +++ b/src/sec/BUILD.bazel @@ -31,6 +31,13 @@ cc_library( "kinduction/KInductionEngine.cpp", "kinduction/OutputBatching.cpp", "kinduction/SatEncoding.cpp", + "latch/LatchBoundaryEncoding.cpp", + "latch/LatchEventModel.cpp", + "latch/LatchNetlistAdapter.cpp", + "latch/LatchSettlingCompiler.cpp", + "latch/LatchSupportOptions.cpp", + "latch/NajaEventPrimitive.cpp", + "model/OpaquePolicy.cpp", "model/SecNetlistChecks.cpp", "model/SequentialDesignModel.cpp", "pdr/PDREngine.cpp", @@ -57,6 +64,14 @@ cc_library( "kinduction/KInductionProblem.h", "kinduction/OutputBatching.h", "kinduction/SatEncoding.h", + "latch/LatchBoundaryEncoding.h", + "latch/LatchEventContract.h", + "latch/LatchEventModel.h", + "latch/LatchNetlistAdapter.h", + "latch/LatchSettlingCompiler.h", + "latch/LatchSupportOptions.h", + "latch/NajaEventPrimitive.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..e70e61c1 100644 --- a/src/sec/CMakeLists.txt +++ b/src/sec/CMakeLists.txt @@ -11,6 +11,13 @@ add_library(kepler_sec STATIC kinduction/KInductionEngine.cpp kinduction/OutputBatching.cpp model/SecNetlistChecks.cpp + model/OpaquePolicy.cpp + latch/LatchEventModel.cpp + latch/LatchSettlingCompiler.cpp + latch/LatchBoundaryEncoding.cpp + latch/LatchSupportOptions.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..98d0d1a4 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())); 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/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/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/LatchNetlistAdapter.cpp b/src/sec/latch/LatchNetlistAdapter.cpp new file mode 100644 index 00000000..90ce2c07 --- /dev/null +++ b/src/sec/latch/LatchNetlistAdapter.cpp @@ -0,0 +1,355 @@ +// Copyright 2026 keplertech.io +// SPDX-License-Identifier: Apache-2.0 +#include "latch/LatchNetlistAdapter.h" + +#include +#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/LatchSupportOptions.h" +#include "latch/NajaEventPrimitive.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; }; + +// 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); + } +}; + +struct DisjointSets { + std::vector parent; + explicit DisjointSets(size_t size) : parent(size) { std::iota(parent.begin(), parent.end(), 0); } + size_t root(size_t value) { + while (parent[value] != value) { parent[value] = parent[parent[value]]; value = parent[value]; } + return value; + } + void join(size_t a, size_t b) { a = root(a); b = root(b); if (a != b) parent[std::max(a,b)] = std::min(a,b); } +}; + +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=" + std::to_string(*options.initialInputs) + + ";initial_storage=" + std::to_string(*options.initialStorage); + DnlScope scope(top); + const auto* dnl = naja::DNL::get(); + 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) { + 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; + 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); + } + try { + auto primitive = makeNajaEventPrimitive(instance.getSNLInstance(), fallback.name, pins); + // 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(); + network.primitives.push_back(std::move(fallback)); + } + 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; })); + 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. + DisjointSets groups(network.primitives.size()); + 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) { + groups.join(i, owner[output]); + netErrors[output] = "multiple primitive writers"; + } else owner[output] = i; + } + } + for (size_t i = 0; i < network.primitives.size(); ++i) + for (auto input : network.primitives[i].inputs) + if (owner[input] != absent) groups.join(i, owner[input]); + std::map> components; + for (size_t i = 0; i < network.primitives.size(); ++i) components[groups.root(i)].push_back(i); + + size_t nextVar = 2; + 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); + inputExpressions.assign(inputs.size(), BoolExpr::createFalse()); + } + + for (const auto& [componentID, members] : components) { + 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]); + } + 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)); + } + std::optional table; + if (failure.empty()) { + try { + EventModel reference(local, {}, options.workers); + CompileOptions compile; + compile.initialInputs.assign(local.externalInputs.size(), *options.initialInputs); + compile.singleExternalInputChange = options.singleInputChange; + compile.limits = options.limits; + for (const auto& primitive : local.primitives) + compile.initialStorage.emplace_back(primitive.storageBits, uint8_t(*options.initialStorage)); + auto result = compileTransitionTable(reference, compile); + if (result.certified()) table = std::move(result.table); + else failure = std::string(certificationStatusName(result.status)) + ": " + result.detail; + } 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 && groups.root(owner[output.net]) == componentID) { + model.skippedObservedOutputs.push_back(output.key); + opaque(model, output.key, output.name, reason); + } + continue; + } + std::vector state; + std::vector stateKeys; + for (size_t bit = 0; bit < boundaryEncodingBits(table->boundaries.size()); ++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)); + model.initialStateValueByKey.emplace(stateKey, (table->initials[0].boundary >> bit) & 1); + } + const auto encoded = 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 (const auto& output : outputs) { + if (owner[output.net] == absent || groups.root(owner[output.net]) != componentID) continue; + model.observedOutputs.push_back(output.key); + model.observedOutputExprByKey.emplace(output.key, encoded.observedNets.at(localNet.at(output.net))); + } + } + 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); + 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 {}; + if (!options.initialInputs || !options.initialStorage || !pairs.empty()) { + SequentialDesignModel result; + result.unsupportedReasons.push_back(!pairs.empty() + ? "event semantics do not yet support selected leaf boundaries" + : "event semantics require explicit Boolean initial_inputs and initial_storage"); + 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..302d1955 --- /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 only when the explicit event contract is disabled. In event mode even +// a latch-free comparison side must use the same external-event semantics. +std::optional extractEventDesign( + naja::NL::SNLDesign* top, const BoundaryPairs& pairs, size_t side); +} // 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..7b6ef6a1 --- /dev/null +++ b/src/sec/latch/LatchSupportOptions.h @@ -0,0 +1,37 @@ +// 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 = false; + bool singleInputChange = false; + std::optional initialInputs; + std::optional initialStorage; + size_t workers = 0; + CompilerLimits limits; +}; + +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/NajaEventPrimitive.cpp b/src/sec/latch/NajaEventPrimitive.cpp new file mode 100644 index 00000000..1d628110 --- /dev/null +++ b/src/sec/latch/NajaEventPrimitive.cpp @@ -0,0 +1,237 @@ +// 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" + +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; + 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) { + 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); + }; + } 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)); + } + primitive.react = [tables = std::move(tables)](const Bits&, const Bits&, const Bits& input, + std::optional, bool) { + Reaction result; + for (const auto& table : tables) result.outputs.push_back(table(input)); + return result; + }; + } + 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..301c11ec --- /dev/null +++ b/src/sec/latch/NajaEventPrimitive.h @@ -0,0 +1,14 @@ +// 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 { +// No cell-name heuristics: only explicit sequential expressions/truth tables. +Primitive makeNajaEventPrimitive( + naja::NL::SNLInstance* instance, std::string path, + const std::map& nets); +} // namespace KEPLER_FORMAL::SEC::LATCH 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..d39a51e6 100644 --- a/src/sec/model/SequentialDesignModel.h +++ b/src/sec/model/SequentialDesignModel.h @@ -64,6 +64,9 @@ 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; // 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/strategy/SequentialEquivalenceStrategy.cpp b/src/sec/strategy/SequentialEquivalenceStrategy.cpp index 82956fb4..1cbcee77 100644 --- a/src/sec/strategy/SequentialEquivalenceStrategy.cpp +++ b/src/sec/strategy/SequentialEquivalenceStrategy.cpp @@ -40,6 +40,7 @@ #include "kinduction/OutputBatching.h" #include "kinduction/SatEncoding.h" #include "model/SequentialDesignModel.h" +#include "latch/LatchEventContract.h" #include "pdr/PDREngine.h" #include "proof/DualRailEncoding.h" #include "proof/TransitionExprResolver.h" @@ -3737,6 +3738,10 @@ 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 (auto error = LATCH::eventContractError(model0.eventContract, model1.eventContract, resetSpec_.enabled())) { + return makeSecResult(SequentialEquivalenceStatus::Unsupported, 0, *error, + OutputCoverageSelection{}, extractedBoundaryReports); + } AlignedSecInterface aligned = alignSecInterface( model0, model1, @@ -3868,7 +3873,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..259f0187 100644 --- a/test/python/CMakeLists.txt +++ b/test/python/CMakeLists.txt @@ -34,3 +34,9 @@ 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) 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_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..6ad32d77 100644 --- a/test/sec/BUILD.bazel +++ b/test/sec/BUILD.bazel @@ -28,6 +28,11 @@ cc_test( cc_test( name = "SequentialEquivalenceStrategyTests", srcs = [ + "LatchBoundaryEncodingTests.cpp", + "LatchEventModelTests.cpp", + "LatchNetlistAdapterTests.cpp", + "LatchSettlingCompilerTests.cpp", + "OpaquePolicyTests.cpp", "SequentialEquivalenceStrategyTests.cpp", "InternalRelationsTests.cpp", "Btor2ExportStrategyTests.cpp", diff --git a/test/sec/CMakeLists.txt b/test/sec/CMakeLists.txt index 861264ab..9686dff7 100644 --- a/test/sec/CMakeLists.txt +++ b/test/sec/CMakeLists.txt @@ -2,6 +2,11 @@ # SPDX-License-Identifier: Apache-2.0 add_executable(secStrategyTests + OpaquePolicyTests.cpp + LatchEventModelTests.cpp + LatchSettlingCompilerTests.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/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..91fc2d4f --- /dev/null +++ b/test/sec/LatchNetlistAdapterTests.cpp @@ -0,0 +1,659 @@ +// Copyright 2026 keplertech.io +// SPDX-License-Identifier: Apache-2.0 + +#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::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); + } + + NLLibrary* designs = nullptr; + NLLibrary* primitives = nullptr; +}; + +TEST_F(LatchNetlistAdapterTests, DefaultOptionsLeaveExistingExtractionUntouched) { + auto* top = directTop("top", NLDB0::getDLatch()); + EXPECT_FALSE(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, ExplicitContractRejectsUnspecifiedInitialization) { + auto setting = options(); + setting.initialInputs.reset(); + ScopedSupportOptions scope(setting); + const auto model = SequentialDesignModel::extract(directTop("top", NLDB0::getDLatch())); + EXPECT_TRUE(model.hasUnsupportedFeatures()); + EXPECT_TRUE(model.observedOutputs.empty()); +} + +TEST_F(LatchNetlistAdapterTests, ScopedOptionsRestorePreviousSemanticContract) { + EXPECT_FALSE(supportOptions().enabled); + { + ScopedSupportOptions outer(options()); + EXPECT_TRUE(supportOptions().enabled); + { + ScopedSupportOptions inner(SupportOptions{}); + EXPECT_FALSE(supportOptions().enabled); + } + EXPECT_TRUE(supportOptions().enabled); + EXPECT_TRUE(supportOptions().singleInputChange); + } + EXPECT_FALSE(supportOptions().enabled); +} + +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, 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; + } +} + +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, ExtractedEventModelRejectsCycleCountedResetBootstrap) { + 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-cycle reset"), 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/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/OpaquePolicyTests.cpp b/test/sec/OpaquePolicyTests.cpp new file mode 100644 index 00000000..1a1a8afc --- /dev/null +++ b/test/sec/OpaquePolicyTests.cpp @@ -0,0 +1,238 @@ +// 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 "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) { + 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/strategies/miter/BUILD.bazel b/test/strategies/miter/BUILD.bazel index 2fb4ed33..b6842af6 100644 --- a/test/strategies/miter/BUILD.bazel +++ b/test/strategies/miter/BUILD.bazel @@ -32,3 +32,23 @@ cc_test( "@googletest//:gtest_main", ], ) + +cc_test( + name = "LatchEventCliTests", + srcs = [ + "LatchEventConfigTests.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..61f17806 100644 --- a/test/strategies/miter/CMakeLists.txt +++ b/test/strategies/miter/CMakeLists.txt @@ -27,6 +27,9 @@ add_executable(keplerFormalCliTests Btor2ExportCliTests.cpp Btor2ExportDriverTests.cpp DriverSeparationTests.cpp + OpaquePolicyCliTests.cpp + LibertyLatchModelsTests.cpp + LatchEventConfigTests.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..29f99791 --- /dev/null +++ b/test/strategies/miter/LatchEventConfigTests.cpp @@ -0,0 +1,357 @@ +// 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, DisabledByDefaultWithoutAnImplicitContract) { + LatchEventConfig config; + std::string error; + EXPECT_TRUE(config.parseYaml(YAML::Load("{}"), error)); + EXPECT_FALSE(config.options().enabled); + EXPECT_FALSE(config.options().initialInputs.has_value()); + EXPECT_TRUE(config.validate(false, false, false, 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, EventTuningNeverEnablesTheMasterGate) { + for (const auto* gate : {"", "latch_support: false\n"}) { + LatchEventConfig config; + std::string error; + ASSERT_TRUE(config.parseYaml(YAML::Load(std::string(gate) + "sec_latch_events: " + complete), error)); + EXPECT_FALSE(config.options().enabled); + EXPECT_FALSE(config.validate(true, false, false, error)); + 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_FALSE(config.options().enabled); + EXPECT_FALSE(config.validate(true, false, false, error)); +} + +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_FALSE(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_FALSE(config.validate(true, false, false, error)); + EXPECT_NE(error.find("explicit input_changes"), std::string::npos); + ASSERT_EQ(parseArgument(config, {"--sec-latch-initial-inputs", "0"}, error), Result::Parsed); + ASSERT_EQ(parseArgument(config, {"--sec-latch-initial-storage", "0"}, error), Result::Parsed); + EXPECT_FALSE(config.validate(true, false, false, error)); + 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, RequiresExplicitInputChangeAndBothInitializationChoices) { + for (const auto* body : {"{}", "{input_changes: single}", + "{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_FALSE(config.validate(true, false, false, error)); + EXPECT_NE(error.find("explicit"), std::string::npos); + } +} + +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}", 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); +} + +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"}) { + 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, 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_FALSE(config.validate(true, true, false, error)); + EXPECT_NE(error.find("reset cycles"), std::string::npos); + 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, RejectsTuningWithoutMasterGateInBothFrontends) { + auto arguments = options(); + arguments.erase(arguments.begin()); // Keep tuning, deliberately omit master. + arguments.insert(arguments.end(), {"-verilog", "-v", "sec", "self.v", "self.v", "cells.lib"}); + EXPECT_NE(run(arguments).exitCode, 0); + 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, 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, RejectsLecResetCyclesAndSelectedLeafBoundaries) { + 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, WorkflowRestoresOuterEventContractAndDisablesItByDefault) { + 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 legacy = run({"-verilog", "-v", "sec", "self.v", "self.v", "cells.lib"}); + EXPECT_EQ(legacy.coveredOutputs, 0u); + EXPECT_TRUE(Latch::supportOptions().enabled); + EXPECT_EQ(Latch::supportOptions().initialStorage, true); +} + +} // 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 From b50acae823dc7b6df6cdb6adce6ed2d0c4aca50f Mon Sep 17 00:00:00 2001 From: Noam Cohen Date: Fri, 25 Sep 2026 17:21:52 +0200 Subject: [PATCH 03/10] feat(sec): certify symbolic latch events and support clock-cycle reset --- docs/sec-latch-implementation.md | 312 ++++++++++-- docs/sec-latch-support.md | 54 ++- docs/sec-reset-bootstrap.md | 6 + regress/run_python_regress.py | 6 +- src/bin/KeplerFormal.cpp | 5 +- src/bin/LatchEventConfig.cpp | 11 +- src/python/BorrowedLatchOptions.cpp | 58 +++ src/python/BorrowedLatchOptions.h | 32 ++ src/python/CMakeLists.txt | 5 +- src/python/KeplerBorrowedDesigns.cpp | 7 +- src/python/KeplerBorrowedDesigns.h | 4 +- src/python/PyKeplerFormal.cpp | 11 +- src/python/PyLatchOptions.cpp | 91 ++++ src/python/PyLatchOptions.h | 16 + src/python/kepler_formal/_latch_options.py | 55 +++ src/python/kepler_formal/api.py | 20 + src/sec/BUILD.bazel | 15 + src/sec/CMakeLists.txt | 6 + src/sec/latch/LatchConstantNet.h | 47 ++ src/sec/latch/LatchDependencyGraph.cpp | 143 ++++++ src/sec/latch/LatchDependencyGraph.h | 26 + src/sec/latch/LatchInputHistory.h | 22 + src/sec/latch/LatchNetlistAdapter.cpp | 109 +++-- src/sec/latch/LatchResetAdapter.cpp | 391 +++++++++++++++ src/sec/latch/LatchResetAdapter.h | 40 ++ src/sec/latch/LatchResetClock.cpp | 183 +++++++ src/sec/latch/LatchResetClock.h | 43 ++ src/sec/latch/LatchSupportOptions.h | 3 + src/sec/latch/LatchSymbolicCompiler.cpp | 365 ++++++++++++++ src/sec/latch/LatchSymbolicCompiler.h | 48 ++ src/sec/latch/LatchSymbolicEncoding.cpp | 101 ++++ src/sec/latch/LatchSymbolicEncoding.h | 14 + src/sec/latch/LatchSymbolicModel.cpp | 419 ++++++++++++++++ src/sec/latch/LatchSymbolicModel.h | 75 +++ src/sec/latch/NajaEventPrimitive.cpp | 50 +- src/sec/latch/NajaEventPrimitive.h | 5 +- src/sec/latch/NajaSymbolicLogic.h | 112 +++++ src/sec/model/SequentialDesignModel.h | 6 + .../SequentialEquivalenceStrategy.cpp | 34 +- test/python/CMakeLists.txt | 15 + test/python/borrowed_latch_options_tests.cpp | 186 +++++++ test/python/borrowed_latch_tests.cpp | 169 +++++++ test/python/test_latch_api.py | 135 ++++++ test/python/test_latch_native.py | 83 ++++ test/python/test_latch_options.py | 146 ++++++ test/sec/BUILD.bazel | 9 + test/sec/CMakeLists.txt | 9 + test/sec/LatchConstantNetTests.cpp | 295 +++++++++++ test/sec/LatchDependencyGraphTests.cpp | 177 +++++++ test/sec/LatchEventExportTests.cpp | 401 +++++++++++++++ test/sec/LatchNetlistAdapterTests.cpp | 7 +- test/sec/LatchResetAdapterTests.cpp | 403 ++++++++++++++++ test/sec/LatchResetClockTests.cpp | 264 ++++++++++ test/sec/LatchResetIntegrationTests.cpp | 412 ++++++++++++++++ test/sec/LatchSymbolicCompilerTests.cpp | 342 +++++++++++++ test/sec/LatchSymbolicIntegrationTests.cpp | 188 ++++++++ test/sec/LatchSymbolicModelTests.cpp | 456 ++++++++++++++++++ test/strategies/miter/BUILD.bazel | 1 + test/strategies/miter/CMakeLists.txt | 1 + .../miter/LatchEventConfigTests.cpp | 35 +- test/strategies/miter/LatchResetCliTests.cpp | 212 ++++++++ 61 files changed, 6783 insertions(+), 113 deletions(-) create mode 100644 src/python/BorrowedLatchOptions.cpp create mode 100644 src/python/BorrowedLatchOptions.h create mode 100644 src/python/PyLatchOptions.cpp create mode 100644 src/python/PyLatchOptions.h create mode 100644 src/python/kepler_formal/_latch_options.py create mode 100644 src/sec/latch/LatchConstantNet.h create mode 100644 src/sec/latch/LatchDependencyGraph.cpp create mode 100644 src/sec/latch/LatchDependencyGraph.h create mode 100644 src/sec/latch/LatchInputHistory.h create mode 100644 src/sec/latch/LatchResetAdapter.cpp create mode 100644 src/sec/latch/LatchResetAdapter.h create mode 100644 src/sec/latch/LatchResetClock.cpp create mode 100644 src/sec/latch/LatchResetClock.h create mode 100644 src/sec/latch/LatchSymbolicCompiler.cpp create mode 100644 src/sec/latch/LatchSymbolicCompiler.h create mode 100644 src/sec/latch/LatchSymbolicEncoding.cpp create mode 100644 src/sec/latch/LatchSymbolicEncoding.h create mode 100644 src/sec/latch/LatchSymbolicModel.cpp create mode 100644 src/sec/latch/LatchSymbolicModel.h create mode 100644 src/sec/latch/NajaSymbolicLogic.h create mode 100644 test/python/borrowed_latch_options_tests.cpp create mode 100644 test/python/borrowed_latch_tests.cpp create mode 100644 test/python/test_latch_api.py create mode 100644 test/python/test_latch_native.py create mode 100644 test/python/test_latch_options.py create mode 100644 test/sec/LatchConstantNetTests.cpp create mode 100644 test/sec/LatchDependencyGraphTests.cpp create mode 100644 test/sec/LatchEventExportTests.cpp create mode 100644 test/sec/LatchResetAdapterTests.cpp create mode 100644 test/sec/LatchResetClockTests.cpp create mode 100644 test/sec/LatchResetIntegrationTests.cpp create mode 100644 test/sec/LatchSymbolicCompilerTests.cpp create mode 100644 test/sec/LatchSymbolicIntegrationTests.cpp create mode 100644 test/sec/LatchSymbolicModelTests.cpp create mode 100644 test/strategies/miter/LatchResetCliTests.cpp diff --git a/docs/sec-latch-implementation.md b/docs/sec-latch-implementation.md index 88301681..b14b2052 100644 --- a/docs/sec-latch-implementation.md +++ b/docs/sec-latch-implementation.md @@ -1,15 +1,20 @@ # SEC Latch Event Implementation -This document describes the first implementation of the -[latch-support design](sec-latch-support.md). It is an **opt-in, finite Boolean -event model with exhaustive certification and table-based compilation**. It is -not an implementation of every proposed optimization, a scalable symbolic -settling prover, or a timing-accurate model of arbitrary asynchronous circuits. +This document describes the deterministic Boolean-event implementation of the +[latch-support design](sec-latch-support.md). It includes an executable reference, +**SAT-certified symbolic bounded unfolding**, an independent exhaustive finite +compiler, dependency-closed scheduling regions, parallel wave evaluation, and +SEC integration. It is not a timing-accurate model of arbitrary asynchronous +circuits. Optional phase abstraction and stronger scheduling reductions are not +required by, or enabled in, this implementation. The master switch is `latch_support: true` in YAML or `--latch_support` on the command line. It is **off by default**. New latch extraction and supplemental Liberty latch modeling are behind this switch; leaving it off preserves the -existing SEC path and its opaque-latch behavior. +existing SEC path and its opaque-latch behavior. In particular, with +`latch_support` off, `sec_reset` uses the unchanged legacy reset-bootstrap path; +none of the event adapter's clock-discovery, initialization, or single-reset-port +requirements apply. ## 1. Enabling the model requires an explicit contract @@ -89,12 +94,105 @@ not clock cycles or internal waves. An independent enable can open and close between flip-flop edges through separate transactions. Data changes while a latch is open also propagate without requiring a flip-flop edge. -The current event path rejects clock-cycle reset bootstrap and selected leaf -boundaries. Asynchronous reset/set pins still participate in the event model -as ordinary external or internally generated controls; the existing -`sec_reset.cycles` mechanism is not reinterpreted as an event sequence. BTOR2 export +Selected leaf boundaries remain unsupported. Asynchronous reset/set pins +participate in the event model as external or internally generated controls. +**`sec_reset.cycles` still means clock cycles**, using the automatic adapter +below; it never means an arbitrary count of input events or settling waves. BTOR2 export uses the compiled transaction transition system and records the event contract; -its steps must not be interpreted as hardware clock ticks. +outside the reset prefix its steps must not be interpreted as hardware clock +ticks. Counterexample reports distinguish reset cycles from **event transactions** +and print the contract. + +### Reset-cycle adapter + +The existing reset configuration can be used with latch support: + +```yaml +latch_support: true +sec_latch_events: + input_changes: single + initial_inputs: 0 + initial_storage: 0 +sec_reset: + cycles: 3 + ports: + - name: rst + active_value: 1 +``` + +The clock is discovered from explicit flip-flop clock expressions and exact +combinational routing, not from names or arbitrary latch enables. The initial +automatic subset requires one common top-level clock root, allowing direct, +buffered, and inverted routes and both sampling polarities. Clock discovery +only supplies the reset stimulus: latches still use the full event model, not +a phase-register approximation. + +Each of the N reset cycles composes these already-certified transactions: + +1. Assert reset and settle. Put the source clock low and settle. If initialization + placed it high, this establishes the low starting phase while reset is active; + that alignment edge is represented, not silently discarded. +2. Sample unconstrained levels for **all** non-clock/non-reset inputs, preserving + the reset environment's free data inputs. With `any`, admit the entire vector + as one transaction. With `single`, assign each sampled input through its own + settled transaction, with additional shared environment choices covering every + arrival order. There is no fixed favorable order and no one-input-per-cycle + restriction. The sampled levels are then held during the two clock edges. +3. Drive the source clock high and fully settle all affected logic and storage. +4. Drive it low and fully settle again. This completes one clock cycle, including + positive- and negative-edge state updates and intervening latch transparency. +5. After the final cycle, deassert reset and settle before normal checking resumes. + +```mermaid +flowchart LR + R[Assert reset and establish low clock] --> D[Sample non-control inputs] + D --> P[Rising edge and full settling] + P --> F[Falling edge and full settling] + F -->|More reset cycles| D + F -->|N cycles complete| U[Release reset and settle] + U --> E[Ordinary event transactions] +``` + +A saturating countdown implements this prefix; the compiler does not build N +copies of the full reset logic. Outputs are compared only after the prefix, +as with reset bootstrap. The countdown must finish after exactly N macrosteps, +and each composed event already has a universal progress certificate, so the +mask cannot hide a non-settling modeled reset execution. The requested post-reset +`max_k` budget is retained by extending the engine bound by N. + +Composition is not free: with m non-control input bits, `single` reset sampling +composes m event transitions plus a fixed number of clock/reset transitions, +and the total arrival-order selector construction has quadratic work in m. +The existing `max_symbolic_nodes` budget also bounds the cumulative expression +DAG visited during reset composition (not peak process memory); excessive +ordering work is rejected before construction. Exceeding that budget reports +unsupported reset adaptation rather than choosing a subset of input orders. + +Afterward, clocks resume ordinary event-driven behavior and reset is held inactive, +matching existing reset-bootstrap policy. A requested reset change is ignored by +the effective input adapter (a stutter for the selector interface); witness input +bits must be read with this constraint. For exported models and witnesses, the +first N steps are **reset clock cycles** and subsequent steps are **external event +transactions**. Contract metadata and witness headers record this distinction. +In the selector interface, the original top-input interface variables provide +the sampled levels during reset, while extra reset-order inputs specify their +arrival order; after the prefix only the usual selector/value inputs drive events. +This is a cycle-sampled reset environment, not unrestricted glitch timing inside +a cycle. All such input-arrival choices are shared external stimulus for the two +designs, not private internal scheduling choices discarded by the certifier. + +The first automatic adapter supports **one reset port**. Multiple reset ports +would require a justified assertion/release ordering; multiple independent clock +roots require a defined schedule. Gated/state-generated/ambiguous clock roots, +clockless latch-only designs, unsupported primitive clock metadata, and a reset +port used as its own clock are rejected with specific diagnostics. These limits +affect automatic reset-cycle expansion, not ordinary event-mode reset signals. +No clock is guessed and no reset configuration silently falls back to N events. + +The adapter composes copies of the certified boundary-to-boundary transition, +preserving complete state between events. It therefore inherits progress and +determinism from those certificates. This is a Kepler integration construction, +not a claim that the cited papers prescribe this particular reset protocol. Both comparison designs use the same contract, even if one contains no latches. Contract metadata prevents mixing ordinary clock-cycle models, event models, @@ -124,6 +222,12 @@ truth tables into pure primitive callbacks. Worker evaluations do not access the original Naja objects. This also allows compact extraction to release the source design after constructing its SEC model. +Explicit constant nets do not need a cell driver. If flattened connectivity +omits their equipotential entry, a read-only connected-hierarchy walk resolves +consistent Boolean annotations. Floating, X/Z, conflicting, or unexpectedly +driven nets do not silently become constants; the original netlist and its +flattened connectivity are not modified. + The accepted subset is intentionally conservative: - Physical output mappings and clear/preset behavior must be defined. Undefined @@ -148,6 +252,14 @@ Shared read-only primary inputs do not by themselves join otherwise independent components. A component can contain latch chains, feedback loops, combinational logic, and flip-flops. +Dependency analysis also identifies directed feedback SCCs, including self-loops, +separately from these undirected event-connected components. Both analyses are +iterative and deterministically ordered. SCC membership is structural information, +not a termination certificate, and a flip-flop is not an unconditional cut of its +generated clock or asynchronous-control dependencies. The first implementation +certifies whole event-connected components; it never invents independent local +settling bounds from SCC sizes. + This conservative closure prevents a component's temporary pulse from being discarded at a neighboring storage element. It is not an implementation of arbitrary independently settling local islands or final-output-only summaries. @@ -157,8 +269,10 @@ arbitrary independently settling local islands or final-output-only summaries. Initialization is itself a checked settling episode: 1. Install the explicit initial external values and primitive storage values. -2. Initialize physical storage outputs from their model; enumerate auxiliary - internal net seeds rather than choosing convenient values. +2. Initialize physical storage outputs from their model; universally quantify + auxiliary internal net seeds rather than choosing convenient values. The + finite reference compiler enumerates them; the symbolic compiler uses + unconstrained Boolean variables. 3. Set previous values equal to initial current values, so merely beginning with a high external clock does not invent a rising edge. 4. Force a BOOT evaluation. Gates evaluate, latches apply level-sensitive and @@ -169,7 +283,7 @@ Initialization is itself a checked settling episode: to the same complete boundary for the prescribed intended initialization. When a physical output's initial projection reads input pins, certification -also enumerates seeds of other storage-output nets that the projection can +also quantifies seeds of other storage-output nets that the projection can read before they are overwritten. Otherwise those hidden seed choices could determine the retained result. @@ -193,7 +307,68 @@ choose a latch capture. A producer's chosen result is shared across its fanout, not resampled independently for each consumer. The complete-wave update barrier preserves the reference epochs. -## 5. Exact finite certification and compilation +## 5. Certification and compilation + +### 5.1 Symbolic bounded unfolding (first choice) + +The symbolic model mirrors the reference's complete state, BOOT, admission, +pin-order choices, sticky errors, frozen snapshots, and update barriers. Primitive +expressions are copied directly into Boolean DAGs. Generic AND/OR/XOR families +are linear-size constructions; small explicit library truth tables are local +expansions, not circuit-wide input or state enumeration. + +Each wave allocates fresh ordering selectors before parallel evaluation. Every +selector encoding denotes a legal pin order, including otherwise unused binary +codes. Unchanged pins are skipped; restricting a full-pin permutation in this +way represents every changed-pin permutation. A producer's choice is shared +across all fanout. Errors remain nonstable, and completed states have exact +identity successors. + +For each component: + +1. Retain every current net bit and primitive storage bit in the macrostate. + At completed boundaries, previous=current and active=BOOT=error=0, so those + omitted fields are exactly reconstructible, not discarded history. +2. Construct candidate invariant `B`: a normalized error-free boundary with + consistent constants, where forced no-edge evaluation of each primitive + preserves its storage and physical outputs. This overapproximates reachable + boundaries; it is **proved**, not assumed, to hold after initialization and + to be inductive. +3. Unfold BOOT from the specified input/storage values, quantifying independent + auxiliary seeds and every permitted pin order. Try increasing bounds up to + `max_waves`; prove `not Stable(xK)` UNSAT. Independently seeded/ordered copies + must also have the same **complete** boundary, and that boundary must satisfy B. +4. Share symbolic pre-admission `q` and external input `u` between two ordinary + episodes. Under `B(q) AND Allowed(q,u)`, prove bounded progress, complete-state + outcome uniqueness with independent ordering choices, and `B(next)` closure. + `single` constrains admission only, never internally generated pin changes. +5. Only after those obligations succeed, rebuild the same K waves using one + legal canonical pin order and a legal BOOT seed. Uniqueness justifies removing + the choices. Export no seed/choice variables. The certificate retains its + admission contract and external-net mapping; encoding cannot change either. +6. Substitute SEC state/input variables simultaneously into the compiled DAG, + with shared subexpressions retained. This logic is built once, not regenerated + on each transaction. The SEC engines prove arbitrarily long boundary traces + using these next-state and observation expressions. + +These are ordinary SAT safety obligations over the finite symbolic relation, +using CaDiCaL with cumulative conflict/decision budgets. SAT at an insufficient +bound is not proof of oscillation. A failure found only from the overapproximate +invariant is an **unproved certificate**, not a reachable design defect. UNKNOWN +or resource exhaustion cannot become a successful certificate. + +```mermaid +flowchart TD + N[Copied primitives and complete dependency regions] --> S[Symbolic BOOT and K-wave proof] + S -->|Progress, uniqueness, invariant all proved| U[Canonical K-wave Boolean DAG] + S -->|Certificate not established| F[Exact reachable finite reference compiler] + F -->|Complete certificate| T[Boundary transition table] + F -->|Unsupported or resource limit| O[Opaque with coverage or strict error] + U --> M[Shared transaction interface and SEC] + T --> M +``` + +### 5.2 Exact finite reference and fallback For each component the compiler performs exhaustive finite-state analysis, subject to explicit resource limits: @@ -239,30 +414,44 @@ For those witnesses, `$event.select[i]` contributes bit `i` of the zero-based selector, with bit zero least significant. Original top input names, including their bit indices, are sorted lexicographically to define that selector order. `$event.value` supplies the assigned Boolean value. The retained -`$event.interface.*` variables are alignment sentinels only; their witness values -do not drive the modeled input levels. - -This implementation does **not** yet generate a scalable symbolic `K`-copy -unfolding of arbitrary latch networks. It uses the finite decision procedure -described in the design document to certify and explicitly compile small -event-connected components. Both state exploration and table formulas can grow -exponentially. Larger symbolic certificates, finer island scheduling, stronger -reductions, and phase abstraction remain subsequent work. +original input-interface variables are alignment sentinels outside the reset +prefix; their witness values do not drive ordinary event transactions. During +the optional reset prefix they instead supply the sampled levels, as described +in the reset-cycle adapter above. + +The exact subject of the symbolic `K`-copy unfolding is the declared +Boolean-event contract, not unrestricted physical latch networks. It avoids the finite +fallback's whole-state/input enumeration, but SAT cost and unfolded DAG size +can still grow substantially. Either backend may certify a component; if neither +does, opacity is preserved. The finite backend is also an independent oracle +for differential tests of the symbolic compiler. Stronger regional summaries +and phase abstraction remain optional future reductions. ## 6. Resource controls -The first three limits below and the worker count can be tuned through -`sec_latch_events` or the corresponding CLI flags: +The following limits can be tuned through `sec_latch_events`, the corresponding +CLI flags, or the Python options in Section 8: | Setting | Default | Scope | | --- | ---: | --- | | `max_waves` / `--sec-latch-max-waves` | 256 | Maximum accepted settling depth of an episode | -| `max_states` / `--sec-latch-max-states` | 4,096 | Reachable complete boundaries per component | -| `max_transactions` / `--sec-latch-max-transactions` | 65,536 | Compiled boundary/input rows per component | +| `max_symbolic_nodes` / `--sec-latch-max-nodes` | 2,000,000 | Cumulative visited symbolic DAG nodes, including proof copies; also the reset-composition budget | +| `max_sat_conflicts` / `--sec-latch-sat-conflicts` | 500,000 | Cumulative symbolic certification SAT conflicts | +| `max_sat_decisions` / `--sec-latch-sat-decisions` | 5,000,000 | Cumulative symbolic certification SAT decisions | +| `max_states` / `--sec-latch-max-states` | 4,096 | Finite fallback: reachable complete boundaries per component | +| `max_transactions` / `--sec-latch-max-transactions` | 65,536 | Finite fallback: boundary/input rows per component | | `workers` / `--sec-latch-workers` | 0 | Automatic TBB worker selection; `1` selects serial evaluation | -Other conservative limits currently live in the standalone compiler/reference -API, not in additional YAML keys: +The symbolic node budget is not a hard process-memory ceiling: temporary nodes +may be constructed inside a wave before accounting, and the shared expression +cache has its own lifetime. SAT budgets and certification node limits apply per +component. Reset composition separately applies the node budget to each complete +design's adapted model. + +Other conservative finite-compiler/reference limits currently live in the +standalone API, not in additional YAML keys. The 100,000 local pin-permutation +limit also applies to symbolic sequential primitives; gates do not enumerate +pin orders: | Resource | Default | | --- | ---: | @@ -278,9 +467,18 @@ API, not in additional YAML keys: `max_states` is not the internal episode-state limit. Raising one setting does not disable the others or guarantee that a component becomes tractable. +The symbolic path is not subject to the finite fallback's 12-input/seed-bit or +512-complete-state-bit limits; selecting a tiny finite-table limit alone does +not disable symbolic certification. These limits reject unproved cases; they do not truncate the relation while claiming a successful certificate. +Certification of the event model does not guarantee that an SEC engine can +prove equivalence within its own bound and resource limits. Retained event +history and reset-counter bits also contribute to backend state size. An +inconclusive backend result remains inconclusive; it is not equivalence and +does not justify restricting the event or reset-input contract. + ## 7. Opacity, errors, and coverage With latch support disabled, the existing extraction path remains in use. @@ -301,31 +499,71 @@ It does not reclassify every unrelated connectivity skip as an opaque cell. Invalid global configuration, incompatible contracts, and unsupported whole-run interfaces are rejected rather than represented as a successfully modeled event -run. The borrowed-design/Python API currently does **not** enable this event -path and explicitly prevents inheriting an ambient event-model scope. Its -independent `error_on_opaque` setting is supported for SEC. +run. Borrowed-design calls explicitly scope their own settings; they do not +inherit a caller's ambient event contract or leak their settings back to it. + +## 8. Borrowed C++ and Python APIs + +The Python interface uses the same default-off gate and explicit contract: + +```python +from kepler_formal import VerificationOptions + +options = VerificationOptions( + mode="sec", + latch_support=True, + latch_input_changes="single", + latch_initial_inputs=0, + latch_initial_storage=0, +) +``` + +Optional fields are `latch_workers`, `latch_max_waves`, `latch_max_states`, +`latch_max_transactions`, `latch_max_symbolic_nodes`, `latch_max_sat_conflicts`, +and `latch_max_sat_decisions`. `latch_workers=0` selects automatic parallelism; +proof budgets must be positive. Invalid types, missing contract fields, tuning +without enablement, non-SEC mode, and selected leaf boundaries are rejected. +The independent `error_on_opaque` option remains default-off. + +The native C++ equivalent is `BorrowedDesignOptions::latchSupport`, a validated +`BorrowedLatchOptions` object. Borrowed APIs consume the caller's explicit Naja +models; they do not load Liberty files or guess latch semantics. Source models, +selected tops, DNL pointers/order IDs, and ambient configuration are restored +after success or failure. Matching-runtime native and Python tests run through +`regress/run_python_regress.py`, without modifying installed Python packages. -## 8. Implementation map and verification +## 9. Implementation map and verification | File | Responsibility | | --- | --- | | `src/bin/LatchEventConfig.*` | Master enable, explicit contract, tuning, and CLI/YAML validation | | `src/bin/LibertyLatchModels.*` | Opt-in supplemental scalar Liberty latch descriptions | | `src/sec/latch/NajaEventPrimitive.*` | Copy supported Naja primitive expressions into immutable callbacks | +| `src/sec/latch/LatchConstantNet.h` | Read-only resolution of driverless Boolean constants, preserving conflict diagnostics | | `src/sec/latch/LatchEventModel.*` | Boolean BOOT/admission/wave semantics, complete state, parallel primitive evaluation | | `src/sec/latch/LatchSettlingCompiler.*` | Exhaustive settling/uniqueness checks and reachable macro-transition table | +| `src/sec/latch/LatchSymbolicModel.*`, `NajaSymbolicLogic.h` | Exact symbolic waves and direct primitive DAGs | +| `src/sec/latch/LatchSymbolicCompiler.*` | SAT-certified progress, BOOT independence, uniqueness, closure, and bounded unfolding | +| `src/sec/latch/LatchSymbolicEncoding.*` | Certified contract checks and simultaneous SEC-variable substitution | +| `src/sec/latch/LatchDependencyGraph.*` | Iterative feedback SCCs and event-connected scheduling components | +| `src/sec/latch/LatchResetClock.*`, `LatchResetAdapter.*`, `LatchInputHistory.h` | Automatic source-clock discovery, remembered levels, and reset-cycle composition | | `src/sec/latch/LatchBoundaryEncoding.*` | Injective boundary-state encoding and shared event-input decoding | | `src/sec/latch/LatchNetlistAdapter.*` | Full data/control component closure, extraction, opacity, and SEC integration | | `src/sec/latch/LatchSupportOptions.*`, `LatchEventContract.h` | Scoped options and protection against incompatible step/initialization contracts | | `src/sec/model/OpaquePolicy.*` | Independent default-off error-on-opaque policy | +| `src/python/BorrowedLatchOptions.*`, `PyLatchOptions.*`, `kepler_formal/_latch_options.py` | Typed borrowed/Python contract validation and scoped activation | New tests cover the reference waves, latch chains and feedback, initialization seed dependence, all permitted pin orders, transient controls, serial/parallel agreement, exhaustive small-graph certification, resource failures, table encoding, original-input alignment, Naja/Liberty adapters, and configuration and -opacity policies. Test names are in the new `Latch*Tests.cpp` and -`OpaquePolicy*Tests.cpp` suites. This document intentionally does not claim a -fixed passing-test count or completion of the entire long-term architecture. +opacity policies. Symbolic tests compare complete states with concrete reference +successors and exact table rows, test long/wide networks beyond enumeration +limits, independent proof choices and seed copies, invariant overapproximation, +hidden-state races, SAT/resource failures, and contract-preserving export. +Test names are in the new `Latch*Tests.cpp`, `OpaquePolicy*Tests.cpp`, and Python +latch suites. Optional reductions and unrestricted timing models are not implied +by completion of this deterministic Boolean-event implementation. For the sources, conditional proof arguments, and remaining theoretical extensions, see [the design and literature references](sec-latch-support.md). diff --git a/docs/sec-latch-support.md b/docs/sec-latch-support.md index 2ccdeea4..5fa26160 100644 --- a/docs/sec-latch-support.md +++ b/docs/sec-latch-support.md @@ -1,9 +1,11 @@ # Proposed SEC Latch Support -Status: architectural design and conditional correctness arguments. The initial -opt-in, resource-bounded Boolean event implementation is documented separately -in [SEC Latch Event Implementation](sec-latch-implementation.md); it does not -implement every optimization proposed here. This document records the +Status: architectural design and conditional correctness arguments, with the +deterministic Boolean-event path implemented behind the default-off `latch_support` +switch. The implementation, explicit admission/initialization contract, symbolic +certifier, finite fallback, and scoped limitations are documented separately in +[SEC Latch Event Implementation](sec-latch-implementation.md). Optional phase +abstraction and stronger scheduling reductions remain extensions. This document records the literature-backed approach discussed for level-sensitive latch support. It does not itself enable latch extraction or change SEC results. The constructions and proof sketches below close the identified specification gaps for a deliberately @@ -59,8 +61,9 @@ flowchart TD Current documented behavior is described in [SEC Sequential Models](sec-sequential-models.md) and -[SEC Clock Handling](sec-clock-handling.md). Generic latch outputs are currently -opaque. Existing clock handling also has explicit limits on cross-domain cones. +[SEC Clock Handling](sec-clock-handling.md). With `latch_support` disabled, generic +latch outputs remain opaque. The opt-in path models only certified behavior. +Existing clock handling also has explicit limits on cross-domain cones. This proposal would extend those semantics; it is not merely an extraction optimization. In particular, modeling independent latch enables requires more @@ -728,12 +731,27 @@ for either design must stop the run with an error and a nonzero exit status, rather than continuing with partial coverage. The diagnostic must identify the design, hierarchical cell or signal, and reason for opacity. This policy applies to opacity generally, not only to unsupported latches; it does not change which -behavior the modeling strategy supports. The switch's CLI spelling is not yet -specified, and this document does not implement it. +behavior the modeling strategy supports. The implemented spellings are +`--error-on-opaque` and YAML/Python `error_on_opaque`, independently default-off. ## 12. Proposed Implementation and Validation Stages -These are future tasks, not changes made by this document. +The stages below remain the design's acceptance checklist. The core Boolean +implementation realizes stages 1-5 with explicit primitive callbacks, BOOT, +concrete and symbolic waves, dependency regions, SAT certificates, and bounded +compilation. Stage 6 uses an explicit **external-transaction** backend adapter: +native SEC, witnesses, and BTOR2 share that step meaning. Reset bootstrap uses +an automatic cycle adapter for the supported single-clock/single-reset subset: +assert reset, sample unconstrained data, generate both clock edges with full +settling, and release reset after N complete cycles. It never counts propagation +waves as cycles. Ambiguous clock protocols and selected leaf boundaries remain +unsupported; see the implementation document for the exact reset input-arrival +contract and diagnostics. Stage 7 remains optional, as originally proposed. See the implementation +document for exact supported frontend and resource boundaries. + +The entire latch path remains behind `latch_support`, default-off. With it off, +reset uses the existing bootstrap implementation unchanged; the event adapter's +additional clock/reset restrictions do not apply to legacy runs. 1. Encode and review the explicit primitive tables, Boolean bootstrap, shared environment, and observation contract specified here; supply missing frontend @@ -939,14 +957,16 @@ The previously open construction is now specialized as follows: - **Fallback:** unsupported behavior stays opaque by default, with existing reporting/skipping; error-on-opaque is a separate default-off switch. -These close the logical construction under the stated hypotheses. They do not -constitute a tested implementation, machine-checked theorem, or a proof that -arbitrary asynchronous hardware satisfies the hypotheses. Before enabling the -feature, the implementation must instantiate and validate the primitive tables, -bootstrap and invariants, compiler and scheduler, proof certificates, frontend -models, resource limits, and the adapter to SEC's existing cycle/reset/export -conventions. The environment/initial-state contract must be explicit in the user -configuration; it must not be inferred to make a proof succeed. +These close the logical construction under the stated hypotheses; they are not +a machine-checked theorem or a proof that arbitrary asynchronous hardware +satisfies those hypotheses. The opt-in implementation now instantiates the +primitive tables, bootstrap, reference and symbolic compilers, certificates, +dependency regions, resource limits, and SEC reset/export adapters, with tests +described in [the implementation document](sec-latch-implementation.md). +Tests and per-component SAT certificates do not establish that this Boolean +contract models every physical circuit. The environment/initial-state contract +must remain explicit in user configuration; it must not be inferred to make a +proof succeed. Extensions still requiring separate arguments include genuine multivalued semantics, input changes before an episode settles, physical timing and diff --git a/docs/sec-reset-bootstrap.md b/docs/sec-reset-bootstrap.md index 77e4ff4f..f0622a19 100644 --- a/docs/sec-reset-bootstrap.md +++ b/docs/sec-reset-bootstrap.md @@ -4,6 +4,12 @@ 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. +This behavior is unchanged when `latch_support` is off (the default). With +`latch_support` on, `cycles` still counts clock cycles, but an explicit event +adapter generates the clock edges and latch settling. Its supported clock/reset +subset and stimulus protocol are described in the +[latch reset-cycle adapter](sec-latch-implementation.md#reset-cycle-adapter). + ## YAML ```yaml 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/KeplerFormal.cpp b/src/bin/KeplerFormal.cpp index 1414d716..b8c78c58 100644 --- a/src/bin/KeplerFormal.cpp +++ b/src/bin/KeplerFormal.cpp @@ -87,8 +87,9 @@ static void print_usage(const char* prog) { SPDLOG_INFO("Boolean event SEC (off by default): --latch_support --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 ]. " - "Steps are settled external events, not clock/reset cycles."); + "[--sec-latch-max-states ] [--sec-latch-max-transactions ] " + "[--sec-latch-max-nodes ] [--sec-latch-sat-conflicts ] [--sec-latch-sat-decisions ]. " + "Normal steps are settled external events; sec_reset counts clock cycles."); // LCOV_EXCL_START } // LCOV_EXCL_STOP diff --git a/src/bin/LatchEventConfig.cpp b/src/bin/LatchEventConfig.cpp index 7df48509..6b267eac 100644 --- a/src/bin/LatchEventConfig.cpp +++ b/src/bin/LatchEventConfig.cpp @@ -43,6 +43,9 @@ bool LatchEventConfig::set(const std::string& key, const std::string& value, std 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; @@ -86,7 +89,10 @@ LatchEventConfig::ArgumentResult LatchEventConfig::parseArgument( {"--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-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; } @@ -95,6 +101,7 @@ LatchEventConfig::ArgumentResult LatchEventConfig::parseArgument( 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 (!explicitTuning_) return true; error = "latch event tuning requires latch_support: true or --latch_support"; @@ -103,8 +110,6 @@ bool LatchEventConfig::validate(bool isSec, bool hasResetCycles, bool hasLeafBou if (!isSec) error = "latch event options require SEC verification"; else if (!inputChangesExplicit_ || !options_.initialInputs || !options_.initialStorage) error = "latch events require explicit input_changes, initial_inputs and initial_storage"; - else if (hasResetCycles) - error = "latch event steps are external transactions, not reset cycles; drive resets as external events"; else if (hasLeafBoundaries) error = "latch events currently require the complete top interface, not selected leaf boundaries"; else return true; diff --git a/src/python/BorrowedLatchOptions.cpp b/src/python/BorrowedLatchOptions.cpp new file mode 100644 index 00000000..30fa71f8 --- /dev/null +++ b/src/python/BorrowedLatchOptions.cpp @@ -0,0 +1,58 @@ +// 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; + if (!enabled) { + if (inputChanges || initialInputs || initialStorage || workers || maxWaves || + maxStates || maxTransactions || maxSymbolicNodes || maxSatConflicts || maxSatDecisions) { + throw std::invalid_argument("latch event tuning requires latch_support=true"); + } + return result; + } + if (!isSec) throw std::invalid_argument("latch_support is only supported for SEC"); + if (!inputChanges || !initialInputs || !initialStorage) { + throw std::invalid_argument( + "latch_support requires explicit latch_input_changes, latch_initial_inputs and latch_initial_storage"); + } + if (*inputChanges != LatchInputChanges::Any && *inputChanges != LatchInputChanges::Single) { + throw std::invalid_argument("latch_input_changes must be any or single"); + } + if (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 == 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..635616c2 --- /dev/null +++ b/src/python/BorrowedLatchOptions.h @@ -0,0 +1,32 @@ +// 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. Merely specifying tuning never enables latch support. +struct BorrowedLatchOptions { + bool enabled = false; + 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 5f2feec6..ddf41a33 100644 --- a/src/python/KeplerBorrowedDesigns.cpp +++ b/src/python/KeplerBorrowedDesigns.cpp @@ -241,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"); @@ -254,9 +256,8 @@ int verifyBorrowedDesigns(naja::NL::SNLDesign* design0, Config::ScopedVerificationContext verificationContext; BorrowedExpressionState expressionState; BorrowedRunState runState; - // BorrowedDesignOptions has no event contract: never inherit ambient - // thread-local event semantics from a caller's direct extraction scope. - SEC::LATCH::ScopedSupportOptions eventOptions({}); + // 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); diff --git a/src/python/KeplerBorrowedDesigns.h b/src/python/KeplerBorrowedDesigns.h index 9d47ee47..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" @@ -24,6 +25,7 @@ struct BorrowedDesignOptions { 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 33afad6e..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 @@ -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; @@ -454,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..bdd95576 --- /dev/null +++ b/src/python/kepler_formal/_latch_options.py @@ -0,0 +1,55 @@ +# Copyright 2026 keplertech.io +# SPDX-License-Identifier: Apache-2.0 + +"""Validation of the explicit, default-off latch event contract.""" + +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") + if not enabled: + if any(value is not None for value in values.values()): + raise ValueError("latch event tuning requires latch_support=True") + elif mode != "sec": + raise ValueError("latch_support is only supported for SEC") + elif any(values[name] is None for name in names[:3]): + raise ValueError( + "latch_support requires explicit latch_input_changes, latch_initial_inputs and latch_initial_storage" + ) + elif 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 ab880604..ee5dee06 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,12 @@ 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 disabled by default. Enabling it requires SEC, an + explicit ``latch_input_changes`` contract (``"any"`` or ``"single"``), + and initial inputs/storage given as integer 0 or 1. Each SEC step then + 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 @@ -66,6 +73,17 @@ class VerificationOptions: learn_internal_relations: bool = True allow_x_equality_in_internal_relations: bool = True error_on_opaque: bool = False + latch_support: bool = False + 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 @@ -172,6 +190,7 @@ def _build_native_design_options( 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") @@ -206,6 +225,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, diff --git a/src/sec/BUILD.bazel b/src/sec/BUILD.bazel index fdca9561..0b20ef49 100644 --- a/src/sec/BUILD.bazel +++ b/src/sec/BUILD.bazel @@ -33,9 +33,15 @@ cc_library( "kinduction/SatEncoding.cpp", "latch/LatchBoundaryEncoding.cpp", "latch/LatchEventModel.cpp", + "latch/LatchDependencyGraph.cpp", "latch/LatchNetlistAdapter.cpp", "latch/LatchSettlingCompiler.cpp", "latch/LatchSupportOptions.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", @@ -65,12 +71,21 @@ cc_library( "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/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", diff --git a/src/sec/CMakeLists.txt b/src/sec/CMakeLists.txt index e70e61c1..949baeb5 100644 --- a/src/sec/CMakeLists.txt +++ b/src/sec/CMakeLists.txt @@ -13,7 +13,13 @@ add_library(kepler_sec STATIC 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/NajaEventPrimitive.cpp 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/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 index 90ce2c07..53d23468 100644 --- a/src/sec/latch/LatchNetlistAdapter.cpp +++ b/src/sec/latch/LatchNetlistAdapter.cpp @@ -4,7 +4,6 @@ #include #include -#include #include #include #include @@ -17,8 +16,14 @@ #include "SNLInstance.h" #include "SNLPath.h" #include "latch/LatchBoundaryEncoding.h" +#include "latch/LatchConstantNet.h" +#include "latch/LatchDependencyGraph.h" +#include "latch/LatchInputHistory.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 { @@ -88,16 +93,6 @@ struct DnlScope { } }; -struct DisjointSets { - std::vector parent; - explicit DisjointSets(size_t size) : parent(size) { std::iota(parent.begin(), parent.end(), 0); } - size_t root(size_t value) { - while (parent[value] != value) { parent[value] = parent[parent[value]]; value = parent[value]; } - return value; - } - void join(size_t a, size_t b) { a = root(a); b = root(b); if (a != b) parent[std::max(a,b)] = std::min(a,b); } -}; - void opaque(SequentialDesignModel& model, const SignalKey& key, const std::string& name, const std::string& reason) { model.displayNameByKey.insert_or_assign(key, name); @@ -121,6 +116,7 @@ SequentialDesignModel extract(naja::NL::SNLDesign* top, size_t side) { ";initial_storage=" + std::to_string(*options.initialStorage); DnlScope scope(top); const auto* dnl = naja::DNL::get(); + DriverlessConstantResolver driverlessConstants(*dnl); Network network; std::map byIso; std::vector netErrors; @@ -133,7 +129,12 @@ SequentialDesignModel extract(naja::NL::SNLDesign* top, size_t side) { network.constantByNet.push_back({}); netErrors.emplace_back(); if (isoID == naja::DNL::DNLID_MAX) { - netErrors.back() = "unconnected signal " + name(term); + 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); @@ -165,6 +166,8 @@ SequentialDesignModel extract(naja::NL::SNLDesign* top, size_t side) { 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; @@ -181,8 +184,10 @@ SequentialDesignModel extract(naja::NL::SNLDesign* top, size_t side) { 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); + 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) @@ -190,8 +195,11 @@ SequentialDesignModel extract(naja::NL::SNLDesign* top, size_t side) { 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 @@ -203,6 +211,14 @@ SequentialDesignModel extract(naja::NL::SNLDesign* top, size_t side) { 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; } @@ -212,23 +228,25 @@ SequentialDesignModel extract(naja::NL::SNLDesign* top, size_t side) { // 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. - DisjointSets groups(network.primitives.size()); + 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) { - groups.join(i, owner[output]); netErrors[output] = "multiple primitive writers"; } else owner[output] = i; } } - for (size_t i = 0; i < network.primitives.size(); ++i) - for (auto input : network.primitives[i].inputs) - if (owner[input] != absent) groups.join(i, owner[input]); - std::map> components; - for (size_t i = 0; i < network.primitives.size(); ++i) components[groups.root(i)].push_back(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.assign(inputs.size(), BoolExpr::Var(*options.initialInputs ? 1 : 0)); std::vector inputExpressions, selector; BoolExpr* eventValue = nullptr; if (!options.singleInputChange) { @@ -244,10 +262,13 @@ SequentialDesignModel extract(naja::NL::SNLDesign* top, size_t side) { 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()); } - for (const auto& [componentID, members] : components) { + 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) { @@ -272,14 +293,33 @@ SequentialDesignModel extract(naja::NL::SNLDesign* top, size_t side) { 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 table; + std::optional symbolic; + std::string symbolicFailure; if (failure.empty()) { + SymbolicCompileOptions compile; + compile.initialInputs.assign(local.externalInputs.size(), *options.initialInputs); + 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.initialStorage.emplace_back(primitive.storageBits, uint8_t(*options.initialStorage)); + auto result = compileSymbolicNetwork({local, std::move(localSymbolic)}, compile); + if (result.certified()) symbolic = std::move(result.model); + else symbolicFailure = result.detail; + } + std::optional table; + if (failure.empty() && !symbolic) { try { EventModel reference(local, {}, options.workers); CompileOptions compile; @@ -290,7 +330,8 @@ SequentialDesignModel extract(naja::NL::SNLDesign* top, size_t side) { compile.initialStorage.emplace_back(primitive.storageBits, uint8_t(*options.initialStorage)); auto result = compileTransitionTable(reference, compile); if (result.certified()) table = std::move(result.table); - else failure = std::string(certificationStatusName(result.status)) + ": " + result.detail; + 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())) @@ -299,7 +340,7 @@ SequentialDesignModel extract(naja::NL::SNLDesign* top, size_t side) { 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 && groups.root(owner[output.net]) == componentID) { + if (owner[output.net] != absent && graph.componentOf[owner[output.net]] == componentID) { model.skippedObservedOutputs.push_back(output.key); opaque(model, output.key, output.name, reason); } @@ -307,17 +348,23 @@ SequentialDesignModel extract(naja::NL::SNLDesign* top, size_t side) { } std::vector state; std::vector stateKeys; - for (size_t bit = 0; bit < boundaryEncodingBits(table->boundaries.size()); ++bit) { + 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)); - model.initialStateValueByKey.emplace(stateKey, (table->initials[0].boundary >> bit) & 1); + model.initialStateValueByKey.emplace(stateKey, symbolic ? symbolic->initialState.at(bit) : (table->initials[0].boundary >> bit) & 1); } - const auto encoded = encodeBoundaryTable(*table, local, state, localInputs, selector, eventValue, globalInputIndices); + 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 || groups.root(owner[output.net]) != componentID) continue; + 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))); } @@ -334,6 +381,12 @@ SequentialDesignModel extract(naja::NL::SNLDesign* top, size_t side) { } } 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; + model.eventResetInterface = std::move(resetInterface); applyOpaquePolicy(model, top->getName().getString(), side); return model; } diff --git a/src/sec/latch/LatchResetAdapter.cpp b/src/sec/latch/LatchResetAdapter.cpp new file mode 100644 index 00000000..5801c5ae --- /dev/null +++ b/src/sec/latch/LatchResetAdapter.cpp @@ -0,0 +1,391 @@ +// 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)); +} + +// 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.initialStateValueByKey.count(key)) + return "Latch reset cycles require fully initialized event state"; + } + 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(); + 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/LatchSupportOptions.h b/src/sec/latch/LatchSupportOptions.h index 7b6ef6a1..62dcf343 100644 --- a/src/sec/latch/LatchSupportOptions.h +++ b/src/sec/latch/LatchSupportOptions.h @@ -18,6 +18,9 @@ struct SupportOptions { std::optional initialStorage; size_t workers = 0; CompilerLimits limits; + size_t maxSymbolicNodes = 2000000; + unsigned maxSatConflicts = 500000; + unsigned maxSatDecisions = 5000000; }; const SupportOptions& supportOptions(); diff --git a/src/sec/latch/LatchSymbolicCompiler.cpp b/src/sec/latch/LatchSymbolicCompiler.cpp new file mode 100644 index 00000000..b8e7e6cc --- /dev/null +++ b/src/sec/latch/LatchSymbolicCompiler.cpp @@ -0,0 +1,365 @@ +// 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_; +}; + +SymbolicBits constants(const Bits& values) { + SymbolicBits result; + result.reserve(values.size()); + for (const auto bit : values) { + if (bit > 1) throw std::invalid_argument("Non-Boolean symbolic initialization"); + result.push_back(BoolExpr::Var(bit)); + } + return result; +} + +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; +} + +void checkFinalLeaves(const SymbolicBits& roots, const SymbolicMacro& macro) { + std::unordered_set retained(macro.stateSymbols.begin(), macro.stateSymbols.end()); + retained.insert(macro.inputSymbols.begin(), macro.inputSymbols.end()); + 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 && !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()); + } + } +} + +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; + if (options_.initialInputs.size() != reference.externalInputs.size() || + options_.initialStorage.size() != reference.primitives.size()) { + throw std::invalid_argument("Explicit symbolic initialization has the wrong shape"); + } + const auto bootInputs = constants(options_.initialInputs); + std::vector bootStorage; + for (size_t i = 0; i < reference.primitives.size(); ++i) { + if (options_.initialStorage[i].size() != reference.primitives[i].storageBits) { + throw std::invalid_argument("Explicit symbolic primitive storage has the wrong width"); + } + bootStorage.push_back(constants(options_.initialStorage[i])); + } + + SymbolicMacro result; + 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.initialState = groundValues(flattenSymbolicBoundary(canonicalBoot)); + auto canonicalNext = model_.admit(boundary, input); + advance(canonicalNext, result.transitionWaves, true); + result.nextState = flattenSymbolicBoundary(canonicalNext); + result.observedNets = canonicalNext.current; + if (result.initialState.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); + checkFinalLeaves(result.nextState, result); + return result; + } + + private: + 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..e317f8a8 --- /dev/null +++ b/src/sec/latch/LatchSymbolicCompiler.h @@ -0,0 +1,48 @@ +// 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 { + Bits initialInputs; + std::vector initialStorage; + 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; + Bits initialState; + 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..a807ddb3 --- /dev/null +++ b/src/sec/latch/LatchSymbolicEncoding.cpp @@ -0,0 +1,101 @@ +// 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 + +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() || + 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..6cec9825 --- /dev/null +++ b/src/sec/latch/LatchSymbolicEncoding.h @@ -0,0 +1,14 @@ +// 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 { +// 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 index 1d628110..42bf3eb0 100644 --- a/src/sec/latch/NajaEventPrimitive.cpp +++ b/src/sec/latch/NajaEventPrimitive.cpp @@ -11,6 +11,8 @@ #include "SNLDesign.h" #include "SNLDesignModeling.h" #include "SNLInstance.h" +#include "latch/NajaSymbolicLogic.h" +#include "latch/LatchResetClock.h" namespace KEPLER_FORMAL::SEC::LATCH { namespace { @@ -24,6 +26,9 @@ using Term = naja::NL::SNLBitTerm; 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) { @@ -150,7 +155,8 @@ struct Table { } // namespace Primitive makeNajaEventPrimitive(naja::NL::SNLInstance* instance, std::string path, - const std::map& nets) { + const std::map& nets, + SymbolicPrimitive* symbolic, ResetClockPrimitive* resetClock) { if (!instance) throw std::runtime_error("missing leaf instance"); Primitive primitive; primitive.name = std::move(path); @@ -206,6 +212,23 @@ Primitive makeNajaEventPrimitive(naja::NL::SNLInstance* instance, std::string pa 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) { @@ -225,12 +248,33 @@ Primitive makeNajaEventPrimitive(naja::NL::SNLInstance* instance, std::string pa throw std::runtime_error("truth table exceeds event index width"); tables.push_back(std::move(table)); } - primitive.react = [tables = std::move(tables)](const Bits&, const Bits&, const Bits& input, + 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 : tables) result.outputs.push_back(table(input)); + 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; } diff --git a/src/sec/latch/NajaEventPrimitive.h b/src/sec/latch/NajaEventPrimitive.h index 301c11ec..d4ee6278 100644 --- a/src/sec/latch/NajaEventPrimitive.h +++ b/src/sec/latch/NajaEventPrimitive.h @@ -7,8 +7,11 @@ 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); + 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/SequentialDesignModel.h b/src/sec/model/SequentialDesignModel.h index d39a51e6..25219e67 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; @@ -67,6 +69,10 @@ struct SequentialDesignModel { // LCOV_EXCL_LINE // 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/strategy/SequentialEquivalenceStrategy.cpp b/src/sec/strategy/SequentialEquivalenceStrategy.cpp index 1cbcee77..956073e2 100644 --- a/src/sec/strategy/SequentialEquivalenceStrategy.cpp +++ b/src/sec/strategy/SequentialEquivalenceStrategy.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -41,6 +42,7 @@ #include "kinduction/SatEncoding.h" #include "model/SequentialDesignModel.h" #include "latch/LatchEventContract.h" +#include "latch/LatchResetAdapter.h" #include "pdr/PDREngine.h" #include "proof/DualRailEncoding.h" #include "proof/TransitionExprResolver.h" @@ -831,7 +833,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 @@ -895,8 +899,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()) { @@ -904,7 +913,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 @@ -923,7 +932,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=" @@ -3738,6 +3747,23 @@ 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); diff --git a/test/python/CMakeLists.txt b/test/python/CMakeLists.txt index 259f0187..f544fec8 100644 --- a/test/python/CMakeLists.txt +++ b/test/python/CMakeLists.txt @@ -40,3 +40,18 @@ target_link_libraries(kepler-borrowed-policy-tests PRIVATE kepler_borrowed_desig 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..6f614629 --- /dev/null +++ b/test/python/borrowed_latch_options_tests.cpp @@ -0,0 +1,186 @@ +// 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.enabled = true; + 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() { + check(!BorrowedLatchOptions{}.validated(false, true).enabled, "default must be disabled"); + 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(); + invalid(options, "requires explicit"); + } + 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; + 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) { + 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"); + if (options.enabled) check(options.initialInputs.has_value() && options.initialStorage.has_value(), + "parser invented incomplete contract"); + } +} + +void pythonOptions() { + const std::string good = "{'latch_support': True, 'latch_input_changes': 'single', " + "'latch_initial_inputs': 0, 'latch_initial_storage': 0}"; + parsed("{}", true, nullptr, false, true); + parsed("{'latch_support': False}", true); + parsed(good, true); + 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}", false, PyExc_ValueError); + 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}", 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'}", false, PyExc_ValueError); + parsed("{'latch_initial_inputs': 0}", false, PyExc_ValueError); + parsed("{'latch_initial_storage': 0}", false, PyExc_ValueError); + parsed("{'latch_support': True}", false, PyExc_ValueError); +} +} // 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..a7312347 --- /dev/null +++ b/test/python/borrowed_latch_tests.cpp @@ -0,0 +1,169 @@ +// 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; // Incomplete: inheriting this would fail extraction. + 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.coveredOutputs == 0, "default unexpectedly enabled latch semantics"); + unchanged(); + + options.latchSupport.enabled = true; + 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(); + check(verifyBorrowedDesigns(first, second, options, result) != 0 && + result.status == RunStatus::Error && result.reason.find("requires explicit") != std::string::npos, + "incomplete event contract accepted"); + unchanged(); + 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/test_latch_api.py b/test/python/test_latch_api.py new file mode 100644 index 00000000..fe78a562 --- /dev/null +++ b/test/python/test_latch_api.py @@ -0,0 +1,135 @@ +# 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, enabled=True, **changes): + values = dict(mode="sec", sec_engine="k_induction", sec_encoding="binary", + log_file=Path(self.temporary.name) / "verification.log") + if enabled: + values.update(latch_support=True, 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_then_enabled_then_default_do_not_leak(self): + default = verify_designs(self.first, self.second, options=self.options(False)) + 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(0, repeated.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_invalid_contract_preserves_live_designs(self): + for changes in (dict(latch_initial_storage=None), 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..3a07473a --- /dev/null +++ b/test/python/test_latch_native.py @@ -0,0 +1,83 @@ +# 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_support=True, 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_gate_defaults_off_and_accepts_explicit_false(self): + for options in ({}, {"latch_support": False}): + self.rejected(options, TypeError, "design1 must be a NativeDesign") + + def test_native_tuning_does_not_enable_gate(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}, ValueError, "requires latch_support") + + def test_native_requires_complete_contract(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, ValueError, "requires explicit") + + 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..11b9e4b4 --- /dev/null +++ b/test/python/test_latch_options.py @@ -0,0 +1,146 @@ +# 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_support=True, latch_input_changes="single", + latch_initial_inputs=0, latch_initial_storage=0) + return VerificationOptions(**(values | changes)) + + def test_default_is_disabled_without_an_invented_contract(self): + for options in (None, VerificationOptions(), VerificationOptions(mode="sec")): + result = _build_native_design_options(options) + self.assertIs(result["latch_support"], False) + 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_tuning_does_not_enable_support(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), self.assertRaisesRegex(ValueError, "requires latch_support"): + _build_native_design_options(VerificationOptions(mode="sec", **{key: value})) + + def test_enabled_requires_each_contract_field(self): + for key in ("latch_input_changes", "latch_initial_inputs", "latch_initial_storage"): + with self.subTest(key=key), self.assertRaisesRegex(ValueError, "requires explicit"): + _build_native_design_options(self.contract(**{key: None})) + + 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_when_enabled(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]) + with self.assertRaisesRegex(ValueError, "requires latch_support"): + _build_native_design_options(VerificationOptions(mode="sec", **{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/sec/BUILD.bazel b/test/sec/BUILD.bazel index 6ad32d77..9cff9919 100644 --- a/test/sec/BUILD.bazel +++ b/test/sec/BUILD.bazel @@ -29,9 +29,18 @@ cc_test( name = "SequentialEquivalenceStrategyTests", srcs = [ "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", diff --git a/test/sec/CMakeLists.txt b/test/sec/CMakeLists.txt index 9686dff7..c64add6e 100644 --- a/test/sec/CMakeLists.txt +++ b/test/sec/CMakeLists.txt @@ -4,7 +4,16 @@ add_executable(secStrategyTests 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 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/LatchNetlistAdapterTests.cpp b/test/sec/LatchNetlistAdapterTests.cpp index 91fc2d4f..9f58362f 100644 --- a/test/sec/LatchNetlistAdapterTests.cpp +++ b/test/sec/LatchNetlistAdapterTests.cpp @@ -469,6 +469,9 @@ TEST_F(LatchNetlistAdapterTests, ProofEnginesFindDifferentPhysicalLatchOutputs) 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); } } @@ -614,14 +617,14 @@ TEST_F(LatchNetlistAdapterTests, ExtractedModelsCannotMixBooleanInitializationCo EXPECT_NE(result.reason.find("initialization contracts"), std::string::npos); } -TEST_F(LatchNetlistAdapterTests, ExtractedEventModelRejectsCycleCountedResetBootstrap) { +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-cycle reset"), std::string::npos); + EXPECT_NE(result.reason.find("clock"), std::string::npos); } TEST_F(LatchNetlistAdapterTests, Btor2ExportRetainsEventStepAndInitializationMetadata) { diff --git a/test/sec/LatchResetAdapterTests.cpp b/test/sec/LatchResetAdapterTests.cpp new file mode 100644 index 00000000..41d714ed --- /dev/null +++ b/test/sec/LatchResetAdapterTests.cpp @@ -0,0 +1,403 @@ +// 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); + } +} + +} // 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..28f3025c --- /dev/null +++ b/test/sec/LatchResetIntegrationTests.cpp @@ -0,0 +1,412 @@ +// 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; + 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/LatchSymbolicCompilerTests.cpp b/test/sec/LatchSymbolicCompilerTests.cpp new file mode 100644 index 00000000..434bd344 --- /dev/null +++ b/test/sec/LatchSymbolicCompilerTests.cpp @@ -0,0 +1,342 @@ +// 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; + } + + 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.clear(); + 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..42177854 --- /dev/null +++ b/test/sec/LatchSymbolicIntegrationTests.cpp @@ -0,0 +1,188 @@ +// 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 "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); +} +} // 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/strategies/miter/BUILD.bazel b/test/strategies/miter/BUILD.bazel index b6842af6..c8c652cc 100644 --- a/test/strategies/miter/BUILD.bazel +++ b/test/strategies/miter/BUILD.bazel @@ -37,6 +37,7 @@ cc_test( name = "LatchEventCliTests", srcs = [ "LatchEventConfigTests.cpp", + "LatchResetCliTests.cpp", "LibertyLatchModelsTests.cpp", "OpaquePolicyCliTests.cpp", ], diff --git a/test/strategies/miter/CMakeLists.txt b/test/strategies/miter/CMakeLists.txt index 61f17806..253b2362 100644 --- a/test/strategies/miter/CMakeLists.txt +++ b/test/strategies/miter/CMakeLists.txt @@ -30,6 +30,7 @@ add_executable(keplerFormalCliTests 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 index 29f99791..6de921be 100644 --- a/test/strategies/miter/LatchEventConfigTests.cpp +++ b/test/strategies/miter/LatchEventConfigTests.cpp @@ -130,7 +130,8 @@ TEST(LatchEventConfigTests, AnyChangesAndExplicitHighInitializationAreAccepted) 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}", error)); + "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); @@ -139,6 +140,9 @@ TEST(LatchEventConfigTests, AnyChangesAndExplicitHighInitializationAreAccepted) 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) { @@ -166,7 +170,8 @@ TEST(LatchEventConfigTests, RejectsBadEnumsAndNonBinaryInitialization) { TEST(LatchEventConfigTests, RejectsNegativeOverflowAndZeroResourceLimits) { for (const auto* argument : {"--sec-latch-max-waves", "--sec-latch-max-states", - "--sec-latch-max-transactions"}) { + "--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; @@ -180,14 +185,34 @@ TEST(LatchEventConfigTests, RejectsNegativeOverflowAndZeroResourceLimits) { EXPECT_EQ(parseArgument(config, {"--sec-latch-workers", "0"}, error), Result::Parsed); } +TEST(LatchEventConfigTests, SymbolicBudgetsAreCheckedAndDoNotEnableGate) { + 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_FALSE(config.options().enabled); + EXPECT_FALSE(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_FALSE(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_FALSE(config.validate(true, true, false, error)); - EXPECT_NE(error.find("reset cycles"), std::string::npos); + 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); } @@ -313,7 +338,7 @@ TEST_F(LatchEventCliTests, FlagsAreAcceptedBeforeAndAfterFormat) { } } -TEST_F(LatchEventCliTests, RejectsLecResetCyclesAndSelectedLeafBoundaries) { +TEST_F(LatchEventCliTests, RejectsLecClocklessResetCyclesAndSelectedLeafBoundaries) { const std::vector> incompatible{ {"-v", "lec"}, {"-v", "sec", "--sec-reset-cycles", "1", "--sec-reset-port", "e=1"}, diff --git a/test/strategies/miter/LatchResetCliTests.cpp b/test/strategies/miter/LatchResetCliTests.cpp new file mode 100644 index 00000000..1b65313a --- /dev/null +++ b/test/strategies/miter/LatchResetCliTests.cpp @@ -0,0 +1,212 @@ +// 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) { + // 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\nlatch_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); + EXPECT_FALSE(KEPLER_FORMAL::SEC::LATCH::supportOptions().enabled); + EXPECT_FALSE(KEPLER_FORMAL::SEC::LATCH::supportOptions().initialInputs.has_value()); + } + } +} + +} // namespace From 4e29b1e10d1cfd75e670d68f1441ff107787a769 Mon Sep 17 00:00:00 2001 From: Noam Cohen Date: Fri, 25 Sep 2026 18:32:05 +0200 Subject: [PATCH 04/10] fix(sec): repair latch CI memory and dependency failures --- CMakeLists.txt | 6 ++- bazel/deps.bzl | 4 +- src/sec/pdr/PDREngine.cpp | 5 ++- test/sec/BUILD.bazel | 1 + test/sec/CMakeLists.txt | 1 + test/sec/PdrTernaryMemoTests.cpp | 65 ++++++++++++++++++++++++++++++++ 6 files changed, 77 insertions(+), 5 deletions(-) create mode 100644 test/sec/PdrTernaryMemoTests.cpp 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/bazel/deps.bzl b/bazel/deps.bzl index 99d67f81..02217876 100644 --- a/bazel/deps.bzl +++ b/bazel/deps.bzl @@ -33,7 +33,7 @@ _FLEX_VERSION = "2.6.4" _CADICAL_COMMIT = "7b99c07f0bcab5824a5a3ce62c7066554017f641" _GLUCOSE_COMMIT = "7f887abba7cf13636a5ac2d28653668a20a91b25" _KISSAT_COMMIT = "8af8e56f174b778aef3aa45af9f739b2a5f492c2" -_NAJA_COMMIT = "83be8a9e9fc7683de50c22a91f7352a72b3783dd" +_NAJA_COMMIT = "1d33c24c408f975aac901dc91cf316e02fe6dc82" _NAJA_VERILOG_COMMIT = "5da040bb34f0e4e5bb8d67223b999a0132fb401f" _NAJA_IF_COMMIT = "099677d9f52c0db11b12c08d03e32543eebc7888" _SLANG_COMMIT = "512c327c209d3043aa98ecfd02d06a1b73fcd5fb" @@ -156,7 +156,7 @@ def _deps_impl(_module_ctx): http_archive( name = "naja", url = "https://github.com/nanocoh/naja/archive/{}.tar.gz".format(_NAJA_COMMIT), - sha256 = "818723897f0db8e19796db9ea7b6af2175ec724253a83e977361b40b92c98f78", + sha256 = "29ee47d9621d9bf597ede5cb8dcd90e502953b86156550c923f79d03002fb634", strip_prefix = "naja-{}".format(_NAJA_COMMIT), build_file = Label("//bazel:naja.BUILD.bazel"), patch_args = ["-p0", "-f"], diff --git a/src/sec/pdr/PDREngine.cpp b/src/sec/pdr/PDREngine.cpp index 840179bf..35d08cdf 100644 --- a/src/sec/pdr/PDREngine.cpp +++ b/src/sec/pdr/PDREngine.cpp @@ -5356,7 +5356,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/test/sec/BUILD.bazel b/test/sec/BUILD.bazel index 9cff9919..06195e19 100644 --- a/test/sec/BUILD.bazel +++ b/test/sec/BUILD.bazel @@ -28,6 +28,7 @@ cc_test( cc_test( name = "SequentialEquivalenceStrategyTests", srcs = [ + "PdrTernaryMemoTests.cpp", "LatchBoundaryEncodingTests.cpp", "LatchConstantNetTests.cpp", "LatchEventModelTests.cpp", diff --git a/test/sec/CMakeLists.txt b/test/sec/CMakeLists.txt index c64add6e..6564c0fa 100644 --- a/test/sec/CMakeLists.txt +++ b/test/sec/CMakeLists.txt @@ -2,6 +2,7 @@ # SPDX-License-Identifier: Apache-2.0 add_executable(secStrategyTests + PdrTernaryMemoTests.cpp OpaquePolicyTests.cpp LatchEventModelTests.cpp LatchConstantNetTests.cpp 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 From bc09a98046eb6eb2e60c4d69c4bbed6b66956d47 Mon Sep 17 00:00:00 2001 From: Noam Cohen Date: Fri, 25 Sep 2026 18:32:21 +0200 Subject: [PATCH 05/10] feat(sec): enable latch support by default without implicit assumptions --- docs/sec-latch-implementation.md | 61 ++++++++------- docs/sec-latch-support.md | 29 ++++--- docs/sec-reset-bootstrap.md | 10 ++- src/bin/KeplerFormal.cpp | 11 ++- src/bin/LatchEventConfig.cpp | 8 +- src/python/BorrowedLatchOptions.cpp | 9 ++- src/python/BorrowedLatchOptions.h | 4 +- src/python/kepler_formal/_latch_options.py | 7 +- src/python/kepler_formal/api.py | 10 ++- src/sec/latch/LatchNetlistAdapter.cpp | 1 + src/sec/latch/LatchSupportOptions.h | 8 +- test/python/borrowed_latch_options_tests.cpp | 31 ++++++-- test/python/borrowed_latch_tests.cpp | 13 +++- test/python/test_latch_api.py | 15 +++- test/python/test_latch_native.py | 12 +-- test/python/test_latch_options.py | 26 ++++--- test/sec/LatchNetlistAdapterTests.cpp | 13 +++- .../miter/LatchEventConfigTests.cpp | 77 +++++++++++++++---- test/strategies/miter/LatchResetCliTests.cpp | 16 +++- 19 files changed, 253 insertions(+), 108 deletions(-) diff --git a/docs/sec-latch-implementation.md b/docs/sec-latch-implementation.md index b14b2052..f714a56b 100644 --- a/docs/sec-latch-implementation.md +++ b/docs/sec-latch-implementation.md @@ -8,18 +8,22 @@ SEC integration. It is not a timing-accurate model of arbitrary asynchronous circuits. Optional phase abstraction and stronger scheduling reductions are not required by, or enabled in, this implementation. -The master switch is `latch_support: true` in YAML or `--latch_support` on the -command line. It is **off by default**. New latch extraction and supplemental -Liberty latch modeling are behind this switch; leaving it off preserves the -existing SEC path and its opaque-latch behavior. In particular, with -`latch_support` off, `sec_reset` uses the unchanged legacy reset-bootstrap path; -none of the event adapter's clock-discovery, initialization, or single-reset-port -requirements apply. +The master switch `latch_support` is **on by default**. Disable it with YAML +`latch_support: false`, CLI `--no-latch_support`, or Python `latch_support=False`. +Default enablement does not invent an initialization or input-event contract: +with no latch-event settings supplied, the existing SEC path and opaque-latch +behavior remain in use. New latch extraction and supplemental Liberty latch +modeling require the complete explicit contract below. With the switch off, or +with no event contract supplied, `sec_reset` uses the unchanged legacy +reset-bootstrap path; none of the event adapter's clock-discovery, +initialization, or single-reset-port requirements apply. Ordinary LEC behavior +also remains unchanged. ## 1. Enabling the model requires an explicit contract The master switch does not select an initialization or input-event assumption. -An enabled run must also provide all three fields below: +To activate event modeling, provide all three fields below; an explicit +`latch_support: true` is optional because it is already the default: ```yaml format: verilog @@ -27,7 +31,6 @@ verification: sec input_paths: [reference.v, implementation.v] liberty_files: [cells.lib] -latch_support: true sec_latch_events: input_changes: any initial_inputs: 0 @@ -42,17 +45,20 @@ contract, not a proof that arbitrary power-up state reaches reset. | Purpose | YAML | Command-line option | | --- | --- | --- | -| Master enable, default off | `latch_support: true` | `--latch_support` | +| Master enable, default on | `latch_support: true` | `--latch_support` | +| Explicitly disable latch support | `latch_support: false` | `--no-latch_support` | | Allowed external transactions | `sec_latch_events.input_changes: any` or `single` | `--sec-latch-events any` or `single` | | Initial value of all external inputs | `sec_latch_events.initial_inputs: 0` or `1` | `--sec-latch-initial-inputs 0` or `1` | | Initial value of all primitive storage bits | `sec_latch_events.initial_storage: 0` or `1` | `--sec-latch-initial-storage 0` or `1` | | Optional strict opacity policy, default off | `error_on_opaque: true` | `--error-on-opaque` | -The `sec_latch_events` fields and `--sec-latch-*` tuning flags do **not** enable -latch support by themselves. Providing tuning while the master switch is off -is rejected, as is an enabled run with an incomplete contract. The strict -opaque policy is independent of the master latch switch: -it can also be used with ordinary SEC. +Providing the complete contract activates event modeling under the default-on +switch. Providing any contract or tuning settings while explicitly disabling +the switch is rejected. Partial contracts, including resource tuning without +all three required contract fields, are also rejected; they do not fall back +silently. Supplying no latch-event settings at all preserves the legacy path. +The strict opaque policy is independent of the master latch switch: it can +also be used with ordinary SEC. ### `any` and `single` are different verification assumptions @@ -481,11 +487,12 @@ does not justify restricting the event or reset-input contract. ## 7. Opacity, errors, and coverage -With latch support disabled, the existing extraction path remains in use. -With it enabled, a certified component is modeled; an unsupported or uncertified -component remains opaque, with reasons and affected output skipping. Independent -supported outputs can still be checked. Skipped outputs are not proved, and a -partial result is not a claim that an excluded nonsettling component terminates. +With latch support disabled, or with no event contract supplied, the existing +extraction path remains in use. With it enabled and a complete contract supplied, +a certified component is modeled; an unsupported or uncertified component remains +opaque, with reasons and affected output skipping. Independent supported outputs +can still be checked. Skipped outputs are not proved, and a partial result is not +a claim that an excluded nonsettling component terminates. Diagnostics distinguish a proven nonsettling cycle, distinct complete boundary results, an invalid reference/primitive case, exhausted resources, and a settling @@ -504,14 +511,13 @@ inherit a caller's ambient event contract or leak their settings back to it. ## 8. Borrowed C++ and Python APIs -The Python interface uses the same default-off gate and explicit contract: +The Python interface uses the same default-on gate and explicit contract: ```python from kepler_formal import VerificationOptions options = VerificationOptions( mode="sec", - latch_support=True, latch_input_changes="single", latch_initial_inputs=0, latch_initial_storage=0, @@ -521,9 +527,12 @@ options = VerificationOptions( Optional fields are `latch_workers`, `latch_max_waves`, `latch_max_states`, `latch_max_transactions`, `latch_max_symbolic_nodes`, `latch_max_sat_conflicts`, and `latch_max_sat_decisions`. `latch_workers=0` selects automatic parallelism; -proof budgets must be positive. Invalid types, missing contract fields, tuning -without enablement, non-SEC mode, and selected leaf boundaries are rejected. -The independent `error_on_opaque` option remains default-off. +proof budgets must be positive. Invalid types, partial contracts, tuning while +explicitly disabled, and event settings in non-SEC mode or with selected leaf +boundaries are rejected. With no event settings supplied, ordinary SEC/LEC +behavior is retained. Use `latch_support=False` to explicitly disable the feature; +it must not be combined with event settings. The independent `error_on_opaque` +option remains default-off. The native C++ equivalent is `BorrowedDesignOptions::latchSupport`, a validated `BorrowedLatchOptions` object. Borrowed APIs consume the caller's explicit Naja @@ -537,7 +546,7 @@ after success or failure. Matching-runtime native and Python tests run through | File | Responsibility | | --- | --- | | `src/bin/LatchEventConfig.*` | Master enable, explicit contract, tuning, and CLI/YAML validation | -| `src/bin/LibertyLatchModels.*` | Opt-in supplemental scalar Liberty latch descriptions | +| `src/bin/LibertyLatchModels.*` | Supplemental scalar Liberty latch descriptions for explicitly configured event modeling | | `src/sec/latch/NajaEventPrimitive.*` | Copy supported Naja primitive expressions into immutable callbacks | | `src/sec/latch/LatchConstantNet.h` | Read-only resolution of driverless Boolean constants, preserving conflict diagnostics | | `src/sec/latch/LatchEventModel.*` | Boolean BOOT/admission/wave semantics, complete state, parallel primitive evaluation | diff --git a/docs/sec-latch-support.md b/docs/sec-latch-support.md index 5fa26160..6c4488ee 100644 --- a/docs/sec-latch-support.md +++ b/docs/sec-latch-support.md @@ -1,8 +1,9 @@ # Proposed SEC Latch Support Status: architectural design and conditional correctness arguments, with the -deterministic Boolean-event path implemented behind the default-off `latch_support` -switch. The implementation, explicit admission/initialization contract, symbolic +deterministic Boolean-event path implemented behind the default-on `latch_support` +switch, with an explicit event contract still required to activate that path. +The implementation, explicit admission/initialization contract, symbolic certifier, finite fallback, and scoped limitations are documented separately in [SEC Latch Event Implementation](sec-latch-implementation.md). Optional phase abstraction and stronger scheduling reductions remain extensions. This document records the @@ -61,9 +62,11 @@ flowchart TD Current documented behavior is described in [SEC Sequential Models](sec-sequential-models.md) and -[SEC Clock Handling](sec-clock-handling.md). With `latch_support` disabled, generic -latch outputs remain opaque. The opt-in path models only certified behavior. -Existing clock handling also has explicit limits on cross-domain cones. +[SEC Clock Handling](sec-clock-handling.md). With `latch_support` disabled, or with +no latch-event settings supplied, generic latch outputs remain opaque. The switch +is enabled by default, but event modeling still requires explicit input-event and +initialization settings; it models only certified behavior. Existing clock +handling also has explicit limits on cross-domain cones. This proposal would extend those semantics; it is not merely an extraction optimization. In particular, modeling independent latch enables requires more @@ -749,9 +752,15 @@ unsupported; see the implementation document for the exact reset input-arrival contract and diagnostics. Stage 7 remains optional, as originally proposed. See the implementation document for exact supported frontend and resource boundaries. -The entire latch path remains behind `latch_support`, default-off. With it off, -reset uses the existing bootstrap implementation unchanged; the event adapter's -additional clock/reset restrictions do not apply to legacy runs. +The entire latch path remains behind `latch_support`, default-on. Explicit YAML +`latch_support: false`, CLI `--no-latch_support`, or Python `latch_support=False` +disables it. Default enablement does not assume initial values or input timing: +without any event settings, legacy SEC/LEC behavior is preserved. A complete +contract activates event modeling; partial contracts or tuning without the +required fields are errors, as is tuning while explicitly disabled. With the +switch off or no event settings supplied, reset uses the existing bootstrap +implementation unchanged; the event adapter's additional clock/reset +restrictions do not apply to legacy runs. 1. Encode and review the explicit primitive tables, Boolean bootstrap, shared environment, and observation contract specified here; supply missing frontend @@ -959,8 +968,8 @@ The previously open construction is now specialized as follows: These close the logical construction under the stated hypotheses; they are not a machine-checked theorem or a proof that arbitrary asynchronous hardware -satisfies those hypotheses. The opt-in implementation now instantiates the -primitive tables, bootstrap, reference and symbolic compilers, certificates, +satisfies those hypotheses. The explicitly configured event implementation now +instantiates the primitive tables, bootstrap, reference and symbolic compilers, certificates, dependency regions, resource limits, and SEC reset/export adapters, with tests described in [the implementation document](sec-latch-implementation.md). Tests and per-component SAT certificates do not establish that this Boolean diff --git a/docs/sec-reset-bootstrap.md b/docs/sec-reset-bootstrap.md index f0622a19..df401d01 100644 --- a/docs/sec-reset-bootstrap.md +++ b/docs/sec-reset-bootstrap.md @@ -4,10 +4,12 @@ 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. -This behavior is unchanged when `latch_support` is off (the default). With -`latch_support` on, `cycles` still counts clock cycles, but an explicit event -adapter generates the clock edges and latch settling. Its supported clock/reset -subset and stimulus protocol are described in the +This behavior is unchanged when `latch_support` is off or no latch-event settings +are supplied. The switch is on by default, but event modeling requires an explicit +complete input-event/initialization contract. With that contract active, `cycles` +still counts clock cycles, but an explicit event adapter generates the clock +edges and latch settling. Its supported clock/reset subset and stimulus protocol +are described in the [latch reset-cycle adapter](sec-latch-implementation.md#reset-cycle-adapter). ## YAML diff --git a/src/bin/KeplerFormal.cpp b/src/bin/KeplerFormal.cpp index b8c78c58..3aaf7582 100644 --- a/src/bin/KeplerFormal.cpp +++ b/src/bin/KeplerFormal.cpp @@ -84,12 +84,13 @@ static void print_usage(const char* prog) { "[--report-skipped-pos] [--error-on-opaque] " "[--dump-btor2 ] [--dump-only] (BTOR2 export requires SEC)", prog); - SPDLOG_INFO("Boolean event SEC (off by default): --latch_support --sec-latch-events " + SPDLOG_INFO("Latch support is on by default (--no-latch_support disables it). Boolean event SEC requires: --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 ]. " - "Normal steps are settled external events; sec_reset counts clock cycles."); + "Without an event contract, latches stay opaque and legacy behavior is unchanged. " + "Normal event-mode steps are settled external events; sec_reset counts clock cycles."); // LCOV_EXCL_START } // LCOV_EXCL_STOP @@ -2354,10 +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.enabled) { + 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 ? 1 : 0, *latch.initialStorage ? 1 : 0); + } else if (latch.enabled) { + SPDLOG_INFO("SEC latch_support: enabled; no explicit event contract, retaining legacy extraction and opaque latch cones"); } if (secResetSpec.enabled()) { SPDLOG_INFO("SEC reset bootstrap: {} cycle(s)", secResetSpec.cycles); @@ -2598,7 +2601,7 @@ static int KeplerFormalMainImpl( std::filesystem::path libraryPath(libraryFile); SPDLOG_INFO("Loading library file: {}", libraryFile); SNLLibertyConstructor constructor(primitivesLibrary); - if (latchEventConfig.options().enabled) + if (latchEventConfig.options().hasEventContract()) KEPLER_FORMAL::constructLibertyWithLatchModels(primitivesLibrary, libraryPath); else constructor.construct(libraryPath); } diff --git a/src/bin/LatchEventConfig.cpp b/src/bin/LatchEventConfig.cpp index 6b267eac..9be03217 100644 --- a/src/bin/LatchEventConfig.cpp +++ b/src/bin/LatchEventConfig.cpp @@ -78,8 +78,9 @@ bool LatchEventConfig::parseYaml(const YAML::Node& config, std::string& error) { LatchEventConfig::ArgumentResult LatchEventConfig::parseArgument( int argc, char** argv, int& index, std::string& error) { - if (std::string_view(argv[index]) == "--latch_support") { - options_.enabled = true; + 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{ @@ -107,6 +108,9 @@ bool LatchEventConfig::validate(bool isSec, bool hasResetCycles, bool hasLeafBou error = "latch event tuning requires latch_support: true or --latch_support"; return false; } + // The default enables the capability, not assumptions about initial state. + // Existing workflows without an event contract keep their legacy semantics. + if (!explicitTuning_) return true; if (!isSec) error = "latch event options require SEC verification"; else if (!inputChangesExplicit_ || !options_.initialInputs || !options_.initialStorage) error = "latch events require explicit input_changes, initial_inputs and initial_storage"; diff --git a/src/python/BorrowedLatchOptions.cpp b/src/python/BorrowedLatchOptions.cpp index 30fa71f8..1f54e40a 100644 --- a/src/python/BorrowedLatchOptions.cpp +++ b/src/python/BorrowedLatchOptions.cpp @@ -10,13 +10,18 @@ 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; if (!enabled) { - if (inputChanges || initialInputs || initialStorage || workers || maxWaves || - maxStates || maxTransactions || maxSymbolicNodes || maxSatConflicts || maxSatDecisions) { + if (hasTuning) { throw std::invalid_argument("latch event tuning requires latch_support=true"); } return result; } + // No supplied event contract keeps legacy LEC/SEC semantics, including + // opaque latches. Do not silently constrain initial inputs or stored state. + if (!hasTuning) return result; if (!isSec) throw std::invalid_argument("latch_support is only supported for SEC"); if (!inputChanges || !initialInputs || !initialStorage) { throw std::invalid_argument( diff --git a/src/python/BorrowedLatchOptions.h b/src/python/BorrowedLatchOptions.h index 635616c2..03ed56c3 100644 --- a/src/python/BorrowedLatchOptions.h +++ b/src/python/BorrowedLatchOptions.h @@ -11,9 +11,9 @@ namespace KEPLER_FORMAL { enum class LatchInputChanges { Any, Single }; // Optional members distinguish an explicitly stated event contract from an -// omitted setting. Merely specifying tuning never enables latch support. +// omitted setting. Default-on support never invents an initial-state contract. struct BorrowedLatchOptions { - bool enabled = false; + bool enabled = true; std::optional inputChanges; std::optional initialInputs; std::optional initialStorage; diff --git a/src/python/kepler_formal/_latch_options.py b/src/python/kepler_formal/_latch_options.py index bdd95576..09096c19 100644 --- a/src/python/kepler_formal/_latch_options.py +++ b/src/python/kepler_formal/_latch_options.py @@ -1,7 +1,7 @@ # Copyright 2026 keplertech.io # SPDX-License-Identifier: Apache-2.0 -"""Validation of the explicit, default-off latch event contract.""" +"""Validation of default-on latch support with an explicit event contract.""" import ctypes @@ -41,9 +41,12 @@ def build_latch_options(settings, *, mode: str, has_boundaries: bool) -> dict: 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 any(value is not None for value in values.values()): + if has_tuning: raise ValueError("latch event tuning requires latch_support=True") + elif not has_tuning: + pass # Preserve legacy extraction without inventing initialization. elif mode != "sec": raise ValueError("latch_support is only supported for SEC") elif any(values[name] is None for name in names[:3]): diff --git a/src/python/kepler_formal/api.py b/src/python/kepler_formal/api.py index ee5dee06..e8c7d2be 100644 --- a/src/python/kepler_formal/api.py +++ b/src/python/kepler_formal/api.py @@ -51,9 +51,11 @@ class VerificationOptions: models have no child instances. Hierarchical paths to leaves are valid; selecting a nonleaf instance is rejected. - ``latch_support`` is disabled by default. Enabling it requires SEC, an - explicit ``latch_input_changes`` contract (``"any"`` or ``"single"``), - and initial inputs/storage given as integer 0 or 1. Each SEC step then + ``latch_support`` is enabled by default. Without event settings, legacy + LEC/SEC behavior is preserved and latches remain opaque. Modeling latches + requires SEC, an explicit ``latch_input_changes`` contract (``"any"`` or + ``"single"``), and initial inputs/storage given as integer 0 or 1. No + initial values are assumed implicitly. Each SEC step then represents an external transaction followed by settling, not a clock cycle. It consumes declared Naja models; it never infers cells by name. """ @@ -73,7 +75,7 @@ class VerificationOptions: learn_internal_relations: bool = True allow_x_equality_in_internal_relations: bool = True error_on_opaque: bool = False - latch_support: bool = False + latch_support: bool = True latch_input_changes: str | None = None latch_initial_inputs: int | None = None latch_initial_storage: int | None = None diff --git a/src/sec/latch/LatchNetlistAdapter.cpp b/src/sec/latch/LatchNetlistAdapter.cpp index 53d23468..d1a01276 100644 --- a/src/sec/latch/LatchNetlistAdapter.cpp +++ b/src/sec/latch/LatchNetlistAdapter.cpp @@ -396,6 +396,7 @@ std::optional extractEventDesign( naja::NL::SNLDesign* top, const BoundaryPairs& pairs, size_t side) { const auto& options = supportOptions(); if (!options.enabled) return {}; + if (!options.initialInputs && !options.initialStorage) return {}; if (!options.initialInputs || !options.initialStorage || !pairs.empty()) { SequentialDesignModel result; result.unsupportedReasons.push_back(!pairs.empty() diff --git a/src/sec/latch/LatchSupportOptions.h b/src/sec/latch/LatchSupportOptions.h index 62dcf343..874d91cd 100644 --- a/src/sec/latch/LatchSupportOptions.h +++ b/src/sec/latch/LatchSupportOptions.h @@ -12,7 +12,7 @@ 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 = false; + bool enabled = true; bool singleInputChange = false; std::optional initialInputs; std::optional initialStorage; @@ -21,6 +21,12 @@ struct SupportOptions { size_t maxSymbolicNodes = 2000000; unsigned maxSatConflicts = 500000; unsigned maxSatDecisions = 5000000; + + // Enabling support never invents power-up values. Without an explicit + // contract, extraction retains the legacy model and opaque latch cones. + bool hasEventContract() const { + return enabled && initialInputs.has_value() && initialStorage.has_value(); + } }; const SupportOptions& supportOptions(); diff --git a/test/python/borrowed_latch_options_tests.cpp b/test/python/borrowed_latch_options_tests.cpp index 6f614629..b7ebb88d 100644 --- a/test/python/borrowed_latch_options_tests.cpp +++ b/test/python/borrowed_latch_options_tests.cpp @@ -18,7 +18,6 @@ void check(bool value, const std::string& detail) { BorrowedLatchOptions contract() { BorrowedLatchOptions options; - options.enabled = true; options.inputChanges = LatchInputChanges::Single; options.initialInputs = false; options.initialStorage = false; @@ -36,7 +35,16 @@ void invalid(const BorrowedLatchOptions& options, const char* message, } void cppOptions() { - check(!BorrowedLatchOptions{}.validated(false, true).enabled, "default must be disabled"); + for (bool sec : {false, true}) { + for (bool boundaries : {false, true}) { + auto defaults = BorrowedLatchOptions{}.validated(sec, boundaries); + check(defaults.enabled && !defaults.initialInputs && !defaults.initialStorage, + "default-on support invented an event contract"); + 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 && @@ -82,6 +90,8 @@ void cppOptions() { } options = {}; options.workers = 0; + invalid(options, "requires explicit"); + options.enabled = false; invalid(options, "requires latch_support"); for (int field = 0; field < 3; ++field) { options = contract(); @@ -103,7 +113,7 @@ void cppOptions() { } void parsed(const std::string& expression, bool success, PyObject* exception = nullptr, - bool sec = true, bool boundaries = false) { + 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); @@ -119,17 +129,23 @@ void parsed(const std::string& expression, bool success, PyObject* exception = n PyErr_Clear(); } else { check(!PyErr_Occurred(), "success retained Python exception"); - if (options.enabled) check(options.initialInputs.has_value() && options.initialStorage.has_value(), - "parser invented incomplete contract"); + check(options.enabled == expectedEnabled, "parser changed the master gate"); + check(options.inputChanges.has_value() == options.initialInputs.has_value() && + options.initialInputs.has_value() == options.initialStorage.has_value(), + "parser invented incomplete contract"); } } void pythonOptions() { - const std::string good = "{'latch_support': True, 'latch_input_changes': 'single', " + const std::string good = "{'latch_input_changes': 'single', " "'latch_initial_inputs': 0, 'latch_initial_storage': 0}"; parsed("{}", true, nullptr, false, true); - parsed("{'latch_support': 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); @@ -165,7 +181,6 @@ void pythonOptions() { parsed("{'latch_input_changes': 'any'}", false, PyExc_ValueError); parsed("{'latch_initial_inputs': 0}", false, PyExc_ValueError); parsed("{'latch_initial_storage': 0}", false, PyExc_ValueError); - parsed("{'latch_support': True}", false, PyExc_ValueError); } } // namespace diff --git a/test/python/borrowed_latch_tests.cpp b/test/python/borrowed_latch_tests.cpp index a7312347..e93ab1e4 100644 --- a/test/python/borrowed_latch_tests.cpp +++ b/test/python/borrowed_latch_tests.cpp @@ -73,7 +73,7 @@ void run(const std::filesystem::path& directory) { const auto firstReference = first->getReference(); const auto secondReference = second->getReference(); SEC::LATCH::SupportOptions ambient; - ambient.enabled = true; // Incomplete: inheriting this would fail extraction. + ambient.enabled = true; ambient.workers = 7; SEC::LATCH::ScopedSupportOptions ambientScope(ambient); @@ -93,10 +93,17 @@ void run(const std::filesystem::path& directory) { "borrowed run leaked its event contract"); }; verifyBorrowedDesigns(first, second, options, result); - check(result.coveredOutputs == 0, "default unexpectedly enabled latch semantics"); + check(result.status == RunStatus::Unsupported && result.coveredOutputs == 0, + "default did not preserve opaque-latch behavior"); unchanged(); - options.latchSupport.enabled = true; + 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; diff --git a/test/python/test_latch_api.py b/test/python/test_latch_api.py index fe78a562..374aa306 100644 --- a/test/python/test_latch_api.py +++ b/test/python/test_latch_api.py @@ -47,11 +47,11 @@ def make_top(self, name, model): cell.getInstTerm(pin).setNet(net) return top - def options(self, enabled=True, **changes): + 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 enabled: - values.update(latch_support=True, latch_input_changes="single", + if with_contract: + values.update(latch_input_changes="single", latch_initial_inputs=0, latch_initial_storage=0) return VerificationOptions(**(values | changes)) @@ -66,6 +66,7 @@ def unchanged(self): def test_default_then_enabled_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()) @@ -74,9 +75,17 @@ def test_default_then_enabled_then_default_do_not_leak(self): 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)): diff --git a/test/python/test_latch_native.py b/test/python/test_latch_native.py index 3a07473a..bd8667dd 100644 --- a/test/python/test_latch_native.py +++ b/test/python/test_latch_native.py @@ -10,7 +10,7 @@ class NativeLatchOptionsTest(unittest.TestCase): def contract(self, **changes): - return dict(mode="sec", latch_support=True, latch_input_changes="single", + return dict(mode="sec", latch_input_changes="single", latch_initial_inputs=0, latch_initial_storage=0) | changes def rejected(self, options, error, message): @@ -21,17 +21,19 @@ 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_gate_defaults_off_and_accepts_explicit_false(self): - for options in ({}, {"latch_support": False}): + 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_tuning_does_not_enable_gate(self): + def test_native_tuning_requires_contract_and_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}, ValueError, "requires latch_support") + self.rejected({"mode": "sec", key: value}, ValueError, "requires explicit") + self.rejected({"mode": "sec", "latch_support": False, key: value}, + ValueError, "requires latch_support") def test_native_requires_complete_contract(self): for key in ("latch_input_changes", "latch_initial_inputs", "latch_initial_storage"): diff --git a/test/python/test_latch_options.py b/test/python/test_latch_options.py index 11b9e4b4..5d94b31e 100644 --- a/test/python/test_latch_options.py +++ b/test/python/test_latch_options.py @@ -11,14 +11,15 @@ class LatchOptionsTest(unittest.TestCase): def contract(self, **changes): - values = dict(mode="sec", latch_support=True, latch_input_changes="single", + values = dict(mode="sec", latch_input_changes="single", latch_initial_inputs=0, latch_initial_storage=0) return VerificationOptions(**(values | changes)) - def test_default_is_disabled_without_an_invented_contract(self): - for options in (None, VerificationOptions(), VerificationOptions(mode="sec")): + 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"], False) + self.assertIs(result["latch_support"], True) for key in ("latch_input_changes", "latch_initial_inputs", "latch_initial_storage"): self.assertIsNone(result[key]) @@ -38,13 +39,17 @@ def test_any_and_single_and_all_boolean_initial_values(self): self.assertEqual(inputs, result["latch_initial_inputs"]) self.assertEqual(storage, result["latch_initial_storage"]) - def test_tuning_does_not_enable_support(self): + def test_tuning_requires_a_contract_and_cannot_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), self.assertRaisesRegex(ValueError, "requires latch_support"): - _build_native_design_options(VerificationOptions(mode="sec", **{key: value})) + with self.subTest(key=key): + with self.assertRaisesRegex(ValueError, "requires explicit"): + _build_native_design_options(VerificationOptions(mode="sec", **{key: value})) + with self.assertRaisesRegex(ValueError, "requires latch_support"): + _build_native_design_options(VerificationOptions( + mode="sec", latch_support=False, **{key: value})) def test_enabled_requires_each_contract_field(self): for key in ("latch_input_changes", "latch_initial_inputs", "latch_initial_storage"): @@ -55,7 +60,7 @@ 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_when_enabled(self): + 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"]) @@ -118,8 +123,11 @@ 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]) - with self.assertRaisesRegex(ValueError, "requires latch_support"): + with self.assertRaisesRegex(ValueError, "requires explicit"): _build_native_design_options(VerificationOptions(mode="sec", **{key: 123})) + 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): diff --git a/test/sec/LatchNetlistAdapterTests.cpp b/test/sec/LatchNetlistAdapterTests.cpp index 9f58362f..b5def3b7 100644 --- a/test/sec/LatchNetlistAdapterTests.cpp +++ b/test/sec/LatchNetlistAdapterTests.cpp @@ -183,6 +183,8 @@ class LatchNetlistAdapterTests : public ::testing::Test { }; TEST_F(LatchNetlistAdapterTests, DefaultOptionsLeaveExistingExtractionUntouched) { + EXPECT_TRUE(supportOptions().enabled); + EXPECT_FALSE(supportOptions().hasEventContract()); auto* top = directTop("top", NLDB0::getDLatch()); EXPECT_FALSE(extractEventDesign(top, {}, 0).has_value()); const auto model = SequentialDesignModel::extract(top); @@ -200,18 +202,23 @@ TEST_F(LatchNetlistAdapterTests, ExplicitContractRejectsUnspecifiedInitializatio } TEST_F(LatchNetlistAdapterTests, ScopedOptionsRestorePreviousSemanticContract) { - EXPECT_FALSE(supportOptions().enabled); + EXPECT_TRUE(supportOptions().enabled); + EXPECT_FALSE(supportOptions().hasEventContract()); { ScopedSupportOptions outer(options()); EXPECT_TRUE(supportOptions().enabled); { - ScopedSupportOptions inner(SupportOptions{}); + 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_FALSE(supportOptions().enabled); + EXPECT_TRUE(supportOptions().enabled); + EXPECT_FALSE(supportOptions().hasEventContract()); } TEST_F(LatchNetlistAdapterTests, Db0LatchFollowsOpenDataAndRetainsClosingValue) { diff --git a/test/strategies/miter/LatchEventConfigTests.cpp b/test/strategies/miter/LatchEventConfigTests.cpp index 6de921be..58f97850 100644 --- a/test/strategies/miter/LatchEventConfigTests.cpp +++ b/test/strategies/miter/LatchEventConfigTests.cpp @@ -34,13 +34,15 @@ Result parseArgument(LatchEventConfig& config, std::vector args, constexpr auto complete = "{input_changes: single, initial_inputs: 0, initial_storage: 0}"; -TEST(LatchEventConfigTests, DisabledByDefaultWithoutAnImplicitContract) { +TEST(LatchEventConfigTests, EnabledByDefaultWithoutAnImplicitContract) { LatchEventConfig config; std::string error; EXPECT_TRUE(config.parseYaml(YAML::Load("{}"), error)); - EXPECT_FALSE(config.options().enabled); + EXPECT_TRUE(config.options().enabled); EXPECT_FALSE(config.options().initialInputs.has_value()); + EXPECT_FALSE(config.options().hasEventContract()); EXPECT_TRUE(config.validate(false, false, false, error)); + EXPECT_TRUE(config.validate(true, true, true, error)); EXPECT_EQ(parseArgument(config, {"--unrelated"}, error), Result::NotHandled); } @@ -52,28 +54,41 @@ TEST(LatchEventConfigTests, ExplicitMasterOffPreservesLegacyWithoutTuning) { EXPECT_TRUE(config.validate(false, true, true, error)); } -TEST(LatchEventConfigTests, EventTuningNeverEnablesTheMasterGate) { - for (const auto* gate : {"", "latch_support: false\n"}) { +TEST(LatchEventConfigTests, CompleteEventContractUsesDefaultEnableUnlessExplicitlyDisabled) { + for (const bool disabled : {false, true}) { LatchEventConfig config; std::string error; - ASSERT_TRUE(config.parseYaml(YAML::Load(std::string(gate) + "sec_latch_events: " + complete), error)); - EXPECT_FALSE(config.options().enabled); - EXPECT_FALSE(config.validate(true, false, false, error)); - EXPECT_NE(error.find("latch_support"), std::string::npos); + 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_EQ(config.options().hasEventContract(), !disabled); + 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_FALSE(config.options().enabled); + EXPECT_TRUE(config.options().enabled); EXPECT_FALSE(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().hasEventContract()); +} + 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_FALSE(config.options().enabled); + EXPECT_TRUE(config.options().enabled); EXPECT_NE(error.find("latch_support"), std::string::npos); } } @@ -83,8 +98,8 @@ TEST(LatchEventConfigTests, MasterEnableAloneDoesNotInventAnEventContract) { std::string error; ASSERT_EQ(parseArgument(config, {"--latch_support"}, error), Result::Parsed); EXPECT_TRUE(config.options().enabled); - EXPECT_FALSE(config.validate(true, false, false, error)); - EXPECT_NE(error.find("explicit input_changes"), std::string::npos); + EXPECT_TRUE(config.validate(true, false, false, error)); + EXPECT_FALSE(config.options().hasEventContract()); ASSERT_EQ(parseArgument(config, {"--sec-latch-initial-inputs", "0"}, error), Result::Parsed); ASSERT_EQ(parseArgument(config, {"--sec-latch-initial-storage", "0"}, error), Result::Parsed); EXPECT_FALSE(config.validate(true, false, false, error)); @@ -185,13 +200,14 @@ TEST(LatchEventConfigTests, RejectsNegativeOverflowAndZeroResourceLimits) { EXPECT_EQ(parseArgument(config, {"--sec-latch-workers", "0"}, error), Result::Parsed); } -TEST(LatchEventConfigTests, SymbolicBudgetsAreCheckedAndDoNotEnableGate) { +TEST(LatchEventConfigTests, SymbolicBudgetsAreCheckedAndDoNotSupplyAnEventContract) { 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_FALSE(config.options().enabled); + EXPECT_TRUE(config.options().enabled); + EXPECT_FALSE(config.options().hasEventContract()); EXPECT_FALSE(config.validate(true, false, false, error)); } for (const auto* key : {"max_symbolic_nodes", "max_sat_conflicts", "max_sat_decisions"}) { @@ -278,11 +294,21 @@ TEST_F(LatchEventCliTests, YamlModelsFourTransparentLatchesInAChain) { EXPECT_EQ(result.coveredOutputs, 1u); } -TEST_F(LatchEventCliTests, RejectsTuningWithoutMasterGateInBothFrontends) { +TEST_F(LatchEventCliTests, ModelsWithDefaultEnableAndRejectsExplicitDisableWithTuning) { auto arguments = options(); - arguments.erase(arguments.begin()); // Keep tuning, deliberately omit master. + 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"); @@ -303,6 +329,23 @@ TEST_F(LatchEventCliTests, MasterDisabledRetainsOpaqueLatchesAndIndependentStric EXPECT_NE(strict.reason.find("error-on-opaque"), std::string::npos); } +TEST_F(LatchEventCliTests, DisableFlagBeforeAndAfterFormatMatchesDefaultWithoutContract) { + 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, implicit.skippedObservedOutputs); + } +} + TEST_F(LatchEventCliTests, SelfFeedbackRetainsExplicitInitialStorage) { const auto result = config("self.v", "any"); EXPECT_EQ(result.status, KEPLER_FORMAL::RunStatus::Equivalent) << result.reason; @@ -361,7 +404,7 @@ TEST_F(LatchEventCliTests, GzipLibraryIsModeledInCompactMode) { EXPECT_EQ(result.coveredOutputs, 1u); } -TEST_F(LatchEventCliTests, WorkflowRestoresOuterEventContractAndDisablesItByDefault) { +TEST_F(LatchEventCliTests, WorkflowRestoresOuterEventContractWithoutInheritingIt) { namespace Latch = KEPLER_FORMAL::SEC::LATCH; Latch::SupportOptions original; original.enabled = true; diff --git a/test/strategies/miter/LatchResetCliTests.cpp b/test/strategies/miter/LatchResetCliTests.cpp index 1b65313a..10993b63 100644 --- a/test/strategies/miter/LatchResetCliTests.cpp +++ b/test/strategies/miter/LatchResetCliTests.cpp @@ -171,12 +171,14 @@ TEST_F(LatchResetCliTests, MultipleResetPortsAreRejectedExplicitly) { } TEST_F(LatchResetCliTests, OffOnOffPreservesLegacyResetSemanticsForEveryEngine) { - const auto legacy = [&](const std::string& engine, const std::string& second) { + 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\nlatch_support: false\n" + "\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" @@ -203,7 +205,15 @@ TEST_F(LatchResetCliTests, OffOnOffPreservesLegacyResetSemanticsForEveryEngine) 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); - EXPECT_FALSE(KEPLER_FORMAL::SEC::LATCH::supportOptions().enabled); + 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().hasEventContract()); EXPECT_FALSE(KEPLER_FORMAL::SEC::LATCH::supportOptions().initialInputs.has_value()); } } From f370271ebab27d7af218f6c0f8420533db969b0e Mon Sep 17 00:00:00 2001 From: Noam Cohen Date: Fri, 25 Sep 2026 23:10:36 +0200 Subject: [PATCH 06/10] fix(sec): preserve symbolic latch initialization without forced defaults --- docs/sec-latch-implementation.md | 111 ++++--- docs/sec-latch-support.md | 5 +- docs/sec-reset-bootstrap.md | 6 +- src/bin/KeplerFormal.cpp | 13 +- src/bin/LatchEventConfig.cpp | 16 +- src/bin/LatchEventConfig.h | 2 - src/python/BorrowedLatchOptions.cpp | 18 +- src/python/BorrowedLatchOptions.h | 3 +- src/python/kepler_formal/_latch_options.py | 12 +- src/python/kepler_formal/api.py | 15 +- src/sec/BUILD.bazel | 2 + src/sec/CMakeLists.txt | 1 + src/sec/export/SecBtor2Exporter.cpp | 8 +- .../imc/CraigInterpolatingModelChecker.cpp | 14 +- src/sec/imc/IMCEngine.cpp | 10 +- src/sec/kinduction/BaseCaseSolver.cpp | 14 +- src/sec/kinduction/InductionStepSolver.cpp | 4 +- src/sec/kinduction/KInductionProblem.h | 10 +- src/sec/latch/LatchInitialState.cpp | 106 +++++++ src/sec/latch/LatchInitialState.h | 32 ++ src/sec/latch/LatchNetlistAdapter.cpp | 138 ++++++++- src/sec/latch/LatchNetlistAdapter.h | 4 +- src/sec/latch/LatchResetAdapter.cpp | 42 ++- src/sec/latch/LatchSupportOptions.h | 7 +- src/sec/latch/LatchSymbolicCompiler.cpp | 84 +++-- src/sec/latch/LatchSymbolicCompiler.h | 14 + src/sec/latch/LatchSymbolicEncoding.cpp | 44 ++- src/sec/latch/LatchSymbolicEncoding.h | 8 + src/sec/model/SequentialDesignModel.h | 6 + src/sec/pdr/PDREngine.cpp | 2 + src/sec/proof/ProofEngineShared.cpp | 8 +- .../SequentialEquivalenceStrategy.cpp | 20 +- test/python/borrowed_latch_options_tests.cpp | 35 ++- test/python/borrowed_latch_tests.cpp | 26 +- test/python/test_latch_api.py | 28 +- test/python/test_latch_native.py | 8 +- test/python/test_latch_options.py | 22 +- test/sec/BUILD.bazel | 1 + test/sec/CMakeLists.txt | 1 + test/sec/LatchNetlistAdapterTests.cpp | 148 ++++++++- test/sec/LatchResetAdapterTests.cpp | 27 ++ test/sec/LatchResetIntegrationTests.cpp | 1 + test/sec/LatchSymbolicCompilerTests.cpp | 143 +++++++++ test/sec/LatchSymbolicIntegrationTests.cpp | 48 +++ test/sec/OpaquePolicyTests.cpp | 4 + test/sec/RelationalInitializationTests.cpp | 286 ++++++++++++++++++ .../SequentialEquivalenceStrategyTests.cpp | 7 + .../miter/LatchEventConfigTests.cpp | 78 +++-- test/strategies/miter/LatchResetCliTests.cpp | 39 ++- 49 files changed, 1467 insertions(+), 214 deletions(-) create mode 100644 src/sec/latch/LatchInitialState.cpp create mode 100644 src/sec/latch/LatchInitialState.h create mode 100644 test/sec/RelationalInitializationTests.cpp diff --git a/docs/sec-latch-implementation.md b/docs/sec-latch-implementation.md index f714a56b..c3f0b5a0 100644 --- a/docs/sec-latch-implementation.md +++ b/docs/sec-latch-implementation.md @@ -10,20 +10,16 @@ required by, or enabled in, this implementation. The master switch `latch_support` is **on by default**. Disable it with YAML `latch_support: false`, CLI `--no-latch_support`, or Python `latch_support=False`. -Default enablement does not invent an initialization or input-event contract: -with no latch-event settings supplied, the existing SEC path and opaque-latch -behavior remain in use. New latch extraction and supplemental Liberty latch -modeling require the complete explicit contract below. With the switch off, or -with no event contract supplied, `sec_reset` uses the unchanged legacy -reset-bootstrap path; none of the event adapter's clock-discovery, -initialization, or single-reset-port requirements apply. Ordinary LEC behavior -also remains unchanged. +No initialization settings are required. Designs with explicitly modeled latches +use the event path with arbitrary Boolean starting inputs and storage, and `any` +input changes. Unsupported components remain opaque. With the switch off, +`sec_reset` uses the unchanged legacy reset-bootstrap path. Latch-free designs +also retain that path unless event settings are explicitly supplied. Ordinary +LEC behavior remains unchanged. -## 1. Enabling the model requires an explicit contract +## 1. Defaults and optional restrictions -The master switch does not select an initialization or input-event assumption. -To activate event modeling, provide all three fields below; an explicit -`latch_support: true` is optional because it is already the default: +No `sec_latch_events` map or explicit `latch_support: true` is needed: ```yaml format: verilog @@ -31,17 +27,15 @@ verification: sec input_paths: [reference.v, implementation.v] liberty_files: [cells.lib] -sec_latch_events: - input_changes: any - initial_inputs: 0 - initial_storage: 0 ``` -`initial_inputs` sets **every external input** to the specified Boolean value -before initialization. `initial_storage` sets **every modeled primitive storage -bit** to its specified value. Each accepts only `0` or `1`; these are separate -choices, not per-port mappings. They describe a particular initial-state -contract, not a proof that arbitrary power-up state reaches reset. +Each unspecified input and storage bit retains both Boolean possibilities, +including mixed initial values. Matching external input levels are shared between +the designs; internal storage origins are independent, not assumed equal. +Optional `initial_inputs` and `initial_storage` restrictions remain available +only when explicitly requested: each fixes all bits of its category to `0` or +`1`, independently of whether the other setting is supplied. Neither is a reset. +Without `sec_reset`, no reset sequence is inserted. | Purpose | YAML | Command-line option | | --- | --- | --- | @@ -52,17 +46,15 @@ contract, not a proof that arbitrary power-up state reaches reset. | Initial value of all primitive storage bits | `sec_latch_events.initial_storage: 0` or `1` | `--sec-latch-initial-storage 0` or `1` | | Optional strict opacity policy, default off | `error_on_opaque: true` | `--error-on-opaque` | -Providing the complete contract activates event modeling under the default-on -switch. Providing any contract or tuning settings while explicitly disabling -the switch is rejected. Partial contracts, including resource tuning without -all three required contract fields, are also rejected; they do not fall back -silently. Supplying no latch-event settings at all preserves the legacy path. +Event and resource settings can be supplied independently; no initialization +field is mandatory. Explicit event tuning also selects the event path for a +latch-free design. Tuning while explicitly disabling the switch is rejected. The strict opaque policy is independent of the master latch switch: it can also be used with ordinary SEC. ### `any` and `single` are different verification assumptions -- `any` permits every Boolean valuation of a component's external inputs at +- `any` (the default) permits every Boolean valuation of a component's external inputs at each transaction, including several simultaneous changes and no change. - `single` permits at most one original top-level input bit to change per transaction, including no change. It is an explicit restriction on the @@ -117,8 +109,6 @@ The existing reset configuration can be used with latch support: latch_support: true sec_latch_events: input_changes: single - initial_inputs: 0 - initial_storage: 0 sec_reset: cycles: 3 ports: @@ -215,7 +205,7 @@ argument. ## 3. Extraction and primitive modeling -The ordinary Liberty reader is augmented, only in the enabled file-based path, +The ordinary Liberty reader is augmented, only in the enabled SEC file-based path, with explicit scalar `latch` groups. The supplemental reader preserves data, enable, asynchronous clear/preset, conflict behavior, and physical output expressions. An integrated clock gate is supported through its actual latch @@ -274,7 +264,8 @@ arbitrary independently settling local islands or final-output-only summaries. Initialization is itself a checked settling episode: -1. Install the explicit initial external values and primitive storage values. +1. Give each unspecified external input and primitive storage bit an independent + symbolic Boolean origin. Apply a concrete value only if explicitly specified. 2. Initialize physical storage outputs from their model; universally quantify auxiliary internal net seeds rather than choosing convenient values. The finite reference compiler enumerates them; the symbolic compiler uses @@ -286,17 +277,42 @@ Initialization is itself a checked settling episode: invented edge. Generated clock changes from that update remain real modeled events in subsequent waves. 5. Certify that all auxiliary seed choices and permitted event orders settle - to the same complete boundary for the prescribed intended initialization. + to the same complete boundary for **each** intended input/storage origin. + Different genuine origins may produce different boundaries; uniqueness is + required only across artificial seeds and event orders for the same origin. When a physical output's initial projection reads input pins, certification also quantifies seeds of other storage-output nets that the projection can read before they are overwritten. Otherwise those hidden seed choices could determine the retained result. -The standalone compiler can enumerate unspecified initial storage and retain -every origin mapping. The first integrated CLI path instead requires explicit -fixed Boolean input/storage settings and one initialized boundary per component; -it does not select a favorable member of an unspecified initial relation. +The integrated symbolic compiler preserves the settled boundary as a function of +the genuine origins. SEC receives that exact initial relation, plus any derived +constant facts; it never chooses one favorable boundary. All engines and BTOR2 +must retain the relation even when constant initial facts also exist. The initial +relation applies only at frame zero. A reset prefix is composed only when +`sec_reset` is configured, and storage that reset does not determine remains +unspecified. In particular, a closed, unreset latch is not forced to zero merely +because other cells have reset. + +This event model remains Boolean: unspecified storage means an arbitrary fixed +Boolean starting value, not a literal Verilog X. In dual-rail encoding its initial +rails are complementary symbolic bits, not the `11` encoding of X. Explicit X/Z +behavior remains unsupported here; legacy dual-rail initialization is unchanged +when the event path is inactive. Independent unreset storage can therefore +produce a startup mismatch even for identical designs; no hidden equality or +automatic reset is assumed to make such a comparison pass. + +Symbolic starts can also enlarge the proof problem. In particular, IMC can return +inconclusive on a reset-plus-transparent-latch example that KI and PDR prove: +its exact reachable-state path has a small state-count limit and its larger +interpolation path is resource bounded. This is not a requirement to provide +initial values. No initialization restriction is added to obtain a proof. + +The finite fallback is currently used only for fully concrete BOOT inputs and +storage. If symbolic certification cannot establish the required settling +properties with unspecified origins, the component remains opaque; no origins +are discarded to make a certificate succeed. Within an ordinary wave, activated primitives read the same frozen snapshot. A sequential primitive processes every permitted ordering of changed input @@ -487,9 +503,11 @@ does not justify restricting the event or reset-input contract. ## 7. Opacity, errors, and coverage -With latch support disabled, or with no event contract supplied, the existing -extraction path remains in use. With it enabled and a complete contract supplied, -a certified component is modeled; an unsupported or uncertified component remains +With latch support disabled, the existing extraction path remains in use. +Latch-free designs also retain that path unless event settings are supplied. +With support enabled, designs containing modeled latches use event extraction +without requiring initialization settings. A certified component is modeled; +an unsupported or uncertified component remains opaque, with reasons and affected output skipping. Independent supported outputs can still be checked. Skipped outputs are not proved, and a partial result is not a claim that an excluded nonsettling component terminates. @@ -511,26 +529,24 @@ inherit a caller's ambient event contract or leak their settings back to it. ## 8. Borrowed C++ and Python APIs -The Python interface uses the same default-on gate and explicit contract: +The Python interface uses the same default-on gate and symbolic starts: ```python from kepler_formal import VerificationOptions options = VerificationOptions( mode="sec", - latch_input_changes="single", - latch_initial_inputs=0, - latch_initial_storage=0, ) ``` Optional fields are `latch_workers`, `latch_max_waves`, `latch_max_states`, `latch_max_transactions`, `latch_max_symbolic_nodes`, `latch_max_sat_conflicts`, and `latch_max_sat_decisions`. `latch_workers=0` selects automatic parallelism; -proof budgets must be positive. Invalid types, partial contracts, tuning while +proof budgets must be positive. `latch_input_changes`, `latch_initial_inputs`, +and `latch_initial_storage` are independently optional restrictions. Invalid types, tuning while explicitly disabled, and event settings in non-SEC mode or with selected leaf -boundaries are rejected. With no event settings supplied, ordinary SEC/LEC -behavior is retained. Use `latch_support=False` to explicitly disable the feature; +boundaries are rejected. Ordinary LEC and latch-free SEC retain legacy behavior +without event tuning. Use `latch_support=False` to explicitly disable the feature; it must not be combined with event settings. The independent `error_on_opaque` option remains default-off. @@ -546,7 +562,8 @@ after success or failure. Matching-runtime native and Python tests run through | File | Responsibility | | --- | --- | | `src/bin/LatchEventConfig.*` | Master enable, explicit contract, tuning, and CLI/YAML validation | -| `src/bin/LibertyLatchModels.*` | Supplemental scalar Liberty latch descriptions for explicitly configured event modeling | +| `src/bin/LibertyLatchModels.*` | Supplemental scalar Liberty latch descriptions when SEC latch support is enabled | +| `src/sec/latch/LatchInitialState.*` | Exact symbolic BOOT relations and shared initial external levels in SEC | | `src/sec/latch/NajaEventPrimitive.*` | Copy supported Naja primitive expressions into immutable callbacks | | `src/sec/latch/LatchConstantNet.h` | Read-only resolution of driverless Boolean constants, preserving conflict diagnostics | | `src/sec/latch/LatchEventModel.*` | Boolean BOOT/admission/wave semantics, complete state, parallel primitive evaluation | diff --git a/docs/sec-latch-support.md b/docs/sec-latch-support.md index 6c4488ee..6cedbd84 100644 --- a/docs/sec-latch-support.md +++ b/docs/sec-latch-support.md @@ -2,8 +2,9 @@ Status: architectural design and conditional correctness arguments, with the deterministic Boolean-event path implemented behind the default-on `latch_support` -switch, with an explicit event contract still required to activate that path. -The implementation, explicit admission/initialization contract, symbolic +switch. Starting inputs and storage remain symbolic unless explicitly constrained; +initialization settings are not required and reset is never inserted implicitly. +The implementation, admission/initial-state semantics, symbolic certifier, finite fallback, and scoped limitations are documented separately in [SEC Latch Event Implementation](sec-latch-implementation.md). Optional phase abstraction and stronger scheduling reductions remain extensions. This document records the diff --git a/docs/sec-reset-bootstrap.md b/docs/sec-reset-bootstrap.md index df401d01..0048384e 100644 --- a/docs/sec-reset-bootstrap.md +++ b/docs/sec-reset-bootstrap.md @@ -4,9 +4,9 @@ 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. -This behavior is unchanged when `latch_support` is off or no latch-event settings -are supplied. The switch is on by default, but event modeling requires an explicit -complete input-event/initialization contract. With that contract active, `cycles` +This behavior is unchanged when `latch_support` is off, or for latch-free designs +without explicit event tuning. The switch is on by default and requires no +initialization settings. With event modeling active, `cycles` still counts clock cycles, but an explicit event adapter generates the clock edges and latch settling. Its supported clock/reset subset and stimulus protocol are described in the diff --git a/src/bin/KeplerFormal.cpp b/src/bin/KeplerFormal.cpp index 3aaf7582..b5fd9654 100644 --- a/src/bin/KeplerFormal.cpp +++ b/src/bin/KeplerFormal.cpp @@ -84,12 +84,12 @@ static void print_usage(const char* prog) { "[--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). Boolean event SEC requires: --sec-latch-events " - "--sec-latch-initial-inputs <0|1> --sec-latch-initial-storage <0|1> " + 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 ]. " - "Without an event contract, latches stay opaque and legacy behavior is unchanged. " + "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 } @@ -2358,9 +2358,8 @@ static int KeplerFormalMainImpl( 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 ? 1 : 0, *latch.initialStorage ? 1 : 0); - } else if (latch.enabled) { - SPDLOG_INFO("SEC latch_support: enabled; no explicit event contract, retaining legacy extraction and opaque latch cones"); + 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); @@ -2601,7 +2600,7 @@ static int KeplerFormalMainImpl( std::filesystem::path libraryPath(libraryFile); SPDLOG_INFO("Loading library file: {}", libraryFile); SNLLibertyConstructor constructor(primitivesLibrary); - if (latchEventConfig.options().hasEventContract()) + if (verificationMode == VerificationMode::SEC && latchEventConfig.options().enabled) KEPLER_FORMAL::constructLibertyWithLatchModels(primitivesLibrary, libraryPath); else constructor.construct(libraryPath); } diff --git a/src/bin/LatchEventConfig.cpp b/src/bin/LatchEventConfig.cpp index 9be03217..3d8808f3 100644 --- a/src/bin/LatchEventConfig.cpp +++ b/src/bin/LatchEventConfig.cpp @@ -16,10 +16,9 @@ bool number(const std::string& text, size_t& value) { } } bool LatchEventConfig::set(const std::string& key, const std::string& value, std::string& error) { - explicitTuning_ = true; + options_.explicitConfiguration = true; if (key == "input_changes") { if (value == "any" || value == "single") { - inputChangesExplicit_ = true; options_.singleInputChange = value == "single"; return true; } @@ -64,7 +63,7 @@ bool LatchEventConfig::parseYaml(const YAML::Node& config, std::string& error) { } const auto node = config["sec_latch_events"]; if (!node) return true; - explicitTuning_ = 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()) { @@ -104,16 +103,15 @@ bool LatchEventConfig::validate(bool isSec, bool hasResetCycles, bool hasLeafBou std::string& error) const { (void)hasResetCycles; // Clock discovery and cycle expansion need the extracted models. if (!options_.enabled) { - if (!explicitTuning_) return true; + if (!options_.explicitConfiguration) return true; error = "latch event tuning requires latch_support: true or --latch_support"; return false; } - // The default enables the capability, not assumptions about initial state. - // Existing workflows without an event contract keep their legacy semantics. - if (!explicitTuning_) return true; + // 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 (!inputChangesExplicit_ || !options_.initialInputs || !options_.initialStorage) - error = "latch events require explicit input_changes, initial_inputs and initial_storage"; else if (hasLeafBoundaries) error = "latch events currently require the complete top interface, not selected leaf boundaries"; else return true; diff --git a/src/bin/LatchEventConfig.h b/src/bin/LatchEventConfig.h index 04ea02cc..ae9e4133 100644 --- a/src/bin/LatchEventConfig.h +++ b/src/bin/LatchEventConfig.h @@ -14,8 +14,6 @@ class LatchEventConfig { const SEC::LATCH::SupportOptions& options() const { return options_; } private: SEC::LATCH::SupportOptions options_; - bool explicitTuning_ = false; - bool inputChangesExplicit_ = false; bool set(const std::string& key, const std::string& value, std::string& error); }; } // namespace KEPLER_FORMAL diff --git a/src/python/BorrowedLatchOptions.cpp b/src/python/BorrowedLatchOptions.cpp index 1f54e40a..5dde8481 100644 --- a/src/python/BorrowedLatchOptions.cpp +++ b/src/python/BorrowedLatchOptions.cpp @@ -13,24 +13,22 @@ SEC::LATCH::SupportOptions BorrowedLatchOptions::validated( 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; } - // No supplied event contract keeps legacy LEC/SEC semantics, including - // opaque latches. Do not silently constrain initial inputs or stored state. - if (!hasTuning) return result; - if (!isSec) throw std::invalid_argument("latch_support is only supported for SEC"); - if (!inputChanges || !initialInputs || !initialStorage) { - throw std::invalid_argument( - "latch_support requires explicit latch_input_changes, latch_initial_inputs and latch_initial_storage"); + if (!isSec) { + if (hasTuning) throw std::invalid_argument("latch_support is only supported for SEC"); + result.enabled = false; + return result; } - if (*inputChanges != LatchInputChanges::Any && *inputChanges != LatchInputChanges::Single) { + if (inputChanges && *inputChanges != LatchInputChanges::Any && *inputChanges != LatchInputChanges::Single) { throw std::invalid_argument("latch_input_changes must be any or single"); } - if (hasLeafBoundaries) { + if (hasTuning && hasLeafBoundaries) { throw std::invalid_argument( "latch_support requires the complete top interface, not selected leaf boundaries"); } @@ -47,7 +45,7 @@ SEC::LATCH::SupportOptions BorrowedLatchOptions::validated( throw std::invalid_argument("latch SAT limits must fit a positive unsigned int"); } result.enabled = true; - result.singleInputChange = *inputChanges == LatchInputChanges::Single; + result.singleInputChange = inputChanges && *inputChanges == LatchInputChanges::Single; result.initialInputs = initialInputs; result.initialStorage = initialStorage; if (workers) result.workers = *workers; diff --git a/src/python/BorrowedLatchOptions.h b/src/python/BorrowedLatchOptions.h index 03ed56c3..a93d93ff 100644 --- a/src/python/BorrowedLatchOptions.h +++ b/src/python/BorrowedLatchOptions.h @@ -11,7 +11,8 @@ namespace KEPLER_FORMAL { enum class LatchInputChanges { Any, Single }; // Optional members distinguish an explicitly stated event contract from an -// omitted setting. Default-on support never invents an initial-state contract. +// 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; diff --git a/src/python/kepler_formal/_latch_options.py b/src/python/kepler_formal/_latch_options.py index 09096c19..1a970958 100644 --- a/src/python/kepler_formal/_latch_options.py +++ b/src/python/kepler_formal/_latch_options.py @@ -1,7 +1,7 @@ # Copyright 2026 keplertech.io # SPDX-License-Identifier: Apache-2.0 -"""Validation of default-on latch support with an explicit event contract.""" +"""Validation of default-on latch support and optional event assumptions.""" import ctypes @@ -45,14 +45,8 @@ def build_latch_options(settings, *, mode: str, has_boundaries: bool) -> dict: if not enabled: if has_tuning: raise ValueError("latch event tuning requires latch_support=True") - elif not has_tuning: - pass # Preserve legacy extraction without inventing initialization. - elif mode != "sec": + elif has_tuning and mode != "sec": raise ValueError("latch_support is only supported for SEC") - elif any(values[name] is None for name in names[:3]): - raise ValueError( - "latch_support requires explicit latch_input_changes, latch_initial_inputs and latch_initial_storage" - ) - elif has_boundaries: + 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 e8c7d2be..058b313d 100644 --- a/src/python/kepler_formal/api.py +++ b/src/python/kepler_formal/api.py @@ -51,13 +51,14 @@ class VerificationOptions: models have no child instances. Hierarchical paths to leaves are valid; selecting a nonleaf instance is rejected. - ``latch_support`` is enabled by default. Without event settings, legacy - LEC/SEC behavior is preserved and latches remain opaque. Modeling latches - requires SEC, an explicit ``latch_input_changes`` contract (``"any"`` or - ``"single"``), and initial inputs/storage given as integer 0 or 1. No - initial values are assumed implicitly. Each SEC step then - represents an external transaction followed by settling, not a clock - cycle. It consumes declared Naja models; it never infers cells by name. + ``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 diff --git a/src/sec/BUILD.bazel b/src/sec/BUILD.bazel index 0b20ef49..044f088d 100644 --- a/src/sec/BUILD.bazel +++ b/src/sec/BUILD.bazel @@ -37,6 +37,7 @@ cc_library( "latch/LatchNetlistAdapter.cpp", "latch/LatchSettlingCompiler.cpp", "latch/LatchSupportOptions.cpp", + "latch/LatchInitialState.cpp", "latch/LatchSymbolicCompiler.cpp", "latch/LatchSymbolicEncoding.cpp", "latch/LatchSymbolicModel.cpp", @@ -78,6 +79,7 @@ cc_library( "latch/LatchNetlistAdapter.h", "latch/LatchSettlingCompiler.h", "latch/LatchSupportOptions.h", + "latch/LatchInitialState.h", "latch/LatchSymbolicCompiler.h", "latch/LatchSymbolicEncoding.h", "latch/LatchSymbolicModel.h", diff --git a/src/sec/CMakeLists.txt b/src/sec/CMakeLists.txt index 949baeb5..c6cc5000 100644 --- a/src/sec/CMakeLists.txt +++ b/src/sec/CMakeLists.txt @@ -22,6 +22,7 @@ add_library(kepler_sec STATIC latch/LatchResetClock.cpp latch/LatchBoundaryEncoding.cpp latch/LatchSupportOptions.cpp + latch/LatchInitialState.cpp latch/NajaEventPrimitive.cpp latch/LatchNetlistAdapter.cpp model/SequentialDesignModel.cpp diff --git a/src/sec/export/SecBtor2Exporter.cpp b/src/sec/export/SecBtor2Exporter.cpp index 98d0d1a4..8cf78e11 100644 --- a/src/sec/export/SecBtor2Exporter.cpp +++ b/src/sec/export/SecBtor2Exporter.cpp @@ -248,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) { @@ -260,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/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/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/LatchNetlistAdapter.cpp b/src/sec/latch/LatchNetlistAdapter.cpp index d1a01276..ea0cf5ad 100644 --- a/src/sec/latch/LatchNetlistAdapter.cpp +++ b/src/sec/latch/LatchNetlistAdapter.cpp @@ -5,8 +5,8 @@ #include #include #include -#include #include +#include #include "DNL.h" #include "NLDB.h" #include "NLName.h" @@ -19,6 +19,7 @@ #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" @@ -54,6 +55,22 @@ std::string name(const Term& term) { 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 { @@ -112,8 +129,8 @@ SequentialDesignModel extract(naja::NL::SNLDesign* top, size_t side) { SequentialDesignModel model; model.eventContract = std::string("boolean-epochs-v1;") + (options.singleInputChange ? "single;" : "any;") + - "initial_inputs=" + std::to_string(*options.initialInputs) + - ";initial_storage=" + std::to_string(*options.initialStorage); + "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); @@ -246,7 +263,7 @@ SequentialDesignModel extract(naja::NL::SNLDesign* top, size_t side) { resetInterface->inputKeys.push_back(input.key); resetInterface->inputNames.push_back(input.name); } - resetInterface->currentInputs.assign(inputs.size(), BoolExpr::Var(*options.initialInputs ? 1 : 0)); + resetInterface->currentInputs.resize(inputs.size()); std::vector inputExpressions, selector; BoolExpr* eventValue = nullptr; if (!options.singleInputChange) { @@ -267,6 +284,27 @@ SequentialDesignModel extract(naja::NL::SNLDesign* top, size_t side) { 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; @@ -305,7 +343,8 @@ SequentialDesignModel extract(naja::NL::SNLDesign* top, size_t side) { std::string symbolicFailure; if (failure.empty()) { SymbolicCompileOptions compile; - compile.initialInputs.assign(local.externalInputs.size(), *options.initialInputs); + 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; @@ -313,21 +352,29 @@ SequentialDesignModel extract(naja::NL::SNLDesign* top, size_t side) { compile.maxSatDecisions = options.maxSatDecisions; compile.workers = options.workers; for (const auto& primitive : local.primitives) - compile.initialStorage.emplace_back(primitive.storageBits, uint8_t(*options.initialStorage)); + 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; - compile.initialInputs.assign(local.externalInputs.size(), *options.initialInputs); + // 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)); + 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 + @@ -353,7 +400,47 @@ SequentialDesignModel extract(naja::NL::SNLDesign* top, size_t side) { 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)); - model.initialStateValueByKey.emplace(stateKey, symbolic ? symbolic->initialState.at(bit) : (table->initials[0].boundary >> bit) & 1); + 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) @@ -369,6 +456,23 @@ SequentialDesignModel extract(naja::NL::SNLDesign* top, size_t side) { 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]) { @@ -386,7 +490,12 @@ SequentialDesignModel extract(naja::NL::SNLDesign* top, size_t side) { 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; } @@ -396,12 +505,13 @@ std::optional extractEventDesign( naja::NL::SNLDesign* top, const BoundaryPairs& pairs, size_t side) { const auto& options = supportOptions(); if (!options.enabled) return {}; - if (!options.initialInputs && !options.initialStorage) return {}; - if (!options.initialInputs || !options.initialStorage || !pairs.empty()) { + // 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(!pairs.empty() - ? "event semantics do not yet support selected leaf boundaries" - : "event semantics require explicit Boolean initial_inputs and initial_storage"); + result.unsupportedReasons.push_back("event semantics do not yet support selected leaf boundaries"); return result; } return extract(top, side); diff --git a/src/sec/latch/LatchNetlistAdapter.h b/src/sec/latch/LatchNetlistAdapter.h index 302d1955..765d6a14 100644 --- a/src/sec/latch/LatchNetlistAdapter.h +++ b/src/sec/latch/LatchNetlistAdapter.h @@ -6,8 +6,8 @@ #include "model/SequentialDesignModel.h" namespace KEPLER_FORMAL::SEC::LATCH { -// Empty only when the explicit event contract is disabled. In event mode even -// a latch-free comparison side must use the same external-event semantics. +// 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 index 5801c5ae..45c9f4ea 100644 --- a/src/sec/latch/LatchResetAdapter.cpp +++ b/src/sec/latch/LatchResetAdapter.cpp @@ -45,6 +45,42 @@ BoolExpr* mux(BoolExpr* select, BoolExpr* yes, BoolExpr* no) { 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 { @@ -248,8 +284,9 @@ std::string validate(const SequentialDesignModel& model, const SecResetSpec& res 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.initialStateValueByKey.count(key)) - return "Latch reset cycles require fully initialized event state"; + !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); @@ -383,6 +420,7 @@ ResetCycleAdaptation adaptResetCycles(const SequentialDesignModel& model, const 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()}; diff --git a/src/sec/latch/LatchSupportOptions.h b/src/sec/latch/LatchSupportOptions.h index 874d91cd..4d94561c 100644 --- a/src/sec/latch/LatchSupportOptions.h +++ b/src/sec/latch/LatchSupportOptions.h @@ -13,6 +13,8 @@ namespace KEPLER_FORMAL::SEC::LATCH { // 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; @@ -22,10 +24,9 @@ struct SupportOptions { unsigned maxSatConflicts = 500000; unsigned maxSatDecisions = 5000000; - // Enabling support never invents power-up values. Without an explicit - // contract, extraction retains the legacy model and opaque latch cones. + // Initial values are optional restrictions, never an admission requirement. bool hasEventContract() const { - return enabled && initialInputs.has_value() && initialStorage.has_value(); + return enabled; } }; diff --git a/src/sec/latch/LatchSymbolicCompiler.cpp b/src/sec/latch/LatchSymbolicCompiler.cpp index b8e7e6cc..2a8edff8 100644 --- a/src/sec/latch/LatchSymbolicCompiler.cpp +++ b/src/sec/latch/LatchSymbolicCompiler.cpp @@ -75,16 +75,6 @@ class DagBudget { std::unordered_set seen_; }; -SymbolicBits constants(const Bits& values) { - SymbolicBits result; - result.reserve(values.size()); - for (const auto bit : values) { - if (bit > 1) throw std::invalid_argument("Non-Boolean symbolic initialization"); - result.push_back(BoolExpr::Var(bit)); - } - return result; -} - BoolExpr* differs(const SymbolicBits& left, const SymbolicBits& right) { if (left.size() != right.size()) { throw std::invalid_argument("Symbolic boundary layout changed during propagation"); @@ -147,9 +137,8 @@ Bits groundValues(const SymbolicBits& roots) { return result; } -void checkFinalLeaves(const SymbolicBits& roots, const SymbolicMacro& macro) { - std::unordered_set retained(macro.stateSymbols.begin(), macro.stateSymbols.end()); - retained.insert(macro.inputSymbols.begin(), macro.inputSymbols.end()); +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()) { @@ -157,6 +146,7 @@ void checkFinalLeaves(const SymbolicBits& roots, const SymbolicMacro& macro) { 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"); } @@ -165,6 +155,7 @@ void checkFinalLeaves(const SymbolicBits& roots, const SymbolicMacro& macro) { if (node->getRight()) pending.push_back(node->getRight()); } } + return ground; } class Compiler { @@ -180,20 +171,29 @@ class Compiler { resource("Symbolic certification requires a nonzero DAG and SAT work budget"); } const auto& reference = network_.reference; - if (options_.initialInputs.size() != reference.externalInputs.size() || - options_.initialStorage.size() != reference.primitives.size()) { - throw std::invalid_argument("Explicit symbolic initialization has the wrong shape"); + 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"); } - const auto bootInputs = constants(options_.initialInputs); std::vector bootStorage; + result.initialStorageSymbols.resize(reference.primitives.size()); for (size_t i = 0; i < reference.primitives.size(); ++i) { - if (options_.initialStorage[i].size() != reference.primitives[i].storageBits) { - throw std::invalid_argument("Explicit symbolic primitive storage has the wrong width"); - } - bootStorage.push_back(constants(options_.initialStorage[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)); } - - SymbolicMacro result; result.singleExternalInputChange = options_.singleExternalInputChange; result.externalInputNets = reference.externalInputs; SymbolicBits current = variables(reference.netCount, &result.stateSymbols); @@ -242,21 +242,53 @@ class Compiler { auto canonicalBoot = model_.bootstrap(bootInputs, bootStorage, SymbolicBits(reference.netCount, BoolExpr::createFalse())); advance(canonicalBoot, result.bootstrapWaves, true); - result.initialState = groundValues(flattenSymbolicBoundary(canonicalBoot)); + 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.initialState.size() != result.stateSymbols.size() || + 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); - checkFinalLeaves(result.nextState, result); + 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"); diff --git a/src/sec/latch/LatchSymbolicCompiler.h b/src/sec/latch/LatchSymbolicCompiler.h index e317f8a8..92fde3b7 100644 --- a/src/sec/latch/LatchSymbolicCompiler.h +++ b/src/sec/latch/LatchSymbolicCompiler.h @@ -8,8 +8,14 @@ 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; @@ -22,7 +28,15 @@ 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 diff --git a/src/sec/latch/LatchSymbolicEncoding.cpp b/src/sec/latch/LatchSymbolicEncoding.cpp index a807ddb3..0897a545 100644 --- a/src/sec/latch/LatchSymbolicEncoding.cpp +++ b/src/sec/latch/LatchSymbolicEncoding.cpp @@ -57,12 +57,54 @@ SymbolicBits rewrite(const SymbolicBits& roots, const std::unordered_map 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.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 || diff --git a/src/sec/latch/LatchSymbolicEncoding.h b/src/sec/latch/LatchSymbolicEncoding.h index 6cec9825..d7025e1a 100644 --- a/src/sec/latch/LatchSymbolicEncoding.h +++ b/src/sec/latch/LatchSymbolicEncoding.h @@ -5,6 +5,14 @@ #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, diff --git a/src/sec/model/SequentialDesignModel.h b/src/sec/model/SequentialDesignModel.h index 25219e67..5a8d44ed 100644 --- a/src/sec/model/SequentialDesignModel.h +++ b/src/sec/model/SequentialDesignModel.h @@ -56,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. diff --git a/src/sec/pdr/PDREngine.cpp b/src/sec/pdr/PDREngine.cpp index 35d08cdf..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 && 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 956073e2..e8752b8a 100644 --- a/src/sec/strategy/SequentialEquivalenceStrategy.cpp +++ b/src/sec/strategy/SequentialEquivalenceStrategy.cpp @@ -42,6 +42,7 @@ #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" @@ -2531,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; } @@ -2887,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( @@ -3803,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( @@ -3813,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 " diff --git a/test/python/borrowed_latch_options_tests.cpp b/test/python/borrowed_latch_options_tests.cpp index b7ebb88d..d1c29a3d 100644 --- a/test/python/borrowed_latch_options_tests.cpp +++ b/test/python/borrowed_latch_options_tests.cpp @@ -38,8 +38,9 @@ void cppOptions() { for (bool sec : {false, true}) { for (bool boundaries : {false, true}) { auto defaults = BorrowedLatchOptions{}.validated(sec, boundaries); - check(defaults.enabled && !defaults.initialInputs && !defaults.initialStorage, - "default-on support invented an event contract"); + 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"); @@ -73,7 +74,11 @@ void cppOptions() { if (field == 0) options.inputChanges.reset(); if (field == 1) options.initialInputs.reset(); if (field == 2) options.initialStorage.reset(); - invalid(options, "requires explicit"); + 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); @@ -90,7 +95,10 @@ void cppOptions() { } options = {}; options.workers = 0; - invalid(options, "requires explicit"); + 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) { @@ -130,9 +138,11 @@ void parsed(const std::string& expression, bool success, PyObject* exception = n } else { check(!PyErr_Occurred(), "success retained Python exception"); check(options.enabled == expectedEnabled, "parser changed the master gate"); - check(options.inputChanges.has_value() == options.initialInputs.has_value() && - options.initialInputs.has_value() == options.initialStorage.has_value(), - "parser invented incomplete contract"); + 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"); } } @@ -153,7 +163,7 @@ void pythonOptions() { 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}", false, PyExc_ValueError); + 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", "[]"}) @@ -170,7 +180,8 @@ void pythonOptions() { 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}", false, PyExc_ValueError); + 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", @@ -178,9 +189,9 @@ void pythonOptions() { 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'}", false, PyExc_ValueError); - parsed("{'latch_initial_inputs': 0}", false, PyExc_ValueError); - parsed("{'latch_initial_storage': 0}", false, PyExc_ValueError); + parsed("{'latch_input_changes': 'any'}", true); + parsed("{'latch_initial_inputs': 0}", true); + parsed("{'latch_initial_storage': 0}", true); } } // namespace diff --git a/test/python/borrowed_latch_tests.cpp b/test/python/borrowed_latch_tests.cpp index e93ab1e4..740c94d8 100644 --- a/test/python/borrowed_latch_tests.cpp +++ b/test/python/borrowed_latch_tests.cpp @@ -94,7 +94,7 @@ void run(const std::filesystem::path& directory) { }; verifyBorrowedDesigns(first, second, options, result); check(result.status == RunStatus::Unsupported && result.coveredOutputs == 0, - "default did not preserve opaque-latch behavior"); + "default any-change mode certified an ambiguous latch race"); unchanged(); check(options.latchSupport.enabled, "latch support is not enabled by default"); @@ -127,10 +127,30 @@ void run(const std::filesystem::path& directory) { 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::Error && result.reason.find("requires explicit") != std::string::npos, - "incomplete event contract accepted"); + 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 && diff --git a/test/python/test_latch_api.py b/test/python/test_latch_api.py index 374aa306..7a706cb8 100644 --- a/test/python/test_latch_api.py +++ b/test/python/test_latch_api.py @@ -64,7 +64,7 @@ def unchanged(self): self.assertEqual(1, len(list(self.second.getInstances()))) self.assertTrue(self.model.hasSequentialModel()) - def test_default_then_enabled_then_default_do_not_leak(self): + 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) @@ -117,8 +117,32 @@ def test_explicit_one_initialization_is_supported(self): 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=None), dict(latch_support=False), + 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): diff --git a/test/python/test_latch_native.py b/test/python/test_latch_native.py index bd8667dd..ab5d2aaf 100644 --- a/test/python/test_latch_native.py +++ b/test/python/test_latch_native.py @@ -25,24 +25,24 @@ 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_tuning_requires_contract_and_cannot_override_explicit_false(self): + 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}, ValueError, "requires explicit") + 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_requires_complete_contract(self): + 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, ValueError, "requires explicit") + self.rejected(options, TypeError, "design1 must be a NativeDesign") def test_native_requires_real_boolean_gate(self): for value in (None, 0, 1, "true", []): diff --git a/test/python/test_latch_options.py b/test/python/test_latch_options.py index 5d94b31e..ed941c17 100644 --- a/test/python/test_latch_options.py +++ b/test/python/test_latch_options.py @@ -39,22 +39,26 @@ def test_any_and_single_and_all_boolean_initial_values(self): self.assertEqual(inputs, result["latch_initial_inputs"]) self.assertEqual(storage, result["latch_initial_storage"]) - def test_tuning_requires_a_contract_and_cannot_override_explicit_false(self): + 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): - with self.assertRaisesRegex(ValueError, "requires explicit"): - _build_native_design_options(VerificationOptions(mode="sec", **{key: value})) + 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_enabled_requires_each_contract_field(self): + 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), self.assertRaisesRegex(ValueError, "requires explicit"): - _build_native_design_options(self.contract(**{key: None})) + 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"): @@ -123,8 +127,10 @@ 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]) - with self.assertRaisesRegex(ValueError, "requires explicit"): - _build_native_design_options(VerificationOptions(mode="sec", **{key: 123})) + 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})) diff --git a/test/sec/BUILD.bazel b/test/sec/BUILD.bazel index 06195e19..8d830fd1 100644 --- a/test/sec/BUILD.bazel +++ b/test/sec/BUILD.bazel @@ -28,6 +28,7 @@ cc_test( cc_test( name = "SequentialEquivalenceStrategyTests", srcs = [ + "RelationalInitializationTests.cpp", "PdrTernaryMemoTests.cpp", "LatchBoundaryEncodingTests.cpp", "LatchConstantNetTests.cpp", diff --git a/test/sec/CMakeLists.txt b/test/sec/CMakeLists.txt index 6564c0fa..f783de12 100644 --- a/test/sec/CMakeLists.txt +++ b/test/sec/CMakeLists.txt @@ -2,6 +2,7 @@ # SPDX-License-Identifier: Apache-2.0 add_executable(secStrategyTests + RelationalInitializationTests.cpp PdrTernaryMemoTests.cpp OpaquePolicyTests.cpp LatchEventModelTests.cpp diff --git a/test/sec/LatchNetlistAdapterTests.cpp b/test/sec/LatchNetlistAdapterTests.cpp index b5def3b7..d327c2cc 100644 --- a/test/sec/LatchNetlistAdapterTests.cpp +++ b/test/sec/LatchNetlistAdapterTests.cpp @@ -9,6 +9,8 @@ #include #include #include +#include +#include #include #include "BoolExprCache.h" @@ -145,6 +147,39 @@ class LatchNetlistAdapterTests : public ::testing::Test { 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; @@ -176,34 +211,51 @@ class LatchNetlistAdapterTests : public ::testing::Test { }; 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, DefaultOptionsLeaveExistingExtractionUntouched) { +TEST_F(LatchNetlistAdapterTests, DefaultAnyChangeAttemptKeepsUnprovedRaceOpaque) { EXPECT_TRUE(supportOptions().enabled); - EXPECT_FALSE(supportOptions().hasEventContract()); + EXPECT_TRUE(supportOptions().hasEventContract()); + EXPECT_FALSE(supportOptions().initialInputs); + EXPECT_FALSE(supportOptions().initialStorage); auto* top = directTop("top", NLDB0::getDLatch()); - EXPECT_FALSE(extractEventDesign(top, {}, 0).has_value()); + 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, ExplicitContractRejectsUnspecifiedInitialization) { +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())); - EXPECT_TRUE(model.hasUnsupportedFeatures()); - EXPECT_TRUE(model.observedOutputs.empty()); + 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_FALSE(supportOptions().hasEventContract()); + EXPECT_TRUE(supportOptions().hasEventContract()); { ScopedSupportOptions outer(options()); EXPECT_TRUE(supportOptions().enabled); @@ -218,7 +270,63 @@ TEST_F(LatchNetlistAdapterTests, ScopedOptionsRestorePreviousSemanticContract) { EXPECT_TRUE(supportOptions().singleInputChange); } EXPECT_TRUE(supportOptions().enabled); - EXPECT_FALSE(supportOptions().hasEventContract()); + 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) { @@ -468,6 +576,30 @@ TEST_F(LatchNetlistAdapterTests, ProofEnginesAcceptSelfEquivalentLatchInBothEnco } } +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)); diff --git a/test/sec/LatchResetAdapterTests.cpp b/test/sec/LatchResetAdapterTests.cpp index 41d714ed..0786facd 100644 --- a/test/sec/LatchResetAdapterTests.cpp +++ b/test/sec/LatchResetAdapterTests.cpp @@ -399,5 +399,32 @@ TEST_F(LatchResetAdapterTests, CompositionBudgetFailsWithoutPublishingPartialMod } } +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/LatchResetIntegrationTests.cpp b/test/sec/LatchResetIntegrationTests.cpp index 28f3025c..a00ae8ba 100644 --- a/test/sec/LatchResetIntegrationTests.cpp +++ b/test/sec/LatchResetIntegrationTests.cpp @@ -273,6 +273,7 @@ TEST_F(LatchResetIntegrationTests, ResetSensitivePhysicalSequentialOutputIsMaske 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); diff --git a/test/sec/LatchSymbolicCompilerTests.cpp b/test/sec/LatchSymbolicCompilerTests.cpp index 434bd344..bdc4e85c 100644 --- a/test/sec/LatchSymbolicCompilerTests.cpp +++ b/test/sec/LatchSymbolicCompilerTests.cpp @@ -59,6 +59,16 @@ class LatchSymbolicCompilerTests : public ::testing::Test { 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); @@ -302,7 +312,140 @@ TEST_F(LatchSymbolicCompilerTests, InvalidInitializationIsRejected) { 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); } diff --git a/test/sec/LatchSymbolicIntegrationTests.cpp b/test/sec/LatchSymbolicIntegrationTests.cpp index 42177854..31430528 100644 --- a/test/sec/LatchSymbolicIntegrationTests.cpp +++ b/test/sec/LatchSymbolicIntegrationTests.cpp @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 #include #include +#include #include #include #include "BoolExprCache.h" @@ -184,5 +185,52 @@ TEST_F(LatchSymbolicIntegrationTests, EncoderCannotBroadenCertificateOrRelabelIn 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/OpaquePolicyTests.cpp b/test/sec/OpaquePolicyTests.cpp index 1a1a8afc..f17dc992 100644 --- a/test/sec/OpaquePolicyTests.cpp +++ b/test/sec/OpaquePolicyTests.cpp @@ -15,6 +15,7 @@ #include "Tree2BoolExpr.h" #include "model/OpaquePolicy.h" #include "model/SequentialDesignModel.h" +#include "latch/LatchSupportOptions.h" #include "strategy/SequentialEquivalenceStrategy.h" namespace { @@ -162,6 +163,9 @@ TEST_F(OpaquePolicyTests, EnabledExtractionRejectsDisconnectedNonLatchCell) { } 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())); 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/LatchEventConfigTests.cpp b/test/strategies/miter/LatchEventConfigTests.cpp index 58f97850..a3270a90 100644 --- a/test/strategies/miter/LatchEventConfigTests.cpp +++ b/test/strategies/miter/LatchEventConfigTests.cpp @@ -40,7 +40,9 @@ TEST(LatchEventConfigTests, EnabledByDefaultWithoutAnImplicitContract) { EXPECT_TRUE(config.parseYaml(YAML::Load("{}"), error)); EXPECT_TRUE(config.options().enabled); EXPECT_FALSE(config.options().initialInputs.has_value()); - EXPECT_FALSE(config.options().hasEventContract()); + 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); @@ -61,7 +63,7 @@ TEST(LatchEventConfigTests, CompleteEventContractUsesDefaultEnableUnlessExplicit 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_EQ(config.options().hasEventContract(), !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); } @@ -69,7 +71,7 @@ TEST(LatchEventConfigTests, CompleteEventContractUsesDefaultEnableUnlessExplicit std::string error; ASSERT_EQ(parseArgument(config, {"--sec-latch-events", "single"}, error), Result::Parsed); EXPECT_TRUE(config.options().enabled); - EXPECT_FALSE(config.validate(true, false, false, error)); + EXPECT_TRUE(config.validate(true, false, false, error)); } TEST(LatchEventConfigTests, DisableFlagPreservesLegacyAndCanBeOverridden) { @@ -80,7 +82,7 @@ TEST(LatchEventConfigTests, DisableFlagPreservesLegacyAndCanBeOverridden) { 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().hasEventContract()); + EXPECT_FALSE(config.options().explicitConfiguration); } TEST(LatchEventConfigTests, MasterGateRequiresExactBooleanYamlValues) { @@ -99,10 +101,13 @@ TEST(LatchEventConfigTests, MasterEnableAloneDoesNotInventAnEventContract) { 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().hasEventContract()); + 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_FALSE(config.validate(true, false, false, error)); + 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)); } @@ -115,8 +120,9 @@ TEST(LatchEventConfigTests, MasterEnableCanFollowTuningFlags) { EXPECT_TRUE(config.validate(true, false, false, error)); } -TEST(LatchEventConfigTests, RequiresExplicitInputChangeAndBothInitializationChoices) { +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}"}) { @@ -124,8 +130,14 @@ TEST(LatchEventConfigTests, RequiresExplicitInputChangeAndBothInitializationChoi LatchEventConfig config; std::string error; ASSERT_TRUE(parseYaml(config, body, error)); - EXPECT_FALSE(config.validate(true, false, false, error)); - EXPECT_NE(error.find("explicit"), std::string::npos); + 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"); } } @@ -200,15 +212,17 @@ TEST(LatchEventConfigTests, RejectsNegativeOverflowAndZeroResourceLimits) { EXPECT_EQ(parseArgument(config, {"--sec-latch-workers", "0"}, error), Result::Parsed); } -TEST(LatchEventConfigTests, SymbolicBudgetsAreCheckedAndDoNotSupplyAnEventContract) { +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().hasEventContract()); - EXPECT_FALSE(config.validate(true, false, false, error)); + 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; @@ -219,7 +233,7 @@ TEST(LatchEventConfigTests, SymbolicBudgetsAreCheckedAndDoNotSupplyAnEventContra std::string error; ASSERT_EQ(parseArgument(config, {"--sec-latch-max-nodes", "99"}, error), Result::Parsed); EXPECT_EQ(config.options().maxSymbolicNodes, 99u); - EXPECT_FALSE(config.validate(true, false, false, error)); + EXPECT_TRUE(config.validate(true, false, false, error)); } TEST(LatchEventConfigTests, RejectsMissingFlagValueAndIncompatibleWorkflows) { @@ -329,7 +343,7 @@ TEST_F(LatchEventCliTests, MasterDisabledRetainsOpaqueLatchesAndIndependentStric EXPECT_NE(strict.reason.find("error-on-opaque"), std::string::npos); } -TEST_F(LatchEventCliTests, DisableFlagBeforeAndAfterFormatMatchesDefaultWithoutContract) { +TEST_F(LatchEventCliTests, DisableFlagBeforeAndAfterFormatKeepsUnsupportedRaceOpaque) { const std::vector base{ "-verilog", "-v", "sec", "race.v", "race.v", "cells.lib"}; const auto implicit = run(base); @@ -342,8 +356,37 @@ TEST_F(LatchEventCliTests, DisableFlagBeforeAndAfterFormatMatchesDefaultWithoutC EXPECT_EQ(disabled.status, implicit.status) << disabled.reason; EXPECT_EQ(disabled.exitCode, implicit.exitCode); EXPECT_EQ(disabled.coveredOutputs, implicit.coveredOutputs); - EXPECT_EQ(disabled.skippedObservedOutputs, implicit.skippedObservedOutputs); + 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) { @@ -416,8 +459,9 @@ TEST_F(LatchEventCliTests, WorkflowRestoresOuterEventContractWithoutInheritingIt EXPECT_TRUE(Latch::supportOptions().enabled); EXPECT_EQ(Latch::supportOptions().initialInputs, true); EXPECT_EQ(Latch::supportOptions().workers, 3u); - const auto legacy = run({"-verilog", "-v", "sec", "self.v", "self.v", "cells.lib"}); - EXPECT_EQ(legacy.coveredOutputs, 0u); + 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); } diff --git a/test/strategies/miter/LatchResetCliTests.cpp b/test/strategies/miter/LatchResetCliTests.cpp index 10993b63..db9ca81b 100644 --- a/test/strategies/miter/LatchResetCliTests.cpp +++ b/test/strategies/miter/LatchResetCliTests.cpp @@ -213,10 +213,47 @@ TEST_F(LatchResetCliTests, OffOnOffPreservesLegacyResetSemanticsForEveryEngine) 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().hasEventContract()); + 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 From 601d337d0676af7af0d887d697afe3d09719e053 Mon Sep 17 00:00:00 2001 From: Noam Cohen Date: Mon, 28 Sep 2026 17:09:38 +0200 Subject: [PATCH 07/10] docs(sec): explain latch primitive register and mux representation --- docs/sec-latch-implementation.md | 101 +++++++++++++++++++++++++++++++ docs/sec-latch-support.md | 11 ++++ 2 files changed, 112 insertions(+) diff --git a/docs/sec-latch-implementation.md b/docs/sec-latch-implementation.md index c3f0b5a0..ae38910b 100644 --- a/docs/sec-latch-implementation.md +++ b/docs/sec-latch-implementation.md @@ -260,6 +260,107 @@ This conservative closure prevents a component's temporary pulse from being discarded at a neighboring storage element. It is not an implementation of arbitrary independently settling local islands or final-output-only summaries. +### Basic latch primitive: register plus transparent mux + +The basic active-high Boolean latch has one remembered bit `H`, data `D`, +enable `E`, and visible output `Q`. For one internal pin-event evaluation, +without asynchronous controls: + +```text +Q = E ? D : H +next(H) = Q +``` + +```mermaid +flowchart LR + D[Current data D] -->|Select when open| M[Transparent mux] + E[Enable E] --> M + H[Abstract register: H] -->|Select when closed| M + M --> Q[Visible output Q] + Q -->|Formal storage update: next H = Q| H +``` + +When open, the mux selects current data. When closed, it selects remembered +storage. The visible value is the **mux result**, not merely the register's old +output. `H` is abstract storage, not a flip-flop connected to the design clock. +The formal event machinery evaluates the block when it is activated; a hardware +clock edge is not required. Changes propagate to consumers through internal +waves within an external transaction, rather than waiting for another hardware +clock cycle. + +#### Representation in the code + +The first diagram shows the register/mux construction. The second shows its +packaging in the implemented primitive; the box contains a calculation, not a +new hardware cell or a second clock domain: + +```mermaid +flowchart TD + I[Pin values D and E for this event] --> M + H[Incoming remembered bit H] --> M + subgraph P[One latch primitive evaluation] + M[Select D when open, otherwise H] --> V[Mux result V] + V --> Q[Returned output Q = V] + V --> S[Returned next storage H = V] + end + S --> N[Next pin visit uses this storage] + Q --> W[Publish final outputs and storage at wave commit] + S --> W + W --> C[Changed outputs activate downstream logic] +``` + +If multiple pins changed, the scheduler repeats this evaluation for each pin +in each permitted ordering. Only that ordering's final outputs are published +at wave commit; intermediate storage updates are retained inside the primitive. + +The network contains one `Primitive` with one storage bit for the simple latch, +not two independently scheduled primitive instances. The +[`latch()` convenience constructor](../src/sec/latch/LatchEventModel.cpp) +computes the mux value and returns it as both `Reaction::outputs` and +`Reaction::storage`. The +[Naja adapter](../src/sec/latch/NajaEventPrimitive.cpp) applies the same +data-or-hold rule to explicit library state expressions; its +[symbolic counterpart](../src/sec/latch/NajaSymbolicLogic.h) constructs a +`symbolicMux` expression. Library physical outputs are then evaluated from the +updated state and current pins, so inverted outputs or an integrated clock +gate's output expression need not equal the simple latch's `Q` directly. + +This is a composed register-and-mux **behavioral representation**. Different +packaging alone is not evidence of different latch behavior: the basic equations +match for the same incoming history and pin values. Conversely, matching those +equations alone does not prove whole-network equivalence under different event +or startup rules. No separately scheduled register/mux decomposition is claimed +to have been verified. + +#### Event ordering, startup, and controls + +- Within one permitted changed-pin ordering, each pin is visited in turn. Its + reaction's next storage becomes the incoming storage for the next visit. + A wave publishes the final storage and outputs from that ordering together. + Other primitives evaluate the same pre-wave snapshot; this is not a new + clock edge for every latch or a physical propagation-delay model. +- 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 + remain in the reference model. The certifier cannot silently choose one + outcome to make the component deterministic. +- Before forced bootstrap evaluation, physical storage outputs are projected + from remembered state (and the library's output mapping). Transparency is + evaluated during bootstrap propagation. Unspecified storage remains symbolic; + the primitive does not assume zero, insert a reset, or fabricate a flip-flop + clock edge at startup. +- Active-low enables use the corresponding polarity. Explicit asynchronous + clear/preset rules override ordinary transparency according to the library + model. Undefined or unsupported control combinations remain errors. + +Splitting the mux and register into ordinary, independently scheduled primitives +would not be a cosmetic refactor of this event model. It can add a propagation +wave, alter data-versus-closing order, or change pulses seen by downstream clock +and enable pins. Such a decomposition requires a separate preservation argument. + +The basic primitive is distinct from the whole-design extraction and component +grouping policy described above. Explaining its register/mux correspondence +does not resolve the known large-component certification/coverage failure. + ## 4. Initialization and internal waves Initialization is itself a checked settling episode: diff --git a/docs/sec-latch-support.md b/docs/sec-latch-support.md index 6cedbd84..5372f76c 100644 --- a/docs/sec-latch-support.md +++ b/docs/sec-latch-support.md @@ -132,6 +132,17 @@ visible output Q. The presentation [R1] gives the schematic construction: The register in this construction is mathematical storage. It is not a claim that the physical latch samples on the main flip-flop clock. +The implementation represents this block as **one storage-bearing latch +primitive**, not as separately scheduled register and mux objects. For each +internal pin-event evaluation, its basic reaction computes the mux value and +returns that value as both the visible output and the next remembered value. +The symbolic implementation constructs the corresponding Boolean mux expression. +This packaging does not itself change the data-or-hold equation; equivalence of +complete executions also requires matching initialization, event ordering, and +storage-update timing. See [the implemented primitive and its diagram]( +sec-latch-implementation.md#basic-latch-primitive-register-plus-transparent-mux) +for the exact scope of this correspondence. + When the latch is closed, its output is its remembered value. When open, changes at its data input can change its output and activate downstream logic without waiting for a flip-flop edge. Reset and preset semantics must be incorporated From 90719eb7ef5bc3729fe38167a27e53bc7714f3a8 Mon Sep 17 00:00:00 2001 From: Noam Cohen Date: Mon, 28 Sep 2026 17:26:55 +0200 Subject: [PATCH 08/10] docs(sec): condense latch documents to algorithm and behavior --- docs/sec-clock-handling.md | 6 +- docs/sec-latch-implementation.md | 807 ++++--------------- docs/sec-latch-support.md | 1247 +++++++----------------------- docs/sec-reset-bootstrap.md | 10 +- docs/sec-sequential-models.md | 18 +- 5 files changed, 425 insertions(+), 1663 deletions(-) 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-latch-implementation.md b/docs/sec-latch-implementation.md index ae38910b..94099bae 100644 --- a/docs/sec-latch-implementation.md +++ b/docs/sec-latch-implementation.md @@ -1,270 +1,31 @@ -# SEC Latch Event Implementation - -This document describes the deterministic Boolean-event implementation of the -[latch-support design](sec-latch-support.md). It includes an executable reference, -**SAT-certified symbolic bounded unfolding**, an independent exhaustive finite -compiler, dependency-closed scheduling regions, parallel wave evaluation, and -SEC integration. It is not a timing-accurate model of arbitrary asynchronous -circuits. Optional phase abstraction and stronger scheduling reductions are not -required by, or enabled in, this implementation. - -The master switch `latch_support` is **on by default**. Disable it with YAML -`latch_support: false`, CLI `--no-latch_support`, or Python `latch_support=False`. -No initialization settings are required. Designs with explicitly modeled latches -use the event path with arbitrary Boolean starting inputs and storage, and `any` -input changes. Unsupported components remain opaque. With the switch off, -`sec_reset` uses the unchanged legacy reset-bootstrap path. Latch-free designs -also retain that path unless event settings are explicitly supplied. Ordinary -LEC behavior remains unchanged. - -## 1. Defaults and optional restrictions - -No `sec_latch_events` map or explicit `latch_support: true` is needed: - -```yaml -format: verilog -verification: sec -input_paths: [reference.v, implementation.v] -liberty_files: [cells.lib] +# 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. -Each unspecified input and storage bit retains both Boolean possibilities, -including mixed initial values. Matching external input levels are shared between -the designs; internal storage origins are independent, not assumed equal. -Optional `initial_inputs` and `initial_storage` restrictions remain available -only when explicitly requested: each fixes all bits of its category to `0` or -`1`, independently of whether the other setting is supplied. Neither is a reset. -Without `sec_reset`, no reset sequence is inserted. - -| Purpose | YAML | Command-line option | -| --- | --- | --- | -| Master enable, default on | `latch_support: true` | `--latch_support` | -| Explicitly disable latch support | `latch_support: false` | `--no-latch_support` | -| Allowed external transactions | `sec_latch_events.input_changes: any` or `single` | `--sec-latch-events any` or `single` | -| Initial value of all external inputs | `sec_latch_events.initial_inputs: 0` or `1` | `--sec-latch-initial-inputs 0` or `1` | -| Initial value of all primitive storage bits | `sec_latch_events.initial_storage: 0` or `1` | `--sec-latch-initial-storage 0` or `1` | -| Optional strict opacity policy, default off | `error_on_opaque: true` | `--error-on-opaque` | - -Event and resource settings can be supplied independently; no initialization -field is mandatory. Explicit event tuning also selects the event path for a -latch-free design. Tuning while explicitly disabling the switch is rejected. -The strict opaque policy is independent of the master latch switch: it can -also be used with ordinary SEC. - -### `any` and `single` are different verification assumptions - -- `any` (the default) permits every Boolean valuation of a component's external inputs at - each transaction, including several simultaneous changes and no change. -- `single` permits at most one original top-level input bit to change per - transaction, including no change. It is an explicit restriction on the - environment, not an inferred property of the circuit. - -With `any`, a data change concurrent with a latch closing can produce two -different retained values. Such a component remains opaque if the complete -boundary result is not unique. Selecting `single` is legitimate only when that -restricted environment is the intended proof contract; it is not a sound way -to waive races in a design that must tolerate simultaneous external changes. - -Even in `single` mode, one external change can generate several simultaneous -internal changes. All permitted changed-pin orders inside the component remain -part of certification. The setting does not impose a one-pin-change assumption -on latch data, enables, internal clocks, or asynchronous controls. - -## 2. What one SEC step means - -One step supplies a permitted external transaction, holds those external values -fixed, and completes all internal propagation before the next observation. -The environment cannot interrupt an unfinished settling episode. +## External transactions -```mermaid -flowchart LR - B[Previous complete boundary] --> I[Admit external transaction] - I --> W[Evaluate and commit an internal wave] - W -->|Changed nets activate consumers| W - W -->|Quiescent and error-free| N[Next complete boundary] - W -->|Error, nonsettling, or certification limit| R[Reject component certificate] - N --> O[Observe outputs and permit next transaction] -``` +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. -Consequently, `max_k` and counterexample steps count **external transactions**, -not clock cycles or internal waves. An independent enable can open and close -between flip-flop edges through separate transactions. Data changes while a -latch is open also propagate without requiring a flip-flop edge. - -Selected leaf boundaries remain unsupported. Asynchronous reset/set pins -participate in the event model as external or internally generated controls. -**`sec_reset.cycles` still means clock cycles**, using the automatic adapter -below; it never means an arbitrary count of input events or settling waves. BTOR2 export -uses the compiled transaction transition system and records the event contract; -outside the reset prefix its steps must not be interpreted as hardware clock -ticks. Counterexample reports distinguish reset cycles from **event transactions** -and print the contract. - -### Reset-cycle adapter - -The existing reset configuration can be used with latch support: - -```yaml -latch_support: true -sec_latch_events: - input_changes: single -sec_reset: - cycles: 3 - ports: - - name: rst - active_value: 1 -``` - -The clock is discovered from explicit flip-flop clock expressions and exact -combinational routing, not from names or arbitrary latch enables. The initial -automatic subset requires one common top-level clock root, allowing direct, -buffered, and inverted routes and both sampling polarities. Clock discovery -only supplies the reset stimulus: latches still use the full event model, not -a phase-register approximation. - -Each of the N reset cycles composes these already-certified transactions: - -1. Assert reset and settle. Put the source clock low and settle. If initialization - placed it high, this establishes the low starting phase while reset is active; - that alignment edge is represented, not silently discarded. -2. Sample unconstrained levels for **all** non-clock/non-reset inputs, preserving - the reset environment's free data inputs. With `any`, admit the entire vector - as one transaction. With `single`, assign each sampled input through its own - settled transaction, with additional shared environment choices covering every - arrival order. There is no fixed favorable order and no one-input-per-cycle - restriction. The sampled levels are then held during the two clock edges. -3. Drive the source clock high and fully settle all affected logic and storage. -4. Drive it low and fully settle again. This completes one clock cycle, including - positive- and negative-edge state updates and intervening latch transparency. -5. After the final cycle, deassert reset and settle before normal checking resumes. - -```mermaid -flowchart LR - R[Assert reset and establish low clock] --> D[Sample non-control inputs] - D --> P[Rising edge and full settling] - P --> F[Falling edge and full settling] - F -->|More reset cycles| D - F -->|N cycles complete| U[Release reset and settle] - U --> E[Ordinary event transactions] -``` +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. -A saturating countdown implements this prefix; the compiler does not build N -copies of the full reset logic. Outputs are compared only after the prefix, -as with reset bootstrap. The countdown must finish after exactly N macrosteps, -and each composed event already has a universal progress certificate, so the -mask cannot hide a non-settling modeled reset execution. The requested post-reset -`max_k` budget is retained by extending the engine bound by N. - -Composition is not free: with m non-control input bits, `single` reset sampling -composes m event transitions plus a fixed number of clock/reset transitions, -and the total arrival-order selector construction has quadratic work in m. -The existing `max_symbolic_nodes` budget also bounds the cumulative expression -DAG visited during reset composition (not peak process memory); excessive -ordering work is rejected before construction. Exceeding that budget reports -unsupported reset adaptation rather than choosing a subset of input orders. - -Afterward, clocks resume ordinary event-driven behavior and reset is held inactive, -matching existing reset-bootstrap policy. A requested reset change is ignored by -the effective input adapter (a stutter for the selector interface); witness input -bits must be read with this constraint. For exported models and witnesses, the -first N steps are **reset clock cycles** and subsequent steps are **external event -transactions**. Contract metadata and witness headers record this distinction. -In the selector interface, the original top-input interface variables provide -the sampled levels during reset, while extra reset-order inputs specify their -arrival order; after the prefix only the usual selector/value inputs drive events. -This is a cycle-sampled reset environment, not unrestricted glitch timing inside -a cycle. All such input-arrival choices are shared external stimulus for the two -designs, not private internal scheduling choices discarded by the certifier. - -The first automatic adapter supports **one reset port**. Multiple reset ports -would require a justified assertion/release ordering; multiple independent clock -roots require a defined schedule. Gated/state-generated/ambiguous clock roots, -clockless latch-only designs, unsupported primitive clock metadata, and a reset -port used as its own clock are rejected with specific diagnostics. These limits -affect automatic reset-cycle expansion, not ordinary event-mode reset signals. -No clock is guessed and no reset configuration silently falls back to N events. - -The adapter composes copies of the certified boundary-to-boundary transition, -preserving complete state between events. It therefore inherits progress and -determinism from those certificates. This is a Kepler integration construction, -not a claim that the cited papers prescribe this particular reset protocol. - -Both comparison designs use the same contract, even if one contains no latches. -Contract metadata prevents mixing ordinary clock-cycle models, event models, -or incompatible Boolean initialization contracts. Original top-input identities -remain part of interface alignment, so a selector cannot silently refer to -different pins on the two sides. - -This is zero-delay Boolean behavior under the declared primitive granularity. -It does not cover propagation delays, setup/hold violations, metastability, -HDL X/Z behavior, multiple drivers, or all IEEE Verilog scheduling semantics. -Replacing one modeled primitive with a gate decomposition may move events -between waves and therefore needs more than a Boolean-function equivalence -argument. - -## 3. Extraction and primitive modeling - -The ordinary Liberty reader is augmented, only in the enabled SEC file-based path, -with explicit scalar `latch` groups. The supplemental reader preserves data, -enable, asynchronous clear/preset, conflict behavior, and physical output -expressions. An integrated clock gate is supported through its actual latch -and output expressions, such as a stored enable ANDed with a clock; neither -cell names nor `clock_gating_integrated_cell` metadata alone establish those -semantics. Unsupported library descriptions remain unmodeled. - -The Naja adapter copies explicit sequential expressions and supported Boolean -truth tables into pure primitive callbacks. Worker evaluations do not access -the original Naja objects. This also allows compact extraction to release the -source design after constructing its SEC model. - -Explicit constant nets do not need a cell driver. If flattened connectivity -omits their equipotential entry, a read-only connected-hierarchy walk resolves -consistent Boolean annotations. Floating, X/Z, conflicting, or unexpectedly -driven nets do not silently become constants; the original netlist and its -flattened connectivity are not modified. - -The accepted subset is intentionally conservative: - -- Physical output mappings and clear/preset behavior must be defined. Undefined - simultaneous controls become errors if reached, not don't-cares. A reached - simultaneous clear/preset `Toggle` rule is also unsupported: its implicit - state-dependent asynchronous activity is not represented by pin events alone. -- Latch data and asynchronous control expressions may not depend directly on - internal state variables in a way requiring unmodeled internal feedback. - Such definitions are rejected by the adapter. Feedback through actual net - connections is handled by the event model and certification. -- Flip-flop next-state expressions may use stored state, because their update - is explicitly edge-triggered. Clock/enable expressions cannot depend on - internal state variables. -- Unmodeled primitives, unsupported generic arithmetic/table-select models, - unsupported state-table descriptions, invalid pin mappings, and missing or - multiple drivers do not silently acquire new semantics. - -Each component contains every primitive connected through internally driven -data **or control** nets, including enable, clock, and asynchronous controls. -This deliberately groups more than just feedback strongly connected components. -Shared read-only primary inputs do not by themselves join otherwise independent -components. A component can contain latch chains, feedback loops, combinational -logic, and flip-flops. - -Dependency analysis also identifies directed feedback SCCs, including self-loops, -separately from these undirected event-connected components. Both analyses are -iterative and deterministically ordered. SCC membership is structural information, -not a termination certificate, and a flip-flop is not an unconditional cut of its -generated clock or asynchronous-control dependencies. The first implementation -certifies whole event-connected components; it never invents independent local -settling bounds from SCC sizes. - -This conservative closure prevents a component's temporary pulse from being -discarded at a neighboring storage element. It is not an implementation of -arbitrary independently settling local islands or final-output-only summaries. +## Primitive behavior ### Basic latch primitive: register plus transparent mux -The basic active-high Boolean latch has one remembered bit `H`, data `D`, -enable `E`, and visible output `Q`. For one internal pin-event evaluation, -without asynchronous controls: +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 @@ -273,427 +34,143 @@ next(H) = Q ```mermaid flowchart LR - D[Current data D] -->|Select when open| M[Transparent mux] + D[Data D] -->|Open| M[Transparent mux] E[Enable E] --> M - H[Abstract register: H] -->|Select when closed| M + H[Abstract register H] -->|Closed| M M --> Q[Visible output Q] - Q -->|Formal storage update: next H = Q| H + Q -->|next H = Q| H ``` -When open, the mux selects current data. When closed, it selects remembered -storage. The visible value is the **mux result**, not merely the register's old -output. `H` is abstract storage, not a flip-flop connected to the design clock. -The formal event machinery evaluates the block when it is activated; a hardware -clock edge is not required. Changes propagate to consumers through internal -waves within an external transaction, rather than waiting for another hardware -clock cycle. - -#### Representation in the code +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 first diagram shows the register/mux construction. The second shows its -packaging in the implemented primitive; the box contains a calculation, not a -new hardware cell or a second clock domain: +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 - I[Pin values D and E for this event] --> M - H[Incoming remembered bit H] --> M - subgraph P[One latch primitive evaluation] - M[Select D when open, otherwise H] --> V[Mux result V] - V --> Q[Returned output Q = V] - V --> S[Returned next storage H = V] + A[Start with incoming storage and previous pin values] --> P + subgraph L[One composed latch primitive] + P[Apply next changed pin in the chosen order] --> M + M[Select data or remembered storage] --> R + R[Update private storage and output from mux result] + R -->|More changed pins: carry history forward| P end - S --> N[Next pin visit uses this storage] - Q --> W[Publish final outputs and storage at wave commit] - S --> W - W --> C[Changed outputs activate downstream logic] + R -->|Last pin| S[Stage final storage and output] + S --> W[Commit together with the complete wave] + W --> N[Changed outputs activate consumers for next wave] ``` -If multiple pins changed, the scheduler repeats this evaluation for each pin -in each permitted ordering. Only that ordering's final outputs are published -at wave commit; intermediate storage updates are retained inside the primitive. - -The network contains one `Primitive` with one storage bit for the simple latch, -not two independently scheduled primitive instances. The -[`latch()` convenience constructor](../src/sec/latch/LatchEventModel.cpp) -computes the mux value and returns it as both `Reaction::outputs` and -`Reaction::storage`. The -[Naja adapter](../src/sec/latch/NajaEventPrimitive.cpp) applies the same -data-or-hold rule to explicit library state expressions; its -[symbolic counterpart](../src/sec/latch/NajaSymbolicLogic.h) constructs a -`symbolicMux` expression. Library physical outputs are then evaluated from the -updated state and current pins, so inverted outputs or an integrated clock -gate's output expression need not equal the simple latch's `Q` directly. - -This is a composed register-and-mux **behavioral representation**. Different -packaging alone is not evidence of different latch behavior: the basic equations -match for the same incoming history and pin values. Conversely, matching those -equations alone does not prove whole-network equivalence under different event -or startup rules. No separately scheduled register/mux decomposition is claimed -to have been verified. - -#### Event ordering, startup, and controls - -- Within one permitted changed-pin ordering, each pin is visited in turn. Its - reaction's next storage becomes the incoming storage for the next visit. - A wave publishes the final storage and outputs from that ordering together. - Other primitives evaluate the same pre-wave snapshot; this is not a new - clock edge for every latch or a physical propagation-delay model. -- 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 - remain in the reference model. The certifier cannot silently choose one - outcome to make the component deterministic. -- Before forced bootstrap evaluation, physical storage outputs are projected - from remembered state (and the library's output mapping). Transparency is - evaluated during bootstrap propagation. Unspecified storage remains symbolic; - the primitive does not assume zero, insert a reset, or fabricate a flip-flop - clock edge at startup. -- Active-low enables use the corresponding polarity. Explicit asynchronous - clear/preset rules override ordinary transparency according to the library - model. Undefined or unsupported control combinations remain errors. - -Splitting the mux and register into ordinary, independently scheduled primitives -would not be a cosmetic refactor of this event model. It can add a propagation -wave, alter data-versus-closing order, or change pulses seen by downstream clock -and enable pins. Such a decomposition requires a separate preservation argument. - -The basic primitive is distinct from the whole-design extraction and component -grouping policy described above. Explaining its register/mux correspondence -does not resolve the known large-component certification/coverage failure. - -## 4. Initialization and internal waves +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. Give each unspecified external input and primitive storage bit an independent - symbolic Boolean origin. Apply a concrete value only if explicitly specified. -2. Initialize physical storage outputs from their model; universally quantify - auxiliary internal net seeds rather than choosing convenient values. The - finite reference compiler enumerates them; the symbolic compiler uses - unconstrained Boolean variables. -3. Set previous values equal to initial current values, so merely beginning - with a high external clock does not invent a rising edge. -4. Force a BOOT evaluation. Gates evaluate, latches apply level-sensitive and +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 from that update remain real modeled - events in subsequent waves. -5. Certify that all auxiliary seed choices and permitted event orders settle - to the same complete boundary for **each** intended input/storage origin. - Different genuine origins may produce different boundaries; uniqueness is - required only across artificial seeds and event orders for the same origin. - -When a physical output's initial projection reads input pins, certification -also quantifies seeds of other storage-output nets that the projection can -read before they are overwritten. Otherwise those hidden seed choices could -determine the retained result. - -The integrated symbolic compiler preserves the settled boundary as a function of -the genuine origins. SEC receives that exact initial relation, plus any derived -constant facts; it never chooses one favorable boundary. All engines and BTOR2 -must retain the relation even when constant initial facts also exist. The initial -relation applies only at frame zero. A reset prefix is composed only when -`sec_reset` is configured, and storage that reset does not determine remains -unspecified. In particular, a closed, unreset latch is not forced to zero merely -because other cells have reset. - -This event model remains Boolean: unspecified storage means an arbitrary fixed -Boolean starting value, not a literal Verilog X. In dual-rail encoding its initial -rails are complementary symbolic bits, not the `11` encoding of X. Explicit X/Z -behavior remains unsupported here; legacy dual-rail initialization is unchanged -when the event path is inactive. Independent unreset storage can therefore -produce a startup mismatch even for identical designs; no hidden equality or -automatic reset is assumed to make such a comparison pass. - -Symbolic starts can also enlarge the proof problem. In particular, IMC can return -inconclusive on a reset-plus-transparent-latch example that KI and PDR prove: -its exact reachable-state path has a small state-count limit and its larger -interpolation path is resource bounded. This is not a requirement to provide -initial values. No initialization restriction is added to obtain a proof. - -The finite fallback is currently used only for fully concrete BOOT inputs and -storage. If symbolic certification cannot establish the required settling -properties with unspecified origins, the component remains opaque; no origins -are discarded to make a certificate succeed. - -Within an ordinary wave, activated primitives read the same frozen snapshot. -A sequential primitive processes every permitted ordering of changed input -positions, applying each change once. A flip-flop edge is not reused when a -later data pin is visited. The final primitive storage/output tuple is staged; -all staged tuples commit at the wave boundary. Changed nets activate all their -consumers for the next wave. Primitive-local intermediate pin-processing values -are private under this contract, but transitions between network waves are not -discarded. - -Independent primitive evaluations within a wave run through TBB. They read -immutable inputs and stage separate results, so worker completion order cannot -choose a latch capture. A producer's chosen result is shared across its fanout, -not resampled independently for each consumer. The complete-wave update barrier -preserves the reference epochs. - -## 5. Certification and compilation - -### 5.1 Symbolic bounded unfolding (first choice) - -The symbolic model mirrors the reference's complete state, BOOT, admission, -pin-order choices, sticky errors, frozen snapshots, and update barriers. Primitive -expressions are copied directly into Boolean DAGs. Generic AND/OR/XOR families -are linear-size constructions; small explicit library truth tables are local -expansions, not circuit-wide input or state enumeration. - -Each wave allocates fresh ordering selectors before parallel evaluation. Every -selector encoding denotes a legal pin order, including otherwise unused binary -codes. Unchanged pins are skipped; restricting a full-pin permutation in this -way represents every changed-pin permutation. A producer's choice is shared -across all fanout. Errors remain nonstable, and completed states have exact -identity successors. - -For each component: - -1. Retain every current net bit and primitive storage bit in the macrostate. - At completed boundaries, previous=current and active=BOOT=error=0, so those - omitted fields are exactly reconstructible, not discarded history. -2. Construct candidate invariant `B`: a normalized error-free boundary with - consistent constants, where forced no-edge evaluation of each primitive - preserves its storage and physical outputs. This overapproximates reachable - boundaries; it is **proved**, not assumed, to hold after initialization and - to be inductive. -3. Unfold BOOT from the specified input/storage values, quantifying independent - auxiliary seeds and every permitted pin order. Try increasing bounds up to - `max_waves`; prove `not Stable(xK)` UNSAT. Independently seeded/ordered copies - must also have the same **complete** boundary, and that boundary must satisfy B. -4. Share symbolic pre-admission `q` and external input `u` between two ordinary - episodes. Under `B(q) AND Allowed(q,u)`, prove bounded progress, complete-state - outcome uniqueness with independent ordering choices, and `B(next)` closure. - `single` constrains admission only, never internally generated pin changes. -5. Only after those obligations succeed, rebuild the same K waves using one - legal canonical pin order and a legal BOOT seed. Uniqueness justifies removing - the choices. Export no seed/choice variables. The certificate retains its - admission contract and external-net mapping; encoding cannot change either. -6. Substitute SEC state/input variables simultaneously into the compiled DAG, - with shared subexpressions retained. This logic is built once, not regenerated - on each transaction. The SEC engines prove arbitrarily long boundary traces - using these next-state and observation expressions. - -These are ordinary SAT safety obligations over the finite symbolic relation, -using CaDiCaL with cumulative conflict/decision budgets. SAT at an insufficient -bound is not proof of oscillation. A failure found only from the overapproximate -invariant is an **unproved certificate**, not a reachable design defect. UNKNOWN -or resource exhaustion cannot become a successful certificate. + 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. -```mermaid -flowchart TD - N[Copied primitives and complete dependency regions] --> S[Symbolic BOOT and K-wave proof] - S -->|Progress, uniqueness, invariant all proved| U[Canonical K-wave Boolean DAG] - S -->|Certificate not established| F[Exact reachable finite reference compiler] - F -->|Complete certificate| T[Boundary transition table] - F -->|Unsupported or resource limit| O[Opaque with coverage or strict error] - U --> M[Shared transaction interface and SEC] - T --> M -``` +A closed, unreset latch therefore keeps its unspecified history even when +other storage is reset. Initialization is not an implicit reset sequence. -### 5.2 Exact finite reference and fallback - -For each component the compiler performs exhaustive finite-state analysis, -subject to explicit resource limits: - -1. Certify the initialization episodes and retain their complete stable states. -2. From each reachable complete boundary, enumerate every permitted external - transaction, including stutters, and admit it into the event model. -3. Explore every reachable internal state and successor. The retained state - includes current/previous net values, primitive storage, activations, BOOT, - and errors; equal visible outputs do not imply equal state. -4. Require totality. Missing successors and reachable errors fail certification. - Stable states must have identity successors, so early completion can be - padded without changing history or hiding a failure. -5. Detect reachable nonstable cycles. A cycle with an exit still permits an - infinite execution and therefore fails universal settling. -6. For an acyclic episode graph, calculate the longest path to stability. It is - an exact sufficient wave bound, not a guessed latch count or shortest path. -7. Require one **complete** stable boundary for the transaction. Different - retained storage or history is rejected even if present outputs agree. -8. Add the boundary-to-boundary row and continue until the reachable boundary - set is closed under every permitted transaction. - -No partial table is returned as certified. Resource exhaustion, an excessive -wave bound, a race, or a nonsettling execution cannot be turned into an -assumption that removes the troublesome input from the proof. - -The accepted table is encoded into Boolean next-state and observation formulas -for the existing SEC engines. Its boundary IDs are injective identifiers of -complete states, including remembered input values and history. Output formulas -describe the completed observation of the incoming transaction. Unused IDs have -a total encoding but are unreachable from the explicitly initialized, closed -table. - -In `single` mode, both designs share selector bits and one value bit. The selector -names an original top input; that input is assigned the value. Selecting an -unrelated component's input, selecting a reserved/out-of-range code, or assigning -the existing value produces the appropriate local stutter. The input interface -is aligned before verification. Counterexample inputs therefore include these -synthetic selector/value signals; they are not a complete ordinary input-vector -sample at each step. - -For those witnesses, `$event.select[i]` contributes bit `i` of the zero-based -selector, with bit zero least significant. Original top input names, including -their bit indices, are sorted lexicographically to define that selector order. -`$event.value` supplies the assigned Boolean value. The retained -original input-interface variables are alignment sentinels outside the reset -prefix; their witness values do not drive ordinary event transactions. During -the optional reset prefix they instead supply the sampled levels, as described -in the reset-cycle adapter above. - -The exact subject of the symbolic `K`-copy unfolding is the declared -Boolean-event contract, not unrestricted physical latch networks. It avoids the finite -fallback's whole-state/input enumeration, but SAT cost and unfolded DAG size -can still grow substantially. Either backend may certify a component; if neither -does, opacity is preserved. The finite backend is also an independent oracle -for differential tests of the symbolic compiler. Stronger regional summaries -and phase abstraction remain optional future reductions. - -## 6. Resource controls - -The following limits can be tuned through `sec_latch_events`, the corresponding -CLI flags, or the Python options in Section 8: - -| Setting | Default | Scope | -| --- | ---: | --- | -| `max_waves` / `--sec-latch-max-waves` | 256 | Maximum accepted settling depth of an episode | -| `max_symbolic_nodes` / `--sec-latch-max-nodes` | 2,000,000 | Cumulative visited symbolic DAG nodes, including proof copies; also the reset-composition budget | -| `max_sat_conflicts` / `--sec-latch-sat-conflicts` | 500,000 | Cumulative symbolic certification SAT conflicts | -| `max_sat_decisions` / `--sec-latch-sat-decisions` | 5,000,000 | Cumulative symbolic certification SAT decisions | -| `max_states` / `--sec-latch-max-states` | 4,096 | Finite fallback: reachable complete boundaries per component | -| `max_transactions` / `--sec-latch-max-transactions` | 65,536 | Finite fallback: boundary/input rows per component | -| `workers` / `--sec-latch-workers` | 0 | Automatic TBB worker selection; `1` selects serial evaluation | - -The symbolic node budget is not a hard process-memory ceiling: temporary nodes -may be constructed inside a wave before accounting, and the shared expression -cache has its own lifetime. SAT budgets and certification node limits apply per -component. Reset composition separately applies the node budget to each complete -design's adapted model. - -Other conservative finite-compiler/reference limits currently live in the -standalone API, not in additional YAML keys. The 100,000 local pin-permutation -limit also applies to symbolic sequential primitives; gates do not enumerate -pin orders: - -| Resource | Default | -| --- | ---: | -| Complete reference-state Boolean bits | 512 | -| Reachable internal states per episode | 65,536 | -| Successor transitions explored per episode | 262,144 | -| Enumerated external input bits per component | 12 | -| Unspecified initial-storage bits | 12 | -| Auxiliary bootstrap seed bits | 12 | -| Initial storage/seed configurations | 4,096 | -| Changed-pin permutations per primitive activation | 100,000 | -| Combined successor alternatives per wave | 100,000 | - -`max_states` is not the internal episode-state limit. Raising one setting does -not disable the others or guarantee that a component becomes tractable. -The symbolic path is not subject to the finite fallback's 12-input/seed-bit or -512-complete-state-bit limits; selecting a tiny finite-table limit alone does -not disable symbolic certification. -These limits reject unproved cases; they do not truncate the relation while -claiming a successful certificate. - -Certification of the event model does not guarantee that an SEC engine can -prove equivalence within its own bound and resource limits. Retained event -history and reset-counter bits also contribute to backend state size. An -inconclusive backend result remains inconclusive; it is not equivalence and -does not justify restricting the event or reset-input contract. - -## 7. Opacity, errors, and coverage - -With latch support disabled, the existing extraction path remains in use. -Latch-free designs also retain that path unless event settings are supplied. -With support enabled, designs containing modeled latches use event extraction -without requiring initialization settings. A certified component is modeled; -an unsupported or uncertified component remains -opaque, with reasons and affected output skipping. Independent supported outputs -can still be checked. Skipped outputs are not proved, and a partial result is not -a claim that an excluded nonsettling component terminates. - -Diagnostics distinguish a proven nonsettling cycle, distinct complete boundary -results, an invalid reference/primitive case, exhausted resources, and a settling -depth beyond the accepted bound. A resource limit is not evidence of oscillation. - -`error_on_opaque: true` or `--error-on-opaque` changes the policy to a hard -unsupported result with a nonzero exit and an identified design/signal/reason. -It is default-off, applies to general opaque cells/signals rather than just -latches, and is checked in either design, including disconnected opaque cells. -It does not reclassify every unrelated connectivity skip as an opaque cell. - -Invalid global configuration, incompatible contracts, and unsupported whole-run -interfaces are rejected rather than represented as a successfully modeled event -run. Borrowed-design calls explicitly scope their own settings; they do not -inherit a caller's ambient event contract or leak their settings back to it. - -## 8. Borrowed C++ and Python APIs - -The Python interface uses the same default-on gate and symbolic starts: - -```python -from kepler_formal import VerificationOptions - -options = VerificationOptions( - mode="sec", -) -``` +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 -Optional fields are `latch_workers`, `latch_max_waves`, `latch_max_states`, -`latch_max_transactions`, `latch_max_symbolic_nodes`, `latch_max_sat_conflicts`, -and `latch_max_sat_decisions`. `latch_workers=0` selects automatic parallelism; -proof budgets must be positive. `latch_input_changes`, `latch_initial_inputs`, -and `latch_initial_storage` are independently optional restrictions. Invalid types, tuning while -explicitly disabled, and event settings in non-SEC mode or with selected leaf -boundaries are rejected. Ordinary LEC and latch-free SEC retain legacy behavior -without event tuning. Use `latch_support=False` to explicitly disable the feature; -it must not be combined with event settings. The independent `error_on_opaque` -option remains default-off. - -The native C++ equivalent is `BorrowedDesignOptions::latchSupport`, a validated -`BorrowedLatchOptions` object. Borrowed APIs consume the caller's explicit Naja -models; they do not load Liberty files or guess latch semantics. Source models, -selected tops, DNL pointers/order IDs, and ambient configuration are restored -after success or failure. Matching-runtime native and Python tests run through -`regress/run_python_regress.py`, without modifying installed Python packages. - -## 9. Implementation map and verification - -| File | Responsibility | -| --- | --- | -| `src/bin/LatchEventConfig.*` | Master enable, explicit contract, tuning, and CLI/YAML validation | -| `src/bin/LibertyLatchModels.*` | Supplemental scalar Liberty latch descriptions when SEC latch support is enabled | -| `src/sec/latch/LatchInitialState.*` | Exact symbolic BOOT relations and shared initial external levels in SEC | -| `src/sec/latch/NajaEventPrimitive.*` | Copy supported Naja primitive expressions into immutable callbacks | -| `src/sec/latch/LatchConstantNet.h` | Read-only resolution of driverless Boolean constants, preserving conflict diagnostics | -| `src/sec/latch/LatchEventModel.*` | Boolean BOOT/admission/wave semantics, complete state, parallel primitive evaluation | -| `src/sec/latch/LatchSettlingCompiler.*` | Exhaustive settling/uniqueness checks and reachable macro-transition table | -| `src/sec/latch/LatchSymbolicModel.*`, `NajaSymbolicLogic.h` | Exact symbolic waves and direct primitive DAGs | -| `src/sec/latch/LatchSymbolicCompiler.*` | SAT-certified progress, BOOT independence, uniqueness, closure, and bounded unfolding | -| `src/sec/latch/LatchSymbolicEncoding.*` | Certified contract checks and simultaneous SEC-variable substitution | -| `src/sec/latch/LatchDependencyGraph.*` | Iterative feedback SCCs and event-connected scheduling components | -| `src/sec/latch/LatchResetClock.*`, `LatchResetAdapter.*`, `LatchInputHistory.h` | Automatic source-clock discovery, remembered levels, and reset-cycle composition | -| `src/sec/latch/LatchBoundaryEncoding.*` | Injective boundary-state encoding and shared event-input decoding | -| `src/sec/latch/LatchNetlistAdapter.*` | Full data/control component closure, extraction, opacity, and SEC integration | -| `src/sec/latch/LatchSupportOptions.*`, `LatchEventContract.h` | Scoped options and protection against incompatible step/initialization contracts | -| `src/sec/model/OpaquePolicy.*` | Independent default-off error-on-opaque policy | -| `src/python/BorrowedLatchOptions.*`, `PyLatchOptions.*`, `kepler_formal/_latch_options.py` | Typed borrowed/Python contract validation and scoped activation | - -New tests cover the reference waves, latch chains and feedback, initialization -seed dependence, all permitted pin orders, transient controls, serial/parallel -agreement, exhaustive small-graph certification, resource failures, table -encoding, original-input alignment, Naja/Liberty adapters, and configuration and -opacity policies. Symbolic tests compare complete states with concrete reference -successors and exact table rows, test long/wide networks beyond enumeration -limits, independent proof choices and seed copies, invariant overapproximation, -hidden-state races, SAT/resource failures, and contract-preserving export. -Test names are in the new `Latch*Tests.cpp`, `OpaquePolicy*Tests.cpp`, and Python -latch suites. Optional reductions and unrestricted timing models are not implied -by completion of this deterministic Boolean-event implementation. - -For the sources, conditional proof arguments, and remaining theoretical -extensions, see [the design and literature references](sec-latch-support.md). -Those references motivate the construction; the exact Boolean initialization, -barrier semantics, compiler, and adapter are Kepler implementation choices whose -correctness must be checked against the stated contract. +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 index 5372f76c..cd1b294e 100644 --- a/docs/sec-latch-support.md +++ b/docs/sec-latch-support.md @@ -1,997 +1,290 @@ -# Proposed SEC Latch Support - -Status: architectural design and conditional correctness arguments, with the -deterministic Boolean-event path implemented behind the default-on `latch_support` -switch. Starting inputs and storage remain symbolic unless explicitly constrained; -initialization settings are not required and reset is never inserted implicitly. -The implementation, admission/initial-state semantics, symbolic -certifier, finite fallback, and scoped limitations are documented separately in -[SEC Latch Event Implementation](sec-latch-implementation.md). Optional phase -abstraction and stronger scheduling reductions remain extensions. This document records the -literature-backed approach discussed for level-sensitive latch support. It does -not itself enable latch extraction or change SEC results. The constructions and -proof sketches below close the identified specification gaps for a deliberately -restricted digital contract. They are not machine-checked proofs of an -implementation, or a claim to support every physical latch network. - -The central distinction is between **modeling behavior** and **optimizing that -model**. Latch storage, transparency, event ordering, and internal propagation -must be defined first. Loop unfolding, local scheduling, parallel evaluation, -and phase abstraction are subsequent transformations with separate correctness -conditions. No single referenced paper proves this complete Kepler architecture. - -## 1. Summary - -The proposed pipeline is: - -1. Extract explicit latch and flip-flop models, including asynchronous controls. -2. Build the finite, snapshot-based evaluate/update reference system in Section 5, - preserving history and events between flip-flop clock edges. -3. Analyze data and control dependencies. Identify feedback components and - candidate scheduling regions; these are different partitions. -4. Prove that each accepted settling episode terminates within a sufficient bound. -5. Compile supported internal propagation into bounded unfolded logic, preserving - all relevant intermediate effects. -6. Schedule regions using complete, wave-tagged inputs as specified in Section 8; - preserve intermediate events rather than publishing only settled endpoints. -7. Optionally apply periodic phase discovery and phase abstraction to the valid - model, with their own observation-preservation conditions. -8. Compare the two designs at corresponding external observation boundaries, - checking progress as well as output agreement. - -The first compiled path additionally requires a unique complete boundary state -for each admitted initial state and transaction. The conservative fallback is -the existing opaque behavior, with explicit skipped-output coverage, or the -opt-in error policy in Section 11. A backend retaining internal steps would be -a separate extension, not a capability assumed here. Never choose an arbitrary -fixed point, assume convergence, or discard a non-settling execution. +# Latch Support Algorithm -```mermaid -flowchart TD - P[Explicit primitive models and initialization] --> R[Finite event reference model] - R --> C[Check admission, progress, invariant closure and boundary uniqueness] - C -->|All obligations established| U[Compile a complete episode with K waves] - U --> L[Schedule locally with complete wave-tagged inputs] - L --> S[SEC at corresponding external boundaries] - L -. Optional .-> A[Phase abstraction] - A --> S - C -->|Unsupported or unproved| O[Opaque with coverage report] - O -->|Error-on-opaque enabled| E[Stop with error] -``` +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. -## 2. Relationship to Current Kepler Behavior - -Current documented behavior is described in -[SEC Sequential Models](sec-sequential-models.md) and -[SEC Clock Handling](sec-clock-handling.md). With `latch_support` disabled, or with -no latch-event settings supplied, generic latch outputs remain opaque. The switch -is enabled by default, but event modeling still requires explicit input-event and -initialization settings; it models only certified behavior. Existing clock -handling also has explicit limits on cross-domain cones. - -This proposal would extend those semantics; it is not merely an extraction -optimization. In particular, modeling independent latch enables requires more -than attaching another state-update mux to the existing abstract clock tick. -The current clock-domain restrictions must not be removed until the replacement -semantics and coverage rules have been implemented and verified. - -Naja's explicit sequential models should provide the element kind, data/state -expressions, enable or clock expression, clear/preset behavior, and physical -output mapping. Do not infer latch semantics from cell or pin names. - -This is a frontend prerequisite, not an assumption that the current Liberty -reader already supplies every such model. Its latch/state-table support must -be addressed separately. Existing clock-tree name exclusions do not establish -latch semantics. Reusing a clock-carrier classifier also does not prove gate -stability, transparency through chains, or absence of active feedback. - -## 3. Initial Semantic Contract - -The first target is a **zero-delay digital model with explicit internal -propagation steps**, not a physical timing model. - -- An external transaction supplies the next permitted input valuation or event. - Clocks, latch enables, data, and asynchronous controls can change independently - when allowed by the environment contract. -- New external stimulus is admitted only at a quiescent boundary. During the - resulting settling episode, external values are held fixed while internal - events continue to propagate. -- Quiescence means no remaining modeled work can change the state: pending - updates and event/edge history must be accounted for, not just latch outputs. -- A transaction supplies Boolean external values and any declared ordering - constraints. At a primitive activation, preserve every permitted ordering of - changed input pins, as specified in Section 5. Do not silently select one - order or impose a one-input-change restriction. -- The initial supported primitive set is Boolean, single-driver combinational - logic plus explicitly modeled latches and flip-flops. Cell-specific - clear/preset priority and invalid combinations must be defined. Unmodeled - X/Z, multiple drivers, and missing primitive rules are unsupported, not - don't-cares. Section 5 defines a distinct Boolean initialization contract. -- Observations initially occur at completed external transactions. Any internal - event that affects stored state remains relevant even if outputs are observed - only after settling. - -There is no inferred physical sampling interval. A formal internal step advances -the event semantics, not necessarily a circuit clock. An independent enable -pulse between flip-flop edges can be represented by external transactions for -its opening and closing. This does not establish fidelity for arbitrary physical -pulses, gate delays, setup/hold violations, or metastability. - -Allowing the external environment to interrupt an unfinished settling episode -would be a separate extension. The initial quiescent-environment contract must -be documented rather than mistaken for unrestricted asynchronous hardware. - -## 4. Basic Latch Building Block - -For a simple active-high latch, distinguish the remembered value H from the -visible output Q. The presentation [R1] gives the schematic construction: - - Q = enable ? data : H - next(H) = Q - -The register in this construction is mathematical storage. It is not a claim -that the physical latch samples on the main flip-flop clock. - -The implementation represents this block as **one storage-bearing latch -primitive**, not as separately scheduled register and mux objects. For each -internal pin-event evaluation, its basic reaction computes the mux value and -returns that value as both the visible output and the next remembered value. -The symbolic implementation constructs the corresponding Boolean mux expression. -This packaging does not itself change the data-or-hold equation; equivalence of -complete executions also requires matching initialization, event ordering, and -storage-update timing. See [the implemented primitive and its diagram]( -sec-latch-implementation.md#basic-latch-primitive-register-plus-transparent-mux) -for the exact scope of this correspondence. - -When the latch is closed, its output is its remembered value. When open, changes -at its data input can change its output and activate downstream logic without -waiting for a flip-flop edge. Reset and preset semantics must be incorporated -from the actual primitive model. - -These equations alone are not a complete network semantics. In a feedback -network, treating them as simultaneous unconstrained equations can lose history -or eliminate behavior. An open self-feedback latch can reduce to Q = Q, which -allows either value unless its previous state is preserved. Inverting feedback -can produce Q = not Q, which has no Boolean solution; excluding that case would -hide a problematic execution. - -The reference model must therefore start propagation from the actual preceding -state and apply defined event rules. Closing a latch and changing its data in -the same transaction must use those rules, not an assumed universal old-data -or new-data convention. - -Four open latches in a chain are not four clock cycles of delay. Their changes -propagate through internal evaluations within the same settling episode. The -eventually settled result is not a claim of zero physical propagation time. - -## 5. Reference Internal-Step Transition System - -The direct formal foundation is the VERICELL construction [R2], which models -primitive evaluation and updates and encodes them as a Boolean transition -system. Its published implementation makes restrictions that must not be -inherited implicitly; see the reference notes below. - -### 5.1 Complete finite state and primitive contract - -Select the following zero-delay barrier semantics as the reference. This is an -explicit digital contract derived from the operational structure in [R2], not -a claim of complete IEEE Verilog or physical-timing equivalence. - -The state contains primitive storage, current and previous net values, active -evaluations, staged updates, initialization status, sticky errors, and any finite -environment monitor needed for later transactions. Each net has one writer; -multi-output primitives own an output tuple committed together. A wave has at -most one pending output tuple per primitive, so no unbounded event queue or -absolute time counter is required. A reference step is one complete wave, -not a worker dispatch or an arbitrary scheduler delay. - -Every supported primitive supplies a complete finite reaction table: data and -control polarity, storage, clock edge, asynchronous clear/preset behavior, -conflicting-control behavior, and physical output mapping. An undefined or -unsupported case produces a sticky error, never an absent successor. Different -cell types need not share a universal reset priority. - -At a quiescent boundary, admission copies the old current values into history, -atomically replaces external input values with the transaction's values, and -activates exactly their changed-net consumers. Internal values and storage are -initially unchanged; staged updates are empty. Update the finite environment -monitor once for the transaction, not once per wave. Hold the supplied external -values throughout the episode. Changes intended to occur at distinct settled -moments are separate transactions. In the absence of an explicit constraint, -simultaneous changes leave every local pin order permitted. Invalid admission -must produce an error rather than remove that transaction from the relation. - -### 5.2 One ordinary evaluate/update wave - -1. Freeze the complete current/previous snapshot for this wave. Every activated - primitive reads that same snapshot; no worker reads another worker's staged - result. -2. A combinational primitive stages its Boolean function of the current inputs. - For a sequential primitive, begin with its remembered state and previous pin - values. Process the changed pin positions in every permitted order. For each - visited pin, apply its change once, then apply the primitive's reaction table - to the resulting local pin vector and remembered state. -3. For a simple latch reaction, apply the specified asynchronous controls first; - otherwise copy the local data value while enabled and retain storage while - disabled. A flip-flop captures only on a visit to its clock pin that carries - the specified edge, subject to its controls. Do not re-use that edge while - subsequently visiting a data or enable pin. -4. Stage only the final primitive state/output tuple from that evaluation. - Intermediate values inside its pin-processing sequence are private under - this contract. This does not permit hiding changes between network waves. -5. Commit all staged updates together, set previous net values to the pre-commit - current values, and clear the consumed activations and committed update - buffer. Replace the active set with exactly the consumers of changed nets, - including data, enable, clock, reset, and preset consumers. Inactive - primitives retain state. -6. Continue if work remains. Otherwise normalize previous values to current - values and mark a completed boundary, provided no error occurred. An idle - region alone does not establish whole-episode quiescence. - -The primitive-local recurrence is: +Phase reduction is optional and comes afterward. The cited research supports +individual steps; no single article proves this complete combination. + +## 2. Latch and event behavior + +For an active-high latch with data D, enable E, remembered bit H, and output Q: ```text -b := previous input-pin vector -z := current primitive storage -choose a permitted ordering pi of changed pin positions -for each j in pi: - before := b - b[j] := current value of input pin j - z := reaction(z, before, b, j) -stage z and its physical output tuple +Q = E ? D : H +next(H) = Q ``` -The reference relation retains all permitted outcomes. Only after the -boundary-uniqueness check in Section 9 may a legal deterministic implementation -replace those alternatives. No pin-stability restriction may be inferred merely -from a primary input being held: internally generated changes remain possible. -Any such restriction must hold at the actual reference activations. +H is abstract storage, not a flip-flop connected to the hardware clock. +The output follows data while open and retains history while closed [R1]. +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. -This resolves simultaneous-event ambiguity explicitly. An open latch whose data -changes as it closes can retain old data or capture new data depending on the -permitted pin order. A clock/data race can similarly change a flip-flop capture. -The accepted deterministic path rejects unresolved outcome dependence; it does -not silently choose whichever outcome makes two designs agree. +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 [R2]. ```mermaid flowchart LR - Q[Complete quiescent state] --> J[Admit external transaction] - J --> V[Evaluate active primitives from one snapshot] - V --> C[Commit staged updates together] - C -->|Changed nets activate consumers| V - C -->|No work and no error| B[Next observation boundary] - V -->|Undefined reaction| E[Sticky nonstable error] + A[External transaction] --> E[Evaluate activated elements] + E --> C[Commit updates together] + C -->|Signal changes| E + C -->|No pending work or error| B[Settled observation] ``` -Do not freeze an internally generated control during settling. Conversely, -adding/removing gates or replacing a primitive with a decomposition can move -events between waves and change capture. Such rewrites need preservation -arguments; Boolean function equality alone is insufficient for event-sensitive -consumers. The primitive granularity is part of this reference contract. - -### 5.3 Initialization is an actual settling episode - -For the first proposed Boolean path, supply and hold an initial external input -valuation u0, and choose initial storage bits once under an explicit -initialization relation. Initialize their physical outputs consistently. Choose -declared Boolean seeds for the remaining internal nets, or quantify over all -such seeds; do not select a convenient stable solution. Set previous values -equal to current values for all initialized nets before the forced first -evaluation, including the external clocks. - -Run a dedicated first evaluation: combinational functions evaluate; latches -apply their level-sensitive and asynchronous rules; flip-flops apply asynchronous -controls but otherwise retain their initial storage, without an invented clock -edge. Commit together, then use ordinary waves. A generated clock change caused -by that commit is an actual event under the chosen model and is processed. - -Certify bootstrap progress and the resulting boundary invariant, just as for -later episodes. If only storage bits and initial external inputs are intended -initial parameters, compare bootstrap runs with those parameters shared but -auxiliary net seeds and permitted event choices independent. Require a unique -complete boundary result, or report unsupported. Otherwise arbitrary gate seeds -could silently decide which state is retained. - -This is a Kepler-specific Boolean bootstrap, not the all-X initialization in -[R2]. A symbolic but fixed initial Boolean bit is not HDL X and is never -resampled during propagation. A future multivalued path needs its own explicit -rules, including genuine startup transitions that its edge semantics recognize. -It must not silently inherit the Boolean no-startup-edge convention. Existing -X-handling policy outside this proposed path is not changed by this document. - -## 6. Feedback Components and Scheduling Islands - -### Feedback components - -Construct a dependency graph that includes both data and control paths. Strongly -connected components identify candidate feedback regions. A loop may contain -one latch, many latches, and intervening combinational logic. - -A closed latch can break a transparent dependency for a particular condition. -To classify an entire feedback region as inactive, prove that every directed -cycle is broken under every admitted condition. Structural cyclicity alone -does not establish oscillation. - -If controls change during the episode, acyclicity of an individual snapshot is -not by itself a termination proof. The complete event computation must still -satisfy the progress obligation; do not silently freeze changing controls when -using a structural shortcut. - -A flip-flop's data-to-state update is normally a sequential boundary, but its -clock and asynchronous controls cannot be treated as unconditional cuts: -internally generated events on those pins can change its state during settling. - -### Scheduling islands - -A scheduling island may contain several feedback components and the acyclic -logic or latch chains connecting them. It need not equal a component being -unfolded. DeVane [R5] supplies a concrete precedent for trigger-based regions, -not a rule to merge everything that is connected. - -Candidate grouping must account for enable, clock, reset, and data dependencies, -shared downstream storage, and pending-event effects. Two events that change -the same latch's data and enable are not independent simply because their -source logic belongs to separate graph components. Conversely, sharing a held, -read-only external input does not necessarily make two regions dependent. - -### Why final outputs alone are insufficient - -Suppose region A produces a temporary enable pulse for a latch in region B. -The enable starts and ends low, but the intervening high value lets B capture -data. Sending only A's final low value to B would lose that capture. - -Either preserve the relevant boundary event sequence, enlarge the modeled -region while retaining those internal events, or prove that the intermediate -events cannot affect any required behavior. Merely merging regions does not -justify deleting their internal propagation history. - -## 7. Certifying a Settling Bound - -Unfolding replaces repeated applications of internal transition logic with a -chain of copies. It is built once during model construction and reused for each -external transaction. It is exact only after a sufficient bound is justified. - -The finite-state eventuality result in [R3] provides the foundation: universal -eventual arrival at a target condition has a finite uniform bound, and checking -a candidate bound is a safety obligation. Finite state by itself does not imply -convergence; a reachable cycle can avoid stability forever. - -For one settling episode, define: - - q complete quiescent boundary state - B(q) admitted boundary invariant - Allowed(e, u) shared environment's allowed transaction, with monitor e - J(q, u, x0) admission of the transaction, retaining every allowed outcome - x complete internal state, including history and events - u held external stimulus - Entry(x, u) all admissible episode-entry configurations - T_hold(x, x', u) one permitted internal transition with stimulus held - Stable(x) full quiescence - -Entry describes the complete state immediately after admission of the new -external transaction, including its pending events or changed-value history. -The state and held stimulus must be consistent. Entry is not the quiescent -state before that transaction is applied. - -The finite environment monitor is part of the complete state; the same -environment contract supplies both designs. Do not intersect two different -implementation-specific admissibility conditions to hide a disagreement. - -Establish these obligations before using a bound: - -1. Bootstrap covers the prescribed initial conditions and establishes B. It must - not replace them with a convenient subset or an empty initial relation. -2. For every B(q) and Allowed(e, u), J has a successor. Unsupported admission - produces an explicit error, not an absent transition. Establish this totality - structurally or with a quantified/exhaustive check; it is not implied by the - bounded SAT query below. -3. Every such admission satisfies Entry(x0, u), including all admitted choices. -4. T_hold is total. Stable states have identity successors, preserving the - entire state, not merely the truth of Stable. -5. Every completed episode from B returns to B. Together with bootstrap, this - supplies induction over arbitrarily many external transactions. - -Make Stable absorbing within this episode. Totalize non-stable deadlocks and -modeled failures as absorbing error states for which Stable is false, or prove -separately that such states are unreachable. Short failing paths must survive -to depth K rather than disappear from the formula. For a candidate K, prove -the following formula unsatisfiable: - - Entry(x0, u) - AND T_hold(x0, x1, u) AND ... AND T_hold(x[K-1], x[K], u) - AND NOT Stable(x[K]) - -This is our proposed specialization of the published finite-state result, not -a latch-specific algorithm quoted from [R3]. It covers all represented entry -states and scheduling choices, not just states visited in a reset simulation. -An entry-state invariant or over-approximation must cover every reachable -episode entry; restrictions on impossible states require justification. - -After proving the obligation, K copies of the same transition logic implement -the complete settling episode. Early completion is padded with identity steps. -All intermediate capture/reset effects are still computed inside those copies. - -### Exact bounded-compilation theorem - -Initially retain the complete quiescent reference state as the macrostate. -Removing a field requires a reconstruction or future-behavior preservation -proof; being irrelevant to the current outputs is not sufficient. Define: +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 [R5, R9]. 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 -M(q, u, q_next) iff there exist x0, ..., xK such that - J(q, u, x0) - AND T_hold(x0, x1, u) AND ... AND T_hold(x[K-1], xK, u) - AND q_next = xK +Entry(x0, u) +AND T(x0, x1, u) AND ... AND T(x[K-1], xK, u) +AND NOT Settled(xK) ``` -**Claim, under the obligations above:** M equals the reference relation from one -completed external boundary to the next, for states in B and allowed inputs. - -**Proof sketch.** Every permitted reference episode reaches its first stable -state by K; identity padding extends it to length K without changing its result, -giving a witness for M. Conversely, every M witness reaches stability by K; -trimming its identity suffix yields a permitted reference episode with exactly -the same complete final state. Boundary-invariant closure permits concatenating -this argument, proving equality of boundary traces for any transaction sequence. -This is our relational-compilation argument using [R3, R4, R9], not a theorem -quoted verbatim from a latch-specific paper. Turning M into one next-state -function requires the separate uniqueness check in Section 9. - -### A finite decision procedure, not a guessed depth - -With frozen stimulus, explore the complete internal states reachable from the -episode entries. A reachable cycle entirely outside Stable gives a non-settling -execution. Otherwise the nonstable graph is acyclic, and its longest path to -stability supplies a sufficient K. The number of nonstable states is a coarse -upper bound, at most 2^b for a b-bit complete-state encoding. This is a finite -theoretical procedure, not a claim that exhaustive exploration is practical. - -An implementation can search candidate bounds with the safety query, use -symbolic cycle checks, or use ranking proofs. Resource exhaustion means an -unproved/unsupported case. A failure found only from an over-approximate B or -Entry must be concretized before being reported as a reachable design defect; -a successful universal certificate over that over-approximation remains sound. - -Alternatives include a decreasing ranking function or a liveness-to-safety -check for a non-quiescent repeating execution [R4]. Event history and pending -work matter when identifying repeated states. A proof of eventual convergence -does not by itself provide a convenient small numerical bound. - -Important limits: - -- A counterexample at depth K can mean only that more steps are needed. -- A timeout is an unproven bound, not a proof of convergence or oscillation. -- Fair eventual scheduling alone permits arbitrary postponement and does not - establish a uniform bound on scheduler steps. -- Counting latches, taking graph depth through a cycle, or using an ordinary - shortest-path reachability diameter is not a general bound on settling. -- A bound can be too large for useful unfolding. - -Initially, certify complete event-connected episodes. Local bounds require -contracts describing incoming events, not an assumption that neighboring -regions stay fixed. Do not combine component bounds by an unjustified maximum -or sum and assume that the complete design is covered. - -## 8. Safe Scheduling Reduction and Parallelism - -### 8.1 First construction: preserve reference-wave epochs - -Local scheduling need not mean changing the circuit's event order. Use the -following concrete construction, initially with the certified complete-episode -bound K from Section 7: - -1. Partition primitive evaluations into islands using the full data/control - dependency graph. SCCs may guide grouping but are not the correctness proof. -2. Associate every boundary value, relevant history, and activity indication - with its reference wave number, or epoch. At epoch k an island reads only the - complete epoch-k inputs and its epoch-k local state, producing epoch-(k+1) - updates. -3. Each predecessor supplies either its value/update or an explicit - unchanged/epoch-complete indication. Do not interpret silence as no change, - and never combine input values from different epochs. -4. Preserve every relevant boundary transition. A pulse is an opening and a - closing in their respective epochs, even when its final value equals its - initial value. Lossless compression of unchanged intervals is permitted. -5. Schedule a computation only after all its input epochs are complete. Include - shared error, quiescence, and environment monitors in this dependency rule; - their reductions may require a barrier. A locally idle island must still - receive later events and must not declare the episode globally stable. -6. Build or evaluate the finite epoch-indexed dependency graph through K. Every - task completes once; worker waiting is not an extra circuit step. Identity - padding is permitted only where it agrees with the reference, including its - global stable/error guards. - -For symbolic compilation these interfaces are epoch-indexed expressions and -activity guards, not necessarily runtime event queues inside the SEC machine. -Workers may evaluate independent tasks in parallel. Cross-island dependency -cycles are handled by successive epochs; they do not become same-epoch circular -equations. Computing each island's entire K-wave boundary trace in topological -order is another option only if the full inter-island dependency graph is -acyclic. Arbitrarily merging nonadjacent SCCs need not preserve that property. - -**Preservation claim.** This construction has exactly the reference's full state -at each epoch, or the same set of full-state traces when primitive choices remain -nondeterministic. - -**Proof sketch.** At entry, each island receives its exact projection of the -reference state. Assume equality at epoch k. The complete-input rule gives each -primitive the same activation, values, controls, and history as the reference; -it therefore has the same permitted staged updates. Unique output ownership and -the same monitor rules produce the same committed epoch-(k+1) state. Induction -proves equality through K. Nondeterministic choices retain their identities and -constraints across fanout; duplicating a producer must not create independent -copies of its choice. Matching choices gives both directions of trace inclusion. - -This is our direct scheduling proof for the selected reference. It closes the -local-scheduling gap without assuming an island can settle atomically or relying -on a general partial-order-reduction theorem whose hypotheses were not checked. - -**Why complete epochs matter.** Suppose two branches make a and b rise in the -same reference update, and their XOR enables a latch. The reference sees both -new values together, so the XOR stays low. A scheduler that reads one new value -and one old value invents an enable pulse and can change stored state. This -failure needs no feedback loop; finding SCCs alone does not prevent it. - -### 8.2 Optional stronger reductions - -Termination and order independence are separate properties. All schedules may -terminate yet produce different stored values. A single canonical schedule is -valid only if it is the declared reference semantics or is proven to represent -all permitted observable outcomes. Otherwise retain nondeterminism. - -Confluence and partial-order reduction results [R6, R7] provide sufficient -conditions for selected scheduling reductions. Applying them here requires a -mapping from latch events to their formal hypotheses; an SCC partition is not -that proof. - -For each proposed reordering, establish that it preserves activation conditions, -capture behavior, relevant observations, and progress. Read/write dependencies -must include control history, event enqueue/cancel effects, and shared monitors. -Only steps invisible under the declared observation contract may be hidden. -Matching final outputs alone is not enough to justify reordering or hiding. - -McDonald and Bryant [R8] demonstrate local event queues for symbolic timing -simulation. Their timing assumptions and event-cluster definition differ from -the proposed zero-delay latch islands. This is an optimization reference, not -the correctness basis for the complete SEC model. - -Practical parallelism can begin conservatively: - -- Parallel extraction and independent graph analyses with deterministic results. -- Parallel primitive evaluations within a reference evaluation wave, reading - the same immutable snapshot and staging updates for the same update barrier. -- Parallel proof jobs for independent regions with validated boundary contracts. - -Do not let worker completion order determine latch capture. The epoch-preserving -construction above may compute an island's complete trace locally, but must -still expose its relevant boundary trace. Publishing only a final settled value -needs an additional proof that discarded transitions cannot affect external -storage, control-event detection, errors, or required observations, and that -retained state preserves all future observations. Atomic summaries, shortcuts -across epochs, and alternative scheduling semantics remain separate extensions. - -## 9. Connection to SEC - -The internal-round abstraction and transparent-latch example in [R9] support -the idea of grouping propagation into observations. The stronger progress and -interface-preservation obligations below are part of our proposed adaptation. - -Supply both designs with the same allowed external transaction sequence. Each -design may require a different number of internal steps. Compare corresponding -completed observations, not identically numbered internal steps. - -In a reference paired model, a design that finishes early waits at the boundary -while the other finishes; no new external transaction is accepted prematurely. -With certified bounded compilation, each side can instead expose its complete -episode as a boundary-to-boundary transition. - -Two independent obligations are required: - -1. **Progress:** every accepted episode completes within its certified bound, - or an explicitly supported liveness analysis establishes completion. -2. **Agreement:** the required outputs agree at matched observation boundaries - under the specified initialization/reset relation. - -Only checking agreement when both designs finish can pass vacuously if a design -never finishes. A bound overflow must therefore be an error or unsupported -result, never an assumption excluding that execution. - -### 9.1 A sufficient determinism check - -For the first conventional SEC path, use a stronger, concrete condition than -same-episode output equality: prove uniqueness of the complete retained boundary -state. With the macro relation M from Section 7, require this query to be -unsatisfiable: +Here u is held external stimulus; T is one complete internal round. +Proving this formula impossible establishes the bound [R3, R4]. +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(e, u) - AND M(q, u, a) AND M(q, u, b) - AND a != b +B(q) AND Allowed(u) AND Episode(q, u, a) AND Episode(q, u, b) +IMPLIES a = b ``` -The two copies share the **pre-admission** state q and external transaction u, -but independently choose admission outcomes and all permitted primitive orders. -Sharing a particular post-admission x0 could hide nondeterminism in J. Initial -storage bits already belong to q; they are not independently resampled for the -two executions. Bootstrap separately checks auxiliary-seed independence as -specified in Section 5.3. - -This test is conservative: two different internal states might still have the -same future observable behavior. Accepting them would require a proved -behavioral quotient or a separate all-future-observations check. Comparing only -the current output vector is insufficient; a later input may expose a hidden -latch-state difference. A failure on an over-approximate B is not automatically -a reachable design defect, but it prevents acceptance without refinement. - -As another sufficient route, primitive pin-order independence for every admitted -activation implies deterministic waves under the unique-writer barrier model. -The local commutation results in [R2] support such checks. They do not establish -network termination, bootstrap independence, or regional abstraction by -themselves. The complete-boundary test can also accept cases where local -ambiguity disappears before the full state settles. - -### 9.2 From a relation to ordinary sequential equivalence - -Admission totality, certified progress, and boundary uniqueness make M a total -function on B and the allowed transactions. A compiler can retain the relation -or implement it using a legal deterministic selection of its choices, after -showing that the selection realizes M. Uniqueness does not validate arbitrary -new event rules or an independently written scheduler. - -For two accepted designs, fix an explicit initial/reset relation R0 and a shared -environment transaction sequence. Each design uses its own compiled function -and its own certified K. An inductive paired-state invariant R must: - -1. Contain all initial pairs prescribed by R0, including the bootstrap results. -2. Be preserved by both compiled transitions under every shared allowed input. -3. Imply equality of the required observations at corresponding boundaries. - -Together with each design's independent progress certificate, these conditions -prove the stated SEC property by induction. They do not change the initial-state -quantification or establish equivalence under a different environment. Choosing -a favorable subset of initial pairs or implementation-specific input assumptions -would not prove the declared contract. - -If complete-boundary uniqueness is not established, the first path reports -unsupported/opaque under Section 11. It does not align two arbitrary scheduler -choices merely to obtain equal outputs. General nondeterministic trace-set -equivalence is a separate extension, not a prerequisite of this construction. - -Reset bootstrap and cycle counts must also distinguish external clock/reset -events from internal settling steps. An internal propagation round is not an -additional hardware reset cycle. - -The macrostep here is an admitted external transaction, not automatically the -existing SEC clock tick. Connecting this construction to existing clock-cycle -observations, reset counters, and exporters requires an explicit adapter. A -cycle-level reduction may hide transactions only after preserving their capture -effects and the required observation relation. Until that adapter is established, -the mathematical construction must not be advertised as existing-backend support. - -## 10. Optional Phase Optimization - -Phase discovery [R10] belongs after construction of a valid transition system. It -cannot repair missed enable pulses, unspecified event order, or unsafe loop -handling. - -The intended periodic-analysis layer starts from a normalized sequential -machine with an explicitly defined step, performs reset-based ternary analysis, -and looks for deterministic periodic state signals. It does not begin by -assigning a phase to each latch or grouping latches solely by syntactically equal -enables. A latch-specific adapter -can then interpret enable expressions relative to discovered carriers while -retaining residual local gating. - -Scheduler activity is not automatically a hardware phase: a period measured in -internal propagation steps must not be interpreted as a circuit clock period. -The relationship between the selected transition-system step and SEC observation -boundaries has to be preserved. - -Any subsequent phase abstraction needs its own observation-preservation -conditions. Failure to discover a periodic carrier is a valid outcome: retain -the unreduced model. Phase reduction is optional; generic latch semantics must -not depend on its success. - -## 11. Unsupported Cases and Diagnostics - -The proposed bounded-settling path cannot automatically accept: - -- **Non-settling feedback.** For example, a known Boolean value circulating - through an open latch and an inverter can alternate indefinitely under the - chosen propagation semantics. Unknown-value initialization must not be used - to disguise that case as a useful settled Boolean result. -- **Order-dependent boundary results.** This can occur with or without feedback. - The first path requires complete-boundary uniqueness; failure or inability to - prove it leaves the affected behavior unsupported, not arbitrarily resolved. -- **Unproven or impractical bounds.** A valid circuit may fall outside the - resource limits of this compiler. -- **Unmodeled timing or primitives.** Physical delay-sensitive behavior, - metastability, unresolved asynchronous-control rules, and unsupported - multi-driver/unknown-value semantics are outside the declared contract. - -A history-preserving self-feedback latch is not inherently unsupported. Several -stable values are also not inherently a problem when prior state and permitted -events determine which value is reached. - -Diagnostics should identify the affected region, relevant latch/control paths, -the failed obligation, and the impacted observed outputs. Distinguish a proven -non-settling trace from an unproven bound and from a resource limit. Preserve -explicit checked-output coverage rather than representing skipped behavior by -free shared symbols or claiming a complete SEC pass. - -### Default opaque behavior and optional error mode - -Keep the existing opaque-handling policy as the default. A latch must no longer -be classified as opaque merely because it is a latch: model its behavior when -the strategy's semantic and proof requirements are satisfied. Latch behavior -that cannot be modeled safely remains opaque, as do other unsupported cells -and signals. Ordinary propagation of opacity through dependent logic still -applies. - -By default, encountering opacity is not itself a fatal error. Preserve the -existing opaque diagnostics/reports and affected-output skipping behavior, -continue checking supported outputs, and report the resulting coverage. Skipped -outputs are not proved equivalent, and opaque signals must not be replaced by -unconstrained shared values to obtain a proof. - -Propagate opacity through data and event/control dependencies, not only data -cones. Certificates for remaining dependency-closed modeled cones make no claim -about progress or equivalence of the excluded behavior. Reporting an unsupported -region is not permission to drop one of its executions inside a purported proof -of that region. - -Add a separate opt-in error-on-opaque switch, **disabled by default**. When -enabled, encountering an opaque cell or signal during SEC model construction -for either design must stop the run with an error and a nonzero exit status, -rather than continuing with partial coverage. The diagnostic must identify the -design, hierarchical cell or signal, and reason for opacity. This policy applies -to opacity generally, not only to unsupported latches; it does not change which -behavior the modeling strategy supports. The implemented spellings are -`--error-on-opaque` and YAML/Python `error_on_opaque`, independently default-off. - -## 12. Proposed Implementation and Validation Stages - -The stages below remain the design's acceptance checklist. The core Boolean -implementation realizes stages 1-5 with explicit primitive callbacks, BOOT, -concrete and symbolic waves, dependency regions, SAT certificates, and bounded -compilation. Stage 6 uses an explicit **external-transaction** backend adapter: -native SEC, witnesses, and BTOR2 share that step meaning. Reset bootstrap uses -an automatic cycle adapter for the supported single-clock/single-reset subset: -assert reset, sample unconstrained data, generate both clock edges with full -settling, and release reset after N complete cycles. It never counts propagation -waves as cycles. Ambiguous clock protocols and selected leaf boundaries remain -unsupported; see the implementation document for the exact reset input-arrival -contract and diagnostics. Stage 7 remains optional, as originally proposed. See the implementation -document for exact supported frontend and resource boundaries. - -The entire latch path remains behind `latch_support`, default-on. Explicit YAML -`latch_support: false`, CLI `--no-latch_support`, or Python `latch_support=False` -disables it. Default enablement does not assume initial values or input timing: -without any event settings, legacy SEC/LEC behavior is preserved. A complete -contract activates event modeling; partial contracts or tuning without the -required fields are errors, as is tuning while explicitly disabled. With the -switch off or no event settings supplied, reset uses the existing bootstrap -implementation unchanged; the event adapter's additional clock/reset -restrictions do not apply to legacy runs. - -1. Encode and review the explicit primitive tables, Boolean bootstrap, shared - environment, and observation contract specified here; supply missing frontend - models without inventing semantics from names. -2. Implement a small executable reference model and symbolic transition encoder - with the same evaluate/update and sticky-error rules. -3. Establish bootstrap progress, admission totality, and boundary invariants. - Support acyclic propagation and provably inactive loops before active-loop - certification; these still require correct data/control event handling. -4. Implement the bounded-progress and boundary-uniqueness checks, then exact - bounded compilation. Validate that compiled transitions realize the reference - macro relation, including all retained state and error effects. -5. Implement epoch-complete regional compilation/parallel scheduling and validate - the per-wave preservation argument. Stronger atomic summaries remain optional. -6. Integrate paired observations, the cycle/reset adapter, exporters, coverage, - and the default-off error-on-opaque policy with SEC. -7. Add optional phase abstraction or stronger scheduling reductions only with - their own observation-preservation conditions. - -Essential regressions include: - -- Open/closed latch behavior and a chain of four simultaneously open latches. -- Self-feedback retaining both possible previous Boolean values. -- Complementary-enable feedback with a provably closed path. -- A known-state inverting loop that cannot settle. -- A convergent example needing more than the first attempted bound. -- Simultaneous data/enable or data/clock changes under the explicit contract. -- A flip-flop clock edge consumed once, not reused on a later data-pin visit. -- Bootstrap with an initially open latch, independent auxiliary net seeds, and - generated clock events, without a fabricated initial flip-flop edge. -- Internally generated enable/reset/clock pulses that capture state, including - pulses crossing a proposed island boundary. -- Equal final boundary signals but different transiently captured state. -- Reconvergent same-wave changes without mixed-epoch enable pulses. -- Shared nondeterministic producer choices preserved across island fanout. -- Equal current outputs but hidden latch states distinguishable by a later input. -- Different internal settling depths on equivalent designs. -- One side failing to settle, preventing a vacuous equivalence result. -- Missing admission successors and short error paths that must not disappear - from the bounded query; independent admission choices in uniqueness checks. -- Explicit X/reset behavior, unproven-bound reporting, and partial coverage. -- Supported latches becoming modeled while unsupported latch behavior remains - opaque, with existing diagnostics and affected-output skipping by default. -- Error-on-opaque disabled by default, and explicit enablement producing an error - for an opaque cell or signal in either design, including non-latch opacity. -- Identical semantics and results under different worker counts. - -Differential simulation is useful for examples, but does not replace universal -bound, scheduling-preservation, and observation-correspondence obligations. - -## References and What They Establish - -### R1. Hjort: latch building block and modeling pitfalls - -Håkan Hjort, *On Applying Model Checking in Formal Verification*, FMCAD 2022 -tutorial. [Presentation](https://fmcad.org/FMCAD22/presentations/00%20-%20tutorials/02_hjort.pdf). -Printed slides 38-41 discuss the common discrete time base and sequential -elements; slide 41 gives the latch state/bypass construction; slides 44-50 -discuss feedback and delta propagation. This reference is the tutorial slide -deck, not the separate one-page published tutorial abstract. It motivates the -building block and its hazards, not a universal safe sampling interval or a -general settling bound. - -### R2. Raffelsieper, Roorda, Mousavi: explicit formal event semantics - -Matthias Raffelsieper, Jan-Willem Roorda, MohammadReza Mousavi, -*Model Checking Verilog Descriptions of Cell Libraries*, ACSD 2009, pp. 128-137. -[Publication record](https://research.tue.nl/en/publications/model-checking-verilog-descriptions-of-cell-libraries). -[DOI](https://doi.org/10.1109/ACSD.2009.18). -[Indexed author PDF](https://www.cs.le.ac.uk/people/mm789/pub/mousavi_acsd_2009.pdf). -The author PDF's indexed text was available during review, but direct download -returned an error; the publication record is included as a stable locator. - -An accessible later author treatment is Matthias Raffelsieper's 2011 TU Eindhoven -dissertation, *Cell Libraries and Verification*, Chapter 3, printed pp. 15-31, -explicitly based on the 2009 paper. -[University PDF](https://pure.tue.nl/ws/files/3499974/717717.pdf#page=24). -[Repository record](https://research.tue.nl/en/publications/cell-libraries-and-verification). -This is a later treatment, not the identical conference article. - -For the selected reference, the detailed anchors are Chapter 3, Section 3.1, -printed pp. 20-25 (pin-order evaluation and Table 3.2's operational rules), and -Section 3.2, printed p. 27 (the implementation's fixed-order restriction). -Chapter 4, Section 4.1 gives local order-independence/commutation checks. These -do not automatically prove settling or safe island abstraction. Chapter 5's -translation of timing restrictions through combinational input logic has -additional assumptions; such restrictions must be checked on the actual -activations of our wave model. The Boolean bootstrap and preservation proofs in -this proposal are explicitly our adaptations, not claims made by the thesis. - -Sections 2-3 of the conference paper define primitive/history semantics, -execute/update iteration, and a Boolean transition-system encoding. Section 4 -compares stable outputs and separately checks eventual stability. The published -encoding is zero-delay and -fixes a UDP input-processing order. Its example equivalence checks restrict -external changes and treat X outputs as don't-cares. Those choices are not -implicit Kepler defaults. Experiments concern cell libraries, not proof of -whole-design scalability. - -### R3. Claessen and Sörensson: proving finite bounds - -Koen Claessen and Niklas Sörensson, *A Liveness Checking Algorithm that Counts*, -FMCAD 2012, pp. 52-59. -[Proceedings, paper](https://www.cs.utexas.edu/~hunt/fmcad/FMCAD12/fmcad2012.pdf#page=59). -Section III-A, printed p. 53 (PDF p. 60), states the finite-state eventuality -bound; Section III-B develops the related k-liveness result. It supports -checking a proposed settling bound as a safety obligation. Applying it to a -complete, history-preserving latch episode is our specialization. It does not -provide a small bound from the number of latches. - -### R4. Schuppan and Biere: checking progress through safety - -Viktor Schuppan and Armin Biere, *Efficient Reduction of Finite State Model -Checking to Reachability Analysis*, STTT 5, 2004, pp. 185-204. -[Author preprint](https://www.schuppan.de/viktor/VSchuppanABiere-STTT-2004.pdf). -Sections 2 and 6 describe and justify state-recording reductions of liveness to -safety. This is the extended, corrected follow-up to *Liveness Checking as -Safety Checking* (Biere, Artho, Schuppan, 2002). It supplies a formal route for -detecting non-settling executions; it does not establish that a latch circuit -converges or that the proof is inexpensive. - -### R5. DeVane: trigger-based circuit regions - -Charles J. DeVane, *Efficient Circuit Partitioning to Extend Cycle Simulation -Beyond Synchronous Circuits*, ICCAD 1997, pp. 154-161. -[Paper](https://cecs.uci.edu/~papers/compendium94-03/papers/1997/iccad97/pdffiles/03a_1.pdf). -Sections 3.5-3.7 and 4 treat generated clocks, asynchronous controls, transparent -latches, and trigger-based partitioning. The principal algorithm assumes no -combinational feedback; separate handling is discussed. This is a scheduling -precedent, not a proof of bounded latch-loop unfolding or of our SEC adaptation. - -### R6. Lang and Mateescu: compositional scheduling reduction - -Frédéric Lang and Radu Mateescu, *Partial Order Reductions using Compositional -Confluence Detection*, FM 2009; full report INRIA RR-7078. -[Official publication page](https://cadp.inria.fr/publications/Lang-Mateescu-09.html). -The report gives conditions for lifting local confluence through composition -and prioritizing suitable acyclic invisible transitions while preserving -branching equivalence. These are conditional proof tools, not automatic -permission to serialize arbitrary latch regions. - -### R7. Neele, Valmari, Willemse: observation-preserving reduction - -Thomas Neele, Antti Valmari, Tim A. C. Willemse, *A Detailed Account of the -Inconsistent Labelling Problem of Stutter-Preserving Partial-Order Reduction*, -Logical Methods in Computer Science 17(3), 2021. -[Paper](https://lmcs.episciences.org/7709/pdf). -Section 5 gives corrected conditions and proofs for stutter-trace preservation. -Its counterexamples explain why endpoint agreement alone is insufficient when -intermediate state observations matter. Our observation mapping must satisfy -the selected reduction's conditions. - -### R8. McDonald and Bryant: local symbolic event queues - -Clayton B. McDonald and Randal E. Bryant, *Symbolic Timing Simulation Using -Cluster Scheduling*, DAC 2000, pp. 254-259. -[Author PDF](https://www.cs.cmu.edu/~bryant/pubdir/dac00a.pdf). -Section 3.2 describes local event queues and ordering safeguards. Event clusters -in this work are not synonymous with latch-loop components. Its symbolic -timing model is an optimization reference, not a direct correctness theorem for -our proposed zero-delay SEC frontend. - -### R9. Alur and Henzinger: internal rounds and round abstraction - -Rajeev Alur and Thomas A. Henzinger, *Reactive Modules*, Formal Methods in System -Design 15, 1999, pp. 7-48. -[Paper](https://www.cis.upenn.edu/~alur/FMSD99.pdf). -Section 6.2, printed pp. 37-38 (PDF pp. 31-32), Figure 13, explicitly treats -transparent-latch feedback and internal stabilization. Sections 6.1 and 6.3 -provide round abstraction and triggering. This supports combining internal -rounds under stated conditions; it does not prove universal bounded convergence -for arbitrary latch networks. In particular, its round-marker continuation -condition must not be substituted for our stronger all-executions progress -obligation. - -### R10. Bjesse and Kukula: later phase abstraction - -Per Bjesse and James Kukula, *Automatic Generalized Phase Abstraction for Formal -Verification*, ICCAD 2005. -[Author PDF](http://www.perbjesse.com/iccad05.pdf). -[DOI](https://doi.org/10.1109/ICCAD.2005.1560220). -Section II assumes clock/latch modeling and combinational-loop resolution have -already occurred. Its periodic analysis and abstraction operate on that -normalized machine. It supports the optional optimization layer, not the -construction of correct arbitrary-latch event semantics. - -## Resolved Algorithmic Choices and Remaining Work - -The previously open construction is now specialized as follows: - -- **Reference semantics:** finite, single-writer Boolean primitives; - snapshot-based waves; permitted pin-order alternatives retained; complete - cell-specific control rules; explicit Boolean bootstrap and sticky errors. -- **Compilation:** total admission and internal transitions, an inductive - boundary invariant, a certified all-executions bound, identity padding, and - retention of complete boundary state. Section 7 supplies the exactness proof. -- **Local scheduling:** complete epoch-tagged inputs, preserved boundary events - and choice correlations, with the per-wave induction in Section 8. Atomic - settle-and-publish islands are not assumed. -- **SEC:** complete-boundary uniqueness before deterministic compilation; - shared external transactions and explicit initial relation; independent - progress and paired-observation obligations, as specified in Section 9. -- **Fallback:** unsupported behavior stays opaque by default, with existing - reporting/skipping; error-on-opaque is a separate default-off switch. - -These close the logical construction under the stated hypotheses; they are not -a machine-checked theorem or a proof that arbitrary asynchronous hardware -satisfies those hypotheses. The explicitly configured event implementation now -instantiates the primitive tables, bootstrap, reference and symbolic compilers, certificates, -dependency regions, resource limits, and SEC reset/export adapters, with tests -described in [the implementation document](sec-latch-implementation.md). -Tests and per-component SAT certificates do not establish that this Boolean -contract models every physical circuit. The environment/initial-state contract -must remain explicit in user configuration; it must not be inferred to make a -proof succeed. - -Extensions still requiring separate arguments include genuine multivalued -semantics, input changes before an episode settles, physical timing and -metastability, nondeterministic trace-set equivalence, atomic regional summaries, -and transformations that alter primitive granularity or internal event order. -The cited research supports the foundations; the short specialization proofs -here are our own and remain subject to implementation validation. +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 [R3, R4, R9]. + +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 [R6–R8]. + +## 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 [R10]. 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. + +## References + +- **R1.** Håkan Hjort, [On Applying Model Checking in Formal Verification](https://fmcad.org/FMCAD22/presentations/00%20-%20tutorials/02_hjort.pdf), + FMCAD 2022 tutorial, slides 41 and 44–50: latch register/mux construction and + feedback hazards; no general safe sampling interval or settling bound. +- **R2.** Raffelsieper, Roorda, Mousavi, + [Model Checking Verilog Descriptions of Cell Libraries](https://doi.org/10.1109/ACSD.2009.18), + ACSD 2009. Accessible treatment: + [Cell Libraries and Verification, Chapter 3](https://pure.tue.nl/ws/files/3499974/717717.pdf#page=24), + 2011, especially pp. 20–27: pin history, evaluation, and updates. + Published fixed-order/input restrictions and unknown-value conventions are + not adopted implicitly here; the experiments do not establish whole-design scalability. +- **R3.** Claessen and Sörensson, + [A Liveness Checking Algorithm that Counts](https://www.cs.utexas.edu/~hunt/fmcad/FMCAD12/fmcad2012.pdf#page=59), + FMCAD 2012, Section III-A: finite-state eventuality bounds. + Applying the result to complete latch episodes is our specialization. +- **R4.** Schuppan and Biere, + [Efficient Reduction of Finite State Model Checking to Reachability Analysis](https://www.schuppan.de/viktor/VSchuppanABiere-STTT-2004.pdf), + 2004: liveness-to-safety reasoning; no guarantee that a particular loop settles. +- **R5.** DeVane, + [Efficient Circuit Partitioning to Extend Cycle Simulation Beyond Synchronous Circuits](https://cecs.uci.edu/~papers/compendium94-03/papers/1997/iccad97/pdffiles/03a_1.pdf), + ICCAD 1997, Sections 3.5–4: trigger-based regions, latches, generated clocks, + and asynchronous controls. The principal algorithm restricts combinational feedback. +- **R6.** Lang and Mateescu, + [Partial Order Reductions using Compositional Confluence Detection](https://cadp.inria.fr/publications/Lang-Mateescu-09.html), + FM 2009: conditional composition and reordering results, not permission to + serialize arbitrary latch regions. +- **R7.** Neele, Valmari, Willemse, + [A Detailed Account of the Inconsistent Labelling Problem of Stutter-Preserving Partial-Order Reduction](https://lmcs.episciences.org/7709/pdf), + 2021, Section 5: conditions for preserving observations under reduction. +- **R8.** McDonald and Bryant, + [Symbolic Timing Simulation Using Cluster Scheduling](https://www.cs.cmu.edu/~bryant/pubdir/dac00a.pdf), + DAC 2000, Section 3.2: local symbolic event queues with ordering safeguards; + not a proof of this zero-delay latch model. +- **R9.** Alur and Henzinger, + [Reactive Modules](https://www.cis.upenn.edu/~alur/FMSD99.pdf), + 1999, Section 6.2, pp. 37–38, Figure 13: transparent-latch feedback and + internal-round abstraction; not universal convergence of arbitrary loops. +- **R10.** Bjesse and Kukula, + [Automatic Generalized Phase Abstraction for Formal Verification](http://www.perbjesse.com/iccad05.pdf), + ICCAD 2005: periodic reduction after correct latch/clock modeling and loop + resolution, not a replacement for those foundations. diff --git a/docs/sec-reset-bootstrap.md b/docs/sec-reset-bootstrap.md index 0048384e..00c6884f 100644 --- a/docs/sec-reset-bootstrap.md +++ b/docs/sec-reset-bootstrap.md @@ -4,13 +4,9 @@ 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. -This behavior is unchanged when `latch_support` is off, or for latch-free designs -without explicit event tuning. The switch is on by default and requires no -initialization settings. With event modeling active, `cycles` -still counts clock cycles, but an explicit event adapter generates the clock -edges and latch settling. Its supported clock/reset subset and stimulus protocol -are described in the -[latch reset-cycle adapter](sec-latch-implementation.md#reset-cycle-adapter). +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 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 From a0a1a33288c74b1232b6c8c402954ed28e48efa4 Mon Sep 17 00:00:00 2001 From: Noam Cohen Date: Mon, 28 Sep 2026 17:32:43 +0200 Subject: [PATCH 09/10] docs(sec): shorten and wrap latch diagram labels --- docs/sec-latch-implementation.md | 26 +++++++++++++------------- docs/sec-latch-support.md | 8 ++++---- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/docs/sec-latch-implementation.md b/docs/sec-latch-implementation.md index 94099bae..5756eee5 100644 --- a/docs/sec-latch-implementation.md +++ b/docs/sec-latch-implementation.md @@ -34,10 +34,10 @@ 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[Visible output Q] + 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 ``` @@ -51,16 +51,16 @@ storage and pin history left by the preceding visit: ```mermaid flowchart TD - A[Start with incoming storage and previous pin values] --> P - subgraph L[One composed latch primitive] - P[Apply next changed pin in the chosen order] --> M - M[Select data or remembered storage] --> R - R[Update private storage and output from mux result] - R -->|More changed pins: carry history forward| P + 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 together with the complete wave] - W --> N[Changed outputs activate consumers for next wave] + 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 diff --git a/docs/sec-latch-support.md b/docs/sec-latch-support.md index cd1b294e..a38d8894 100644 --- a/docs/sec-latch-support.md +++ b/docs/sec-latch-support.md @@ -57,11 +57,11 @@ remain visible to downstream elements. This primitive granularity is part of the model, not a claim to reproduce arbitrary physical glitches [R2]. ```mermaid -flowchart LR - A[External transaction] --> E[Evaluate activated elements] - E --> C[Commit updates together] +flowchart TD + A["External
transaction"] --> E["Evaluate
active elements"] + E --> C["Commit together"] C -->|Signal changes| E - C -->|No pending work or error| B[Settled observation] + C -->|"No pending work
and no error"| B["Settled observation"] ``` A simultaneous data change and latch closure can retain either old or new From fe26f4dc2c782af641188c20f1e8166db7d02cac Mon Sep 17 00:00:00 2001 From: Noam Cohen Date: Wed, 30 Sep 2026 17:40:09 +0200 Subject: [PATCH 10/10] update docs --- docs/sec-flags-spec.md | 3 -- docs/sec-internal-relations.md | 20 +++++------ docs/sec-latch-support.md | 62 ++++++---------------------------- 3 files changed, 19 insertions(+), 66 deletions(-) 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-support.md b/docs/sec-latch-support.md index a38d8894..18afaf9c 100644 --- a/docs/sec-latch-support.md +++ b/docs/sec-latch-support.md @@ -15,8 +15,9 @@ contains the primitive diagrams and event examples. 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. The cited research supports -individual steps; no single article proves this complete combination. +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 @@ -28,7 +29,7 @@ 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 [R1]. +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 @@ -54,7 +55,7 @@ Each round: 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 [R2]. +the model, not a claim to reproduce arbitrary physical glitches. ```mermaid flowchart TD @@ -103,7 +104,7 @@ Find these paths using both data and control dependencies. change during propagation. A scheduling region is not necessarily a feedback loop: it may contain several -loops and connecting logic [R5, R9]. Flip-flop data paths can supply boundaries +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. @@ -146,7 +147,7 @@ AND NOT Settled(xK) ``` Here u is held external stimulus; T is one complete internal round. -Proving this formula impossible establishes the bound [R3, R4]. +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 @@ -185,7 +186,7 @@ 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 [R3, R4, R9]. +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. @@ -211,7 +212,7 @@ 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 [R6–R8]. +needs stronger preservation arguments. ## 7. Equivalence and unsupported behavior @@ -240,51 +241,8 @@ 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 [R10]. Preserve residual gating, +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. - -## References - -- **R1.** Håkan Hjort, [On Applying Model Checking in Formal Verification](https://fmcad.org/FMCAD22/presentations/00%20-%20tutorials/02_hjort.pdf), - FMCAD 2022 tutorial, slides 41 and 44–50: latch register/mux construction and - feedback hazards; no general safe sampling interval or settling bound. -- **R2.** Raffelsieper, Roorda, Mousavi, - [Model Checking Verilog Descriptions of Cell Libraries](https://doi.org/10.1109/ACSD.2009.18), - ACSD 2009. Accessible treatment: - [Cell Libraries and Verification, Chapter 3](https://pure.tue.nl/ws/files/3499974/717717.pdf#page=24), - 2011, especially pp. 20–27: pin history, evaluation, and updates. - Published fixed-order/input restrictions and unknown-value conventions are - not adopted implicitly here; the experiments do not establish whole-design scalability. -- **R3.** Claessen and Sörensson, - [A Liveness Checking Algorithm that Counts](https://www.cs.utexas.edu/~hunt/fmcad/FMCAD12/fmcad2012.pdf#page=59), - FMCAD 2012, Section III-A: finite-state eventuality bounds. - Applying the result to complete latch episodes is our specialization. -- **R4.** Schuppan and Biere, - [Efficient Reduction of Finite State Model Checking to Reachability Analysis](https://www.schuppan.de/viktor/VSchuppanABiere-STTT-2004.pdf), - 2004: liveness-to-safety reasoning; no guarantee that a particular loop settles. -- **R5.** DeVane, - [Efficient Circuit Partitioning to Extend Cycle Simulation Beyond Synchronous Circuits](https://cecs.uci.edu/~papers/compendium94-03/papers/1997/iccad97/pdffiles/03a_1.pdf), - ICCAD 1997, Sections 3.5–4: trigger-based regions, latches, generated clocks, - and asynchronous controls. The principal algorithm restricts combinational feedback. -- **R6.** Lang and Mateescu, - [Partial Order Reductions using Compositional Confluence Detection](https://cadp.inria.fr/publications/Lang-Mateescu-09.html), - FM 2009: conditional composition and reordering results, not permission to - serialize arbitrary latch regions. -- **R7.** Neele, Valmari, Willemse, - [A Detailed Account of the Inconsistent Labelling Problem of Stutter-Preserving Partial-Order Reduction](https://lmcs.episciences.org/7709/pdf), - 2021, Section 5: conditions for preserving observations under reduction. -- **R8.** McDonald and Bryant, - [Symbolic Timing Simulation Using Cluster Scheduling](https://www.cs.cmu.edu/~bryant/pubdir/dac00a.pdf), - DAC 2000, Section 3.2: local symbolic event queues with ordering safeguards; - not a proof of this zero-delay latch model. -- **R9.** Alur and Henzinger, - [Reactive Modules](https://www.cis.upenn.edu/~alur/FMSD99.pdf), - 1999, Section 6.2, pp. 37–38, Figure 13: transparent-latch feedback and - internal-round abstraction; not universal convergence of arbitrary loops. -- **R10.** Bjesse and Kukula, - [Automatic Generalized Phase Abstraction for Formal Verification](http://www.perbjesse.com/iccad05.pdf), - ICCAD 2005: periodic reduction after correct latch/clock modeling and loop - resolution, not a replacement for those foundations.