Skip to content

Commit 543cce4

Browse files
committed
share helpers between #model_check and #simulate, add widget support
1 parent df4f8e5 commit 543cce4

1 file changed

Lines changed: 75 additions & 108 deletions

File tree

Veil/Frontend/DSL/Module/Elaborators.lean

Lines changed: 75 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -550,6 +550,66 @@ def getModelCheckingMode (modeStx : Syntax) : ModelCheckingMode :=
550550
| `(modelCheckMode| compiled) => .compiled
551551
| _ => .default
552552

553+
554+
/-- Get all action label names for never-enabled action warnings. -/
555+
private def getActionLabelNames (mod : Module) : CommandElabM (List String) := do
556+
let labelTypeName ← resolveGlobalConstNoOverload labelType
557+
return mod.actions.map (fun a => s!"{labelTypeName}.{a.name}") |>.toList
558+
559+
private def warnAboutTransitions (mod : Module) : CommandElabM Unit := do
560+
let transitions := mod.procedures.filter (·.info.isTransition)
561+
if transitions.isEmpty then return
562+
let names := ", ".intercalate (transitions.map (·.info.name.toString) |>.toList)
563+
logWarning m!"Explicit state model checking of transitions is SLOW!\n\n\
564+
The current implementation enumerates all possible states and filters those satisfying \
565+
the transition relation. Your specification has {transitions.size} \
566+
transition{if transitions.size > 1 then "s" else ""}: {names}\n\n\
567+
Consider encoding transitions as imperative actions where possible."
568+
569+
private def resolveTheoryTerm (cmdName : String) (theoryTermOpt : Option Term)
570+
(mod : Module) (instTerm : Term) : CommandElabM Term := do
571+
match theoryTermOpt with
572+
| some t => pure t
573+
| none =>
574+
unless mod.immutableComponents.isEmpty do
575+
let fieldStrs := mod.immutableComponents.map (fun c => s!"{c.name} := ...")
576+
let theoryExample := "{ " ++ ", ".intercalate fieldStrs.toList ++ " }"
577+
throwError "This module has immutable fields, so you must specify the theory instantiation:\n\
578+
{cmdName} {instTerm} {theoryExample}"
579+
`({})
580+
581+
/-- Prepend `name` with `mod.name`. -/
582+
private def mkIdentWithModName' (mod : Module) (name : Name) : Ident :=
583+
Lean.mkIdent (mod.name ++ name)
584+
585+
/-- Build search parameters for model checking / simulation. -/
586+
private def buildSearchParameters (mod : Module) (config : ModelCheckerConfig) : CommandElabM Term := do
587+
let mkProp (sa : StateAssertion) : CommandElabM Term :=
588+
`($(mkIdent ``Veil.ModelChecker.SafetyProperty.mk)
589+
($(mkIdent `name) := $(quote sa.name))
590+
($(mkIdent `property) := fun $(mkIdent `th) $(mkIdent `st) => $(mkIdentWithModName' mod sa.name) $(mkIdent `th) $(mkIdent `st)))
591+
let safetyList ← `([$((← mod.invariants.mapM mkProp)),*])
592+
let terminatingProp ← match mod.terminations[0]? with
593+
| some t => mkProp t
594+
| none => `($(mkIdent `default))
595+
let earlyTermConds ← do
596+
let base ← `([$(mkIdent ``Veil.ModelChecker.EarlyTerminationCondition.foundViolatingState),
597+
$(mkIdent ``Veil.ModelChecker.EarlyTerminationCondition.assertionFailed),
598+
$(mkIdent ``Veil.ModelChecker.EarlyTerminationCondition.deadlockOccurred)])
599+
if config.maxDepth > 0 then `($base ++ [$(mkIdent ``Veil.ModelChecker.EarlyTerminationCondition.reachedDepthBound) $(quote config.maxDepth)])
600+
else pure base
601+
`({ $(mkIdent `invariants):ident := $safetyList, $(mkIdent `terminating):ident := $terminatingProp,
602+
$(mkIdent `earlyTerminationConditions):ident := $earlyTermConds })
603+
604+
/-- Display a TraceDisplayViewer widget with the given result JSON. -/
605+
private def displayResultWidget (stx : Syntax) (resultTerm : Term) : CommandElabM Unit := do
606+
let widgetExpr ← `(open ProofWidgets.Jsx in
607+
<ProofWidgets.TraceDisplayViewer result={$resultTerm} layout={"vertical"} />)
608+
let html ← ← liftTermElabM <| ProofWidgets.HtmlCommand.evalCommandMHtml <| ← ``(ProofWidgets.HtmlEval.eval $widgetExpr)
609+
liftCoreM <| Widget.savePanelWidgetInfo
610+
(hash ProofWidgets.HtmlDisplayPanel.javascript)
611+
(return json% { html: $(← Server.rpcEncode html) }) stx
612+
553613
@[command_elab Veil.modelCheck]
554614
def elabModelCheck : CommandElab := fun stx => do
555615
-- Use dynamic trace class name for detailed profiling
@@ -561,19 +621,6 @@ def elabModelCheck : CommandElab := fun stx => do
561621
let cfg := stx[4]
562622
elabModelCheckCore stx mode instTerm theoryTermOpt cfg
563623
where
564-
/-- Get the theory term, defaulting to `{}` if not provided and there are no theory fields.
565-
Throws a helpful error if theory fields exist but no term was provided. -/
566-
getTheoryTerm (theoryTermOpt : Option Term) (mod : Module) (instTerm : Term) : CommandElabM Term := do
567-
match theoryTermOpt with
568-
| some t => pure t
569-
| none =>
570-
unless mod.immutableComponents.isEmpty do
571-
let fieldStrs := mod.immutableComponents.map (fun c => s!"{c.name} := ...")
572-
let theoryExample := "{ " ++ ", ".intercalate fieldStrs.toList ++ " }"
573-
throwError "This module has immutable fields, so you must specify the theory instantiation:\n\
574-
#model_check {instTerm} {theoryExample}"
575-
`({})
576-
577624
/-- Generate the model source for compilation:
578625
1. Insert `set_option veil.__modelCheckCompileMode true` after imports
579626
2. Keep everything up to the point where the spec was finalized
@@ -599,65 +646,22 @@ where
599646
let modelCheckCmd := String.Pos.Raw.extract src modelCheckStart modelCheckEnd
600647
return beforeImports ++ compileModePreamble ++ afterImportsToSpecFinalized ++ "\n" ++ modelCheckCmd ++ "\n"
601648

