Quando è abbastanza lexing, quando hai bisogno di EBNF?
EBNF in realtà non aggiunge molto al potere delle grammatiche. È solo una comodità / notazione di scorciatoia / "zucchero sintattico" rispetto alle regole grammaticali standard di forma normale (CNF) di Chomsky. Ad esempio, l'alternativa EBNF:
S --> A | B
puoi ottenere in CNF elencando separatamente ciascuna produzione alternativa:
S --> A // `S` can be `A`,
S --> B // or it can be `B`.
L'elemento opzionale da EBNF:
S --> X?
puoi ottenere in CNF usando una produzione nullable , cioè quella che può essere sostituita da una stringa vuota (indicata qui da una produzione vuota; altri usano epsilon o lambda o cerchio incrociato):
S --> B // `S` can be `B`,
B --> X // and `B` can be just `X`,
B --> // or it can be empty.
Una produzione in una forma come l'ultima B
sopra è chiamata "cancellazione", perché può cancellare qualunque cosa rappresenti in altre produzioni (prodotto una stringa vuota invece di qualcos'altro).
Zero o più ripetizioni da EBNF:
S --> A*
puoi ottenere usando la produzione ricorsiva , cioè quella che si incorpora da qualche parte in essa. Può essere fatto in due modi. Il primo è la ricorsione sinistra (che di solito dovrebbe essere evitata, perché i parser Discesa ricorsiva dall'alto verso il basso non possono analizzarla):
S --> S A // `S` is just itself ended with `A` (which can be done many times),
S --> // or it can begin with empty-string, which stops the recursion.
Sapendo che genera solo una stringa vuota (in definitiva) seguita da zero o più A
s, la stessa stringa ( ma non la stessa lingua! ) Può essere espressa usando la ricorsione a destra :
S --> A S // `S` can be `A` followed by itself (which can be done many times),
S --> // or it can be just empty-string end, which stops the recursion.
E quando si tratta di +
una o più ripetizioni da EBNF:
S --> A+
può essere fatto prendendo in considerazione uno A
e usando *
come prima:
S --> A A*
che puoi esprimere in CNF in quanto tale (io uso la giusta ricorsione qui; prova a capire l'altro come esercizio):
S --> A S // `S` can be one `A` followed by `S` (which stands for more `A`s),
S --> A // or it could be just one single `A`.
Sapendo ciò, ora puoi probabilmente riconoscere una grammatica per un'espressione regolare (cioè grammatica regolare ) come quella che può essere espressa in una singola produzione EBNF costituita solo da simboli terminali. Più in generale, puoi riconoscere grammatiche regolari quando vedi produzioni simili a queste:
A --> // Empty (nullable) production (AKA erasure).
B --> x // Single terminal symbol.
C --> y D // Simple state change from `C` to `D` when seeing input `y`.
E --> F z // Simple state change from `E` to `F` when seeing input `z`.
G --> G u // Left recursion.
H --> v H // Right recursion.
Cioè, usando solo stringhe vuote, simboli di terminale, semplici non terminali per sostituzioni e cambiamenti di stato e usando la ricorsione solo per ottenere la ripetizione (iterazione, che è solo ricorsione lineare - quella che non si ramifica come un albero). Niente di più avanzato di questi, quindi sei sicuro che si tratti di una sintassi regolare e puoi farlo solo con Lexer.
Ma quando la tua sintassi usa la ricorsione in un modo non banale, per produrre strutture annidate simili ad alberi, auto-simili, come la seguente:
S --> a S b // `S` can be itself "parenthesized" by `a` and `b` on both sides.
S --> // or it could be (ultimately) empty, which ends recursion.
allora puoi facilmente vedere che questo non può essere fatto con un'espressione regolare, perché non puoi risolverlo in una sola produzione EBNF in alcun modo; vi ritroverete con sostituendo S
a tempo indeterminato, che sarà sempre aggiungere un altro a
s e b
s su entrambi i lati. I Lexer (più specificamente: gli automi a stati finiti usati dai lexer) non possono contare su un numero arbitrario (sono finiti, ricordi?), Quindi non sanno quanti a
s erano lì per abbinarli uniformemente con così tanti b
s. Grammatiche come questa sono chiamate grammatiche senza contesto (per lo meno) e richiedono un parser.
Le grammatiche senza contesto sono ben note per l'analisi, quindi sono ampiamente utilizzate per descrivere la sintassi dei linguaggi di programmazione. Ma c'è di più. A volte è necessaria una grammatica più generale, quando hai più cose da contare contemporaneamente, indipendentemente. Ad esempio, quando si desidera descrivere una lingua in cui è possibile utilizzare parentesi tonde e parentesi quadre interlacciate, ma devono essere accoppiate correttamente tra loro (parentesi graffe con parentesi graffe, arrotondate con arrotondate). Questo tipo di grammatica si chiama sensibile al contesto . Puoi riconoscerlo dal fatto che ha più di un simbolo a sinistra (prima della freccia). Per esempio:
A R B --> A S B
Puoi considerare questi simboli aggiuntivi a sinistra come un "contesto" per applicare la regola. Ci potrebbero essere alcune precondizioni, postcondizioni ecc Ad esempio, la regola precedente sostituirà R
in S
, ma solo quando è in mezzo A
e B
, lasciando quelli A
e B
si invariato. Questo tipo di sintassi è davvero difficile da analizzare, perché ha bisogno di una macchina di Turing in piena regola. È tutta un'altra storia, quindi finirò qui.