feat(ltl): bound fields on AlwaysFormula and named thunks

Add StepBound/Duration/Deadline to AlwaysFormula as the dual of bounded
Eventually, give ThunkFormula a Name for stable identity, add ThunkNamed,
and surface both in describe() and MarshalJSON.
This commit is contained in:
pj committed 2026-06-01 13:49:24 +05:30
1 parent 88db9653e5
commit 4163f43736
1 file changed
+56 -11
+56 -11
View File
@@ -13,9 +13,9 @@ type Formula interface {
describe() string
}
// PredicateLabel lets a ThunkFormula carry a human-readable name for the
// closure it wraps. Verifier wires this in when the spec gives the predicate
// a property name; otherwise it stays empty and serializes without a name.
// PredicateLabel lets a ThunkFormula expose the identity of the closure it
// wraps. ThunkFormula satisfies it through its Name field; an empty name
// serializes without a name.
type PredicateLabel interface {
PredicateName() string
}
@@ -33,18 +33,33 @@ func (e ErrorFormula) describe() string {
return fmt.Sprintf("Error(%q)", e.Message)
}
// AlwaysFormula obliges its inner formula to hold at every step. A bounded
// Always (the dual of a bounded Eventually) holds for the steps inside its
// window and is vacuously satisfied once the window closes. An unbounded
// Always carries no bound fields and is checked at every observed step.
type AlwaysFormula struct {
Inner Formula
Inner Formula
StepBound int
HasStepBound bool
Duration time.Duration
Deadline time.Time
HasDeadline bool
}
type PureFormula struct {
Value bool
}
// ThunkFormula wraps an opaque predicate closure. Name carries the predicate's
// identity so two distinct predicates produce distinct describe() keys and are
// never merged during obligation collapse.
type ThunkFormula struct {
Func func() bool
Name string
}
func (t ThunkFormula) PredicateName() string { return t.Name }
// NowFormula marks its inner formula for evaluation at the current step only.
// Primarily used so that now(...).implies(...) parses unambiguously.
type NowFormula struct {
@@ -98,6 +113,10 @@ func Pure(value bool) Formula { return PureFormula{Value: value} }
func Thunk(function func() bool) Formula { return ThunkFormula{Func: function} }
func ThunkNamed(name string, function func() bool) Formula {
return ThunkFormula{Func: function, Name: name}
}
func Now(inner Formula) Formula { return NowFormula{Inner: inner} }
func Next(inner Formula) Formula { return NextFormula{Inner: inner} }
@@ -137,9 +156,25 @@ func (OrFormula) isFormula() {}
func (AndFormula) isFormula() {}
func (NotFormula) 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(...)" }
func (a AlwaysFormula) describe() string {
parts := []string{a.Inner.describe()}
if a.HasStepBound {
parts = append(parts, fmt.Sprintf("steps=%d", a.StepBound))
}
if a.HasDeadline {
parts = append(parts, "deadline="+a.Deadline.Format(time.RFC3339Nano))
} else if a.Duration > 0 {
parts = append(parts, "within="+a.Duration.String())
}
return "Always(" + strings.Join(parts, ", ") + ")"
}
func (p PureFormula) describe() string { return fmt.Sprintf("Pure(%t)", p.Value) }
func (t ThunkFormula) describe() string {
if t.Name != "" {
return "Thunk(" + t.Name + ")"
}
return "Thunk(...)"
}
func (n NowFormula) describe() string { return "Now(" + n.Inner.describe() + ")" }
func (n NextFormula) describe() string { return "Next(" + n.Inner.describe() + ")" }
func (e EventuallyFormula) describe() string {
@@ -176,10 +211,20 @@ type withinNode struct {
}
func (a AlwaysFormula) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
Op string `json:"op"`
Arg Formula `json:"arg"`
}{"always", a.Inner})
payload := struct {
Op string `json:"op"`
Arg Formula `json:"arg"`
Within *withinNode `json:"within,omitempty"`
}{Op: "always", Arg: a.Inner}
switch {
case a.HasStepBound:
payload.Within = &withinNode{Amount: int64(a.StepBound), Unit: "steps"}
case a.Duration > 0:
payload.Within = &withinNode{Amount: a.Duration.Milliseconds(), Unit: "milliseconds"}
case a.HasDeadline:
payload.Within = &withinNode{Amount: a.Deadline.UnixMilli(), Unit: "deadline"}
}
return json.Marshal(payload)
}
func (n NowFormula) MarshalJSON() ([]byte, error) {