mirror of
https://github.com/priyanshujain/sanderling.git
synced 2026-10-04 12:07:09 +00:00
fix(ltl): give every thunk a construction identity
Two distinct unnamed predicates both described as "Thunk(...)", so obligation collapse merged their residuals and could drop a live violation. Identity is assigned at construction and the fields are unexported, so a thunk cannot be built without one. Claude-Session: https://claude.ai/code/session_01Fj4wJUikdABuMQEETwW55J
This commit is contained in:
1 parent
7343085614
commit
0108fc80d5
3 files changed
+75
-21
No files matched your search
@@ -152,7 +152,7 @@ func TestViolationLatchIsMonotonic(t *testing.T) {
|
||||
// holds/violated at run end, making sanderling lie about pass/fail.
|
||||
func TestFinalize_KleeneConnectives(t *testing.T) {
|
||||
pure := func(v bool) Formula { return PureFormula{Value: v} }
|
||||
pendingThunk := ThunkFormula{Name: "t", Func: func() (bool, error) { return true, nil }}
|
||||
pendingThunk := ThunkNamed("t", func() (bool, error) { return true, nil })
|
||||
eventuallyViolated := EventuallyFormula{Inner: PureFormula{Value: false}}
|
||||
nextPending := NextFormula{Inner: PureFormula{Value: true}}
|
||||
alwaysHolds := AlwaysFormula{Inner: PureFormula{Value: true}}
|
||||
@@ -224,3 +224,41 @@ func TestCollapse_NamedThunkLeakBoundsPendingSet(t *testing.T) {
|
||||
t.Errorf("pending set leaked to %d obligations", len(evaluator.pending))
|
||||
}
|
||||
}
|
||||
|
||||
// TestCollapse_UnnamedPredicatesDoNotMerge is the lost-violation counterexample
|
||||
// from the attribution analysis, run with unnamed thunks. Every unnamed thunk
|
||||
// used to print "Thunk(...)", so the four Eventually residuals below shared one
|
||||
// collapse key and the obligation spawned at step 2 was dropped: the run
|
||||
// reported holds while a genuine violation was outstanding.
|
||||
//
|
||||
// root = And(Or(F a, c), Or(F b, d)), d = not c
|
||||
// a never true, b true from step 6, c true except at steps 2 and 4
|
||||
//
|
||||
// At steps 1, 3 and 5 the left disjunct discharges via c and the right spawns
|
||||
// F b; at steps 2 and 4 the right discharges via d and the left spawns F a.
|
||||
// F a can never discharge, so the run violates with origin 2.
|
||||
func TestCollapse_UnnamedPredicatesDoNotMerge(t *testing.T) {
|
||||
step := 0
|
||||
a := Thunk(func() (bool, error) { return false, nil })
|
||||
b := Thunk(func() (bool, error) { return step >= 6, nil })
|
||||
c := Thunk(func() (bool, error) { return step != 2 && step != 4, nil })
|
||||
d := Thunk(func() (bool, error) { return step == 2 || step == 4, nil })
|
||||
|
||||
evaluator := NewEvaluator(And(Or(Eventually(a), c), Or(Eventually(b), d)))
|
||||
for index := 1; index <= 10; index++ {
|
||||
step = index
|
||||
if got := evaluator.ObserveAtStep(time.Unix(int64(index), 0), index); got == VerdictViolated {
|
||||
t.Fatalf("step %d violated early", index)
|
||||
}
|
||||
}
|
||||
if got := evaluator.Finalize(); got != VerdictViolated {
|
||||
t.Fatalf("Finalize = %v, want violated (F a can never discharge)", got)
|
||||
}
|
||||
witness := evaluator.Violation()
|
||||
if witness == nil {
|
||||
t.Fatal("Violation = nil, want non-nil")
|
||||
}
|
||||
if witness.Step != 2 {
|
||||
t.Errorf("origin = %d, want 2", witness.Step)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user