Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
91 commits
Select commit Hold shift + click to select a range
434f5e1
add #simulate command for random-walk state exploration
rnbguy Mar 16, 2026
f15aca5
lazy trace recording with replay-on-violation
rnbguy Mar 16, 2026
df4f8e5
try/catch error handling with seed reporting in simulate loop
rnbguy Mar 16, 2026
543cce4
share helpers between #model_check and #simulate, add widget support
rnbguy Mar 16, 2026
1e9c481
restore docstrings and inline comments removed during refactor
rnbguy Mar 16, 2026
2f12dd7
uncomment violationIsError option in MutexViolation example
rnbguy Mar 16, 2026
c066bee
simplify assertion failure handling to single-pass in simulateOnceLoop
rnbguy Mar 16, 2026
53cbc95
remove totalSteps tracking from simulate pipeline
rnbguy Mar 16, 2026
e2783dd
remove unused displayResultWidget, clean up formatting
rnbguy Mar 16, 2026
542db5c
add SharedCounter example where #simulate outperforms #model_check
rnbguy Mar 16, 2026
b5c3bef
Merge branch 'veil-2.0-preview' into feat/simulate
rnbguy Apr 12, 2026
2c1598e
fix: build
rnbguy Apr 12, 2026
8201073
fix: qualify compilation status constructors
rnbguy Apr 12, 2026
a8e9419
feat: align command architecture with #model_check
rnbguy Apr 12, 2026
ed7f810
test: cover parity regressions
rnbguy Apr 12, 2026
534c9f2
refactor: share executed path semantics
rnbguy Apr 12, 2026
2fc5d47
refactor: share pure and runtime trace loops
rnbguy Apr 12, 2026
96d39f0
feat: use runtime runner for command execution
rnbguy Apr 12, 2026
7ad5f60
refactor: clean result rendering semantics
rnbguy Apr 12, 2026
12adb4c
refactor: split simulation into modular files
rnbguy Apr 12, 2026
55e15e4
feat: support assumptions checks
rnbguy Apr 12, 2026
7f58696
feat: add simulation-native results and progress
rnbguy Apr 12, 2026
6075426
refactor: add theorem-level soundness bridges
rnbguy Apr 12, 2026
9dba342
fix: recreate temp build folders
rnbguy Apr 12, 2026
4353e5f
refactor: emit prop-level soundness theorems
rnbguy Apr 12, 2026
a4483a8
refactor: make random selection proof-carrying
rnbguy Apr 12, 2026
b304781
refactor: simplify single-trace proof foundations
rnbguy Apr 12, 2026
7ee56dc
refactor: prove single-trace engine soundness
rnbguy Apr 12, 2026
d26185f
feat: lift engine soundness through runtime
rnbguy Apr 12, 2026
8f2c504
refactor: emit engine-level soundness theorem
rnbguy Apr 12, 2026
f05510e
refactor: remove bool soundness certificates
rnbguy Apr 12, 2026
78542c0
refactor: prove runtime soundness directly
rnbguy Apr 12, 2026
8ca3b6f
refactor: make simulate theorem assumptions-aware
rnbguy Apr 12, 2026
a379849
refactor: align simulate theorem boundary with command semantics
rnbguy Apr 13, 2026
64f76af
fix: persist final progress metrics
rnbguy Apr 14, 2026
f2656c2
fix: encode trace budget termination
rnbguy Apr 14, 2026
025d752
fix: use a single result log path
rnbguy Apr 14, 2026
1fde08c
test: cover emitted mode behavior
rnbguy Apr 14, 2026
d81587c
refactor: drop unused path helpers
rnbguy Apr 14, 2026
dd97553
refactor: remove unused simulate names
rnbguy Apr 14, 2026
d09d7d2
chore: remove new proof warnings
rnbguy Apr 14, 2026
dcf6735
fix: make trace limits part of core results
rnbguy Apr 14, 2026
41f2602
fix: align display trace-limit counts
rnbguy Apr 14, 2026
e9ad7d3
test: add simulate violation mode regression
rnbguy Apr 14, 2026
797f9f6
fix(model-checker): isolate compiled command instances
rnbguy Apr 15, 2026
7140a15
fix(simulate): keep default compilation running
rnbguy Apr 15, 2026
c27a938
fix(simulate): tighten handoff cancellation and parity
rnbguy Apr 15, 2026
ed23a67
fix(examples): replace SharedCounter with lease race examples
rnbguy Apr 15, 2026
3f9605f
fix(examples): add simulate-friendly reliable broadcast
rnbguy Apr 15, 2026
9971ba1
fix(simulate): preserve explicit default config values
rnbguy Apr 15, 2026
6da6208
fix(simulate): show chosen seed in output
rnbguy Apr 15, 2026
48fdf21
fix(simulate): short-circuit empty initial states
rnbguy Apr 15, 2026
312d8e2
test(simulate): make interpreted mode explicit
rnbguy Apr 16, 2026
17228a1
chore: reduce noises
rnbguy Apr 16, 2026
fd86b56
fix(simulate): align violation soundness with runtime semantics
rnbguy Apr 16, 2026
6dd0370
fix(simulate): respect config bounds and omitted theory
rnbguy Apr 26, 2026
ec620dd
fix(simulate): use dedicated simulation result
rnbguy May 7, 2026
0ebceb9
refactor(model-checker): share invariant violation helper
rnbguy May 7, 2026
18746f9
refactor(simulate): restrict constrained system once
rnbguy May 7, 2026
ad7cc3f
refactor(model-checker): move state constraint helper
rnbguy May 7, 2026
ebfbe4b
refactor(model-checker): reuse state constraint helper
rnbguy May 7, 2026
e543551
refactor(simulate): reuse model checker result json
rnbguy May 8, 2026
bec1247
chore: merge upstream veil-2.0-preview
rnbguy Jun 5, 2026
b42bb43
fix(simulate): unify result json serialization
rnbguy Jun 5, 2026
93793e5
refactor(model-checker): remove unused status prefix
rnbguy Jun 5, 2026
ea1dc6b
refactor(frontend): remove unnecessary prime from ident helper
rnbguy Jun 5, 2026
8550732
refactor(simulation): use relational transition system directly
rnbguy Jun 5, 2026
732188c
refactor(simulation): reuse execution outcome partitioning
rnbguy Jun 5, 2026
effe56d
refactor(simulation): use state monad for random traces
rnbguy Jun 5, 2026
747c4cc
refactor(simulation): rename indexed trace helper
rnbguy Jun 5, 2026
85a9dd6
docs(model-checker): document compiled command fields
rnbguy Jun 5, 2026
956968e
chore(model-checker): remove redundant helpers
rnbguy Jun 5, 2026
5c213c9
fix(simulation): clarify empty initial state result
rnbguy Jun 5, 2026
e6b0d6a
refactor(simulation): inline step decision logic
rnbguy Jun 5, 2026
cee1687
refactor(simulation): inline random path selection
rnbguy Jun 6, 2026
cf689c5
refactor(simulation): use relational trace validity
rnbguy Jun 6, 2026
6c873e6
refactor(simulation): remove pure simulation loop
rnbguy Jun 6, 2026
41be257
refactor(progress): split simulation payload
rnbguy Jun 6, 2026
242d22b
refactor(progress): consolidate viewer rendering
rnbguy Jun 6, 2026
dbdae2c
test(simulation): avoid compiled-mode cases
rnbguy Jun 6, 2026
a2fd109
fix(model-checker): avoid per-command build folders
rnbguy Jun 6, 2026
b3ed49e
refactor(simulation): clarify config precedence
rnbguy Jun 6, 2026
e717e6c
refactor(simulation): consolidate handoff cleanup
rnbguy Jun 6, 2026
535b237
test(simulation): add compiled smoke test
rnbguy Jun 17, 2026
2abddc6
fix(simulation): structure no-initial result metadata
rnbguy Jun 17, 2026
2e74826
fix(progress): preserve cancelled simulation metadata
rnbguy Jun 17, 2026
9d47395
fix(widget): render simulation result metadata
rnbguy Jun 17, 2026
9fdd830
refactor(simulation): derive result depth from traces
rnbguy Jun 17, 2026
90cb9de
fix(model-checker): preserve compiled build cache
rnbguy Jun 17, 2026
a5f18e3
chore: merge upstream veil-2.0-preview
rnbguy Jun 17, 2026
0d5d0f4
fix(simulation): generate executable definitions for simulate
rnbguy Jun 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 93 additions & 0 deletions Examples/Simulate/CheckpointLeaseFailover.lean
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import Veil

