¿Cuándo es bastante lexing, cuándo necesita EBNF?
EBNF realmente no agrega mucho al poder de las gramáticas. Es solo una conveniencia / notación de acceso directo / "azúcar sintáctico" sobre las reglas gramaticales de forma normal de Chomsky (CNF). Por ejemplo, la alternativa EBNF:
S --> A | B
puede lograr en CNF simplemente enumerando cada producción alternativa por separado:
S --> A // `S` can be `A`,
S --> B // or it can be `B`.
El elemento opcional de EBNF:
S --> X?
puede lograr en CNF utilizando una producción anulable , es decir, la que se puede reemplazar por una cadena vacía (indicada aquí solo por producción vacía; otras usan epsilon o lambda o círculo cruzado):
S --> B // `S` can be `B`,
B --> X // and `B` can be just `X`,
B --> // or it can be empty.
Una producción en una forma como la B
anterior se llama "borrado", porque puede borrar lo que representa en otras producciones (producto una cadena vacía en lugar de otra cosa).
Repetición cero o más de EBNF:
S --> A*
puede obtan mediante el uso de producción recursiva , es decir, una que se incrusta en algún lugar. Se puede hacer de dos maneras. El primero es la recursión izquierda (que generalmente debe evitarse, porque los analizadores descendentes recursivos descendentes no pueden analizarla):
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.
Sabiendo que genera solo una cadena vacía (en última instancia) seguida de cero o más A
s, la misma cadena (¡ pero no el mismo idioma! ) Se puede expresar utilizando la recursión correcta :
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.
Y cuando se trata +
de una o más repeticiones de EBNF:
S --> A+
puede hacerse factorizando uno A
y usando *
como antes:
S --> A A*
que puede expresar en CNF como tal (uso la recursividad correcta aquí; trate de descubrir el otro usted mismo como ejercicio):
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`.
Sabiendo eso, ahora probablemente pueda reconocer una gramática para una expresión regular (es decir, gramática regular ) como una que se puede expresar en una sola producción EBNF que consiste solo en símbolos terminales. En términos más generales, puede reconocer las gramáticas regulares cuando ve producciones similares a estas:
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.
Es decir, usar solo cadenas vacías, símbolos de terminal, no terminales simples para sustituciones y cambios de estado, y usar la recursión solo para lograr la repetición (iteración, que es solo recursión lineal , la que no se ramifica en forma de árbol). Nada más avanzado por encima de estos, entonces estás seguro de que es una sintaxis regular y puedes usar solo lexer para eso.
Pero cuando su sintaxis usa la recursión de una manera no trivial, para producir estructuras anidadas similares a árboles, similares a las siguientes, como la siguiente:
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.
entonces puede ver fácilmente que esto no puede hacerse con expresión regular, porque no puede resolverlo en una sola producción EBNF de ninguna manera; que va a terminar con la sustitución de S
forma indefinida, que siempre será añadir otros a
s y b
s en ambos lados. Los Lexers (más específicamente: Autómatas de estado finito utilizados por lexers) no pueden contar hasta un número arbitrario (son finitos, ¿recuerdan?), Por lo que no saben cuántos a
s estaban allí para combinarlos de manera uniforme con tantos b
s. Las gramáticas como esta se denominan gramáticas libres de contexto (como mínimo) y requieren un analizador sintáctico.
Las gramáticas libres de contexto son bien conocidas por analizar, por lo que se usan ampliamente para describir la sintaxis de los lenguajes de programación. Pero hay más. A veces se necesita una gramática más general, cuando tiene más cosas que contar al mismo tiempo, de forma independiente. Por ejemplo, cuando desea describir un lenguaje en el que uno puede usar paréntesis redondos y llaves cuadradas intercaladas, pero deben estar correctamente emparejadas entre sí (llaves con llaves, redondas con redondas). Este tipo de gramática se llama sensible al contexto . Puede reconocerlo porque tiene más de un símbolo a la izquierda (antes de la flecha). Por ejemplo:
A R B --> A S B
Puede pensar en estos símbolos adicionales a la izquierda como un "contexto" para aplicar la regla. Podría haber algunas precondiciones, postcondiciones, etc. Por ejemplo, la regla anterior sustituirá R
a S
, pero solo cuando esté en el medio A
y B
, dejando a ellos A
y a B
ellos mismos sin cambios. Este tipo de sintaxis es realmente difícil de analizar, ya que necesita una máquina Turing completa. Es otra historia, así que terminaré aquí.