¿Son los lexers y analizadores realmente tan diferentes en teoría?
Parece estar de moda odiar las expresiones regulares: codificar el horror , otra publicación de blog .
Sin embargo, las herramientas populares basadas en lexing: pygments , geshi o prettify , todas usan expresiones regulares. Parecen dejarlo todo ...
¿Cuándo es bastante lexing, cuándo necesita EBNF?
¿Alguien ha usado los tokens producidos por estos lexers con generadores de analizadores bison o antlr?
Respuestas:
Qué analizadores y lexers tienen en común:
Leen símbolos de algún alfabeto de su entrada.
Analizan estos símbolos y tratan de relacionarlos con la gramática del lenguaje que entendieron.
Adjuntan la semántica (significado) a las piezas de lenguaje que encuentran.
*
,==
,<=
,^
serán clasificados como "operador" Token por lexer la C / C ++.[number][operator][number]
,[id][operator][id]
,[id][operator][number][operator][number]
serán clasificados como "expresión" no terminal por el analizador de C / C ++.Pueden adjuntar algún significado adicional (datos) a los elementos reconocidos.
Todos producen en su salida una oración apropiada del lenguaje que reconocen.
[TXT][TAG][TAG][TXT][TAG][TXT]...
.Como puede ver, los analizadores y los tokenizadores tienen mucho en común. Un analizador puede ser un tokenizador para otro analizador, que lee sus tokens de entrada como símbolos de su propio alfabeto (los tokens son simplemente símbolos de algún alfabeto) de la misma manera que las oraciones de un idioma pueden ser símbolos alfabéticos de otro nivel superior. idioma. Por ejemplo, si
*
y-
son los símbolos del alfabetoM
(como "símbolos del código Morse"), puede crear un analizador sintáctico que reconozca las cadenas de estos puntos y líneas como letras codificadas en el código Morse. Las oraciones en el lenguaje "Código Morse" podrían ser tokens para algún otro analizador, para el cual estos tokensson símbolos atómicos de su idioma (p. ej., lenguaje de "palabras en inglés"). Y estas "palabras en inglés" podrían ser tokens (símbolos del alfabeto) para algún analizador de nivel superior que entienda el lenguaje de "oraciones en inglés". Y todos estos idiomas difieren solo en la complejidad de la gramática . Nada mas.Entonces, ¿qué hay de estos "niveles de gramática de Chomsky"? Bueno, Noam Chomsky clasificó las gramáticas en cuatro niveles según su complejidad:
Nivel 3: gramáticas regulares
Utilizan expresiones regulares, es decir, que sólo puede consistir de los símbolos del alfabeto (a
,b
), sus concatenaciones (ab
,aba
,bbb
ETD.), O alternativas (por ejemploa|b
).Se pueden implementar como autómatas de estado finito (FSA), como NFA (autómata finito no determinista) o mejor DFA (autómata finito determinista).
Las gramáticas regulares no pueden manejar con sintaxis anidada , por ejemplo, paréntesis anidados / emparejados correctamente
(()()(()()))
, etiquetas HTML / BBcode anidadas, bloques anidados, etc.Nivel 2: gramáticas sin contexto
Pueden tener ramas anidadas, recursivas y similares en sus árboles de sintaxis, por lo que pueden manejarse bien con estructuras anidadas.Se pueden implementar como autómata de estado con pila. Esta pila se usa para representar el nivel de anidamiento de la sintaxis. En la práctica, generalmente se implementan como un analizador descendente recursivo descendente que usa la pila de llamadas de procedimiento de la máquina para rastrear el nivel de anidación y usa procedimientos / funciones llamados recursivamente para cada símbolo no terminal en su sintaxis.
Pero no pueden manejar con una sintaxis sensible al contexto . Por ejemplo, cuando tiene una expresión
x+3
y en un contexto, estex
podría ser el nombre de una variable, y en otro contexto podría ser el nombre de una función, etc.Nivel 1: gramáticas sensibles al contexto
Nivel 0: gramáticas no restringidas
También llamadas gramáticas enumerables recursivamente.
fuente
STMT_END
en su sintaxis (para el analizador) para indicar el final de las instrucciones. Ahora puede tener un token con el mismo nombre asociado, generado por el lexer. Pero puede cambiar el lexema real que representa. P.ej. se puede definirSTMT_END
como;
tener C / C ++ - como el código fuente. O puede definirloend
para que sea de alguna manera similar al estilo Pascal. O puede definirlo solo'\n'
para finalizar la instrucción con el final de línea, como en Python. Pero la sintaxis de la instrucción (y el analizador sintáctico) se mantiene sin cambios :-) Solo es necesario cambiar el lexer.Sí, son muy diferentes en teoría y en implementación.
Los Lexers se usan para reconocer "palabras" que componen elementos del lenguaje, porque la estructura de tales palabras es generalmente simple. Las expresiones regulares son extremadamente buenas para manejar esta estructura más simple, y existen motores de coincidencia de expresiones regulares de muy alto rendimiento que se utilizan para implementar lexers.
Los analizadores se utilizan para reconocer la "estructura" de las frases de un idioma. Dicha estructura generalmente está mucho más allá de lo que las "expresiones regulares" pueden reconocer, por lo que uno necesita analizadores "sensibles al contexto" para extraer dicha estructura. Los analizadores sensibles al contexto son difíciles de construir, por lo que el compromiso de ingeniería es utilizar gramáticas "sin contexto" y agregar hacks a los analizadores ("tablas de símbolos", etc.) para manejar la parte sensible al contexto.
Es probable que ni la tecnología lexing ni la parsing desaparezcan pronto.
Ellos pueden ser unificados con la decisión de utilizar "analizar" la tecnología para reconocer las "palabras", como se hace actualmente explorada por los llamados analizadores GLR scannerless. Eso tiene un costo de tiempo de ejecución, ya que está aplicando maquinaria más general a lo que a menudo es un problema que no lo necesita, y generalmente paga eso en gastos generales. Donde tenga muchos ciclos libres, esa sobrecarga puede no importar. Si procesa una gran cantidad de texto, la sobrecarga sí importa y se seguirán utilizando los analizadores de expresiones regulares clásicas.
fuente
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:
puede lograr en CNF simplemente enumerando cada producción alternativa por separado:
El elemento opcional de EBNF:
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):
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:
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):
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 :Y cuando se trata
+
de una o más repeticiones de EBNF:puede hacerse factorizando uno
A
y usando*
como antes:que puede expresar en CNF como tal (uso la recursividad correcta aquí; trate de descubrir el otro usted mismo como ejercicio):
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:
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:
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 otrosa
s yb
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ántosa
s estaban allí para combinarlos de manera uniforme con tantosb
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:
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
aS
, pero solo cuando esté en el medioA
yB
, dejando a ellosA
y aB
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í.fuente
Para responder a la pregunta como se le preguntó (sin repetir indebidamente lo que aparece en otras respuestas)
Los lexers y analizadores no son muy diferentes, como lo sugiere la respuesta aceptada. Ambos se basan en formalismos de lenguaje simples: lenguajes regulares para lexers y, casi siempre, lenguajes sin contexto (CF) para analizadores sintácticos. Ambos están asociados con modelos computacionales bastante simples, el autómata de estado finito y el autómata de pila push-down. Los lenguajes regulares son un caso especial de lenguajes libres de contexto, por lo que los lexers podrían producirse con la tecnología CF algo más compleja. Pero no es una buena idea por al menos dos razones.
Un punto fundamental en la programación es que un componente del sistema debe construirse con la tecnología más adecuada, de modo que sea fácil de producir, comprender y mantener. La tecnología no debe ser excesiva (utilizando técnicas mucho más complejas y costosas de lo necesario), ni debe estar al límite de su poder, por lo que requiere contorsiones técnicas para lograr el objetivo deseado.
Es por eso que "parece estar de moda odiar las expresiones regulares". Aunque pueden hacer mucho, a veces requieren una codificación muy ilegible para lograrlo, sin mencionar el hecho de que varias extensiones y restricciones en la implementación reducen su simplicidad teórica. Los Lexers no suelen hacer eso, y suelen ser una tecnología simple, eficiente y apropiada para analizar el token. Usar analizadores CF para el token sería excesivo, aunque es posible.
Otra razón para no utilizar el formalismo CF para los lexers es que podría ser tentador usar toda la potencia de CF. Pero eso podría plantear problemas de estructura con respecto a la lectura de programas.
Fundamentalmente, la mayor parte de la estructura del texto del programa, del cual se extrae el significado, es una estructura de árbol. Expresa cómo se genera la oración de análisis (programa) a partir de las reglas de sintaxis. La semántica se deriva de las técnicas de composición (homomorfismo para los orientados matemáticamente) de la forma en que se componen las reglas de sintaxis para construir el árbol de análisis. Por lo tanto, la estructura del árbol es esencial. El hecho de que los tokens se identifiquen con un lexer basado en un conjunto regular no cambia la situación, porque la CF compuesta con regular todavía da CF (estoy hablando muy libremente sobre transductores regulares, que transforman un flujo de caracteres en un flujo de token).
Sin embargo, la FQ compuesta con FQ (a través de transductores de FQ ... perdón por las matemáticas), no necesariamente da FQ, y puede hacer que las cosas sean más generales, pero menos manejables en la práctica. Por lo tanto, la CF no es la herramienta adecuada para los lexers, aunque puede usarse.
Una de las principales diferencias entre el lenguaje regular y el CF es que los lenguajes regulares (y transductores) se componen muy bien con casi cualquier formalismo de varias maneras, mientras que los lenguajes CF (y transductores) no lo hacen, ni siquiera con ellos mismos (con algunas excepciones).
(Tenga en cuenta que los transductores regulares pueden tener otros usos, como la formalización de algunas técnicas de manejo de errores de sintaxis).
BNF es solo una sintaxis específica para presentar las gramáticas de la FQ.
EBNF es un azúcar sintáctico para BNF , que utiliza las instalaciones de notación regular para dar una versión más tersa de las gramáticas de BNF. Siempre se puede transformar en un BNF puro equivalente.
Sin embargo, la notación regular a menudo se usa en EBNF solo para enfatizar estas partes de la sintaxis que corresponden a la estructura de los elementos léxicos, y deben reconocerse con el lexer, mientras que el resto debe presentarse en BNF directo. Pero no es una regla absoluta.
Para resumir, la estructura más simple del token se analiza mejor con la tecnología más simple de los lenguajes regulares, mientras que la estructura de árbol del lenguaje (de la sintaxis del programa) se maneja mejor con las gramáticas de CF.
Sugeriría también mirar la respuesta de AHR .
Pero esto deja una pregunta abierta: ¿Por qué los árboles?
Los árboles son una buena base para especificar la sintaxis porque
dan una estructura simple al texto
son muy convenientes para asociar la semántica con el texto sobre la base de esa estructura, con una tecnología matemáticamente bien entendida (composicionalidad a través de homomorfismos), como se indicó anteriormente. Es una herramienta algebraica fundamental para definir la semántica de los formalismos matemáticos.
Por lo tanto, es una buena representación intermedia, como lo demuestra el éxito de Abstract Syntax Trees (AST). Tenga en cuenta que AST a menudo son diferentes del árbol de análisis porque la tecnología de análisis utilizada por muchos profesionales (como LL o LR) se aplica solo a un subconjunto de gramáticas de FQ, lo que obliga a distorsiones gramaticales que luego se corrigen en AST. Esto se puede evitar con una tecnología de análisis más general (basada en programación dinámica) que acepte cualquier gramática CF.
La declaración sobre el hecho de que los lenguajes de programación son sensibles al contexto (CS) en lugar de CF son arbitrarios y discutibles.
El problema es que la separación de la sintaxis y la semántica es arbitraria. Las declaraciones de verificación o el acuerdo de tipo pueden verse como parte de la sintaxis o parte de la semántica. Lo mismo sería cierto para el acuerdo de género y número en los idiomas naturales. Pero hay lenguajes naturales donde la concordancia plural depende del significado semántico real de las palabras, por lo que no encaja bien con la sintaxis.
Muchas definiciones de lenguajes de programación en semántica denotacional colocan declaraciones y verificaciones de tipo en la semántica. Por lo tanto, afirmar como lo hizo Ira Baxter que los analizadores de FQ están siendo pirateados para obtener una sensibilidad al contexto requerida por la sintaxis es, en el mejor de los casos, una visión arbitraria de la situación. Puede estar organizado como un truco en algunos compiladores, pero no tiene que ser así.
Además, no es solo que los analizadores CS (en el sentido utilizado en otras respuestas aquí) sean difíciles de construir y menos eficientes. También son inadecuados para expresar claramente el tipo de sensibilidad al contexto que podría ser necesaria. Y, naturalmente, no producen una estructura sintáctica (como parse-trees) que sea conveniente para derivar la semántica del programa, es decir, para generar el código compilado.
fuente
Existen varias razones por las cuales la parte de análisis de un compilador normalmente se separa en fases de análisis léxico y análisis (análisis de sintaxis).
recurso___ Compiladores (2ª edición) escrito por Alfred V. Abo Universidad de Columbia Monica S. Lam Universidad de Stanford Ravi Sethi Avaya Jeffrey D. Ullman Universidad de Stanford
fuente