/-
Original source/reference:
- Local modeling analogues:
- Examples/Ivy/DecentralizedLock.lean (epoched authority transfer)
- Examples/TLA/Raft.lean (leader failover)
- Production inspiration: etcd lease checkpoint persistence across leader
failover (etcd-io/etcd#13508)

Bug/race shape:
A leader checkpoints a lease's reduced remaining TTL. After failover, the new
leader forgets that checkpointed TTL and reconstructs the lease from the older
grant state, reviving a lease that should already be considered expired.

Why #simulate here:
The violating trace is short, but exhaustive search must branch over leaders,
leases, epochs, and recovery schedules.
-/

veil module CheckpointLeaseFailover

type node
type epoch
type lease

instantiate epochOrd : TotalOrder epoch

individual leader : node
immutable individual initial_leader : node
immutable individual initial_epoch : epoch
relation current_epoch (e : epoch)
relation granted_until (l : lease) (e : epoch)
relation checkpointed_until (l : lease) (e : epoch)
relation active_on_leader (l : lease)

#gen_state

after_init {
leader := initial_leader
current_epoch E := E == initial_epoch
granted_until L E := false
checkpointed_until L E := false
active_on_leader L := false
}

action grantLease (l : lease) (expiry : epoch) {
require ∀ now, current_epoch now -> ¬ epochOrd.le expiry now
granted_until l E := E == expiry
active_on_leader l := true
}

action checkpointRemainingTTL (l : lease) (expiry : epoch) {
require active_on_leader l
require ∀ now, current_epoch now -> ¬ epochOrd.le expiry now
checkpointed_until l E := E == expiry
}

action failover (newLeader : node) (newEpoch : epoch) {
require newLeader != leader
require ∀ oldEpoch, current_epoch oldEpoch -> ¬ epochOrd.le newEpoch oldEpoch
leader := newLeader
current_epoch E := E == newEpoch
active_on_leader L := false
}

action recoverLeaseFromGrant (l : lease) (expiry : epoch) {
require granted_until l expiry
active_on_leader l := true
}

invariant [one_current_epoch]
∀ (e1 e2 : epoch), current_epoch e1 ∧ current_epoch e2 -> e1 = e2

safety [checkpointed_expiry_respected]
∀ (l : lease) (expiry now : epoch),
checkpointed_until l expiry ∧ current_epoch now ∧ epochOrd.le expiry now ->
¬ active_on_leader l

#gen_spec

-- model_check must branch over failovers, recovery choices, and many lease/epoch combinations.
-- set_option veil.violationIsError false in
-- #model_check { node := Fin 12, epoch := Fin 8, lease := Fin 8 }
-- { initial_leader := (0 : Fin 12), initial_epoch := (0 : Fin 8) }

-- simulate quickly finds the stale-recovery bug after failover.
set_option veil.violationIsError false in
#simulate { node := Fin 12, epoch := Fin 8, lease := Fin 8 }
{ initial_leader := (0 : Fin 12), initial_epoch := (0 : Fin 8) }
(seed := 23) (maxTraces := 2000) (maxSteps := 10)

end CheckpointLeaseFailover
85 changes: 85 additions & 0 deletions Examples/Simulate/LeaseKeepaliveRace.lean
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import Veil

/-
Original source/reference:
- Closest local modeling analogue: Examples/Ivy/DecentralizedLock.lean
- Production reference: etcd KeepAlive vs lease-expiry revocation race
(etcd-io/etcd#21389, issue #14758)

Bug/race shape:
A client still has keys attached to a lease when revocation starts. A late
keepalive succeeds after revocation has already removed the keys, so the client
appears renewed even though its data is gone.

Why #simulate here:
The bad trace is only a handful of steps, but exhaustive search must branch over
clients, keys, revoke timing, and keepalive interleavings.
-/

veil module LeaseKeepaliveRace

type client
type key

relation lease_alive (c : client)
relation revoke_started (c : client)
relation keepalive_succeeded (c : client)
relation key_attached (c : client) (k : key)
relation key_present (k : key)

#gen_state

after_init {
lease_alive C := false
revoke_started C := false
keepalive_succeeded C := false
key_attached C K := false
key_present K := false
}

action grantLease (c : client) {
require !(lease_alive c)
lease_alive c := true
revoke_started c := false
keepalive_succeeded c := false
}

action attachKey (c : client) (k : key) {
require lease_alive c
key_attached c k := true
key_present k := true
}

action startRevoke (c : client) {
require lease_alive c
revoke_started c := true
lease_alive c := false
}

action deleteKey (c : client) (k : key) {
require revoke_started c
require key_attached c k
key_present k := false
}

action keepAlive (c : client) {
require revoke_started c
keepalive_succeeded c := true
lease_alive c := true
}

safety [renewal_keeps_keys_live]
∀ (c : client) (k : key), keepalive_succeeded c ∧ key_attached c k -> key_present k

#gen_spec

-- model_check must branch over clients, keys, revoke order, and late keepalives.
-- set_option veil.violationIsError false in
-- #model_check { client := Fin 8, key := Fin 8 } {}

-- simulate usually hits the keepalive-after-revoke race in a short trace.
set_option veil.violationIsError false in
#simulate { client := Fin 8, key := Fin 8 } {}
(seed := 11) (maxTraces := 300) (maxSteps := 12)

end LeaseKeepaliveRace
73 changes: 73 additions & 0 deletions Examples/Simulate/ReliableBroadcast.lean
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import Veil

/-
Original source/reference:
- Local analogue: Examples/Ivy/ReliableBroadcast.lean
- External family reference: tlaplus/Examples/specifications/bcastByz/bcastByz.tla

Bug/race shape:
This simulate-focused variant keeps the initial/echo/vote/deliver structure, but
intentionally uses weak quorum rules. An equivocating originator can get two
different values delivered by different receivers.

Why #simulate here:
The violating trace is short, but exhaustive search must branch over broadcast,
echo, vote, and delivery orderings across many nodes and values.
-/

veil module ReliableBroadcastSim

type node
type value

immutable individual originator : node

relation initial_msg (src : node) (dst : node) (v : value)
relation echo_msg (src : node) (dst : node) (v : value)
relation vote_msg (src : node) (dst : node) (v : value)
relation delivered (dst : node) (v : value)

#gen_state

after_init {
initial_msg S D V := false
echo_msg S D V := false
vote_msg S D V := false
delivered D V := false
}

action initialSend (dst : node) (v : value) {
require ∀ V, !(initial_msg originator dst V)
initial_msg originator dst v := true
}

action echo (src : node) (v : value) {
require initial_msg originator src v
echo_msg src D v := true
}

action vote (observer : node) (v : value) {
require ∃ (n1 n2 : node), n1 != n2 ∧ echo_msg n1 observer v ∧ echo_msg n2 observer v
vote_msg observer D v := true
}

action deliver (observer : node) (v : value) {
require ∃ (n1 n2 : node), n1 != n2 ∧ vote_msg n1 observer v ∧ vote_msg n2 observer v
delivered observer v := true
}

safety [agreement]
∀ (n1 n2 : node) (v1 v2 : value), delivered n1 v1 ∧ delivered n2 v2 -> v1 = v2

#gen_spec

-- model_check must enumerate many broadcast, echo, vote, and delivery schedules.
-- set_option veil.violationIsError false in
-- #model_check { node := Fin 10, value := Fin 2 } { originator := (0 : Fin 10) }

-- simulate quickly finds an equivocation trace with the weak quorum rules above.
set_option veil.violationIsError false in
#simulate { node := Fin 10, value := Fin 2 } { originator := (0 : Fin 10) }
(seed := 41) (maxTraces := 2000) (maxSteps := 24)

end ReliableBroadcastSim
2 changes: 1 addition & 1 deletion Examples/TLA/MutexViolation.lean
Original file line number Diff line number Diff line change
Expand Up @@ -345,8 +345,8 @@ termination [AllDone] ∀s ≠ NONE, pc s Done = true
#time #gen_spec
-- NOTE: comment out the line containing `BUG:` to fix the violation

-- set_option veil.violationIsError false in
/- `Fin n` means `n-1` valid threads.-/
set_option veil.violationIsError false in
#model_check { process := Fin 10 } { NONE := 0 }

end MutexViolation
10 changes: 10 additions & 0 deletions Veil/Base.lean
Original file line number Diff line number Diff line change
Expand Up @@ -142,4 +142,14 @@ register_option veil.smt.timeout : Nat := {
descr := "Timeout for the SMT solver in seconds. Default is 60 seconds."
}

register_option veil.simulate.maxTraces : Nat := {
defValue := 10000
descr := "Maximum number of traces to generate during simulation. Default is 10000."
}

register_option veil.simulate.maxSteps : Nat := {
defValue := 100
descr := "Maximum number of steps per trace during simulation. Default is 100 (same as TLC)."
}

end Veil
10 changes: 3 additions & 7 deletions Veil/Core/Tools/ModelChecker/Concrete/Checker.lean
Original file line number Diff line number Diff line change
Expand Up @@ -91,13 +91,7 @@ def findReachable {ρ σ κ : Type} {m : Type → Type}
setViolationFound progressInstanceId
return ModelCheckingResult.foundViolation 0 (.assumptionFailure assumptionViolations) none
-- Create a "filtered" version of the system
let sys := if params.stateConstraints.isEmpty then sys else {
initStates := sys.initStates.filter (params.satisfiesConstraints th)
tr := fun th' s => (sys.tr th' s).filter (fun (_, o) => match o with
| .success s' => params.satisfiesConstraints th s'
| .assertionFailure _ s' => params.satisfiesConstraints th s' -- assertion failures should satisfy constraints to be considered
| .divergence => true) -- well
}
let sys := Veil.ModelChecker.restrictSystemByStateConstraints sys params th
let (ctx, distinctCount) ← match parallelCfg with
| some cfg => do
let mctx ← breadthFirstSearchParallel params sys cfg progressInstanceId cancelToken
Expand All @@ -115,6 +109,8 @@ def findReachable {ρ σ κ : Type} {m : Type → Type}
| some (.earlyTermination (.reachedDepthBound _)) =>
-- No violation found within depth bound; report number of states explored
return ModelCheckingResult.noViolationFound distinctCount (.earlyTermination (.reachedDepthBound ctx.completedDepth))
| some (.earlyTermination (.reachedTraceLimit maxTraces)) =>
return ModelCheckingResult.noViolationFound distinctCount (.earlyTermination (.reachedTraceLimit maxTraces))
| some (.earlyTermination .cancelled) =>
-- Search was cancelled by the user
return ModelCheckingResult.cancelled
Expand Down
28 changes: 19 additions & 9 deletions Veil/Core/Tools/ModelChecker/Concrete/Core.lean
Original file line number Diff line number Diff line change
Expand Up @@ -192,16 +192,16 @@ def BaseSearchContext.initial (initialStates : List σ) : BaseSearchContext σ
-- achieve zero additional memory allocation here?

/-- Partition a list of `(label × ExecutionOutcome)` pairs into two components:
a list of successful transitions, and a list of transitions where exceptions
were raised. The divergence part is discarded. -/
a list of successful transitions, and a list of labeled transitions where
exceptions were raised. The divergence part is discarded. -/
def partitionExecutionOutcome (outcomes : List (κ × ExecutionOutcome Int σ)) :
List (κ × σ) × List (Int × σ) :=
List (κ × σ) × List (κ × Int × σ) :=
outcomes.foldr
(init := ([], []))
(fun (label, outcome) (succs, exns) =>
match outcome with
| .success st => ((label, st) :: succs, exns)
| .assertionFailure exId st => (succs, (exId, st) :: exns)
| .assertionFailure exId st => (succs, (label, exId, st) :: exns)
| .divergence => (succs, exns))

theorem partitionExecutionOutcome.fst_spec {κ σ : Type} (outcomes : List (κ × ExecutionOutcome Int σ)) :
Expand All @@ -213,16 +213,24 @@ theorem partitionExecutionOutcome.fst_spec {κ σ : Type} (outcomes : List (κ
| nil => simp
| cons x l ih => rcases x with ⟨l, _ | _ | _⟩ <;> grind

theorem partitionExecutionOutcome.snd_spec {κ σ : Type} (outcomes : List (κ × ExecutionOutcome Int σ)) :
∀ (label : κ) (exId : Int) (st : σ),
(label, exId, st) ∈ (partitionExecutionOutcome outcomes).snd ↔
(label, ExecutionOutcome.assertionFailure exId st) ∈ outcomes := by
introv ; unfold partitionExecutionOutcome
induction outcomes with
| nil => simp
| cons x l ih => rcases x with ⟨l, _ | _ | _⟩ <;> grind

-- NOTE: If this function is put inside `BaseSearchContext.checkViolationsAndMaybeTerminate`,
-- `specialize` of `List.filterMap` may not exhibit
def checkViolationsAndMaybeTerminate
(completedDepth : Nat)
(hasSuccessfulTransition : Bool)
(assertionFailures : List (Int × σ)) :
(assertionFailures : List (κ × Int × σ)) :
List (σₕ × ViolationKind) × Option (EarlyTerminationReason σₕ) :=
-- Compute all violation conditions once
let safetyViolations := params.invariants.filterMap fun p =>
if !p.holdsOn th curr then some p.name else none
let safetyViolations := violatedInvariantNames params th curr
let safetyViolation := !safetyViolations.isEmpty
let deadlock := !hasSuccessfulTransition && !params.terminating.holdsOn th curr

Expand All @@ -231,13 +239,14 @@ def checkViolationsAndMaybeTerminate
(if safetyViolation then [(fpSt, .safetyFailure safetyViolations)] else []) ++
(if deadlock then [(fpSt, .deadlock)] else []) ++
-- NOTE: This should be further optimized to avoid extra memory allocation
(assertionFailures.map fun (exId, _) => (fpSt, .assertionFailure exId))
(assertionFailures.map fun (_, exId, _) => (fpSt, .assertionFailure exId))

let earlyTermination := params.earlyTerminationConditions.findSome? fun
| .foundViolatingState => if safetyViolation then some (.foundViolatingState fpSt safetyViolations) else none
| .reachedDepthBound bound => if completedDepth >= bound then some (.reachedDepthBound bound) else none
| .reachedTraceLimit _ => none
| .deadlockOccurred => if deadlock then some (.deadlockOccurred fpSt) else none
| .assertionFailed => assertionFailures.head?.map fun (exId, _) => .assertionFailed fpSt exId
| .assertionFailed => assertionFailures.head?.map fun (_, exId, _) => .assertionFailed fpSt exId
| .cancelled => none -- Cancellation is handled externally via cancel token, not through early termination conditions
(newViolations, earlyTermination)

Expand All @@ -258,6 +267,7 @@ def BaseSearchContext.processState
match x with
| .foundViolatingState fp violations => {ctx with finished := some (.earlyTermination (.foundViolatingState fp violations))}
| .reachedDepthBound bound => {ctx with finished := some (.earlyTermination (.reachedDepthBound bound))}
| .reachedTraceLimit maxTraces => {ctx with finished := some (.earlyTermination (.reachedTraceLimit maxTraces))}
| .deadlockOccurred fp => {ctx with finished := some (.earlyTermination (.deadlockOccurred fp))}
| .assertionFailed fp exId => {ctx with finished := some (.earlyTermination (.assertionFailed fp exId))}
| .cancelled => {ctx with finished := some (.earlyTermination .cancelled)}
Expand Down
Loading