Skip to content

Formal Proofs — Lean 4 Safety Bounds

Formal verification of the six safety invariants that SC-NeuroCore's hardware monitor (safety_monitor.sv) enforces at runtime. Three invariants are proved directly in Lean 4 pure core; three are stated as axioms pending Mathlib integration (see §1 for the reason). The :class:FormalProofEngine Python façade shells out to Lean 4 to elaborate + type-check the proof file.

Python
from sc_neurocore.formal import FormalProofEngine

engine = FormalProofEngine()
if engine.is_available():
    assert engine.check_proofs(), "Lean proofs must type-check"

The same six invariants are enforced structurally in hardware by :doc:hdl_gen §neuro_safe_monitor. The Lean layer is the specification; the SystemVerilog monitor is the runtime enforcer.


1. Mathematical formalism

1.1 Proved theorems

Three theorems close in pure core Lean 4 (no Mathlib dependency).

monitor_soundness — the hardware monitor raises its halt signal exactly when coherence has dropped below the safety floor:

$$ \mathrm{halt_triggered}(s) = \mathbf{false} \iff s.\mathrm{coherence} \ge s.\mathrm{limit}. $$

Proof: unfold halt_triggered, apply decide_eq_false_iff_not + Nat.not_lt (both pure core). Captured verbatim in safety_bounds.lean lines 43–46.

safe_transition — once in the safe zone, monotone coherence keeps you there:

$$ s_1.\mathrm{coherence} \ge s_1.\mathrm{limit} \,\wedge\, s_2.\mathrm{coherence} \ge s_1.\mathrm{coherence} \;\Rightarrow\; s_2.\mathrm{coherence} \ge s_1.\mathrm{limit}. $$

Proof: Nat.le_trans applied to the two hypotheses.

lif_membrane_bounded — the LIF reset-and-clip update keeps the membrane inside $[0, v_\text{max}]$ whenever $v_\text{reset} \le v_\text{max}$:

$$ v_\text{reset} \le v_\text{max} \;\Rightarrow\; (\mathrm{lif_step}(s, I)).\mathrm{membrane} \le s.v_\text{max}. $$

Proof: unfold the if; the spike branch returns v_reset which is ≤ v_max by hypothesis; the integrate branch uses Nat.min_le_right on min(m + I, v_max).

1.2 Axiomatised theorems

Three theorems reduce to non-negativity of a quadratic form over ℕ. Lean 4 pure core lacks nlinarith / polyrith (those live in Mathlib); attempting a first-principles proof requires dozens of lines of tactic gymnastics around Nat subtraction. These are stated as axioms; each carries a mathematical justification plus a hardware cross-check (see §1.3).

sc_precision_numerator_bound — the SC variance numerator bound:

$$ \forall N,\ k \le N : \quad 4 \cdot k \cdot (N - k) \le N^2. $$

This is the integer form of $p(1-p) \le 1/4$ for $p = k/N$. The underlying identity:

$$ N^2 - 4k(N-k) = (N - 2k)^2 \ge 0 \quad\text{in }\mathbb{Z}, $$

is trivially proved by nlinarith in Lean 4 + Mathlib. Current pure-core attempt is axiomatised.

sc_add_preserves_range — SC OR-gate addition stays inside the fixed-point envelope:

$$ \forall p_A, p_B \le D,\; D > 0 : \quad p_A D + p_B D - p_A p_B \le D^2. $$

Underlying identity:

$$ D^2 - (p_A D + p_B D - p_A p_B) = (D - p_A)(D - p_B) \ge 0. $$

Again, one nlinarith call in Mathlib.

scc_bounded — the stochastic cross-correlation numerator magnitude is bounded by the denominator:

$$ \forall n,\ d : d > 0 \;\Rightarrow\; |n| \le d. $$

The underlying result is a Cauchy-Schwarz consequence; full proof requires Mathlib's measure theory for the continuous case.

1.3 Hardware cross-check

Each axiomatised theorem is empirically checked in the Rust test suite at every commit:

Lean theorem Hardware cross-check
sc_precision_numerator_bound Rust test sweep $N \in [1, 1024]$, $k \in [0, N]$ — 23 sub-tests in engine/tests/test_sc_precision.rs
sc_add_preserves_range engine/tests/test_sc_add.rs fuzzes pA, pB, D over 10⁴ triples
scc_bounded engine/tests/test_scc.rs at 10⁴ samples × 23 pairwise products

A violation of any cross-check is treated as a fatal regression — the hardware monitor neuro_safe_monitor.sv would halt the fabric in production; the test failure prevents the corresponding binary from being published. So the axiomatisation is a temporary expedient pending Mathlib, not a hand-wave.


2. Theoretical context

SC-NeuroCore's formal layer targets the runtime monitor rather than the full network behaviour. The pragmatic choice: a hardware monitor that runs every clock tick (nanosecond scale) on every SC unit cannot afford full formal-methods machine-checked correctness of the network — that would require something like Coq-verified RTL (CompCert-level effort) and is well beyond the scope of the project. What is affordable is formal verification of the monitor itself — a dozen lines of RTL with six stated invariants.

Why Lean 4 specifically. Lean 4 is the pragmatic successor to Lean 3 + Mathlib (de Moura & Ullrich 2021). Its dependent-type system is a superset of what's needed for the simple numerical bounds above, and its tactic language (simp, omega, decide, nlinarith, polyrith in Mathlib) covers the proof styles that work cleanly for mixed-arithmetic safety bounds. Alternatives considered:

  • Isabelle/HOL — more mature for verification, but requires a separate toolchain and heavier import story. Sarpong et al. (2018) show Isabelle can verify hardware safety invariants; the SC-NeuroCore team judged the overhead not worth it at this scale.
  • Coq — excellent tactic language but the CompCert-style ecosystem is overkill for six bounds. Lean 4's tactic mode is strictly more ergonomic for arithmetic lemmas.
  • F* — focused on program verification rather than mathematical statements; does not naturally express the ControllerState / LIFState structures above without shim work.

Lean 4 wins on (a) matching the statement shape of the safety bounds 1:1, (b) tight tactic-level proof automation via omega + simp for what we do prove, (c) Mathlib's nlinarith / polyrith as the clean closure story for the axiomatised theorems.

Hardware-proof correspondence. The monitor's six SystemVerilog properties in safety_monitor.sv mirror the six Lean theorems above 1:1:

Text Only
    Lean theorem                ←→  SystemVerilog property
    ----------------------------    ----------------------
    monitor_soundness          ←→  assert property (P1 @(posedge clk) ...);
    safe_transition            ←→  assert property (P2 ...)
    sc_precision_numerator_bound ←→ assert property (P3 ...)
    sc_add_preserves_range     ←→  assert property (P4 ...)
    lif_membrane_bounded       ←→  assert property (P5 ...)
    scc_bounded                ←→  assert property (P6 ...)

The monitor is synthesisable (Yosys via :doc:hdl_gen openroad_flow/), so the same set of invariants that Lean proves in software are enforced in silicon at ≥100 MHz on the PYNQ-Z2 reference platform.


3. Pipeline position

Formal verification sits at the outermost of SC-NeuroCore's safety concentric rings. From inside out:

Text Only
   SC compute kernel (inner loop — Rust SIMD / Mojo / Julia)
             │
             ▼  runs every tick
   stochastic_doctor (software-level invariant checker)
             │
             ▼  runs every inference
   neuro_safe_monitor.sv (hardware runtime enforcer)
             │
             ▼  runs every clock edge in silicon
   safety_bounds.lean (formal specification — compile-time)
             │
             ▼  runs once per release
   FormalProofEngine.check_proofs()

Inputs — the safety_bounds.lean source file (tracked in git).

OutputsTrue / False from :meth:check_proofs; True means Lean elaborated the file without errors.

Dispatch policy — :class:FormalProofEngine is a release gate, not a runtime path. Intended usage: run it in CI (docs.yml or a dedicated formal.yml workflow) and fail the release if it returns False. Call it from test code only when explicitly validating the theorems (not on every unit test).


4. Features

Feature Detail
:class:FormalProofEngine Python façade; is_available() + check_proofs() -> bool
3 proved theorems (pure core Lean 4) monitor_soundness, safe_transition, lif_membrane_bounded
3 axiomatised theorems With explicit Mathlib roadmap note in file header
1:1 Lean ↔ SystemVerilog property map Every Lean theorem corresponds to a neuro_safe_monitor.sv property
Hardware cross-check Each axiomatised theorem has a matching Rust fuzz/sweep test
Graceful lean absence is_available() returns False if lean not on PATH
Clear failure reporting check_proofs prints the first error line + returns False
No Mathlib dependency (current) Project compiles against Lean 4 pure core — zero extra setup

5. Usage example with output

Python
from sc_neurocore.formal import FormalProofEngine

engine = FormalProofEngine()

print("Lean binary:", engine._lean_bin)
print("Proof file :", engine.proof_file)
print("Available  :", engine.is_available())

if engine.is_available():
    ok = engine.check_proofs()
    print("Proofs OK  :", ok)

Verified output on the reference host (Linux x86-64, Lean 4.30.0-rc2 from elan, 2026-04-20):

Text Only
Lean binary: /home/anulum/.elan/bin/lean
Proof file : /<repo>/src/sc_neurocore/formal/proofs/safety_bounds.lean
Available  : True
[Formal] Running formal checking across physical stochastic theorems...
Proofs OK  : True

[Formal] Running formal checking… is printed by the engine itself before it shells out to lean. The elaboration completes in ~0.7 s on the reference host — see §7 for the measured wall time.


6. Technical reference

6.1 FormalProofEngine

Python
class FormalProofEngine:
    _lean_bin: str | None     # shutil.which("lean") result
    proof_file: Path          # <package>/proofs/safety_bounds.lean

    def __init__(self) -> None
    def is_available(self) -> bool
    def check_proofs(self) -> bool
Method Semantic
__init__() Resolves lean on PATH, resolves the bundled proof file path.
is_available() -> bool True iff lean is callable and safety_bounds.lean is present in the package.
check_proofs() -> bool subprocess.run([self._lean_bin, str(self.proof_file)]); returns True iff Lean elaborated without an error line in stdout/stderr.

Error-handling semantics: check_proofs catches CalledProcessError and prints the captured stderr; returns False. A missing Lean binary produces False with a dedicated log line — is_available should be called first to distinguish "Lean missing" from "proofs broken".

6.2 safety_bounds.lean structure

Lean
-- §1. Controller safety ─────────────────────────────────────────────
structure ControllerState where
  coherence : Nat
  limit : Nat

def halt_triggered (s : ControllerState) : Bool :=
  s.coherence < s.limit

theorem monitor_soundness (s : ControllerState) :
    (halt_triggered s = false)  (s.coherence  s.limit) := ...

theorem safe_transition (s1 s2 : ControllerState) :
    (s1.coherence  s1.limit)  (s2.coherence  s1.coherence) 
    (s2.coherence  s1.limit) := ...

-- §2. SC precision bound ─────────────────────────────────────────────
axiom sc_precision_numerator_bound (N k : Nat) :
    k  N  4 * (k * (N - k))  N * N

-- §3. SC addition preserves unit interval ────────────────────────────
axiom sc_add_preserves_range
    (pA pB D : Nat) : pA  D  pB  D  0 < D 
    pA * D + pB * D - pA * pB  D * D

-- §4. LIF membrane boundedness ───────────────────────────────────────
structure LIFState where
  membrane : Nat
  threshold : Nat
  v_max : Nat
  v_reset : Nat

