01 · Problem
Wofür braucht man das?
Protokolle und Grammatiken brauchen Sequenz, Alternativen, Wiederholung und gute Fehlerpositionen, ohne einen monolithischen handgeschriebenen Parser zu erzeugen.
02 · Denkmodell
Das mentale Modell
Ein Parser ist eine pure Funktion von Inputzustand zu Erfolg plus neuem Offset oder Fehler plus Position. Kombinatoren verkabeln diese Zustandsübergänge.
Das umfangreiche Modul arbeitet bytebasiert mit DataView, Backtracking, User-State und vielen Kombinatoren. Mehrere Typen sind falsch, Views verlieren Offset/Länge und Wiederholung kann bei Parsern ohne Fortschritt endlos laufen.
03 · Kontrollfluss
Was passiert in welcher Reihenfolge?
- Input als unveränderliche Byteview plus aktuellen Offset modellieren.
- Primitive lesen genau einen Vertrag und liefern neuen Zustand oder erwartetes Token.
- Sequenz reicht Erfolg weiter; Alternative startet vom ursprünglichen Zustand.
- Bei mehreren Fehlern die weiteste Position und erwartete Tokens zusammenführen.
- parseAll kombiniert den Zielparser mit einem expliziten End-of-input-Parser.
04 · Bauteile
Die entscheidenden Verträge
Parser.map / chain / ap
Transformiert Resultate oder wählt den nächsten zustandsabhängigen Parser.
sequenceOf / choice / many / exactly
Kombiniert Reihenfolge, Alternativen und Wiederholung.
char / str / regex / digit / letter
Stringprimitive auf einem UTF-8-Byteinput.
lookAhead / possibly / recursiveParser
Kontrolliert Konsum, Optionalität und rekursive Grammatiken.
05 · Build it yourself
Selbst implementieren
Beginne mit einem minimalen Result-Typ und map/flatMap/orElse. Wiederholung und Komfortsyntax kommen erst nach korrekter Fortschritts- und Fehlersemantik.
- Definiere State, Success und Failure mit explizitem Offset.
- Implementiere map, flatMap und orElse ohne den Eingabestate zu mutieren.
- Breche many ab, wenn ein erfolgreicher Parser den Offset nicht erhöht.
type State = { input: Uint8Array; offset: number };
type Result<T> =
| { ok: true; value: T; state: State }
| { ok: false; offset: number; expected: Set<string> };
class Parser<T> {
constructor(readonly parse: (state: State) => Result<T>) {}
map<U>(fn: (value: T) => U) {
return new Parser<U>(state => {
const result = this.parse(state);
return result.ok ? { ...result, value: fn(result.value) } : result;
});
}
}06 · Verifizieren
Was du testen solltest
- Sequenz, Alternative und Backtracking erhalten den korrekten ursprünglichen oder fortgeschrittenen Offset.
- many erkennt einen erfolgreichen Nullfortschritt und terminiert mit einem definierten Fehler.
- UTF-8, TypedArray-Slices, Inputende und der weiteste kombinierte Fehler werden geprüft.
07 · Grenzen
Kompromisse und Stolperfallen
- Vollständiges Backtracking kann exponentiell werden; commit/cut begrenzt bewusste Alternativen.
- Bytepositionen sind für Binärdaten korrekt, für Nutzerfehler in Unicode-Texten aber erklärungsbedürftig.
- Ein gemeinsamer Parser für Text und Binärdaten erhöht Flexibilität und Typkomplexität zugleich.
Jeder Wiederholungskombinator braucht eine Fortschrittsinvariante: Erfolg ohne Inputverbrauch darf nicht erneut in derselben Schleife laufen.
08 · Weiterdenken