Community写作与编辑github.com

可读的无I/O状态机技能

用于对无I/O(sans-IO)Rust协议核心进行行为保持的重构。适用于像quinn-proto、str0m这样的crate,或解析器、握手、帧处理等核心。

O状态机技能 是什么?

该技能专门针对无I/O(sans-IO)架构的Rust crate进行重构,目标是让状态机的代码更易读、更易维护。它通过一系列小步骤,在保持行为完全不变的前提下,将散乱的标志位和`Option`组合替换为清晰的状态枚举,将复杂的handler拆分为命名的转换函数,并对输出和定时器逻辑进行整理。支持多种规模的重构,从单个handler到整个模块重组,甚至长文件拆分为模块。该技能提供了一套完整的工作流:先建立安全网(运行项目现有测试),然后绘制状态机映射(列出实体、输入、输出、不变量和怪癖),接着基于模式库选择最有效的更改,逐步应用,最后验证公共API并输出总结。它适用于Claude Code、Cursor、Codex等AI Agent平台,帮助开发者快速理清状态机的状态、转换、输出和定时这四个核心问题。

兼容平台✓Claude Code~Codex CLI~Cursor
npx skills add https://github.com/FalkWoldmann/skills/tree/HEAD/sansio-readability

Installed? Explore more 写作与编辑 skills: steipete/notion, langchain-ai/langchain, bytedance/podcast-generation · View all 6 →

在你喜欢的 AI 中提问

打开一个已预加载此 Agent Skill 的新对话。

文档

可读的无I/O状态机

无I/O库是一个将I/O抽离出来的状态机。调用者向其提供输入(handle_*,每个都带now),取出其输出(poll_transmit、poll_event、poll_timeout),并拥有套接字和定时器。只有人们能够理解它的逻辑,这样的代码才值得存在:无论是下一位维护者、驱动的作者,还是对照RFC进行审查的人。他们每个人都应该能快速回答四个问题:

  1. 状态:每个长期存在的实体(连接、查询、流、会话)可以处于哪些状态,每个状态下存在哪些数据?
  2. 转换:哪些输入使它从一个状态转换到另一个状态,每个转换发生在代码的哪个位置?
  3. 输出:每个转换排队了什么(传输、事件、写入),顺序如何?
  4. 时间:它什么时候需要被唤醒,哪个规则设定了每个截止时间?

你的工作是让这些答案在类型和代码中变得可见,同时让状态机保持完全相同的功能。你编辑工作树,并以总结结束。

<skill> 下面指的是该技能的基础目录,即包含此SKILL.md的文件夹。

基本规则

  • 行为保持完全相同。 在无I/O crate中,行为涵盖看起来像是表面功夫的东西:

    • 输出排队的顺序
    • 映射的迭代顺序(BTreeMap或插入顺序的Vec决定谁先谁后)
    • poll_timeout返回的即时时刻
    • 是否发出空的写入或无操作事件
    • 实体失败或关闭时的错误
    • 你更改字段的#[derive(Debug)]类型的输出(如果快照、日志断言或错误消息打印了它)

    通过公共API的测试以及每个快照都必须保持不变。如果某个测试需要编辑,说明那个步骤改变了行为:撤销它。crate内部命名私有项的单元测试可能需要与代码相同的机械编辑(变为变体的标志、重命名的辅助函数)。只做那个编辑,保持每个断言检查的内容,并在总结中列出该测试。

  • 不要改动公共API。 保持pub项的名称、签名、变体和字段不变,因为驱动程序和下游crate依赖于它们(以及语义化版本)。pub(crate)和私有项目是可以修改的。将API改进作为建议放在总结中。如何检查这一点见步骤5。

  • 不要为了可读性牺牲性能。 不要在每次输入(每个数据包、消息或查询)的路径上引入新的克隆、分配或对映射的多余遍历。mem::replace或mem::take是免费的;克隆一个Vec来绕过借用检查则不是。

  • 小步骤工作。 每个步骤都要能构建并通过检查,这样失败的检查就能指向一个更改,并且diff可以逐块审查。

  • 遵循项目规范。 在编辑之前,阅读其代理和贡献者文档(AGENTS.md、CLAUDE.md、CONTRIBUTING)、lint配置和CI工作流。匹配周围的注释密度和惯用风格。如果项目将文档与代码保持同步(生命周期表、将行映射到函数的注释、规范覆盖表),在同一个步骤中更新它们。

根据更改调整工作量

