¿Cuáles son buenos recursos sobre cómo escribir un lexer en C ++ (libros, tutoriales, documentos), cuáles son algunas buenas técnicas y prácticas?
He buscado en internet y todo el mundo dice que use un generador lexer como lex. No quiero hacer eso, quiero escribir un lexer a mano.
Respuestas:
Tenga en cuenta que cada máquina de estados finitos corresponde a una expresión regular, que corresponde a un programa estructurado que usa
if
ywhile
declaraciones.Entonces, por ejemplo, para reconocer enteros, podría tener la máquina de estado:
o la expresión regular:
o el código estructurado:
Personalmente, siempre escribo lexers usando este último, porque en mi humilde opinión, no está menos claro, y no hay nada más rápido.
fuente
*pc
, ¿verdad? Al igualwhile(isdigit(*pc)) { value += pc; pc++; }
. Luego, después de}
convertir el valor en un número y asignarlo a un token.n = n * 10 + (*pc++ - '0');
. Se vuelve un poco más complejo para el punto flotante y la notación 'e', pero no está mal. Estoy seguro de que podría guardar un pequeño código empaquetando los caracteres en un búfer y llamandoatof
o lo que sea. No funcionaría más rápido.Los Lexers son máquinas de estados finitos. Por lo tanto, pueden ser construidos por cualquier biblioteca FSM de propósito general. Para los fines de mi propia educación, sin embargo, escribí el mío, usando plantillas de expresión. Aquí está mi lexer:
Está respaldado por una biblioteca de máquinas de estados finitos basada en iteradores, seguimiento posterior, que tiene ~ 400 líneas de longitud. Sin embargo, es fácil ver que todo lo que tenía que hacer era construir operaciones booleanas simples, como
and
,or
ynot
, y un par de operadores de estilo regex como*
cero o más,eps
para significar "coincidir con cualquier cosa" yopt
significar "coincidir cualquier cosa pero no lo consumas ". La biblioteca es totalmente genérica y se basa en iteradores. El material MakeEquality es una prueba simple para la igualdad*it
y el valor pasado, y MakeRange es una<= >=
prueba simple .Eventualmente, planeo pasar de retroceder a predictivo.
fuente
MakeEquality
fragmentos? Específicamente el objeto devuelto por esa función. Se ve muy interesante.En primer lugar, hay diferentes cosas pasando aquí:
En general, esperamos que un lexer realice los 3 pasos de una vez, sin embargo, este último es inherentemente más difícil y hay algunos problemas con la automatización (más sobre esto más adelante).
El lexer más sorprendente que conozco es Boost.Spirit.Qi . Utiliza plantillas de expresión para generar sus expresiones léxicas, y una vez acostumbrado a su sintaxis, el código se siente realmente ordenado. Sin embargo, se compila muy lentamente (plantillas pesadas), por lo que es mejor aislar las diversas porciones en archivos dedicados para evitar volver a compilarlas cuando no se han tocado.
Hay algunos inconvenientes en el rendimiento, y el autor del compilador de Epoch explica cómo obtuvo una aceleración de 1000x mediante la creación de perfiles e investigación intensivos sobre cómo funciona Qi en un artículo .
Finalmente, también hay código generado por herramientas externas (Yacc, Bison, ...).
Pero prometí una reseña sobre lo que estaba mal con la automatización de la verificación gramatical.
Si echa un vistazo a Clang, por ejemplo, se dará cuenta de que en lugar de usar un analizador generado y algo como Boost.Spirit, en su lugar, se propusieron validar la gramática manualmente usando una técnica genérica de Descent Parsing. ¿Seguramente esto parece al revés?
De hecho, hay una razón muy simple: recuperación de errores .
El ejemplo típico, en C ++:
¿Notaste el error? Falta un punto y coma justo después de la declaración de
Foo
.Es un error común, y Clang se recupera perfectamente al darse cuenta de que simplemente falta y
void
no es una instancia deFoo
sino parte de la próxima declaración. Esto evita los mensajes de error crípticos difíciles de diagnosticar.La mayoría de las herramientas automatizadas no tienen formas (al menos obvias) de especificar esos posibles errores y cómo recuperarse de ellos. A menudo, la recuperación requiere un pequeño análisis sintáctico, por lo que está lejos de ser evidente.
Por lo tanto, existe una compensación al usar una herramienta automatizada: obtienes tu analizador rápidamente, pero es menos fácil de usar.
fuente
Como quiere aprender cómo funcionan los lexers, supongo que realmente quiere saber cómo funcionan los generadores de lexer.
Un generador de lexer toma una especificación léxica, que es una lista de reglas (pares de símbolos de expresión regular) y genera un lexer. Este lexer resultante puede transformar una cadena de entrada (carácter) en una cadena de token de acuerdo con esta lista de reglas.
El método que se usa más comúnmente consiste principalmente en transformar una expresión regular en un autómata finito determinista (DFA) a través de un autómata no determinista (NFA), más algunos detalles.
Una guía detallada de hacer esta transformación se puede encontrar aquí . Tenga en cuenta que no lo he leído yo mismo, pero se ve bastante bien. Además, casi cualquier libro sobre construcción de compiladores presentará esta transformación en los primeros capítulos.
Si está interesado en diapositivas de cursos sobre el tema, no hay duda de que hay una cantidad infinita de cursos sobre la construcción de compiladores. Desde mi universidad, puedes encontrar tales diapositivas aquí y aquí .
Hay algunas cosas más que no se emplean comúnmente en lexers o que se tratan en textos, pero que son bastante útiles:
En primer lugar, manejar Unicode es algo no trivial. El problema es que la entrada ASCII tiene solo 8 bits de ancho, lo que significa que puede tener fácilmente una tabla de transición para cada estado en el DFA, porque solo tienen 256 entradas. Sin embargo, Unicode, que tiene 16 bits de ancho (si usa UTF-16), requiere tablas de 64k para cada entrada en el DFA. Si tiene gramáticas complejas, esto puede comenzar a ocupar bastante espacio. Llenar estas tablas también comienza a tomar bastante tiempo.
Alternativamente, podría generar árboles de intervalos. Un árbol de rango puede contener las tuplas ('a', 'z'), ('A', 'Z'), por ejemplo, que es mucho más eficiente en la memoria que tener la tabla completa. Si mantiene intervalos no superpuestos, puede usar cualquier árbol binario equilibrado para este propósito. El tiempo de ejecución es lineal en la cantidad de bits que necesita para cada carácter, por lo que O (16) en el caso de Unicode. Sin embargo, en el mejor de los casos, generalmente será un poco menos.
Otro problema es que los lexers, como se generan comúnmente, en realidad tienen un rendimiento cuadrático en el peor de los casos. Aunque este comportamiento en el peor de los casos no se ve comúnmente, puede morderte. Si se encuentra con el problema y quiere resolverlo, aquí puede encontrar un documento que describe cómo lograr el tiempo lineal .
Probablemente querrá poder describir expresiones regulares en forma de cadena, como normalmente aparecen. Sin embargo, analizar estas descripciones de expresiones regulares en NFA (o posiblemente una estructura intermedia recursiva primero) es un problema de huevo de gallina. Para analizar descripciones de expresiones regulares, el algoritmo Shunting Yard es muy adecuado. Wikipedia parece tener una página extensa sobre el algoritmo .
fuente