mirror of
https://github.com/priyanshujain/sanderling.git
synced 2026-10-02 11:07:10 +00:00
feat(ltl): formula AST and step evaluator for v0.1
Supports Always over Pure/Thunk leaves; eventually/next/bounds deferred. Once a thunk returns false under an Always, the verdict latches to violated so the runner can surface the offending step without later observations masking it.
This commit is contained in:
1 parent
cee90694da
commit
0c277b2833
3 files changed
+223
No files matched your search
@@ -0,0 +1,60 @@
|
|||||||
|
package ltl
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
type Verdict int
|
||||||
|
|
||||||
|
const (
|
||||||
|
VerdictHolds Verdict = iota
|
||||||
|
VerdictViolated
|
||||||
|
)
|
||||||
|
|
||||||
|
func (v Verdict) String() string {
|
||||||
|
switch v {
|
||||||
|
case VerdictHolds:
|
||||||
|
return "holds"
|
||||||
|
case VerdictViolated:
|
||||||
|
return "violated"
|
||||||
|
default:
|
||||||
|
return fmt.Sprintf("verdict(%d)", int(v))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Evaluator folds a formula across observed steps. v0.1 semantics:
|
||||||
|
// Always(P) is satisfied if P held at every observed step; once P is false,
|
||||||
|
// the verdict latches to Violated.
|
||||||
|
type Evaluator struct {
|
||||||
|
formula Formula
|
||||||
|
violated bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewEvaluator(formula Formula) *Evaluator {
|
||||||
|
return &Evaluator{formula: formula}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Observe evaluates the formula against the current state and returns the
|
||||||
|
// running verdict. Once Violated, subsequent calls keep returning Violated
|
||||||
|
// regardless of what later observations look like.
|
||||||
|
func (e *Evaluator) Observe() Verdict {
|
||||||
|
if e.violated {
|
||||||
|
return VerdictViolated
|
||||||
|
}
|
||||||
|
if !holdsAtCurrentStep(e.formula) {
|
||||||
|
e.violated = true
|
||||||
|
return VerdictViolated
|
||||||
|
}
|
||||||
|
return VerdictHolds
|
||||||
|
}
|
||||||
|
|
||||||
|
func holdsAtCurrentStep(formula Formula) bool {
|
||||||
|
switch concrete := formula.(type) {
|
||||||
|
case AlwaysFormula:
|
||||||
|
return holdsAtCurrentStep(concrete.Inner)
|
||||||
|
case PureFormula:
|
||||||
|
return concrete.Value
|
||||||
|
case ThunkFormula:
|
||||||
|
return concrete.Func()
|
||||||
|
default:
|
||||||
|
panic(fmt.Sprintf("ltl: unsupported formula type %T", formula))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
package ltl
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func observe(formula Formula, count int) []Verdict {
|
||||||
|
evaluator := NewEvaluator(formula)
|
||||||
|
verdicts := make([]Verdict, 0, count)
|
||||||
|
for range count {
|
||||||
|
verdicts = append(verdicts, evaluator.Observe())
|
||||||
|
}
|
||||||
|
return verdicts
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPure_HoldsThenStays(t *testing.T) {
|
||||||
|
got := observe(Always(Pure(true)), 3)
|
||||||
|
for index, verdict := range got {
|
||||||
|
if verdict != VerdictHolds {
|
||||||
|
t.Errorf("step %d: got %v, want holds", index, verdict)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPure_FalseImmediatelyViolates(t *testing.T) {
|
||||||
|
got := observe(Always(Pure(false)), 3)
|
||||||
|
for index, verdict := range got {
|
||||||
|
if verdict != VerdictViolated {
|
||||||
|
t.Errorf("step %d: got %v, want violated", index, verdict)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestThunk_TransitionFromHoldToViolate(t *testing.T) {
|
||||||
|
values := []bool{true, true, false, true, true}
|
||||||
|
step := 0
|
||||||
|
evaluator := NewEvaluator(Always(Thunk(func() bool {
|
||||||
|
current := values[step]
|
||||||
|
step++
|
||||||
|
return current
|
||||||
|
})))
|
||||||
|
|
||||||
|
wantSequence := []Verdict{
|
||||||
|
VerdictHolds, // true
|
||||||
|
VerdictHolds, // true
|
||||||
|
VerdictViolated, // false — latches
|
||||||
|
VerdictViolated, // true after violation — still violated
|
||||||
|
VerdictViolated, // true after violation — still violated
|
||||||
|
}
|
||||||
|
for index, want := range wantSequence {
|
||||||
|
got := evaluator.Observe()
|
||||||
|
if got != want {
|
||||||
|
t.Errorf("step %d: got %v, want %v", index, got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEvaluator_StickinessAfterViolation(t *testing.T) {
|
||||||
|
state := true
|
||||||
|
evaluator := NewEvaluator(Always(Thunk(func() bool { return state })))
|
||||||
|
|
||||||
|
if got := evaluator.Observe(); got != VerdictHolds {
|
||||||
|
t.Fatalf("step 1: got %v, want holds", got)
|
||||||
|
}
|
||||||
|
state = false
|
||||||
|
if got := evaluator.Observe(); got != VerdictViolated {
|
||||||
|
t.Fatalf("step 2: got %v, want violated", got)
|
||||||
|
}
|
||||||
|
state = true
|
||||||
|
if got := evaluator.Observe(); got != VerdictViolated {
|
||||||
|
t.Fatalf("step 3 (recovered state): violation should latch, got %v", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEvaluator_TopLevelPureCountedAtEachStep(t *testing.T) {
|
||||||
|
got := observe(Pure(true), 2)
|
||||||
|
if got[0] != VerdictHolds || got[1] != VerdictHolds {
|
||||||
|
t.Errorf("bare Pure(true): %v", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEvaluator_TopLevelThunkRespectsObservation(t *testing.T) {
|
||||||
|
state := true
|
||||||
|
evaluator := NewEvaluator(Thunk(func() bool { return state }))
|
||||||
|
if got := evaluator.Observe(); got != VerdictHolds {
|
||||||
|
t.Errorf("expected holds, got %v", got)
|
||||||
|
}
|
||||||
|
state = false
|
||||||
|
if got := evaluator.Observe(); got != VerdictViolated {
|
||||||
|
t.Errorf("expected violated, got %v", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVerdict_String(t *testing.T) {
|
||||||
|
if VerdictHolds.String() != "holds" {
|
||||||
|
t.Errorf("VerdictHolds.String() = %q", VerdictHolds.String())
|
||||||
|
}
|
||||||
|
if VerdictViolated.String() != "violated" {
|
||||||
|
t.Errorf("VerdictViolated.String() = %q", VerdictViolated.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDescribe(t *testing.T) {
|
||||||
|
formula := Always(Pure(true))
|
||||||
|
if got := Describe(formula); !strings.Contains(got, "Always") || !strings.Contains(got, "Pure(true)") {
|
||||||
|
t.Errorf("Describe wrong: %q", got)
|
||||||
|
}
|
||||||
|
thunk := Always(Thunk(func() bool { return true }))
|
||||||
|
if got := Describe(thunk); !strings.Contains(got, "Thunk") {
|
||||||
|
t.Errorf("Describe(thunk) wrong: %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestObserve_PanicsOnUnknownFormulaType(t *testing.T) {
|
||||||
|
type unsupportedFormula struct{ Formula }
|
||||||
|
defer func() {
|
||||||
|
if recovered := recover(); recovered == nil {
|
||||||
|
t.Errorf("expected panic on unsupported formula type")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
holdsAtCurrentStep(unsupportedFormula{})
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package ltl
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
// Formula is the AST of a temporal logic property. v0.1 supports only Always
|
||||||
|
// over Pure/Thunk leaves; eventually, next, and bounded operators are
|
||||||
|
// deferred to v0.2+.
|
||||||
|
type Formula interface {
|
||||||
|
isFormula()
|
||||||
|
describe() string
|
||||||
|
}
|
||||||
|
|
||||||
|
type AlwaysFormula struct {
|
||||||
|
Inner Formula
|
||||||
|
}
|
||||||
|
|
||||||
|
type PureFormula struct {
|
||||||
|
Value bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type ThunkFormula struct {
|
||||||
|
Func func() bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func Always(inner Formula) Formula { return AlwaysFormula{Inner: inner} }
|
||||||
|
|
||||||
|
func Pure(value bool) Formula { return PureFormula{Value: value} }
|
||||||
|
|
||||||
|
func Thunk(function func() bool) Formula { return ThunkFormula{Func: function} }
|
||||||
|
|
||||||
|
func (AlwaysFormula) isFormula() {}
|
||||||
|
func (PureFormula) isFormula() {}
|
||||||
|
func (ThunkFormula) isFormula() {}
|
||||||
|
|
||||||
|
func (a AlwaysFormula) describe() string { return "Always(" + a.Inner.describe() + ")" }
|
||||||
|
func (p PureFormula) describe() string { return fmt.Sprintf("Pure(%t)", p.Value) }
|
||||||
|
func (ThunkFormula) describe() string { return "Thunk(...)" }
|
||||||
|
|
||||||
|
// Describe returns a debug-friendly representation of the formula.
|
||||||
|
func Describe(formula Formula) string { return formula.describe() }
|
||||||
Reference in new issue
Block a user