Antra

Themen

PEG-Tutorial für Einsteiger

Dieses Kapitel ist absichtlich für Leute geschrieben, die PEG-Grammatiken noch gar nicht kennen.

Wichtig als Einordnung: Die Grundideen und viele Operatoren von Antra::PEG sind zu großen Teilen von Janet übernommen. Das heißt nicht, dass Antra Janet kopiert, sondern dass sich das PEG-Modell stark an Janet orientiert und dann in normale Antra-Syntax übersetzt wurde.

Wenn du das Originalmodell nachlesen willst, sind die offizielle Janet-Seite janet-lang.org und speziell die PEG-Doku unter janet-lang.org/docs/peg.html die besten Startpunkte.

Die Kurzversion ist:

  • Eine PEG-Grammatik ist eine Beschreibung dafür, wie ein Text gelesen werden soll.
  • Du beschreibst kleine Bausteine.
  • Daraus setzt du dann größere Regeln zusammen.
  • Am Ende bekommst du keine "Magie", sondern einfach Captures als Ergebnis.

Wenn dir das Wort "Grammatik" zu groß vorkommt, lies es erstmal einfach als:

"eine Bauanleitung zum Lesen eines Textes"

Das erste Gefühl#

Nimm diesen Text:

hello

Wenn du prüfen willst, ob genau hello drinsteht, reicht schon das hier:

import Antra::PEG as PEG

const grammar = %{
    main: [:sequence, "hello", -1]
}

println(PEG.match(grammar, "hello"))

Das Ergebnis ist:

[]

Das wirkt erst seltsam. Warum ein leeres Array?

Weil zwar alles erfolgreich gematcht wurde, aber noch nichts gecaptured wurde.

Die wichtigste Idee: kleine Regeln bauen#

Eine Grammatik ist einfach eine Map. Die wichtigste Regel heißt :main.

const grammar = %{
    main: [:sequence, :word, ":", :number, -1],
    word: [:some, :w],
    number: [:some, :d]
}

Das liest sich so:

  • main erwartet erst ein Wort
  • dann einen Doppelpunkt
  • dann eine Zahl
  • dann muss der Input zu Ende sein

Was die Grundbausteine bedeuten#

String#

"hello"

Das matcht genau diesen Text.

Optional: Regex als einzelner Baustein#

Manchmal willst du nicht jeden kleinen Zeichenschritt mit :some, :choice oder Ranges ausformulieren, sondern an einer Stelle bewusst einen Regex verwenden. Dafuer gibt es in Text-PEGs [:regex, regex"..."].

[:regex, regex"[A-Z][a-z]+"]

Wichtig ist die Denkweise dabei:

  • der Regex prueft nur ab der aktuellen Position
  • er konsumiert nur den gematchten Praefix
  • er schreibt noch nichts ins Ergebnis

Wenn du den Treffer spaeter sehen willst, nutzt du weiter :capture:

[:capture, [:regex, regex"[A-Z][a-z]+"]]

Regelreferenz#

:word

Das heißt: spring in die Regel word.

:sequence#

[:sequence, a, b, c]

Das heißt: erst a, dann b, dann c.

:choice#

[:choice, a, b, c]

Das heißt: probiere a, sonst b, sonst c.

:some#

[:some, :d]

Das heißt: eine oder mehr Ziffern.

Der erste echte Capture#

Jetzt wollen wir nicht nur prüfen, sondern auch etwas herausziehen.

import Antra::PEG as PEG

const grammar = %{
    main: [:group, [:sequence,
        [:capture, :word],
        ":",
        [:capture, :number],
        -1
    ]],
    word: [:some, :w],
    number: [:some, :d]
}

println(PEG.match(grammar, "age:42"))

Ergebnis:

[["age", "42"]]

Das ist die wichtigste Sache überhaupt:

  • [:capture, ...] legt den gelesenen Text auf den Capture-Stack
  • [:group, ...] packt mehrere Captures in ein Unter-Array

So denkst du beim Schreiben einer PEG#

Nicht gleich alles auf einmal bauen.

Besser ist diese Reihenfolge:

  1. Wie sieht ein einzelnes kleines Teil aus?
  2. Wie sehen mehrere davon hintereinander aus?
  3. Was soll wirklich im Ergebnis landen?

Beispiel: einfache Komma-Liste#

Wir wollen so etwas lesen:

apfel,banane,kirsche

Die Grammatik kann so aussehen:

import Antra::PEG as PEG

const liste = %{
    main: [:group, [:sequence,
        [:capture, :wort],
        [:any, [:sequence, ',', [:capture, :wort]]],
        -1
    ]],
    wort: [:some, :w]
}

println(PEG.match(liste, "apfel,banane,kirsche"))

Ergebnis:

[["apfel", "banane", "kirsche"]]

Beispiel: Zeile mit Name und Zahl#

Ada:36
import Antra::PEG as PEG

const person = %{
    main: [:group, [:sequence,
        [:capture, [:some, :w]],
        ":",
        [:capture, [:some, :d]],
        -1
    ]]
}

println(PEG.match(person, "Ada:36"))

Ergebnis:

[["Ada", "36"]]

-1 nicht vergessen#

-1 ist wichtig, wenn du wirklich den ganzen Input lesen willst.

Ohne -1 würde das hier trotzdem matchen:

age:42kaputt

weil die Grammatik schon nach age:42 erfolgreich wäre.

Mit -1 sagst du:

"es darf nichts mehr übrig bleiben"

Häufige Anfängerfehler#

1. Janet-Beispiele ohne Kommas kopieren#

Falsch für Antra:

[:sequence [:some :w] ":" [:some :d]]

Richtig in Antra:

[:sequence, [:some, :w], ":", [:some, :d]]

2. :capture vergessen#

Dann matcht die Grammatik zwar, aber dein Ergebnis ist leer.

3. -1 vergessen#

Dann kann der Anfang passen, obwohl hinten noch Müll übrig ist.

4. Zu groß anfangen#

Wenn du gleich HTML, JSON oder eine komplizierte Config-Datei bauen willst, verlierst du leicht den Überblick.

Starte lieber mit:

  • einer Zeile
  • einem Feld
  • einem Separator

Ein gutes Arbeitsmuster#

Wenn du eine neue PEG baust, arbeite so:

  1. Baue zuerst nur main mit einem ganz kleinen Beispiel.
  2. Prüfe mit println(PEG.match(...)), was wirklich zurückkommt.
  3. Erst dann :capture und :group ergänzen.
  4. Danach Stück für Stück auf mehrere Fälle erweitern.

Wo du weitermachen solltest#

Wenn du jetzt das Grundgefühl verstanden hast, lies als Nächstes die Referenz in 24-peg-grammatiken.md.

Wenn du lieber Beispiele anschaust, sind diese Dateien gut geeignet:

  • examples/peg/csv_parser.ant
  • examples/peg/math_parser.ant
  • examples/peg/url_parser.ant
  • examples/peg/ini_parser.ant

Mini-Spickzettel#

"text"                  # exakter Text
:rule                   # andere Regel
[:sequence, a, b]       # hintereinander
[:choice, a, b]         # entweder oder
[:some, x]              # eins oder mehr
[:any, x]               # null oder mehr
[:opt, x]               # optional
[:capture, x]           # Ergebnis mitschreiben
[:group, x]             # mehrere Captures gruppieren
-1                      # Input muss zu Ende sein

Quelle dieser Seite