scieee AI-readable full text Open interactive document viewer

Desarrollo de una herramienta de refactorización en Erlang integrada en un IDE

Tomás Franco, César

Full text

Desarrollo de una Herramienta de Refactorización en Erlang Integrada en un IDE César Tomás Franco Universidad Politécnica de Valencia Directores: Germán Fco. Vidal Oriola Salvador Tamarit Muñoz Valencia, Septiembre 2010 2 Índice 1. Introducción ...................................................................................................................... 3 1.1 Concurrencia ..................................................................................................................... 4 1.2 Erlang ................................................................................................................................. 6 1.2.1 Historia de Erlang ...................................................................................................... 7 1.3 Refactorización .......................................................................................................... 9 1.3.1 Herramientas de Refactorización ............................................................................ 12 Distribución del documento ..................................................................................................... 15 2. Diseño .............................................................................................................................. 16 2.1. Front-End ......................................................................................................................... 17 2.2. Interfaz ............................................................................................................................ 20 3. Implementación .............................................................................................................. 22 3.1. Estructura General de las transformaciones ................................................................... 22 3.2. Implementación de las refactorizaciones ....................................................................... 26 3.2.1 Refactoring Sobre Listas .......................................................................................... 26 3.2.2 Refactoring Sobre Declaraciones de Import ........................................................... 30 3.2.3 Refactoring Sobre Funciones con una sola instrucción case ................................... 33 4. Integración en Eclipse ..................................................................................................... 36 5. Manual de usuario .......................................................................................................... 40 Conclusión ................................................................................................................................ 46 Bibliografía ................................................................................................................................ 47 Apéndices ................................................................................................................................. 49 Apéndice A - The Abstract Format ......................................................................................... 49 Apéndice B - SMERL ............................................................................................................... 56 3 1. I NTRODUCCIÓN El presente proyecto se centra en la implementación de una herramienta para refactorizar programas codificados mediante el lenguaje Erlang. Esta herramienta está integrada en un entorno de desarrollo dotándola así de una mayor usabilidad y funcionalidad. A continuación se va a definir a grandes rasgos algunos de los conceptos clave para entender de forma clara la temática de este proyecto. Erlang [5] es un lenguaje de programación funcional con soporte de programación concurrente y un sistema de ejecución que incluye una máquina virtual y bibliotecas. Originalmente, Erlang era un lenguaje propietario de la compañía Ericsson, pero fue cedido como software de código abierto en 1998. La mayor fortaleza de Erlang es el soporte para concurrencia. Tiene un pequeño pero potente conjunto de primitivas para crear procesos y comunicar entre los mismos. El soporte para procesos distribuidos es también parte de Erlang. Los procesos se pueden crear en nodos remotos, y la comunicación con ellos es transparente. El término refactoring o refactorización [16] se usa a menudo para describir la modificación del código fuente sin cambiar su comportamiento, lo que se conoce informalmente por limpiar el código. La refactorización se realiza a menudo como parte del proceso de desarrollo del software: los desarrolladores alternan la inserción de nuevas funcionalidades y casos de prueba con la refactorización del código para mejorar su consistencia interna y su claridad. Los test aseguran que la refactorización no cambia el comportamiento del código. La refactorización es la parte del mantenimiento del código que no arregla errores ni añade funcionalidad. El objetivo, por el contrario, es mejorar la facilidad de comprensión del código o cambiar su estructura y diseño y eliminar código muerto, para facilitar el mantenimiento en el futuro así como incrementar su eficiencia. En este proyecto se va a diseñar una herramienta que analizará el código proporcionado y devolverá el programa refactorizado (atendiendo a los distintos criterios de refactorización contemplados) con la misma funcionalidad. En este proyecto se van a tener en cuenta diversos criterios a la hora de aplicar las refactorizaciones a un programa Erlang dado. Por último, destacaremos el hecho que esta herramienta esté integrada el entorno de programación (IDE) Eclipse [21]. Se puede considerar que este es uno de los puntos fuertes del desarrollo del proyecto, ya que hoy en día la mayor parte de desarrollo de software (y especialmente el de Erlang) se realiza bajo este tipo entornos. Los IDE proporcionan un marco sencillo y amigable a la hora de desarrollar software al tener múltiples opciones y herramientas enfocadas a facilitar al programador su tarea de codificación. Pese a que Erlang es un lenguaje relativamente nuevo, existen actualmente diversos entornos de programación que ya soportan dicho lenguaje, en el que se proporcionan 4 herramientas (como por ejemplo debuggers) y funcionalidades que simplifican enormemente el esfuerzo dedicado por los programadores a tal fin. Así, la integración de esta herramienta en Eclipse le proporciona una usabilidad y funcionalidad que es esencial para que esta sea aceptada y usada por la comunidad de programadores que estén interesados en el desarrollo de software con el lenguaje Erlang. Con este proyecto se pretende impulsar y fomentar varios conceptos que están experimentando un aumento en el interés de la comunidad de programadores, como son la programación funcional, la concurrencia además de la eficiencia y legibilidad del código desarrollado. Si a esto le añadimos el hecho de que esta herramienta esté integrada en un entorno de programación podemos concluir que nos encontramos delante de un proyecto que puede llegar a tener una aplicación práctica realmente útil. 1.1 C ONCURRENCIA Concurrencia en el ámbito de la computación se conoce por la habilidad de diferentes tareas para ejecutarse simultáneamente sin afectarse entre ellas (en términos de comunicación y sincronización) a menos que se desee explícitamente. Según Joe Amstrong en [4], el mundo real en el que vivimos es concurrente, sin embargo la mayoría de los lenguajes de programación que usamos para escribir programas que interactúan con el mundo real son generalmente secuenciales. El hecho que se usen lenguajes de programación esencialmente secuenciales para codificar programas concurrentes dificulta aún más esta tarea e induce a la noción de que la programación concurrente es complicada. Efectivamente si escribimos algún programa concurrente en lenguajes como Java o C++ que están diseñados para la programación secuencial nos encontraremos ante un problema realmente complicado. Por el contrario, existe una generación de lenguajes orientados a la concurrencia, como son Erlang u Oz [23], en los que codificar programas concurrentes es la forma natural de expresarse. Joe Amstrong afirma que para poder clasificar un lenguaje como un Lenguaje Orientado a la Concurrencia se debería aplicar el siguiente criterio: - Debe permitir crear un gran número de procesos. - La creación y destrucción de procesos debe ser muy eficiente. - El paso de mensajes entre procesos debe tener un coste (computacional, temporal) muy bajo. - Los procesos no deben compartir información y deberían comportarse como si estuvieran ejecutándose en maquinas distintas. El concepto de proceso es muy conocido entre los programadores y los científicos de computación, lamentablemente pocos lenguajes o sistemas operativos están enfocados para proporcionar al programador usabilidad respecto a estos. En la mayoría de los lenguajes el modelo de concurrencia es el mismo que el del sistema operativo subyacente. Esto implica, 5 por ejemplo, que un programa concurrente escrito en Java ejecutándose en un sistema operativo (con sus mecanismos de creación de procesos o política de planificación particular) tiene una semántica completamente diferente a la que tendría ejecutándose en otro sistema operativo. Esta dependencia del sistema operativo subyacente también ofrece desventajas en el sentido que los sistemas operativos más utilizados actualmente no soportan “procesos ligeros”, además de no permitir la ejecución concurrente de un gran número de procesos. Joe Amstrong afirma que la concurrencia debería ser responsabilidad del lenguaje de programación y no del sistema operativo sobre el que se está ejecutando, hecho que se da precisamente en el lenguaje Erlang. Para ilustrar esto Joe Amstrong nos ofrece multitud de pruebas y experimentos realizados sobre distintos sistemas operativos y lenguajes de programación que nos muestran el alto coste asociado a la creación y destrucción de procesos así como el paso de mensajes entre estos. A continuación se va a detallar cual es la filosofía adoptada por Erlang respecto a su modelo de concurrencia. - En Erlang los procesos son “ligeros”, esto significa que se necesita muy poco coste para la creación o destrucción de estos, concretamente del orden de magnitud de una o dos veces más “ligeros” o rápidos que los hilos de los sistemas operativos. - Además, se pueden crear cientos o miles de estos procesos sin acusar un descenso notable del rendimiento del sistema (a menos, claro está, que todos estuviera ejecutando “algo” al mismo tiempo). - Los procesos en Erlang no comparten ningún tipo de información, hecho que incrementa enormemente la eficiencia del código. La única forma de intercambiar la información es mediante paso de mensajes explícito. Los mensajes en Erlang no contienen punteros, así que cada proceso debe trabajar con una copia de la información que requiere, hecho que propicia una programación simple de sistemas tolerantes a fallos. Así, la sincronización se realiza también mediante el intercambio de mensajes. Las principales ventajas que ofrece la filosofía de no compartir información son: o Los sistemas son fácilmente distribuibles, para convertir un programa en distribuible es tan sencillo como ubicar los distintos procesos paralelos en maquinas distintas. o Los sistemas se pueden hacer fácilmente tolerantes a fallos, distinguiendo entre procesos ejecutores (o “workers”) y procesos observadores, que se encargan de realizar el tratamiento del error en el caso de que alguna cosa haya fallado en los procesos ejecutores. o Facilidad en la escalabilidad de los sistemas, añadiendo más procesadores y moviendo procesos entre estos. Supongamos que quisiéramos programar un servidor de mensajería instantánea en Erlang, que soportara transmisión de mensajes entre miles de usuarios en un sistema como lo es Google Talk o el chat de Facebook. La filosofía de diseño de Erlang es asignar un proceso nuevo a cada evento para que la estructura del programa refleje directamente la concurrencia de múltiples usuarios intercambiando mensajes. En un sistema de mensajería instantánea, un evento podría considerarse a una actualización de presencia de usuario, un mensaje enviado o recibido, una solicitud de acceso… Cada proceso se encargaría del evento que maneja y terminaría cuando la petición hubiera sido completada. Se podría hacer lo mismo en lenguajes tales como C o Java, pero ofrecerían grandes dificultades a la hora de escalar el sistema a cientos de miles de eventos simultáneos. Erlang no presenta estas dificultades ya que no usa los hilos del sistema subyacente para representar los procesos, sino que tiene su propio planificador en la maquina virtual, que permite crear procesos de forma muy eficiente y 6 minimizando su uso de memoria. Esta eficiencia se mantiene sin tener en cuenta el número de procesos concurrentes presentes en el sistema. Con todo esto, se puede afirmar que el uso de Erlang para programar sistemas concurrentes implica usar el modelo de concurrencia más potente que se conoce hoy en día. 1.2 E RLANG Como hemos introducido anteriormente, Erlang es un lenguaje de programación concurrente y un sistema de ejecución que incluye una máquina virtual y bibliotecas. El subconjunto de programación secuencial de Erlang es un lenguaje funcional, con evaluación estricta, asignación única y tipado dinámico. Fue diseñado para realizar aplicaciones distribuidas, tolerantes a fallos, soft-real-time y de funcionamiento ininterrumpido. Proporciona el cambio en caliente del código de forma que éste se puede cambiar sin parar el sistema. La creación y gestión de procesos es trivial en Erlang, mientras que, en muchos lenguajes, los hilos de ejecución se consideran un apartado complejo y propenso a errores. En Erlang toda concurrencia es explícita, los distintos procesos se comunican usando el paso de mensajes mediante variables compartidas, sin tener la necesidad de usar bloqueos. A continuación se describen las principales características del lenguaje Erlang: - Lenguaje Funcional: Constituido únicamente por definiciones de funciones, entendiendo éstas no como subprogramas clásicos de un lenguaje imperativo, sino como funciones puramente matemáticas, en las que se verifican ciertas propiedades como la transparencia referencial, y por tanto, la carencia total de efectos laterales. - Tiempo real: Erlang está diseñado para programar sistemas complejos de tiempo real cuando se requiere un tiempo de respuesta de orden de milisegundos. - Operación continua: Erlang tiene primitivas que permiten reemplazar código en un sistema en ejecución permitiendo a las distintas versiones del código ejecutarse al mismo tiempo. Esta característica tiene una gran aplicación en sistemas que no se pueden interrumpir para realizar cambios en el software, tales como sistemas de control de tráfico aéreo, intercambio telefónico…etc. - Concurrencia: Erlang tiene un modelo basado en el proceso de concurrencia con paso de mensajes asíncrono. Los mecanismos de concurrencia en Erlang requieren muy poca memoria permitiendo así soportar aplicaciones con un número muy elevado de procesos concurrentes. Además, la creación y destrucción de procesos y paso de mensajes requiere muy poco coste computacional. - Robustez: La seguridad es un requerimiento crucial en ciertos sistemas como los descritos anteriormente. Erlang provee tres construcciones para la detección de 7 errores en tiempo real, que pueden ser utilizadas para programar aplicaciones robustas en cuanto a seguridad se refiere. - Gestión de la memoria: Erlang es un lenguaje de programación simbólico con un recolector de basura en tiempo real. La memoria se asigna automáticamente cuando es requerida y se desasigna cuando lleva un tiempo si ser utilizada. De esta forma nunca pueden aparecer los típicos errores de asignación de memoria. - Distribución: Erlang no tiene memoria compartida. Todas las interacciones entre procesos se realizan mediante paso de mensajes asíncrono, de esta forma la construcción de sistemas distribuidos se realiza de forma muy sencilla. Cabe destacar también que aplicaciones diseñadas para ser ejecutadas en un solo procesador se pueden migrar para ser ejecutadas en redes de procesadores fácilmente. - Integración: Erlang tiene un mecanismo de “puerto” que permite a los diferentes procesos comunicarse con el “exterior” (otros sistemas o procesos escritos en otros lenguajes de programación) de forma semánticamente equivalente al paso de mensajes entre los procesos en Erlang. El objetivo inicial para el cual se concibió Erlang fue la producción de un lenguaje pequeño, simple y eficiente para programar aplicaciones industriales concurrentes robustas a gran escala. Así, por razones de eficiencia se evitaron incluir muchas características presentes en los lenguajes lógicos o funcionales modernos, tales como currificación, evaluación perezosa, variables lógicas…etc. No obstante su ausencia no representa un inconveniente a la hora de programar aplicaciones típicas de control industrial. El uso de la sintaxis de ajuste de patrones (pattern matching) junto con la propiedad de asignación única de variables de Erlang dan como resultado programas claros, cortos y fiables. 1.2.1 H ISTORIA DE E RLANG La historia de Erlang es importante para poder entender su filosofía. Erlang fue desarrollado para resolver requerimientos de sistemas distribuidos, tolerantes a fallos, masivamente concurrentes y de tiempo real. El hecho de que servicios web, computación de telefonía, sistemas de mensajes, integración empresarial, por mencionar solo algunos, comparten los mismos requerimientos en sistemas de telecomunicación explica porque Erlang ha ganando mucho terreno en todos estos sectores. A mediados de los 80, el Laboratorio Científico de Computación de Ericsson empezó a investigar en lenguajes de programación para su nueva generación de productos de telecomunicaciones. Joe Armstrong, Robert Virding, y Mike Williams (bajo la supervisión de Bjarne Däcker) estuvieron dos años realizando prototipos de aplicaciones de telecomunicación con muchos de los lenguajes de programación disponibles en ese momento. Su conclusión fue que muchos de los lenguajes tenían características interesantes y no existía un único lenguaje que las abarcara casi todas, así que decidieron crear su propio lenguaje. Erlang estuvo influenciado por lenguajes funcionales tales como ADA, Modula y Chill, así como el lenguaje de programación lógica Prolog. Se tuvieron en cuenta las propiedades de actualización del software de Smalltalk, al igual que otras propiedades de los lenguajes de Ericsson EriPascal y PLEX . 8 Con una máquina virtual de Erlang basada en Prolog (VM) (programación lógica), se prototiparon aplicaciones de telecomunicaciones con una evolución de lenguaje que al final se fue convirtiendo en el lenguaje Erlang tal y como lo conocemos actualmente (lenguaje funcional). En 1991, Mike Williams creó la primera máquina virtual basada en C y un año después se puso en marcha el primer proyecto comercial con un pequeño grupo de desarrolladores. Este proyecto consistía en un servidor de movilidad que permitía comunicarse mediante teléfonos inalámbricos a través de redes de oficinas privadas. Este producto se puso en marcha finalmente en 1994 y proporcionó información valiosa sobre algunas mejoras que fueron integradas en Erlang en 1995. Fue entonces cuando se consideró suficientemente maduro para su uso en grandes proyectos con cientos de desarrolladores tales como la banda ancha de Ericsson, GPRS o soluciones de conmutación ATM. Junto con estos proyectos se desarrolló el framework Open Telecom Platform, OTP que fue desarrollado por Ericsson para programar la siguiente generación de conmutadores. OTP incluía el sistema completo de desarrollo en Erlang junto con un conjunto de librerías escritas en Erlang y otros lenguajes. En 1998 Ericsson lanzó Erlang y las librerías OTP como código abierto. Actualmente OTP representa el uso comercial del lenguaje de programación funcional más importante fuera del ámbito académico. El lanzamiento de Erlang como código abierto se realizó sin presupuesto ni comunicados de prensa ni ayuda del departamento de marketing de las empresas. En enero de 1999 el sitio web erlang.org tenía unas 36000 impresiones. Diez años después este número ha alcanzado los 2.8 millones. Este aumento es un reflejo de una comunidad cada vez mayor como resultado de una combinación de éxito comercial, investigación y proyectos de código abierto, blogs, y libros, todos impulsados por la necesidad de resolver problemas de software complejos en el dominio para el que Erlang originalmente fue concebido. Actualmente nos podemos encontrar con numerosos e importantes proyectos desarrollados en Erlang tales como: - Sistema de chat de Facebook - Sistema de chat de la red social Tuenti - Ejabberd: Servidor de mensajería instantánea - Yahoo! Delicious - Amazon SimpleDB: Base de datos distribuida que forma parte de los servicios web de Amazon. - Twitterfal: Servicio para ver tendencias y diseños de Twitter. - Wings 3D: Modelador 3D - Servidor web Yaws 9 1.3 R EFACTORIZACIÓN Escribir código de forma clara y simple es una tarea recomendable, esto sin embargo puede resultar difícil incluso tedioso. Para poder adquirir esta habilidad se requiere mucha experiencia en el desarrollo de programas en dicho lenguaje y un buen conocimiento de todas las construcciones del lenguaje disponibles. De esta forma se pueden encontrar alternativas a la hora de expresar de distintas formas las intenciones de programación, incluso aplicar un poco de “disciplina” o buenas prácticas a la hora de escribir código. Para ayudar a los programadores a escribir código “de calidad” la mayoría de los lenguajes actuales tienen asociados sitios web, libros o incluso blogs que proporcionan abundante información sobre buenas prácticas de codificación. No obstante, los programadores normalmente muestran cierta reticencia a la lectura y estudio de estos documentos, tendiendo a escribir y acumular grandes cantidades de código de baja calidad. Se denomina refactoring al proceso de mejora del diseño de un programa sin cambiarle el comportamiento externo. Así, esta preservación del comportamiento garantiza que el refactoring no introduce o elimina ningún error. El objetivo principal que persigue la técnica del refactoring es eliminar las grandes cantidades de código de baja calidad existente, mejorando su facilidad de comprensión, cambiando su estructura y diseño, incluso eliminando código muerto, facilitando su mantenimiento en el futuro así como incrementando su eficiencia. La refactorización no se basan únicamente en la sintaxis de los programas, sino que también requiere conocimiento semántico (tipos, estructura de módulos, …) adquirido mediante diversos análisis estáticos. Además, cada refactorización tiene normalmente asociadas una serie de precondiciones que han de tenerse en cuenta antes de poder aplicarse. A continuación introducimos una serie de ejemplos que muestran distintos fragmentos de código Erlang con el objetivo de ilustrar este concepto de una forma más clara: - Ejemplo 1: En este ejemplo se muestra una de las posibles aplicaciones de refactoring más sencillas que se pueden encontrar, a la par que efectivas en algunos casos. La transformación “extracción de función” extrae una expresión o una secuencia de expresiones de una función a una nueva función diferente. Esta transformación crea una nueva definición de función y reemplaza el código en cuestión con una aplicación de dicha función. Las variables que se usan en el interior del código pero son declaradas o usadas en el exterior se convierten en parámetros formales de la nueva función. En el siguiente código se puede observar como quedaría el código después de aplicar esta transformación: 2. D ISEÑO En este punto se va a detallar la aproximación elegida en este proyecto para el desarrollo del mismo. Después de investigar desarrollo de la herramienta de refactorización se optaron por los siguientes componentes y características: - En cuanto al frontend distribuc ión Erlang/OTP. Esta librería contiene módulos para el manejo de los Árboles de sintaxis abstracta (AST) de Erlang, proporcionando numerosas funcionalidades para realizar análisis sintáctico (Parsing) de código Erlang. - Para la implementación representación estándar de los Árboles de Sintaxis Abstracta de Erlang o también conocido como Abstract Format - Si nos fijamos en la interfaz con el usuario basado en la herramienta de refactorización para el desarrollo de Erlang en el IDE Eclipse) han permitido la integración de esta herramienta de refactorización en dicho entorno de desarrollo tan extendido y usado actualmente. En la figura 2.1 se puede ver un diagrama de la implementación de la arquitectura de la herramienta de refactorización. Usuario Figura 2.1: Arquitectura general de la herramienta de refactorización. A continuación se va a describir con más detalle cada uno de lo arquitectura elegida para el desarrollo de esta herramienta. Eclipse / Erlide Wrangler ISEÑO En este punto se va a detallar la aproximación elegida en este proyecto para el desarrollo del mismo. Después de investigar un poco sobre las distintas opciones disponibles para el desarrollo de la herramienta de refactorización se optaron por los siguientes componentes y end de Erlang elegido, se ha optado por las SyntaxTools ión Erlang/OTP. Esta librería contiene módulos para el manejo de los Árboles de sintaxis abstracta (AST) de Erlang, proporcionando numerosas funcionalidades para realizar análisis sintáctico (Parsing) de código Erlang. implementación de los distint os refactorings se ha decidido trabajar con la representación estándar de los Árboles de Sintaxis Abstracta de Erlang o también Abstract Format [Apéndice A]. la interfaz con el usuario hay que destacar que este proyecto se h basado en la herramienta de refactorización Wrangler, que junto con para el desarrollo de Erlang en el IDE Eclipse) han permitido la integración de esta herramienta de refactorización en dicho entorno de desarrollo tan extendido y usado se puede ver un diagrama de la implementación de la arquitectura de la herramienta de refactorización. Programa Fuente Abstract Format Figura 2.1: Arquitectura general de la herramienta de refactorización. A continuación se va a describir con más detalle cada uno de lo s componentes presentes en la arquitectura elegida para el desarrollo de esta herramienta. Eclipse / Erlide Wrangler Front-End Erlang Syntax_tools 16 En este punto se va a detallar la aproximación elegida en este proyecto para el desarrollo del un poco sobre las distintas opciones disponibles para el desarrollo de la herramienta de refactorización se optaron por los siguientes componentes y SyntaxTools de la ión Erlang/OTP. Esta librería contiene módulos para el manejo de los Árboles de sintaxis abstracta (AST) de Erlang, proporcionando numerosas funcionalidades para os refactorings se ha decidido trabajar con la representación estándar de los Árboles de Sintaxis Abstracta de Erlang o también hay que destacar que este proyecto se h a que junto con Erlide (plug-in para el desarrollo de Erlang en el IDE Eclipse) han permitido la integración de esta herramienta de refactorización en dicho entorno de desarrollo tan extendido y usado se puede ver un diagrama de la implementación de la arquitectura de la Programa Fuente Abstract Format Figura 2.1: Arquitectura general de la herramienta de refactorización. s componentes presentes en la Refactoring 17 2.1. F RONT -E ND Después de investigar en las distintas opciones disponibles en cuanto a Front-End (parte del software que interactúa con el o los usuarios) de Erlang, se decidió implementar la herramienta de refactorización con la infraestructura proporcionada por la librería SyntaxTools [26] de la distribución oficial de Erlang/OTP. La forma de trabajar sobre esta librería se ha realizado mediante un módulo llamado SMERL [20] que abstrae y simplifica enormemente el proceso de generación y manipulación de código Erlang en tiempo de ejecución. Para desarrollar las distintas refactorizaciones se decidió trabajar directamente sobre el Abstract Format de Erlang. A continuación se explican con más detalle los distintos componentes mencionados: - La librería SyntaxTools de Erlang contiene módulos para el manejo de Arboles de Análisis Sintáctico (AST) de forma que sean compatibles con los AST del módulo de la librería estándar erl_parse [27] junto con utilidades para la lectura de archivos de código fuente, impresión de dichos AST y reversión al código fuente. Así, la capa abstracta definida por las SyntaxTools (definida en el módulo erl_syntax) está muy bien estructurada con los tipos de los nodos independientes del contexto. Esta capa permite conectar de forma transparente los comentarios de código fuente y las anotaciones de usuario con los nodos del árbol. El uso de esta capa abstracta hace a las aplicaciones menos sensibles a los cambios en las estructuras de datos de erl_parse, y sólo requiere el módulo erl_syntax para estar actualizada. o En la librería erl_parse se definen las funciones básicas para realizar tareas de análisis sintáctico de Erlang, que convierte los distintos componentes léxicos en la forma abstracta, ya sean formularios (por ejemplo, construcciones de alto nivel), expresiones o términos. En el siguiente ejemplo se puede ver el uso de esta librería trabajando sobre la instrucción: foo()-> “Hello!”. 1> erl_scan:string("foo()->\"Hello!\".") [{atom,1,foo},{'(',1},{')',1},{'->',1}{string,1,"Hello!"}, {dot,1}] 2> erl_parse:parse_form([{atom,1,foo},{'(',1},{')',1},{'- >',1}, {string,1,"Hello!"}, {dot,1}]). {ok,{function,4,foo,0,[{clause,4,[],[],[{string,4,"Hello!" }]}]}} Se puede observar como el módulo erl_scan de la librería estándar se usa para la conversión de código en los distintos tokens (o componentes léxicos), y que el resultado de la llamada a la función parse_form del módulo erl_parse devuelve la forma abstracta de dichos tokens. - Las capacidades que ofrece Erlang en cuanto a su flexibilidad a la hora de manipular código en tiempo de ejecución (o metaprogramación) queda mermada por la densidad 18 y complejidad de los módulos que las contienen. Es por este motivo por el que se decidió usar el módulo SMERL (Simple Metaprogramming for Erlang), librería que facilita y abstrae el uso de las librerías relacionadas con el área de la metaprogramación en Erlang. El módulo SMERL, desarrollado por Yariv Sadan y publicado en su blog sobre Erlang [13], permite tanto la creación de nuevos módulos como la manipulación de los existentes en tiempo de ejecución. A continuación mostramos un pequeño ejemplo que ilustra la facilidad con la que se puede crear un módulo usando el módulo SMERL: 3> test_smerl()-> % Creamos el modulo y le añadimos la función bar() C1 = smerl:new(foo), {ok, C2} = smerl:add_func(C1, "bar() -> 1 + 1." ), smerl:compile(C2), foo:bar(). % devuelve 2 En este proyecto se ha hecho uso de este módulo sobretodo en la lectura/escritura de ficheros y su transformación en el Abstract Format, como se detallará en el apartado de implementación (Capítulo 3). - El Abstract Format es la representación estándar de los árboles sintácticos de programas Erlang como términos Erlang. Para aplicar las refactorizaciones en este proyecto se ha decidido trabajar directamente sobre el Abstract Format del código (previa transformación del mismo al Abstract Format mediante el uso de las syntax_tools y el módulo Smerl). En el apéndice A del presente documento se puede encontrar la descripción detallada de esta representación. A continuación se muestra un pequeño ejemplo de la correspondencia entre un fragmento de código y su representación en la Abstract Format para ilustrar de forma más clara el formato de este tipo de representación. Código - module(example). - export([foo/0]). foo() -> "Hello!". Abstract Format [{attribute,1,module,example}, {attribute,3,export,[{foo/0}]}, {function,5,foo,0,[{clause,5,[],[], [{string,5,"Hello!"}]}]}] 19 Como se puede observar, el Abstract Format está compuesto por una lista de formularios (forms), que son tuplas que representan las construcciones de alto nivel como declaraciones de función o atributos. Así, por ejemplo {attribute,1,module,example1} seria un form compuesto por tupla que representa a la declaración de módulo –module(example1). Estos forms se generan agrupando y interpretando los componentes léxicos escaneados del código fuente mediante el uso del módulo erl_scan. En la figura 2.2 se muestra un pequeño esquema en el que se ilustran de forma más gráfica los pasos que se siguen en el proceso de refactorización de código en nuestra herramienta. Programa Fuente erl_scan Arbol Sintáctico Syntax_tools/Smerl AST / Abstract Form Análisis del programa y transformación por parte de la refactorización AST / Abstract Form Refactorizado Syntax_tools/Smerl Programa Fuente Refactorizado Figura 2.2: Esquema de los pasos seguidos en la refactorización En el proceso de estudio de las diferentes herramientas de refactorización actuales de Erlang se ha observado que se pone especial hincapié en lo referente a las anotaciones del código y la preservación del layout (o apariencia) del mismo en la medida de lo posible para presentar al usuario un código lo más parecido posible al original (exceptuando las transformaciones). Actualmente entre las múltiples funciones que proporciona la librería syntax_tools de Erlang para la impresión de código mediante el módulo prettypr no se contempla la preservación del layout en cuanto a los comentarios se refiere, ya que por ejemplo, sólo se guarda la línea del mismo sin incluir información relativa a la columna que estos ocupan. Las 20 distintas aproximaciones estudiadas adoptan diversas soluciones interesantes como por ejemplo la adoptada por la herramienta Wrangler que exponemos a continuación. Wrangler opta por extender cada nodo del AST producido por las syntax_tools con mas información ( como posiciones en el código de variables, funciones, espacios en blanco, paréntesis y comentarios o información semántica que facilita la implementación de algunas transformaciones ) , obteniendo así un Árbol de Sintaxis Abstracta Anotado (AAST, Annotated Abstract Syntax Tree) y usando una base de datos relacional MySQL para su almacenamiento y manipulación. En la figura 2.3 se muestra la arquitectura de dicha aproximación: Figura 2.3: Arquitectura de la aproximación de la herramienta Wrangler En el caso de este proyecto, por simplicidad (ya que extender el AST es una tarea bastante costosa) y al no considerarse como uno de los objetivos principales subyacentes al caso de estudio del mismo, ha sido omitida la inclusión de los comentarios y la preservación de la apariencia del código. El código resultante después de la aplicación de las distintas transformaciones se presenta como una traducción inmediata del Abstract Form que se obtiene después de la aplicación de las mismas. De esta forma se presenta al usuario un código que difiere del código original y que presenta cierta dificultad a la hora de la lectura. 2.2. I NTERFAZ Uno de los puntos más importantes que se presentó en la fase de diseño de esta herramienta fue el momento de la elección de la interfaz con el usuario. Después de contemplar distintas opciones se eligió integrar esta herramienta en el entorno de programación Eclipse basándose en la estructura proporcionada por la herramienta de refactorización Wrangler. Todos los detalles de la implementación de esta integración se pueden encontrar en el capítulo 4 de este documento, no obstante en este punto se va a especificar la arquitectura seguida para el desarrollo de dicha integración. A continuación se describen brevemente los distintos componentes implicados en la capa de la interacción con el usuario. - El entorno de programación (IDE) en el que se ha integrado esta herramienta es Eclipse, u no de los más utilizados actualmente con soporte multi extensiones, utilidades y facilidades para el desarrollador. - Erlide es el entorno de desarrollo para Eclipse, implementado como un plug Proporciona numerosas características p codificación en Erlang, como por ejemplo editor con remarcado sintáctico, herramienta de depuración, consola integrada, herramienta de generación automática o predicción de código. - Esta herramienta se ha basado en que se han integrado las distintas refactorizaciones desarrolladas, eliminando las que ya tenía implementada la herramienta Wrangler. De esta forma se ha logrado simplificar enormemente el esfuerzo dedi el entorno de desarrollo Eclipse. En la figura 2.4 se ilustra cómo están distribuidos los componentes recién descritos en la comunicación con el usuario. Usuario Figura 2.4: Distribución de los co Cabe destacar que el desarrollo de la interfaz de este proyecto adaptar el plug-in de Wrangler a nuestra herramienta de refactorización, como se podrá ver en el siguiente capítulo del documento (4. In A continuación se describen brevemente los distintos componentes implicados en la capa de la El entorno de programación (IDE) en el que se ha integrado esta herramienta es no de los más utilizados actualmente con soporte multi - lenguaje, multitud de extensiones, utilidades y facilidades para el desarrollador. es el entorno de desarrollo para Eclipse, implementado como un plug Proporciona numerosas características p ara facilitar al desarrollador la tarea de codificación en Erlang, como por ejemplo editor con remarcado sintáctico, herramienta de depuración, consola integrada, herramienta de generación automática o predicción de código. Esta herramienta se ha basado en la herramienta de refactorización Wrangler que se han integrado las distintas refactorizaciones desarrolladas, eliminando las que ya tenía implementada la herramienta Wrangler. De esta forma se ha logrado simplificar enormemente el esfuerzo dedi cado a la integración de esta herramienta en el entorno de desarrollo Eclipse. se ilustra cómo están distribuidos los componentes recién descritos en la Figura 2.4: Distribución de los co mponentes de la interfaz Cabe destacar que el desarrollo de la interfaz de este proyecto se ha centrado básicamente de Wrangler a nuestra herramienta de refactorización, como se podrá ver en del documento (4. In tegración en Eclipse). Eclipse / Erlide Wrangler Front-End Erlang Syntax_tools Smerl 21 A continuación se describen brevemente los distintos componentes implicados en la capa de la El entorno de programación (IDE) en el que se ha integrado esta herramienta es lenguaje, multitud de es el entorno de desarrollo para Eclipse, implementado como un plug -in. ara facilitar al desarrollador la tarea de codificación en Erlang, como por ejemplo editor con remarcado sintáctico, herramienta de depuración, consola integrada, herramienta de generación automática Wrangler , sobre la que se han integrado las distintas refactorizaciones desarrolladas, eliminando las que ya tenía implementada la herramienta Wrangler. De esta forma se ha logrado cado a la integración de esta herramienta en se ilustra cómo están distribuidos los componentes recién descritos en la se ha centrado básicamente en de Wrangler a nuestra herramienta de refactorización, como se podrá ver en Erlang 22 3. IMPLEMENTACIÓN Este punto del presente documento se va a centrar en el análisis de la implementación de las distintas refactorizaciones contempladas en la herramienta desarrollada. No se va a tener en cuenta la implementación de la integración en el entorno de programación Eclipse, sino que únicamente se va a detallar la estrategia adoptada para el desarrollo de dichas transformaciones, tanto a nivel general de la herramienta como particular de cada transformación. 3.1. E STRUCTURA G ENERAL DE LAS TRANSFORMACIONES Todas las transformaciones soportadas por esta herramienta se han desarrollado partiendo de una misma estructura general. A continuación se detalla las principales componentes y características de esta estructura, para poder así entender con facilidad la implementación de cada uno de los refactorings que se explicaran posteriormente. - Como se ha descrito en el apartado del diseño, el front-end de nuestra herramienta es la encargada de transformar el código fuente Erlang del fichero en cuestión a su Abstract Format subyacente del AST. Esta transformación se ha realizado mediante el uso de la librería estándar syntax_tools de la distribución oficial Erlang/OTP. La complejidad y densidad asociada a esta librería ha sido abstraída por el módulo Smerl. En todas nuestras transformaciones se ha hecho uso de este módulo para realizar la mencionada transformación al Abstract Format y su uso se ilustra a continuación: {ok,Meta_model} = smerl:for_file(NombreFichero), Forms=smerl:get_forms(Meta_model) En la primera instrucción se invoca a la función for_file del módulo Smerl, que se encarga de devolver (si todo ha ido bien) una tupla con un átomo ok indicando que la llamada devuelve un resultado correcto y el meta modelo asociado al módulo Erlang cuyo nombre se le ha pasado como argumento (Meta_model). Este meta modelo contiene el Abstract Format así como información (nombre, ruta…) del módulo en cuestión. La segunda instrucción se encarga de extraer la lista de forms que conforman el Abstract Format del módulo. En el caso contrario, una vez se ha aplicado la refactorización pertinente al Abstract Format se ha de volver a traducir al formato de código fuente, este proceso se realiza también de forma muy sencilla mediante el uso del módulo Smerl de la siguiente forma: 23 TransformedAbstractForm = applyRefac(Forms), MetaModelNew = smerl:set_forms(Meta_model,lists:reverse(TransformedAbstractForm), smerl:to_src(MetaModelNew,FileName), La variable TransformedAbstractForm almacena el Abstract Format con la refactorización aplicada (applyRefac(Forms) ). La función set_forms sustituye el nuevo Abstract Format en el meta modelo inicial (Meta_model). Por último, se realiza la traducción de dicho meta modelo al código fuente y almacenándolo en el archivo FileName mediante la instrucción smerl:to_src(MetaModelNew,FileName). A continuación se va a explicar la estructura general del Abstract Format de un programa, aunque este se puede ver con más detalle en el apéndice A de este documento. o El Abstract Format de un módulo está compuesto por una secuencia (lista) de forms. o Los forms pueden ser a su vez declaraciones de función o atributos o Los atributos pueden ser por ejemplo las funciones a exportar/importar del módulo, la propia declaración del módulo…etc. o Las declaraciones de función están compuestas por clausulas de función. o Las clausulas de función tienen unos patrones, unas guardas y un cuerpo. o El cuerpo (Body) de las clausulas de función están formadas por una lista de expresiones o Los patrones y guardas de una clausula de función están compuestos por un subconjunto de las expresiones. Modulo  Forms Funciones Atributos Clausulas Patrones Guardas Body Expresiones Figura 3.1: Esquema de los componentes del Abstract Format 24 En la figura 3.1 se puede observar como los patrones y las guardas están relacionadas con una flecha discontinua con las expresiones. Esto es por el hecho de que los patrones y las guardas estén compuestos por un subconjunto de las expresiones, es decir en este tipo de construcciones no se admiten ciertas expresiones que, por el contrario, sí que se aceptan en el cuerpo de una clausula de función. No obstante, en el apéndice A se puede ver como se consideran por separado los tres casos. Una vez entendida la estructura seguida por el Abstract Format para representar el Árbol sintáctico de un programa Erlang se va a mostrar y analizar el código de la implementación de una transformación. Cabe destacar que esta transformación no se trata de ningún refactoring, ya que lo único que realiza es añadir el carácter ‘x’ al final del nombre de todas las variables existentes. La única finalidad de esta transformación es mostrar un ejemplo para entender la estructura del Abstract Format sobre la cual se ha trabajado en la implementación de los distintos refactorings de esta herramienta. transform(Program)-> {ok,Meta_Model} = smerl:for_file(Program), Forms=smerl:get_forms(Meta_Model), NXX = smerl:set_forms( Meta_Model,lists:reverse(change_varName_funsGeneric(Forms))), smerl:to_src(NXX,FileName), %**************************************************************************** %************* change_varName_funsGeneric %**************************************************************************** change_varName_funsGeneric([])->[]; change_varName_funsGeneric([{function,LINE,Name,Arity,Clauses}|Funs]) -> NewClauses = change_varName_clausesGeneric(Clauses), [{function,LINE,Name,Arity,NewClauses}|change_varName_funsGeneric(Funs)]; change_varName_funsGeneric(Other)-> Other. %**************************************************************************** %************* change_varName_clausesGeneric %**************************************************************************** change_varName_clausesGeneric([]) -> []; change_varName_clausesGeneric([{clause,LINE,Patterns,Guards,Body}|Clauses])-> NBody = change_varName_expressionsGeneric(Body), NPatterns = change_varName_expressionsGeneric(Patterns), NGuards = change_varName_expressionsGeneric(Guards), [{clause,LINE,NPatterns,NGuards,NBody}|change_varName_clausesGeneric( Clauses)]; change_varName_clausesGeneric(Other)-> Other. %**************************************************************************** %************* change_varName_expressionsGeneric %**************************************************************************** change_varName_expressionsGeneric([])->[]; change_varName_expressionsGeneric([Exp|Exps]) -> [change_varName_expressionGeneric(Exp)|change_varName_expressionsGeneric (Exps)]; change_varName_expressionsGeneric(Other)-> change_varName_expressionGeneric(Other). 25 %**************************************************************************** %************* change_varName_expressionGeneric %**************************************************************************** change_varName_expressionGeneric({var,LINE,V}) -> {var,LINE,list_to_atom(lists:append(atom_to_list(V),"x"))}; change_varName_expressionGeneric({match,LINE,E1,E2})-> {match,LINE,change_varName_expressionsGeneric(E1), change_varName_expressionsGeneric(E2)}; change_varName_expressionGeneric({tuple,LINE,Exps})-> {tuple,LINE,change_varName_expressionsGeneric(Exps)}; change_varName_expressionGeneric({cons,LINE,E1,E2})-> {cons,LINE,change_varName_expressionsGeneric(E1), change_varName_expressionsGeneric(E2)}; change_varName_expressionGeneric({op,LINE,Op,E1,E2})-> {op,LINE,Op,change_varName_expressionsGeneric(E1), change_varName_expressionsGeneric(E2)}; change_varName_expressionGeneric({op,LINE,Op,E})-> {op,LINE,Op,change_varName_expressionsGeneric(E)}; . . . . . change_varName_expressionGeneric(Other)-> Other. Destacar que no se han puesto todas las expresiones que se deberían tratar en el ejemplo por simplicidad del mismo, sólo se han puesto las primeras, que incluyen las que realizan la transformación deseada. Con la ayuda de este ejemplo se puede observar el recorrido que se realiza sobre todo el Abstract Format de forma recursiva, desde los forms hasta todas las posibles expresiones que puedan aparecer. Si nos fijamos, solo se ajustan los patrones de aquellas expresiones, forms y clausulas que tienen a su vez expresiones o clausulas en su interior, por ejemplo en el caso de las tuplas sería: change_varName_expressionGeneric({tuple,LINE,Exps})-> {tuple,LINE, change_varName_expressionsGeneric (Exps)}; El resto se dejan tal cual (por ejemplo, en el caso de las expresiones, la guarda: change_varName_expressionGeneric(Other)->Other) . También remarcar que en la estructura general adoptada no se ha hecho ningún tipo de diferenciación entre los conjuntos de expresiones pertenecientes a las guardas, patrones y cuerpo de las clausulas de función. Esto viene dado porque el proceso de parseo (o traducción) realizado por el Front-End mediante las syntax_tools nos asegura que el Abstract Format que se recibe a la hora de aplicar dicha transformación es sintácticamente correcto, con lo que nunca se va a producir una violación en cuanto al dominio de dichos conjuntos se refiere. En el ámbito del ejemplo, se ha remarcado en color amarillo la instrucción concreta que realiza la transformación del programa analizado, es decir, modifica el nombre de las variables añadiéndoles el carácter ‘x’. 32 La instrucción Forms_ImportDeleted=deleteImports(Forms) se encarga de devolver el Abstract Format que se le pasa como argumento pero eliminando la directiva import (previo guardado de la lista de funciones presente en esta). La función findImport se encarga de comprobar que la función que se le pasa como primer argumento está o no presente en la lista de funciones importadas que se le pasa como segundo argumento, devolviendo así una tupla con un átomo ok y el nombre del módulo al que pertenece dicha función ({ok,Mod}) en caso de ser una de las que estaba en la lista de imports o un false en caso contrario. De esta forma cuando estamos recorriendo el Abstract Format (concretamente en la función applyRefac_expressionGeneric, tal y como se ve en la parte final del código anterior ) , si nos encontramos con una llamada a función local ({call,LINE,EF,Exps}) pasaremos a comprobar la presencia de dicha función en la lista de imports que se le pasa como nuevo argumento. En el caso de estar presente se sustituye dicha llamada local por una llamada remota a la función del módulo que nos ha devuelto la función findImport como parte del resultado, en caso contrario la llamada se deja igual que estaba ya que no es una de las funciones externas importadas. A continuación mostramos un ejemplo de aplicación de esta refactorización: Código original -import(lists, [reverse/1, nth/2, sort/1]). test() -> lists:append([1], [2, 3]), fun_local(), reverse("prova"), sort([1,2, 3]), X=nth(2,[1,2, 3]). fun_local() -> true. Código refactorizado Test () -> lists:append([1], [2, 3]), fun_local(), lists:reverse("prova"), lists:sort([1, 2, 3]), X = lists:nth(2, [1, 2, 3]). fun_local() -> true. Como era de esperar, ha sido eliminada la directiva import del código y las llamadas a las funciones externas que estaban en la directiva han sido sustituidas por llamadas externas al módulo correspondiente. 33 3.2.3 R EFACTORING S OBRE F UNCIONES CON UNA SOLA INSTRUCCIÓN CASE S.Thompson y F.Cesarini en su libro [10] (pags. 47,48) afirman que las funciones que tienen una sola instrucción case se pueden reescribir como distintas guardas de la misma función, en las que se añadiría como argumento la variable o expresión sobre la que se codificaba la instrucción case. Esto es porque se considera que el ajuste de patrones o pattern matching (que se realizaría en las guardas de la función transformada) en las definiciones de funciones puede ser más compacto que el uso de una expresión case en su cuerpo. De esta forma, la funcionalidad de este refactoring es la de sustituir aquellas funciones que tienen una sola instrucción case en su interior por una función con tantas guardas como casos tenia dicha instrucción, dejando en el cuerpo de cada guarda de la función las instrucciones que pertenecían a cada caso de la instrucción case. Respecto a la implementación de este refactoring se va a detallar a partir del código: applyRefacCaseFun([])->[]; applyRefacCaseFun([{function,LINE_FUN,Name,Arity,Clauses}|Funs]) -> [{function,LINE_FUN,Name,Arity,applyRefac_clausesGeneric( applyRefacCaseFunRecursive(Clauses))}|applyRefacCaseFun(Funs)]; applyRefacCaseFun(Other)-> Other. applyRefacCaseFunRecursive([])->[]; applyRefacCaseFunRecursive([{clause,LINE_CLAUSE,Patterns,Guards,[{'case',LINE, CASE,VarCase,ClausesCase}]}|ClausesTail])-> case VarCase of [{_,_,_}] -> NewClauses=fusionaClauses(Patterns,VarCase,ClausesCase), applyRefac_clausesGeneric(NewClauses) ++ applyRefacCaseFunRecursive(ClausesTail); {_,_,_} -> NewClauses=fusionaClauses(Patterns,VarCase,ClausesCase), applyRefac_clausesGeneric(NewClauses)++ applyRefacCaseFunRecursive(ClausesTail); _ -> [{clause,LINE_CLAUSE,Patterns,Guards, [{'case',LINE_CASE,VarCase,ClausesCase}]}] ++ applyRefacCaseFunRecursive(ClausesTail) end; applyRefacCaseFunRecursive([{clause,LINE_CLAUSE,Patterns,Guards,Body}| ClausesTail])-> [{clause,LINE_CLAUSE,Patterns,Guards,Body}| applyRefacCaseFunRecursive(ClausesTail)]. fusionaClauses(ArgsFunc,VarCase,ClausesCase) -> NewPatterns=marcaPatterns(ArgsFunc,VarCase), addPatterns(NewPatterns,ClausesCase). marcaPatterns([],_)->[]; marcaPatterns(Args,[])->Args; marcaPatterns([{Tipo,_,Valor}|ArgTail],{Tipo,_,Valor})-> [{Tipo,x,Valor}|ArgTail]; %Marcamos el argumento a sustituir marcaPatterns([ArgHead|ArgTail],VarCase)-> [ArgHead|marcaPatterns(ArgTail,VarCase)]. 34 addPatterns(_,[]) -> []; addPatterns(Args,[{clause,LINE_CLAUSE,Patterns,Guards,Body}|ClauseTail]) -> [{clause,LINE_CLAUSE,addPatternRecurs(Args,Patterns),Guards,Body}| addPatterns(Args,ClauseTail)]. addPatternRecurs([],_)->[]; addPatternRecurs([{_,x,_}|ArgsTail],Patterns)->Patterns++ArgsTail; addPatternRecurs([Other|ArgsTail],Patterns)-> [Other|addPatternRecurs(ArgsTail,Patterns)]. El código de la implementación de este refactoring es un poco más complicado que en los anteriores. Las principales características de esta implementación son: - La función applyRefacCaseFunRecursive recorre las clausulas de función que se le pasan en busca de alguna que tenga una única instrucción case en su cuerpo. En caso de encontrarla realiza una llamada a la función fusionaClauses. - La función fusionaClauses es la encargada de devolver las nuevas clausulas de la función fusionando las clausulas de la función original y las de la instrucción case. Para realizar esto realiza llamadas a las funciones marcaPatterns y addPatterns. - La función marcaPatterns se encarga de marcar el argumento de la función original cuyo uso es el de la variable de la instrucción case. Este marcado del argumento nos servirá a continuación para saber cuál es el argumento a sustituir por cada una de las expresiones que conforman los distintos casos de la instrucción case. Para marcar dicho argumento se pone una x en el lugar que ocuparía la línea en la construcción del Abstract Format de este argumento, ya que como se ha mencionado en puntos anteriores, en esta herramienta no se preserva la apariencia del código original y, por lo tanto, se hace caso omiso de este atributo pudiéndose utilizar para guardar esta información. - Para añadir la expresión de la guarda correspondiente de la instrucción case en el lugar que ocupaba la variable de dicha instrucción en los argumentos de la función original se hace uso de las funciones addPatterns y addPatternRecurs. Estas funciones se encargan de recorrer todas la clausulas que se le pasan como argumento sustituyendo en sus patrones la variable marcada con la x con la expresión de la guarda de la instrucción case correspondiente. A continuación podemos observar que este código se comporta de la forma esperada mediante un ejemplo: 35 Código original prueba(A,B,C,D) -> E=A+B, F=lists:reverse(D), case C of true -> 0; false -> 1 end. add_to_hs(a,b,c,d,e,f) -> true; add_to_hs(b,c,d, T,e,f) -> case T of {Min} -> true; _ -> false end. add_to_hs(a, _) -> true; add_to_hs(T, b) -> case T of a -> true; _ -> false end. Código refactorizado prueba(A, B, C, D) -> E = A + B, F = lists:reverse(D), case C of true -> 0; false -> 1 end. add_to_hs(a, b, c, d, e, f) -> true; add_to_hs(b, c, d, {Min}, e, f) -> true; add_to_hs(b, c, d, _, e, f) -> false. add_to_hs(a, _) -> true; add_to_hs(a, b) -> true; add_to_hs(_, b) -> false. Se puede observar que en el caso de que la instrucción case no sea la única dentro de la función (como es el caso de la función prueba) no se aplica la refactorización. En los dos casos de la función add_to_hs se puede comprobar cómo se comporta conforme se espera, extrayendo la variable de la instrucción case como argumento de la nueva función en el lugar que les corresponde y sustituyendo el cuerpo de la función por la clausula de la instrucción case correspondiente. 36 4. I NTEGRACIÓN EN E CLIPSE Sin duda uno de las tareas más complicadas y a la que se ha dedicado más tiempo en este proyecto ha sido la de la integración de la herramienta en un entorno de desarrollo. Paralelamente al gran crecimiento que se ha producido en los últimos años en el número de desarrolladores, ha aumentado la popularidad de los entornos de programación integrados o (IDE Integrated Development Environment). Un IDE es un entorno de programación que ha sido empaquetado como un programa de aplicación, es decir, consiste en un editor de código, un compilador, un depurador y un constructor de interfaz gráfica (GUI). Los IDEs pueden ser aplicaciones por sí solas o pueden ser parte de aplicaciones existentes. Los IDE proporcionan un marco sencillo y amigable a la hora de desarrollar software al tener múltiples opciones y herramientas enfocadas a facilitar al programador su tarea de codificación. En el momento de plantearse la plataforma sobre la que se iba desarrollar esta herramienta surgieron distintas alternativas, de la cuales se estudió la viabilidad de las siguientes: - La opción más sencilla que se planteó fue la de implementar esta herramienta como un programa de consola en el que se especificaría mediante un comando el tipo de refactorización a aplicar y la ruta del programa a transformar. No obstante, esta opción fue descartada por ofrecer un entorno demasiado “plano” y con una interacción más bien pobre con el usuario, hecho que le restaría bastante utilidad y usabilidad a la herramienta. - Otra opción que se planteó y que no se descartó hasta última hora fue la de la integración de esta herramienta en el entorno de programación Emacs. Emacs es un editor de texto altamente configurable con una herramienta de resaltado de sintaxis, interfaz de depuración y otros, pero (como su nombre indica "Editor de MACroS") es sólo un editor con funcionalidades adicionales. Cabe destacar que la integración es este entorno hubiese sido bastante más sencilla que la opción elegida. - Finalmente la opción elegida fue el entorno de programación Eclipse, probablemente el mejor IDE de código abierto multiplataforma. Eclipse es también una comunidad de usuarios, extendiendo constantemente las áreas de aplicación cubiertas. Eclipse fue desarrollado originalmente por IBM y es ahora desarrollado por la Fundación Eclipse, una organización independiente sin ánimo de lucro que fomenta una comunidad de código abierto y un conjunto de productos complementarios, capacidades y servicios. Eclipse tiene un sistema de plug-ins (o extensiones) para proporcionar toda su funcionalidad en la parte superior (e incluyendo) del sistema de ejecución, a diferencia de otros entornos monolíticos donde las funcionalidades están todas incluidas, las necesite el usuario o no. Está escrito principalmente en Java y se puede utilizar para desarrollar aplicaciones en Java y, por medio de diversos plug-ins, se puede utilizar para desarrollar en otros idiomas, incluyendo Erlang mediante el plug-in Erlide. 37 Figura 4.1: Arquitectura de Eclipse El plug-in Erlide proporciona numerosas características para facilitar al desarrollador la tarea de codificación en Erlang, como por ejemplo editor con remarcado sintáctico, herramienta de depuración, consola integrada, herramienta de generación automática o predicción de código. Ya en el marco de este proyecto, la decisión de integrar esta herramienta en Eclipse fue principalmente condicionada por el hecho de que se observó que se podría “personalizar” con relativa facilidad el plug-in de Wrangler para adaptarlo a las posibles refactorizaciones de esta herramienta. Wrangler es una conocida herramienta de refactorización de programas Erlang desarrollada como plug-in de Eclipse y sus principales características se han descrito en la sección 1.3 de este documento. Así, la tarea de integración de esta herramienta en Eclipse se ha basado básicamente en la adaptación del plug-in Wrangler a las distintas refactorizaciones desarrolladas. A continuación se va a explicar el proceso de adaptación del plug-in de Wrangler a nuestras refactorizaciones. - Después de descargar el código de este plug-in, ponerlo en ejecución y entender su funcionamiento se pasó a la depuración del mismo viendo instrucción a instrucción como ejecutaba los distintos refactorings. En este punto se analizó la distribución de los distintos archivos, paquetes y proyectos implicados, observando cuales eran los que se tendrían que modificar para poder añadir los refactorings desarrollados. De esta forma los dos prinicipales proyectos de eclipse implicados en el plug-in para nuestro interés son:  El proyecto básico del plug-in es org.erlide.wrangler.refactoring y es donde se encuentran la mayor parte de los archivos implicados en la ejecución del mismo. Este proyecto está escrito en Java en casi toda su totalidad , excepto algún archivo .xml de configuración de la herramienta. Está estructurado en 14 paquetes que agrupan cada uno archivos con funcionalidades distintas como por ejemplo las 38 clases abstractas (org.erlide.wrangler.refactoring.core), archivos que proporcionan dsitintas utilidades para el plug-in (org.erlide.wrangler .refactoring.util) o los archivos que implementan la interfaz de comunicación con los archivos Erlang (org.erlide.wrangler.refactoring.backend).  El proyecto en el que se encuentran los archivos Erlang implicados en esta herramienta es org.erlide.wrangler.core. En este proyecto se encuentran las implementaciones de las distintas refactorizaciones además de otros archivos necesarios para poder llevar a cabo dicha ejecución. - El siguiente paso fue modificar los archivos identificados en el paso anterior o crear las clases correspondientes para añadir los distintos refactorings. Dichos archivos son:  org.erlide.wrangler.refactoring.ui.RefactoringHandler.java: Clase que extiende a la clase Abstracta org.eclipse.core.commands.AbstractHandler del proyecto Eclipse. Su método execute recibe el evento de la pulsación de las distintas opciones del menú del plug-in. Dependiendo del identificador de dicho evento se crea una instancia de la clase que crea la infraestructura necesaria para lanzar el refactoring correspondiente ejecutándolo a continuación.  Se añaden las distintas clases que crean la infraestructura necesaria para poder lanzar el refactoring correspondiente dentro del paquete org.erlide. wrangler.refactoring.core.internal: Estas clases son ImportRefactoring.java, ListaRefactoring.java y CaseFunRefactoring.java. En esta clase se envía la llamada a procedimiento remoto (o RPC, Remote Procedure Call) correspondiente a la interfaz de Erlang invocando al módulo y función de Erlang correspondiente al refactoring en cuestión.  En el proyecto org.erlide.wrangler.core se han tenido que añadir los distintos ficheros Erlang que implementan las transformaciones, en este caso los archivos refac_CaseFun.erl, refac_Import.erl y refac_Lista.erl la implementación de los cuales se ha explicado con detalle en el punto 3 de este documento. Además se ha tenido que modificar el fichero wrangler.erl que es el que actúa como interfaz de comunicación entre los archivos Erlang de la implementación de las transformaciones y las RPC que se envían desde el proyecto org.erlide.wrangler.refactoring.  Finalmente se ha modificado el fichero plugin.xml del proyecto org.erlide.wrangler.refactoring. Este fichero es el que describe el plug-in y indica a Eclipse todo lo necesario para poder activarlo (menús, botones, posibles acciones…etc.). De esta forma se ha conseguido añadir correctamente los menús correspondientes a los nuevos refactorings implementados y enlazarlos correctamente al código de Wrangler. 39 - La última tarea que se realizó en esta integración fue la de quitar todos los refactorings de Wrangler para dejar solamente los implementados para nuestra herramienta. De esta forma se eliminaron numerosos archivos implicados en los refactorings de Wrangler y se eliminó gran parte de código de otros, como por ejemplo el archivo RefactoringHandler.java o plugin.xml consiguiendo reducir considerablemente el tamaño de estos proyectos y en consecuencia el tamaño del plug-in, dejando únicamente los necesarios para la correcta ejecución del mismo. Tal y como se ha indicado al principio de este punto, la tarea de integración en este IDE ha requerido mucho esfuerzo. Esto ha sido provocado por el gran tamaño del proyecto de la herramienta de refactorización Wrangler, ya que ha sido necesario invertir bastante tiempo en entender la estructura del mismo, su modo de funcionamiento interno e identificar cuáles eran los ficheros que había que modificar para la correcta integración de nuestras transformaciones. De esta forma, en el desarrollo de esta tarea se han encontrado muchos obstáculos que ha habido que sortear, quedando algunos aspectos de la integración en el aire como por ejemplo el mensaje de confirmación al usuario indicando que el refactoring se ha realizado correctamente, ya que muestra de forma equivocada que el fichero sobre el que se ha aplicado la refactorización no se ha modificado. Cabe destacar que el principal objetivo que se pretendía alcanzar con el desarrollo de este proyecto era centrarse más en la implementación de los distintos refactorings para el lenguaje Erlang que el de una buena integración en el entorno de desarrollo. No obstante, se asume que con más esfuerzo y dedicación al estudio de la herramienta Wrangler se hubiera podido realizar una buena integración de esta herramienta. 40 5. M ANUAL DE USUARIO Este capítulo consiste en un manual de introducción a la herramienta de refactorización del presente documento, con la intención de ofrecer la información necesaria para la correcta instalación y uso de esta herramienta. R EQUISITOS Para poder ejecutar esta herramienta es necesario: - Tener instalado el Entorno de Desarrollo Eclipse, que se puede descargar desde su página web http://www.eclipse.org, así como poseer conocimientos de su uso (se pueden consultar distintos tutoriales desde la misma página web de la descarga). - Tener instalado el plug-in de Erlang para el entorno de desarrollo Eclipse, Erlide. Desde su página web oficial http://erlide.sourceforge.net/ se puede descargar dicho plug-in así como guías de instalación y diversos tutoriales. C ARGA DEL PROYECTO Una vez se han descargado y configurado los componentes de los requisitos, se ha de cargar el código fuente herramienta en el entorno de desarrollo Eclipse. Para realizar esto arrancamos el entorno de desarrollo Eclipse y seleccionamos la ruta donde tenemos ubicado el workspace que contiene los proyectos que conforman el código de esta herramienta tal y como se muestra en la figura 5.1 Figura 5.1 Selección del workspace a cargar 41 A continuación Eclipse se encargará de cargar dicho workspace con los distintos proyectos, quedando el entorno de desarrollo como se muestra en la figura 5.2 Figura 5.2: Worksape del código funte de la herramienta En la ventana de la parte izquierda Package Explorer se puede observar cómo se han cargado los distintos proyectos que conforman el workspace del código fuente de esta herramienta. Y en la ventana Problems podemos comprobar que se han cargado correctamente, sin ningún error. En el caso que hayan aparecido errores es probable que se deba a una mala configuración del plug-in Erlide, consulte con las guias de instalación existentes en su página web http://erlide.sourceforge.net/. E JECUCIÓN DEL PROYECTO El siguiente paso a la carga del workspace, si todo ha ido correctamente, es el de la ejecución del mismo para poder empezar a usar esta herramienta. Para realizar esto nos hemos de dirigir al proyecto org.erlide.wrangler.refactoring en la ventana Package Explorer de la parte derecha del entorno de desarrollo pulsando sobre el mismo el botón derecho del ratón. En el menú contextual que aparece seleccionamos la opción Run As – Eclipse Aplication. Hay que destacar que esta acción solo es necesaria realizarla la primera vez que se va a poner en ejecución la herramienta, ya que a partir de ese momento se puede poner en ejecución pulsando simplemente sobre el botón de la barra superior. 48 [18] G. Orosz. The Eclipse integration of the Wrangler Erlang refactor tool. Report, Computing Lab, Univ. of Kent, 2008. [19] Li, H., Thompson, S., Orosz, G., T oth, M.: Refactoring with Wrangler, updated. In: ACM SIGPLAN Erlang Workshop 2008, Victoria, British Columbia, Canada [20] Smerl: Simple Metaprogramming for Erlang., Module smerl, http://erlyweb.org/doc/ smerl.html [21] Eclipse.org home, http://www.eclipse.org [22] erlIDE, Turbo charged Erlang development, http://erlide.sourceforge.net/ [23] Oz (Programming language), http://en.wikipedia.org/wiki/Oz (programming_language) [24] Wrangler, an Erlang Refactorer, http://www.cs.kent.ac.uk/projects/forse/wrangler/doc/ overview-summary.html [25] RefactorErl, Erlang Refactorer, http://plc.inf.elte.hu/erlang/dl/ [26] Erlang Syntax Tools, http://www.erlang.org/doc/apps/syntax_tools/chapter.html [27] Módulo erl_parse, http://www.erlang.org/doc/man/erl_parse.html 49 A PÉNDICES A PÉNDICE A - T HE A BSTRACT F ORMAT This document describes the standard representation of parse trees for Erlang programs as Erlang terms. This representation is known as the Abstract Format. Functions dealing with such parse trees are compile:forms/ [1,2] and functions in the modules epp, erl_eval, erl_lint, erl_pp, erl_parse, and io. They are also used as input and output for parse transforms (see the module compile). We use the function Rep to denote the mapping from an Erlang source construct C to its abstract format representation R, and write R = Rep(C). The word LINE below represents an integer, and denotes the number of the line in the source file where the construction occurred. Several instances of LINE in the same construction may denote different lines. Since operators are not terms in their own right, when operators are mentioned below, the representation of an operator should be taken to be the atom with a printname consisting of the same characters as the operator. M ODULE DECLARATIONS AND FORMS A module declaration consists of a sequence of forms that are either function declarations or attributes. • If D is a module declaration consisting of the forms F_1, ..., F_k, then Rep(D) = [Rep(F_1), ..., Rep(F_k)]. • If F is an attribute -module(Mod), then Rep(F) = {attribute,LINE,module,Mod}. • If F is an attribute -export([Fun_1/A_1, ..., Fun_k/A_k]), then Rep(F) = {attribute,LINE,export,[{Fun_1,A_1}, ..., {Fun_k,A_k}]}. • If F is an attribute -import(Mod,[Fun_1/A_1, ..., Fun_k/A_k]), then Rep(F) ={attribute,LINE,import,{Mod,[{Fun_1,A_1}, ..., {Fun_k,A_k}]}}. • If F is an attribute -compile(Options), then Rep(F) = {attribute,LINE,compile,Options}. • If F is an attribute -file(File,Line), then Rep(F) = {attribute,LINE,file,{File,Line}}. • If F is a record declaration -record(Name,{V_1, ..., V_k}), then Rep(F) ={attribute,LINE,record,{Name,[Rep(V_1), ..., Rep(V_k)]}}. For Rep(V), see below. • If F is a wild attribute -A(T), then Rep(F) = {attribute,LINE,A,T}. • If F is a function declaration Name Fc_1 ; ... ; Name Fc_k, where each Fc_i is a function clause with a pattern sequence of the same length Arity, then Rep(F) = {function,LINE,Name,Arity,[Rep(Fc_1), ...,Rep(Fc_k)]}. 50 R ECORD FIELDS Each field in a record declaration may have an optional explicit default initializer expression • If V is A, then Rep(V) = {record_field,LINE,Rep(A)}. • If V is A = E, then Rep(V) = {record_field,LINE,Rep(A),Rep(E)}. R EPRESENTATION OF PARSE ERRORS AND END OF FILE In addition to the representations of forms, the list that represents a module declaration (as returned by functions inerl_parse and epp) may contain tuples {error,E} and {warning,W}, denoting syntactically incorrect forms and warnings, and {eof,LINE}, denoting an end of stream encountered before a complete form had been parsed. A TOMIC LITERALS There are five kinds of atomic literals, which are represented in the same way in patterns, expressions and guards: • If L is an integer or character literal, then Rep(L) = {integer,LINE,L}. • If L is a float literal, then Rep(L) = {float,LINE,L}. • If L is a string literal consisting of the characters C_1, ..., C_k, then Rep(L) = {string,LINE,[C_1, ..., C_k]}. • If L is an atom literal, then Rep(L) = {atom,LINE,L}. Note that negative integer and float literals do not occur as such; they are parsed as an application of the unary negation operator. P ATTERNS If Ps is a sequence of patterns P_1, ..., P_k, then Rep(Ps) = [Rep(P_1), ..., Rep(P_k)]. Such sequences occur as the list of arguments to a function or fun. Individual patterns are represented as follows: • If P is an atomic literal L, then Rep(P) = Rep(L). • If P is a compound pattern P_1 = P_2, then Rep(P) = {match,LINE,Rep(P_1),Rep(P_2)}. • If P is a variable pattern V, then Rep(P) = {var,LINE,A}, where A is an atom with a printname consisting of the same characters as V. • If P is a universal pattern _, then Rep(P) = {var,LINE,'_'}. • If P is a tuple pattern {P_1, ..., P_k}, then Rep(P) = {tuple,LINE,[Rep(P_1), ..., Rep(P_k)]}. • If P is a nil pattern [], then Rep(P) = {nil,LINE}. • If P is a cons pattern [P_h | P_t], then Rep(P) = {cons,LINE,Rep(P_h),Rep(P_t)}. 51 • If E is a binary pattern <<P_1:Size_1/TSL_1, ..., P_k:Size_k/TSL_k>>, then Rep(E) = {bin,LINE, [{bin_element, LINE, Rep(P_1), Rep(Size_1), Rep(TSL_1)}, ..., {bin_element,LINE,Rep(P_k),Rep(Size_k),Rep(TSL_k)}]}. For Rep(TSL), see below. An omitted Size is represented by default. An omitted TSL (type specifier list) is represented by default. • If P is P_1 Op P_2, where Op is a binary operator (this is either an occurrence of ++ applied to a literal string or character list, or an occurrence of an expression that can be evaluated to a number at compile time), then Rep(P) ={op,LINE,Op,Rep(P_1),Rep(P_2)}. • If P is Op P_0, where Op is a unary operator (this is an occurrence of an expression that can be evaluated to a number at compile time), then Rep(P) = {op,LINE,Op,Rep(P_0)}. • If P is a record pattern #Name{Field_1=P_1, ..., Field_k=P_k}, then Rep(P) = {record,LINE,Name, [{record_field,LINE,Rep(Field_1),Rep(P_1)}, ..., {record_field,LINE,Rep(Field_k),Rep(P_k)}]}. • If P is #Name.Field, then Rep(P) = {record_index,LINE,Name,Rep(Field)}. • If P is ( P_0 ), then Rep(P) = Rep(P_0), i.e., patterns cannot be distinguished from their bodies. Note that every pattern has the same source form as some expression, and is represented the same way as the corresponding expression. E XPRESSIONS A body B is a sequence of expressions E_1, ..., E_k, and Rep(B) = [Rep(E_1), ..., Rep(E_k)]. An expression E is one of the following alternatives: • If P is an atomic literal L, then Rep(P) = Rep(L). • If E is P = E_0, then Rep(E) = {match,LINE,Rep(P),Rep(E_0)}. • If E is a variable V, then Rep(E) = {var,LINE,A}, where A is an atom with a printname consisting of the same characters as V. • If E is a tuple skeleton {E_1, ..., E_k}, then Rep(E) = {tuple,LINE,[Rep(E_1), ..., Rep(E_k)]}. • If E is [], then Rep(E) = {nil,LINE}. • If E is a cons skeleton [E_h | E_t], then Rep(E) = {cons,LINE,Rep(E_h),Rep(E_t)}. • If E is a binary constructor <<V_1:Size_1/TSL_1, ..., V_k:Size_k/TSL_k>>, then Rep(E) ={bin,LINE,[{bin_element,LINE,Rep(V_1),Rep(Size_1),Rep(TSL_1)}, ..., {bin_element,LINE,Rep(V_k),Rep(Size_k),Rep(TSL_k)}]}. For Rep(TSL), see below. An omitted Size is represented by default. An omitted TSL (type specifier list) is represented by default. • If E is E_1 Op E_2, where Op is a binary operator, then Rep(E) = {op,LINE,Op,Rep(E_1),Rep(E_2)}. • If E is Op E_0, where Op is a unary operator, then Rep(E) = {op,LINE,Op,Rep(E_0)}. • If E is #Name{Field_1=E_1, ..., Field_k=E_k}, then Rep(E) = {record,LINE,Name, [{record_field,LINE,Rep(Field_1),Rep(E_1)}, ..., {record_field,LINE,Rep(Field_k),Rep(E_k)}]}. • If E is E_0#Name{Field_1=E_1, ..., Field_k=E_k}, then Rep(E) = {record,LINE,Rep(E_0),Name, [{record_field,LINE,Rep(Field_1),Rep(E_1)}, ..., {record_field,LINE,Rep(Field_k),Rep(E_k)}]}. • If E is #Name.Field, then Rep(E) = {record_index,LINE,Name,Rep(Field)}. 52 • If E is E_0#Name.Field, then Rep(E) = {record_field,LINE,Rep(E_0),Name,Rep(Field)}. • If E is catch E_0, then Rep(E) = {'catch',LINE,Rep(E_0)}. • If E is E_0(E_1, ..., E_k), then Rep(E) = {call,LINE,Rep(E_0),[Rep(E_1), ..., Rep(E_k)]}. • If E is E_m:E_0(E_1, ..., E_k), then Rep(E) = {call,LINE,{remote,LINE,Rep(E_m),Rep(E_0)},[Rep(E_1), ..., Rep(E_k)]}. • If E is a list comprehension [E_0 || W_1, ..., W_k], where each W_i is a generator or a filter, then Rep(E) ={lc,LINE,Rep(E_0),[Rep(W_1), ..., Rep(W_k)]}. For Rep(W), see below. • If E is a binary comprehension <<E_0 || W_1, ..., W_k>>, where each W_i is a generator or a filter, then Rep(E) ={bc,LINE,Rep(E_0),[Rep(W_1), ..., Rep(W_k)]}. For Rep(W), see below. • If E is begin B end, where B is a body, then Rep(E) = {block,LINE,Rep(B)}. • If E is if Ic_1 ; ... ; Ic_k end, where each Ic_i is an if clause then Rep(E) = {'if',LINE,[Rep(Ic_1), ..., Rep(Ic_k)]}. • If E is case E_0 of Cc_1 ; ... ; Cc_k end, where E_0 is an expression and each Cc_i is a case clause then Rep(E) ={'case',LINE,Rep(E_0),[Rep(Cc_1), ..., Rep(Cc_k)]}. • If E is try B catch Tc_1 ; ... ; Tc_k end, where B is a body and each Tc_i is a catch clause then Rep(E) ={'try',LINE,Rep(B),[],[Rep(Tc_1), ..., Rep(Tc_k)],[]}. • If E is try B of Cc_1 ; ... ; Cc_k catch Tc_1 ; ... ; Tc_n end, where B is a body, each Cc_i is a case clause and each Tc_j is a catch clause then Rep(E) = {'try',LINE,Rep(B),[Rep(Cc_1), ..., Rep(Cc_k)],[Rep(Tc_1), ..., Rep(Tc_n)],[]}. • If E is try B after A end, where B and A are bodies then Rep(E) = {'try',LINE,Rep(B),[],[],Rep(A)}. • If E is try B of Cc_1 ; ... ; Cc_k after A end, where B and A are a bodies and each Cc_i is a case clause then Rep(E) = {'try',LINE,Rep(B),[Rep(Cc_1), ..., Rep(Cc_k)],[],Rep(A)}. • If E is try B catch Tc_1 ; ... ; Tc_k after A end, where B and A are bodies and each Tc_i is a catch clause then Rep(E) = {'try',LINE,Rep(B),[],[Rep(Tc_1), ..., Rep(Tc_k)],Rep(A)}. • If E is try B of Cc_1 ; ... ; Cc_k catch Tc_1 ; ... ; Tc_n after A end, where B and A are a bodies, each Cc_i is a case clause and each Tc_j is a catch clause then Rep(E) = {'try',LINE,Rep(B),[Rep(Cc_1), ..., Rep(Cc_k)],[Rep(Tc_1), ..., Rep(Tc_n)],Rep(A)}. • If E is receive Cc_1 ; ... ; Cc_k end, where each Cc_i is a case clause then Rep(E) ={'receive',LINE,[Rep(Cc_1), ..., Rep(Cc_k)]}. • If E is receive Cc_1 ; ... ; Cc_k after E_0 -> B_t end, where each Cc_i is a case clause, E_0 is an expression and B_t is a body, then Rep(E) = {'receive',LINE,[Rep(Cc_1), ..., Rep(Cc_k)],Rep(E_0),Rep(B_t)}. • If E is fun Name / Arity, then Rep(E) = {'fun',LINE,{function,Name,Arity}}. • If E is fun Module:Name/Arity, then Rep(E) = {'fun',LINE,{function,Module,Name,Arity}}. • If E is fun Fc_1 ; ... ; Fc_k end where each Fc_i is a function clause then Rep(E) ={'fun',LINE,{clauses,[Rep(Fc_1), ..., Rep(Fc_k)]}}. • If E is query [E_0 || W_1, ..., W_k] end, where each W_i is a generator or a filter, then Rep(E) ={'query',LINE,{lc,LINE,Rep(E_0),[Rep(W_1), ..., Rep(W_k)]}}. For Rep(W), see below. • If E is E_0.Field, a Mnesia record access inside a query, then Rep(E) = {record_field,LINE,Rep(E_0),Rep(Field)}. • If E is ( E_0 ), then Rep(E) = Rep(E_0), i.e., parenthesized expressions cannot be distinguished from their bodies. 53 G ENERATORS AND FILTERS When W is a generator or a filter (in the body of a list or binary comprehension), then: • If W is a generator P <- E, where P is a pattern and E is an expression, then Rep(W) ={generate,LINE,Rep(P),Rep(E)}. • If W is a generator P <= E, where P is a pattern and E is an expression, then Rep(W) ={b_generate,LINE,Rep(P),Rep(E)}. • If W is a filter E, which is an expression, then Rep(W) = Rep(E). B INARY ELEMENT TYPE SPECIFIERS A type specifier list TSL for a binary element is a sequence of type specifiers TS_1 - ... - TS_k. Rep(TSL) = [Rep(TS_1), ..., Rep(TS_k)]. When TS is a type specifier for a binary element, then: • If TS is an atom A, Rep(TS) = A. • If TS is a couple A:Value where A is an atom and Value is an integer, Rep(TS) = {A, Value}. C LAUSES There are function clauses, if clauses, case clauses and catch clauses. A clause C is one of the following alternatives: • If C is a function clause ( Ps ) -> B where Ps is a pattern sequence and B is a body, then Rep(C) ={clause,LINE,Rep(Ps),[],Rep(B)}. • If C is a function clause ( Ps ) when Gs -> B where Ps is a pattern sequence, Gs is a guard sequence and B is a body, then Rep(C) = {clause,LINE,Rep(Ps),Rep(Gs),Rep(B)}. • If C is an if clause Gs -> B where Gs is a guard sequence and B is a body, then Rep(C) ={clause,LINE,[],Rep(Gs),Rep(B)}. • If C is a case clause P -> B where P is a pattern and B is a body, then Rep(C) = {clause,LINE,[Rep(P)],[],Rep(B)}. • If C is a case clause P when Gs -> B where P is a pattern, Gs is a guard sequence and B is a body, then Rep(C) ={clause,LINE,[Rep(P)],Rep(Gs),Rep(B)}. • If C is a catch clause P -> B where P is a pattern and B is a body, then Rep(C) ={clause,LINE,[Rep({throw,P,_})],[],Rep(B)}. • If C is a catch clause X : P -> B where X is an atomic literal or a variable pattern, P is a pattern and B is a body, then Rep(C) = {clause,LINE,[Rep({X,P,_})],[],Rep(B)}. • If C is a catch clause P when Gs -> B where P is a pattern, Gs is a guard sequence and B is a body, then Rep(C) ={clause,LINE,[Rep({throw,P,_})],Rep(Gs),Rep(B)}. • If C is a catch clause X : P when Gs -> B where X is an atomic literal or a variable pattern, P is a pattern, Gs is a guard sequence and B is a body, then Rep(C) = {clause,LINE,[Rep({X,P,_})],Rep(Gs),Rep(B)}. 54 G UARDS A guard sequence Gs is a sequence of guards G_1; ...; G_k, and Rep(Gs) = [Rep(G_1), ..., Rep(G_k)]. If the guard sequence is empty, Rep(Gs) = []. A guard G is a nonempty sequence of guard tests Gt_1, ..., Gt_k, and Rep(G) = [Rep(Gt_1), ..., Rep(Gt_k)]. A guard test Gt is one of the following alternatives: • If Gt is an atomic literal L, then Rep(Gt) = Rep(L). • If Gt is a variable pattern V, then Rep(Gt) = {var,LINE,A}, where A is an atom with a printname consisting of the same characters as V. • If Gt is a tuple skeleton {Gt_1, ..., Gt_k}, then Rep(Gt) = {tuple,LINE,[Rep(Gt_1), ..., Rep(Gt_k)]}. • If Gt is [], then Rep(Gt) = {nil,LINE}. • If Gt is a cons skeleton [Gt_h | Gt_t], then Rep(Gt) = {cons,LINE,Rep(Gt_h),Rep(Gt_t)}. • If Gt is a binary constructor <<Gt_1:Size_1/TSL_1, ..., Gt_k:Size_k/TSL_k>>, then Rep(Gt) ={bin,LINE,[{bin_element,LINE,Rep(Gt_1),Rep(Size_1),Rep(TSL_1)}, ..., {bin_element,LINE,Rep(Gt_k),Rep(Size_k),Rep(TSL_k)}]}. For Rep(TSL), see above. An omitted Size is represented by default. An omitted TSL (type specifier list) is represented by default. • If Gt is Gt_1 Op Gt_2, where Op is a binary operator, then Rep(Gt) = {op,LINE,Op,Rep(Gt_1),Rep(Gt_2)}. • If Gt is Op Gt_0, where Op is a unary operator, then Rep(Gt) = {op,LINE,Op,Rep(Gt_0)}. • If Gt is #Name{Field_1=Gt_1, ..., Field_k=Gt_k}, then Rep(E) = {record,LINE,Name, [{record_field,LINE,Rep(Field_1),Rep(Gt_1)}, ..., {record_field,LINE,Rep(Field_k),Rep(Gt_k)}]}. • If Gt is #Name.Field, then Rep(Gt) = {record_index,LINE,Name,Rep(Field)}. • If Gt is Gt_0#Name.Field, then Rep(Gt) = {record_field,LINE,Rep(Gt_0),Name,Rep(Field)}. • If Gt is A(Gt_1, ..., Gt_k), where A is an atom, then Rep(Gt) = {call,LINE,Rep(A),[Rep(Gt_1), ..., Rep(Gt_k)]}. • If Gt is A_m:A(Gt_1, ..., Gt_k), where A_m is the atom erlang and A is an atom or an operator, then Rep(Gt) ={call,LINE,{remote,LINE,Rep(A_m),Rep(A)},[Rep(Gt_1), ..., Rep(Gt_k)]}. • If Gt is {A_m,A}(Gt_1, ..., Gt_k), where A_m is the atom erlang and A is an atom or an operator, then Rep(Gt) ={call,LINE,Rep({A_m,A}),[Rep(Gt_1), ..., Rep(Gt_k)]}. • If Gt is ( Gt_0 ), then Rep(Gt) = Rep(Gt_0), i.e., parenthesized guard tests cannot be distinguished from their bodies. Note that every guard test has the same source form as some expression, and is represented the same way as the corresponding expression. 55 T HE ABSTRACT FORMAT AFTER PREPROCESSING The compilation option debug_info can be given to the compiler to have the abstract code stored in the abstract_code chunk in the BEAM file (for debugging purposes). In OTP R9C and later, the abstract_code chunk will contain {raw_abstract_v1,AbstractCode} where AbstractCode is the abstract code as described in this documen. 56 A PÉNDICE B - SMERL En este anexo se adjunta la implementación de las funciones del módulo SMERL utilizadas en este proyecto. T IPO DE DATOS META _ MOD () -record(meta_mod, {module, file, exports = [], forms = [], export_all = false}). Se trata de la estructura de datos que contiene la representación abstracta del módulo en cuestión. SMERL : FOR _ FILE (F ILE N AME ) for_file(SrcFilePath) -> for_file(SrcFilePath, []). for_file(SrcFilePath, IncludePaths) -> for_file(SrcFilePath, IncludePaths, []). for_file(SrcFilePath, IncludePaths, Macros) -> case epp:parse_file(SrcFilePath, [filename:dirname(SrcFilePath) | IncludePaths], Macros) of {ok, Forms} -> mod_for_forms(Forms); _err -> {error, {invalid_module, SrcFilePath}} end. mod_for_forms([{attribute,_,file,{FileName,_FileNum}}, {attribute, _, module, ModuleName}|Forms]) -> {Exports, OtherForms, ExportAll} = lists:foldl( fun({attribute, _, export, ExportList}, {ExportsAcc, FormsAcc, ExportAll}) -> {ExportList ++ ExportsAcc, FormsAcc, ExportAll}; ({attribute, _, compile, export_all}, {ExportsAcc, FormsAcc, _ExportAll}) -> {ExportsAcc, FormsAcc, true}; ({eof, _}, Acc) -> Acc; (Form, {ExportsAcc, FormsAcc, ExportAll}) -> {ExportsAcc, [Form | FormsAcc], ExportAll} end, {[], [], false}, Forms), {ok, #meta_mod{module = ModuleName, file = FileName, exports = Exports, forms = OtherForms, export_all = ExportAll }}; mod_for_forms(Mod) -> {error, {invalid_module, Mod}}. 57 Devuelve el meta modelo (record #meta_mod ) del fichero que se le pasa como argumento. La función encargada del parseo del fichero es parse_file de la librería estándar de erlang epp que preprocesa el código de Erlang. La función mod_for_forms se encarga de crear el meta_mod. SMERL : GET _ FORMS (M ETA _ MOD ) get_forms(MetaMod) -> MetaMod#meta_mod.forms. Devuelve la lista de forms que conforman el Abstract Format del fichero en cuestión accediendo al campo correspondiente de la estructura de datos meta_mod. SMERL : SET _ FORMS (M ETA _ MOD ,F ORMS ) set_forms(MetaMod, Forms) -> MetaMod#meta_mod{forms = Forms}. Asigna al campo correspondiente de la estructura de datos meta_mod la lista de forms (que conforman el Abstract Format) que se le pasa como segundo argumento. SMERL : TO _ SRC to_src(MetaMod, FileName) -> Src = to_src(MetaMod), file:write_file(FileName, list_to_binary(Src)). -spec(to_src(MetaMod::t_meta_mod()) -> string()). to_src(MetaMod) -> ExportsForm = {attribute,1,export,get_exports(MetaMod)}, AllForms = [{attribute,1,module,get_module(MetaMod)}, ExportsForm |get_forms(MetaMod)], erl_prettypr:format(erl_syntax:form_list(AllForms)).