quint-modeling
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseQuint Modelling
Quint建模
Produce a Quint specification from whatever the user starts with. This skill owns the
shared modelling discipline (below) and routes to a flow-specific guideline for the
intake — the part that turns a particular kind of input into the understanding the shared
steps build on.
For language syntax, operators, the CLI, and , defer to the quint-lang
reference. This skill is about how to model, not Quint syntax.
basicSpells根据用户提供的初始内容生成Quint规格说明。本技能涵盖通用建模规范(如下所述),并会根据输入类型导向特定的流程指南——即从特定输入中提取信息,为后续通用步骤构建基础认知的环节。
关于语言语法、运算符、CLI及,请参考quint-lang参考文档。本技能聚焦于建模方法,而非Quint语法。
basicSpellsPick the flow
选择流程
Identify what the user is starting from, then read the matching guideline. The guideline
handles intake — extracting the system's state, operations, and assumptions from that
source. After intake, everyone converges on the Shared modelling spine below.
| Starting point | Flow | Guideline |
|---|---|---|
| Only an idea / informal proposal, built interactively with the user | from nothing | |
| A written requirements / functional-spec document | from requirements | |
| Source code (Rust, Go, TypeScript, …) | from code | |
| An existing TLA+ specification | from TLA+ | |
A finished | review | |
The first four are build flows: intake → the Shared modelling spine below. The review
flow is different — there is no new model to produce; you audit an existing spec against a
checklist and report findings. It does not use the spine; read and follow
it directly.
guidelines/review.mdIf the starting point is ambiguous (e.g. "a design doc with some pseudocode"), ask the user
which source is authoritative before picking — the intake differs materially.
确定用户的初始输入类型,然后阅读对应的指南。指南负责信息提取——从输入源中提取系统的状态、操作及假设。完成信息提取后,所有流程都会收敛到下方的通用建模主干流程。
| 初始输入类型 | 流程 | 指南 |
|---|---|---|
| 仅为交互式构思的想法/非正式提案 | 从零构建 | |
| 书面需求/功能规格文档 | 基于需求构建 | |
| 源代码(Rust、Go、TypeScript等) | 基于代码构建 | |
| 现有TLA+规格说明 | 基于TLA+构建 | |
待审计的已完成 | 审查流程 | |
前四种属于构建流程:信息提取 → 下方的通用建模主干流程。审查流程有所不同——无需生成新模型,只需对照清单审计现有规格并报告结果,不使用主干流程,请直接阅读并遵循。
guidelines/review.md若初始输入类型模糊(例如“包含伪代码的设计文档”),请先询问用户以确定权威输入源,再选择流程——不同输入源的信息提取方式差异显著。
Shared modelling spine
通用建模主干流程
Every flow lands here. Intake (flow-specific) produces an understanding of the system:
its entities, the operations they perform, the state each operation reads and writes, and the
system-model assumptions (communication, failures, time, participants). The spine turns that
understanding into a verified Quint spec.
Two principles hold throughout:
- Executable from the first line. Quint specs run immediately — you validate design
decisions as you make them, not at the end. Never build more than one step without
verifying the previous one: , then
quint typecheck. Typecheck is not enough — a spec can typecheck and still fail to execute, which defeats the whole point of an executable spec. The trap to watch for: aquint runnot given a value typechecks but cannot run —constfails withquint run. To make the spec runnable, instantiate the const in a concrete instance module —Uninitialized const— and run that module. (module spec_3 { import spec(C = Set(1,2,3)).* }documents a premise; it does not supply a value — see Step 1 and quint-lang'sassume.) After each addition, actually run it — if you can'tguidelines/simulations.mdthe module, it isn't done. Usequint runonly — neverquint run. This holds for the whole skill: building, iterating, reviewing, sanity-checking invariants — every step uses sampledquint verify, not oncequint run, not even as a final pass.quint verifyis exhaustive bounded model checking (Apalache): far slower (minutes to hours) and not part of the modeling workflow. It is a model-checking tool, documented in quint-lang (verify,guidelines/cli.md) — reach for it only when the user explicitly asks to model-check the spec, never on your own.guidelines/simulations.md - What, not how. Model observable state transitions and their effects. Abstract away implementation detail (serialization, memory management, retry plumbing). If a detail doesn't affect an invariant you care about, it doesn't belong in the spec.
所有流程最终都会进入此环节。流程特定的信息提取会生成对系统的认知:系统实体、实体执行的操作、每个操作读写的状态,以及系统模型假设(通信、故障、时间、参与者)。主干流程会将这些认知转化为经过验证的Quint规格。
全程遵循两大原则:
- 从第一行代码即可执行。Quint规格可立即运行——在做出设计决策时即可验证,无需等到全部完成。每完成一步都要验证前一步:先执行,再执行
quint typecheck。仅类型检查是不够的——规格可能通过类型检查但无法运行,这违背了可执行规格的核心意义。需要注意的陷阱:未赋值的quint run可通过类型检查但无法运行——const会因quint run失败。要使规格可运行,需在具体实例模块中实例化const——例如Uninitialized const——并运行该模块。(module spec_3 { import spec(C = Set(1,2,3)).* }用于记录前提,但不提供值——请参阅步骤1及quint-lang的assume。)每次添加内容后都要实际运行——若无法用guidelines/simulations.md运行模块,则说明未完成。仅使用quint run——绝不使用quint run。本技能的所有环节均遵循此规则:构建、迭代、审查、不变量 sanity检查——每一步都使用抽样的quint verify,绝不使用quint run,即使是最终环节也不例外。quint verify是 exhaustive有界模型检查工具(基于Apalache):速度慢得多(耗时数分钟至数小时),不属于建模工作流。它是模型检查工具,在quint-lang中有相关文档(verify、guidelines/cli.md)——仅当用户明确要求对规格进行模型检查时才使用,切勿自行使用。guidelines/simulations.md - 关注“是什么”而非“怎么做”。对可观察的状态转换及其影响进行建模,抽象掉实现细节(序列化、内存管理、重试机制等)。若某细节不影响你关心的不变量,则不应纳入规格。
Reference examples
参考示例
Two complete, runnable specs are bundled in , one for each of the two ways
components coordinate — by passing messages or by sharing state. Almost any system maps
onto one of them; pick by that question, not by domain label, and read the closer match as a
worked instance of the spine patterns (Step 2 state-shaping, Step 4 guarded actions, Step 5
witnesses + invariants) — to see how the pieces compose into a whole spec. (They are canonical
shapes to learn from, not the only valid ones.)
examples/| Example | Coordinates by… | Read it when modelling… |
|---|---|---|
| message passing ( | any distributed protocol where parties exchange messages — consensus, BFT, replication, atomic commit, leader election, reliable broadcast/gossip, request/response, a mempool. This is the default reference for distributed systems. Shows: grouped |
| shared state (no messages) | systems whose parts coordinate through common state rather than messages — mutexes/locks, token rings, shared registers, self-stabilization, anything where a process reads its neighbours/the environment directly. Shows: grouped-map state (Step 2), guarded |
If a system has both (e.g. message-passing nodes that also touch a shared ledger), start from
tendermint (the message-passing structure dominates) and add shared state as its own ,
per Step 2. Plain message soup () without the Choreo framework isn't a separate file —
Choreo's broadcast is soup underneath, so is also the reference for that shape (read
the state decls + a guarded transition; the wrapper is the only added layer).
varSet[Msg]tendermint/choreo::examples/| 示例 | 协作方式 | 适用建模场景 |
|---|---|---|
| 消息传递( | 任何通过消息交换协作的分布式协议——共识、BFT、复制、原子提交、 leader选举、可靠广播/ gossip、请求/响应、内存池。这是分布式系统的默认参考示例。展示内容:分组的 |
| 状态共享(无消息) | 通过公共状态而非消息协作的系统——互斥锁/锁、令牌环、共享寄存器、自稳定系统、任何进程直接读取邻居/环境状态的场景。展示内容:分组映射状态(步骤2)、受保护 |
若系统同时包含两种协作方式(例如既传递消息又访问共享账本的节点),请从tendermint开始(消息传递结构占主导),并按照步骤2的要求将共享状态作为独立添加。未使用Choreo框架的纯消息“池”()没有单独的示例文件——Choreo的广播底层就是消息池,因此也可作为该形式的参考示例(只需阅读状态声明及受保护转换;包装是唯一新增的层)。
varSet[Msg]tendermint/choreo::Step 1 — Separate concerns
步骤1 — 分离关注点
Partition the understanding into three layers. Keeping them apart is what makes a spec
testable; mixing them is the most common source of untestable specs.
- State machine — variables, initialization, transitions → ,
var, actionsinit - Functional logic — pure computations: guards, quorum checks, state updates →
pure def - Properties — what must always hold, what must be reachable, what must eventually happen →
invariants + witnesses, plus
valproperties for liveness (Step 5; quint-lang has the detail)temporal
Also pin down the system-model assumptions — they shape every invariant built on top.
Encode them as declarations. To actually check an assumption (e.g. a quorum
condition like ), write a test that asserts it — the keyword
documents a premise but is not enforced, so don't rely on it as a check (see quint-lang's
for why and the -test pattern). Each flow's intake fills this in from its own
source (interview, requirements doc, code, or TLA+); the checklist is the same regardless:
const2 * f < nrunassume## Assumerun| Concern | Questions to answer |
|---|---|
| Communication | How do the actors communicate — messages or shared state? If messages: reliable or lossy? ordered or unordered? broadcast or point-to-point? (The other shaping question — answer it right after actors; it picks the message medium in Step 2 and, with the actor count, settles the Choreo decision.) |
| Failures | Crash-stop? Crash-recovery? Byzantine? What fraction |
| Time | Synchronous? Asynchronous? Partial synchrony? Are timeouts modelled? |
| Actors / Participants | Who are the actors and roles, and what local state does each hold? (This is the most concrete intake question — answer it first; it feeds Step 2's |
If an assumption is wrong, every property built on it is wrong — surface them before building.
将对系统的认知划分为三层。保持分层是规格可测试的关键;混合分层是导致规格不可测试的最常见原因。
- 状态机——变量、初始化、转换 → 、
var、动作init - 功能逻辑——纯计算:守卫条件、法定人数检查、状态更新 →
pure def - 属性——必须始终成立的规则、必须可达的状态、最终必须发生的事件 → 不变量 + 见证者,以及用于活性的
val属性(步骤5;quint-lang有详细说明)temporal
同时明确系统模型假设——它们会影响所有基于此构建的不变量。将假设编码为**声明**。要实际检查假设(例如这样的法定人数条件),请编写**测试**来断言该假设——关键字仅用于记录前提,不强制执行,因此不要依赖它进行检查(请参阅quint-lang的了解原因及测试模式)。各流程的信息提取环节会从自身输入源(访谈、需求文档、代码或TLA+)填充这些内容,无论输入源是什么,检查清单都是相同的:
const2 * f < nrunassume## Assumerun| 关注点 | 需回答的问题 |
|---|---|
| 通信 | 参与者如何通信——消息传递还是状态共享?若为消息传递:可靠还是不可靠?有序还是无序?广播还是点对点?(这是另一个关键问题——在确定参与者后立即回答,它会决定步骤2的消息媒介,并结合参与者数量确定是否使用Choreo。) |
| 故障 | 崩溃停止?崩溃恢复?拜占庭故障?故障节点占总节点数 |
| 时间 | 同步?异步?部分同步?是否建模超时? |
| 参与者/角色 | 参与者和角色是谁,每个参与者持有哪些本地状态?(这是最具体的信息提取问题——首先回答;它直接为步骤2的 |
若假设错误,基于此构建的所有属性都会错误——请在开始构建前明确这些假设。
Step 2 — Shape the state
步骤2 — 塑造状态
Start from the two questions you answered in Step 1 — who the actors are and how they
communicate. Together they drive every structural decision this step makes. They are not just
background; they pick the shape, the message medium, and the framework:
- How many actors → the state shape. One actor → a single . N actors of the same kind → one
var s: Recordmap (the record describes one actor; see "Scaling to N" below). Distinct roles → either a field onId -> LocalState(e.g.LocalState) or separate maps. Get the actor count/roles right and the shape falls out.role: Coordinator | Participant - How actors communicate → the message medium. Messages → a separate for the medium, not crammed into
var: unorderedLocalStatesoup (the default), orSet[Message]ordered per-pair queues only when delivery order is load-bearing. No messages, coordination through common state → shared memory (no medium var; see... -> List[Message]). This is the "Communication shapes" choice detailed just below.examples/ewd426.qnt - Actors + communication → Choreo or not. Multiple actors coordinating by exchanging messages → default to Choreo (see "Default to Choreo" below). A single actor, or actors coordinating through shared state → plain Quint. So answer "who, how many, and how do they talk?" first, and all three decisions below are mostly settled before you write a type.
Group cohesive state into a record and let the functional layer operate on that record — this is
the recommended shape, because most of the time the variables that describe one entity are
cohesive, and grouping keeps the functional layer clean and the invariants readable. Keeping
variables flat is the deliberate exception, not a co-equal default:
- Group into a record when the fields are the local state of one entity (a node's phase + log + vote), are always passed together, or an invariant relates several of them. This is the usual case.
- Keep flat (separate s) only when the variables are genuinely independent concerns that change on their own, or the spec is small enough that grouping adds no clarity. (Some canonical examples, e.g.
var, keep per-field maps flat because the fields don't form an obvious unit.)ewd840
The cohesive case — one unit:
quint
type State = {
phase: str,
balances: NodeId -> int,
members: Set[NodeId],
}
var state: StateThe pure functions take and return ; actions assign the next-state record directly
(). No per-field reassembly.
apply…Statestate' = applyTransfer(state, …)Scaling to N instances — when there are N actors, the record describes one actor's
local state, and the variable becomes a map keyed by actor id. The pure functions are
unchanged — they still operate on a single ; only the wiring around them
changes:
LocalStatequint
type NodeId = int
type LocalState = { phase: str, balance: int, voted: bool }
var nodes: NodeId -> LocalState // per-actor cohesive state
var messages: Set[Message] // genuinely global concern → its OWN var, not inside LocalState| One instance | N instances | |
|---|---|---|
| state var | | |
| pure fns | | |
| action read | reads | |
| action write | | |
| one record literal | |
The guideline, stated once: group cohesive state into a record (one var for one unit, an
map for N), and keep genuinely independent concerns — the message soup, a shared
registry — as their own separate vars, never crammed into the per-instance record. Reach for flat
per-field vars only when the fields don't cohere into a unit.
Id -> LocalStateCommunication shapes (decide alongside the state shape):
- Message soup — , processes receive any in-flight message. The default for asynchronous protocols; use it unless ordering genuinely matters.
var network: Set[Message] - Shared memory — no message medium; processes read/write common state directly. See the
bundled (a token ring where each node reads its neighbours).
examples/ewd426.qnt - Ordered per-pair queues — with head/tail ops. Use only when delivery order is semantically required (e.g. logical-clock causality); it is more expensive to model-check than soup. The corpus spec
... -> List[Message]is the canonical example — prefer soup unless you can name why ordering is load-bearing.LamportMutex
**从步骤1中回答的两个问题入手——参与者是谁以及他们如何通信。这两个问题共同驱动本步骤的所有结构决策。**它们并非背景信息,而是决定状态形状、消息媒介及框架的关键:
- 参与者数量 → 状态形状。单个参与者 → 单个。N个同类参与者 → 一个
var s: Record映射(记录描述单个参与者的状态;请参阅下方“扩展到N个实例”)。不同角色 → 要么在Id -> LocalState中添加字段(例如LocalState),要么使用单独的映射。只要正确确定参与者数量/角色,状态形状就会自然确定。role: Coordinator | Participant - 参与者通信方式 → 消息媒介。消息传递 → 为媒介创建单独的,不要塞进
var:默认使用无序的LocalState消息池,仅当交付顺序至关重要时才使用Set[Message]的点对点有序队列。无消息传递,通过公共状态协作 → 共享内存(无需媒介var;请参阅... -> List[Message])。这是下方详细说明的“通信形状”选择。examples/ewd426.qnt - 参与者 + 通信方式 → 是否使用Choreo。多个参与者通过消息传递协作 → 默认使用Choreo(请参阅下方“默认使用Choreo”)。单个参与者,或通过状态共享协作的参与者 → 使用纯Quint。因此请先回答“谁、数量多少、如何通信?”这三个问题,下方的三个决策基本就已确定,无需编写类型定义。
将内聚状态分组为记录,让功能层操作该记录——这是推荐的形状,因为大多数情况下描述一个实体的变量是内聚的,分组可保持功能层简洁、不变量可读性强。保持变量扁平化是刻意的例外,而非同等默认选择:
- 分组为记录的场景:字段是一个实体的本地状态(节点的阶段+日志+投票)、始终一起传递,或者某个不变量关联多个字段。这是常见场景。
- 保持扁平化(单独)的场景:变量是真正独立的关注点,各自独立变化,或者规格足够小,分组不会增加清晰度。(一些标准示例,例如
var,保持每个字段的映射扁平化,因为这些字段无法形成明显的单元。)ewd840
内聚场景——单个单元:
quint
type State = {
phase: str,
balances: NodeId -> int,
members: Set[NodeId],
}
var state: Stateapply…Statestate' = applyTransfer(state, …)扩展到N个实例——当有N个参与者时,记录描述单个参与者的本地状态,变量变为以参与者ID为键的映射。纯函数保持不变——它们仍操作单个;仅周围的 wiring 发生变化:
LocalStatequint
type NodeId = int
type LocalState = { phase: str, balance: int, voted: bool }
var nodes: NodeId -> LocalState // 每个参与者的内聚状态
var messages: Set[Message] // 真正的全局关注点 → 单独的var,不放在LocalState中| 单个实例 | N个实例 | |
|---|---|---|
| 状态变量 | | |
| 纯函数 | | |
| 动作读取 | 读取 | |
| 动作写入 | | |
| 单个记录字面量 | |
统一规则:将内聚状态分组为记录(单个单元对应一个var,N个单元对应映射),将真正独立的关注点——消息池、共享注册表——作为单独的var,绝不要塞进每个实例的记录中。仅当字段无法内聚为单元时,才使用扁平化的每个字段var。
Id -> LocalState通信形状(与状态形状一起决定):
- 消息池——,进程接收任何在途消息。异步协议的默认选择;除非排序真正重要,否则使用此方式。
var network: Set[Message] - 共享内存——无消息媒介;进程直接读写公共状态。请参阅附带的****(每个节点读取邻居状态的令牌环)。
examples/ewd426.qnt - 点对点有序队列——,包含头部/尾部操作。仅当交付顺序在语义上是必需的(例如逻辑时钟因果关系)时才使用;它的模型检查成本比消息池更高。标准示例
... -> List[Message]是典型场景——除非能说明排序至关重要的原因,否则优先选择消息池。LamportMutex
Default to Choreo for distributed protocols
分布式协议默认使用Choreo
For any distributed, message-passing protocol with N processes (consensus, BFT, replication,
multi-phase commit, broadcast/gossip, leader election), default to the Choreo framework — and
make a deliberate, justified decision if you are not going to. This isn't a stylistic toss-up:
Choreo's listen → react structure and its clean split between local-state updates and network
effects (the pattern) actively push you toward a better-organized, more reviewable
spec than a hand-rolled + message-soup . The default expectation is
Choreo; plain Quint for a message-passing protocol is the thing that needs a reason.
choreo::cueNodeId -> LocalStatestepThe one legitimate reason to decline: on a small protocol, Choreo's scaffolding (the type
boilerplate — //effects — and framework wiring) can add more ceremony
than the spec's structure is worth, making it materially more verbose for little gain. That's a
real judgment call — but it is the only routine exception, and even then lean toward Choreo.
Two structural cases also fall outside Choreo: a single actor (no inter-process messaging to
choreograph) and shared-memory coordination (no message medium at all → plain Quint, see
).
LocalContextTransitionexamples/ewd426.qntNot a reason: "still exploring the design." You can explore in Choreo, and switching frameworks
later is expensive — so don't defer the decision on those grounds.
This is a structural fork — decide it before writing logic. If you intend to go plain Quint for
a protocol that would otherwise be a Choreo candidate, say so to the user, name the
verbosity/benefit tradeoff, and get agreement (fold this into the type-sketch sign-off below).
When Choreo fits, the per-process state still follows the grouping rule above — Choreo just
supplies the messaging and handler scaffolding around it. See
for the framework's API and patterns, and
for a complete, runnable Choreo spec — real BFT consensus
demonstrating the listen/act split, //
invariants, and -based tests in a separate (run via
).
../quint-lang/guidelines/choreo.mdexamples/tendermint/choreo::cueagreementvalidityaccountabilitywith_cuetendermintTest.qnt--main=validDecide grouping (and Choreo-or-not) with the user and get explicit approval on the type
sketch before writing any logic (Step 3). Typecheck the types + declarations first;
this is the highest-leverage review point — a wrong shape is expensive to unwind later.
var对于任何包含N个进程的分布式消息传递协议(共识、BFT、复制、多阶段提交、广播/gossip、 leader选举),默认使用Choreo框架——如果不使用,请做出明确且合理的决策。这并非风格选择:Choreo的监听→响应结构,以及本地状态更新与网络效果的清晰分离(模式),会主动引导你构建组织更良好、更易于审查的规格,而非手动实现的 + 消息池。默认预期是使用Choreo;对于消息传递协议,使用纯Quint需要理由。
choreo::cueNodeId -> LocalStatestep唯一合理的不使用理由:在小型协议中,Choreo的脚手架(类型样板——//effects——及框架wiring)会增加不必要的繁琐,导致规格明显更冗长,却几乎没有收益。这是一个真正的判断问题——但这是唯一常规例外,即使如此也应倾向于使用Choreo。还有两种结构场景也不适用Choreo:单个参与者(无需编排进程间消息传递)和共享内存协作(无消息媒介→使用纯Quint,请参阅)。
LocalContextTransitionexamples/ewd426.qnt不构成理由:*“仍在探索设计。”*你可以在Choreo中探索设计,之后切换框架成本很高——因此不要以此为由推迟决策。
这是一个结构性分支——编写逻辑前必须决定。如果对于原本适合Choreo的协议,你打算使用纯Quint,请告知用户,说明冗长性/收益的权衡,并获得同意(将此纳入下方的类型草图签署环节)。
当Choreo适用时,每个进程的状态仍遵循上述分组规则——Choreo只是在其周围提供消息传递和处理程序脚手架。请参阅了解框架的API和模式,以及****——一个完整的可运行Choreo规格,展示了真实BFT共识中的监听/动作分离、//不变量,以及单独中基于的测试(通过运行)。
../quint-lang/guidelines/choreo.mdexamples/tendermint/choreo::cueagreementvalidityaccountabilitytendermintTest.qntwith_cue--main=valid与用户一起决定分组(及是否使用Choreo),并在编写任何逻辑前获得对类型草图的明确批准(步骤3)。先对类型+声明进行类型检查;这是最高效的审查环节——错误的形状后期修正成本很高。
varStep 3 — Write the functional logic (pure functions)
步骤3 — 编写功能逻辑(纯函数)
Implement each operation's logic as s — they take the state record and arguments and
return plain values, so you can exercise them in the REPL with hand-built states. Split the two
distinct questions a transition answers:
pure def- Can it happen? — a guard (the precondition).
pure def … : bool - What's the resulting state? — a that computes the next state, assuming the guard holds.
pure def … : State
quint
pure def canTransfer(s: State, from: NodeId, recipient: NodeId, amount: int): bool =
amount > 0 and from != recipient and s.balances.get(from) >= amount
pure def applyTransfer(s: State, from: NodeId, recipient: NodeId, amount: int): State =
{ ...s, balances: s.balances.setBy(from, b => b - amount)
.setBy(recipient, b => b + amount) }Write signatures first (stub the bodies), typecheck, then fill in one function at a time and
exercise each in the REPL with a hand-built state before moving on. If the output surprises
you, the function has a bug — fix it before wiring it into the state machine.
Keep the guard separate from the update because they are separate facts about the operation: one says when it is allowed, the other says what it does. The action (Step 4) uses the guard to decide whether it fires at all. A singlereturningpure def— handing back the unchanged state when the guard fails — fuses the two and pushes the action toward a no-op branch; prefer the split. (If a failed operation is itself something the system observes and reacts to, model that failure as its own guarded action, not as a fallback.){ success, newState }
将每个操作的逻辑实现为——它们接收状态记录和参数并返回纯值,因此你可以在REPL中使用手动构建的状态测试这些函数。拆分转换需要回答的两个不同问题:
pure def- 是否允许执行?——守卫条件(前置条件)。
pure def … : bool - 执行后的状态是什么?——,计算下一个状态,假设守卫条件成立。
pure def … : State
quint
pure def canTransfer(s: State, from: NodeId, recipient: NodeId, amount: int): bool =
amount > 0 and from != recipient and s.balances.get(from) >= amount
pure def applyTransfer(s: State, from: NodeId, recipient: NodeId, amount: int): State =
{ ...s, balances: s.balances.setBy(from, b => b - amount)
.setBy(recipient, b => b + amount) }先编写签名(存根实现),进行类型检查,然后逐个填充函数体,并在编写下一个函数前,在REPL中使用手动构建的状态测试每个函数。如果输出不符合预期,说明函数存在bug——在将其连接到状态机前修复。
将守卫条件与更新逻辑分离,因为它们是关于操作的两个独立事实:一个说明何时允许执行,另一个说明执行后会发生什么。动作(步骤4)使用守卫条件决定是否触发。单个返回pure def——当守卫条件失败时返回不变的状态——会将两者融合,并导致动作倾向于无操作分支;因此优先选择拆分。(如果失败的操作本身是系统需要观察并响应的事件,请将该失败建模为单独的受保护动作,而非作为回退。){ success, newState }
Step 4 — Wire the state machine (guarded actions)
步骤4 — 连接状态机(受保护动作)
An action puts the guard and the update together in one : the guard gates whether the
action fires, the update sets the next state. When the guard is false the action is disabled —
it does not fire and does not change anything. No flag, no no-op branch.
all { ... }successquint
action init: bool = all {
state' = { phase: "idle",
balances: NODES.mapBy(_ => INITIAL_BALANCE), // pre-populate every map
members: NODES },
}
action transferAction(from: NodeId, recipient: NodeId, amount: int): bool = all {
canTransfer(state, from, recipient, amount), // guard — disables the action when false
state' = applyTransfer(state, from, recipient, amount),
}A disabled action is the faithful model of "this cannot happen in this state." Resist adding a
blanket / fallback to keep things moving — that puts a transition in the
model that the real system can't take, which is a modeling inaccuracy regardless of what you do
with the spec afterward.
elseunchanged_allAn action and a cannot share a name. Pre-populate every map in — on an
absent key is undefined behavior. See quint-lang for the full list of such gotchas.
pure definit.get()动作在中将守卫条件和更新逻辑结合在一起:守卫条件决定动作是否触发,更新逻辑设置下一个状态。当守卫条件为false时,动作被禁用——不会触发,也不会改变任何状态。无需标志,无需无操作分支。
all { ... }successquint
action init: bool = all {
state' = { phase: "idle",
balances: NODES.mapBy(_ => INITIAL_BALANCE), // 预填充每个映射
members: NODES },
}
action transferAction(from: NodeId, recipient: NodeId, amount: int): bool = all {
canTransfer(state, from, recipient, amount), // 守卫条件——为false时禁用动作
state' = applyTransfer(state, from, recipient, amount),
}被禁用的动作是“在该状态下无法执行此操作”的忠实模型。不要添加通用的/回退来推进流程——这会在模型中添加真实系统无法执行的转换,无论后续如何使用规格,这都是建模不准确的表现。
elseunchanged_all动作和不能同名。在中预填充每个映射——对不存在的键调用是未定义行为。请参阅quint-lang了解此类陷阱的完整列表。
pure definit.get()Step 5 — Properties: witnesses first, then invariants
步骤5 — 属性:先见证者,后不变量
Witnesses first. A witness names a target state and asks whether it is reachable — write the
predicate positively (the state you hope to reach) and pass it to , which
reports how many sampled traces reached it. Add one per major action: a witness reached in 0
traces means the action is dead (its precondition can never hold), so fix that before spending
effort on safety.
quint run --witnessesquint
// the state you want to be reachable — written plainly, no negation
val someBalanceChanged: bool =
state.members.exists(n => state.balances.get(n) != INITIAL_BALANCE)quint run spec.qnt --witnesses someBalanceChanged --max-steps 10
→ someBalanceChanged was witnessed in 90 trace(s) out of 100 explored (90.00%)A non-zero count means the state is reachable; 0% means it never happened — investigate.
There is a second, complementary form: write the negation as an invariant —
— so a reported "violation" is an execution that reaches the state. It can't batch with the other
invariants and you read a "violation" as success, but it gives you something cannot: an
actual trace that reaches the state. Pick by what you need — for the routine
reachability + coverage check, the negated-invariant form when you want to see the path (to
understand or document how the state is reached, or to debug a witness that fires unexpectedly).
val w = not(target)--witnesses--witnessesThen invariants — the safety properties that must hold in every reachable state:
quint
val noNegativeBalances: bool =
state.members.forall(n => state.balances.get(n) >= 0)Invariants and witnesses run together in one pass: .
quint run --invariant noNegativeBalances --witnesses someBalanceChangedWhen a source yields many candidate properties and you need to prioritize verification effort,
the from-requirements flow describes a High/Medium/Low impact-scoring aid ();
it applies wherever you're modelling against stated requirements.
guidelines/from-requirements.md(Witness vs invariant interpretation, fairness, and temporal properties are detailed in quint-lang's
. is state evidence — phrase such a target so only
an action can make it true, since a predicate that already holds at is "reached" at step 0 and
witnesses nothing. To confirm a specific action fired when several could reach the same state, use a
per-action flag — quint-lang's §7 covers it and how to debug a witness
that never fires.)
guidelines/simulations.mdsomeBalanceChangedinitvarguidelines/patterns.md先见证者。见证者指定目标状态,并询问该状态是否可达——正向编写谓词(你希望达到的状态),并将其传递给,该命令会报告有多少抽样轨迹到达了该状态。为每个主要动作添加一个见证者:如果见证者的轨迹数为0,说明该动作是死的(其前置条件永远无法满足),因此在投入精力处理安全属性前修复该问题。
quint run --witnessesquint
// 你希望可达的状态——直接编写,不要否定
val someBalanceChanged: bool =
state.members.exists(n => state.balances.get(n) != INITIAL_BALANCE)quint run spec.qnt --witnesses someBalanceChanged --max-steps 10
→ someBalanceChanged在100条探索轨迹中被见证了90条(90.00%)非零计数表示状态可达;0%表示从未达到——请调查原因。
还有一种互补形式:将目标状态的否定编写为不变量————这样报告的“违规”就是到达该状态的执行轨迹。它不能与其他不变量批量运行,并且你需要将“违规”视为成功,但它能提供无法提供的内容:到达该状态的实际轨迹。根据需求选择——用于常规可达性+覆盖检查,否定不变量形式用于查看路径(了解或记录如何到达该状态,或调试意外触发的见证者)。
val w = not(target)--witnesses--witnesses然后是不变量——必须在所有可达状态中成立的安全属性:
quint
val noNegativeBalances: bool =
state.members.forall(n => state.balances.get(n) >= 0)不变量和见证者可在一次运行中同时检查:。
quint run --invariant noNegativeBalances --witnesses someBalanceChanged当输入源提供大量候选属性,而你需要优先验证工作时,基于需求的流程描述了高/中/低影响评分辅助工具();该工具适用于任何基于既定需求建模的场景。
guidelines/from-requirements.md(见证者与不变量的解释、公平性及时态属性在quint-lang的中有详细说明。是状态证据——请将此类目标表述为只有动作才能使其为真,因为在时已成立的谓词会在步骤0“到达”,无法见证任何内容。要确认多个动作中哪个触发了状态变化,请使用每个动作对应的标志——quint-lang的第7节涵盖了此内容及如何调试从未触发的见证者。)
guidelines/simulations.mdsomeBalanceChangedinitvarguidelines/patterns.mdStep 6 — Compose step
and simulate
step步骤6 — 组合step
并模拟
stepCompose the actions in and stress the model with random walks.
any { ... }quint
action step: bool = {
nondet from = NODES.oneOf()
nondet dst = NODES.oneOf() // `dst`, not `to` (a built-in) — see quint-lang
any {
transferAction(from, dst, 10),
// other actions…
}
}- Check witnesses with — expect a non-zero trace count (reachable); 0% means a dead action.
quint run --witnesses - Check invariants with (sampled) — expect no violation; read counterexample traces step by step when one appears. Use
quint runfor this, notquint run(see the "Executable from the first line" principle —quint verifyis only for when the user explicitly asks to model-check).verify
在中组合动作,并通过随机游走测试模型。
any { ... }quint
action step: bool = {
nondet from = NODES.oneOf()
nondet dst = NODES.oneOf() // 使用`dst`而非`to`(`to`是内置关键字)——请参阅quint-lang
any {
transferAction(from, dst, 10),
// 其他动作…
}
}- 使用检查见证者——预期轨迹数非零(可达);0%表示动作已死。
quint run --witnesses - 使用(抽样)检查不变量——预期无违规;若出现违规,请逐步阅读反例轨迹。使用
quint run进行此操作,不要使用quint run(请参阅“从第一行代码即可执行”原则——quint verify仅用于用户明确要求模型检查的场景)。verify
A guarded model can reach a state where nothing is enabled
受保护模型可能进入无动作可用的状态
Because actions are guarded (Step 4), the model can reach a state with no enabled action. The
trap during a build: does not detect deadlocks — it stops early and prints with a trace shorter than . A short trace is the only hint, and the
run reads as success, so watch trace length when you expect the protocol to keep going. (For the
record, reports a deadlock outright — — but that's a
model-checking step, only in play if the user explicitly asks for it; don't switch to just
to chase a suspected deadlock mid-build — inspect the short trace with instead.)
quint run[ok] No violation found--max-stepsquint verifyFound a deadlockverifyquint runWhether reaching such a state is correct depends on the system: one that should keep serving must
never get stuck, while one that genuinely finishes (everyone committed, nothing left to do) is
supposed to end in a terminal state. The tool can't tell these apart — only you know which you
intended. (This is the other reason Step 4 avoids the no-op fallback: a blanket
lets every trace extend forever, so the model can never reach — or reveal — such a state.)
unchanged_allA note on bounding. (how deep a run explores) is not a modeling decision — it's
always there. Whether the model itself has a bounded state space is: some protocols are
terminating by construction (a one-shot commit reaches a final state and stops), and modeling
them that way is correct — reaching a terminal state is the design, not a deadlock. You may also
bound an otherwise-unbounded model deliberately to shrink the state space, but never with a no-op
escape hatch (Step 4) — bound it by modeling the real terminal states, not by a fake step.
--max-stepsTests ( definitions) go in a separate file that imports the spec — keep
the main module free of test scenarios. Build incrementally: typecheck after every addition,
simulate after every action.
run*_test.qnt由于动作是受保护的(步骤4),模型可能进入无可用动作的状态。构建过程中的陷阱:不会检测死锁——它会提前停止并打印,且轨迹长度短于。短轨迹是唯一的提示,且运行结果显示为成功,因此当你预期协议会持续运行时,请关注轨迹长度。(需要说明的是,会直接报告死锁————但这是模型检查步骤,仅当用户明确要求时才使用;不要在构建过程中为了追查疑似死锁而切换到——请改用检查短轨迹。)
quint run[ok] No violation found--max-stepsquint verifyFound a deadlockverifyquint run进入此类状态是否正确取决于系统:应持续提供服务的系统绝不能卡住,而真正会结束的系统(所有参与者已提交,无剩余操作)应该进入终端状态。工具无法区分这两种情况——只有你知道预期的结果。(这也是步骤4避免无操作回退的另一个原因:通用的会让所有轨迹无限延长,因此模型永远无法到达——或揭示——此类状态。)
unchanged_all关于边界的说明。(运行探索的深度)不是建模决策——它始终存在。而模型本身是否有有界状态空间是建模决策:一些协议本质上是终止的(一次性提交会到达最终状态并停止),按此建模是正确的——到达终端状态是设计预期,而非死锁。你也可以故意限制原本无界的模型以缩小状态空间,但绝不要使用无操作逃逸舱(步骤4)——通过建模真实的终端状态来限制,而非添加虚假步骤。
--max-steps测试(定义)应放在单独的文件中,该文件导入主规格——保持主模块不含测试场景。增量构建:每次添加内容后进行类型检查,每次添加动作后进行模拟。
run*_test.qntStep 7 — Self-review, then hand off the spec
步骤7 — 自我审查,然后交付规格
Before writing the handoff record, do a quick pass over your own spec against the same bar a
reviewer would apply — catching these now is cheaper than having them bounce back. Most you've
already followed while building; this is the closing check that they all hold together:
- Guarded actions — no business logic in an action beyond the guard and assignments; it lives
in s, and the guard is lifted into the action so it's disabled when it can't fire (Step 4).
pure def - Witness per major action — every action in has a witness, and each one is reached in
step0 traces under, not dead at 0 traces (Step 5).quint run --witnesses - At least one protocol-level invariant — a real safety property (no two leaders, conservation,
agreement), not just a type/bounds check; and every invariant references a .
var - assigns every
initand every action assigns everyvar(a missingvaris a silent stutter bug); every map is pre-populated (Step 4).x' = x - Right abstraction — IDs are opaque (no string manipulation), messages are a unless ordering is the property, no serialization/retry/memory detail leaked in.
Set
If a spec is large or being handed to someone else, run the full audit in
(the C1–C6 / R1–R2 checklist with a report) — reach for it when a self-pass isn't enough.
guidelines/review.mdA finished spec needs a short record so the next reader knows its scope and can trust it.
Produce one (as a comment block, a , or whatever the flow prefers) covering:
README.md- What it covers — which modules/protocols are modelled, which assumptions (declarations, with
const-test checks for the ones that must hold) are encoded, which properties are verified.run - What it does NOT cover — implementation detail left out on purpose, edge cases excluded (and why), properties still to add. This honesty note is what stops the spec from being over-trusted.
- When to update it — for a spec that grounds an artifact (code, a TLA+ source), update the spec and re-verify before changing the artifact.
The spec is the ground truth. Never edit the spec to match broken code. If the spec is
wrong, stop and discuss with the user — that is the highest-leverage review point.
Each flow adds its own format detail (source-file correspondence for code, a translation
README with recorded output for TLA+, a comment block for an interactive build).
quint run在编写交付记录前,对照审查者的标准快速检查自己的规格——现在发现问题比后续返工成本更低。大多数规则在构建过程中已遵循;这是确保所有规则协同工作的最终检查:
- 受保护动作——动作中除守卫条件和赋值外无业务逻辑;业务逻辑位于中,守卫条件被提升到动作中,以便在无法触发时禁用动作(步骤4)。
pure def - 每个主要动作对应一个见证者——中的每个动作都有见证者,且在
step下的轨迹数>0,而非0轨迹的死动作(步骤5)。quint run --witnesses - 至少一个协议级不变量——真实的安全属性(无双leader、守恒、一致性),而非仅类型/边界检查;且每个不变量都引用。
var - 为每个
init赋值,每个动作都为每个var赋值(缺少var是静默的停滞bug);每个映射都已预填充(步骤4)。x' = x - 正确的抽象——ID是不透明的(无字符串操作),消息是(除非排序是要验证的属性),无序列化/重试/内存细节泄露。
Set
若规格较大或要交付给他人,请运行中的完整审计(包含C1–C6 / R1–R2清单及报告)——当自我检查不够时使用此方法。
guidelines/review.md完成的规格需要简短的记录,以便后续读者了解其范围并信任它。生成一份记录(作为注释块、或流程偏好的格式),涵盖:
README.md- 涵盖内容——建模了哪些模块/协议,编码了哪些假设(声明,包含对必须成立的假设的
const测试检查),验证了哪些属性。run - 未涵盖内容——故意省略的实现细节,排除的边缘情况(及原因),仍需添加的属性。这份诚实的说明可防止规格被过度信任。
- 更新时机——对于作为工件(代码、TLA+源)基础的规格,在更改工件之前更新规格并重新验证。
**规格是基准事实。绝不要为了匹配有问题的代码而修改规格。**若规格错误,请停止并与用户讨论——这是最高效的审查环节。
各流程会添加自己的格式细节(代码的源文件对应关系、包含记录的输出的转换README、交互式构建的注释块)。
quint runConventions
约定
- /
NodeIdfor per-actor state, andLocalStatefor the participant set (theNODESthe model ranges over); descriptive type names after the protocol (Set[NodeId],VoteRequest) notPrepareMsg,Msg1.Msg2 - Small domains: actors (or
N = 3for first exploration) and value ranges likeN = 2are enough for most safety properties and keep simulation cheap. Use symbolic IDs (0..10orint) for participants.str - Message soup: store sent messages in one and don't model delivery order unless ordering is what you're verifying.
Set - Abstract time: when the protocol has a notion of time or progress, model it as an integer
round/epoch counter (), not wall-clock time or timer mechanics — a timeout is just the round advancing. Exception: model timing explicitly only when the timing bound itself is what you're verifying.
round: int - Start with one action: get + one action + one witness working before adding the rest. For N actors, model one actor first, verify, then generalize to the map — never wire up all N at once.
init - Test pure functions in isolation in the REPL with hand-built states; that catches logic bugs before they get tangled into the state machine.
For everything syntactic — operators, undefined-behavior rules, , the CLI flags —
consult the quint-lang reference rather than reproducing it here.
basicSpells- 使用/
NodeId表示每个参与者的状态,使用LocalState表示参与者集合(模型覆盖的NODES);使用协议相关的描述性类型名称(Set[NodeId]、VoteRequest)而非PrepareMsg、Msg1。Msg2 - 小型领域:个参与者(或首次探索时
N = 3),值范围如N = 2足以验证大多数安全属性,并保持模拟成本低廉。使用符号ID(0..10或int)表示参与者。str - 消息池:将已发送的消息存储在一个中,除非要验证排序,否则不要建模交付顺序。
Set - 抽象时间:当协议有时间或进度概念时,将其建模为整数轮次/纪元计数器(),而非 wall-clock时间或计时器机制——超时仅表示轮次推进。例外情况:仅当要验证时间边界本身时,才显式建模时间。
round: int - 从一个动作开始:在添加其他动作前,先让+一个动作+一个见证者正常工作。对于N个参与者,先建模一个参与者,验证后再推广到映射——不要同时连接所有N个参与者。
init - 在REPL中单独测试纯函数,使用手动构建的状态;这会在逻辑bug卷入状态机前发现它们。
所有语法相关内容——运算符、未定义行为规则、、CLI标志——请查阅quint-lang参考文档,而非在此处重复。
basicSpells