def lif_step (s : LIFState) (input : Nat) : LIFState := ...

theorem lif_membrane_bounded
    (s : LIFState) (input : Nat) (h_reset : s.v_reset  s.v_max) :
    (lif_step s input).membrane  s.v_max := ...

-- §5. SCC correlation range ──────────────────────────────────────────
axiom scc_bounded (n d : Int) :
    0 < d  -d  n  n  d  -d  n  n  d

6.3 Toolchain expectations

  • Lean 4 on PATH. Install via elan from https://leanprover.github.io/; recommended toolchain is leanprover/lean4:v4.30.0-rc2 or newer.
  • No Mathlib required for the current proof file — the three proved theorems use only pure core tactics (simp, omega, decide_eq_false_iff_not, Nat.not_lt, Nat.le_trans, Nat.min_le_right).
  • Mathlib upgrade path — when the project adopts Mathlib, the three axiomatised theorems become one-liner proofs with nlinarith. Target milestone: first CI run that exercises the Mathlib-based version.

6.4 Error modes

Scenario Behaviour
lean not on PATH is_available() returns False, clear diagnostic
safety_bounds.lean missing is_available() returns False
Lean elaboration error check_proofs() returns False + prints Lean stderr
Lean version mismatch (tactic unknown) Lean prints unknown tactic; engine reports False

7. Performance benchmarks

Measured on 2026-04-20 — Linux x86-64 (Intel i5-11600K, Lean 4.30.0-rc2 via elan, no Mathlib).

7.1 FormalProofEngine.check_proofs() wall time

Scenario Wall time Notes
Cold (first lean invocation) 0.613 s 3 theorems + 3 axioms elaborated
Warm (repeat calls) 0.542 s file already in kernel page cache

Measured by benchmarks/bench_formal.py (2026-04-20). Raw JSON at benchmarks/results/bench_formal.json — includes the theorem / axiom counts for regression tracking.

Elaboration of the full safety_bounds.lean file completes in sub-second wall clock. The cost grows with theorem complexity; the three proved theorems use cheap tactics (simp, one Nat.le_trans, one Nat.min_le_right) so there's no bottleneck today. A future Mathlib-based proof of the three axiomatised theorems will increase the wall clock (Mathlib import alone takes seconds); budget for ~5–10 s total once Mathlib lands.

7.2 Reproducer

Bash
# Direct Lean invocation (no Python wrapper).
time lean src/sc_neurocore/formal/proofs/safety_bounds.lean
# → real    0m0.613s (cold on Intel i5-11600K, elan 4.30.0-rc2)

# Via Python façade.
PYTHONPATH=src python -c "
from sc_neurocore.formal import FormalProofEngine
e = FormalProofEngine()
print('available:', e.is_available())
print('proofs OK:', e.check_proofs())
"
# → available: True
#   [Formal] Running formal checking across physical stochastic theorems...
#   proofs OK: True

A non-zero Lean exit code or any error line in the output forces check_proofs() to return False — the engine does not try to distinguish "syntax error in Lean" from "proof failure"; both are release-blockers.


8. Citations

  1. de Moura, L. & Ullrich, S. (2021). The Lean 4 theorem prover and programming language. In Proceedings of the 28th International Conference on Automated Deduction, LNCS 12699, pp. 625–635. — Lean 4 reference.
  2. The Mathlib4 community (2024). Mathlib4: the Lean mathematical library. URL: https://leanprover-community.github.io/mathlib4_docs/. — Source of nlinarith / polyrith used by the roadmap's Mathlib-based proofs of the three axiomatised theorems.
  3. Sarpong, K., Sinclair, D., Carter, K. (2018). Formal verification of hardware safety properties in Isabelle/HOL. FMCAD — discussed as alternative toolchain; not chosen for SC-NeuroCore.
  4. Alaghi, A. & Hayes, J. P. (2013). Survey of stochastic computing. ACM TECS 12(2s): 92. — SC variance bound $p(1-p)/N \le 1/(4N)$ cited in §1.2.

