Pages

Monday, November 21, 2011

Problemas técnicos con Flex++ y Bison++

Un cambio importante entre la fase de traducción del hito actual (v0.0.5) y la implementada en la primera entrada, es la completa migración a C++ de los mismos. El «scanner» de flex ahora ha pasado a ser de flex++ y el «parser» de bison ahora es bison++. Para ambas migraciones, hay un par de retos a superar.

Flex++: redefinición

En el caso de flex++, todo ha sido relativamente cómodo. Existe una opción, que puede indicarse tanto por la línea de comandos de flex como en la sección de declaraciones de su '.l' (ahora pasado a ser '.ll'), que es la opción c++ (%option c++), y ya flex se encarga de hacer la traducción necesaria para tener una clase con funciones miembro y atributos en vez de una serie de funciones y variables globales.

Pero el compilador me da un error de redefinición que no lograba solventar. El problema radica en que, por defecto, el nombre de la clase obtenida es yyFlexLexer, y no puede ser cambiada, al menos no mediante órdenes de flex. Esto es así porque estas clases están definidas (no implementadas) en <FlexLexer.h>, de la librería de flex. Una posible solución sería hacer una copia local y modificarla, pero no es una solución elegante. La otra vía es mediante un cambio de prefijo, con la opción %option prefix="Code" (en mi caso). El fichero .cpp generado (CodeFlexLexer.cpp en mi caso) contiene todas las definiciones con este cambio de prefijo (CodeFlexLexer en vez de yyFlexLexer), pero en <flexlexer.h> dichas redefiniciones no están.

Evidentemente, todos los módulos que necesiten incluir la clase, deberán incluir la cabecera <flexlexer.h> (que no contendrá el cambio de prefijo), así que, si se incluye una variable, por ejemplo "CodeFlexLexer _lexer", el compilador dará un aviso de tipo no declarado. En la documentación, te indican que bastaría incluir el siguiente código en los módulos que lo necesiten:

// Módulo donde usar la clase:
#undef yyFlexLexer
#define yyFlexLexer codeFlexLexer
#include <FlexLexer.h>

De esta forma, el módulo ya conoce el tipo "codeFlexLexer" y todo va de perlas. Pero hay un problema técnico adicional que complica un poco las cosas. La inclusión de #undef yyFlexLexer es por si existen varios lexers diferentes, y así evitar conflictos de nombres (cosa que me hará falta en un futuro).

Analizemos la siguiente situación. Tenemos el módulo CodeParser.hpp (donde tengo mi parser), y el módulo CodeFlexer.hpp, que incluye a <FlexLexer.h>

:
// CodeParser.hpp

#include "CodeFlexer.hpp"

// Definición de la clase CodeParser, con la variable «FlexLexer _lexer»
class CodeParser : public CodeBisonParser
{ ... }
// CodeFlexer.hpp

#undef yyFlexLexer
#define yyFlexLexer CodeFlexLexer
#include <FlexLexer.h>

// Definición de la clase CodeLexer, que hereda de CodeFlexLexer redefinida
class CodeLexer : public CodeFlexLexer
{ ... }
Y por último, el fichero generado por flex, CodeFlexLexer.cpp, que necesita tipos definidos en CodeParser, cuya sección de interés es la siguiente:
// CodeFlexLexer.cpp

// ... definiciones macro de flex

// La sección #undef no viene, porque esta es la versión generada de flex,
// y no le hace falta.
#define yyFlexLexer CodeFlexLexer
#include <FlexLexer.h>

#include "CodeParser.hpp"

// ... implementación de las funciones miembro.

El problema viene de la mano de la compilación de este último eslabón. Si detenemos su compilación después de la fase de preprocesado con la opción -E de gcc, obtendremos algo tal que así:

// CodeFlexLexer.cpp tras fase de preprocesado

// definiciones macro de flex

#define yyFlexLexer CodeFlexLexer
#include <FlexLexer.h>

// Inclusión de CodeLexer a través de CodeParser
#undef yyFlexLexer
#define yyFlexLexer CodeFlexLexer
#include >FlexLexer.h<

class CodeParser : public CodeBisonParser
{ ... };

// Continuación de CodeLexer

class CodeLexer : public CodeFlexLexer
{ ... }

// Continuación de CodeFlexLexer.cpp

// Implementación de las funciones miembro.

Aquí, el problema es que FlexLexer.h no tiene un buen control de las macros sobre su propia inclusión, y <FlexLexer.h> ¡se incluye dos veces!, lo que provoca que "CodeFlexLexer", ¡se defina dos veces!, dando un error de compilación por redefinición:

/usr/include/FlexLexer.h:112:7: error: redefinición de ‘class CodeFlexLexer’
/usr/include/FlexLexer.h:112:7: error: definición previa de ‘class CodeFlexLexer’

Siendo la línea 112 precisamente donde comienza la definición de la clase en dicho fichero de cabecera.

Llegar a descubrir este error me ha llevado varias horas de trabajo, hasta que por fin dí con la tecla, y ahora os brindo la solución:

En el fichero '.l' de flex he añadido las siguientes macros como santo y seña del analizador léxico que voy a generar, que debe añadir antes de la inclusión de cualquier fichero necesitado, que a su vez incluya directa o indirectamente a <FlexLexer.h>

// CodeFlexLexer.ll

%{
// ...

#ifndef _CODE_FLEX_LEXER_
#define _CODE_FLEX_LEXER_
#endif

#include "CodeParser.hpp"
%}

Y en CodeParser:

// CodeParser.hpp

#ifndef _CODE_FLEX_LEXER
#define yyFlexLexer CodeFlexLexer
#include <FlexLexer.h>
#endif

class CodeLexer : public CodeFlexLexer
{ ... }

De esta forma, <FlexLexer.h> solo se incluirá una vez ¡por cada analizador léxico distinto! (por ejemplo, _CODE_FLEX_LEXER y _COMMAND_FLEX_LEXER_).

Comunicación flex-bison

El segundo problema, aunque aquí no hay ni trampa ni cartón, puesto que ésto viene explicado en la documentación de flex por un lado y de bison por otro, es como conseguir comunicar flex y bison.

Hay dos problemas; el primero es que flex no permite definir los parámetros que contendrá la función yylex(), aunque creo que sí puede cambiarse su nombre (se pueden especificar analizadores léxicos reentrantes, pero eso es otro amplio tema en el que no he entrado), y el segundo es que bison++ no incluye ningún lexer como variable, solo una función virtual yylex() (que esta vez sí puede redefinirse tanto en nombre como en parámetros de entrada).

Esta función hay que implementarla manualmente, mediante la opción de bison %define FLEX_BODY <cuerpo>. También se pueden especificar sus miembros con %define MEMBERS <miembros>, donde <cuerpo> y <miembros> es código C++ puro. El nombre de la función puede cambiarse con %define LEX <nuevo_nombre>.

Hay dos opciones. La primera es indicar el analizador léxico como miembro del parser e implementar el cuerpo de FLEX_BODY con una llamada a la función que devuelve el token necesitado. Por ejemplo:

%define LEX lexer 
%define MEMEBERS private: CodeLexer _lexer
%define FLEX_BODY { return _lexer.yylex(); }

La segunda opción, y es la elegida por mí, es especificar la función como virtual pura y no añadir miembros:

%define LEX lexer
%define FLEX_BODY =0

De esta forma, y como la función lexer es virtual, puede implementarse en una clase heredada por él: que en mi caso es CodeParser herendaod de CodeBisonParser. De esta forma, el código queda más elegante y tratable.

Y ahora viene el último problema y eslabón de la comunicación: los parámetros de entrada para la función yylex() del analizador léxico. Como hemos dicho, flex no permite modificar los parámetros de entrada de la función yylex(). Pero flex necesita su famoso "yylval" para poder incluir en él los valores semánticos de cada token devuelto.

La solución consiste, una vez más, en hacer uso de la herencia, en este caso, CodeLexer heredando de CodeFlexLexer. Heredando la clase, se puede especificar la variable yylval como variable privada para que la función tenga acceso a él, y con la opción %option yyclass = "CodeLexer", indicar que flex debe generar la función CodeLexer::yylex() en vez de CodeFlexLexer:yylex(), para que la función sea miembro de la clase heredada y no de su clase base, y así tener acceso a las variables privadas que necesita.

Bison++: definición de tipos

