mirror of
https://github.com/priyanshujain/sanderling.git
synced 2026-10-02 19:17:10 +00:00
feat(ltl): NNF in NewEvaluator, bounded-always, Finalize, collapse
Apply nnf on construction, reduce bounded Always symmetric to bounded Eventually (vacuous holds once the window closes), add Finalize to resolve undischarged liveness obligations to Violated at run end, and collapse structurally-identical pending obligations.
This commit is contained in:
1 parent
88295fc02e
commit
c965089825
1 file changed
+109
-2
+109
-2
@@ -34,10 +34,11 @@ type Evaluator struct {
|
||||
root Formula
|
||||
pending []Formula
|
||||
violated bool
|
||||
steps int
|
||||
}
|
||||
|
||||
func NewEvaluator(formula Formula) *Evaluator {
|
||||
return &Evaluator{root: formula}
|
||||
return &Evaluator{root: nnf(formula)}
|
||||
}
|
||||
|
||||
// Observe evaluates the formula against the current state and returns the
|
||||
@@ -52,6 +53,7 @@ func (e *Evaluator) ObserveAt(now time.Time) Verdict {
|
||||
if e.violated {
|
||||
return VerdictViolated
|
||||
}
|
||||
e.steps++
|
||||
|
||||
fresh := rootObligation(e.root)
|
||||
obligations := append(e.pending, fresh)
|
||||
@@ -71,12 +73,98 @@ func (e *Evaluator) ObserveAt(now time.Time) Verdict {
|
||||
}
|
||||
}
|
||||
|
||||
e.pending = collapse(e.pending)
|
||||
|
||||
if len(e.pending) > 0 {
|
||||
return VerdictPending
|
||||
}
|
||||
return VerdictHolds
|
||||
}
|
||||
|
||||
// collapse removes structurally-identical obligations, keeping the first
|
||||
// occurrence in order. Distinct predicates never merge because ThunkFormula's
|
||||
// name participates in its describe() key, so deduping cannot hide a violation.
|
||||
func collapse(obligations []Formula) []Formula {
|
||||
if len(obligations) < 2 {
|
||||
return obligations
|
||||
}
|
||||
seen := make(map[string]struct{}, len(obligations))
|
||||
result := obligations[:0]
|
||||
for _, obligation := range obligations {
|
||||
key := obligation.describe()
|
||||
if _, ok := seen[key]; ok {
|
||||
continue
|
||||
}
|
||||
seen[key] = struct{}{}
|
||||
result = append(result, obligation)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Finalize reports the terminal verdict for the run. Pending obligations that
|
||||
// can never be discharged by a future step (an unbounded eventually that never
|
||||
// fired, a strong next with no successor) resolve to Violated; safety
|
||||
// obligations that were never breached resolve to Holds.
|
||||
func (e *Evaluator) Finalize() Verdict {
|
||||
if e.violated {
|
||||
return VerdictViolated
|
||||
}
|
||||
for _, obligation := range e.pending {
|
||||
if finalize(obligation) == statusViolated {
|
||||
e.violated = true
|
||||
e.pending = nil
|
||||
return VerdictViolated
|
||||
}
|
||||
}
|
||||
return VerdictHolds
|
||||
}
|
||||
|
||||
// finalize collapses a pending obligation to its terminal status assuming no
|
||||
// further steps will occur.
|
||||
func finalize(formula Formula) residualStatus {
|
||||
switch concrete := formula.(type) {
|
||||
case PureFormula:
|
||||
if concrete.Value {
|
||||
return statusHolds
|
||||
}
|
||||
return statusViolated
|
||||
case ThunkFormula:
|
||||
return statusViolated
|
||||
case EventuallyFormula:
|
||||
return statusViolated
|
||||
case NextFormula:
|
||||
return statusViolated
|
||||
case AlwaysFormula:
|
||||
return statusHolds
|
||||
case NowFormula:
|
||||
return finalize(concrete.Inner)
|
||||
case NotFormula:
|
||||
switch finalize(concrete.Inner) {
|
||||
case statusViolated:
|
||||
return statusHolds
|
||||
default:
|
||||
return statusViolated
|
||||
}
|
||||
case AndFormula:
|
||||
if finalize(concrete.Left) == statusViolated || finalize(concrete.Right) == statusViolated {
|
||||
return statusViolated
|
||||
}
|
||||
return statusHolds
|
||||
case OrFormula:
|
||||
if finalize(concrete.Left) == statusHolds || finalize(concrete.Right) == statusHolds {
|
||||
return statusHolds
|
||||
}
|
||||
return statusViolated
|
||||
case ImpliesFormula:
|
||||
if finalize(concrete.Antecedent) == statusViolated {
|
||||
return statusHolds
|
||||
}
|
||||
return finalize(concrete.Consequent)
|
||||
default:
|
||||
return statusHolds
|
||||
}
|
||||
}
|
||||
|
||||
// Residual returns a single Formula describing what the evaluator still has
|
||||
// to prove after the most recent ObserveAt. PureFormula{true} means the
|
||||
// property holds for the run so far; PureFormula{false} means it has latched
|
||||
@@ -233,11 +321,30 @@ func reduce(formula Formula, now time.Time) reduceResult {
|
||||
}
|
||||
|
||||
case AlwaysFormula:
|
||||
// First-reduction deadline resolution mirrors EventuallyFormula so a
|
||||
// relative duration becomes a stable absolute deadline.
|
||||
if !concrete.HasDeadline && concrete.Duration > 0 {
|
||||
concrete.Deadline = now.Add(concrete.Duration)
|
||||
concrete.HasDeadline = true
|
||||
}
|
||||
innerResult := reduce(concrete.Inner, now)
|
||||
if innerResult.status == statusViolated {
|
||||
return violated()
|
||||
}
|
||||
next := AlwaysFormula{Inner: concrete.Inner}
|
||||
// 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
|
||||
// the window a pending inner cannot be deferred, so it resolves to holds.
|
||||
if concrete.HasStepBound && concrete.StepBound <= 1 {
|
||||
return holds()
|
||||
}
|
||||
if concrete.HasDeadline && !now.Before(concrete.Deadline) {
|
||||
return holds()
|
||||
}
|
||||
next := concrete
|
||||
next.Inner = concrete.Inner
|
||||
if concrete.HasStepBound {
|
||||
next.StepBound = concrete.StepBound - 1
|
||||
}
|
||||
if innerResult.status == statusHolds {
|
||||
return pending(next)
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user