Lesbare sans-IO Zustandsautomaten
Eine sans-IO Bibliothek ist ein Zustandsautomat, bei dem die E/A entfernt wurde. Der Aufrufer versorgt ihn mit Eingaben (handle_*, jeweils mit now), leert seine Ausgaben (poll_transmit, poll_event, poll_timeout) und besitzt die Sockets und Timer. Code dieser Art ist nur dann wertvoll, wenn man darüber nachdenken kann: der nächste Maintainer, der Autor eines Treibers, ein Reviewer, der ihn gegen eine RFC überprüft. Jeder von ihnen sollte vier Fragen schnell beantworten können:
- Zustände: In welchen Zuständen kann sich jede langlebige Entität (Verbindung, Abfrage, Stream, Sitzung) befinden, und welche Daten existieren in jedem?
- Übergänge: Welche Eingaben bewegen sie von einem Zustand zum anderen, und wo im Code findet jeder Übergang statt?
- Ausgaben: Was wird durch jeden Übergang in die Warteschlange gestellt (Übertragungen, Ereignisse, Schreibvorgänge), und in welcher Reihenfolge?
- Zeit: Wann muss sie aufgeweckt werden, und welche Regel setzt jede Frist?
Ihre Aufgabe ist es, diese Antworten in den Typen und im Code sichtbar zu machen, während die Maschine weiterhin genau das tut, was sie tat. Sie bearbeiten den Arbeitsbaum und beenden mit einer Zusammenfassung.
<skill> bezieht sich unten auf das Basisverzeichnis dieser Fähigkeit, den Ordner, der diese SKILL.md enthält.
Grundregeln
-
Das Verhalten bleibt identisch. In einer sans-IO Crate umfasst das Verhalten Dinge, die kosmetisch aussehen:
- die Reihenfolge, in der Ausgaben in die Warteschlange gestellt werden
- die Iterationsreihenfolge von Maps (eine
BTreeMapoder eine einfügeordneteVecentscheidet, wer zuerst geht) - welchen Instant
poll_timeoutzurückgibt - ob ein leerer Schreibvorgang oder ein No-Op-Ereignis überhaupt ausgegeben wird
- mit welchem Fehler eine Entität fehlschlägt oder schließt
- die Ausgabe eines
#[derive(Debug)]-Typs, dessen Felder Sie ändern, wenn ein Snapshot, eine Log-Assertion oder eine Fehlermeldung ihn ausgibt
Tests, die durch die öffentliche API gehen, und jeder Snapshot, müssen unverändert bestehen. Wenn einer bearbeitet werden muss, hat dieser Schritt das Verhalten geändert: Machen Sie ihn rückgängig. Unit-Tests innerhalb der Crate, die private Elemente benennen, benötigen möglicherweise dieselbe mechanische Bearbeitung wie der Code (ein Flag, das zu einer Variante wurde, eine umbenannte Hilfsfunktion). Führen Sie nur diese Bearbeitung durch, behalten Sie bei, was jede Assertion prüft, und listen Sie den Test in der Zusammenfassung auf.
-
Lassen Sie die öffentliche API in Ruhe. Behalten Sie die Namen, Signaturen, Varianten und Felder von
pub-Elementen so, wie sie sind, da Treiber und nachgelagerte Crates von ihnen abhängen (und von Semver).pub(crate)und private Elemente sind erlaubt. Verbesserungen der API setzen Sie als Vorschläge in die Zusammenfassung. Wie Sie dies überprüfen, finden Sie in Schritt 5. -
Opfern Sie keine Leistung für Lesbarkeit. Keine neuen Klone, Allokationen oder zusätzlichen Durchläufe über die Maps auf Pfaden, die pro Eingabe (pro Paket, Nachricht oder Abfrage) ausgeführt werden. Ein
mem::replaceodermem::takeist kostenlos; das Klonen einerVec, um den Borrow-Checker zu umgehen, nicht. -
Arbeiten Sie in kleinen Schritten. Jeder Schritt baut und besteht die Überprüfungen, sodass ein fehlgeschlagener Test auf eine Änderung hinweist und der Diff Schritt für Schritt überprüft werden kann.
-
Folgen Sie dem Projekt. Lesen Sie vor der Bearbeitung die Agenten- und Mitwirkendendokumente (AGENTS.md, CLAUDE.md, CONTRIBUTING), die Lint-Konfiguration und den CI-Workflow. Passen Sie die umgebende Kommentardichte und den Idiomstil an. Wenn das Projekt Dokumentationen auf dem neuesten Stand mit dem Code hält (eine Lebenszyklustabelle, ein Kommentar, der Zeilen auf Funktionen abbildet, eine Spezifikationsabdeckungstabelle), aktualisieren Sie diese im selben Schritt.
Größe der Arbeit an die Änderung anpassen
Das Sicherheitsnetz besteht, um Verhaltensänderungen zu erkennen, die die vorhandenen Tests übersehen würden, passen Sie es daher an die geplante Änderung an:
- Klein. Ein Handler oder einige Hilfsfunktionen, keine Änderung daran, welche Felder den Zustand halten. Führen Sie die Überprüfungen des Projekts einmal vorher und einmal nachher durch und überspringen Sie alles, was unten als groß markiert ist. In der Zusammenfassung bilden Sie nur die Entität ab, die Sie berührt haben.
- Mittel. Mehrere Übergänge oder Ausgabehilfsfunktionen in einem Modul. Führen Sie die Überprüfungen vor jedem Schritt und am Ende durch. Bauen Sie einen differentiellen Testrahmen nur, wenn kein vorhandener Test die genauen Ausgaben der Pfade, die Sie ändern, festschreibt, und zwar nur über diese Pfade. Kein Fuzzing, keine zusätzlichen Property-Test-Fälle, keine Benchmarks: Die eigenen Überprüfungen des Projekts mit ihren normalen Einstellungen sind hier das richtige Maß.
- Groß. Flags, die durch ein Phasen-Enum ersetzt wurden, viele Schreiber eines Zustandsfelds, die in Übergänge zusammengeführt wurden, die meisten Handler umgeformt. Verwenden Sie den gesamten Workflow.
Bestimmen Sie die Größe nach der Abbildung (Schritt 2). Wenn die Änderung während der Arbeit wächst, wechseln Sie zu einer größeren Größe und sagen Sie dies in der Zusammenfassung.
Workflow
1. Sicherheitsnetz einrichten
Führen Sie dies vor der ersten Bearbeitung durch, sodass jeder spätere Fehler von Ihnen stammt und nicht bereits vorhanden ist.
- Basis notieren. Führen Sie
git statusaus und notieren Sie den Basis-Commit: Jeder spätere Vergleich erfolgt gegen ihn. Wenn Dateien, die Sie bearbeiten möchten, bereits unbestätigte Änderungen enthalten, informieren Sie den Benutzer, bevor Sie Ihre Bearbeitungen mit ihren vermischen. Wenn Sie auf deren Basis fortfahren, verwenden Siegit stash createals Basis: Es zeichnet sie als Commit auf, ohne den Baum zu berühren. - Einen Check-Befehl fixieren. Verwenden Sie den eigenen des Projekts, wenn es einen hat (einen CI-Workflow, ein Justfile, ein Xtask, die Befehle in den Agentendokumenten). Andernfalls verwenden Sie das gebündelte Gate:
<skill>/scripts/check.sh <repo> default all. Listen Sie die Feature-Sets auf, die CI verwendet, z.B.default all none tls. Das Gate prüft die Formatierung ohne Änderung, führt dann Clippy und die Tests für jedes Feature-Set aus und beendet mit einem Fehlercode, wenn etwas fehlschlägt. Begrenzen Sie es auf das Paket, das Sie ändern, und die Mitglieder, die davon abhängen, mitCHECK_PACKAGES="-p <crate> -p <dependent>", und führen Sie den gesamten Arbeitsbereich einmal am Ende aus. Wenn CI auch etwas anderes ausführt (eine MSRV-Prüfung, zusätzliche Lint-Flags), führen Sie dies ebenfalls aus. - Wenn die Basis nicht grün ist, reparieren Sie sie nicht. Notieren Sie, welche Checks fehlschlagen und wie, informieren Sie den Benutzer, und erwarten Sie ab dann dieselben Fehler und keine neuen. Formatieren Sie nur die Dateien, die Sie bearbeiten. Ein Fehler, der aus der Umgebung und nicht aus dem Code stammt (eine globale Cargo-Konfiguration mit Nightly-only-Flags, eine fehlende Toolchain oder ein fehlendes Tool), wird unter „Verifikation" als „konnte nicht ausgeführt werden" mit dem Grund vermerkt. Wenn das Repository kein CI hat, nehmen Sie die Toolchain-Untergrenze aus
rust-version, der Edition und den verwendeten Sprachfeatures und sagen Sie, was Sie ausführen konnten und was nicht. - Große Änderungen: Bauen Sie einen differentiellen Testrahmen, wenn die vorhandenen Tests nicht die genauen Ausgaben der Pfade, die Sie ändern, festschreiben. Festschreiben bedeutet Trace- oder Snapshot-Tests, die die Ausgabensequenz und die Fristen wörtlich bestätigen; Assertionen zum endgültigen Zustand allein zählen nicht. Der Rahmen führt die Basis und den Arbeitsbaum nebeneinander auf denselben zufälligen Aufrufen aus und vergleicht jede Ausgabe.
<skill>/scripts/difftest_scaffold.pyerstellt das Crate-Paar und eine Vorlage außerhalb des Repos. Das Ausfüllen der Vorlage ist die eigentliche Arbeit, planen Sie Zeit dafür ein: Lesen Sie zuerst references/differential.md. Überprüfen Sie, dass er eine gepflanzte Einzeilen-Mutante erkennt, bevor Sie sich darauf verlassen; eine reicht. - Halten Sie den Rahmen günstiger als die Umgestaltung. Treiben Sie die Pfade, die Sie umstrukturieren, nicht die gesamte Crate. Wenn das Abdecken eines Pfads mehr kosten würde als die Änderung daran, melden Sie die Lücke stattdessen unter Verifikation und verlassen Sie sich auf die vorhandenen Tests dieses Pfads. Beispiele: ein Pfad hinter TLS oder Verschlüsselung, Nichtdeterminismus, den Sie normalisieren müssten, ein Peer, den Sie simulieren müssten.
- Nur für große Änderungen: Führen Sie die langsamen Checks einmal mit mehr Fällen aus. Führen Sie die Property-Tests aus, bemessen, um in einigen Minuten fertig zu sein (zehnmal der Standard, oder
PROPTEST_CASES=10000, wenn Fälle billig sind), und noch einmal am Ende. Führen Sie ein Fuzz-Ziel nur aus, wenn es Code erreicht, den Sie ändern werden, zeitlich begrenzt auf 30–60 s. Führen Sie Benchmarks nur aus, wenn eine Änderung Arbeit auf einem Pro-Eingabe-Pfad hinzufügt; das Umstrukturieren des Kontrollflusses benötigt sie nicht. - Füllen Sie Abdeckungslücken, die die Änderung überdauern sollten. Wenn ein Pfad, den Sie umstrukturieren werden, keine Abdeckung hat und Sie keinen Rahmen bauen, fügen Sie zuerst Tests hinzu, die seine aktuellen Ausgaben festschreiben. Wenn Sie einen bauen, deckt er die Lücke für die Umgestaltung ab; fügen Sie nur einen Test für ein Verhalten hinzu, das er aufdeckt und das es wert ist, behalten zu werden (eine Kuriosität, die Sie absichtlich beibehalten haben). Setzen Sie neue Tests in eine neue Datei oder ein neues Modul, als eigenen Schritt, damit die vorhandenen Tests unverändert bleiben. Die Ausnahme ist die eine
mod-Zeile, die ein Ein-Crate-Test-Layout wietests/main.rsbenötigt.
2. Die Maschine abbilden
Bilden Sie die Entitäten ab, um die es in der Anfrage geht, und jede Entität, die eine geplante Änderung berühren würde. In einer großen Crate, in der die Anfrage nicht sagt, welcher Teil wichtig ist, wählen Sie die Entität, die der Benutzer genannt hat, und sagen Sie, was Sie ausgelassen haben. Schreiben Sie die Abbildung auf, bevor Sie etwas ändern (eine temporäre Datei ist in Ordnung). Sie wird zur Zustandsabbildung der Zusammenfassung und oft zu den Moduldokumentationen.
- Entitäten. Listen Sie für jede Entität im Geltungsbereich ihre Felder auf und gruppieren Sie sie nach der Phase, in der sie eine Bedeutung haben. Kombinationen von
bools undOptions, die eine Phase zwischen ihnen codieren, sind das Hauptziel. - Eingaben. Jede
pub fn, die externe Daten odernownimmt, und was jede auslösen kann. - Schreiber. Für jedes Zustandsfeld jede Stelle, die es zuweist (suchen Sie nach
.field =,mem::replace,.take(),+= 1). Ein Feld mit vielen Schreibern ist ein Übergang, der über die Datei verteilt ist. - Ausgaben. Jedes Pushen auf eine Ausgabewarteschlange und jede Frist, die
poll_timeoutliest. - Invarianten. Was immer gelten muss: eine ID in genau einer Map, eine Frist, die gesetzt ist, wenn etwas wartet, eine referenzierte Verbindung, die existiert.
- Kuriositäten. Verhalten, das versehentlich aussieht: nachsichtiges Parsen, seltsame Priorität zwischen zwei Fehlern, eine Antwort, die auf Müll gesendet wird, Eingabe, die auch nach einer Frist noch akzeptiert wird. Sie behalten es (Regel 1), listen es aber für den Benutzer auf.
Schreiben Sie dann die Abbildung als Tabelle: Zustand, Eingabe, nächster Zustand, Ausgaben. Wo Sie sie nicht aus dem Code ausfüllen können, ohne mehrere Funktionen zu durchlaufen, ist der Code noch nicht lesbar, und dort wird die Arbeit investiert.
3. Die Änderungen auswählen
references/patterns.md ist der Katalog mit Vorher/Nachher-Beispielen und den Fällen, in denen sich jedes Muster nicht lohnt. Lesen Sie sein Inhaltsverzeichnis, dann die Abschnitte für das, was Ihre Abbildung ergeben hat, einschließlich ihrer „Nicht lohnenswert"-Absätze. Lesen Sie die gesamte Datei für eine große Änderung. Bewerten Sie Kandidatenänderungen danach, wie sehr jede die Antwort auf eine der vier Fragen verkürzt, im Verhältnis zur Größe des Diffs. Die Änderungen, die sich normalerweise am meisten auszahlen:
- ein Enum, dessen Varianten die Daten jeder Phase tragen, anstelle von Flags und Optionen, die die Phase codieren (Muster 1)
- eine benannte Funktion pro Übergang, sodass jedes Zustandsfeld wenige Schreiber hat (Muster 2)
- Handler, die die Eingabe matchen, entscheiden, den Übergang anwenden und die Ausgaben in die Warteschlange stellen, entlang einer konsistenten Dispatch-Achse (Muster 3)
- eine Hilfsfunktion pro Art von Ausgabe, benannt nach ihrer Absicht (Muster 4)
- jede Fristregel einmal geschrieben, mit
poll_timeoutabgeleitet vom Zustand (Muster 5) - der Lebenszyklus dort niedergeschrieben, wo ein Leser landet, und Invarianten auf den Feldern (Muster 9)
Wenn der Benutzer eine Funktion oder einen Teil genannt hat, ändern Sie nur diesen und das, was er benötigt. Eine Hilfsfunktion, die Sie extrahieren, kann von ihren natürlichen Geschwistern wiederverwendet werden, aber überarbeiten Sie Code außerhalb der Anfrage nicht nur, weil die Hilfsfunktion auch dort passt. Listen Sie diese Verbesserungen stattdessen unter Vorschläge auf: Ein Reviewer, der nach einer Funktion gefragt hat, sollte keine zweite überprüfen müssen.
Lassen Sie Änderungen aus, die nur Code verschieben, Umbenennungen lokaler Variablen, um einem Geschmack zu entsprechen, und alles, was Code länger macht, ohne eine Frage leichter zu beantworten. Wenn der Benutzer nach der Verschiebung selbst fragt (Aufteilen einer langen Datei in Module), sehen Sie Muster 11. Code, der bereits gut lesbar ist, kann so bleiben, wie er ist: Sagen Sie dies in der Zusammenfassung, anstatt Arbeit zu erfinden.
4. In Schritten anwenden
Ordnen Sie die Schritte so, dass jeder überprüfbar ist und keiner Code hinzufügt, den ein späterer Schritt löscht:
- Reine Hinzufügungen und Extraktionen: Ausgabehilfsfunktionen, benannte Prädikate, Fristenhelfer, feature-gated Schritte, die in Hilfsfunktionen verschoben wurden, Drahtkodierung und Parsing, das in Funktionen verschoben wurde (Muster 4, 5, 6, 10).
- Die Kernänderung: ein Phasen-Enum, zusammen mit den Übergängen und Handlerformen, die es erzwingt (Muster 1–3, 7, 12). Dies muss oft als ein Schritt landen. Wenn ja, sagen Sie warum.
- Namen (Muster 8).
- Doks (Muster 9).
- Neuanordnung von Methoden, zuletzt und für sich allein, wenn überhaupt (Muster 11).
Zwischen den Bearbeitungen verwenden Sie eine schnelle Schleife: Clippy und die Tests des Pakets, das Sie ändern, im Standard-Feature-Set, plus den Rahmen, wenn Sie einen gebaut haben. Am Ende jedes Schritts führen Sie den Check-Befehl aus Schritt 1 aus. Machen Sie dann den Schritt zu einem Commit, wenn der Benutzer Commits möchte (sie haben gefragt, oder der Workflow des Projekts erwartet sie); andernfalls lassen Sie die Arbeit unbestätigt und listen Sie die Schritte in der Zusammenfassung auf. Unterdrücken Sie keinen Lint mit einem Gerüst, das der nächste Schritt entfernt (ein cfg-Gate oder ein #[allow], das nur hinzugefügt wurde, um einen Schritt grün zu bekommen). Ordnen Sie stattdessen die Schritte neu.
Brechen Sie ein Muster an jeder Stelle ab, an der der Borrow-Checker es in Verrenkungen zwingt, da verrenkter Code schlechter lesbar ist als das, womit Sie begonnen haben. Die üblichen Anzeichen:
- eine Methode, die ein Geschwisterfeld von
self(eine Ausgabewarteschlange wie&mut VecDeque<Transmit>) oder ihren eigenen Map-Schlüssel als Argument nimmt, nur um einen Borrow zu umgehen - eine
&self-Methode, die eine Unterstruktur nur nimmt, damit sie die Konfiguration lesen kann - geklonte Schlüssel oder eine zusätzliche Lookup
Die üblichen Korrekturen:
- geben Sie die Ausgabe zurück und lassen Sie den Aufrufer sie in die Warteschlange stellen
- setzen Sie die Hilfsfunktion auf die Entität und übergeben Sie den
Copy-Konfigurationswert, den sie benötigt - halten Sie den Code inline
5. Verifizieren und zusammenfassen
Führen Sie den Check-Befehl ein letztes Mal aus, über den gesamten Arbeitsbereich, plus alles andere, was Sie am Anfang ausgeführt haben: die langsamen Checks und den Rahmen.
Überprüfen Sie dann die öffentliche API, wenn der Diff ein pub-Element, ein Derive, eine Trait-Implementierung, einen Feldtyp (automatische Traits wie Send folgen privaten Feldern) oder ein cfg auf einem Element berührt:
- Verwenden Sie
cargo semver-checks --baseline-rev <base>einmal pro Feature-Set, mit expliziten Flags:--default-features,--all-features,--only-explicit-features(keine), und--only-explicit-features --features x. - Es erkennt nur Breaking Changes, bestätigen Sie daher auch, dass nichts neu öffentlich wurde: Verwenden Sie
cargo public-api diff, wenn installiert, oder lesen Siegit diff <base> | grep -E '^\+.*\bpub\b'. - Wenn kein Tool installiert ist, fragen Sie vor der Installation, und melden Sie andernfalls die API-Prüfung als nicht ausgeführt.
- Wenn der Diff nur Funktionskörper und private Elemente berührt, sagen Sie das.