安全网的存在是为了捕获现有测试可能遗漏的行为更改,因此要根据你计划更改的大小来调整:

  • 小。 一个handler或几个辅助函数,不改变哪些字段持有状态的字段。项目检查在之前和之后各运行一次,并跳过下面标记为大的所有内容。在总结中,只映射你触及的实体。
  • 中。 一个模块中的多个转换或输出辅助函数。在每个步骤之前和结束时运行检查。仅在现有测试没有固定你所更改路径的确切输出时,并且仅在这些路径上构建差分测试工具。不需要模糊测试、额外的属性测试用例或基准测试:项目自身在其正常设置下的检查就是这里合适的权重。
  • 大。 用阶段枚举替换标志,将状态字段的许多写入点合并到转换中,大多数handler被重塑。使用整个工作流。

在映射(步骤2)之后决定大小。如果更改中途变大,提升一个等级并在总结中说明。

工作流

1. 设置安全网

在第一次编辑之前执行此操作,以便后续每个失败都是你的,而不是预先存在的。

  • 记录基准。 运行 git status 并记录基准提交:之后的每次比较都针对它。如果你计划触及的文件已经有未提交的更改,请在将你的编辑混入其中之前告知用户。如果你继续在其基础上进行,请使用 git stash create 作为基准:它会将它们记录为一个提交而不触及工作树。
  • 修复一个检查命令。 使用项目自身的命令(如果有的话)(CI工作流、justfile、xtask、其代理文档中的命令)。否则使用捆绑的门控:<skill>/scripts/check.sh <repo> default all。列出CI使用的特性集,例如 default all none tls。门控会检查格式但不修改,然后为每个特性集运行clippy和测试,如果任何操作失败则退出非零代码。使用 CHECK_PACKAGES="-p <crate> -p <dependent>" 将其范围限制在你更改的包及其依赖它的成员,最后对整个工作空间运行一次。如果CI还运行其他东西(MSRV检查、额外的lint标志),也一起运行。
  • 如果基线不是绿色的,不要修复它。 记录哪些检查失败以及如何失败,告知用户,然后从那时起期望相同的失败并且没有新的失败。只格式化你编辑的文件。来自环境而非代码的失败(带有仅夜间版标志的全局cargo配置、缺少的工具链或工具)会作为“无法运行”放在“验证”下,并说明原因。如果仓库没有CI,则从 rust-version、所用版本和语言特性中获取工具链下限,并说明你能运行什么和不能运行什么。
  • 大型更改:构建差分测试工具 当现有测试没有固定你将更改路径的确切输出时。固定意味着跟踪或快照测试,这些测试会逐字断言输出序列和截止时间;仅对最终状态的断言不算。该工具并排运行基线和工作树,在同一组随机调用上,并比较每个输出。<skill>/scripts/difftest_scaffold.py 在仓库外部创建crate对和模板。填充模板是真正的工作,所以要为此预留时间:先阅读 references/differential.md。在依赖它之前,检查它是否能捕获一个植入的单行变异;一个就足够了。
  • 保持工具比重构更便宜。 驱动你重构的路径,而不是整个crate。如果覆盖某条路径的成本高于对其更改的成本,则在“验证”下报告该缺口,并依靠该路径的现有测试。示例:位于TLS或加密后面的路径、必须规范化的非确定性、必须模拟的对端。
  • 仅在大型更改时:用更多用例运行慢速检查一次。 运行属性测试,调整大小使其在几分钟内完成(默认的十倍,或者当用例便宜时使用 PROPTEST_CASES=10000),并在最后再次运行。仅当模糊测试目标能触及你要更改的代码时运行它,时间限制在30-60秒。仅当更改会加重每个输入路径的工作量时才运行基准测试;重构控制流不需要它们。
  • 填补应该比更改更持久的覆盖缺口。 如果你要重构的路径没有覆盖并且你没有构建工具,则首先添加固定其当前输出的测试。如果你正在构建工具,它会覆盖重构期间的缺口;仅为它发现的、值得保留的行为(你故意保留的怪癖)添加一个测试。将新测试放在新文件或新模块中,作为单独的步骤,以便现有测试保持不变。唯一例外是单个crate测试布局(如 tests/main.rs)需要的那个 mod 行。

2. 绘制机器映射

映射请求涉及的实体,以及计划更改会触及的任何实体。在大型crate中,如果请求没有说明哪个部分重要,选择用户命名的实体并说明你省略了什么。在更改任何内容之前写下映射(临时文件也可以)。它会成为总结的状态映射,并且通常是模块文档。

  • 实体。 对于范围内的每个实体,列出其字段,并按它们有意义的阶段分组。bool和Option的组合(它们共同编码一个阶段)是主要目标。
  • 输入。 每个接受外部数据或 now 的 pub fn,以及每个可以触发什么。
  • 写入点。 对于每个状态字段,每个赋值它的位置(搜索 .field =、mem::replace、.take()、+= 1)。具有许多写入点的字段是一个分散在文件中的转换。
  • 输出。 每次推送到输出队列的操作,以及每个 poll_timeout 读取的截止时间。
  • 不变量。 必须始终成立的条件:一个ID恰好在一个映射中、每当有东西等待时就要设置截止时间、引用的连接存在。
  • 怪癖。 看起来像是偶然行为:宽松的解析、两个错误之间的奇怪优先级、对垃圾输入的回答仍然发送回复、截止时间之后仍接受输入。你保持它(规则1),但为用户列出它。

