Quint Language Reference
Quint is an executable specification language for complex systems, developed by Informal Systems. It compiles to TLA+ and supports simulation and model checking.
Module structure
quint
module MyProtocol {
// type aliases, constants, state, actions, properties
}
Modules can import others:
quint
import Voting.* // all definitions
import Voting(quorum) // specific definition
import Voting as V // namespace alias
Types
| Type | Description | Example |
|---|
| Integers | |
| Booleans | |
| Strings | |
| Finite set | |
| Ordered sequence | |
| Key-value map (type is , not ) | value: |
| Tuple | |
| Record | |
| Sum (variant) | (use type alias) |
Type aliases:
quint
type NodeId = int
type Phase = Idle | Propose | Vote | Commit // enum — prefer over string literals
Definitions
quint
// Module parameter — fixed at instantiation, not a state variable
const N: int
const Nodes: Set[str]
// Pure function — no state access, usable anywhere
pure def max(a: int, b: int): int = if (a > b) a else b
// Stateful operator — can read vars, takes arguments (unlike val)
def isActive(n: str): bool = active.contains(n)
// State-reading value — can read vars, no arguments
val quorum: bool = votes.size() * 2 > nodes.size()
// Compile-time constant
pure val N: int = 4
val threshold: int = N / 2 + 1
vs
:
is a module parameter bound at instantiation (
);
is a fixed expression computed once.
vs
:
takes arguments;
does not.
State variables
quint
type LocalState = {
leader: int,
phase: Phase, // enum (see Type aliases) — prefer over a bare str
votes: Set[int],
log: List[str],
}
var localState: LocalState // cohesive local protocol state
var peers: int -> str // independent concern (peer metadata)
State variables can only be read in
definitions and actions; they cannot be read in
.
Actions
Actions describe state transitions. They return
—
if the action fires.
quint
action init: bool = all {
leader' = 0,
phase' = Idle,
votes' = Set(),
log' = List(),
state' = Map(),
}
action propose(node: int): bool = all {
phase == Idle,
node > 0,
leader' = node,
phase' = Propose,
votes' = votes,
log' = log,
state' = state,
}
Key rules:
- Every must be assigned in every action (use to leave unchanged).
- — all sub-expressions must hold (conjunction). Guards are plain boolean expressions inside .
- — at least one must hold (disjunction); the REPL picks non-deterministically.
Non-determinism
quint
action step: bool = any {
propose(1),
propose(2),
vote,
timeout,
}
// Non-deterministic choice from a set
action deliverMessage: bool = {
nondet msg = pending.oneOf()
all {
pending.size() > 0,
delivered' = delivered.union(Set(msg)),
pending' = pending.exclude(Set(msg)),
// ... other vars unchanged
}
}
Set operators
quint
Set(1, 2, 3).contains(2) // true
Set(1, 2).union(Set(2, 3)) // Set(1, 2, 3)
Set(1, 2, 3).intersect(Set(2, 3)) // Set(2, 3)
Set(1, 2, 3).exclude(Set(2)) // Set(1, 3)
Set(1, 2, 3).filter(x => x > 1) // Set(2, 3)
Set(1, 2, 3).map(x => x * 2) // Set(2, 4, 6)
Set(1, 2, 3).fold(0, (acc, x) => acc + x) // 6
Set(1, 2, 3).size() // 3
Set(1, 2, 3).forall(x => x > 0) // true
Set(1, 2, 3).exists(x => x > 2) // true
1.to(5) // Set(1, 2, 3, 4, 5)
nondet x = Set(1, 2, 3).oneOf() // non-deterministic pick — only valid in nondet bindings
List operators
quint
List(1, 2, 3).head() // 1
List(1, 2, 3).tail() // List(2, 3)
List(1, 2, 3).length() // 3
List(1, 2, 3).nth(1) // 2 (0-indexed)
List(1, 2, 3).append(4) // List(1, 2, 3, 4)
List(1, 2).concat(List(3, 4)) // List(1, 2, 3, 4)
List(1, 2, 3).foldl(0, (acc, x) => acc + x) // 6
List(1, 2, 3).select(x => x > 1) // List(2, 3)
Map operators
quint
Map("a" -> 1, "b" -> 2).get("a") // 1
Map("a" -> 1).put("b", 2) // Map("a" -> 1, "b" -> 2)
Map("a" -> 1, "b" -> 2).keys() // Set("a", "b")
Set(1, 2, 3).mapBy(k => k * 2) // Map(1 -> 2, 2 -> 4, 3 -> 6) — set of keys → map
Records
Records group related fields into a named type. They are the primary tool for modelling structured state in Quint.
Type aliases for records
quint
type NodeState = {
phase: Phase, // enum: Idle | Propose | Vote | Commit
voted: bool,
log: List[int],
}
type Message = {
from: int,
to: int,
round: int,
payload: str,
}
Creating and accessing
quint
val n: NodeState = { phase: Idle, voted: false, log: List() }
n.phase // Idle
n.voted // false
Updating (immutable — returns a new record)
quint
{ ...n, phase: Propose } // ✅ preferred — idiomatic, handles multiple fields
{ ...n, voted: true, phase: Vote } // ✅ multiple fields at once
n.with("phase", Propose) // ⚠️ valid but non-idiomatic — field name is a string literal
Records as state — when to group variables
TLA+ specs typically flatten all state into independent top-level variables. Quint's type system lets you group them. When fields describe one cohesive local state, make a record type and use a single state variable of that type.
Group into a record when:
- They represent the local state of a single actor (e.g. one node's phase + log + vote)
- They are always passed together as function arguments
- An invariant relates multiple fields of the same conceptual entity
Keep flat when:
- The variables represent distinct concerns that change independently
- The component is simple and grouping adds no clarity
- The variables are intentionally in different ownership/lifecycle domains
Example: preferred grouped local state vs. anti-pattern
Preferred (cohesive local state):
quint
type LocalState = {
id: int,
phase: Phase,
est1: int,
est2: Option[int], // Option is from basicSpells, not built in — see Basic spells below
round: int,
crashed: bool,
leader: int,
received_messages: Set[Message],
}
var localState: LocalState
Avoid for cohesive local state:
quint
var id: int
var phase: Phase
var est1: int
var est2: Option[int]
var round: int
var crashed: bool
var leader: int
var received_messages: Set[Message]
For N actors, use a map of grouped records:
quint
type LocalState = { phase: Phase, votedFor: int, log: List[int] }
var nodes: int -> LocalState
action commit(id: int): bool = {
val node = nodes.get(id)
all {
node.phase == Vote,
nodes' = nodes.put(id, {...node, phase: Commit}),
}
}
Nested records
quint
type ClusterState = {
nodes: int -> NodeState,
leader: int,
epoch: int,
}
var cluster: ClusterState
// Read nested field:
cluster.nodes.get(1).phase
// Update nested field (must rebuild from the inside out):
val updated = {...cluster.nodes.get(1), phase: Commit}
cluster' = {...cluster, nodes: cluster.nodes.put(1, updated)}
Records in sets (messages, events)
quint
var inFlight: Set[Message]
action send(src: int, dst: int, r: int, p: str): bool = all {
inFlight' = inFlight.union(Set({ from: src, to: dst, round: r, payload: p })),
// ...
}
// Filter by field:
inFlight.filter(m => m.to == nodeId)
inFlight.exists(m => m.round == currentRound and m.payload == "vote")
Sum types
Sum types (variants) represent a value that can be one of several distinct cases.
quint
type Action =
| Propose({ value: int, proposer: int })
| Vote({ value: int, voter: int })
| Decide({ value: int })
Each variant has a named constructor and carries one payload. A constructor takes exactly one argument — wrap multiple fields in a record (as above) or a tuple.
Construct a value by calling the constructor:
quint
val a: Action = Propose({ value: 1, proposer: 2 })
Pattern-match with
, binding the payload:
quint
pure def describeAction(a: Action): str =
match a {
| Propose(p) => "proposal"
| Vote(v) => "vote"
| Decide(d) => "decision"
}
Use
to ignore the payload when you only care which variant it is:
quint
match a {
| Propose(_) => "proposal"
| _ => "other"
}
Use sum types when a message, event, or state can take structurally different forms — not just different values of the same type.
Enum types
Enum types are a special case of sum types where each case has no additional data.
quint
type Phase = Idle | Propose | Vote | Commit
var phase: Phase
if (phase == Propose) { ... }
Variable grouping — decision guide
Before writing
declarations, answer these questions for each candidate group:
| Question | Group → record if... | Keep flat if... |
|---|
| Do these vars always change together? | Yes, in most actions | No, they're independent |
| Do they describe the same entity? | Same node / same message / same round | Different concerns |
| Is there one instance or N instances? | Either one or N (group if cohesive; for N use ) | Flat only when concerns are truly independent |
| Do invariants relate them? | Invariant spans multiple fields of one entity | Invariant uses vars independently |
Boolean operators
quint
not(p) // negation — Quint has no ! operator
p and q // conjunction
p or q // disjunction
p implies q // p => q (not(p) or q)
p iff q // p == q for booleans
and { p1, p2, p3 } // block form — equivalent to p1 and p2 and p3
or { p1, p2, p3 } // block form — at least one must hold
and
are the same operators as
and
in actions — use whichever reads more naturally in context.
Invariants and temporal properties
quint
// Safety invariant — must hold in every reachable state
// @invariant
val noDuplicateLeader: bool =
leaders.size() <= 1
// Temporal property — evaluated over traces
// @temporal
temporal eventualProgress: bool =
eventually(committed.size() > 0)
// Temporal operators
eventually(p) // p holds in some future state
always(p) // p holds in all future states
p.implies(q) // p => q
Assume
quint
assume nodeCountPositive = N > 0
assume quorumMajority = 2 * quorum > N
An
states a premise about constants, but it is
not enforced — a violated
is silently ignored by
,
, and
(none of them flags
it). It is documentation, not a checked constraint. To actually
check a condition on
constants, write a
test that asserts it (it executes and fails when the condition is
false):
quint
run quorumAssumptionTest = all {
2 * quorum > N,
N > 0,
}
Run it with
; the test fails (reporting which conjunct broke) if a constant
assignment violates the condition.
Conditional and let
quint
if (x > 0) "positive" else "non-positive"
val result = {
val doubled = x * 2
doubled + 1
}
REPL usage
Prefer CLI commands (
,
,
,
) for all validation and execution tasks. Open the REPL (
or
quint -r spec.qnt::ModuleName
) only when you need expression-level interaction the CLI does not provide.
Type inspection:
File layout
Split specs across two files:
<protocol-name>.qnt # main module — step, init, vars, invariants
<protocol-name>_test.qnt # test module — run tests and scenario witnesses (imports main)
Module responsibilities
- Declares all state variables, , actions, and safety invariants
- must live in the main module — it is the entry point for simulation
- The module name matches the file stem: in
- Imports the main module ()
- Contains tests and scenario witnesses invoked via or
- Inherits from the main module through the import
Which to pass for
must receive the module that
owns the property being checked:
| Property location | Correct |
|---|
| Invariant defined in main module | main module name |
| Witness / test defined in test module | test module name (it imports from main) |
The primitive's
field (set during indexing) always holds the correct value. Use it directly — do not derive from the filename.
Basic spells
Many useful operators are not built into Quint but are available in
, a standard library shipped with most Quint projects. Import it with:
quint
import basicSpells.* from "./basicSpells"
Key definitions it provides:
| Definition | What it does |
|---|
type Option[a] = Some(a) | None
| The option type — Quint has no built-in . Any spec field typed depends on this import. |
| The value inside ; undefined on |
| Blocks the action if is false (cleaner than bare ) |
| Set of all values in map |
| New map with applied to every value |
| True if is bound in |
getOrElse(m, key, default)
| if present, otherwise |
| / | Copy of without (or without the set of keys ) |
| / | Copy of set without / with element |
| / | First element of set / list satisfying , as |
| / / | Max / min of two integers; absolute value |
When you see a spec using
,
,
, or
without an import, it is relying on basicSpells — check whether the project includes it. (Less common operators live in a sibling
.)
Guidelines
Detailed references — read these when you need more than the quick reference above:
| File | Contents |
|---|
| Complete operator reference: extended set/list/map operators, /// for tests and witnesses, temporal fairness, |
guidelines/simulations.md
| Witnesses vs invariants, result interpretation, progressive increase protocol, trace analysis, coverage standard |
guidelines/constraints.md
| Hard language limitations: no string ops, no nested match, no destructuring, no loops, no early returns |
| Full CLI reference: , , flags, verbosity guide, reading output |
| 14 core patterns: State Type, Pure Functions, Thin Actions, Map Pre-population, Syntax Rules, Undefined Behavior, Witnesses, Nondeterministic Testing, Separate Test Files, REPL-First Debugging, Separate Concerns First, Extract System Model, Types-First Scaffolding, Logic Stubs |
| Writing and debugging tests: ////, nondeterministic tests, error location ≠ failure point, frame counting, REPL-first debugging |
| Choreo framework for distributed protocols: two-file split, pattern, testing, witness-based test discovery |