8b. CI integration

The formal-proof check is designed to run as a release gate, not on every commit. Recommended CI pattern (not yet applied in the current .github/workflows):

YAML
name: Formal Verification
on:
  pull_request:
    paths:
      - 'src/sc_neurocore/formal/**'
      - 'src/sc_neurocore/hdl_gen/safety/**'
  schedule:
    - cron: '0 6 * * *'   # nightly

jobs:
  lean-check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - run: |
          curl https://raw.githubusercontent.com/leanprover/elan/master/elan-init.sh -sSf | sh -s -- -y
          source $HOME/.elan/env
          lean src/sc_neurocore/formal/proofs/safety_bounds.lean

The single-file elaboration cost (0.7 s) is low enough to gate on every PR that touches the formal directory; nightly runs provide broad regression coverage across dependency drift.

8c. Relationship to the stochastic doctor

:mod:sc_neurocore.doctor.stochastic_doctor implements the same six invariants in Python for simulation-time checking. The correspondences:

Lean axiom / theorem stochastic_doctor check
monitor_soundness check_halt_signal_polarity — halts iff coherence < limit
safe_transition check_monotone_coherence — coherence never decreases
sc_precision_numerator_bound check_variance_bound — $p(1-p)/N \le 1/(4N)$ after pack/unpack
sc_add_preserves_range check_add_range — OR-gate output ≤ SC denominator
lif_membrane_bounded check_lif_bounds — membrane never exceeds v_max
scc_bounded check_scc_range — computed SCC stays inside [−1, +1]

The doctor runs on simulation traces (it does not require Lean); the Lean layer runs on the specification. The doctor catches simulation-time regressions (numerical drift, SC-specific bugs); Lean catches specification-level changes that would silently relax a safety contract.

8d. Future: machine-checked RTL

The gap between the Lean specification and the SystemVerilog safety_monitor.sv is currently hand-verified by correspondence. An open research path is to prove the RTL implementation against the Lean spec directly:

  • SymbiYosys → SV Assertions → Lean. SymbiYosys already drives formal equivalence checks on the RTL. An export path from SymbiYosys's internal model to Lean statements would let a single tool establish "the RTL at git commit X implements the Lean spec".
  • Coq-like approach via Kôika (MIT). Kôika (Bourgeat et al. 2020) compiles a Coq-verified RTL source language to synthesisable Verilog. Not a direct fit for an existing SystemVerilog file, but a full rewrite of safety_monitor.sv in Kôika would give the same guarantee in Coq instead of Lean.
  • Mathlib polyrith for the axiomatised three. Lowest- effort win — adopting Mathlib upgrades the three axioms to proved theorems and eliminates the "axiomatised" line from §1.2. Target: after the Mathlib cache is in the repo and CI builds it in acceptable wall clock.

Decision logged — the axiomatised-now, Mathlib-later strategy is intentional. See the file header of safety_bounds.lean for the roadmap note.

9. Limitations

  • Three theorems are axiomatised. sc_precision_numerator_bound, sc_add_preserves_range, and scc_bounded are stated without proof pending a Mathlib-based project setup. The hardware monitor enforces them at runtime, and Rust fuzz tests sweep the parameter space, but a Lean-proved statement is a stronger guarantee and is a near-term follow-up.
  • Lean 4 version coupling. The syntactic forms used (decide_eq_false_iff_not, Nat.not_lt) are stable in Lean 4.x but may shift across major Lean releases. CI pins a specific toolchain via elan.
  • No RTL equivalence check. The Lean specification and the SystemVerilog monitor are hand-aligned 1:1; SC-NeuroCore does not produce a machine-checked proof that the RTL implements the Lean statements. A formal-RTL bridge (e.g. SymbiYosys with SystemVerilog Assertion → Lean-style spec) is future work.
  • check_proofs is synchronous. Calling it from a tight loop costs ~0.5 s per call. Use it as a release gate, not a runtime invariant check.
  • Pure-core Lean 4 (no Mathlib). The three proved theorems live in the default Lean core library, so check_proofs runs in any environment that has a lean binary on $PATH without a Lake manifest and without the ~10 GB Mathlib cache. This trade keeps the gate runnable in CI containers, at the cost of not yet admitting the real-analysis lemmas needed for the three axiomatised theorems (see §6). The Mathlib roadmap is written into the Lean source comments so the intended proof skeletons are already in the repository.
  • Axioms are explicit, not hidden. :func:list_axioms enumerates every axiom declaration in the proof file; len(list_axioms()) is part of the release checklist and must match the count listed in §6 (currently 3). A silent new axiom would fail that check.
  • Cold-start dominated by elaborator, not proof search. The 0.6 s check_proofs call is mostly Lean core elaborator startup; the three theorems themselves elaborate in a few milliseconds each. Adding more small proved theorems does not linearly increase the gate cost.

Reference

  • Python bridge: src/sc_neurocore/formal/lean_bridge.py (~45 lines) + src/sc_neurocore/formal/__init__.py exports.
  • Lean 4 proofs: src/sc_neurocore/formal/proofs/safety_bounds.lean (~130 lines — 3 proofs + 3 axioms).
  • Hardware monitor: src/sc_neurocore/hdl_gen/safety/safety_monitor.sv.
  • ASIC flow: src/sc_neurocore/hdl_gen/openroad_flow/run_asic_flow.sh.

sc_neurocore.formal.lean_bridge

FormalProofEngine

Invokes Lean 4 safety_bounds.lean to formally verify mathematical parameters dynamically.

Source code in src/sc_neurocore/formal/lean_bridge.py
Python
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
class FormalProofEngine:
    """Invokes Lean 4 `safety_bounds.lean` to formally verify mathematical parameters dynamically."""

    def __init__(self) -> None:
        self._lean_bin: str | None = shutil.which("lean")
        self.proof_file = Path(__file__).parent / "proofs" / "safety_bounds.lean"

    def is_available(self) -> bool:
        return self._lean_bin is not None and self.proof_file.exists()

    def check_proofs(self) -> bool:
        """Invokes the native `lean --check` verification boundary safely testing axioms."""
        if not self.is_available():
            print("[Formal] Lean 4 unavailable on the system path.")
            return False

        assert self._lean_bin is not None, "is_available() guarantees a non-None bin path"
        print("[Formal] Running formal checking across physical stochastic theorems...")
        try:
            result = subprocess.run(
                [self._lean_bin, str(self.proof_file)], capture_output=True, text=True, check=True
            )
            if "error" in result.stdout.lower() or "error" in result.stderr.lower():
                print(f"[Formal] Failure natively detected: {result.stderr}")
                return False
            return True
        except subprocess.CalledProcessError as e:
            print(f"[Formal] Process exception: {e.stderr}")
            return False

check_proofs()

Invokes the native lean --check verification boundary safely testing axioms.

Source code in src/sc_neurocore/formal/lean_bridge.py
Python
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
def check_proofs(self) -> bool:
    """Invokes the native `lean --check` verification boundary safely testing axioms."""
    if not self.is_available():
        print("[Formal] Lean 4 unavailable on the system path.")
        return False

    assert self._lean_bin is not None, "is_available() guarantees a non-None bin path"
    print("[Formal] Running formal checking across physical stochastic theorems...")
    try:
        result = subprocess.run(
            [self._lean_bin, str(self.proof_file)], capture_output=True, text=True, check=True
        )
        if "error" in result.stdout.lower() or "error" in result.stderr.lower():
            print(f"[Formal] Failure natively detected: {result.stderr}")
            return False
        return True
    except subprocess.CalledProcessError as e:
        print(f"[Formal] Process exception: {e.stderr}")
        return False