602-
/-- Prepend `name` with `mod.name`. Useful when expressions are printed out for debugging. -/
603-
mkIdentWithModName (mod : Module) (name : Name) : Ident :=
604-
Lean.mkIdent (mod.name ++ name)
605-
606-
/-- Display a TraceDisplayViewer widget with the given result term. -/
607-
displayResultWidget (stx : Syntax) (resultTerm : Term) : CommandElabM Unit := do
608-
let widgetExpr ← `(open ProofWidgets.Jsx in
609-
<ProofWidgets.TraceDisplayViewer result={$resultTerm} layout={"vertical"} />)
610-
let html ← ← liftTermElabM <| ProofWidgets.HtmlCommand.evalCommandMHtml <| ← ``(ProofWidgets.HtmlEval.eval $widgetExpr)
611-
liftCoreM <| Widget.savePanelWidgetInfo
612-
(hash ProofWidgets.HtmlDisplayPanel.javascript)
613-
(return json% { html: $(← Server.rpcEncode html) }) stx
614-
615-
mkSearchParameters (mod : Module) (config : ModelCheckerConfig) : CommandElabM Term := do
616-
-- Build SafetyProperty.mk syntax for a StateAssertion
617-
let mkProp (sa : StateAssertion) : CommandElabM Term :=
618-
`($(mkIdent ``Veil.ModelChecker.SafetyProperty.mk)
619-
($(mkIdent `name) := $(quote sa.name))
620-
($(mkIdent `property) := fun $(mkIdent `th) $(mkIdent `st) => $(mkIdentWithModName mod sa.name) $(mkIdent `th) $(mkIdent `st)))
621-
let safetyList ← `([$((← mod.invariants.mapM mkProp)),*])
622-
let terminatingProp ← match mod.terminations[0]? with
623-
| some t => mkProp t
624-
| none => `($(mkIdent `default))
625-
let earlyTermConds ← do
626-
let base ← `([$(mkIdent ``Veil.ModelChecker.EarlyTerminationCondition.foundViolatingState),
627-
$(mkIdent ``Veil.ModelChecker.EarlyTerminationCondition.assertionFailed),
628-
$(mkIdent ``Veil.ModelChecker.EarlyTerminationCondition.deadlockOccurred)])
629-
if config.maxDepth > 0 then `($base ++ [$(mkIdent ``Veil.ModelChecker.EarlyTerminationCondition.reachedDepthBound) $(quote config.maxDepth)])
630-
else pure base
631-
`({ $(mkIdent `invariants):ident := $safetyList, $(mkIdent `terminating):ident := $terminatingProp,
632-
$(mkIdent `earlyTerminationConditions):ident := $earlyTermConds })
633-
634649
/-- Build the core model checker call syntax (without parallel config). -/
635650
mkModelCheckerCall (mod : Module) (config : ModelCheckerConfig)
636651
(instTerm theoryTerm : Term) : CommandElabM Term := do
637652
let inst := mkVeilImplementationDetailIdent `inst
638653
let th := mkVeilImplementationDetailIdent `th
639654
let instSortArgs ← (← mod.sortIdents).mapM fun sortIdent => `($inst.$(sortIdent))
640-
let sp ← mkSearchParameters mod config
655+
let sp ← buildSearchParameters mod config
641656
-- Model checker call with type annotation to help inference
642657
-- Note: findReachable takes parallelCfg, progressInstanceId, and cancelToken as the last three args
643658
`((let $inst : $instantiationType := $instTerm
644659
let $th : $theoryIdent $instSortArgs* := $theoryTerm
645660
$(mkIdent ``Veil.ModelChecker.Concrete.findReachable)
646661
($(mkIdent `inhabσ) := $instInhabitedStateFieldConcreteType $instSortArgs*)
647-
($(mkIdentWithModName mod `enumerableTransitionSystem) $instSortArgs* $th)
662+
($(mkIdentWithModName' mod `enumerableTransitionSystem) $instSortArgs* $th)
648663
$sp : _ → _ → _ → IO _))
649664

650-
/-- Warn if the module contains transitions (which are slow to model check). -/
651-
warnAboutTransitions (mod : Module) : CommandElabM Unit := do
652-
let transitions := mod.procedures.filter (·.info.isTransition)
653-
if transitions.isEmpty then return
654-
let names := ", ".intercalate (transitions.map (·.info.name.toString) |>.toList)
655-
logWarning m!"Explicit state model checking of transitions is SLOW!\n\n\
656-
The current implementation enumerates all possible states and filters those satisfying \
657-
the transition relation. Your specification has {transitions.size} \
658-
transition{if transitions.size > 1 then "s" else ""}: {names}\n\n\
659-
Consider encoding transitions as imperative actions where possible."
660-
661665
/-- Create an error JSON object. -/
662666
errorJson (msg : String) : Json := Json.mkObj [("error", msg)]
663667

@@ -743,11 +747,6 @@ where
743747
Term.synthesizeSyntheticMVarsNoPostponing
744748
unsafe Meta.evalExpr (IO Lean.Json) (mkApp (mkConst ``IO) (mkConst ``Lean.Json)) (← instantiateMVars expr)
745749

746-
/-- Get all action label names for never-enabled action warnings. -/
747-
getActionLabelNames (mod : Module) : CommandElabM (List String) := do
748-
let labelTypeName ← resolveGlobalConstNoOverload labelType
749-
return mod.actions.map (fun a => s!"{labelTypeName}.{a.name}") |>.toList
750-
751750
/-- Log model checking result. -/
752751
logModelCheckResult (stx : Syntax) (resultJson : Json) : CommandElabM Unit := do
753752
let msg := TraceDisplay.formatModelCheckingResult resultJson
@@ -805,7 +804,7 @@ where
805804
let mod ← getCurrentModule (errMsg := "You cannot #model_check outside of a Veil module!")
806805
mod.throwIfSpecNotFinalized
807806

808-
let theoryTerm ← getTheoryTerm theoryTermOpt mod instTerm
807+
let theoryTerm ← resolveTheoryTerm "#model_check" theoryTermOpt mod instTerm
809808

810809
warnAboutTransitions mod
811810
let config ← elabModelCheckerConfig cfg
@@ -926,7 +925,7 @@ private def mkSimulatorCall (mod : Module) (instTerm theoryTerm : Term)
926925
`((let $inst : $instantiationType := $instTerm
927926
let $th : $theoryIdent $instSortArgs* := $theoryTerm
928927
$(mkIdent ``Veil.ModelChecker.Simulation.simulate)
929-
($(Lean.mkIdent (mod.name ++ `enumerableTransitionSystem)) $instSortArgs* $th)
928+
($(mkIdentWithModName' mod `enumerableTransitionSystem) $instSortArgs* $th)
930929
$sp $th $cfgTerm))
931930

