C++, se decidió que la arquitectura del generador sea similar
a la de un compilador, donde se distinguen al menos tres
componentes: Analizador Léxico, Analizador Sintactico y
Generador de Código.
En este punto es necesario mencionar los atributos de
calidad deseados para el generador de FLC. En primera
instancia, dada la reducida cantidad de algoritmos
disponibles en esta primera versión de la herramienta, y
tomando en consideración que la incorporación de nuevas
opciones son parte del trabajo futuro, la escalabilidad se
convierte en un aspecto primordial. Se espera también que
estos cambios puedan ser aplicados por cualquier persona,
por lo que además de escalable debe ser modificable, y para
ello usar herramientas estandarizadas es de gran utilidad.
Otra propiedad deseada es la flexibilidad, para que se adapte
de la mejor manera a la mayor cantidad de problemas. Por
último, el lenguaje usado para describir los controladores
debe ser cómodo de utilizar por el usuario, por lo tanto la
legibilidad o usabilidad es también un atributo de calidad
buscado.
La primera parte del generador desarrollada fue el
analizador léxico, encargado de leer el código escrito por el
usuario, reconociendo palabras reservadas, identificadores y
constantes, y volcando sus características en la tabla de
símbolos. Existen varias herramientas que permiten definir
un analizador léxico en base a una descripción de alto nivel,
sin embargo se optó por una implementación propia. Para
ello se generó un autómata finito que guía el proceso. A
partir del estado 0, se comienza a leer el código del usuario
caracter a caracter, construyendo el lexema y cambiando de
estado hasta llegar al final. En ese momento se identifica el
tipo de lexema (identificador, constante, palabra reservada,
etc.) y se envía el token correspondiente a la próxima etapa
del generador. También puede suceder que se encuentren
errores durante el proceso, en especial cuando se leen
caracteres no reconocidos por el lenguaje. Para evitar que
frente a estos fallos el proceso de traducción se interrumpa,
se creó un estado extra, donde cualquier lexema construido
hasta el momento es descartado, se notifica al usuario del
error y se continúa con la lectura del código.
Luego se implementó el analizador sintáctico, para el
cual se utilizó YACC (Yet Another Compiler Compiler), un
programa que a partir de una gramática permite generar un
parser ascendente [18]. Debido al formato que tiene el
lenguaje de entrada, la gramática implementada se dividió
en cinco conjuntos de reglas, uno para cada sección
(Declare, Fuzz, Rules y Defuzz) y otro para las
características generales del FLC. Esta organización,
sumado al formato estandarizado de YACC, otorga gran
modificabilidad y escalabilidad a la herramienta. Además,
se implementaron una serie de reglas que permiten definir
varios controladores desde un mismo código de entrada.
Esta característica tiene como meta agilizar los tiempos de
desarrollo, evitando que las descripciones de controladores
distintos tengan que compilarse de forma separada.
Con respecto al tratamiento de errores sintácticos, se
desarrollaron algunas reglas especiales con expresiones mal
formadas para capturar fallos comunes. Entre ellos se
incluye la falta de punto y coma al cerrar una sentencia,
secciones del código faltantes o declaradas pero sin
contenido. Los errores no contemplados con estas reglas se
marcan simplemente como un error de sintaxis y, como con
todos los fallos, se le notifica al usuario en que linea se
produjo el problema.
Finalmente se encuentra la etapa de generación de
código, que se divide en dos partes: la traducción a código
intermedio y la traducción a lenguaje de salida. Su
funcionamiento se basa en asociar fragmentos de código a la
gramática anteriormente descrita, que se ejecuta cuando una
regla es reducida. En esta etapa se destaca el uso de una
representación propia que busca maximizar la escalabilidad
del sistema. Es usual que en los compiladores para lenguajes
de alto nivel se usen como código intermedio tercetos,
polaca inversa o árboles sintácticos. No obstante, utilizar
algunas de estas representaciones no tiene sentido en este
trabajo, ya que el lenguaje no es tan flexible como otros de
alto nivel y cuenta con una estructura bien definida
fácilmente identificable gracias a la forma en que se
secciona la gramática. La representación intermedia que se
utilizó en su lugar cuenta con seis estructuras o clases
importantes:
● FuzzySet: Una clase abstracta que abarca los atributos
de un conjunto difuso (nombre y tipo), además de los
métodos usados para la traducción a código de salida
de la etapa de fusificación. Las clases que heredan de
FuzzySet implementan los distintos tipos de funciones
fusificadoras mencionadas en la sección 2.A.
● Variable: Clase formada por un conjunto de
FuzzySet’s. Incluye métodos utilizados en la traducción
de todas las etapas del FLC.
● IOVars: Clase con atributos y métodos estáticos, para
que solo exista una instancia de la misma durante
ejecución. Almacena todas las variables y sus
características reconocidas en el código. Está formada
por dos vectores de tipo Variable, uno para las de
entrada y otro para las de salida.
● Fuzzifier: Clase sin atributos, que utiliza la
información de IOVars para compilar la etapa de
fusificación.
● RulesEval: Clase abstracta que incluye una matriz,
donde se almacenan las reglas declaradas por el
usuario, y una serie de métodos para traducir a código
de salida la etapa de evaluación de reglas. La única
clase que hereda de RulesEval implementa el método
de MinMax.
● Defuzzifier: Clase abstracta que incluye los métodos
necesarios para traducir a código de salida la etapa de
defusificación. La única clase que hereda de
Defuzzifier implementa el método de Centroide.
Como se puede observar, en caso de querer agregar un
nuevo algoritmo de fusificación, evaluación de reglas o
defusificación, solo basta con crear una clase que herede de
la correspondiente clase abstracta e implementar los
métodos solicitados.
Durante esta etapa también se verifican errores
semánticos, lo que incluye: redeclaración de variables,
variables o conjuntos difusos no declarados, número de
parámetros incorrectos para determinado tipo de fusificador,
variables declaradas pero sin conjuntos asociados o
variables de salida sin método defusificador. También se