然后以表格形式编写映射:状态、输入、下一个状态、输出。如果你无法在不追踪多个函数的情况下从代码中填写它,那么代码还不够清晰,这就是工作要做的部分。

3. 选择更改

references/patterns.md 是一个目录,包含前后示例以及每个模式不划算的情况。阅读其索引,然后阅读你的映射所揭示的任何内容的相应部分,包括它们的“不值得”段落。对于大型更改,阅读整个文件。根据每个更改缩短四个问题之一答案的程度,对比diff的大小,对候选更改进行排序。通常最有效的方法:

  • 一个枚举,其变体携带每个阶段的数据,取代编码阶段的标志和选项(模式1)
  • 每个转换一个命名函数,以便每个状态字段只有少数写入点(模式2)
  • 处理程序匹配输入、决定、应用转换并排队输出,沿一个一致的调度轴(模式3)
  • 每种输出一个辅助函数,以其意图命名(模式4)
  • 每个截止时间规则只写一次,poll_timeout 从状态派生(模式5)
  • 在读者进入的地方写下生命周期,并在字段上写下不变量(模式9)

如果用户命名了一个函数或一部分,只更改那部分及其所需的内容。你提取的辅助函数可能会被其自然的兄弟姐妹重用,但不要仅仅因为辅助函数也适合那里就重新编写请求之外的代码。将这些改进列在“建议”下:询问一个函数的审查者不应该需要检查第二个函数。

排除那些只移动代码、重命名局部变量以迎合个人品味、以及任何使代码变长但不能使问题更容易回答的事情。当用户要求进行移动本身(将长文件拆分为模块)时,请参见模式11。已经可读性很好的代码可以保持不变:在总结中说明,而不是发明工作。

4. 逐步应用

对步骤进行排序,使每个步骤都可审查,并且没有一个步骤添加后续步骤会删除的代码:

  1. 纯添加和提取:输出辅助函数、命名谓词、截止时间辅助函数、将特性化门控步骤移动到辅助函数、将线路编码和解析移动到函数(模式4、5、6、10)。
  2. 核心更改:阶段枚举,以及它强制进行的转换和处理程序形状(模式1-3、7、12)。这通常必须作为一个步骤落地。如果是这样,说明原因。
  3. 命名(模式8)。
  4. 文档(模式9)。
  5. 重新排序方法,最后且单独进行,如果需要的话(模式11)。

在编辑之间,使用快速循环:clippy和你正在更改的包的测试,在默认特性集中,加上你构建的工具(如果有)。在每个步骤结束时,运行步骤1中的检查命令。然后如果用户想要提交(他们要求了,或者项目的工作流期望提交),则将该步骤作为一个提交;否则保持工作未提交并在总结中列出步骤。不要用下一个步骤会移除的脚手架(仅为了一个步骤变绿而添加的 cfg 门控或 #[allow])来静默lint。相反,重新排序步骤。

在借用检查器迫使代码变形的地方放弃一个模式,因为变形的代码比你开始时的更难看。常见迹象:

  • 一个方法将 self 的兄弟字段(例如输出队列 &mut VecDeque<Transmit>)或其自己的映射键作为参数,仅为了绕过借用
  • 一个 &self 方法仅为了读取配置而接受一个子结构
  • 克隆的键或额外的查找

通常的修复方法:

  • 返回输出并让调用者排队
  • 将辅助函数放在实体上并传入它需要的 Copy 配置值
  • 保持代码内联

5. 验证并总结

最后一次运行检查命令,覆盖整个工作空间,加上你在开始时运行的其他任何东西:慢速检查和工具。

然后检查公共API,如果diff触及了 pub 项、派生、trait实现、字段类型(诸如 Send 之类的自动trait遵循私有字段)或项上的 cfg:

  • 每个特性集使用一次 cargo semver-checks --baseline-rev <base>,使用显式标志:--default-features、--all-features、--only-explicit-features(无),以及 --only-explicit-features --features x。
  • 它只捕获破坏性更改,因此还要确认没有新东西变成公开:如果安装了 cargo public-api diff 则使用它,或者阅读 git diff <base> | grep -E '^\+.*\bpub\b'。
  • 如果没有安装工具,请在安装前询问,否则将API检查报告为未运行。
  • 如果diff只触及函数体和私有项,请说明。

然后编写总结。

相关技能