932931
@[command_elab Veil.simulate]
@@ -936,14 +935,15 @@ def elabSimulate : CommandElab := fun stx => do
936935
let theoryTermOpt : Option Term := if stx[2].isNone then none else some ⟨stx[2][0]⟩
937936
let mod ← getCurrentModule (errMsg := "You cannot #simulate outside of a Veil module!")
938937
mod.throwIfSpecNotFinalized
939-
let theoryTerm ← getTheoryTerm theoryTermOpt mod instTerm
938+
let theoryTerm ← resolveTheoryTerm "#simulate" theoryTermOpt mod instTerm
939+
warnAboutTransitions mod
940940
let cfg0 ← elabSimulateConfig stx[3]
941941
let opts ← getOptions
942942
let maxTraces := if cfg0.maxTraces == 10000 then veil.simulate.maxTraces.get opts else cfg0.maxTraces
943943
let maxSteps := if cfg0.maxSteps == 100 then veil.simulate.maxSteps.get opts else cfg0.maxSteps
944944
let cfg : ModelChecker.Simulation.SimulateConfig := { cfg0 with maxTraces, maxSteps }
945945
let mcCfg : ModelCheckerConfig := { maxDepth := 0, sequential := false, parallelCfg := none }
946-
let sp ← mkSearchParameters mod mcCfg
946+
let sp ← buildSearchParameters mod mcCfg
947947
let callExpr ← mkSimulatorCall mod instTerm theoryTerm sp cfg
948948
let wrappedCallExpr ← `(Functor.map (fun r => Lean.Json.mkObj [
949949
("result", Lean.toJson (Veil.ModelChecker.Simulation.SimulateResult.result r)),
@@ -972,49 +972,16 @@ def elabSimulate : CommandElab := fun stx => do
972972
let stepsPerSec := if elapsedMs > 0 then totalSteps * 1000 / elapsedMs else 0
973973
let isViolation := resultJson.getObjValD "result" == Json.str "found_violation" ||
974974
resultJson.getObjValD "error" != .null
975+
-- Log simulation-specific summary
975976
let summary := if isViolation then
976977
s!"simulation: found violation at depth {depth} (trace #{tracesRun}, {elapsedMs}ms, seed := {seed}). A shorter violation may exist at depth < {depth}."
977978
else
978979
s!"simulation: no violation in {tracesRun} traces ({totalSteps} steps, {elapsedMs}ms, {stepsPerSec} steps/s, seed := {seed}). Not exhaustive -- use #model_check for full coverage."
979-
let details := TraceDisplay.formatModelCheckingResult resultJson
980-
let msg := summary ++ "\n" ++ details
981-
let violationIsError := veil.violationIsError.get opts
982-
if isViolation && violationIsError then logErrorAt stx msg else logInfoAt stx msg
983-
where
984-
/-- Get the theory term, defaulting to `{}` if not provided and there are no theory fields.
985-
Throws a helpful error if theory fields exist but no term was provided. -/
986-
getTheoryTerm (theoryTermOpt : Option Term) (mod : Module) (instTerm : Term) : CommandElabM Term := do
987-
match theoryTermOpt with
988-
| some t => pure t
989-
| none =>
990-
unless mod.immutableComponents.isEmpty do
991-
let fieldStrs := mod.immutableComponents.map (fun c => s!"{c.name} := ...")
992-
let theoryExample := "{ " ++ ", ".intercalate fieldStrs.toList ++ " }"
993-
throwError "This module has immutable fields, so you must specify the theory instantiation:\n\
994-
#simulate {instTerm} {theoryExample}"
995-
`({})
996-
997-
/-- Prepend `name` with `mod.name`. Useful when expressions are printed out for debugging. -/
998-
mkIdentWithModName (mod : Module) (name : Name) : Ident :=
999-
Lean.mkIdent (mod.name ++ name)
1000-
1001-
/-- Build search parameters reused by simulator execution. -/
1002-
mkSearchParameters (mod : Module) (config : ModelCheckerConfig) : CommandElabM Term := do
1003-
let mkProp (sa : StateAssertion) : CommandElabM Term :=
1004-
`($(mkIdent ``Veil.ModelChecker.SafetyProperty.mk)
1005-
($(mkIdent `name) := $(quote sa.name))
1006-
($(mkIdent `property) := fun $(mkIdent `th) $(mkIdent `st) => $(mkIdentWithModName mod sa.name) $(mkIdent `th) $(mkIdent `st)))
1007-
let safetyList ← `([$((← mod.invariants.mapM mkProp)),*])
1008-
let terminatingProp ← match mod.terminations[0]? with
1009-
| some t => mkProp t
1010-
| none => `($(mkIdent `default))
1011-
let earlyTermConds ← do
1012-
let base ← `([$(mkIdent ``Veil.ModelChecker.EarlyTerminationCondition.foundViolatingState),
1013-
$(mkIdent ``Veil.ModelChecker.EarlyTerminationCondition.assertionFailed),
1014-
$(mkIdent ``Veil.ModelChecker.EarlyTerminationCondition.deadlockOccurred)])
1015-
if config.maxDepth > 0 then `($base ++ [$(mkIdent ``Veil.ModelChecker.EarlyTerminationCondition.reachedDepthBound) $(quote config.maxDepth)])
1016-
else pure base
1017-
`({ $(mkIdent `invariants):ident := $safetyList, $(mkIdent `terminating):ident := $terminatingProp,
1018-
$(mkIdent `earlyTerminationConditions):ident := $earlyTermConds })
1019-
980+
logInfoAt stx summary
981+
-- Log the same trace display as #model_check
982+
elabModelCheck.logModelCheckResult stx resultJson
983+
-- Display the same TraceDisplayViewer widget as #model_check
984+
let (instanceId, _) ← ModelChecker.Concrete.allocProgressInstance (← getActionLabelNames mod)
985+
ModelChecker.Concrete.finishProgress instanceId resultJson
986+
ModelChecker.displayStreamingProgress stx instanceId
1020987
end Veil

0 commit comments

Comments
 (0)