También hay problemas con bison++, y también relacionados con las macros que maneja. Todas las opciones %define generan una macro asociada al valor indicado a continuación:

%name CodeBisonParser
%define lex lexer // Genera YY_CodeBisonParser_lex lexer
%define lex_body = 0 // Genera YY_CodeBisonParser_lex_body = 0

No lo he probado, pero creo que de esta forma podemos generar las macros que queramos. El problema es que bison tiene, repartido en todo su esqueleto de código después de esta sección de declaraciones, sentencias del tipo:

#ifndef YY_CodeBisonParser_NOMBRE
#define YY_CodeBisonParser_NOMBRE VALOR_POR_DEFECTO
#endif

Y <NOMBRE> siempre son mayúsculas, de modo que, si los define se indican en minúsculas, no se cambian sus valores. Por ejemplo:

#define YY_CodeBisonParser_lexer lexer

#ifndef YY_CodeBisonParser_LEXER
#define YY_CodeBisonParser_LEXER yylex
#endif

Como podrá suponerse el lector, el valor final de YY_CodeBisonParser_LEXER es yylex en vez de lexer, que será finalmente el valor usado. Así que hay que tener precaución con este detalle, y siempre hacer estas definiciones en mayúsculas.

Pero hay una excepción, y es la variable STYPE. Este es el nombre dado al tipo de la unión que usa bison para especificar todos los posibles tipos para los valores semánticos de los tokens. Esta variable debe indicar en minúsculas, pero además, hay otro problema.

La macro de bison++ para STYPE tiene la siguiente forma: yy_CodeBisonParser_stype. Como habréis podido observar, el prefijo esta vez es yy en vez de YY. Si se intenta redefinir (en mi caso con CodeValueToken), ocurre lo siguiente:

#define YY_CodeBisonParser_stype CodeValueToken

typedef union { ... } yy_CodeBisonParser_stype;

#define YY_CodeBisonParser_stype yy_CodeBisonParser_stype

Es decir, el valor final de YY_CodeBisonParser_stype es yy_CodeBisonParser_stype, que no está definido como macro, sino directamente como nombre del tipo. Parece que está hecho para no ser modificable, pues el tipo yy_CodeBisonParser_stype se usa directamente sin un #ifndef previo, y finalmente YY_CodeBisonParser_stype se sobreescribe. Por lo tanto, si intentas redefinirlo con la directiva %define no servirá para nada.

La solución consiste en hacer lo siguiente en el fichero .y (en mi caso .yy):

%header{
#ifndef yy_CodeBisonParser_stype
#define yy_CodeBisonParser_stype YY_CodeBisonParser_stype
#endif
%}

// Esto se debe incluir antes de la declaración de la unión y después de las definiciones.
De esta forma, el código generado queda como sigue y el renombrado se consigue con efectividad:
#define YY_CodeBisonParser_stype CodeValueToken;

// Nuestro nuevo código
#ifndef yy_CodeBisonParser_stype
#define yy_CodeBisonParser_stype YY_CodeBisonParser_stype
#endif

typedef union { ... } yy_CodeBisonParser_stype

#define YY_CodeBisonParser yy_CodeBisonParser_stype
Esta inserción funciona gracias a que bison++ inserta los bloques por órden: primero una serie de preámbulos generados automáticamente, luego todas las definiciones de usuario e inserciones de código en el mismo órden en que fueron declaras, y luego el resto del código generado automáticamente. La cuestión del órden es importante en bison/bison++, ya que, como hemos dicho, las definiciones se hacen en el mismo órden; por ello es importante añadir, antes que nada, la órden %define name, ya que a partir de ella se generan los nombres de todas las macros. Luego el resto de directivas %define y %union en cualquier órden deseado, pero siempre y cuando el bloque %header que hemos puesto se añada entre %define stype y %union { ... }. Y un último comentario adicional: bison++ «repite» toda la generación de código relativa a definiciones tanto en el código cabecera como en el .cpp, ya que en el .cpp no se generará una instrucción #define "cabecera.hpp". La órden %header, sin embargo, permite al usuario insertar código personalizable en ambos ficheros, cosa necesaria ya que la definición de stype también se inserta en ambos.

No comments:

Post a Comment