ephp y los Analizadores Sintácticos

Estos días me he propuesto seguir mejorando ephp, mi pequeño proyecto para poder incrustar código PHP dentro de aplicaciones hechas 100% en Erlang/OTP aprovechando las capacidades de Erlang/OTP y la facilidad de escribir código en PHP. El problema está en poder parsear ciertos aspectos de PHP que son un poco extraños, ¿adivinas cuáles?

Este ejercicio me ha ayudado a entender aún más y mejor cómo está hecha la sintaxis de PHP, dar un buen repaso a operadores que no conocía (incluso nuevos que se han incluído en la versión 7 de PHP) y pensar un poco más a niveles de optimización.

Experiencia con ephp

Hace tiempo anuncié en en la lista de Erlang ephp, recibí algunas estrellas en github y hubo gente comentándolo en distintos lugares. El software era bastante simple. Un parseador de tipo PEG llamado neotoma me ayudaba a analizar el código PHP y luego un intérprete se encargaba de la ejecución. Todo separado en esas dos capas.

Tuve buenos momentos implementando cada parte y viendo como progresaba como cuando conseguí incrementar el rendimiento 10 veces, pero también problemas como no poder resolver la implementación de heredoc.

Hubo personas como Joe Armstrong o Robert Virding que me aconsejaron emplear algún analizador LALR en lugar de PEG. Sean Cribbs no tenía una solución para el problema de la implementación de heredoc.

Realmente me hizo replantearme el porqué usaba lo que usaba y me hizo estudiar un poco más sobre los analizadores sintácticos.

PEG (Parsing Expression Grammars)

Este tipo de analizadores está basado en el grupo de gramática formal analítica introducido por Bryan Ford en 2004. El texto en este caso es analizado de arriba-abajo (top-down parsing) y las cadenas de texto tienen que concordar con el árbol definido del análisis.

El problema de la especificación del lenguaje PEG es que algo como:

'<<<' c1:Constant .+ c1:Constant ';'

No funciona al no reconocer que c1:Constant tiene que ser igual al terminador. Algo como esto:

<<<EOF

cadena
END;

EOF;

Capturaría como final END; en lugar de EOF;. No obstante, para el análisis de un DSL simple, como ya dije en un post anterior, este tipo de gramática sigue siendo muy simple y útil.

LARL(1) (Look-Ahead Left to Right) parser

Son los más usados. Al menos siempre los he visto presentes tanto en GNU/Linux (flex y bison), basados en los antiguos de Unix (lex y yacc), también disponibles en entornos como Erlang/OTP (leex y yecc). Estas herramientas se usan ambas juntas, la primera (analizador léxico) se encarga de convertir el texto en tokens. El segundo (analizador gramatical o sintáctico) se encarga de comprobar que los tokens están en un orden válido.

La ventaja de este sistema es que ambas tareas suelen ser atómicas y sencillas, y por lo tanto rápidas. El problema está en el mismo que en el ejemplo anterior. Al crear los tokens no podemos decirle al analizador léxico qué es un heredoc ni como tratarlo de forma correcta.

Enfoque heurístico

Después de estudiar un conjunto de elementos como los anteriores (yendo a la teoría) pasando por LALR(1), LL, GLR y PEG, he decidido dejar de buscar teoría e intentar encontrar un enfoque heurístico que me ayude a resolver el problema.

En principio hice un poco de trampa. PHP es un lenguaje que ya se parsea para poder ejecutarse y hay varias implementaciones:

Pero por simplificar me decidí primero por mirar el código fuente de Zend, el original. Ahí descubrí que usan en analizador léxico pero con mucho código C incrustado e igual en el analizador sintáctico.

Al final detecté que usan estados. Cuando inician están en modo documento y van capturando todo hasta encontrar la etiqueta de inicio de código PHP. En ese momento cambian a código. Esto se repite en cada línea. Si se encuentra un caracter alfabético o un guión bajo se toma como constante, si justo después de la constante se abre un paréntesis… cambiamos a función.

Al final, mantener los estados y siempre una pila de los elementos que se van analizando me ha ayudado a construir un parseador que maneja mejor el analisis léxico y sintáctico.

La nota negativa…

Siempre hay un pero. El problema en este caso es la extensión. El parseador de PHP (aunque no funcional en algunas partes como heredoc y un poco mal la parte de print y echo) ocupaba pocas líneas de código (para lo que se espera).

En comparación, el enfoque heurístico es muchísimo mayor. Para poder aprovechar la potencia de la concordancia en Erlang/OTP me he visto obligado a introducir muchas guardas y eso mezclado con el case insensitive de PHP ha hecho que cada palabra reservada fuese difícil de agregar y no muy clara de leer en el código final.

Aún así, entre el incremento de rendimiento que espero alcanzar, el control del análisis que me permite limpiar y agregar más elementos fácilmente y el hecho de estar todo en Erlang/OTP, me da más seguridad a la hora de continuar. Esto no quita que en un futuro no implemente un BNF, EBNF o ABNF para simplificar lo realizado.

Conclusiones

Hablando desde el punto de vista del proyecto ephp mi sensación es muy positiva. He aprendido mucho sobre los analizadores de forma práctica aunque un poco errática. Me queda aún bastante camino por aprender pero estoy motivado.

Y vosotros, ¿habéis jugado con este tipo de herramientas?, ¿sabíais que existen tanto tipos de analizados?, ¿tenéis experiencias positivas? ¿negativas? ¡Dejada un comentario!