Quint language and CLI reference — the expert on Quint syntax, operators, types, `basicSpells`, the toolchain (typecheck/run/test/verify), and how to read simulation and counterexample output. Use when writing or debugging the contents of a `.qnt` file, fixing a typecheck/parse error, looking up an operator or idiom, analyzing an invariant violation or counterexample trace, or optimizing state-space exploration. This is for working IN Quint at the language level — not for analyzing or running TLA+/TLC itself. For building a NEW model end-to-end from some source — including translating a TLA+ spec into Quint, or modeling code/requirements/an idea — use the quint-modeling skill, which owns that workflow and consults this reference for syntax. Keywords: quint, syntax, operators, typecheck, model checking, counterexample, basicSpells, CLI, specification language.
90%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
Passed
No findings from the security scan
The canonical home for this skill is quint-lang in crocoder-dev/shared-context-engineering
Quint is an executable specification language for complex systems, developed by Informal Systems. It compiles to TLA+ and supports simulation and model checking.
module MyProtocol {
// type aliases, constants, state, actions, properties
}Modules can import others:
import Voting.* // all definitions
import Voting(quorum) // specific definition
import Voting as V // namespace alias| Type | Description | Example |
|---|---|---|
int | Integers | -1, 0, 42 |
bool | Booleans | true, false |
str | Strings | "hello" |
Set[T] | Finite set | Set(1, 2, 3) |
List[T] | Ordered sequence | List(1, 2, 3) |
K -> V | Key-value map (type is K -> V, not Map[K, V]) | value: Map("a" -> 1) |
(T1, T2) | Tuple | (1, "x") |
{ f: T, g: U } | Record | { x: 1, ok: true } |
T | U | Sum (variant) | (use type alias) |
Type aliases:
type NodeId = int
type Phase = Idle | Propose | Vote | Commit // enum — prefer over string literals// 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 + 1const vs pure val: const is a module parameter bound at instantiation (import A(N = 3)); pure val is a fixed expression computed once. def vs val: def takes arguments; val does not.
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 val definitions and actions; they cannot be read in pure def.
Actions describe state transitions. They return bool — true if the action fires.
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:
var must be assigned in every action (use x' = x to leave unchanged).all { ... } — all sub-expressions must hold (conjunction). Guards are plain boolean expressions inside all { }.any { ... } — at least one must hold (disjunction); the REPL picks non-deterministically.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(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 bindingsList(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("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 → mapRecords group related fields into a named type. They are the primary tool for modelling structured state in 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,
}val n: NodeState = { phase: Idle, voted: false, log: List() }
n.phase // Idle
n.voted // false{ ...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 literalTLA+ 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:
Keep flat when:
Preferred (cohesive local state):
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: LocalStateAvoid for cohesive local state:
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:
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}),
}
}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)}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 (variants) represent a value that can be one of several distinct cases.
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:
val a: Action = Propose({ value: 1, proposer: 2 })Pattern-match with match, binding the payload:
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:
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 are a special case of sum types where each case has no additional data.
type Phase = Idle | Propose | Vote | Commit
var phase: Phase
if (phase == Propose) { ... }Before writing var 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 Id -> RecordType) | Flat only when concerns are truly independent |
| Do invariants relate them? | Invariant spans multiple fields of one entity | Invariant uses vars independently |
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 holdand { } and or { } are the same operators as all { } and any { } in actions — use whichever reads more naturally in context.
// 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 => qassume nodeCountPositive = N > 0
assume quorumMajority = 2 * quorum > NAn assume states a premise about constants, but it is not enforced — a violated assume
is silently ignored by quint typecheck, quint run, and quint verify (none of them flags
it). It is documentation, not a checked constraint. To actually check a condition on
constants, write a run test that asserts it (it executes and fails when the condition is
false):
run quorumAssumptionTest = all {
2 * quorum > N,
N > 0,
}Run it with quint test; the test fails (reporting which conjunct broke) if a constant
assignment violates the condition.
if (x > 0) "positive" else "non-positive"
val result = {
val doubled = x * 2
doubled + 1
}Prefer CLI commands (quint typecheck, quint run, quint test, quint verify) for all validation and execution tasks. Open the REPL (quint or quint -r spec.qnt::ModuleName) only when you need expression-level interaction the CLI does not provide.
Type inspection:
>>> :type myExpressionSplit 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)Main module (<protocol-name>.qnt):
init, actions, and safety invariantsstep must live in the main module — it is the entry point for quint run simulationmodule myProtocol in myProtocol.qntTest module (<protocol-name>_test.qnt):
import myProtocol.*)run tests and scenario witnesses invoked via quint test or quint runstep from the main module through the import--main to pass for quint runquint run must receive the module that owns the property being checked:
| Property location | Correct --main |
|---|---|
| Invariant defined in main module | main module name |
Witness / run test defined in test module | test module name (it imports step from main) |
The primitive's module_name field (set during indexing) always holds the correct value. Use it directly — do not derive from the filename.
Many useful operators are not built into Quint but are available in basicSpells.qnt, a standard library shipped with most Quint projects. Import it with:
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 Option. Any spec field typed Option[T] depends on this import. |
unwrap(o) | The value inside Some; undefined on None |
require(cond) | Blocks the action if cond is false (cleaner than bare all { cond, ... }) |
values(m) | Set of all values in map m |
transformValues(m, f) | New map with f applied to every value |
has(m, key) | True if key is bound in m |
getOrElse(m, key, default) | m.get(key) if present, otherwise default |
mapRemove(m, key) / mapRemoveAll(m, ks) | Copy of m without key (or without the set of keys ks) |
setRemove(s, e) / setAdd(s, e) | Copy of set s without / with element e |
find(s, f) / findFirst(l, f) | First element of set / list satisfying f, as Option |
max(i, j) / min(i, j) / abs(i) | Max / min of two integers; absolute value |
When you see a spec using Option, require, values, or transformValues without an import, it is relying on basicSpells — check whether the project includes it. (Less common operators live in a sibling rareSpells.qnt.)
Detailed references — read these when you need more than the quick reference above:
| File | Contents |
|---|---|
guidelines/operators.md | Complete operator reference: extended set/list/map operators, run/then/expect/reps for tests and witnesses, temporal fairness, q::debug |
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 |
guidelines/cli.md | Full CLI reference: quint run, quint test, quint verify flags, verbosity guide, reading output |
guidelines/patterns.md | 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 |
guidelines/tests.md | Writing and debugging tests: run/then/expect/reps/fail, nondeterministic tests, error location ≠ failure point, frame counting, REPL-first debugging |
guidelines/choreo.md | Choreo framework for distributed protocols: two-file split, choreo::cue pattern, .with_cue().perform() testing, witness-based test discovery |
crocoder-dev/shared-context-engineering
5bf1376
Canonical home
since Aug 28, 2026
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.