Write, test, and model-check Quint formal specifications for code. Use when the user asks to create a Quint spec, formally verify logic, model-check state machines, or validate code correctness with Quint.
Write Quint (quint-lang) formal specifications, run simulation tests, and model-check invariants with Apalache.
quint is installed at /opt/homebrew/bin/quint (v0.31.0+)quint verify runspec/ directory at the project rootquint test (Rust evaluator) and quint verify (Apalache). Use dangerouslyDisableSandbox: true on these commands.Read and understand the code before writing anything. Identify:
Create a .qnt file in spec/. Follow this structure:
module my_spec {
// ── Constants & pure helpers ──────────────────────────────
pure val MAX_SOMETHING: int = 5
pure def helperFn(n: int): int = { ... }
// ── State variables ───────────────────────────────────────
var phase: str
var counter: int
// ── Initial state ─────────────────────────────────────────
action init = all {
phase' = "idle",
counter' = 0,
}
// ── Actions (state transitions) ───────────────────────────
action doSomething = all {
phase == "idle", // guard
phase' = "running", // next state
counter' = counter + 1,
}
// ── Stutter step (REQUIRED for model checking) ────────────
action stutter = all {
phase' = phase,
counter' = counter,
}
// ── Step (non-deterministic choice) ───────────────────────
action step = any {
doSomething,
stutter, // prevents deadlock in terminal states
}
// ── Invariants ────────────────────────────────────────────
val myInvariant: bool = { ... }
val allInvariants: bool = and { myInvariant, ... }
// ── Tests (deterministic traces) ──────────────────────────
run myTest = {
init.then(doSomething).then(all {
assert(counter == 1),
phase' = phase, counter' = counter, // frame: assign all vars
})
}
}
quint test spec/my_spec.qnt
quint verify --invariant allInvariants spec/my_spec.qnt
These cause hard errors. Not warnings — the model checker will refuse to run.
Apalache cannot handle x.to(stateVar).fold(...) where the range bound is a state variable.
WRONG (will crash Apalache):
pure def pow(base: int, exp: int): int = {
1.to(exp).fold(1, (acc, _) => acc * base)
}
// Used in an action:
totalDelay' = totalDelay + pow(2, retryIndex)
RIGHT — use pre-computed if/else lookup tables:
pure def delayFor(n: int): int = {
if (n == 1) 2
else if (n == 2) 4
else if (n == 3) 8
else if (n == 4) 16
else 32
}
This applies everywhere: actions, invariants, any expression that Apalache evaluates symbolically. If a fold, map, or to range bound depends on a state variable, replace it with a concrete lookup.
Invariants (val) can only reference current-state variables. Using phase' (next-state) in a val is a parse error.
WRONG:
val noRetryAfterSuccess: bool = {
phase == "success" implies phase' != "retrying"
}
RIGHT — enforce this structurally via action guards instead. If an action should be impossible in a given state, make the guard exclude it.
Apalache reports "deadlock" when no transition is enabled. Terminal states (success, failure) naturally have no outgoing transitions.
FIX — always include a stutter step:
action stutter = all {
phase' = phase,
counter' = counter,
// ... assign ALL state vars to themselves
}
action step = any {
realAction1,
realAction2,
stutter, // <-- prevents deadlock
}
If an action doesn't mention a variable, Apalache treats it as unconstrained (any value). This is almost never what you want.
WRONG:
action doThing = all {
phase' = "done",
// forgot counter' — Apalache may assign it to anything
}
RIGHT:
action doThing = all {
phase' = "done",
counter' = counter, // explicitly unchanged
}
x + 1 and you can see it's correct, a spec adds nothingFor success/fail outcomes, use separate actions rather than a nondet oracle variable:
// Model both outcomes as distinct actions, let step pick non-deterministically
action attemptSuccess = all { phase == "trying", phase' = "done", ... }
action attemptFailure = all { phase == "trying", phase' = "failed", ... }
action step = any { attemptSuccess, attemptFailure, ... }
callerUnblockedAfterFirstAttempt, not inv1val for each property, then combine into allInvariantsimplies for conditional properties: phase == "done" implies configLoadedallInvariants for the --invariant flag.then() chaining: init.then(action1).then(action2).then(assertions).then() block must assign all state variables (frame condition)quint test — catches logic errors in your deterministic tracesquint verify --invariant allInvariants — exhaustive model checkSee spec/client_config_retry.qnt in this project for a complete working example covering: