mirror of
https://github.com/priyanshujain/sanderling.git
synced 2026-10-02 19:17:10 +00:00
feat(ltl): witness violations and (bool,error) predicate thunks
This commit is contained in:
1 parent
2a57031bb7
commit
9be00dff90
2 files changed
+102
-23
No files matched your search
+92
-15
@@ -31,10 +31,22 @@ func (v Verdict) String() string {
|
|||||||
// violated) or carries them forward as residuals. Once a single obligation
|
// violated) or carries them forward as residuals. Once a single obligation
|
||||||
// violates, the overall verdict latches to Violated.
|
// violates, the overall verdict latches to Violated.
|
||||||
type Evaluator struct {
|
type Evaluator struct {
|
||||||
root Formula
|
root Formula
|
||||||
pending []Formula
|
pending []Formula
|
||||||
violated bool
|
violated bool
|
||||||
steps int
|
steps int
|
||||||
|
violation *Violation
|
||||||
|
}
|
||||||
|
|
||||||
|
// Violation is the witness for a latched verdict: the failing sub-formula, a
|
||||||
|
// human-readable reason, and the observation step it fired at. A thrown
|
||||||
|
// predicate carries the goja error text as its reason; a plain false carries
|
||||||
|
// "predicate false"; Finalize fills it for liveness obligations that never
|
||||||
|
// discharged.
|
||||||
|
type Violation struct {
|
||||||
|
Formula Formula
|
||||||
|
Reason string
|
||||||
|
Step int
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewEvaluator(formula Formula) *Evaluator {
|
func NewEvaluator(formula Formula) *Evaluator {
|
||||||
@@ -67,6 +79,10 @@ func (e *Evaluator) ObserveAt(now time.Time) Verdict {
|
|||||||
case statusViolated:
|
case statusViolated:
|
||||||
e.violated = true
|
e.violated = true
|
||||||
e.pending = nil
|
e.pending = nil
|
||||||
|
e.violation = result.witness
|
||||||
|
if e.violation != nil {
|
||||||
|
e.violation.Step = e.steps
|
||||||
|
}
|
||||||
return VerdictViolated
|
return VerdictViolated
|
||||||
case statusPending:
|
case statusPending:
|
||||||
e.pending = append(e.pending, result.formula)
|
e.pending = append(e.pending, result.formula)
|
||||||
@@ -113,12 +129,40 @@ func (e *Evaluator) Finalize() Verdict {
|
|||||||
if finalize(obligation) == statusViolated {
|
if finalize(obligation) == statusViolated {
|
||||||
e.violated = true
|
e.violated = true
|
||||||
e.pending = nil
|
e.pending = nil
|
||||||
|
e.violation = &Violation{
|
||||||
|
Formula: obligation,
|
||||||
|
Reason: finalizeReason(obligation),
|
||||||
|
Step: e.steps,
|
||||||
|
}
|
||||||
return VerdictViolated
|
return VerdictViolated
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return VerdictHolds
|
return VerdictHolds
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Violation returns the witness for a latched violation, or nil if the
|
||||||
|
// evaluator has not violated. The witness is set by ObserveAt at the step a
|
||||||
|
// reduction first violated, or by Finalize for a liveness obligation that
|
||||||
|
// never discharged.
|
||||||
|
func (e *Evaluator) Violation() *Violation {
|
||||||
|
return e.violation
|
||||||
|
}
|
||||||
|
|
||||||
|
// finalizeReason describes why an undischarged obligation resolves to violated
|
||||||
|
// at run end.
|
||||||
|
func finalizeReason(formula Formula) string {
|
||||||
|
switch formula.(type) {
|
||||||
|
case EventuallyFormula:
|
||||||
|
return "eventually never satisfied"
|
||||||
|
case NextFormula:
|
||||||
|
return "next obligation unmet at run end"
|
||||||
|
case ThunkFormula:
|
||||||
|
return "obligation unmet at run end"
|
||||||
|
default:
|
||||||
|
return "liveness obligation unmet at run end"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// finalize collapses a pending obligation to its terminal status assuming no
|
// finalize collapses a pending obligation to its terminal status assuming no
|
||||||
// further steps will occur.
|
// further steps will occur.
|
||||||
func finalize(formula Formula) residualStatus {
|
func finalize(formula Formula) residualStatus {
|
||||||
@@ -207,10 +251,36 @@ const (
|
|||||||
type reduceResult struct {
|
type reduceResult struct {
|
||||||
status residualStatus
|
status residualStatus
|
||||||
formula Formula
|
formula Formula
|
||||||
|
witness *Violation
|
||||||
}
|
}
|
||||||
|
|
||||||
func holds() reduceResult { return reduceResult{status: statusHolds} }
|
func holds() reduceResult { return reduceResult{status: statusHolds} }
|
||||||
|
|
||||||
|
// violated reports a violation without an attached witness. Used where the
|
||||||
|
// failing sub-formula is recovered from a child result whose own witness is
|
||||||
|
// carried up by violatedFrom.
|
||||||
func violated() reduceResult { return reduceResult{status: statusViolated} }
|
func violated() reduceResult { return reduceResult{status: statusViolated} }
|
||||||
|
|
||||||
|
// violatedWith reports a violation that originates at the given sub-formula
|
||||||
|
// with the given reason. The reason distinguishes a thrown predicate from a
|
||||||
|
// plain false so callers (and the inspect UI) can render the cause.
|
||||||
|
func violatedWith(formula Formula, reason string) reduceResult {
|
||||||
|
return reduceResult{
|
||||||
|
status: statusViolated,
|
||||||
|
witness: &Violation{Formula: formula, Reason: reason},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// violatedFrom propagates a child violation, preferring the child's witness so
|
||||||
|
// the deepest failing leaf survives. When the child carried no witness the
|
||||||
|
// fallback formula and reason describe this level instead.
|
||||||
|
func violatedFrom(child reduceResult, fallback Formula, reason string) reduceResult {
|
||||||
|
if child.witness != nil {
|
||||||
|
return reduceResult{status: statusViolated, witness: child.witness}
|
||||||
|
}
|
||||||
|
return violatedWith(fallback, reason)
|
||||||
|
}
|
||||||
|
|
||||||
func pending(f Formula) reduceResult {
|
func pending(f Formula) reduceResult {
|
||||||
return reduceResult{status: statusPending, formula: f}
|
return reduceResult{status: statusPending, formula: f}
|
||||||
}
|
}
|
||||||
@@ -221,13 +291,17 @@ func reduce(formula Formula, now time.Time) reduceResult {
|
|||||||
if concrete.Value {
|
if concrete.Value {
|
||||||
return holds()
|
return holds()
|
||||||
}
|
}
|
||||||
return violated()
|
return violatedWith(concrete, "pure false")
|
||||||
|
|
||||||
case ThunkFormula:
|
case ThunkFormula:
|
||||||
if concrete.Func() {
|
result, err := concrete.Func()
|
||||||
|
if err != nil {
|
||||||
|
return violatedWith(concrete, err.Error())
|
||||||
|
}
|
||||||
|
if result {
|
||||||
return holds()
|
return holds()
|
||||||
}
|
}
|
||||||
return violated()
|
return violatedWith(concrete, "predicate false")
|
||||||
|
|
||||||
case NowFormula:
|
case NowFormula:
|
||||||
return reduce(concrete.Inner, now)
|
return reduce(concrete.Inner, now)
|
||||||
@@ -250,10 +324,10 @@ func reduce(formula Formula, now time.Time) reduceResult {
|
|||||||
return holds()
|
return holds()
|
||||||
}
|
}
|
||||||
if concrete.HasStepBound && concrete.StepBound <= 1 {
|
if concrete.HasStepBound && concrete.StepBound <= 1 {
|
||||||
return violated()
|
return violatedFrom(innerResult, concrete, "eventually bound exhausted")
|
||||||
}
|
}
|
||||||
if concrete.HasDeadline && !now.Before(concrete.Deadline) {
|
if concrete.HasDeadline && !now.Before(concrete.Deadline) {
|
||||||
return violated()
|
return violatedFrom(innerResult, concrete, "eventually deadline reached")
|
||||||
}
|
}
|
||||||
next := concrete
|
next := concrete
|
||||||
if concrete.HasStepBound {
|
if concrete.HasStepBound {
|
||||||
@@ -282,7 +356,7 @@ func reduce(formula Formula, now time.Time) reduceResult {
|
|||||||
return holds()
|
return holds()
|
||||||
}
|
}
|
||||||
if left.status == statusViolated && right.status == statusViolated {
|
if left.status == statusViolated && right.status == statusViolated {
|
||||||
return violated()
|
return violatedFrom(left, concrete, "both disjuncts violated")
|
||||||
}
|
}
|
||||||
if left.status == statusViolated {
|
if left.status == statusViolated {
|
||||||
return pending(right.formula)
|
return pending(right.formula)
|
||||||
@@ -295,8 +369,11 @@ func reduce(formula Formula, now time.Time) reduceResult {
|
|||||||
case AndFormula:
|
case AndFormula:
|
||||||
left := reduce(concrete.Left, now)
|
left := reduce(concrete.Left, now)
|
||||||
right := reduce(concrete.Right, now)
|
right := reduce(concrete.Right, now)
|
||||||
if left.status == statusViolated || right.status == statusViolated {
|
if left.status == statusViolated {
|
||||||
return violated()
|
return violatedFrom(left, concrete, "conjunct violated")
|
||||||
|
}
|
||||||
|
if right.status == statusViolated {
|
||||||
|
return violatedFrom(right, concrete, "conjunct violated")
|
||||||
}
|
}
|
||||||
if left.status == statusHolds && right.status == statusHolds {
|
if left.status == statusHolds && right.status == statusHolds {
|
||||||
return holds()
|
return holds()
|
||||||
@@ -313,7 +390,7 @@ func reduce(formula Formula, now time.Time) reduceResult {
|
|||||||
inner := reduce(concrete.Inner, now)
|
inner := reduce(concrete.Inner, now)
|
||||||
switch inner.status {
|
switch inner.status {
|
||||||
case statusHolds:
|
case statusHolds:
|
||||||
return violated()
|
return violatedWith(concrete, "negated formula held")
|
||||||
case statusViolated:
|
case statusViolated:
|
||||||
return holds()
|
return holds()
|
||||||
case statusPending:
|
case statusPending:
|
||||||
@@ -329,7 +406,7 @@ func reduce(formula Formula, now time.Time) reduceResult {
|
|||||||
}
|
}
|
||||||
innerResult := reduce(concrete.Inner, now)
|
innerResult := reduce(concrete.Inner, now)
|
||||||
if innerResult.status == statusViolated {
|
if innerResult.status == statusViolated {
|
||||||
return violated()
|
return violatedFrom(innerResult, concrete, "always inner violated")
|
||||||
}
|
}
|
||||||
// A bounded Always is the dual of a bounded Eventually: once the window
|
// A bounded Always is the dual of a bounded Eventually: once the window
|
||||||
// closes without a breach it is vacuously satisfied. At the last step in
|
// closes without a breach it is vacuously satisfied. At the last step in
|
||||||
|
|||||||
+10
-8
@@ -50,11 +50,13 @@ type PureFormula struct {
|
|||||||
Value bool
|
Value bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// ThunkFormula wraps an opaque predicate closure. Name carries the predicate's
|
// ThunkFormula wraps an opaque predicate closure. Func returns the predicate's
|
||||||
// identity so two distinct predicates produce distinct describe() keys and are
|
// boolean result and a non-nil error when the predicate threw; a thrown
|
||||||
// never merged during obligation collapse.
|
// predicate is a witnessed violation distinct from a plain false. Name carries
|
||||||
|
// the predicate's identity so two distinct predicates produce distinct
|
||||||
|
// describe() keys and are never merged during obligation collapse.
|
||||||
type ThunkFormula struct {
|
type ThunkFormula struct {
|
||||||
Func func() bool
|
Func func() (bool, error)
|
||||||
Name string
|
Name string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,9 +113,9 @@ func Always(inner Formula) Formula { return AlwaysFormula{Inner: inner} }
|
|||||||
|
|
||||||
func Pure(value bool) Formula { return PureFormula{Value: value} }
|
func Pure(value bool) Formula { return PureFormula{Value: value} }
|
||||||
|
|
||||||
func Thunk(function func() bool) Formula { return ThunkFormula{Func: function} }
|
func Thunk(function func() (bool, error)) Formula { return ThunkFormula{Func: function} }
|
||||||
|
|
||||||
func ThunkNamed(name string, function func() bool) Formula {
|
func ThunkNamed(name string, function func() (bool, error)) Formula {
|
||||||
return ThunkFormula{Func: function, Name: name}
|
return ThunkFormula{Func: function, Name: name}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -175,8 +177,8 @@ func (t ThunkFormula) describe() string {
|
|||||||
}
|
}
|
||||||
return "Thunk(...)"
|
return "Thunk(...)"
|
||||||
}
|
}
|
||||||
func (n NowFormula) describe() string { return "Now(" + n.Inner.describe() + ")" }
|
func (n NowFormula) describe() string { return "Now(" + n.Inner.describe() + ")" }
|
||||||
func (n NextFormula) describe() string { return "Next(" + n.Inner.describe() + ")" }
|
func (n NextFormula) describe() string { return "Next(" + n.Inner.describe() + ")" }
|
||||||
func (e EventuallyFormula) describe() string {
|
func (e EventuallyFormula) describe() string {
|
||||||
parts := []string{e.Inner.describe()}
|
parts := []string{e.Inner.describe()}
|
||||||
if e.HasStepBound {
|
if e.HasStepBound {
|
||||||
|
|||||||
Reference in new issue
Block a user