diff --git a/pkg/spec/src/ltl.ts b/pkg/spec/src/ltl.ts index 3ef5b63..aa5f4e9 100644 --- a/pkg/spec/src/ltl.ts +++ b/pkg/spec/src/ltl.ts @@ -14,7 +14,9 @@ export function next(predicate: () => boolean): Formula { // An unbounded `eventually` never forces a violation within a finite run. // Prefer `.within(n, unit)` when you want the verifier to fail a property -// that stalls. +// that stalls. `"steps"` counts observed steps rather than wall-clock time, +// which is what keeps the window the same size across runs of different +// speeds. export function eventually(predicate: () => boolean): EventuallyFormula { return globalThis.__sanderling__.eventually(predicate); } diff --git a/pkg/spec/src/types.ts b/pkg/spec/src/types.ts index 66394a6..32677d0 100644 --- a/pkg/spec/src/types.ts +++ b/pkg/spec/src/types.ts @@ -203,6 +203,10 @@ export interface Formula { } export interface EventuallyFormula extends Formula { + // `"milliseconds"` and `"seconds"` bound the window in wall-clock time, which + // is what a user-perceived deadline means. `"steps"` bounds it in observed + // steps, so the same window costs the same regardless of how long each step + // took: use it for anything compared across runs of different speeds. within(amount: number, unit: "milliseconds" | "seconds" | "steps"): Formula; } diff --git a/pkg/spec/test/api.test.ts b/pkg/spec/test/api.test.ts index 60647fd..ce9590b 100644 --- a/pkg/spec/test/api.test.ts +++ b/pkg/spec/test/api.test.ts @@ -203,6 +203,12 @@ test("eventually().within forwards unit and amount", () => { assert.deepEqual(runtime.withinCalls[0], { amount: 3, unit: "seconds" }); }); +test("eventually().within forwards the step unit unchanged", () => { + const runtime = installFakeRuntime(); + eventually(() => true).within(1915, "steps"); + assert.deepEqual(runtime.withinCalls[0], { amount: 1915, unit: "steps" }); +}); + test("formula chaining exposes implies/or/and/not", () => { const runtime = installFakeRuntime(); const a = now(() => true);