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:
pj committed 2026-04-17 23:42:44 +07:00
1 parent cee90694da
commit 0c277b2833
3 files changed
+223

No files matched your search

+60
View File
@@ -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))
}
}