Skip to content

[Feature]: report the state at each depth in #bmc and #kind counterexamples #163

Description

@geofflittle

Problem Statement

#bmc and #kind print the input at each depth but not the state it produces, so the state that violates the invariant never appears. #kind names a state only at depth 0, #bmc names none at any depth, and since the model is reported per declared constant, an inlined term has no name for a value to attach to.

import Blaster.StateMachine
open Blaster.StateMachine

structure S where
  x : Int
structure I where
  y : Int
instance sm : StateMachine I S where
  init _ := { x := 0 }
  next i s := { x := s.x + i.y }
  assumptions i _ := 0 ≤ i.y ∧ i.y ≤ 1
  invariants _ s := s.x ≤ 2

#kind (max-depth: 3) [sm]

Current output:

❌ Falsified
Counterexample detected at Depth 3:
 - «sm.state@0»: (S.mk 0)
 - «sm.input@0»: (I.mk 1)
 - «sm.input@1»: (I.mk 1)
 - «sm.input@2»: (I.mk 1)
 - «sm.input@3»: (I.mk 1)

The invariant fails at depth 3, so the reader needs state@3. To get it from this trace they have to know that the state at depth d is built from the input at depth d, that input@0 contributes nothing because init ignores its argument, and then sum the remaining three. None of that is shown. Four identical values are printed, one is a decoy, and the value the verdict turns on is absent.

Which inputs contribute is not inferable from the trace either. In this run the encoding below shows that input@0 is genuinely absent from the depth-3 invariant, but nothing in the printed output says so.

Alternatives Considered

  • Reconstruct the states by hand from the inputs. That is the burden itself rather than a way around it, and it grows with the number of steps, since every state depends on the whole prefix once next reads the previous state. All the machines in Tests/StateMachine are of that kind.

  • dump-smt-lib. This shows why the value cannot be reported rather than providing it. Running the same machine with (dump-smt-lib: 1), the problem declares one state constant and four input constants:

    (declare-const $0 @S)
    (declare-const $1 @I)
    (declare-const $2 @I)
    (declare-const $3 @I)
    (declare-const $4 @I)
    

    and the depth-0 and depth-3 invariants are encoded as:

    (define-fun _inv@0 () Bool (not (< 2 (S.mk.0 $0))))
    (define-fun _inv@3 () Bool (not (< 2 (+ (+ (+ (S.mk.0 $0) (I.mk.0 $2)) (I.mk.0 $3)) (I.mk.0 $4)))))
    

    Depth 0 refers to $0, a declared constant, which is why state@0 is reported. Depth 3 is a nested sum with no constant of its own, so there is nothing for the model to attach a value to. The dump gives the reader that expression to evaluate by hand rather than the value.

  • verbose, and the Translate.expr and Translate.optExpr trace classes. As far as we could tell these are pre-solve and echo symbolic expressions rather than model values.

We found no existing setting that reports intermediate states.

Use Case

The same machine, with the states present. This is the requested output, not current behaviour:

❌ Falsified
Counterexample detected at Depth 3:
 - «sm.state@0»: (S.mk 0)
 - «sm.input@0»: (I.mk 1)
 - «sm.input@1»: (I.mk 1)
 - «sm.state@1»: (S.mk 1)
 - «sm.input@2»: (I.mk 1)
 - «sm.state@2»: (S.mk 2)
 - «sm.input@3»: (I.mk 1)
 - «sm.state@3»: (S.mk 3)

The counter walks 0, 1, 2, 3 and the invariant s.x ≤ 2 visibly breaks at the last step. Nothing has to be reconstructed, and there is no way to pair a state with the wrong input.

Priority

Medium - Would be beneficial

Additional Context

#kind already declares a state at depth 0 and asserts it equal to init, so there is precedent for the shape.

Naming intermediate states would change counterexample text, so goldens that pin traces would need regenerating. It may also overlap with any future temporal operators for inspecting past steps, since those would presumably want per-depth state addressable as well.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions