O que Máquinas de estado sans-IO legíveis faz?
Uma biblioteca sans-IO é uma máquina de estado com a E/S removida. O chamador a alimenta com entradas
(handle_*, cada uma com now), drena suas saídas (poll_transmit, poll_event,
poll_timeout) e possui os soquetes e temporizadores. Código como este só vale a pena ter se as pessoas
conseguirem raciocinar sobre ele: o próximo mantenedor, o autor de um driver, um revisor verificando-o
contra um RFC. Cada um deles deve ser capaz de responder rapidamente a quatro perguntas:
- Estados: em quais estados cada entidade de longa duração (conexão, consulta, fluxo, sessão) pode estar, e quais dados existem em cada um?
- Transições: quais entradas movem de um estado para outro, e onde no código cada movimento acontece?
- Saídas: o que cada movimento enfileira (transmissões, eventos, escritas), e em qual ordem?
- Tempo: quando precisa ser despertado e qual regra define cada prazo?
Seu trabalho é tornar essas respostas visíveis nos tipos e no código, enquanto a máquina continua fazendo exatamente o que fazia. Você edita a árvore de trabalho e termina com um resumo.
<skill> abaixo significa o diretório base desta skill, a pasta que contém este SKILL.md.
Regras fundamentais
-
O comportamento permanece idêntico. Em um crate sans-IO, o comportamento abrange coisas que parecem cosméticas:
- a ordem na qual as saídas são enfileiradas
- a ordem de iteração dos mapas (um
BTreeMapou umVecordenado por inserção decide quem vai primeiro) - qual instante
poll_timeoutretorna - se uma escrita vazia ou um evento sem operação é emitido
- com qual erro uma entidade falha ou encerra
- a saída de um tipo
#[derive(Debug)]cujos campos você altera, se um snapshot, log de asserção ou mensagem de erro a imprimir
Testes que passam pela API pública, e todos os snapshots, devem passar inalterados. Se um precisar de edição, essa etapa mudou o comportamento: desfaça-a. Testes de unidade dentro do crate que nomeiam itens privados podem precisar da mesma edição mecânica que o código (uma flag que virou uma variante, um helper renomeado). Faça apenas essa edição, mantenha o que cada asserção verifica e liste o teste no resumo.
-
Deixe a API pública em paz. Mantenha os nomes, assinaturas, variantes e campos de itens
pubcomo estão, pois drivers e crates downstream dependem deles (e do semver).pub(crate)e itens privados são permitidos. Coloque melhorias na API no resumo como sugestões. Como verificar isso está na etapa 5. -
Não gaste desempenho em legibilidade. Sem novos clones, alocações ou passagens extras sobre os mapas em caminhos que executam por entrada (por pacote, mensagem ou consulta). Um
mem::replaceoumem::takeé gratuito; clonar umVecpara driblar o verificador de empréstimos não é. -
Trabalhe em pequenos passos. Cada passo compila e passa nas verificações, então uma verificação com falha aponta para uma alteração e o diff pode ser revisado peça por peça.
-
Siga o projeto. Antes de editar, leia seus documentos de agente e contribuidor (AGENTS.md, CLAUDE.md, CONTRIBUTING), a configuração de lint e o workflow de CI. Combine a densidade de comentários e o idioma ao redor. Onde o projeto mantém documentos em sincronia com o código (uma tabela de ciclo de vida, um comentário mapeando linhas para funções, uma tabela de cobertura de especificação), atualize-os no mesmo passo.
Dimensionar o trabalho para a mudança
A rede de segurança existe para capturar mudanças de comportamento que os testes existentes não pegariam, então dimensione-a para a mudança que você planeja:
- Pequeno. Um manipulador ou alguns helpers, sem mudança em quais campos mantêm estado. Execute as verificações do projeto uma vez antes e uma vez depois, e pule tudo marcado como grande abaixo. No resumo, mapeie apenas a entidade que você tocou.
- Médio. Várias transições ou helpers de saída em um módulo. Execute as verificações antes de cada passo e no final. Construa um harness diferencial apenas se nenhum teste existente fixar as saídas exatas dos caminhos que você altera, e apenas sobre esses caminhos. Sem fuzzing, sem casos extras de teste de propriedade, sem benchmarks: as próprias verificações do projeto em suas configurações normais são o peso certo aqui.
- Grande. Flags substituídas por um enum de fase, muitos escritores de um campo de estado mesclados em transições, a maioria dos manipuladores remodelados. Use o workflow completo.
Decida o tamanho após o mapa (etapa 2). Se a mudança crescer no meio do caminho, suba de tamanho e diga isso no resumo.
Workflow
1. Configure uma rede de segurança
Faça isso antes da primeira edição, para que cada falha posterior seja sua e não pré-existente.
- Anote a base. Execute
git statuse anote o commit base: toda comparação posterior é contra ele. Se arquivos que você planeja tocar já têm alterações não commitadas, avise o usuário antes de misturar suas edições nas deles. Se você prosseguir sobre elas, usegit stash createcomo base: ele as registra como um commit sem tocar na árvore. - Corrija um comando de verificação. Use o próprio do projeto se ele tiver um (um workflow de CI, um
justfile, um xtask, os comandos em seus documentos de agente). Caso contrário, use a porta
fornecida:
<skill>/scripts/check.sh <repo> default all. Liste os conjuntos de recursos que CI usa, por exemplodefault all none tls. A porta verifica a formatação sem alterá-la, depois executa clippy e os testes para cada conjunto de recursos, e sai com código não zero se algo falhar. Escopo para o pacote que você altera e os membros que dependem dele comCHECK_PACKAGES="-p <crate> -p <dependent>", e execute todo o workspace uma vez no final. Se CI também executar algo mais (uma verificação de MSRV, flags de lint extras), execute isso junto. - Se a linha de base não estiver verde, não a corrija. Registre quais verificações falham e como,
avise o usuário e, a partir de então, espere as mesmas falhas e nenhuma nova. Formate apenas os arquivos que
você edita. Uma falha que vem do ambiente, e não do código (uma configuração global do cargo
com flags apenas noturnas, uma toolchain ou ferramenta ausente) vai para Verificação como
"não pôde executar", com o motivo. Se o repositório não tem CI, pegue o piso da toolchain de
rust-version, a edition e os recursos de linguagem em uso, e diga o que você pôde e não pôde executar. - Mudanças grandes: construa um harness diferencial quando os testes existentes não fixam as saídas
exatas dos caminhos que você alterará. Fixar significa testes de rastreamento ou snapshot que afirmam a
sequência de saída e os prazos textualmente; asserções sobre o estado final sozinhas não
contam. O harness executa a base e a árvore de trabalho lado a lado nas mesmas chamadas aleatórias e
compara cada saída.
<skill>/scripts/difftest_scaffold.pycria o par de crates e um modelo fora do repositório. Preencher o modelo é o trabalho real, então planeje para isso: leia references/differential.md primeiro. Verifique se ele pega um mutante plantado de uma linha antes de confiar nele; um é suficiente. - Mantenha o harness mais barato que a refatoração. Dirija os caminhos que você reestrutura, não o crate inteiro. Se cobrir um caminho custaria mais do que a mudança nele, relate a lacuna sob Verificação e confie nos testes existentes daquele caminho. Exemplos: um caminho atrás de TLS ou criptografia, não determinismo que você teria que normalizar, um par que você teria que simular.
- Apenas mudanças grandes: execute as verificações lentas uma vez com mais casos. Execute os testes de propriedade, dimensionados
para terminar em alguns minutos (dez vezes o padrão, ou
PROPTEST_CASES=10000quando os casos são baratos), e novamente no final. Execute um alvo de fuzzing apenas se ele atingir código que você alterará, com tempo limitado a 30–60 s. Execute benchmarks apenas se uma mudança adicionar trabalho em um caminho por entrada; reestruturar o fluxo de controle não precisa deles. - Preencha lacunas de cobertura que devem sobreviver à mudança. Se um caminho que você
reestruturará não tem cobertura e você não está construindo um harness, adicione testes que fixam suas saídas
atuais primeiro. Se você está construindo um, ele cobre a lacuna para a refatoração; adicione um teste apenas para
um comportamento que ele revela e que vale a pena manter (uma peculiaridade que você manteve de propósito). Coloque novos testes em um
novo arquivo ou módulo, como seu próprio passo, para que os testes existentes permaneçam inalterados. A exceção
é a linha
modque um layout de teste de crate único comotests/main.rsprecisa.
2. Mapeie a máquina
Mapeie as entidades sobre as quais o pedido é, e qualquer entidade que uma mudança planejada tocaria. Em um crate grande onde o pedido não diz qual parte importa, pegue a entidade que o usuário nomeou e diga o que você deixou de fora. Escreva o mapa antes de mudar qualquer coisa (um arquivo temporário é suficiente). Ele se torna o mapa de estado do resumo e, frequentemente, a documentação do módulo.
- Entidades. Para cada entidade no escopo, liste seus campos e agrupe-os pela fase na
qual eles significam algo. Combinações de
bools eOptions que codificam uma fase entre eles são o alvo principal. - Entradas. Toda
pub fnque recebe dados externos ounow, e o que cada uma pode acionar. - Escritores. Para cada campo de estado, todo lugar que o atribui (procure por
.field =,mem::replace,.take(),+= 1). Um campo com muitos escritores é uma transição espalhada pelo arquivo. - Saídas. Toda inserção em uma fila de saída, e todo prazo que
poll_timeoutlê. - Invariantes. O que deve sempre valer: um ID em exatamente um mapa, um prazo definido sempre que algo espera, uma conexão referenciada que existe.
- Peculiaridades. Comportamento que parece acidental: análise leniente, precedência estranha entre dois erros, uma resposta enviada em resposta a lixo, entrada ainda aceita após um prazo. Você mantém (regra 1), mas liste para o usuário.
Então escreva o mapa como uma tabela: estado, entrada, próximo estado, saídas. Onde você não pode preenchê-lo a partir do código sem rastrear várias funções, o código ainda não é legível, e é aí que o trabalho vai.
3. Escolha as mudanças
references/patterns.md é o catálogo, com exemplos antes/depois e os casos onde cada padrão não compensa. Leia seu índice, depois as seções para o que seu mapa revelou, incluindo seus parágrafos "Não vale a pena". Leia o arquivo inteiro para uma mudança grande. Classifique as mudanças candidatas por quanto cada uma encurta a resposta a uma das quatro perguntas, contra o tamanho do diff. Os movimentos que geralmente compensam mais:
- um enum cujas variantes carregam os dados de cada fase, no lugar de flags e options que codificam a fase (padrão 1)
- uma função nomeada por transição, para que cada campo de estado tenha poucos escritores (padrão 2)
- manipuladores que casam a entrada, decidem, aplicam a transição e enfileiram as saídas, ao longo de um eixo de despacho consistente (padrão 3)
- um helper por tipo de saída, nomeado após sua intenção (padrão 4)
- cada regra de prazo escrita uma vez, com
poll_timeoutderivado do estado (padrão 5) - o ciclo de vida escrito onde um leitor chega, e invariantes nos campos (padrão 9)
Se o usuário nomeou uma função ou uma parte, mude apenas aquilo e o que ele precisa. Um helper que você extrai pode ser reutilizado por seus irmãos naturais, mas não retrabalhe código fora do pedido só porque o helper cabe ali também. Liste essas melhorias sob Sugestões em vez disso: um revisor que perguntou sobre uma função não deve ter que verificar uma segunda.
Deixe de fora mudanças que apenas movem código, renomeações de variáveis locais para agradar a um gosto, e qualquer coisa que torne o código mais longo sem tornar uma pergunta mais fácil de responder. Quando o usuário pedir pelo movimento em si (dividir um arquivo longo em módulos), veja o padrão 11. Código que já lê bem pode ficar como está: diga isso no resumo, em vez de inventar trabalho.
4. Aplique em passos
Ordene os passos para que cada um seja revisável e nenhum adicione código que um passo posterior delete:
- Adições puras e extrações: helpers de saída, predicados nomeados, helpers de prazo, passos protegidos por features movidos para helpers, codificação e análise de fio movidos para funções (padrões 4, 5, 6, 10).
- A mudança central: um enum de fase, junto com as transições e formas de manipulador que ele força (padrões 1–3, 7, 12). Isso frequentemente tem que chegar como um passo. Se sim, diga por que.
- Nomes (padrão 8).
- Documentação (padrão 9).
- Reordenar métodos, por último e por si só, se for o caso (padrão 11).
Entre edições, use um loop rápido: clippy e os testes do pacote que você está alterando, no
conjunto de recursos padrão, mais o harness se você construiu um. No final de cada passo, execute o
comando de verificação da etapa 1. Então faça do passo um commit se o usuário quiser commits (ele pediu,
ou o workflow do projeto os espera); caso contrário, deixe o trabalho não commitado e liste os
passos no resumo. Não silencie um lint com scaffold que o próximo passo remove (uma porta cfg
ou um #[allow] adicionado apenas para deixar um passo verde). Reordene os passos em vez disso.
Abandone um padrão em qualquer lugar onde o verificador de empréstimos o force a contorções, já que código contorcido lê pior do que o que você começou. Os sinais usuais:
- um método que recebe um campo irmão de
self(uma fila de saída como&mut VecDeque<Transmit>) ou sua própria chave de mapa como argumento, apenas para passar no empréstimo - um método
&selfque recebe uma subestrutura apenas para poder ler a configuração - chaves clonadas ou uma busca extra
As correções usuais:
- retorne a saída e deixe o chamador enfileirá-la
- coloque o helper na entidade e passe o valor de configuração
Copyque ele precisa - mantenha o código inline
5. Verifique e resuma
Execute o comando de verificação uma última vez, sobre todo o workspace, mais o que mais você executou no início: as verificações lentas e o harness.
Então verifique a API pública, se o diff toca um item pub, um derive, uma impl de trait, um tipo de
campo (traits automáticos como Send seguem campos privados) ou um cfg em um item:
- Use
cargo semver-checks --baseline-rev <base>uma vez por conjunto de recursos, com flags explícitas:--default-features,--all-features,--only-explicit-features(nenhuma), e--only-explicit-features --features x. - Ele pega apenas mudanças que quebram, então também confirme que nada novo se tornou público: use
cargo public-api diffse instalado, ou leiagit diff <base> | grep -E '^\+.*\bpub\b'. - Se nenhuma ferramenta está instalada, pergunte antes de instalar uma, e caso contrário reporte a verificação de API como não executada.
- Se o diff toca apenas corpos de função e itens privados, diga isso.