diff --git a/Examples/Simulate/CheckpointLeaseFailover.lean b/Examples/Simulate/CheckpointLeaseFailover.lean
new file mode 100644
index 00000000..2ebc2f38
--- /dev/null
+++ b/Examples/Simulate/CheckpointLeaseFailover.lean
@@ -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
diff --git a/Examples/Simulate/LeaseKeepaliveRace.lean b/Examples/Simulate/LeaseKeepaliveRace.lean
new file mode 100644
index 00000000..7b12f17c
--- /dev/null
+++ b/Examples/Simulate/LeaseKeepaliveRace.lean
@@ -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
diff --git a/Examples/Simulate/ReliableBroadcast.lean b/Examples/Simulate/ReliableBroadcast.lean
new file mode 100644
index 00000000..9c33183b
--- /dev/null
+++ b/Examples/Simulate/ReliableBroadcast.lean
@@ -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
diff --git a/Examples/TLA/MutexViolation.lean b/Examples/TLA/MutexViolation.lean
index 3b962025..74da3964 100644
--- a/Examples/TLA/MutexViolation.lean
+++ b/Examples/TLA/MutexViolation.lean
@@ -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
diff --git a/Veil/Base.lean b/Veil/Base.lean
index dcd9d679..5a78d767 100644
--- a/Veil/Base.lean
+++ b/Veil/Base.lean
@@ -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
diff --git a/Veil/Core/Tools/ModelChecker/Concrete/Checker.lean b/Veil/Core/Tools/ModelChecker/Concrete/Checker.lean
index ec4721f6..9ddc37e7 100644
--- a/Veil/Core/Tools/ModelChecker/Concrete/Checker.lean
+++ b/Veil/Core/Tools/ModelChecker/Concrete/Checker.lean
@@ -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
@@ -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
diff --git a/Veil/Core/Tools/ModelChecker/Concrete/Core.lean b/Veil/Core/Tools/ModelChecker/Concrete/Core.lean
index 92769266..a144bad5 100644
--- a/Veil/Core/Tools/ModelChecker/Concrete/Core.lean
+++ b/Veil/Core/Tools/ModelChecker/Concrete/Core.lean
@@ -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 σ)) :
@@ -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
@@ -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)
@@ -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)}
diff --git a/Veil/Core/Tools/ModelChecker/Concrete/Progress.lean b/Veil/Core/Tools/ModelChecker/Concrete/Progress.lean
index 10a77d10..86386be0 100644
--- a/Veil/Core/Tools/ModelChecker/Concrete/Progress.lean
+++ b/Veil/Core/Tools/ModelChecker/Concrete/Progress.lean
@@ -59,7 +59,14 @@ structure ProgressHistoryPoint where
queue : Nat
deriving ToJson, FromJson, Inhabited, Repr
-/-- Progress information for model checking, using TLC-style terminology. -/
+/-- Progress information for a simulation run. -/
+structure SimulationProgress where
+ tracesRun : Nat := 0
+ maxTraces : Nat := 0
+ depth : Nat := 0
+ deriving ToJson, FromJson, Inhabited, Repr
+
+/-- Progress information for model checking or simulation. -/
structure Progress where
status : String := "Initializing..."
/-- Length of the longest behavior found so far (BFS depth) -/
@@ -82,6 +89,8 @@ structure Progress where
allActionLabels : List String := []
/-- Time-series history for charting progress over time -/
history : Array ProgressHistoryPoint := #[]
+ /-- Progress information for `#simulate`; absent for `#model_check`. -/
+ simulation : Option SimulationProgress := none
deriving ToJson, FromJson, Inhabited, Repr
/-- Refs for tracking progress of a single model checker instance. -/
@@ -90,6 +99,8 @@ structure ProgressRefs where
resultRef : IO.Ref (Option Lean.Json)
/-- Cancellation token for this instance. -/
cancelToken : IO.CancelToken
+ /-- Cancellation token for background compilation, when default handoff is active. -/
+ compilationCancelTokenRef : IO.Ref (Option IO.CancelToken)
/-- Set by compilation task to signal interpreted mode to stop for handoff. -/
handoffRequested : IO.Ref Bool
/-- Set by interpreted mode when a violation is found (prevents handoff). -/
@@ -125,6 +136,7 @@ def allocProgressInstance (allActionLabels : List String := []) : IO (Nat × IO.
progressRef := ← IO.mkRef { startTimeMs := ← IO.monoMsNow, status := "Running...", isRunning := true, allActionLabels }
resultRef := ← IO.mkRef none
cancelToken := cancelTk
+ compilationCancelTokenRef := ← IO.mkRef none
handoffRequested := ← IO.mkRef false
violationFound := ← IO.mkRef false
}
@@ -174,6 +186,27 @@ def updateStatus (instanceId : Nat) (status : String) : IO Unit := withRefs inst
let now ← IO.monoMsNow
refs.progressRef.modify fun p => { p with status, elapsedMs := now - p.startTimeMs }
+/-- Update progress for a simulation run. -/
+def updateSimulationProgress (instanceId : Nat) (status : String)
+ (tracesRun maxTraces depth : Nat) : IO Unit := do
+ let now ← IO.monoMsNow
+ if let some refs ← getProgressRefs instanceId then
+ refs.progressRef.modify fun p =>
+ { p with
+ status
+ elapsedMs := now - p.startTimeMs
+ simulation := some { tracesRun, maxTraces, depth } }
+ if ← compiledModeEnabled.get then
+ let startTime ← compiledModeStartTime.get
+ let p : Progress := {
+ status := status
+ isRunning := true
+ startTimeMs := startTime
+ elapsedMs := now - startTime
+ simulation := some { tracesRun, maxTraces, depth }
+ }
+ IO.eprintln (toJson p).compress
+
/-- Mark progress as complete for a given instance ID. -/
def finishProgress (instanceId : Nat) (resultJson : Lean.Json) : IO Unit := withRefs instanceId fun refs => do
let now ← IO.monoMsNow
@@ -199,14 +232,22 @@ def isCancelled (instanceId : Nat) : IO Bool := do
| none => return false
/-- Request cancellation for an instance. -/
-def requestCancellation (instanceId : Nat) : IO Unit := withRefs instanceId (·.cancelToken.set)
+def requestCancellation (instanceId : Nat) : IO Unit := withRefs instanceId fun refs => do
+ refs.cancelToken.set
+ let compilationCancelToken? ← refs.compilationCancelTokenRef.get
+ if let some compilationCancelToken := compilationCancelToken? then
+ compilationCancelToken.set
+
+/-- Register or clear the background compilation cancellation token for an instance. -/
+def setCompilationCancelToken (instanceId : Nat) (cancelToken? : Option IO.CancelToken) : IO Unit :=
+ withRefs instanceId fun refs => refs.compilationCancelTokenRef.set cancelToken?
/-- Mark progress as cancelled for a given instance ID. -/
-def cancelProgress (instanceId : Nat) : IO Unit := withRefs instanceId fun refs => do
+def cancelProgress (instanceId : Nat) (resultJson : Lean.Json := Json.mkObj [("result", "cancelled")]) : IO Unit := withRefs instanceId fun refs => do
let now ← IO.monoMsNow
refs.progressRef.modify fun p => { p with
status := "Cancelled", isRunning := false, isCancelled := true, elapsedMs := now - p.startTimeMs }
- refs.resultRef.set (some (Json.mkObj [("result", "cancelled")]))
+ refs.resultRef.set (some resultJson)
/-- Wait for model check to complete and return the result JSON. -/
partial def waitForResult (instanceId : Nat) (pollIntervalMs : Nat := 100) : IO (Option Lean.Json) := do
diff --git a/Veil/Core/Tools/ModelChecker/ExecutionOutcome.lean b/Veil/Core/Tools/ModelChecker/ExecutionOutcome.lean
index 96fb3da7..f01f2c0c 100644
--- a/Veil/Core/Tools/ModelChecker/ExecutionOutcome.lean
+++ b/Veil/Core/Tools/ModelChecker/ExecutionOutcome.lean
@@ -21,7 +21,7 @@ inductive ExecutionOutcome (ε σ : Type) where
| assertionFailure (error : ε) (state : σ)
/-- The action diverged (did not terminate). -/
| divergence
-deriving Repr, BEq, Inhabited
+deriving Repr, BEq, DecidableEq, Inhabited
namespace ExecutionOutcome
@@ -33,36 +33,6 @@ def toPostState : ExecutionOutcome ε σ → Option σ
| .assertionFailure _ _ => .none
| .divergence => .none
-/-- Check if the outcome is a successful transition. -/
-@[inline]
-def isSuccess : ExecutionOutcome ε σ → Bool
- | .success _ => true
- | _ => false
-
-/-- Check if the outcome is an assertion failure. -/
-@[inline]
-def isAssertionFailure : ExecutionOutcome ε σ → Bool
- | .assertionFailure _ _ => true
- | _ => false
-
-/-- Check if the outcome is divergence. -/
-@[inline]
-def isDivergence : ExecutionOutcome ε σ → Bool
- | .divergence => true
- | _ => false
-
-/-- Extract the state from a successful outcome. -/
-@[inline]
-def getSuccessState? : ExecutionOutcome ε σ → Option σ
- | .success st => some st
- | _ => none
-
-/-- Extract the error and state from an assertion failure. -/
-@[inline]
-def getAssertionFailure? : ExecutionOutcome ε σ → Option (ε × σ)
- | .assertionFailure e st => some (e, st)
- | _ => none
-
/-- Extract just the exception ID from an assertion failure. -/
@[inline]
def exceptionId? : ExecutionOutcome ε σ → Option ε
diff --git a/Veil/Core/Tools/ModelChecker/Interface.lean b/Veil/Core/Tools/ModelChecker/Interface.lean
index 31f88e0b..fb845a27 100644
--- a/Veil/Core/Tools/ModelChecker/Interface.lean
+++ b/Veil/Core/Tools/ModelChecker/Interface.lean
@@ -51,6 +51,7 @@ inductive EarlyTerminationCondition where
| deadlockOccurred
| assertionFailed
| reachedDepthBound (depth : Nat)
+ | reachedTraceLimit (maxTraces : Nat)
| cancelled
deriving Inhabited, Hashable, BEq, Repr
@@ -61,13 +62,14 @@ inductive EarlyTerminationReason (σₕ : Type) where
| deadlockOccurred (fp : σₕ)
| assertionFailed (fp : σₕ) (exceptionId : Int)
| reachedDepthBound (depth : Nat)
+ | reachedTraceLimit (maxTraces : Nat)
| cancelled
deriving Inhabited, Hashable, BEq, Repr
/-- Check if the termination reason represents a violation that should prevent handoff. -/
def EarlyTerminationReason.isViolation {σₕ : Type} : EarlyTerminationReason σₕ → Bool
| .foundViolatingState _ _ | .deadlockOccurred _ | .assertionFailed _ _ => true
- | .reachedDepthBound _ | .cancelled => false
+ | .reachedDepthBound _ | .reachedTraceLimit _ | .cancelled => false
instance [ToJson σₕ] : ToJson (EarlyTerminationReason σₕ) where
toJson
@@ -75,6 +77,7 @@ instance [ToJson σₕ] : ToJson (EarlyTerminationReason σₕ) where
| .deadlockOccurred fp => Json.mkObj [("kind", "deadlock_occurred"), ("state_fingerprint", toJson fp)]
| .assertionFailed fp exId => Json.mkObj [("kind", "assertion_failed"), ("state_fingerprint", toJson fp), ("exception_id", toJson exId)]
| .reachedDepthBound depth => Json.mkObj [("kind", "reached_depth_bound"), ("depth", toJson depth)]
+ | .reachedTraceLimit maxTraces => Json.mkObj [("kind", "reached_trace_limit"), ("max_traces", toJson maxTraces)]
| .cancelled => Json.mkObj [("kind", "cancelled")]
inductive TerminationReason (σₕ : Type) where
@@ -279,6 +282,29 @@ def SearchParameters.violatedAssumptions (params : SearchParameters ρ σ) (th :
params.assumptions.filterMap fun p =>
if p.holdsOn th then none else some p.name
+/-- Create a filtered transition system that explores only states satisfying
+state constraints. -/
+@[inline]
+def restrictSystemByStateConstraints {ρ σ κ : Type} {th₀ : ρ}
+ (sys : EnumerableTransitionSystem ρ (List ρ) σ (List σ) Int κ (List (κ × ExecutionOutcome Int σ)) th₀)
+ (params : SearchParameters ρ σ) (th : ρ) :
+ EnumerableTransitionSystem ρ (List ρ) σ (List σ) Int κ (List (κ × ExecutionOutcome Int σ)) th₀ :=
+ if params.stateConstraints.isEmpty then sys else {
+ initStates := sys.initStates.filter (params.satisfiesConstraints th)
+ tr := fun th' st => (sys.tr th' st).filter fun (_, outcome) =>
+ match outcome with
+ | .success st' => params.satisfiesConstraints th st'
+ -- Assertion failures should satisfy constraints to be considered.
+ | .assertionFailure _ st' => params.satisfiesConstraints th st'
+ -- Divergence has no successor state to constrain.
+ | .divergence => true
+ }
+
+@[inline]
+def violatedInvariantNames {ρ σ : Type}
+ (params : SearchParameters ρ σ) (th : ρ) (st : σ) : List Lean.Name :=
+ params.invariants.filterMap fun p =>
+ if !p.holdsOn th st then some p.name else none
-- class ModelChecker (ts : TransitionSystem ρ σ l) where
-- isReachable : SearchParameters ρ σ → Option ParallelConfig → ModelCheckingResult ρ σ l σₕ
diff --git a/Veil/Core/Tools/ModelChecker/Simulation.lean b/Veil/Core/Tools/ModelChecker/Simulation.lean
new file mode 100644
index 00000000..146ab9ae
--- /dev/null
+++ b/Veil/Core/Tools/ModelChecker/Simulation.lean
@@ -0,0 +1 @@
+import Veil.Core.Tools.ModelChecker.Simulation.Checker
diff --git a/Veil/Core/Tools/ModelChecker/Simulation/Basic.lean b/Veil/Core/Tools/ModelChecker/Simulation/Basic.lean
new file mode 100644
index 00000000..27b0e6d1
--- /dev/null
+++ b/Veil/Core/Tools/ModelChecker/Simulation/Basic.lean
@@ -0,0 +1,42 @@
+import Veil.Core.Tools.ModelChecker.Interface
+
+namespace Veil.ModelChecker.Simulation
+open Lean
+
+structure SimulateConfig where
+ maxTraces : Nat := 10000
+ maxSteps : Nat := 100
+ seed : Nat := 0
+deriving Inhabited, Repr
+
+inductive SimulationResult (ρ σ κ : Type) where
+ | cancelled
+ | foundViolation (violation : ViolationKind) (viaTrace : Trace ρ σ κ)
+deriving Inhabited, Repr
+
+def SimulationResult.depth {ρ σ κ : Type} : SimulationResult ρ σ κ → Nat
+ | .foundViolation _ trace => trace.steps.size + if trace.failingStep.isSome then 1 else 0
+ | .cancelled => 0
+
+inductive SimulationTerminationReason where
+ | noInitialStates
+deriving Inhabited, Hashable, BEq, Repr
+
+instance : ToJson SimulationTerminationReason where
+ toJson
+ | .noInitialStates => Json.mkObj [("kind", "no_initial_states")]
+
+structure SimulateResult (ρ σ κ : Type) where
+ result : Option (SimulationResult ρ σ κ)
+ tracesRun : Nat
+ maxTraces : Nat
+ elapsedMs : Nat
+ seed : Nat
+ terminationReason : Option SimulationTerminationReason := none
+
+def SimulateResult.depth {ρ σ κ : Type} (result : SimulateResult ρ σ κ) : Nat :=
+ match result.result with
+ | some result => result.depth
+ | none => 0
+
+end Veil.ModelChecker.Simulation
diff --git a/Veil/Core/Tools/ModelChecker/Simulation/Checker.lean b/Veil/Core/Tools/ModelChecker/Simulation/Checker.lean
new file mode 100644
index 00000000..6aaa1b19
--- /dev/null
+++ b/Veil/Core/Tools/ModelChecker/Simulation/Checker.lean
@@ -0,0 +1,7 @@
+import Veil.Core.Tools.ModelChecker.Simulation.Runtime
+import Veil.Core.Tools.ModelChecker.Simulation.Result
+import Veil.Core.Tools.ModelChecker.Simulation.Soundness
+
+namespace Veil.ModelChecker.Simulation
+
+end Veil.ModelChecker.Simulation
diff --git a/Veil/Core/Tools/ModelChecker/Simulation/Path.lean b/Veil/Core/Tools/ModelChecker/Simulation/Path.lean
new file mode 100644
index 00000000..3984b30b
--- /dev/null
+++ b/Veil/Core/Tools/ModelChecker/Simulation/Path.lean
@@ -0,0 +1,103 @@
+import Veil.Core.Tools.ModelChecker.Simulation.Basic
+import Veil.Core.Tools.ModelChecker.Concrete.Core
+
+namespace Veil.ModelChecker.Simulation
+
+theorem randNat_lt_length {α : Type} (xs : List α) (h : xs ≠ []) (gen : StdGen) :
+ (let p := randNat gen 0 (xs.length - 1); p.1 < xs.length) := by
+ have hlen : 0 < xs.length := by simpa [List.length_pos_iff_ne_nil] using h
+ have hk : xs.length - 1 + 1 = xs.length := Nat.sub_add_cancel (Nat.succ_le_of_lt hlen)
+ unfold randNat
+ simp [Nat.not_lt.mpr (Nat.zero_le (xs.length - 1)), hk]
+ exact Nat.mod_lt _ hlen
+
+@[inline, specialize]
+def simulateOnceLoop {ρ σ κ : Type} {th₀ : ρ}
+ (sys : EnumerableTransitionSystem ρ (List ρ) σ (List σ) Int κ (List (κ × ExecutionOutcome Int σ)) th₀)
+ (params : SearchParameters ρ σ)
+ (th : ρ)
+ (stepsLeft : Nat)
+ (currSt : σ)
+ (trace : Trace ρ σ κ)
+ : StateM StdGen (Option (SimulationResult ρ σ κ)) := do
+ match stepsLeft with
+ | 0 => return none
+ | stepsLeft + 1 =>
+ let outcomes := sys.tr th currSt
+ let (nexts, assertionFailures) := Veil.ModelChecker.Concrete.partitionExecutionOutcome outcomes
+ match assertionFailures with
+ | (label, exId, st) :: _ =>
+ let failedTrace := { trace with failingStep := some { transitionLabel := label, nextState := st } }
+ return some (.foundViolation (.assertionFailure exId) failedTrace)
+ | [] =>
+ match nexts with
+ | [] =>
+ if !params.terminating.holdsOn th currSt then
+ return some (.foundViolation .deadlock trace)
+ else
+ return none
+ | hd :: tl =>
+ let nexts := hd :: tl
+ have hNonempty : nexts ≠ [] := by simp
+ let gen ← get
+ let p := randNat gen 0 (nexts.length - 1)
+ let idx := p.1
+ let gen' := p.2
+ have hlt : idx < nexts.length := by
+ dsimp [idx, p]
+ exact randNat_lt_length nexts hNonempty gen
+ set gen'
+ let (label, nextSt) := nexts.get ⟨idx, hlt⟩
+ let trace := trace.push { transitionLabel := label, nextState := nextSt }
+ let violations := violatedInvariantNames params th nextSt
+ if !violations.isEmpty then
+ return some (.foundViolation (.safetyFailure violations) trace)
+ else
+ simulateOnceLoop sys params th stepsLeft nextSt trace
+termination_by stepsLeft
+
+@[inline, specialize]
+def simulateOnce {ρ σ κ : Type} {th₀ : ρ}
+ (sys : EnumerableTransitionSystem ρ (List ρ) σ (List σ) Int κ (List (κ × ExecutionOutcome Int σ)) th₀)
+ (params : SearchParameters ρ σ)
+ (th : ρ)
+ (maxSteps : Nat)
+ : StateM StdGen (Option (SimulationResult ρ σ κ)) := do
+ let initStates := sys.initStates
+ match initStates with
+ | [] => return none
+ | hd :: tl =>
+ let initStates := hd :: tl
+ have hNonempty : initStates ≠ [] := by simp
+ let gen ← get
+ let p := randNat gen 0 (initStates.length - 1)
+ let idx := p.1
+ let gen' := p.2
+ have hlt : idx < initStates.length := by
+ dsimp [idx, p]
+ exact randNat_lt_length initStates hNonempty gen
+ set gen'
+ let initSt := initStates.get ⟨idx, hlt⟩
+ let initTrace : Trace ρ σ κ := { theory := th, initialState := initSt, steps := #[] }
+ let initViolations := violatedInvariantNames params th initSt
+ if !initViolations.isEmpty then
+ return some (.foundViolation (.safetyFailure initViolations) initTrace)
+ else
+ simulateOnceLoop sys params th maxSteps initSt initTrace
+
+/--
+Simulates the trace identified by `traceIndex` using seed `cfg.seed + traceIndex`.
+Returns the first violation found by that trace.
+-/
+def simulateTraceAtIndex {ρ σ κ : Type} {th₀ : ρ}
+ (sys : EnumerableTransitionSystem ρ (List ρ) σ (List σ) Int κ (List (κ × ExecutionOutcome Int σ)) th₀)
+ (params : SearchParameters ρ σ)
+ (th : ρ)
+ (cfg : SimulateConfig)
+ (traceIndex : Nat)
+ : Option (SimulationResult ρ σ κ) :=
+ let traceSeed := cfg.seed + traceIndex
+ let (maybeResult, _) := (simulateOnce sys params th cfg.maxSteps).run (mkStdGen traceSeed)
+ maybeResult
+
+end Veil.ModelChecker.Simulation
diff --git a/Veil/Core/Tools/ModelChecker/Simulation/Result.lean b/Veil/Core/Tools/ModelChecker/Simulation/Result.lean
new file mode 100644
index 00000000..62aba82b
--- /dev/null
+++ b/Veil/Core/Tools/ModelChecker/Simulation/Result.lean
@@ -0,0 +1,35 @@
+import Veil.Core.Tools.ModelChecker.Simulation.Basic
+
+namespace Veil.ModelChecker.Simulation
+open Lean
+
+private def resultToJson {ρ σ κ : Type} [ToJson ρ] [ToJson σ] [ToJson κ]
+ (result : Option (SimulationResult ρ σ κ)) : Json :=
+ match result with
+ | some (.foundViolation violation trace) =>
+ toJson (ModelCheckingResult.foundViolation Json.null violation (some trace) : ModelCheckingResult ρ σ κ Json)
+ | some .cancelled =>
+ toJson (ModelCheckingResult.cancelled : ModelCheckingResult ρ σ κ Json)
+ | none => Json.mkObj [("result", "no_violation_found")]
+
+private def metadataToJsonFields {ρ σ κ : Type} (r : SimulateResult ρ σ κ) : List (String × Json) :=
+ let reasonField := r.terminationReason.map fun reason => ("termination_reason", Lean.toJson reason)
+ [
+ ("traces_run", Lean.toJson r.tracesRun),
+ ("max_traces", Lean.toJson r.maxTraces),
+ ("elapsed_ms", Lean.toJson r.elapsedMs),
+ ("seed", Lean.toJson r.seed),
+ ("depth", Lean.toJson r.depth)
+ ] ++ reasonField.toList
+
+/-- Flatten the result object while keeping simulation metadata at the top level. -/
+def SimulateResult.toDisplayJson {ρ σ κ : Type} [ToJson ρ] [ToJson σ] [ToJson κ]
+ (r : SimulateResult ρ σ κ) : Json :=
+ match resultToJson r.result with
+ | Json.obj kvs => Json.mkObj <| kvs.toList ++ metadataToJsonFields r
+ | other => Json.mkObj <| ("result", other) :: metadataToJsonFields r
+
+instance instToJsonSimulateResult {ρ σ κ : Type} [ToJson ρ] [ToJson σ] [ToJson κ] : ToJson (SimulateResult ρ σ κ) where
+ toJson r := SimulateResult.toDisplayJson r
+
+end Veil.ModelChecker.Simulation
diff --git a/Veil/Core/Tools/ModelChecker/Simulation/Runtime.lean b/Veil/Core/Tools/ModelChecker/Simulation/Runtime.lean
new file mode 100644
index 00000000..bf99f3c7
--- /dev/null
+++ b/Veil/Core/Tools/ModelChecker/Simulation/Runtime.lean
@@ -0,0 +1,211 @@
+import Veil.Core.Tools.ModelChecker.Simulation.Path
+import Veil.Core.Tools.ModelChecker.Simulation.Soundness
+import Veil.Core.Tools.ModelChecker.Concrete.Progress
+
+namespace Veil.ModelChecker.Simulation
+
+private def noInitialStatesResult {ρ σ κ : Type} (cfg : SimulateConfig) : SimulateResult ρ σ κ := {
+ result := none
+ tracesRun := 0
+ maxTraces := cfg.maxTraces
+ elapsedMs := 0
+ seed := cfg.seed
+ terminationReason := some .noInitialStates
+}
+
+private def hasNoInitialStates {ρ σ κ : Type} {th₀ : ρ}
+ (sys : EnumerableTransitionSystem ρ (List ρ) σ (List σ) Int κ (List (κ × ExecutionOutcome Int σ)) th₀) : Bool :=
+ sys.initStates.isEmpty
+
+private structure SimulationHooks (m : Type → Type) where
+ shouldStop : Nat → m Bool
+ onTraceProgress : Nat → m PUnit
+ onViolation : m PUnit
+
+private def simulateLoopM {m : Type → Type} [Monad m] {ρ σ κ : Type} {th₀ : ρ}
+ (hooks : SimulationHooks m)
+ (sys : EnumerableTransitionSystem ρ (List ρ) σ (List σ) Int κ (List (κ × ExecutionOutcome Int σ)) th₀)
+ (params : SearchParameters ρ σ)
+ (th : ρ)
+ (cfg : SimulateConfig)
+ (remaining : Nat)
+ (traceIndex : Nat)
+ : m (SimulateResult ρ σ κ) := do
+ if ← hooks.shouldStop traceIndex then
+ return {
+ result := some .cancelled
+ tracesRun := traceIndex
+ maxTraces := cfg.maxTraces
+ elapsedMs := 0
+ seed := cfg.seed
+ }
+ match remaining with
+ | 0 =>
+ return {
+ result := none
+ tracesRun := cfg.maxTraces
+ maxTraces := cfg.maxTraces
+ elapsedMs := 0
+ seed := cfg.seed
+ }
+ | remaining + 1 =>
+ hooks.onTraceProgress traceIndex
+ match simulateTraceAtIndex sys params th cfg traceIndex with
+ | some result =>
+ hooks.onViolation
+ return {
+ result := some result
+ tracesRun := traceIndex + 1
+ maxTraces := cfg.maxTraces
+ elapsedMs := 0
+ seed := cfg.seed
+ }
+ | none =>
+ simulateLoopM hooks sys params th cfg remaining (traceIndex + 1)
+termination_by remaining
+
+@[inline, specialize]
+def simulateCommandSemantics {ρ σ κ : Type} {th₀ : ρ}
+ (sys : EnumerableTransitionSystem ρ (List ρ) σ (List σ) Int κ (List (κ × ExecutionOutcome Int σ)) th₀)
+ (params : SearchParameters ρ σ)
+ (th : ρ)
+ (shouldStop : Nat → Bool)
+ (cfg : SimulateConfig)
+ : SimulateResult ρ σ κ :=
+ let sys := restrictSystemByStateConstraints sys params th
+ if hasNoInitialStates sys then
+ noInitialStatesResult cfg
+ else
+ simulateLoopM
+ ({ shouldStop := fun traceIndex => shouldStop traceIndex
+ onTraceProgress := fun _ => ()
+ onViolation := () } : SimulationHooks Id)
+ sys params th cfg cfg.maxTraces 0
+
+@[inline, specialize]
+def simulateCore {ρ σ κ : Type} {th₀ : ρ}
+ (sys : EnumerableTransitionSystem ρ (List ρ) σ (List σ) Int κ (List (κ × ExecutionOutcome Int σ)) th₀)
+ (params : SearchParameters ρ σ)
+ (th : ρ)
+ (cfg : SimulateConfig)
+ : SimulateResult ρ σ κ :=
+ simulateCommandSemantics sys params th (fun _ => false) cfg
+
+@[inline, specialize]
+def simulateWithProgress {ρ σ κ : Type} {th₀ : ρ}
+ (sys : EnumerableTransitionSystem ρ (List ρ) σ (List σ) Int κ (List (κ × ExecutionOutcome Int σ)) th₀)
+ (params : SearchParameters ρ σ)
+ (th : ρ)
+ (cfg : SimulateConfig)
+ (progressInstanceId : Nat)
+ (cancelToken : IO.CancelToken)
+ : IO (SimulateResult ρ σ κ) := do
+ let actualSeed ← if cfg.seed == 0 then IO.rand 0 0xFFFFFFFFFFFFFFFF else pure cfg.seed
+ let cfg := { cfg with seed := actualSeed }
+ let startMs ← IO.monoMsNow
+ let sys := restrictSystemByStateConstraints sys params th
+ if hasNoInitialStates sys then
+ let simResult := { noInitialStatesResult cfg with elapsedMs := (← IO.monoMsNow) - startMs }
+ Veil.ModelChecker.Concrete.updateSimulationProgress progressInstanceId
+ "Complete"
+ simResult.tracesRun simResult.maxTraces simResult.depth
+ return simResult
+ let lastStatusUpdateRef ← IO.mkRef startMs
+ let simResult ← simulateLoopM
+ { shouldStop := fun _ => Veil.ModelChecker.Concrete.shouldStop cancelToken progressInstanceId
+ onTraceProgress := fun tracesRun => do
+ let now ← IO.monoMsNow
+ let lastStatusUpdate ← lastStatusUpdateRef.get
+ if now - lastStatusUpdate ≥ 100 then
+ Veil.ModelChecker.Concrete.updateSimulationProgress progressInstanceId
+ s!"Running random traces ({tracesRun}/{cfg.maxTraces})"
+ tracesRun cfg.maxTraces 0
+ lastStatusUpdateRef.set now
+ onViolation := do
+ Veil.ModelChecker.Concrete.setViolationFound progressInstanceId }
+ sys params th cfg cfg.maxTraces 0
+ let simResult := { simResult with elapsedMs := (← IO.monoMsNow) - startMs }
+ match simResult.result with
+ | some .cancelled => pure ()
+ | _ =>
+ Veil.ModelChecker.Concrete.updateSimulationProgress progressInstanceId
+ "Complete"
+ simResult.tracesRun simResult.maxTraces simResult.depth
+ return simResult
+
+@[inline, specialize]
+def simulate {ρ σ κ : Type} {th₀ : ρ}
+ (sys : EnumerableTransitionSystem ρ (List ρ) σ (List σ) Int κ (List (κ × ExecutionOutcome Int σ)) th₀)
+ (params : SearchParameters ρ σ)
+ (th : ρ)
+ (cfg : SimulateConfig)
+ : IO (SimulateResult ρ σ κ) := do
+ let cancelToken ← IO.CancelToken.new
+ simulateWithProgress sys params th cfg 0 cancelToken
+
+private theorem simulateLoopM_id_sound {ρ σ κ : Type}
+ [DecidableEq σ] [DecidableEq κ]
+ [Inhabited σ] [Inhabited (κ × σ)]
+ (th : ρ)
+ (sys : EnumerableTransitionSystem ρ (List ρ) σ (List σ) Int κ (List (κ × ExecutionOutcome Int σ)) th)
+ (params : SearchParameters ρ σ)
+ (cfg : SimulateConfig)
+ (shouldStop : Nat → Bool) :
+ ∀ remaining traceIndex,
+ ReportedViolationSound sys params
+ (SimulateResult.result
+ (simulateLoopM
+ ({ shouldStop := fun traceIndex => shouldStop traceIndex
+ onTraceProgress := fun _ => ()
+ onViolation := () } : SimulationHooks Id)
+ sys params th cfg remaining traceIndex)) := by
+ intro remaining
+ induction remaining with
+ | zero =>
+ intro traceIndex
+ cases hStop : shouldStop traceIndex <;>
+ simp [simulateLoopM, hStop, ReportedViolationSound, Id.instMonad]
+ | succ remaining ih =>
+ intro traceIndex
+ cases hStop : shouldStop traceIndex with
+ | true =>
+ simp [simulateLoopM, hStop, ReportedViolationSound, Id.instMonad]
+ | false =>
+ by_cases hTrace : simulateTraceAtIndex sys params th cfg traceIndex = none
+ · simpa [simulateLoopM, hStop, hTrace, Id.instMonad] using ih (traceIndex + 1)
+ · cases hRun : simulateTraceAtIndex sys params th cfg traceIndex with
+ | none => contradiction
+ | some result =>
+ simpa [simulateLoopM, hStop, hRun, Id.instMonad] using
+ simulateTraceAtIndex_sound th sys params cfg traceIndex result hRun
+
+theorem simulateCommandSemantics_sound {ρ σ κ : Type}
+ [DecidableEq σ] [DecidableEq κ]
+ [Inhabited σ] [Inhabited (κ × σ)]
+ (th : ρ)
+ (sys : EnumerableTransitionSystem ρ (List ρ) σ (List σ) Int κ (List (κ × ExecutionOutcome Int σ)) th)
+ (params : SearchParameters ρ σ)
+ (shouldStop : Nat → Bool)
+ (cfg : SimulateConfig) :
+ ReportedViolationSound (restrictSystemByStateConstraints sys params th) params
+ (SimulateResult.result (simulateCommandSemantics sys params th shouldStop cfg)) := by
+ let restrictedSys := restrictSystemByStateConstraints sys params th
+ cases hNoInit : hasNoInitialStates restrictedSys with
+ | true =>
+ simp [simulateCommandSemantics, restrictedSys, hNoInit, noInitialStatesResult, ReportedViolationSound]
+ | false =>
+ simpa [simulateCommandSemantics, restrictedSys, hNoInit] using
+ simulateLoopM_id_sound th restrictedSys params cfg shouldStop cfg.maxTraces 0
+
+theorem simulateCore_sound {ρ σ κ : Type}
+ [DecidableEq σ] [DecidableEq κ]
+ [Inhabited σ] [Inhabited (κ × σ)]
+ (th : ρ)
+ (sys : EnumerableTransitionSystem ρ (List ρ) σ (List σ) Int κ (List (κ × ExecutionOutcome Int σ)) th)
+ (params : SearchParameters ρ σ)
+ (cfg : SimulateConfig) :
+ ReportedViolationSound (restrictSystemByStateConstraints sys params th) params
+ (SimulateResult.result (simulateCore sys params th cfg)) := by
+ simpa [simulateCore] using simulateCommandSemantics_sound th sys params (fun _ => false) cfg
+
+end Veil.ModelChecker.Simulation
diff --git a/Veil/Core/Tools/ModelChecker/Simulation/Soundness.lean b/Veil/Core/Tools/ModelChecker/Simulation/Soundness.lean
new file mode 100644
index 00000000..fa30ae37
--- /dev/null
+++ b/Veil/Core/Tools/ModelChecker/Simulation/Soundness.lean
@@ -0,0 +1,297 @@
+import Veil.Core.Tools.ModelChecker.Simulation.Basic
+import Veil.Core.Tools.ModelChecker.Simulation.Path
+import Veil.Core.Tools.ModelChecker.Concrete.Core
+
+namespace Veil.ModelChecker.Simulation
+
+theorem pickedTransition_valid {ρ σ κ : Type}
+ [DecidableEq σ] [DecidableEq κ]
+ (th : ρ)
+ (sys : EnumerableTransitionSystem ρ (List ρ) σ (List σ) Int κ (List (κ × ExecutionOutcome Int σ)) th)
+ (_params : SearchParameters ρ σ) (currSt : σ)
+ (nexts : List (κ × σ))
+ (hNexts : nexts = (Veil.ModelChecker.Concrete.partitionExecutionOutcome
+ (sys.tr th currSt)).fst)
+ (selected : κ × σ)
+ (hSelected : selected ∈ nexts) :
+ sys.toRelational.tr th currSt selected.1 selected.2 := by
+ have hGood : selected ∈
+ (Veil.ModelChecker.Concrete.partitionExecutionOutcome
+ (sys.tr th currSt)).fst := by
+ simpa [hNexts] using hSelected
+ simpa [EnumerableTransitionSystem.toRelational] using
+ (Veil.ModelChecker.Concrete.partitionExecutionOutcome.fst_spec _ _ _).mp hGood
+
+theorem pickedInitialState_valid {ρ σ κ : Type}
+ [DecidableEq σ] [DecidableEq κ]
+ (th : ρ)
+ (sys : EnumerableTransitionSystem ρ (List ρ) σ (List σ) Int κ (List (κ × ExecutionOutcome Int σ)) th)
+ (_params : SearchParameters ρ σ)
+ (initStates : List σ)
+ (hInitStates : initStates = sys.initStates)
+ (selectedInit : σ)
+ (hSelected : selectedInit ∈ initStates) :
+ ({ theory := th, initialState := selectedInit, steps := #[] } : Trace ρ σ κ).isValid
+ sys.toRelational := by
+ refine Trace.isValid_empty sys.toRelational th selectedInit ?_ ?_
+ · simp [EnumerableTransitionSystem.toRelational]
+ · simpa [EnumerableTransitionSystem.toRelational, hInitStates] using hSelected
+
+def Trace.witnessesSimulationViolation {ρ σ κ : Type} {th₀ : ρ}
+ [DecidableEq σ] [DecidableEq κ]
+ (sys : EnumerableTransitionSystem ρ (List ρ) σ (List σ) Int κ (List (κ × ExecutionOutcome Int σ)) th₀)
+ (params : SearchParameters ρ σ) (trace : Trace ρ σ κ) : ViolationKind → Prop
+ | .assumptionFailure violates =>
+ trace.isValid sys.toRelational ∧
+ params.violatedAssumptions trace.theory = violates ∧
+ violates ≠ []
+ | .safetyFailure violates =>
+ trace.isValid sys.toRelational ∧
+ trace.failingStep = none ∧
+ violatedInvariantNames params trace.theory trace.lastState = violates ∧
+ violates ≠ []
+ | .deadlock =>
+ trace.isValid sys.toRelational ∧
+ trace.failingStep = none ∧
+ (Veil.ModelChecker.Concrete.partitionExecutionOutcome
+ (sys.tr trace.theory trace.lastState)).fst = [] ∧
+ (Veil.ModelChecker.Concrete.partitionExecutionOutcome
+ (sys.tr trace.theory trace.lastState)).snd = [] ∧
+ !params.terminating.holdsOn trace.theory trace.lastState = true
+ | .assertionFailure exId =>
+ trace.isValid sys.toRelational ∧
+ ∃ step,
+ trace.failingStep = some step ∧
+ (step.transitionLabel, ExecutionOutcome.assertionFailure exId step.nextState) ∈
+ sys.tr trace.theory trace.lastState
+
+theorem Trace.witnessesSimulationViolation_valid {ρ σ κ : Type} {th : ρ}
+ [DecidableEq σ] [DecidableEq κ]
+ (sys : EnumerableTransitionSystem ρ (List ρ) σ (List σ) Int κ (List (κ × ExecutionOutcome Int σ)) th)
+ (params : SearchParameters ρ σ) (trace : Trace ρ σ κ) (violation : ViolationKind) :
+ Trace.witnessesSimulationViolation sys params trace violation →
+ trace.isValid sys.toRelational := by
+ intro h
+ cases violation with
+ | assumptionFailure _ => exact h.1
+ | safetyFailure _ => exact h.1
+ | deadlock => exact h.1
+ | assertionFailure _ => exact h.1
+
+def ReportedViolationSound {ρ σ κ : Type} {th₀ : ρ}
+ [DecidableEq σ] [DecidableEq κ]
+ (sys : EnumerableTransitionSystem ρ (List ρ) σ (List σ) Int κ (List (κ × ExecutionOutcome Int σ)) th₀)
+ (params : SearchParameters ρ σ) (result : Option (SimulationResult ρ σ κ)) : Prop :=
+ match result with
+ | some (.foundViolation violation trace) => Trace.witnessesSimulationViolation sys params trace violation
+ | some .cancelled => True
+ | none => True
+
+theorem simulateOnceLoop_sound {ρ σ κ : Type}
+ [DecidableEq σ] [DecidableEq κ] [Inhabited (κ × σ)]
+ (th : ρ)
+ (sys : EnumerableTransitionSystem ρ (List ρ) σ (List σ) Int κ (List (κ × ExecutionOutcome Int σ)) th)
+ (params : SearchParameters ρ σ)
+ (currSt : σ)
+ (trace : Trace ρ σ κ)
+ (hTheory : trace.theory = th)
+ (hValid : trace.isValid sys.toRelational)
+ (hLast : trace.lastState = currSt)
+ (hNoFail : trace.failingStep = none) :
+ ∀ stepsLeft gen result,
+ ((simulateOnceLoop sys params th stepsLeft currSt trace).run gen).1 = some result ->
+ ReportedViolationSound sys params (some result) := by
+ intro stepsLeft
+ induction stepsLeft generalizing currSt trace with
+ | zero =>
+ intro gen result h
+ simp [simulateOnceLoop] at h
+ | succ steps ih =>
+ intro gen result h
+ cases hPartition : Veil.ModelChecker.Concrete.partitionExecutionOutcome
+ (sys.tr th currSt) with
+ | mk nexts assertionFailures =>
+ cases hFailures : assertionFailures with
+ | cons failure failures =>
+ rcases failure with ⟨label, exId, st⟩
+ simp [simulateOnceLoop, hPartition, hFailures] at h
+ cases h
+ have hFailureMem : (label, exId, st) ∈
+ (Veil.ModelChecker.Concrete.partitionExecutionOutcome
+ (sys.tr th currSt)).snd := by
+ rw [hPartition]
+ simp [hFailures]
+ have hMem : (label, ExecutionOutcome.assertionFailure exId st) ∈
+ sys.tr th currSt :=
+ (Veil.ModelChecker.Concrete.partitionExecutionOutcome.snd_spec _ _ _ _).mp hFailureMem
+ let step : Step σ κ := { transitionLabel := label, nextState := st }
+ let failedTrace := { trace with failingStep := some step }
+ have hValidFail : failedTrace.isValid sys.toRelational := by
+ exact {
+ theorySatisfiesAssumptions := hValid.theorySatisfiesAssumptions
+ initialStateSatisfiesInit := hValid.initialStateSatisfiesInit
+ stepsValid := hValid.stepsValid
+ }
+ refine ⟨hValidFail, step, rfl, ?_⟩
+ have hLastFail : failedTrace.lastState = currSt := by
+ simpa [failedTrace, Trace.lastState] using hLast
+ rw [hLastFail]
+ simpa [failedTrace, step, hTheory] using hMem
+ | nil =>
+ cases hNexts : nexts with
+ | nil =>
+ cases hTerminating : !params.terminating.holdsOn th currSt with
+ | true =>
+ simp [simulateOnceLoop, hPartition, hFailures, hNexts, hTerminating] at h
+ cases h
+ have hNoSuccesses : (Veil.ModelChecker.Concrete.partitionExecutionOutcome
+ (sys.tr trace.theory trace.lastState)).fst = [] := by
+ simp [hTheory, hLast, hPartition, hNexts]
+ have hNoFailures : (Veil.ModelChecker.Concrete.partitionExecutionOutcome
+ (sys.tr trace.theory trace.lastState)).snd = [] := by
+ simp [hTheory, hLast, hPartition, hFailures]
+ have hDeadlock : !params.terminating.holdsOn trace.theory trace.lastState = true := by
+ simpa [hTheory, hLast] using hTerminating
+ exact ⟨hValid, hNoFail, hNoSuccesses, hNoFailures, hDeadlock⟩
+ | false =>
+ simp [simulateOnceLoop, hPartition, hFailures, hNexts, hTerminating] at h
+ | cons hd tl =>
+ let nexts' : List (κ × σ) := hd :: tl
+ have hNonempty : nexts' ≠ [] := by simp [nexts']
+ let p := randNat gen 0 (nexts'.length - 1)
+ let idx := p.1
+ let gen' := p.2
+ have hlt : idx < nexts'.length := by
+ dsimp [idx, p]
+ exact randNat_lt_length nexts' hNonempty gen
+ let selected := nexts'.get ⟨idx, hlt⟩
+ let trace' := trace.push { transitionLabel := selected.1, nextState := selected.2 }
+ have hSelected : selected ∈ nexts' := by
+ dsimp [selected]
+ exact List.get_mem nexts' ⟨idx, hlt⟩
+ have hNextsHd : nexts' = (Veil.ModelChecker.Concrete.partitionExecutionOutcome
+ (sys.tr th currSt)).fst := by
+ simp [nexts', hPartition, hNexts]
+ have hRel : sys.toRelational.tr th currSt selected.1 selected.2 :=
+ pickedTransition_valid th sys params currSt nexts' hNextsHd selected hSelected
+ have hValid' : trace'.isValid sys.toRelational := by
+ exact Trace.push_isValid trace { transitionLabel := selected.1, nextState := selected.2 }
+ sys.toRelational hValid (by simpa [hTheory, hLast] using hRel)
+ have hTheory' : trace'.theory = th := by
+ simpa [trace', hTheory]
+ have hNoFail' : trace'.failingStep = none := by
+ simpa [trace', hNoFail]
+ have hLast' : trace'.lastState = selected.2 := by
+ simp [trace']
+ cases hViol : (violatedInvariantNames params th selected.2).isEmpty with
+ | true =>
+ have hViolNil : violatedInvariantNames params th selected.2 = [] := by
+ simpa using hViol
+ have hViolNilRaw :
+ violatedInvariantNames params th (hd :: tl)[(randNat gen 0 tl.length).1].2 = [] := by
+ simpa [nexts', p, idx, selected] using hViolNil
+ have hLoop : ((simulateOnceLoop sys params th steps selected.2 trace').run gen').1 = some result := by
+ rw [simulateOnceLoop] at h
+ simp only [hPartition, hFailures, hNexts, StateT.run_bind, Id.instMonad] at h
+ simpa [nexts', p, idx, gen', hlt, selected, trace', hViolNilRaw] using h
+ exact ih selected.2 trace' hTheory' hValid' hLast' hNoFail' gen' result hLoop
+ | false =>
+ have hNonempty : violatedInvariantNames params th selected.2 ≠ [] := by
+ intro hNil
+ simp [hNil] at hViol
+ have hNonemptyRaw :
+ violatedInvariantNames params th (hd :: tl)[(randNat gen 0 tl.length).1].2 ≠ [] := by
+ simpa [nexts', p, idx, selected] using hNonempty
+ have hFound : (some (SimulationResult.foundViolation
+ (ViolationKind.safetyFailure (violatedInvariantNames params th selected.2)) trace') :
+ Option (SimulationResult ρ σ κ)) = some result := by
+ rw [simulateOnceLoop] at h
+ simp only [hPartition, hFailures, hNexts, StateT.run_bind, Id.instMonad] at h
+ simpa [nexts', p, idx, gen', hlt, selected, trace', hNonemptyRaw] using h
+ cases hFound
+ have hViolEq : violatedInvariantNames params trace'.theory trace'.lastState =
+ violatedInvariantNames params th selected.2 := by
+ simp [hTheory', hLast']
+ exact ⟨hValid', hNoFail', hViolEq, hNonempty⟩
+
+theorem simulateOnce_sound {ρ σ κ : Type}
+ [DecidableEq σ] [DecidableEq κ] [Inhabited σ] [Inhabited (κ × σ)]
+ (th : ρ)
+ (sys : EnumerableTransitionSystem ρ (List ρ) σ (List σ) Int κ (List (κ × ExecutionOutcome Int σ)) th)
+ (params : SearchParameters ρ σ) (gen : StdGen) (maxSteps : Nat) (result : SimulationResult ρ σ κ) :
+ ((simulateOnce sys params th maxSteps).run gen).1 = some result ->
+ ReportedViolationSound sys params (some result) := by
+ intro h
+ unfold simulateOnce at h
+ cases hStates : sys.initStates with
+ | nil => simp [hStates] at h
+ | cons initSt rest =>
+ let initStates : List σ := initSt :: rest
+ have hNonempty : initStates ≠ [] := by simp [initStates]
+ let p := randNat gen 0 (initStates.length - 1)
+ let idx := p.1
+ let gen' := p.2
+ have hlt : idx < initStates.length := by
+ dsimp [idx, p]
+ exact randNat_lt_length initStates hNonempty gen
+ let selectedInit := initStates.get ⟨idx, hlt⟩
+ let initTrace : Trace ρ σ κ := { theory := th, initialState := selectedInit, steps := #[] }
+ have hSelectedInit : selectedInit ∈ initStates := by
+ dsimp [selectedInit]
+ exact List.get_mem initStates ⟨idx, hlt⟩
+ have hInitStates : initStates = sys.initStates := by
+ simp [initStates, hStates]
+ have hValid : initTrace.isValid sys.toRelational := by
+ simpa [initTrace] using
+ pickedInitialState_valid th sys params initStates hInitStates selectedInit hSelectedInit
+ have hLast : initTrace.lastState = selectedInit := by
+ simp [initTrace]
+ have hNoFail : initTrace.failingStep = none := by
+ simp [initTrace]
+ cases hViol : (violatedInvariantNames params th selectedInit).isEmpty with
+ | true =>
+ have hViolNil : violatedInvariantNames params th selectedInit = [] := by
+ simpa using hViol
+ have hViolNilRaw :
+ violatedInvariantNames params th (initSt :: rest)[(randNat gen 0 rest.length).1] = [] := by
+ simpa [initStates, p, idx, selectedInit] using hViolNil
+ have hLoop : ((simulateOnceLoop sys params th maxSteps selectedInit initTrace).run gen').1 = some result := by
+ rw [hStates] at h
+ simp only [StateT.run_bind, Id.instMonad] at h
+ simpa [initStates, p, idx, gen', hlt, selectedInit, initTrace, hViolNilRaw] using h
+ exact simulateOnceLoop_sound th sys params selectedInit initTrace rfl hValid hLast hNoFail maxSteps gen' result hLoop
+ | false =>
+ have hNonempty : violatedInvariantNames params th selectedInit ≠ [] := by
+ intro hNil
+ simp [hNil] at hViol
+ have hNonemptyRaw :
+ violatedInvariantNames params th (initSt :: rest)[(randNat gen 0 rest.length).1] ≠ [] := by
+ simpa [initStates, p, idx, selectedInit] using hNonempty
+ have hFound : (some (SimulationResult.foundViolation
+ (ViolationKind.safetyFailure (violatedInvariantNames params th selectedInit)) initTrace) :
+ Option (SimulationResult ρ σ κ)) = some result := by
+ rw [hStates] at h
+ simp only [StateT.run_bind, Id.instMonad] at h
+ simpa [initStates, p, idx, gen', hlt, selectedInit, initTrace, hNonemptyRaw] using h
+ cases hFound
+ exact ⟨hValid, hNoFail, rfl, hNonempty⟩
+
+/-- Any violation reported by a single indexed random trace is sound. -/
+theorem simulateTraceAtIndex_sound {ρ σ κ : Type}
+ [DecidableEq σ] [DecidableEq κ] [Inhabited σ] [Inhabited (κ × σ)]
+ (th : ρ)
+ (sys : EnumerableTransitionSystem ρ (List ρ) σ (List σ) Int κ (List (κ × ExecutionOutcome Int σ)) th)
+ (params : SearchParameters ρ σ)
+ (cfg : SimulateConfig)
+ (traceIndex : Nat)
+ (result : SimulationResult ρ σ κ) :
+ simulateTraceAtIndex sys params th cfg traceIndex = some result ->
+ ReportedViolationSound sys params (some result) := by
+ intro h
+ unfold simulateTraceAtIndex at h
+ set traceSeed := cfg.seed + traceIndex
+ have hSimSome : ((simulateOnce sys params th cfg.maxSteps).run (mkStdGen traceSeed)).1 = some result := by
+ simpa [traceSeed] using h
+ exact simulateOnce_sound th sys params (mkStdGen traceSeed) cfg.maxSteps result hSimSome
+
+end Veil.ModelChecker.Simulation
diff --git a/Veil/Core/UI/Trace/TraceDisplay.lean b/Veil/Core/UI/Trace/TraceDisplay.lean
index 155e0e9d..1606c242 100644
--- a/Veil/Core/UI/Trace/TraceDisplay.lean
+++ b/Veil/Core/UI/Trace/TraceDisplay.lean
@@ -88,6 +88,15 @@ def formatTrace (j : Json) (ind : String := " ") : String := Id.run do
| .arr states => r ++ (states.toList.map (fmtState · ind) |> "\n".intercalate)
| _ => r ++ s!"{ind}(no states)"
+private def fmtSeedSuffix (j : Json) : String :=
+ let seed := j.getObjValD "seed"
+ if seed == .null then "" else s!"\nSeed: {fmtJson seed}"
+
+private def isNoInitialStatesTermination (j : Json) : Bool :=
+ match j.getObjValD "termination_reason" with
+ | .obj reason => fmtJson ((Json.obj reason).getObjValD "kind") == "no_initial_states"
+ | _ => false
+
def formatModelCheckingResult (j : Json) : MessageData :=
match fmtJson (j.getObjValD "result") with
| "found_violation" =>
@@ -97,12 +106,17 @@ def formatModelCheckingResult (j : Json) : MessageData :=
| _ => ""
let trace := j.getObjValD "trace"
let traceMsg := if trace == .null then "" else s!"\n{formatTrace trace}"
- m!"❌ Violation: {fmtJson (v.getObjValD "kind")}{violates}{traceMsg}"
+ m!"❌ Violation: {fmtJson (v.getObjValD "kind")}{violates}{traceMsg}{fmtSeedSuffix j}"
| "no_violation_found" =>
let trace := j.getObjValD "trace"
- if trace != .null then m!"✅ Satisfying trace found\n{formatTrace trace}"
- else m!"✅ No violation (explored {fmtJson (j.getObjValD "explored_states")} states)"
- | "cancelled" => m!"⚠️ Cancelled"
+ if trace != .null then m!"✅ Satisfying trace found\n{formatTrace trace}{fmtSeedSuffix j}"
+ else if isNoInitialStatesTermination j then
+ m!"✅ No initial states available after applying state constraints{fmtSeedSuffix j}"
+ else if j.getObjValD "traces_run" != .null then
+ m!"✅ No violation in {fmtJson (j.getObjValD "traces_run")} traces{fmtSeedSuffix j}"
+ else
+ m!"✅ No violation (explored {fmtJson (j.getObjValD "explored_states")} states){fmtSeedSuffix j}"
+ | "cancelled" => m!"⚠️ Cancelled{fmtSeedSuffix j}"
| r => if j.getObjValD "error" != .null then m!"💥 Error: {fmtJson (j.getObjValD "error")}" else m!"Unknown: {r}"
end Veil.TraceDisplay
diff --git a/Veil/Core/UI/Widget/ProgressViewer.lean b/Veil/Core/UI/Widget/ProgressViewer.lean
index 39644ea5..1247f615 100644
--- a/Veil/Core/UI/Widget/ProgressViewer.lean
+++ b/Veil/Core/UI/Widget/ProgressViewer.lean
@@ -498,8 +498,23 @@ private def metricsHistoryHtml (history : Array ProgressHistoryPoint) : Html :=
/-- Convert Progress to Html for display, with optional Stop button. Uses TLC-style terminology. -/
-def progressToHtml (p : Progress) (instanceId? : Option Nat := none) : Html :=
-
+def progressToHtml (p : Progress) (instanceId? : Option Nat := none) : Html := Id.run do
+ let (progressRows, metricsHistory, actionCoverage) : Array Html × Html × Html :=
+ match p.simulation with
+ | some sim =>
+ (#[
+ statRow "Traces Run:" (toString sim.tracesRun),
+ statRow "Max Traces:" (toString sim.maxTraces),
+ statRow "Depth:" (toString sim.depth),
+ ], .text "", .text "")
+ | none =>
+ (#[
+ statRow "Diameter:" (toString p.diameter),
+ statRow "States Found:" (toString p.statesFound),
+ statRow "Distinct States:" (toString p.distinctStates),
+ statRow "Queue:" (toString p.queue),
+ ], metricsHistoryHtml p.history, actionCoverageHtml p.actionStats p.allActionLabels)
+ return
{if p.isRunning then
@@ -518,16 +533,10 @@ def progressToHtml (p : Progress) (instanceId? : Option Nat := none) : Html :=
Done!
}
-
- {statRow "Diameter:" (toString p.diameter)}
- {statRow "States Found:" (toString p.statesFound)}
- {statRow "Distinct States:" (toString p.distinctStates)}
- {statRow "Queue:" (toString p.queue)}
- {statRow "Elapsed time:" (formatElapsedTime p.elapsedMs)}
-
+ {.element "tbody" #[] (progressRows.push (statRow "Elapsed time:" (formatElapsedTime p.elapsedMs)))}
- {metricsHistoryHtml p.history}
- {actionCoverageHtml p.actionStats p.allActionLabels}
+ {metricsHistory}
+ {actionCoverage}
{match p.compilationStatus with
| .inProgress ms lines => if lines.isEmpty then .text "" else compilationLogHtml ms lines
| .failed err => compilationFailureHtml err
diff --git a/Veil/Frontend/DSL/Module/Elaborators.lean b/Veil/Frontend/DSL/Module/Elaborators.lean
index 8bbf05c2..78726036 100644
--- a/Veil/Frontend/DSL/Module/Elaborators.lean
+++ b/Veil/Frontend/DSL/Module/Elaborators.lean
@@ -13,6 +13,7 @@ import Veil.Core.Tools.Verifier.Results
import Veil.Core.UI.Verifier.VerificationResults
import Veil.Core.UI.Trace.TraceDisplay
import Veil.Core.Tools.ModelChecker.Concrete.Checker
+import Veil.Core.Tools.ModelChecker.Simulation
import Veil.Frontend.DSL.Action.Extract
import Veil.Frontend.DSL.Module.Util.Enumeration
import Veil.Util.Multiprocessing
@@ -606,6 +607,35 @@ def defaultThresholdToParallel : Nat := 20
declare_command_config_elab elabModelCheckerConfig ModelCheckerConfig
+declare_command_config_elab elabSimulateConfig ModelChecker.Simulation.SimulateConfig
+
+/--
+Check whether a particular config field was written explicitly in the command syntax.
+
+`elabSimulateConfig` returns a complete `SimulateConfig`, so omitted fields are
+indistinguishable from fields explicitly written with their structure defaults
+after elaboration. Inspect the raw `Parser.Tactic.optConfig` only for this
+distinction. A `(config := cfg)` item is an opaque full `SimulateConfig`, so it
+is treated as explicitly providing all fields rather than being overlaid with
+global options.
+-/
+def simulateConfigHasField (cfgStx : Syntax) (fieldName : Name) : Bool :=
+ Lean.Elab.Tactic.mkConfigItemViews (Lean.Parser.Tactic.getConfigItems cfgStx) |>.any
+ (fun item =>
+ let optionName := item.option.getId.eraseMacroScopes
+ optionName == fieldName || optionName == `config)
+
+/-- Return which `#simulate` trace-bound fields were supplied by command syntax. -/
+private def simulateTraceBoundFieldsExplicit (cfgStx : Syntax) : Bool × Bool :=
+ (simulateConfigHasField cfgStx `maxTraces, simulateConfigHasField cfgStx `maxSteps)
+
+/-- Resolve `#simulate` trace-bound fields, preserving explicit default literals. -/
+def resolveSimulateTraceBounds (cfg0 : ModelChecker.Simulation.SimulateConfig)
+ (commandHasMaxTraces commandHasMaxSteps : Bool) (optionMaxTraces optionMaxSteps : Nat) : Nat × Nat :=
+ let maxTraces := if commandHasMaxTraces then cfg0.maxTraces else optionMaxTraces
+ let maxSteps := if commandHasMaxSteps then cfg0.maxSteps else optionMaxSteps
+ (maxTraces, maxSteps)
+
/-- Model checking mode: interpreted only, compiled only, or default (both with handoff). -/
inductive ModelCheckingMode where
| interpreted
@@ -630,6 +660,73 @@ def getModelCheckingMode (modeStx : Syntax) : ModelCheckingMode :=
| `(modelCheckMode| compiled) => .compiled
| _ => .default
+/-- Get all action label names for never-enabled action warnings. -/
+private def getActionLabelNames (mod : Module) : CommandElabM (List String) := do
+ let labelTypeName ← resolveGlobalConstNoOverload labelType
+ return mod.actions.map (fun a => s!"{labelTypeName}.{a.name}") |>.toList
+
+/-- Warn if the module contains transitions (which are slow to model check). -/
+private def warnAboutTransitions (mod : Module) : CommandElabM Unit := do
+ let transitions := mod.procedures.filter (·.info.isTransition)
+ if transitions.isEmpty then return
+ let names := ", ".intercalate (transitions.map (·.info.name.toString) |>.toList)
+ logWarning m!"Explicit state model checking of transitions is SLOW!\n\n\
+ The current implementation enumerates all possible states and filters those satisfying \
+ the transition relation. Your specification has {transitions.size} \
+ transition{if transitions.size > 1 then "s" else ""}: {names}\n\n\
+ Consider encoding transitions as imperative actions where possible."
+
+/-- Get the theory term, defaulting to `{}` if not provided and there are no theory fields. -/
+private def resolveTheoryTerm (cmdName : String) (theoryTermOpt : Option Term)
+ (mod : Module) (instTerm : Term) : CommandElabM Term := do
+ match theoryTermOpt with
+ | some t => pure t
+ | none =>
+ unless mod.immutableComponents.isEmpty do
+ let fieldStrs := mod.immutableComponents.map (fun c => s!"{c.name} := ...")
+ let theoryExample := "{ " ++ ", ".intercalate fieldStrs.toList ++ " }"
+ throwError "This module has immutable fields, so you must specify the theory instantiation:\n\
+ {cmdName} {instTerm} {theoryExample}"
+ `({})
+
+/-- Prepend `name` with `mod.name`. -/
+private def mkIdentWithModName (mod : Module) (name : Name) : Ident :=
+ Lean.mkIdent (mod.name ++ name)
+
+/-- Build search parameters for model checking / simulation. -/
+private def buildSearchParameters (mod : Module) (config : ModelCheckerConfig) : CommandElabM Term := do
+ let mkAssumption (sa : StateAssertion) : CommandElabM Term :=
+ `($(mkIdent ``Veil.ModelChecker.TheoryProperty.mk)
+ ($(mkIdent `name) := $(quote sa.name))
+ ($(mkIdent `property) := fun $(mkIdent `th) => $(mkIdentWithModName mod sa.name) $(mkIdent `th)))
+ -- Build SafetyProperty.mk syntax for a StateAssertion
+ let mkProp (sa : StateAssertion) : CommandElabM Term :=
+ `($(mkIdent ``Veil.ModelChecker.SafetyProperty.mk)
+ ($(mkIdent `name) := $(quote sa.name))
+ ($(mkIdent `property) := fun $(mkIdent `th) $(mkIdent `st) => $(mkIdentWithModName mod sa.name) $(mkIdent `th) $(mkIdent `st)))
+ let assumptionList ← `([$((← mod.assumptions.mapM mkAssumption)),*])
+ let safetyList ← `([$((← mod.invariants.mapM mkProp)),*])
+ -- FIXME: Only recognizing the first termination property might confuse users
+ let terminatingProp ← match mod.terminations[0]? with
+ | some t => mkProp t
+ | none => `($(mkIdent `default))
+ let constraintList ← `([$((← mod.stateConstraints.mapM mkProp)),*])
+ let earlyTermConds ← do
+ let base ← `([$(mkIdent ``Veil.ModelChecker.EarlyTerminationCondition.foundViolatingState),
+ $(mkIdent ``Veil.ModelChecker.EarlyTerminationCondition.assertionFailed),
+ $(mkIdent ``Veil.ModelChecker.EarlyTerminationCondition.deadlockOccurred)])
+ if config.maxDepth > 0 then `($base ++ [$(mkIdent ``Veil.ModelChecker.EarlyTerminationCondition.reachedDepthBound) $(quote config.maxDepth)])
+ else pure base
+ `({ $(mkIdent `assumptions):ident := $assumptionList, $(mkIdent `invariants):ident := $safetyList, $(mkIdent `terminating):ident := $terminatingProp,
+ $(mkIdent `stateConstraints):ident := $constraintList,
+ $(mkIdent `earlyTerminationConditions):ident := $earlyTermConds })
+
+/-- Stable identity for a single compiled command invocation within a file. -/
+private def getCompiledCommandId (cmdName : String) (stx : Syntax) : CommandElabM String := do
+ let some startPos := stx.getPos? | throwError s!"Unexpected error: {cmdName} has no position"
+ let some endPos := stx.getTailPos? | throwError s!"Unexpected error: {cmdName} has no end position"
+ pure s!"{startPos.1}-{endPos.1}"
+
@[command_elab Veil.modelCheck]
def elabModelCheck : CommandElab := fun stx => do
-- Use dynamic trace class name for detailed profiling
@@ -644,19 +741,6 @@ def elabModelCheck : CommandElab := fun stx => do
let cfg := stx[4]
elabModelCheckCore stx mode instTerm theoryTermOpt assumptionsHoldBy cfg
where
- /-- Get the theory term, defaulting to `{}` if not provided and there are no theory fields.
- Throws a helpful error if theory fields exist but no term was provided. -/
- getTheoryTerm (theoryTermOpt : Option Term) (mod : Module) (instTerm : Term) : CommandElabM Term := do
- match theoryTermOpt with
- | some t => pure t
- | none =>
- unless mod.immutableComponents.isEmpty do
- let fieldStrs := mod.immutableComponents.map (fun c => s!"{c.name} := ...")
- let theoryExample := "{ " ++ ", ".intercalate fieldStrs.toList ++ " }"
- throwError "This module has immutable fields, so you must specify the theory instantiation:\n\
- #model_check {instTerm} {theoryExample}"
- `({})
-
/-- Generate the model source for compilation:
1. Insert `set_option veil.__modelCheckCompileMode true` after imports
2. Keep everything up to the point where the spec was finalized
@@ -685,72 +769,21 @@ where
let modelCheckCmd := String.Pos.Raw.extract src modelCheckStart modelCheckEnd
return beforeImports ++ compileModePreamble ++ afterImportsToSpecFinalized ++ "\n" ++ modelCheckCmd ++ "\n"
- /-- Prepend `name` with `mod.name`. Useful when expressions are printed out for debugging. -/
- mkIdentWithModName (mod : Module) (name : Name) : Ident :=
- Lean.mkIdent (mod.name ++ name)
-
- /-- Display a TraceDisplayViewer widget with the given result term. -/
- displayResultWidget (stx : Syntax) (resultTerm : Term) : CommandElabM Unit := do
- let widgetExpr ← `(open ProofWidgets.Jsx in
-
)
- let html ← ← liftTermElabM <| ProofWidgets.HtmlCommand.evalCommandMHtml <| ← ``(ProofWidgets.HtmlEval.eval $widgetExpr)
- liftCoreM <| Widget.savePanelWidgetInfo
- (hash ProofWidgets.HtmlDisplayPanel.javascript)
- (return json% { html: $(← Server.rpcEncode html) }) stx
-
- mkSearchParameters (mod : Module) (config : ModelCheckerConfig) : CommandElabM Term := do
- let mkAssumption (sa : StateAssertion) : CommandElabM Term :=
- `($(mkIdent ``Veil.ModelChecker.TheoryProperty.mk)
- ($(mkIdent `name) := $(quote sa.name))
- ($(mkIdent `property) := fun $(mkIdent `th) => $(mkIdentWithModName mod sa.name) $(mkIdent `th)))
- -- Build SafetyProperty.mk syntax for a StateAssertion
- let mkProp (sa : StateAssertion) : CommandElabM Term :=
- `($(mkIdent ``Veil.ModelChecker.SafetyProperty.mk)
- ($(mkIdent `name) := $(quote sa.name))
- ($(mkIdent `property) := fun $(mkIdent `th) $(mkIdent `st) => $(mkIdentWithModName mod sa.name) $(mkIdent `th) $(mkIdent `st)))
- let assumptionList ← `([$((← mod.assumptions.mapM mkAssumption)),*])
- let safetyList ← `([$((← mod.invariants.mapM mkProp)),*])
- -- FIXME: Only recognizing the first termination property might confuse users
- let terminatingProp ← match mod.terminations[0]? with
- | some t => mkProp t
- | none => `($(mkIdent `default))
- let constraintList ← `([$((← mod.stateConstraints.mapM mkProp)),*])
- let earlyTermConds ← do
- let base ← `([$(mkIdent ``Veil.ModelChecker.EarlyTerminationCondition.foundViolatingState),
- $(mkIdent ``Veil.ModelChecker.EarlyTerminationCondition.assertionFailed),
- $(mkIdent ``Veil.ModelChecker.EarlyTerminationCondition.deadlockOccurred)])
- if config.maxDepth > 0 then `($base ++ [$(mkIdent ``Veil.ModelChecker.EarlyTerminationCondition.reachedDepthBound) $(quote config.maxDepth)])
- else pure base
- `({ $(mkIdent `assumptions):ident := $assumptionList, $(mkIdent `invariants):ident := $safetyList, $(mkIdent `terminating):ident := $terminatingProp,
- $(mkIdent `stateConstraints):ident := $constraintList,
- $(mkIdent `earlyTerminationConditions):ident := $earlyTermConds })
-
/-- Build the core model checker call syntax (without parallel config). -/
mkModelCheckerCall (mod : Module) (config : ModelCheckerConfig)
(instTerm theoryTerm : Term) : CommandElabM Term := do
let inst := mkVeilImplementationDetailIdent `inst
let th := mkVeilImplementationDetailIdent `th
let instSortArgs ← (← mod.uninterpretedParamIdents).mapM fun paramIdent => `($inst.$(paramIdent))
- let sp ← mkSearchParameters mod config
+ let sp ← buildSearchParameters mod config
-- Model checker call with type annotation to help inference
-- Note: findReachable takes parallelCfg, progressInstanceId, and cancelToken as the last three args
`((let $inst : $instantiationType := $instTerm
let $th : $theoryIdent $instSortArgs* := $theoryTerm
$(mkIdent ``Veil.ModelChecker.Concrete.findReachable)
- ($(mkIdent `inhabσ) := $instInhabitedStateFieldConcreteType)
- ($(mkIdentWithModName mod `enumerableTransitionSystem) $instSortArgs* $th)
- $sp : _ → _ → _ → IO _))
-
- /-- Warn if the module contains transitions (which are slow to model check). -/
- warnAboutTransitions (mod : Module) : CommandElabM Unit := do
- let transitions := mod.procedures.filter (·.info.isTransition)
- if transitions.isEmpty then return
- let names := ", ".intercalate (transitions.map (·.info.name.toString) |>.toList)
- logWarning m!"Explicit state model checking of transitions is SLOW!\n\n\
- The current implementation enumerates all possible states and filters those satisfying \
- the transition relation. Your specification has {transitions.size} \
- transition{if transitions.size > 1 then "s" else ""}: {names}\n\n\
- Consider encoding transitions as imperative actions where possible."
+ ($(mkIdent `inhabσ) := $instInhabitedStateFieldConcreteType)
+ ($(mkIdentWithModName mod `enumerableTransitionSystem) $instSortArgs* $th)
+ $sp : _ → _ → _ → IO _))
/-- Check that the provided theory satisfies all module assumptions by
elaborating a proof obligation using the assembled `Assumptions` definition.
@@ -777,7 +810,6 @@ where
dsimp only [$assembledAssumptions:ident]
$userTac:tactic)
elabVeilCommand proofCmd
-
/-- Create an error JSON object. -/
errorJson (msg : String) : Json := Json.mkObj [("error", msg)]
@@ -791,7 +823,10 @@ where
/-- Handle internal mode: define and export the model checker result. -/
elabModelCheckInternalMode (mod : Module) (callExpr : Term) : CommandElabM Unit := do
- elabVeilCommand (← `(def $(mkIdent `modelCheckerResult) (pcfg : Option Veil.ModelChecker.ParallelConfig) (progressInstanceId : Nat) (cancelToken : IO.CancelToken) := $callExpr pcfg progressInstanceId cancelToken))
+ elabVeilCommand (← `(def $(mkIdent `modelCheckerResult)
+ (pcfg : Option Veil.ModelChecker.ParallelConfig) (progressInstanceId : Nat)
+ (cancelToken : IO.CancelToken) : IO Lean.Json :=
+ Lean.toJson <$> $callExpr pcfg progressInstanceId cancelToken))
elabVeilCommand (← `(end $(mkIdent mod.name)))
elabVeilCommand (← `(export $(mkIdent mod.name) ($(mkIdent `modelCheckerResult))))
@@ -808,12 +843,10 @@ where
ModelChecker.Concrete.finishProgress instanceId (errorJson s!"Binary not found at {binPath}")
return none
- /-- Run the model checker binary and finish with result. Returns true if completed. -/
- runBinaryAndFinish (binPath : System.FilePath) (parallelCfg : Option ModelChecker.ParallelConfig)
- (instanceId : Nat) (cancelToken : IO.CancelToken)
- (assertionSources : Std.HashMap AssertionId AssertionSourceInfo) : IO Bool := do
+ /-- Run the compiled binary and return its JSON result if completed. -/
+ runBinaryForJson (binPath : System.FilePath) (args : Array String)
+ (instanceId : Nat) (cancelToken : IO.CancelToken) : IO (Option Json) := do
ModelChecker.Concrete.updateStatus instanceId "Running compiled binary..."
- let args := parallelCfg.map (fun p => #[s!"{p.numSubTasks}", s!"{p.thresholdToParallel}"]) |>.getD #[]
let child ← IO.Process.spawn {
cmd := toString (binPath / "ModelCheckerMain"), args,
stdin := .piped, stdout := .piped, stderr := .piped }
@@ -826,30 +859,32 @@ where
match Json.parse line >>= FromJson.fromJson? (α := ModelChecker.Concrete.Progress) with
| .ok p => if let some refs ← ModelChecker.Concrete.getProgressRefs instanceId then
refs.progressRef.modify fun old =>
- let historyPoint : ModelChecker.Concrete.ProgressHistoryPoint := {
- timestamp := p.elapsedMs
- diameter := p.diameter
- statesFound := p.statesFound
- distinctStates := p.distinctStates
- queue := p.queue
- }
- { p with allActionLabels := old.allActionLabels, history := old.history.push historyPoint }
+ let history := match p.simulation with
+ | some _ => old.history
+ | none =>
+ let historyPoint : ModelChecker.Concrete.ProgressHistoryPoint := {
+ timestamp := p.elapsedMs
+ diameter := p.diameter
+ statesFound := p.statesFound
+ distinctStates := p.distinctStates
+ queue := p.queue
+ }
+ old.history.push historyPoint
+ { p with allActionLabels := old.allActionLabels, history }
| .error _ => stderrAccum.modify (· ++ line)
let stdoutTask ← IO.asTask (prio := .dedicated) child.stdout.readToEnd
let waitTask ← IO.asTask (prio := .dedicated) child.wait
-- Monitor for cancellation
while !(← IO.hasFinished waitTask) do
- if ← checkCancelled cancelToken instanceId then child.kill; return false
+ if ← checkCancelled cancelToken instanceId then child.kill; return none
IO.sleep 100
let stdout ← IO.ofExcept (← IO.wait stdoutTask)
let exitCode ← IO.ofExcept (← IO.wait waitTask)
let stderr ← stderrAccum.get
if exitCode != 0 then
ModelChecker.Concrete.finishProgress instanceId (errorJson s!"Binary exited with code {exitCode}{if stderr.isEmpty then "" else s!"\n{stderr}"}")
- return true
- let json := Json.parse stdout |>.toOption.getD (errorJson s!"Failed to parse output: {stdout.take 500}")
- ModelChecker.Concrete.finishProgress instanceId (enrichJsonWithAssertions json assertionSources)
- return true
+ return none
+ return some (Json.parse stdout |>.toOption.getD (errorJson s!"Failed to parse output: {stdout.take 500}"))
/-- Elaborate the interpreted mode computation. Must be called synchronously. -/
elaborateInterpretedComputation (instanceId : Nat) (callExpr : Term)
@@ -863,11 +898,6 @@ where
Term.synthesizeSyntheticMVarsNoPostponing
unsafe Meta.evalExpr (IO Lean.Json) (mkApp (mkConst ``IO) (mkConst ``Lean.Json)) (← instantiateMVars expr)
- /-- Get all action label names for never-enabled action warnings. -/
- getActionLabelNames (mod : Module) : CommandElabM (List String) := do
- let labelTypeName ← resolveGlobalConstNoOverload labelType
- return mod.actions.map (fun a => s!"{labelTypeName}.{a.name}") |>.toList
-
/-- Log model checking result. -/
logModelCheckResult (stx : Syntax) (resultJson : Json) : CommandElabM Unit := do
let msg := TraceDisplay.formatModelCheckingResult resultJson
@@ -895,24 +925,43 @@ where
logModelCheckResult ctx.stx json
ModelChecker.Concrete.finishProgress ctx.instanceId json
+ modelCheckerCommandSpec : ModelChecker.Compilation.CompiledCommandSpec := {
+ exportedName := "modelCheckerResult"
+ supportsParallelConfig := true
+ }
+
+ simulateCommandSpec : ModelChecker.Compilation.CompiledCommandSpec := {
+ exportedName := "simulateResult"
+ }
+
/-- Run the compiled binary and log the result. -/
runBinaryAndLogResult (ctx : ModelCheckContext) (buildFolder : System.FilePath)
- (sourceFile : String) : CommandElabM Unit := do
+ (sourceFile : String) (command : ModelChecker.Compilation.CompiledCommandSpec)
+ (commandId : String) : CommandElabM Unit := do
let some binPath ← verifyBinaryExists buildFolder ctx.instanceId | return
- let _ ← runBinaryAndFinish binPath ctx.parallelCfg ctx.instanceId ctx.cancelToken ctx.assertionSources
- ModelChecker.Compilation.markRegistryFinished sourceFile buildFolder
+ let args := ctx.parallelCfg.map (fun p => #[s!"{p.numSubTasks}", s!"{p.thresholdToParallel}"]) |>.getD #[]
+ let some json ← runBinaryForJson binPath args ctx.instanceId ctx.cancelToken | return
+ ModelChecker.Concrete.finishProgress ctx.instanceId (enrichJsonWithAssertions json ctx.assertionSources)
+ ModelChecker.Compilation.markRegistryFinished sourceFile command commandId buildFolder
let some resultJson ← ModelChecker.Concrete.getResultJson ctx.instanceId | return
logModelCheckResult ctx.stx resultJson
/-- Compile the model. Returns the build folder path if compilation succeeded, none otherwise. -/
compileModel (mod : Module) (sourceFile : String) (modelSource : String)
- (instanceId : Nat) : IO (Option System.FilePath) := do
- let buildFolder ← ModelChecker.Compilation.createBuildFolder sourceFile modelSource mod.name.toString
- ModelChecker.Compilation.markRegistryInProgress sourceFile instanceId buildFolder
+ (commandId : String) (instanceId : Nat) (cancelToken : IO.CancelToken)
+ (command : ModelChecker.Compilation.CompiledCommandSpec) : IO (Option System.FilePath) := do
+ let buildFolder ← ModelChecker.Compilation.createBuildFolder sourceFile modelSource mod.name.toString command commandId
+ ModelChecker.Compilation.markRegistryInProgress sourceFile command commandId instanceId buildFolder
let result ← ModelChecker.Compilation.runProcessWithStatusCallback
+ sourceFile
+ command
+ commandId
{ cmd := "lake", args := #["build", "ModelCheckerMain"], cwd := buildFolder }
+ instanceId cancelToken
(fun elapsedMs => ModelChecker.Concrete.updateCompilationElapsed instanceId elapsedMs)
(fun line isError elapsedMs => ModelChecker.Concrete.updateCompilationLog instanceId elapsedMs line isError)
+ if result.interrupted then
+ return none
if result.exitCode != 0 then
ModelChecker.Concrete.updateCompilationStatus instanceId (.failed (mkCompilationErrorMsg result))
return none
@@ -927,7 +976,7 @@ where
let mod ← getCurrentModule (errMsg := "You cannot #model_check outside of a Veil module!")
mod.throwIfSpecNotFinalized
- let theoryTerm ← getTheoryTerm theoryTermOpt mod instTerm
+ let theoryTerm ← resolveTheoryTerm "#model_check" theoryTermOpt mod instTerm
warnAboutTransitions mod
let config ← elabModelCheckerConfig cfg
@@ -980,13 +1029,15 @@ where
-- dbg_trace "elabModelCheckCompiledMode"
let ctx ← allocModelCheckContext mod stx parallelCfg
let sourceFile ← getFileName
+ let commandId ← getCompiledCommandId "#model_check" stx
let modelSource ← generateModelSource mod stx
let compilationComputation ← Command.wrapAsyncAsSnapshot (fun () => do
try
- let some buildFolder ← compileModel mod sourceFile modelSource ctx.instanceId | return
+ let some buildFolder ← compileModel mod sourceFile modelSource commandId ctx.instanceId ctx.cancelToken
+ modelCheckerCommandSpec | return
if ← checkCancelled ctx.cancelToken ctx.instanceId then return
- runBinaryAndLogResult ctx buildFolder sourceFile
+ runBinaryAndLogResult ctx buildFolder sourceFile modelCheckerCommandSpec commandId
catch e : Exception =>
handleModelCheckError ctx e
) ctx.cancelToken
@@ -1001,6 +1052,7 @@ where
-- dbg_trace "elabModelCheckWithHandoff"
let ctx ← allocModelCheckContext mod stx parallelCfg
let sourceFile ← getFileName
+ let commandId ← getCompiledCommandId "#model_check" stx
let modelSource ← generateModelSource mod stx
let ioComputation ← elaborateInterpretedComputation ctx.instanceId callExpr parallelCfg
@@ -1022,10 +1074,11 @@ where
let compilationCancelTk ← IO.CancelToken.new
let compilationComputation ← Command.wrapAsyncAsSnapshot (fun () => do
try
- let some buildFolder ← compileModel mod sourceFile modelSource ctx.instanceId | return
+ let some buildFolder ← compileModel mod sourceFile modelSource commandId ctx.instanceId compilationCancelTk
+ modelCheckerCommandSpec | return
-- Skip handoff if violation found or interpreted finished
if (← ModelChecker.Concrete.isViolationFound ctx.instanceId) || (← IO.hasFinished interpretedTask) then
- ModelChecker.Compilation.markRegistryFinished sourceFile buildFolder
+ ModelChecker.Compilation.markRegistryFinished sourceFile modelCheckerCommandSpec commandId buildFolder
return
-- Handoff to compiled binary
ModelChecker.Concrete.requestHandoff ctx.instanceId
@@ -1033,7 +1086,7 @@ where
let _ ← IO.wait interpretedTask
let some newCancelToken ← ModelChecker.Concrete.resetProgressForHandoff ctx.instanceId | return
let ctxWithNewToken := { ctx with cancelToken := newCancelToken }
- runBinaryAndLogResult ctxWithNewToken buildFolder sourceFile
+ runBinaryAndLogResult ctxWithNewToken buildFolder sourceFile modelCheckerCommandSpec commandId
catch e : Exception =>
ModelChecker.Concrete.updateCompilationStatus ctx.instanceId (.failed s!"{← e.toMessageData.toString}")
) compilationCancelTk
@@ -1042,4 +1095,222 @@ where
ModelChecker.displayStreamingProgress stx ctx.instanceId
+/-- Build the progress-aware simulator runtime call syntax. -/
+private def mkSimulatorRuntimeCall (mod : Module) (instTerm theoryTerm : Term)
+ (sp : Term) (cfg : ModelChecker.Simulation.SimulateConfig) : CommandElabM Term := do
+ let inst := mkVeilImplementationDetailIdent `inst
+ let th := mkVeilImplementationDetailIdent `th
+ let instSortArgs ← (← mod.uninterpretedParamIdents).mapM fun paramIdent => `($inst.$(paramIdent))
+ let cfgTerm ← `($(mkIdent ``Veil.ModelChecker.Simulation.SimulateConfig.mk)
+ $(quote cfg.maxTraces) $(quote cfg.maxSteps) $(quote cfg.seed))
+ `((let $inst : $instantiationType := $instTerm
+ let $th : $theoryIdent $instSortArgs* := $theoryTerm
+ $(mkIdent ``Veil.ModelChecker.Simulation.simulateWithProgress)
+ ($(mkIdentWithModName mod `enumerableTransitionSystem) $instSortArgs* $th)
+ $sp $th $cfgTerm : _ → _ → IO _))
+
+/-- Build the simulator runtime call syntax with progress and cancellation hooks. -/
+private def mkSimulateJsonExpr (resultIdent : Ident) : CommandElabM Term :=
+ `($(mkIdent ``Veil.ModelChecker.Simulation.SimulateResult.toDisplayJson) $resultIdent)
+
+private def generateCompiledModelSourcePrefix (mod : Module) (stx : Syntax) : CommandElabM String := do
+ let src := (← getFileMap).source
+ let afterImportsPos := ModelChecker.Compilation.findPosAfterImports src
+ let compileModePreamble := "\nset_option veil.__modelCheckCompileMode true\n"
+ let some specFinalizedAtStx := mod.specFinalizedAtStx
+ | throwError "Internal error: spec should be finalized before generating model source"
+ let some commandStart := stx.getPos? | throwError "Unexpected error: command has no position"
+ let modelCheckTriggeredFinalization := specFinalizedAtStx.getPos? == stx.getPos?
+ let specFinalizedAtPos := if modelCheckTriggeredFinalization
+ then commandStart
+ else specFinalizedAtStx.getTailPos?.getD commandStart
+ let beforeImports := String.Pos.Raw.extract src 0 afterImportsPos
+ let afterImportsToSpecFinalized := String.Pos.Raw.extract src afterImportsPos specFinalizedAtPos
+ return beforeImports ++ compileModePreamble ++ afterImportsToSpecFinalized ++ "\n"
+
+private def getSourceSlice (stx : Syntax) : CommandElabM String := do
+ let some startPos := stx.getPos? | throwError "Unexpected error: syntax has no start position"
+ let some endPos := stx.getTailPos? | throwError "Unexpected error: syntax has no end position"
+ return String.Pos.Raw.extract (← getFileMap).source startPos endPos
+
+private def generateSimulateModelSource (mod : Module) (stx : Syntax)
+ (cfg : ModelChecker.Simulation.SimulateConfig) : CommandElabM String := do
+ let srcPrefix ← generateCompiledModelSourcePrefix mod stx
+ let instSrc ← getSourceSlice stx[2]
+ let theorySrc ← if stx[3].isNone then pure " {}" else do
+ let raw ← getSourceSlice stx[3][0]
+ pure s!" {raw}"
+ let cmd := s!"#simulate {instSrc}{theorySrc} (maxTraces := {cfg.maxTraces}) (maxSteps := {cfg.maxSteps}) (seed := {cfg.seed})"
+ return srcPrefix ++ cmd ++ "\n"
+
+private def elaborateSimulateComputation (instanceId : Nat) (callExpr : Term) : CommandElabM (IO Lean.Json) := do
+ let resultIdent := mkVeilImplementationDetailIdent `simulateRuntimeResult
+ let jsonExpr ← mkSimulateJsonExpr resultIdent
+ let resultExpr ← `(do
+ let some refs ← Veil.ModelChecker.Concrete.getProgressRefs $(quote instanceId) | pure Lean.Json.null
+ let $resultIdent ← ($callExpr $(quote instanceId) refs.cancelToken)
+ pure $jsonExpr)
+ liftTermElabM do
+ let expr ← Term.elabTerm resultExpr none
+ Term.synthesizeSyntheticMVarsNoPostponing
+ unsafe Meta.evalExpr (IO Lean.Json) (mkApp (mkConst ``IO) (mkConst ``Lean.Json)) (← instantiateMVars expr)
+
+private def simulationResultWasCancelled (combinedJson : Json) : Bool :=
+ match combinedJson.getObjValAs? String "result" |>.toOption with
+ | some "cancelled" => true
+ | _ => false
+
+private def finishWithSimulationResult (ctx : ModelCheckContext) (combinedJson : Json) : CommandElabM Unit := do
+ if simulationResultWasCancelled combinedJson then
+ liftIO <| ModelChecker.Concrete.cancelProgress ctx.instanceId combinedJson
+ else
+ elabModelCheck.finishWithResult ctx combinedJson
+
+private def runSimulateBinaryAndLogResult (ctx : ModelCheckContext) (buildFolder : System.FilePath)
+ (sourceFile : String) (commandId : String) : CommandElabM Unit := do
+ let some binPath ← elabModelCheck.verifyBinaryExists buildFolder ctx.instanceId | return
+ let some combinedJson ← elabModelCheck.runBinaryForJson binPath #[] ctx.instanceId ctx.cancelToken | return
+ ModelChecker.Compilation.markRegistryFinished sourceFile elabModelCheck.simulateCommandSpec commandId buildFolder
+ finishWithSimulationResult ctx combinedJson
+
+private def elabSimulateInternalMode (mod : Module) (callExpr : Term) : CommandElabM Unit := do
+ let resultIdent := mkVeilImplementationDetailIdent `simulateRuntimeResult
+ let jsonExpr ← mkSimulateJsonExpr resultIdent
+ elabVeilCommand (← `(def $(mkIdent `simulateResult)
+ (progressInstanceId : Nat) (cancelToken : IO.CancelToken) : IO Lean.Json := do
+ let $resultIdent ← ($callExpr progressInstanceId cancelToken)
+ pure $jsonExpr))
+ elabVeilCommand (← `(end $(mkIdent mod.name)))
+ elabVeilCommand (← `(export $(mkIdent mod.name) ($(mkIdent `simulateResult))))
+
+private def elabSimulateInterpretedMode (mod : Module) (stx : Syntax) (callExpr : Term) : CommandElabM Unit := do
+ let ctx ← elabModelCheck.allocModelCheckContext mod stx none
+ let ioComputation ← elaborateSimulateComputation ctx.instanceId callExpr
+ let computation ← Command.wrapAsyncAsSnapshot (fun () => do
+ try
+ if ← elabModelCheck.checkCancelled ctx.cancelToken ctx.instanceId then return
+ let combinedJson ← IO.ofExcept (← ioComputation.toIO')
+ finishWithSimulationResult ctx combinedJson
+ catch e : Exception =>
+ elabModelCheck.handleModelCheckError ctx e
+ ) ctx.cancelToken
+ let task ← BaseIO.asTask (computation ()) (prio := .dedicated)
+ Command.logSnapshotTask { stx? := none, cancelTk? := ctx.cancelToken, task }
+ ModelChecker.displayStreamingProgress stx ctx.instanceId
+
+private def elabSimulateCompiledMode (mod : Module) (stx : Syntax)
+ (cfg : ModelChecker.Simulation.SimulateConfig) : CommandElabM Unit := do
+ let ctx ← elabModelCheck.allocModelCheckContext mod stx none
+ let sourceFile ← getFileName
+ let commandId ← getCompiledCommandId "#simulate" stx
+ let modelSource ← generateSimulateModelSource mod stx cfg
+ let compilationComputation ← Command.wrapAsyncAsSnapshot (fun () => do
+ try
+ let some buildFolder ← elabModelCheck.compileModel mod sourceFile modelSource commandId ctx.instanceId ctx.cancelToken
+ elabModelCheck.simulateCommandSpec | return
+ if ← elabModelCheck.checkCancelled ctx.cancelToken ctx.instanceId then return
+ runSimulateBinaryAndLogResult ctx buildFolder sourceFile commandId
+ catch e : Exception =>
+ elabModelCheck.handleModelCheckError ctx e
+ ) ctx.cancelToken
+ let compilationTask ← BaseIO.asTask (compilationComputation ()) (prio := .dedicated)
+ Command.logSnapshotTask { stx? := none, cancelTk? := ctx.cancelToken, task := compilationTask }
+ ModelChecker.displayStreamingProgress stx ctx.instanceId
+
+private def elabSimulateWithHandoff (mod : Module) (stx : Syntax) (callExpr : Term)
+ (cfg : ModelChecker.Simulation.SimulateConfig) : CommandElabM Unit := do
+ let ctx ← elabModelCheck.allocModelCheckContext mod stx none
+ let sourceFile ← getFileName
+ let commandId ← getCompiledCommandId "#simulate" stx
+ let modelSource ← generateSimulateModelSource mod stx cfg
+ let ioComputation ← elaborateSimulateComputation ctx.instanceId callExpr
+ let compilationCancelTk ← IO.CancelToken.new
+ liftIO <| ModelChecker.Concrete.setCompilationCancelToken ctx.instanceId (some compilationCancelTk)
+ let finishCompilation (buildFolder : System.FilePath) : IO Unit := do
+ ModelChecker.Compilation.markRegistryFinished sourceFile elabModelCheck.simulateCommandSpec commandId buildFolder
+ ModelChecker.Concrete.setCompilationCancelToken ctx.instanceId none
+ let interpretedComputation ← Command.wrapAsyncAsSnapshot (fun () => do
+ try
+ let combinedJson ← IO.ofExcept (← ioComputation.toIO')
+ match (← ctx.cancelToken.isSet, ← ModelChecker.Concrete.checkHandoffRequested ctx.instanceId) with
+ | (true, false) =>
+ if simulationResultWasCancelled combinedJson then
+ finishWithSimulationResult ctx combinedJson
+ else
+ ModelChecker.Concrete.cancelProgress ctx.instanceId
+ | (false, _) => finishWithSimulationResult ctx combinedJson
+ | (true, true) => pure ()
+ catch e : Exception =>
+ elabModelCheck.handleModelCheckError ctx e
+ ) ctx.cancelToken
+ let interpretedTask ← BaseIO.asTask (interpretedComputation ()) (prio := .dedicated)
+ Command.logSnapshotTask { stx? := none, cancelTk? := ctx.cancelToken, task := interpretedTask }
+ let compilationComputation ← Command.wrapAsyncAsSnapshot (fun () => do
+ try
+ let some buildFolder ← elabModelCheck.compileModel mod sourceFile modelSource commandId ctx.instanceId compilationCancelTk
+ elabModelCheck.simulateCommandSpec | do
+ ModelChecker.Concrete.setCompilationCancelToken ctx.instanceId none
+ return
+ if (← ModelChecker.Concrete.isViolationFound ctx.instanceId) || (← IO.hasFinished interpretedTask) ||
+ (← ModelChecker.Concrete.isCancelled ctx.instanceId) then
+ finishCompilation buildFolder
+ return
+ ModelChecker.Concrete.requestHandoff ctx.instanceId
+ ctx.cancelToken.set
+ let _ ← IO.wait interpretedTask
+ if (← ctx.cancelToken.isSet) && !(← ModelChecker.Concrete.checkHandoffRequested ctx.instanceId) then
+ ModelChecker.Concrete.cancelProgress ctx.instanceId
+ finishCompilation buildFolder
+ return
+ if (← ModelChecker.Concrete.getResultJson ctx.instanceId).isSome || (← ModelChecker.Concrete.isCancelled ctx.instanceId) then
+ finishCompilation buildFolder
+ return
+ let some newCancelToken ← ModelChecker.Concrete.resetProgressForHandoff ctx.instanceId | return
+ ModelChecker.Concrete.setCompilationCancelToken ctx.instanceId none
+ let ctxWithNewToken := { ctx with cancelToken := newCancelToken }
+ runSimulateBinaryAndLogResult ctxWithNewToken buildFolder sourceFile commandId
+ catch e : Exception =>
+ ModelChecker.Concrete.setCompilationCancelToken ctx.instanceId none
+ ModelChecker.Concrete.updateCompilationStatus ctx.instanceId (.failed s!"{← e.toMessageData.toString}")
+ ) compilationCancelTk
+ let compilationTask ← BaseIO.asTask (compilationComputation ()) (prio := .dedicated)
+ Command.logSnapshotTask { stx? := none, cancelTk? := compilationCancelTk, task := compilationTask }
+ ModelChecker.displayStreamingProgress stx ctx.instanceId
+
+@[command_elab Veil.simulate]
+def elabSimulate : CommandElab := fun stx => do
+ withTraceNode `veil.perf.elaborator.simulate (fun _ => return "#simulate") do
+ let mode := getModelCheckingMode stx[1]
+ let instTerm : Term := ⟨stx[2]⟩
+ let theoryTermOpt : Option Term := if stx[3].isNone then none else some ⟨stx[3][0]⟩
+ let assumptionsHoldBy : Option (TSyntax `Lean.Parser.Tactic.tacticSeq) :=
+ if stx[5].isNone then none else some ⟨stx[5][0][1]⟩
+ let mod ← getCurrentModule (errMsg := "You cannot #simulate outside of a Veil module!")
+ mod.throwIfSpecNotFinalized
+ let theoryTerm ← resolveTheoryTerm "#simulate" theoryTermOpt mod instTerm
+ warnAboutTransitions mod
+ let simulateCfgStx := stx[4]
+ let cfg0 ← elabSimulateConfig simulateCfgStx
+ let opts ← getOptions
+ let (hasMaxTraces, hasMaxSteps) := simulateTraceBoundFieldsExplicit simulateCfgStx
+ let optionMaxTraces := veil.simulate.maxTraces.get opts
+ let optionMaxSteps := veil.simulate.maxSteps.get opts
+ let (maxTraces, maxSteps) := resolveSimulateTraceBounds cfg0 hasMaxTraces hasMaxSteps
+ optionMaxTraces optionMaxSteps
+ let seed ← liftIO <| if cfg0.seed == 0 then IO.rand 0 0xFFFFFFFFFFFFFFFF else pure cfg0.seed
+ let cfg : ModelChecker.Simulation.SimulateConfig := { cfg0 with maxTraces, maxSteps, seed }
+ let mcCfg : ModelCheckerConfig := { maxDepth := 0, sequential := false, parallelCfg := none }
+ if assumptionsHoldBy.isSome && !(← isModelCheckCompileMode) && !mod.assumptions.isEmpty then
+ elabModelCheck.checkTheorySatisfiesAssumptions mod instTerm theoryTerm assumptionsHoldBy
+ mod.ensureExecutableModelCheckerDefinitions
+ let sp ← buildSearchParameters mod mcCfg
+ let runtimeCallExpr ← mkSimulatorRuntimeCall mod instTerm theoryTerm sp cfg
+ if ← isModelCheckCompileMode then
+ elabSimulateInternalMode mod runtimeCallExpr
+ return
+ let effectiveMode := if (← liftIO isVeilOnlineEnv) then .interpreted else mode
+ match effectiveMode with
+ | .interpreted => elabSimulateInterpretedMode mod stx runtimeCallExpr
+ | .compiled => elabSimulateCompiledMode mod stx cfg
+ | .default => elabSimulateWithHandoff mod stx runtimeCallExpr cfg
end Veil
diff --git a/Veil/Frontend/DSL/Module/Syntax.lean b/Veil/Frontend/DSL/Module/Syntax.lean
index 81b84348..d5a32ca2 100644
--- a/Veil/Frontend/DSL/Module/Syntax.lean
+++ b/Veil/Frontend/DSL/Module/Syntax.lean
@@ -373,4 +373,27 @@ scoped syntax (name := crFunction) kw_function : concreteRepField
scoped syntax (name := concreteRepresentationDecl) "veil_set_field_representation " concreteRepField ident : command
+/-- Run random-walk simulation on the current module.
+ Explores random traces to find shallow invariant violations quickly.
+
+## Execution Modes
+
+**Default behavior** (`#simulate`):
+- Runs interpreted mode immediately and shows streaming progress
+- Starts compilation in background
+- When compilation finishes before interpreted mode does, restarts with the
+ compiled binary using the same chosen seed
+
+**Interpreted-only mode** (`#simulate interpreted`):
+- Runs only interpreted mode without background compilation
+
+**Compiled-only mode** (`#simulate compiled`):
+- Builds and runs the compiled binary directly
+
+Seed defaults to a generated value if omitted; the chosen seed is always shown
+in output for reproducibility and reused consistently across mode handoff.
+
+Example: `#simulate {}` or `#simulate compiled {} (maxTraces := 100, seed := 42)` -/
+scoped syntax (name := simulate) "#simulate " (modelCheckMode)? term:max (term:max)? Parser.Tactic.optConfig (assumptionsHoldBy)? : command
+
end Veil
diff --git a/Veil/Frontend/DSL/Module/Util/ForModelChecker.lean b/Veil/Frontend/DSL/Module/Util/ForModelChecker.lean
index 795ca921..74bfc012 100644
--- a/Veil/Frontend/DSL/Module/Util/ForModelChecker.lean
+++ b/Veil/Frontend/DSL/Module/Util/ForModelChecker.lean
@@ -19,17 +19,43 @@ inductive Status
| finished (buildDir : System.FilePath)
deriving Inhabited
-/-- Global state tracking compilation status for multiple models.
-Keyed by the source file path (absolute path).
-Uses `Std.Mutex` to prevent race conditions when multiple tasks access the registry. -/
-initialize compilationRegistry : Std.Mutex (Std.HashMap String Status) ←
+/-- Description of a command that can be compiled into a generated executable. -/
+structure CompiledCommandSpec where
+ /-- Name of the generated definition that the compiled executable calls. -/
+ exportedName : String
+ /-- Whether the generated definition accepts an optional parallel configuration. -/
+ supportsParallelConfig : Bool := false
+
+/-- Registry key for one compiled command invocation. -/
+structure CompilationKey where
+ /-- Source file containing the compiled command invocation. -/
+ sourceFile : String
+ /-- Generated definition called by the compiled executable. -/
+ exportedName : String
+ /-- Identity of the specific command invocation within `sourceFile`. -/
+ commandId : String
+ deriving BEq, Hashable, Inhabited
+
+/-- Global state tracking compilation status for multiple compiled commands.
+ Keyed by source file path, exported command name, and command identity so
+ different command invocations in the same file do not supersede each other.
+ Uses `Std.Mutex` to prevent race conditions when multiple tasks access the registry. -/
+initialize compilationRegistry : Std.Mutex (Std.HashMap CompilationKey Status) ←
Std.Mutex.new {}
@[inline]
-def stillCurrentCont (sourceFile : String) (instanceId : Nat) (k : Std.AtomicT (Std.HashMap String Status) IO Unit) : IO Bool :=
+def mkCompilationKey (sourceFile : String) (command : CompiledCommandSpec) (commandId : String) : CompilationKey := {
+ sourceFile,
+ exportedName := command.exportedName,
+ commandId,
+}
+
+@[inline]
+def stillCurrentCont (sourceFile : String) (command : CompiledCommandSpec) (commandId : String) (instanceId : Nat)
+ (k : Std.AtomicT (Std.HashMap CompilationKey Status) IO Unit) : IO Bool :=
compilationRegistry.atomically fun ref => do
let registry ← ref.get
- match registry[sourceFile]? with
+ match registry[mkCompilationKey sourceFile command commandId]? with
| some info =>
match info with
| .inProgress id _ => if id == instanceId then k ref ; pure true else pure false
@@ -37,27 +63,30 @@ def stillCurrentCont (sourceFile : String) (instanceId : Nat) (k : Std.AtomicT (
| none => pure false
/-- Mark compilation as finished in the registry. -/
-def markRegistryFinished (sourceFile : String) (buildFolder : System.FilePath) : IO Unit :=
+def markRegistryFinished (sourceFile : String) (command : CompiledCommandSpec) (commandId : String)
+ (buildFolder : System.FilePath) : IO Unit :=
compilationRegistry.atomically fun ref =>
- ref.modify fun registry => registry.insert sourceFile (.finished buildFolder)
+ ref.modify fun registry =>
+ registry.insert (mkCompilationKey sourceFile command commandId) (.finished buildFolder)
/-- Mark compilation as in progress in the registry. -/
-def markRegistryInProgress (sourceFile : String) (instanceId : Nat) (buildFolder : System.FilePath) : IO Unit :=
+def markRegistryInProgress (sourceFile : String) (command : CompiledCommandSpec) (commandId : String)
+ (instanceId : Nat) (buildFolder : System.FilePath) : IO Unit :=
compilationRegistry.atomically fun ref =>
- ref.modify fun registry => registry.insert sourceFile (.inProgress instanceId buildFolder)
+ ref.modify fun registry =>
+ registry.insert (mkCompilationKey sourceFile command commandId) (.inProgress instanceId buildFolder)
/-- Base directory for model checker build folders. This is an absolute path. -/
def getBuildBaseDir : IO System.FilePath := do
let pwd ← IO.currentDir
return pwd / ".lake" / "model_checker_builds"
-/-- Generate a build folder name based on the source file name, so that for the
-same source file we get the same build folder. -/
-def generateBuildFolderName (sourceFile : String) : IO System.FilePath := do
- -- Use the source file's stem (filename without extension) for readability
+/-- Generate a build folder name based on the source file and exported command. -/
+def generateBuildFolderName (sourceFile : String) (command : CompiledCommandSpec) (_commandId : String) : IO System.FilePath := do
let stem := System.FilePath.mk sourceFile |>.fileStem.getD "unrecognized_model"
+ let suffix := toString (hash (sourceFile ++ ":" ++ command.exportedName))
let baseDir ← getBuildBaseDir
- return baseDir / stem
+ return baseDir / s!"{stem}_{command.exportedName}_{suffix}"
/-- Template for the `lakefile.lean` in the temp project. Note that it does
not only require the parent Veil project, but also *all the dependencies*;
@@ -93,7 +122,7 @@ lean_exe ModelCheckerMain where
/-- Template for the ModelCheckerMain.lean in the temp project.
Takes the namespace of the specification to open scoped instances. -/
-def modelCheckerMainTemplate (specNamespace : String) : String :=
+def modelCheckerMainTemplate (specNamespace : String) (command : CompiledCommandSpec) : String :=
"import Model
set_option maxHeartbeats 6400000
@@ -129,25 +158,29 @@ def main (args : List String) : IO Unit := do
-- Instance ID is not used in compiled mode, pass 0
-- Cancel token is created locally; cancellation is handled by killing the process from outside
let cancelTk ← IO.CancelToken.new
- let res ← modelCheckerResult pcfg 0 cancelTk
- IO.println s!\"{Lean.toJson res}\"
+ let res ← " ++
+ (if command.supportsParallelConfig
+ then command.exportedName ++ " pcfg 0 cancelTk"
+ else command.exportedName ++ " 0 cancelTk") ++ "
+ IO.println s!\"{res}\"
flushStdoutAndStderr
IO.Process.forceExit 0
"
/-- Create the temp build folder with all necessary files.
-Returns the absolute path to the build folder. -/
-def createBuildFolder (sourceFile : String) (modelSource : String) (specNamespace : String) : IO System.FilePath := do
+Returns the absolute path to the build folder. Generated inputs are overwritten
+on each call while preserving the Lake build cache in the folder. -/
+def createBuildFolder (sourceFile : String) (modelSource : String) (specNamespace : String)
+ (command : CompiledCommandSpec) (commandId : String) : IO System.FilePath := do
let veilPath ← IO.currentDir
- let buildFolder ← generateBuildFolderName sourceFile
- -- Create the build folder
+ let buildFolder ← generateBuildFolderName sourceFile command commandId
IO.FS.createDirAll buildFolder
-- Write the lakefile
IO.FS.writeFile (buildFolder / "lakefile.lean") lakefileTemplate
-- Write the model source (renamed to Model.lean)
IO.FS.writeFile (buildFolder / "Model.lean") modelSource
-- Write the ModelCheckerMain.lean
- IO.FS.writeFile (buildFolder / "ModelCheckerMain.lean") (modelCheckerMainTemplate specNamespace)
+ IO.FS.writeFile (buildFolder / "ModelCheckerMain.lean") (modelCheckerMainTemplate specNamespace command)
-- Create a minimal lean-toolchain file (copy from parent)
let toolchainPath := veilPath / "lean-toolchain"
if ← toolchainPath.pathExists then
@@ -155,12 +188,6 @@ def createBuildFolder (sourceFile : String) (modelSource : String) (specNamespac
IO.FS.writeFile (buildFolder / "lean-toolchain") toolchain
return buildFolder
-/-- Update elapsed time status for a progress instance. -/
-def updateElapsedTimeStatus (instanceId : Nat) (statusPrefix : String) : IO Unit := do
- if let some refs ← ModelChecker.Concrete.getProgressRefs instanceId then
- let elapsed := ModelChecker.formatElapsedTime (← refs.progressRef.get).elapsedMs
- ModelChecker.Concrete.updateStatus instanceId s!"{statusPrefix} ({elapsed})"
-
/-- Result of running a compilation process. -/
structure ProcessResult where
exitCode : UInt32
@@ -169,41 +196,11 @@ structure ProcessResult where
interrupted : Bool := false
deriving Inhabited
-/-- Run a process with status updates, checking if compilation is still current or cancelled.
- Returns the exit code, stdout, stderr, and whether it was interrupted. -/
-def runProcessWithStatus (sourceFile : String) (cfg : IO.Process.SpawnArgs)
- (instanceId : Nat) (statusPrefix : String) (cancelToken : IO.CancelToken) : IO ProcessResult := do
- let proc ← IO.Process.spawn { cfg with stdin := .piped, stdout := .piped, stderr := .piped }
- -- Start reading stdout/stderr in background tasks to avoid blocking
- let stdoutTask ← IO.asTask (prio := .dedicated) proc.stdout.readToEnd
- let stderrTask ← IO.asTask (prio := .dedicated) proc.stderr.readToEnd
- let waitTask ← IO.asTask (prio := .dedicated) proc.wait
- let mut interrupted := false
- while !(← IO.hasFinished waitTask) do
- -- Check for explicit cancellation request
- if ← cancelToken.isSet then
- proc.kill
- interrupted := true
- break
- -- Check if this compilation is still current (not superseded)
- let current? ← stillCurrentCont sourceFile instanceId do
- updateElapsedTimeStatus instanceId statusPrefix
- unless current? do
- proc.kill
- interrupted := true
- break
- IO.sleep 100
- let stdout ← IO.ofExcept (← IO.wait stdoutTask)
- let stderr ← IO.ofExcept (← IO.wait stderrTask)
- match ← IO.wait waitTask with
- | .ok exitCode => return { exitCode, stdout, stderr, interrupted }
- | .error err => return { exitCode := 1, stdout, stderr := s!"{stderr}\nIO error: {err}", interrupted }
-
-/-- Run a process with callbacks for status updates and line-by-line output capture.
- - `statusCallback` is called periodically (every 500ms) with the elapsed time in ms.
- - `lineCallback` is called for each line of output (content, isError, elapsedMs).
- This variant does not check for cancellation - it runs to completion. -/
-def runProcessWithStatusCallback (cfg : IO.Process.SpawnArgs)
+/-- Run a process with callbacks for status updates and line-by-line output capture,
+checking both explicit cancellation and whether this compilation is still current. -/
+def runProcessWithStatusCallback (sourceFile : String) (command : CompiledCommandSpec) (commandId : String)
+ (cfg : IO.Process.SpawnArgs)
+ (instanceId : Nat) (cancelToken : IO.CancelToken)
(statusCallback : Nat → IO Unit)
(lineCallback : String → Bool → Nat → IO Unit := fun _ _ _ => pure ())
: IO ProcessResult := do
@@ -221,14 +218,24 @@ def runProcessWithStatusCallback (cfg : IO.Process.SpawnArgs)
let stdoutTask ← IO.asTask (prio := .dedicated) (readLines proc.stdout stdoutAccum false)
let stderrTask ← IO.asTask (prio := .dedicated) (readLines proc.stderr stderrAccum true)
let waitTask ← IO.asTask (prio := .dedicated) proc.wait
+ let mut interrupted := false
while !(← IO.hasFinished waitTask) do
- statusCallback ((← IO.monoMsNow) - startTime)
+ if ← cancelToken.isSet then
+ proc.kill
+ interrupted := true
+ break
+ let current? ← stillCurrentCont sourceFile command commandId instanceId do
+ statusCallback ((← IO.monoMsNow) - startTime)
+ unless current? do
+ proc.kill
+ interrupted := true
+ break
IO.sleep 500
let _ ← IO.wait stdoutTask
let _ ← IO.wait stderrTask
match ← IO.wait waitTask with
- | .ok exitCode => return { exitCode, stdout := ← stdoutAccum.get, stderr := ← stderrAccum.get, interrupted := false }
- | .error err => return { exitCode := 1, stdout := ← stdoutAccum.get, stderr := s!"{← stderrAccum.get}\nIO error: {err}", interrupted := false }
+ | .ok exitCode => return { exitCode, stdout := ← stdoutAccum.get, stderr := ← stderrAccum.get, interrupted }
+ | .error err => return { exitCode := 1, stdout := ← stdoutAccum.get, stderr := s!"{← stderrAccum.get}\nIO error: {err}", interrupted }
-- /-- Clean up all build folders older than the specified age (in milliseconds). -/
-- def cleanupOldBuildFolders (maxAgeMs : Nat := 24 * 60 * 60 * 1000) : IO Nat := do
diff --git a/VeilTest/Regression/CompilationRegistryKey.lean b/VeilTest/Regression/CompilationRegistryKey.lean
new file mode 100644
index 00000000..92c173a2
--- /dev/null
+++ b/VeilTest/Regression/CompilationRegistryKey.lean
@@ -0,0 +1,57 @@
+import Veil
+
+open Veil.ModelChecker.Compilation
+
+#eval do
+ let sourceFile := "/tmp/compilation-registry-key.lean"
+ let modelCheckCommand : CompiledCommandSpec := {
+ exportedName := "modelCheckerResult"
+ supportsParallelConfig := true
+ }
+ let simulateCommand : CompiledCommandSpec := {
+ exportedName := "simulateResult"
+ }
+ let modelCheckFolderA ← generateBuildFolderName sourceFile modelCheckCommand "model-check-a"
+ let modelCheckFolderB ← generateBuildFolderName sourceFile modelCheckCommand "model-check-b"
+ let simulateFolderA ← generateBuildFolderName sourceFile simulateCommand "simulate-a"
+ assert! (toString modelCheckFolderA == toString modelCheckFolderB)
+ assert! (toString modelCheckFolderA != toString simulateFolderA)
+ let modelCheckBuildDirA := System.FilePath.mk "build/model-check-a"
+ let modelCheckBuildDirB := System.FilePath.mk "build/model-check-b"
+ let simulateBuildDirA := System.FilePath.mk "build/simulate-a"
+ let simulateBuildDirB := System.FilePath.mk "build/simulate-b"
+ let simulateBuildDirC := System.FilePath.mk "build/simulate-c"
+ markRegistryInProgress sourceFile modelCheckCommand "model-check-a" 1 modelCheckBuildDirA
+ markRegistryInProgress sourceFile modelCheckCommand "model-check-b" 2 modelCheckBuildDirB
+ markRegistryInProgress sourceFile simulateCommand "simulate-a" 3 simulateBuildDirA
+ markRegistryInProgress sourceFile simulateCommand "simulate-b" 4 simulateBuildDirB
+ markRegistryInProgress sourceFile simulateCommand "simulate-c" 5 simulateBuildDirC
+ assert! (← stillCurrentCont sourceFile modelCheckCommand "model-check-a" 1 (pure ()))
+ assert! (← stillCurrentCont sourceFile modelCheckCommand "model-check-b" 2 (pure ()))
+ assert! (← stillCurrentCont sourceFile simulateCommand "simulate-a" 3 (pure ()))
+ assert! (← stillCurrentCont sourceFile simulateCommand "simulate-b" 4 (pure ()))
+ assert! (← stillCurrentCont sourceFile simulateCommand "simulate-c" 5 (pure ()))
+ markRegistryFinished sourceFile modelCheckCommand "model-check-a" modelCheckBuildDirA
+ markRegistryFinished sourceFile modelCheckCommand "model-check-b" modelCheckBuildDirB
+ markRegistryFinished sourceFile simulateCommand "simulate-a" simulateBuildDirA
+ markRegistryFinished sourceFile simulateCommand "simulate-b" simulateBuildDirB
+ markRegistryFinished sourceFile simulateCommand "simulate-c" simulateBuildDirC
+
+#eval do
+ let sourceFile := "/tmp/compilation-build-folder-cache.lean"
+ let command : CompiledCommandSpec := {
+ exportedName := "simulateResult"
+ }
+ let firstSource := "namespace CacheFirst\nend CacheFirst\n"
+ let secondSource := "namespace CacheSecond\nend CacheSecond\n"
+ let firstFolder ← createBuildFolder sourceFile firstSource "CacheFirst" command "simulate-cache"
+ let cacheDir := firstFolder / ".lake" / "build"
+ IO.FS.createDirAll cacheDir
+ let cacheSentinel := cacheDir / "cache-sentinel"
+ IO.FS.writeFile cacheSentinel "cached"
+ let secondFolder ← createBuildFolder sourceFile secondSource "CacheSecond" command "simulate-cache"
+ assert! (toString firstFolder == toString secondFolder)
+ assert! (← cacheSentinel.pathExists)
+ assert! ((← IO.FS.readFile (secondFolder / "Model.lean")) == secondSource)
+ assert! ((← IO.FS.readFile (secondFolder / "ModelCheckerMain.lean")) == modelCheckerMainTemplate "CacheSecond" command)
+ assert! ((← IO.FS.readFile (secondFolder / "lakefile.lean")) == lakefileTemplate)
diff --git a/VeilTest/Regression/MultipleSimulate.lean b/VeilTest/Regression/MultipleSimulate.lean
new file mode 100644
index 00000000..e32fd814
--- /dev/null
+++ b/VeilTest/Regression/MultipleSimulate.lean
@@ -0,0 +1,28 @@
+import Veil
+
+veil module MultipleSimulate
+
+type node
+
+relation flag : node -> Bool
+
+after_init {
+ flag N := false
+}
+
+action set_flag (n : node) {
+ flag n := true
+}
+
+invariant [bounded] ∀ n, flag n -> flag n
+
+#guard_msgs(drop warning) in
+#gen_spec
+
+#guard_msgs(drop info) in
+#simulate interpreted { node := Fin 2 } {} (seed := 1) (maxTraces := 1) (maxSteps := 1)
+
+#guard_msgs(drop info) in
+#simulate interpreted { node := Fin 2 } {} (seed := 2) (maxTraces := 1) (maxSteps := 1)
+
+end MultipleSimulate
diff --git a/VeilTest/Regression/SimulateAssertionFailure.lean b/VeilTest/Regression/SimulateAssertionFailure.lean
new file mode 100644
index 00000000..baeb7bba
--- /dev/null
+++ b/VeilTest/Regression/SimulateAssertionFailure.lean
@@ -0,0 +1,39 @@
+import Veil
+
+set_option linter.unusedVariables false
+
+veil module SimulateAssertionFailure
+
+type node
+
+relation pending : node -> node -> Bool
+
+#gen_state
+
+after_init {
+ pending M N := false
+}
+
+action send (n next : node) {
+ assert false
+ pending n next := true
+}
+
+invariant true
+
+/-- error: This assertion might fail when called from send -/
+#guard_msgs in
+#gen_spec
+
+/--
+error: ❌ Violation: assertion_failure
+ State 0 (via init):
+ pending = []
+ State 1 (via send(n=0, next=0)):
+ pending = []
+Seed: 1
+-/
+#guard_msgs in
+#simulate interpreted { node := Fin 2 } {} (seed := 1) (maxTraces := 1) (maxSteps := 1)
+
+end SimulateAssertionFailure
diff --git a/VeilTest/Regression/SimulateAssumptions.lean b/VeilTest/Regression/SimulateAssumptions.lean
new file mode 100644
index 00000000..f27a36d4
--- /dev/null
+++ b/VeilTest/Regression/SimulateAssumptions.lean
@@ -0,0 +1,119 @@
+import Veil
+
+set_option linter.unusedVariables false
+
+veil module SimulateAssumptionsTest
+
+type node
+
+immutable relation leader : node → Bool
+relation flag : node → Bool
+
+#gen_state
+
+assumption ∀ (n1 n2 : node), leader n1 ∧ leader n2 → n1 = n2
+
+after_init {
+ flag N := false
+}
+
+action do_something (n : node) {
+ require leader n
+ flag n := true
+}
+
+invariant true
+
+#gen_spec
+
+/--
+info: ✅ No violation in 1 traces
+Seed: 1
+-/
+#guard_msgs in
+#simulate interpreted { node := Fin 3 } { leader := fun n => n == (0 : Fin 3) }
+ (seed := 1) (maxTraces := 1) (maxSteps := 1)
+ assumptions_hold_by native_decide
+
+/--
+error: Tactic `native_decide` evaluated that the proposition
+ assumption_0 { leader := fun n => n == 0 || n == 1 }
+is false
+---
+info: ✅ No violation in 1 traces
+Seed: 1
+-/
+#guard_msgs in
+#simulate interpreted { node := Fin 3 } { leader := fun n => n == (0 : Fin 3) || n == (1 : Fin 3) }
+ (seed := 1) (maxTraces := 1) (maxSteps := 1)
+ assumptions_hold_by native_decide
+
+/--
+info: ✅ No violation in 1 traces
+Seed: 1
+-/
+#guard_msgs in
+#simulate interpreted { node := Fin 3 } { leader := fun n => n == (0 : Fin 3) || n == (1 : Fin 3) }
+ (seed := 1) (maxTraces := 1) (maxSteps := 1)
+
+/--
+info: ✅ No violation in 1 traces
+Seed: 1
+-/
+#guard_msgs in
+#simulate interpreted { node := Fin 3 } { leader := fun n => n == (0 : Fin 3) }
+ (seed := 1) (maxTraces := 1) (maxSteps := 1)
+ assumptions_hold_by decide
+
+#guard_msgs(drop info, drop warning) in
+#simulate interpreted { node := Fin 3 } { leader := fun n => n == (0 : Fin 3) }
+ (seed := 1) (maxTraces := 1) (maxSteps := 1)
+ assumptions_hold_by native_decide
+
+/--
+info: ✅ No violation in 1 traces
+Seed: 1
+-/
+#guard_msgs in
+#simulate interpreted { node := Fin 3 } { leader := fun n => n == (0 : Fin 3) }
+ (seed := 1) (maxTraces := 1) (maxSteps := 1)
+ assumptions_hold_by native_decide
+
+end SimulateAssumptionsTest
+
+veil module SimulateAssumptionsCustomProof
+
+type node
+
+immutable function weight : node → Nat
+
+relation active : node → Bool
+
+#gen_state
+
+assumption ∀ (n : node), 0 < weight n
+assumption ∀ (n1 n2 : node), weight n1 = weight n2 → n1 = n2
+
+after_init {
+ active N := false
+}
+
+action activate (n : node) {
+ active n := true
+}
+
+invariant true
+
+#gen_spec
+
+/--
+info: ✅ No violation in 1 traces
+Seed: 1
+-/
+#guard_msgs in
+#simulate interpreted { node := Fin 3 } { weight := fun (n : Fin 3) => n.val + 1 }
+ (seed := 1) (maxTraces := 1) (maxSteps := 1)
+ assumptions_hold_by
+ constructor <;> decide
+
+end SimulateAssumptionsCustomProof
diff --git a/VeilTest/Regression/SimulateCompiledSmoke.lean b/VeilTest/Regression/SimulateCompiledSmoke.lean
new file mode 100644
index 00000000..3d5c65b8
--- /dev/null
+++ b/VeilTest/Regression/SimulateCompiledSmoke.lean
@@ -0,0 +1,24 @@
+import Veil
+
+veil module SimulateCompiledSmoke
+
+individual flag : Bool
+
+#gen_state
+
+after_init {
+ flag := false
+}
+
+action set_flag {
+ flag := true
+}
+
+invariant [safe_flag] true
+
+#gen_spec
+
+#guard_msgs(drop info, drop warning) in
+#simulate compiled {} {} (seed := 1) (maxTraces := 1) (maxSteps := 1)
+
+end SimulateCompiledSmoke
diff --git a/VeilTest/Regression/SimulateConfigDefaults.lean b/VeilTest/Regression/SimulateConfigDefaults.lean
new file mode 100644
index 00000000..2e460444
--- /dev/null
+++ b/VeilTest/Regression/SimulateConfigDefaults.lean
@@ -0,0 +1,85 @@
+import Veil
+
+open Veil.ModelChecker.Simulation
+
+example :
+ Veil.resolveSimulateTraceBounds
+ { maxTraces := 10000, maxSteps := 100, seed := 0 }
+ true true 7 3 = (10000, 100) := rfl
+
+example :
+ Veil.resolveSimulateTraceBounds
+ { maxTraces := 10000, maxSteps := 100, seed := 0 }
+ false false 7 3 = (7, 3) := rfl
+
+example :
+ Veil.resolveSimulateTraceBounds
+ { maxTraces := 10000, maxSteps := 100, seed := 0 }
+ true false 7 3 = (10000, 3) := rfl
+
+example :
+ Veil.resolveSimulateTraceBounds
+ { maxTraces := 10000, maxSteps := 100, seed := 0 }
+ false true 7 3 = (7, 100) := rfl
+
+veil module SimulateConfigDefaults
+
+individual flag : Bool
+individual tripped : Bool
+
+#gen_state
+
+after_init {
+ flag := false
+ tripped := false
+}
+
+action set_flag {
+ require !flag
+ flag := true
+}
+
+action trip {
+ require flag
+ tripped := true
+}
+
+invariant [still_safe] ¬ tripped
+
+#gen_spec
+
+set_option veil.simulate.maxSteps 1 in
+/--
+error: ❌ Violation: safety_failure (violates: still_safe)
+ State 0 (via init):
+ flag = false
+ tripped = false
+ State 1 (via set_flag):
+ flag = true
+ tripped = false
+ State 2 (via trip):
+ flag = true
+ tripped = true
+Seed: 1
+-/
+#guard_msgs in
+#simulate interpreted {} {} (seed := 1) (maxTraces := 1) (maxSteps := 100)
+
+set_option veil.simulate.maxSteps 1 in
+/--
+error: ❌ Violation: safety_failure (violates: still_safe)
+ State 0 (via init):
+ flag = false
+ tripped = false
+ State 1 (via set_flag):
+ flag = true
+ tripped = false
+ State 2 (via trip):
+ flag = true
+ tripped = true
+Seed: 1
+-/
+#guard_msgs in
+#simulate interpreted {} {} (config := { maxTraces := 1, maxSteps := 100, seed := 1 })
+
+end SimulateConfigDefaults
diff --git a/VeilTest/Regression/SimulateDeadlock.lean b/VeilTest/Regression/SimulateDeadlock.lean
new file mode 100644
index 00000000..15d36852
--- /dev/null
+++ b/VeilTest/Regression/SimulateDeadlock.lean
@@ -0,0 +1,31 @@
+import Veil
+
+veil module SimulateDeadlock
+
+individual stuck : Bool
+
+#gen_state
+
+after_init {
+ stuck := true
+}
+
+invariant true
+termination false = true
+
+/--
+warning: you have not defined any actions for this specification; did you forget?
+-/
+#guard_msgs in
+#gen_spec
+
+/--
+error: ❌ Violation: deadlock
+ State 0 (via init):
+ stuck = true
+Seed: 1
+-/
+#guard_msgs in
+#simulate interpreted {} {} (seed := 1) (maxTraces := 1) (maxSteps := 1)
+
+end SimulateDeadlock
diff --git a/VeilTest/Regression/SimulateEmptyFilteredInitStates.lean b/VeilTest/Regression/SimulateEmptyFilteredInitStates.lean
new file mode 100644
index 00000000..0af484bc
--- /dev/null
+++ b/VeilTest/Regression/SimulateEmptyFilteredInitStates.lean
@@ -0,0 +1,26 @@
+import Veil
+
+veil module SimulateEmptyFilteredInitStates
+
+after_init {
+ pure ()
+}
+
+invariant true
+
+state_constraint [no_initial_states] False
+
+/--
+warning: you have not defined any actions for this specification; did you forget?
+-/
+#guard_msgs in
+#gen_spec
+
+/--
+info: ✅ No initial states available after applying state constraints
+Seed: 1
+-/
+#guard_msgs in
+#simulate interpreted {} {} (seed := 1) (maxTraces := 5) (maxSteps := 1)
+
+end SimulateEmptyFilteredInitStates
diff --git a/VeilTest/Regression/SimulateEmptySpec.lean b/VeilTest/Regression/SimulateEmptySpec.lean
new file mode 100644
index 00000000..f3958ec5
--- /dev/null
+++ b/VeilTest/Regression/SimulateEmptySpec.lean
@@ -0,0 +1,24 @@
+import Veil
+
+veil module SimulateEmptySpec
+
+after_init {
+ pure ()
+}
+
+invariant true
+
+/--
+warning: you have not defined any actions for this specification; did you forget?
+-/
+#guard_msgs in
+#gen_spec
+
+/--
+info: ✅ No violation in 1 traces
+Seed: 1
+-/
+#guard_msgs in
+#simulate interpreted { } {} (seed := 1) (maxTraces := 1) (maxSteps := 1)
+
+end SimulateEmptySpec
diff --git a/VeilTest/Regression/SimulateModes.lean b/VeilTest/Regression/SimulateModes.lean
new file mode 100644
index 00000000..bb0f3e38
--- /dev/null
+++ b/VeilTest/Regression/SimulateModes.lean
@@ -0,0 +1,51 @@
+import Veil
+
+veil module SimulateModes
+
+individual flag : Bool
+
+#gen_state
+
+after_init {
+ flag := false
+}
+
+action set_flag {
+ flag := true
+}
+
+invariant [safe_flag] true
+
+#gen_spec
+
+/--
+info: ✅ No violation in 1 traces
+Seed: 1
+-/
+#guard_msgs in
+#simulate interpreted {} {} (seed := 1) (maxTraces := 1) (maxSteps := 1)
+
+#guard_msgs(drop info, drop warning) in
+#simulate interpreted {} {} (seed := 1) (maxTraces := 1) (maxSteps := 1)
+
+set_option veil.simulate.maxTraces 1 in
+set_option veil.simulate.maxSteps 1 in
+#guard_msgs(drop info, drop warning) in
+#simulate interpreted {}
+
+/--
+info: ✅ No violation in 1 traces
+Seed: 1
+-/
+#guard_msgs in
+#simulate interpreted {} {} (seed := 1) (maxTraces := 1) (maxSteps := 1)
+
+set_option veil.simulate.maxTraces 2 in
+/--
+info: ✅ No violation in 1 traces
+Seed: 1
+-/
+#guard_msgs in
+#simulate interpreted {} {} (config := { maxTraces := 1, maxSteps := 1, seed := 1 })
+
+end SimulateModes
diff --git a/VeilTest/Regression/SimulateResultJson.lean b/VeilTest/Regression/SimulateResultJson.lean
new file mode 100644
index 00000000..8f9744d1
--- /dev/null
+++ b/VeilTest/Regression/SimulateResultJson.lean
@@ -0,0 +1,70 @@
+import Veil
+
+open Veil.ModelChecker
+open Veil.ModelChecker.Simulation
+
+/--
+info: {"depth":0,"elapsed_ms":0,"max_traces":3,"result":"no_violation_found","seed":1,"traces_run":3}
+-/
+#guard_msgs in
+#eval IO.println <| (Lean.toJson ({
+ result := none
+ tracesRun := 3
+ maxTraces := 3
+ elapsedMs := 0
+ seed := 1
+} : SimulateResult Unit Unit Unit)).compress
+
+/--
+info: {"depth":0,"elapsed_ms":0,"max_traces":3,"result":"no_violation_found","seed":1,"traces_run":3}
+-/
+#guard_msgs in
+#eval IO.println <| (SimulateResult.toDisplayJson ({
+ result := none
+ tracesRun := 3
+ maxTraces := 3
+ elapsedMs := 0
+ seed := 1
+} : SimulateResult Unit Unit Unit)).compress
+
+/--
+info: {"depth":0,"elapsed_ms":0,"max_traces":3,"result":"no_violation_found","seed":1,"termination_reason":{"kind":"no_initial_states"},"traces_run":0}
+-/
+#guard_msgs in
+#eval IO.println <| (Lean.toJson ({
+ result := none
+ tracesRun := 0
+ maxTraces := 3
+ elapsedMs := 0
+ seed := 1
+ terminationReason := some .noInitialStates
+} : SimulateResult Unit Unit Unit)).compress
+
+/--
+info: {"depth":0,"elapsed_ms":12,"max_traces":10,"result":"cancelled","seed":7,"traces_run":5}
+-/
+#guard_msgs in
+#eval IO.println <| (Lean.toJson ({
+ result := some .cancelled
+ tracesRun := 5
+ maxTraces := 10
+ elapsedMs := 12
+ seed := 7
+} : SimulateResult Unit Unit Unit)).compress
+
+/--
+info: {"depth":2,"elapsed_ms":0,"max_traces":3,"result":"found_violation","seed":1,"state_fingerprint":null,"trace":{"states":[{"fields":"()","index":0,"transition":"after_init"},{"fields":"()","index":1,"transition":"()"},{"failing":true,"fields":"()","index":2,"transition":"()"}],"theory":"()"},"traces_run":1,"violation":{"exception_id":5,"kind":"assertion_failure"}}
+-/
+#guard_msgs in
+#eval IO.println <| (Lean.toJson ({
+ result := some (.foundViolation (.assertionFailure 5) ({
+ theory := ()
+ initialState := ()
+ steps := #[{ transitionLabel := (), nextState := () }]
+ failingStep := some { transitionLabel := (), nextState := () }
+ } : Trace Unit Unit Unit))
+ tracesRun := 1
+ maxTraces := 3
+ elapsedMs := 0
+ seed := 1
+} : SimulateResult Unit Unit Unit)).compress
diff --git a/VeilTest/Regression/SimulateViolationModes.lean b/VeilTest/Regression/SimulateViolationModes.lean
new file mode 100644
index 00000000..2cdae613
--- /dev/null
+++ b/VeilTest/Regression/SimulateViolationModes.lean
@@ -0,0 +1,47 @@
+import Veil
+
+veil module SimulateViolationModes
+
+individual flag : Bool
+
+#gen_state
+
+after_init {
+ flag := false
+}
+
+action set_flag {
+ flag := true
+}
+
+invariant [safe_flag] ¬ flag
+
+#gen_spec
+
+/--
+error: ❌ Violation: safety_failure (violates: safe_flag)
+ State 0 (via init):
+ flag = false
+ State 1 (via set_flag):
+ flag = true
+Seed: 1
+-/
+#guard_msgs in
+#simulate interpreted {} {} (seed := 1) (maxTraces := 1) (maxSteps := 1)
+
+#guard_msgs(drop info, drop warning) in
+set_option veil.violationIsError false in
+#simulate interpreted {} {} (seed := 1) (maxTraces := 1) (maxSteps := 1)
+
+/--
+error: ❌ Violation: safety_failure (violates: safe_flag)
+ State 0 (via init):
+ flag = false
+ State 1 (via set_flag):
+ flag = true
+Seed: 1
+-/
+#guard_msgs in
+#simulate interpreted {} {} (seed := 1) (maxTraces := 1) (maxSteps := 1)
+
+end SimulateViolationModes
diff --git a/VeilTest/RequiresGenSpec.lean b/VeilTest/RequiresGenSpec.lean
index cb4fcf73..31d448bc 100644
--- a/VeilTest/RequiresGenSpec.lean
+++ b/VeilTest/RequiresGenSpec.lean
@@ -3,8 +3,9 @@ import Veil
/-!
# Tests that commands requiring finalized spec throw errors without #gen_spec
-These tests verify that `#check_invariants`, `#model_check`, and `sat trace`/`unsat trace`
-all throw an appropriate error message when called before `#gen_spec`.
+These tests verify that `#check_invariants`, `#model_check`, `#simulate`, and
+`sat trace`/`unsat trace` all throw an appropriate error message when called
+before `#gen_spec`.
-/
veil module TestCheckInvariants
@@ -49,6 +50,25 @@ invariant ¬ flag
end TestModelCheck
+veil module TestSimulate
+
+individual flag : Bool
+
+#gen_state
+
+after_init { flag := false }
+
+action set_flag { flag := true }
+
+invariant ¬ flag
+
+/-- error: The specification of module TestSimulate has not been finalized. Please call #gen_spec first! -/
+#guard_msgs in
+#simulate interpreted { }
+
+end TestSimulate
+
+
veil module TestSatTrace
individual flag : Bool
diff --git a/VeilTest/UninterpretedParameter.lean b/VeilTest/UninterpretedParameter.lean
index 417a3d76..1ab4e343 100644
--- a/VeilTest/UninterpretedParameter.lean
+++ b/VeilTest/UninterpretedParameter.lean
@@ -34,4 +34,7 @@ invariant [bounded] ∀ (x : node), counter x ≤ n
#model_check interpreted { node := Fin 2, n := 1, color := Fin 2, m := ⟨1, by decide⟩ } {}
+#guard_msgs(drop info) in
+#simulate interpreted { node := Fin 2, n := 1, color := Fin 2, m := ⟨1, by decide⟩ } {} (seed := 1) (maxTraces := 1) (maxSteps := 1)
+
end TestParameter
diff --git a/widget/src/traceDisplay.tsx b/widget/src/traceDisplay.tsx
index 32d4a6f1..c7856fe1 100644
--- a/widget/src/traceDisplay.tsx
+++ b/widget/src/traceDisplay.tsx
@@ -45,13 +45,17 @@ interface Violation {
}
interface EarlyTerminationCondition {
- kind: "found_violating_state" | "deadlock_occurred" | "reached_depth_bound";
+ kind: "found_violating_state" | "deadlock_occurred" | "reached_depth_bound" | "reached_trace_limit";
depth?: number;
+ traces_run?: number;
+ max_traces?: number;
}
interface TerminationReason {
- kind: "explored_all_reachable_states" | "early_termination";
+ kind: "explored_all_reachable_states" | "early_termination" | "reached_trace_limit" | "no_initial_states";
condition?: EarlyTerminationCondition;
+ traces_run?: number;
+ max_traces?: number;
}
interface TraceData {
@@ -71,15 +75,22 @@ type ModelCheckingResult =
result: "found_violation";
violation: Violation;
trace: TraceData | null;
+ seed?: number;
}
| {
result: "no_violation_found";
- explored_states: number;
- termination_reason: TerminationReason;
+ explored_states?: number;
+ termination_reason?: TerminationReason;
+ traces_run?: number;
+ max_traces?: number;
trace?: TraceData | null;
+ seed?: number;
}
| {
result: "cancelled";
+ traces_run?: number;
+ max_traces?: number;
+ seed?: number;
}
| {
// Trace-only data without a result (for displaying execution traces)
@@ -297,15 +308,30 @@ const ResultHeader: React.FC<{
violation?: Violation;
exploredStates?: number;
terminationReason?: TerminationReason;
-}> = ({ resultType, violation, exploredStates, terminationReason }) => {
+ tracesRun?: number;
+ maxTraces?: number;
+ seed?: number;
+}> = ({ resultType, violation, exploredStates, terminationReason, tracesRun, maxTraces, seed }) => {
+ const seedDetails = seed !== undefined ? (
+
+ Seed: {seed}
+
+ ) : null;
+
if (resultType === "cancelled") {
+ const details = tracesRun !== undefined && maxTraces !== undefined
+ ? `Checked ${tracesRun}/${maxTraces} traces before cancellation`
+ : tracesRun !== undefined
+ ? `Checked ${tracesRun} traces before cancellation`
+ : 'Run was cancelled before completion';
return (
⊘
Cancelled
- Model checking was cancelled before completion
+ {details}
+ {seedDetails}
);
}
@@ -344,6 +370,7 @@ const ResultHeader: React.FC<{
Location: {violation.assertion_info.moduleName}.{violation.assertion_info.procedureName} (line {violation.assertion_info.line}, column {violation.assertion_info.column})
)}
+ {seedDetails}
);
}
@@ -353,10 +380,32 @@ const ResultHeader: React.FC<{
const countSuffix = count !== undefined ? ` (explored ${count} states)` : '';
const countText = count !== undefined ? `Explored ${count} states` : null;
- if (!reason) return countText;
+ if (!reason) {
+ if (tracesRun !== undefined && maxTraces !== undefined) {
+ return `Checked ${tracesRun}/${maxTraces} traces`;
+ }
+ if (tracesRun !== undefined) {
+ return `Checked ${tracesRun} traces`;
+ }
+ return countText;
+ }
if (reason.kind === "explored_all_reachable_states") {
return count !== undefined ? `Explored all reachable states (${count})` : `Explored all reachable states`;
}
+ if (reason.kind === "reached_trace_limit") {
+ const tracesRun = reason.traces_run;
+ const maxTraces = reason.max_traces;
+ if (tracesRun !== undefined && maxTraces !== undefined) {
+ return `Checked ${tracesRun}/${maxTraces} traces`;
+ }
+ if (tracesRun !== undefined) {
+ return `Checked ${tracesRun} traces`;
+ }
+ return `Checked configured trace budget`;
+ }
+ if (reason.kind === "no_initial_states") {
+ return `No initial states available after applying state constraints`;
+ }
if (reason.kind === "early_termination" && reason.condition) {
switch (reason.condition.kind) {
case "found_violating_state":
@@ -365,6 +414,17 @@ const ResultHeader: React.FC<{
return `Stopped: deadlock occurred${countSuffix}`;
case "reached_depth_bound":
return `Reached depth bound ${reason.condition.depth}${countSuffix}`;
+ case "reached_trace_limit":
+ if (tracesRun !== undefined && maxTraces !== undefined) {
+ return `Checked ${tracesRun}/${maxTraces} traces`;
+ }
+ if (reason.condition.traces_run !== undefined && reason.condition.max_traces !== undefined) {
+ return `Checked ${reason.condition.traces_run}/${reason.condition.max_traces} traces`;
+ }
+ if (reason.condition.traces_run !== undefined) {
+ return `Checked ${reason.condition.traces_run} traces`;
+ }
+ return `Checked configured trace budget`;
default:
return `Early termination${countSuffix}`;
}
@@ -383,6 +443,7 @@ const ResultHeader: React.FC<{
{terminationText}
)}
+ {seedDetails}
);
};
@@ -784,17 +845,26 @@ const ModelCheckerView: React.FC = ({
{'result' in result && (
<>
{result.result === "cancelled" ? (
-
+
) : result.result === "no_violation_found" ? (
) : (
)}
>