Universidad Politécnica de Valencia Facultad de Informática Ingenierı́a Informática Proyecto Fin de Carrera DATALOG SOLVE Un evaluador de consultas Datalog y su aplicación al análisis de código Java Alumno: Marco A. Feliú Año Académico 2007/2008 Universidad Politécnica de Valencia Camino de Vera, s/n 46022 Valencia España Índice general Introducción al análisis de programas III I.1. Motivación . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . iii I.2. Nuestra solución . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . v I.3. OPEN/CÆSAR . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . vi I.4. Aportaciones originales . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . vi I.5. Organización del documento . . . . . . . . . . . . . . . . . . . . . . . . . . . . vii 1. Preliminares 1 1.1. Análisis de programas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 1.1.1. Conceptos básicos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 1.1.2. Análisis interprocedimental . . . . . . . . . . . . . . . . . . . . . . . . 3 1.1.3. ¿Para qué es necesario el análisis interprocedimental? . . . . . . . . . 4 1.2. Datalog: una representación lógica del flujo de datos . . . . . . . . . . . . . . 6 1.2.1. Sintaxis y semántica . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6 1.2.2. Un algoritmo de análisis de punteros sencillo . . . . . . . . . . . . . . 11 1.2.3. Análisis interprocedimental insensible al contexto . . . . . . . . . . . . 17 1.2.4. Análisis de punteros sensible al contexto . . . . . . . . . . . . . . . . . 20 1.2.5. Implementación de Datalog mediante DDBs . . . . . . . . . . . . . . . 24 1.3. Pbes: un formalismo para analizar programas . . . . . . . . . . . . . . . . . . 28 2. De Datalog a Bes 31 2.1. Representación de una consulta Datalog . . . . . . . . . . . . . . . . . . . . . 31 2.2. Instanciación a un BES sin parámetros . . . . . . . . . . . . . . . . . . . . . . 34 2.2.1. Optimizaciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36 2.2.2. Extracción de soluciones . . . . . . . . . . . . . . . . . . . . . . . . . . 38 ÍNDICE GENERAL ii 3. Arquitectura de la aplicación 39 3.1. Vista general . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39 3.2. Una visión externa . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40 3.2.1. Entrada del programa . . . . . . . . . . . . . . . . . . . . . . . . . . . 40 3.2.2. Salida del programa . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43 3.2.3. Modos de ejecución . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 44 3.3. Una visión interna . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 44 3.3.1. Primera fase: traducción . . . . . . . . . . . . . . . . . . . . . . . . . . 44 3.3.2. Segunda fase: resolución del Bes . . . . . . . . . . . . . . . . . . . . . 56 3.3.3. Tercera fase: extracción de respuestas . . . . . . . . . . . . . . . . . . 58 3.3.4. Versiones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59 3.4. DATALOG SOLVE: Ejecutable . . . . . . . . . . . . . . . . . . . . . . . . . . . 59 Conclusiones y trabajo futuro 61 C.1. Trabajo relacionado . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61 C.2. Evaluación experimental . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 62 A. DATALOG SOLVE API Bibliografı́a 65 67 Introducción al análisis de programas I.1. Motivación En el proceso de transición desde una economı́a industrial hacia una economı́a global y basada en el conocimiento, las tecnologı́as de la informática se han convertido en un factor determinante de los avances de la productividad y, en consecuencia, del crecimiento económico. Los resultados de estudios como [JV05] son concluyentes en relación con el hecho de que, a partir de la segunda mitad de la década de los noventa, estas tecnologı́as han desempeñado un papel determinante y creciente en la explicación de los avances de la productividad del trabajo en los paı́ses del G7 [VT06]. Uno de los problemas actuales en el desarrollo de sistemas de software es la complejidad, cada vez más alta, de analizar y garantizar el comportamiento fiable de estos sistemas. Con el crecimiento de los sistemas de software, que han evolucionado hasta alcanzar tamaños y complejidades extraordinariamente altos, su inestabilidad se está convirtiendo, lamentablemente, en algo cotidiano y asumido con resignación por los usuarios en una sociedad de la información cada vez más exigente. La fiabilidad se ha convertido por este motivo en una necesidad reconocida y urgente en las industrias de desarrollo de software. Para garantizar esta fiabilidad son necesarias técnicas, métodos y herramientas que soporten de forma adecuada el proceso de desarrollo. Este proyecto se enmarca dentro del desarrollo de métodos, técnicas y herramientas para la construcción de software fiable y de calidad, dedicando especial atención a su posible aplicación en los procesos reales de producción de la industria del software. Dentro del amplio abanico de aproximaciones para mejorar la construcción del software, la propuesta de este trabajo se centra en el uso “ágil” (lightweight) de los métodos formales, y en particular los métodos basados en la lógica, en la Ingenierı́a del Software. La aproximación ágil se basa en la utilización parcial de formalismos a distintos niveles (lenguaje, modelado, análisis y composición), donde la idea fundamental es la de sacrificar el objetivo de lograr métodos generalistas que soporten todo el proceso de desarrollo de software en beneficio del INTRODUCCIÓN AL ANÁLISIS DE PROGRAMAS iv uso puntual de formalismos en determinadas etapas del ciclo de vida del software. Métodos lógicos en la informática. La teorı́a, las técnicas y las herramientas basadas en lógicas están teniendo un impacto cada vez mayor en muy diversas áreas de la informática, pero también en la resolución de numerosos problemas computacionales en la industria y en otras ciencias como la biologı́a. Este auge de los métodos basados en la lógica tiene diversas razones. Por un lado, puesto que la informática es aún una ciencia joven, sus numerosas técnicas ad-hoc todavı́a están evolucionando hacia fundamentos comunes más generales y mejor estudiados, y, como veremos, a menudo estos fundamentos resultan estar basados en la lógica. El auge de los métodos lógicos también tiene su explicación teórica: la mayorı́a de los formalismos (autómatas, lenguajes, clases de complejidad, etc.) tienen sus contrapartidas lógicas. Existen correspondencias entre mecanismos de cómputo y lógicas tales como las establecidas por el isomorfismo de CurryHoward. Pero sobre todo cabe destacar las repercusiones para la práctica, tal y como ya predijo Alan Turing: “I expect that digital computing machines will eventually stimulate a considerable interest in symbolic logic (...). The language in which one communicates with these machines (...) forms a sort of symbolic logic.” Asimismo, McCarthy escribió, ya en los años 60, que la lógica estaba llamada a tener para la informática una importancia comparable a la que tuvo el análisis matemático para la fı́sica en el siglo XIX. Manna y Waldinger [MW85] han llamado a la lógica “The Calculus of Computer Science” debido a que su papel en la informática tanto teórica como práctica es similar al de las matemáticas en las ciencias fı́sicas y, en particular, al del cálculo en ingenierı́a. Al igual que los arquitectos e ingenieros analizan matemáticamente sus construcciones, los informáticos pueden analizar las propiedades lógicas de sus sistemas mientras los diseñan, desarrollan, verifican y mantienen, especialmente cuando se trata de sistemas crı́ticos económicamente, en seguridad o en privacidad. Los análisis lógicos pueden ser reveladores también en sistemas cuya eficiencia resulta crı́tica. Además, en todos los tipos de sistemas, los métodos y herramientas basados en la lógica pueden mejorar la calidad y reducir costes. Esta visión está ampliamente documentada en el artı́culo “On the Unusual Effectiveness of Logic in Computer Science” [HHI+ 01], donde el papel crucial de la lógica en áreas como las bases de datos, los lenguajes de programación, la complejidad, los sistemas de agentes y la verificación es explicado por expertos mundiales en cada una de estas áreas; véase también http://www.cs.rice.edu/~vardi/logic/. NUESTRA SOLUCIÓN Finalidad de este proyecto. v El objetivo principal de este proyecto es desarrollar una técnica de análisis de programas Java usando el lenguaje de especificación Datalog para expresar los análisis y el formalismo de los sistemas de ecuaciones booleanas (Bes, del inglés boolean equation systems) para su resolución. En la literatura podemos encontrar una de las aproximaciones clásicas para el análisis y certificación de programas donde se utilizan los sistemas de tipos y efectos para establecer la relación entre las propiedades de seguridad y los programas [NL96, NR01] para código de bajo nivel ensamblador o bytecode. También se ha aplicado interpretación abstracta al bytecode de Java en [BJP05] empleando el demostrador de teoremas Coq. En este proyecto nos planteamos seguir una aproximación similar a la de Bddbddb [WACL05] donde se especifica un análisis determinado como un programa escrito en un lenguaje lógico y se extraen los datos necesarios (la entrada del análisis) a partir del bytecode de Java mediante un programa diseñado para tal propósito. De esta manera, la resolución del programa lógico se corresponde esencialmente a la ejecución del análisis. En nuestro caso, siendo la eficiencia uno de los factores clave para la aceptación de las técnicas de análisis de programas a nivel práctico, la hemos buscado en los Besya que existen motores de resolución para los mismos altamente eficientes. En la siguiente sección describiremos más detalladamende este proceso. I.2. Nuestra solución Proponemos una traducción del lenguaje lógico de especificación Datalog a Bes de forma que transformaremos un análisis determinado especificado mediante un programa Datalog a un sistema de ecuaciones booleanas que será resuelto de forma eficiente gracias a los motores de resolución existentes. La resolución del sistema de ecuaciones booleanas se corresponderá con la resolución del análisis especificado inicialmente. Esquemáticamente, nuestra aproximación al análisis estático consta de las siguientes fases: 1. Especificación de un análisis estático como un programa lógico en el lenguaje Datalog. 2. Extracción de la información del programa Java necesaria para el análisis mediante el compilador JOEQ. 3. Evaluación del programa Datalog transformándolo a un Bes y evaluándolo. 4. Extracción de las soluciones del programa datalog a partir del resultado de la ejecución del Bes asociado. INTRODUCCIÓN AL ANÁLISIS DE PROGRAMAS vi Resolución guiada por objetivos. La resolución del programa Datalog propuesta será top-down. Esto es, se partirá de las consultas u objetivos del programa y se computarán los predicados necesarios para probar la veracidad de los anteriores. En esencia, la resolución top-down supone la construcción de un árbol de demostración desde la raı́z a las hojas, como harı́a un demostrador de teoremas basado en resolución. BESs on the fly. En nuestra aproximación, la resolución del programa datalog se hace resolviendo un Bes formalmente equivalente al primero. Este Bes no se desarrollará explı́citamente en memoria, lo que serı́a excesivamente costoso, sino que estará almacenado de forma implı́cita en memoria y se irá generando de forma explı́cita on-the-fly, esto es, conforme avance su resolución. I.3. OPEN/CÆSAR En este proyecto usamos el marco de trabajo OPEN/CÆSAR especialmente apropiado para desarrollar herramientas orientadas a la simulación y verificación de sistemas. OPEN/CÆSAR ofrece la funcionalidad tı́picamente necesaria para este tipo de desarrollos tales como un lenguaje genérico sobre el que trabajar, algoritmos de exploración o estructuras de datos variadas. Este framework ofrece esta funcionalidad estructurada en 3 módulos distintos: graph Ofrece una visión del sistema o programa a verificar como una sistema de transiciones etiquetadas entre estados. Esto hace que OPEN/CÆSAR sea independiente del lenguaje de especificación del sistema usado. library Es un conjunto de bibliotecas. Cada una de ellas ofrece estructuras de datos y funciones para manipularlas (tablas, pilas, etc.). exploration Determina cómo se explorará el sistema de transiciones etiquetadas entre estados, ya que contiene los algoritmos de verificación, simulación o testing. Además, OPEN/CÆSAR está escrito en C. Por todo lo expuesto, tiene un diseño muy genérico ofreciendo la flexibilidad necesaria para que formalismos con expresividad muy variable puedan ser adaptados al mismo. Usando las facilidades ofrecidas por OPEN/CÆSAR se reduce el esfuerzo necesario para el desarrollo de este tipo de herramientas. I.4. Aportaciones originales La aportación novedosa que supone este trabajo es doble. Por una parte, propone la transformación de un programa datalog a un Bes equivalente, permitiendo la resolución de ORGANIZACIÓN DEL DOCUMENTO vii consultas del primero mediante los motores de evaluación existentes para el segundo. Por otra parte, aplica la idea anterior en el contexto del análisis estático a código Java. Se ha demostrado la equivalencia formal entre un programa lógico datalog y las traducciones propuestas, fundamentando formalmente el trabajo realizado. De esta demostración, se ha deducido que los sistemas de ecuaciones booleanas parametrizadas (Pbes, del inglés parameterised boolean equation systems), y por ende los Bes, son al menos tan expresivos como Datalog. La traducción a Bes aporta todas las mejoras existentes para su resolución eficiente. Desde este punto de vista es interesante considerar la existencia de algoritmos distribuidos de resolucion de Bes que podrı́an aplicarse en un futuro cercano a nuestra aproximación al análisis de programas. En última instancia, además de las expectativas que puede suscitar la aproximación seguida, las ideas están concretadas en una herramienta real: DATALOG SOLVE. I.5. Organización del documento El documento introduce progresivamente las nociones necesarias para la comprensión del trabajo realizado dentro del proyecto. La presente introducción expone los factores que motivan el trabajo realizado, ası́ como en qué consiste a grandes rasgos éste. El primer capı́tulo expone los fundamentos requeridos para la comprensión del formalismo desarrollado, ası́ como para la comprensión de la naturaleza de los problemas que éste pretende resolver y la principal aplicación del mismo. El Capı́tulo 2 presenta el formalismo aportado para la resolución de análisis de programas. El Capı́tulo 3 detalla, a un nivel medio, las caracterı́sticas de implementación de la aplicación que da soporte al formalismo introducido. Las conclusiones finales presentan un resumen del trabajo realizado, enumeran los estudios previos considerados de interés para el presente trabajo, exponen los resultados experimentales y, por último, describen la dirección que toma en estos momentos el trabajo realizado. viii INTRODUCCIÓN AL ANÁLISIS DE PROGRAMAS Capı́tulo 1 Preliminares En este capı́tulo vamos a introducir los conceptos básicos que utilizaremos a lo largo del texto. Estos conceptos estarán relacionados con el análisis de programas, el lenguaje lógico Datalog y los sistemas de ecuaciones booleanas paremetrizadas. 1.1. Análisis de programas El análisis de programas es un subcampo de las ciencias de la computación que trata con la obtención de aproximaciones estáticas1 lo más precisas posibles a la ejecución de programas. 1.1.1. Conceptos básicos Análisis de flujo de datos El análisis de flujo de datos (del inglés data-flow analysis) se refiere a un conjunto de técnicas que extraen información sobre el flujo de los datos a lo largo de los caminos de ejecución de un programa. La ejecución de un programa puede ser vista como una serie de transformaciones del estado del programa, que consiste en los valores de todas las variables del mismo, incluyendo aquéllas presentes en los registros de activación de la pila de ejecución. La ejecución de una instrucción transforma un estado de entrada en un estado de salida. El estado de entrada se asocia con el punto del programa anterior a la instrucción y el estado de salida se asocia con el punto del programa posterior a la instrucción. Al analizar el comportamiento de un programa, debemos considerar todas las posibles secuencias de puntos de programa, que son llamadas caminos, a través del grafo de flujo que puede seguir la ejecución del programa. A partir de los caminos se extrae, de todos los 1 En tiempo de compilación. 2 CAPÍTULO 1. PRELIMINARES posibles estados del programa, la información necesaria para resolver el problema particular de análisis de flujo de datos. Grafo de llamadas Un grafo de llamadas (del inglés call-graph) de un programa es un conjunto de nodos y arcos tales que: 1. Hay un nodo por cada procedimiento en el programa 2. Hay un nodo por cada punto de llamada (del inglés call site), esto es, un punto del programa donde se invoca un procedimiento. 3. Si el punto de llamada c puede invocar al procedimiento p, entonces existe una arista desde el nodo asociado a c al nodo asociado a p. Si la invocación a procedimientos es directa, el destino de la llamada puede conocerse estáticamente. En tal caso, cada punto de llamada tendrı́a solamente un arco hacia un procedimiento en el grafo de llamadas. Sin embargo, la norma en los lenguajes orientados a objetos con enlace dinámico es la invocación indirecta a procedimiento —también se puede encontrar en lenguajes como C mediante el uso de los punteros a función, o en Fortran mediante el uso de parámetros que permiten recibir referencias a procedimiento. Especı́ficamente, cuando se sobrescribe un método m en una subclase B de una clase A, una llamada a m sobre una variable polimórfica de tipo A puede referirse a distintos métodos, dependiendo del objeto receptor de la misma. El uso de esas invocaciones virtuales a método implica que se debe conocer el tipo del receptor antes de que podamos determinar el método que es invocado. En general, la invocación indirecta nos obliga a realizar una aproximación estática de las condiciones en las que se realizan las llamadas a procedimientos —condiciones que pueden ser valores de punteros a función, referencias a procedimientos o tipos de objeto receptores, dependiendo del contexto. Dicha aproximación estática no es sino un caso particular de análisis de programas. Análisis intraprocedimental vs. interprocedimental Una de las formas de clasificar el análisis de programas es según el alcance de sus técnicas en un programa. Siguiendo esta clasificación, se distinguen los análisis intraprocedimentales e interprocedimentales. 1.1. ANÁLISIS DE PROGRAMAS 3 Las técnicas de análisis intraprocedimental son una familia de técnicas de análisis de programas cuyo alcance son los procedimientos a nivel local. Esto es, intentan analizar un procedimiento independientemente de las relaciones de éste con el resto del programa. Dado lo restringido de su ámbito, existen muchas técnicas eficientes para realizarlos. No obstante, la información que extraen de los programas es mucho menos precisa que la que permiten extraer técnicas cuyo ámbito sea mayor, esto es, técnicas que actúen sobre el programa de forma global. Estas técnicas son calificadas como “interprocedimentales”, y son aquellas que son de mayor interés dadas su gran complejidad y los valiosos resultados entregados por ellas. 1.1.2. Análisis interprocedimental El análisis interprocedimental es, como previamente se introdujo, un tipo de análisis cuyo alcance es todo el programa. Por alcance total se entiende que el análisis tiene en cuenta todas las divisiones del programa (procedimientos) a la hora de extraer información. El análisis interprocedimental es complicado porque el comportamiento de cada procedimiento es dependiente del contexto en el cual es llamado. Una aproximación simple al análisis interprocedimental pero a la vez muy imprecisa es el llamado análisis insensible al contexto. En él, cada instrucción de llamada y retorno de procedimiento se consideran instrucciones goto (salto incondicional), creando un super grafo de flujo de control con arcos de flujo de control adicionales que unen: 1. Cada punto de llamada con el comienzo del procedimiento al que llama, y 2. cada instrucción de retorno con la siguiente instrucción a la del punto de llamada. Además, se añaden instrucciones de asignación para copiar cada parámetro actual en su correspondiente parámetro formal y cada valor retornado en la variable que recibe el resultado. Entonces, se aplica un análisis estándar intraprocedimental sobre el super grafo de flujo de control para obtener resultados interprocedimentales insensibles al contexto. Pese a su simplicidad, este modelo elimina la relación entre los valores de entrada y salida en las invocaciones a procedimiento. Esto se debe a que no se establece ninguna correspondencia entre cada arco de entrada y salida a procedimiento, tratando todas las entradas y salidas al mismo como un todo, lo que supone una fuente de imprecisión. Cadenas de llamadas Un contexto de llamada queda determinado por el contenido de la pila de llamadas en el momento de realizar la invocación. Se define una cadena de llamadas (del inglés call string) como la secuencia de puntos de llamada que hay en la pila. 4 CAPÍTULO 1. PRELIMINARES Al diseñar un análisis sensible al contexto se puede elegir la precisión del mismo basándose en la forma de distinguir contextos. Si se distinguen los contextos exclusivamente por los k puntos de llamada más inmediatos tendrı́amos un análisis de contexto k-limitado. Por otra parte, se podrı́an distinguir completamente todas las cadenas de llamadas acı́clicas (cadenas sin ciclos recursivos) para acotar el número de contextos distintos a analizar. Las cadenas de llamadas con recursión podrı́an simplificarse reduciendo cada secuencia recursiva de puntos de llamada a un punto de llamada único. No obstante, aun para programas sin recursión, el número de contextos de llamada puede ser exponencial con el número de procedimientos en un programa. Técnicas de análisis sensibles al contexto Existen distintas aproximaciones para realizar análisis sensibles al contexto entre las que destacamos dos: la que se basa en la clonación y la que se basa en el resumen. Una análisis sensible al contexto basado en clonación consiste en clonar conceptualmente cada procedimiento para cada contexto de interés. De esta forma, cada contexto tendrá un clon exclusivo del procedimiento y, por lo tanto, podremos aplicar un análisis insensible al contexto sin que haya ambigüedad (ya que cada invocación a procedimiento siempre se hará desde un único contexto). Un análisis sensible al contexto basado en resumen consiste en la representación de cada procedimiento mediante una concisa descripción, el “resumen”2 , que describe parte del comportamiento observable del mismo, y usar éstas para calcular el efecto de todas las llamadas ocurridas en el programa. 1.1.3. ¿Para qué es necesario el análisis interprocedimental? Dada la dificultad del análisis interprocedimental, se deben remarcar las razones por las que este tipo de análisis es útil como, por ejemplo, la optimización de invocaciones a métodos virtuales, el análisis de punteros, la paralelización de programas, y la detección de errores y vulnerabilidades. Invocaciones a métodos virtuales Los programas escritos en lenguajes orientados a objetos suelen estar compuestos de muchos métodos de tamaño reducido. Si solamente se intentaran optimizar los métodos por separado (análisis intraprocedimental), las oportunidades para optimizar serı́an escasas dado 2 El fin del resumen es evitar analizar el cuerpo del procedimiento para cada punto de llamada que invoque al mismo. 1.1. ANÁLISIS DE PROGRAMAS 5 su reducido tamaño. Resolver las invocaciones a métodos permite aumentar las oportunidades de optimización. Si el código fuente de un programa está disponible, es posible realizar un análisis interprocedimental para determinar los posibles tipos de objeto a los que podrı́a apuntar una variable x en toda llamada a método x.m(). Si el tipo para una variable x fuera único, la invocación al método podrı́a resolverse estáticamente y el método podrı́a desplegarse, evitando que se realice una llamada a función al ejecutarlo. Varios lenguajes, como Java, cargan dinámicamente sus clases, por lo que en tiempo de compilación no sabemos qué método m será invocado para la llamada x.m(). Una optimización común en compiladores JIT (del inglés Just-In-Time) es analizar la ejecución y ası́ determinar los tipos más comunes. Los métodos de los tipos más comunes se despliegan y se inserta código que compruebe dinámicamente los tipos para asegurar una ejecución correcta. Análisis de punteros El análisis de punteros es un tipo de análisis interprocedimental que permite saber a qué puede apuntar un puntero en el programa. Los resultados de este análisis permiten mejorar la precisión de otros análisis tanto interprocedimentales como intraprocedimentales. Paralelización La forma más efectiva de paralelizar una aplicación es encontrar la granularidad más grande de paralelismo. Para ello, el análisis interprocedimental es esencial debido a su actuación en partes más grandes del programa (granularidad más gruesa) y a su mayor precisión, lo que permitirá perder menos oportunidades de optimización. Detección de errores en el software y vulnerabilidades El análisis interprocedimental no sólo es útil para optimizar código. Sus técnicas pueden usarse para analizar muchos tipos de errores de codificación comunes. Estos errores, que hacen al software menos fiable, muchas veces no son detectables localmente a un procedimiento, sino que su búsqueda requiere una exploración interprocedimental. Si los errores son vulnerabilidades de la seguridad se hace incluso más importante su total detección. 6 CAPÍTULO 1. PRELIMINARES 1.2. Datalog: una representación lógica del flujo de datos Las aproximaciones más tradicionales al análisis del flujo de datos usan una notación basada en conjuntos del tipo “la definición D está en el conjunto ENTRADA[BLOQUE ]”. En nuestra aproximación usamos una notación más general y condensada basada en la lógica con la que escribiremos entrada(BLOQUE,D) para expresar lo mismo. Utilizando este tipo de notación podremos expresar reglas, de una forma concisa, que nos permitirán deducir hechos del programa. También nos permitirá implementar estas reglas de forma eficiente independientemente del análisis concreto que se desee realizar. Finalmente, la aproximación lógica nos permitirá combinar varios análisis aparentemente independientes en uno único integrado. 1.2.1. Sintaxis y semántica Datalog es un lenguaje que usa una notación parecida a Prolog, pero cuya semántica es mucho más simple. Para empezar, los componentes básicos de un programa Datalog son átomos de la forma p(X1 , X2 , ..., Xn ), donde: 1. p es un sı́mbolo de predicado —un sı́mbolo que, por ejemplo en el caso de entrada, podrı́a representar un tipo de afirmación como “una definición llega al comienzo de un bloque”. 2. X1 , X2 , ..., Xn son términos que pueden ser variables o constantes. Un átomo básico (del inglés ground atom) es un predicado con sólo constantes como argumentos. Todo átomo básico afirma un hecho particular y su valor es verdadero o falso. A menudo es conveniente representar un predicado mediante una relación, o tabla de sus átomos básicos verdaderos. Cada átomo básico está representado por una única fila, o tupla, de la relación. Las columnas de la relación se llaman atributos, y cada tupla tiene un componente para cada atributo. Los atributos correponden a los componentes de los átomos básicos representados por la relación. Todo átomo básico en la relación es verdadero y los átomos básicos que no estén en la relación son falsos. También se permitirán átomos con la forma de comparaciones entre variables y constantes. Un ejemplo podrı́a ser X! = Y o X = 10. En estos ejemplos, el sı́mbolo de predicado es realmente el operador de comparación. Esto es, podemos pensar en X = 10 (una comparación) como si estuviera escrito en la forma de predicado: equals(X,10). De todas formas, hay una diferencia importante entre los predicados de comparación y el resto. Un predicado 1.2. DATALOG: UNA REPRESENTACIÓN LÓGICA DEL FLUJO DE DATOS 7 de comparación tiene su interpretación estándar, mientras que un predicado como entrada se interpreta sólo en función de su definición en el programa Datalog. Un literal es o un átomo o la negación de un átomo. Se indica la negación con la palabra NOT delante del átomo. Ası́, N OT entrada(BLOQU E, D) es una afirmación que indica que la definición D no alcanza el comienzo del bloque BLOQU E. Reglas en Datalog Las reglas son una forma de expresar inferencias lógicas. En Datalog, las reglas también sirven para sugerir cómo deberı́a llevarse a cabo una computación de los hechos verdaderos. La forma de una regla es: H : −B1 &B2 &...&Bn (1.2.1) donde: H y B1 , B2 , ..., Bn son literales —átomos o negaciones de átomos. H es la cabeza y B1 , B2 , ..., Bn forman el cuerpo de la regla. Cada uno de los Bi s a veces es llamado subobjetivo de la regla. El sı́mbolo : − ha de ser leı́do como “si”. El sentido de una regla es “la cabeza es verdadera si el cuerpo es verdadero”. Más precisamente, aplicamos una regla a un conjunto de átomos básicos dado como sigue: se consideran todas las posibles sustituciones de constantes por variables de la regla. Si una sustitución hace que todo subobjetivo del cuerpo sea verdadero (asumiendo que solamente los átomos básicos dados son verdaderos), entonces podemos inferir que la cabeza con esta sustitución de constantes por variables es un hecho verdadero. Las sustituciones que no hacen todos los subobjetivos verdaderos no nos dan información; el átomo de la cabeza podrı́a ser verdadero o no3 . Un programa Datalog es una colección de reglas. Este programa se aplica a “datos”, esto es, a un conjunto de átomos básicos. El resultado del programa es el conjunto de átomos básicos inferidos mediante la aplicación sucesiva de las reglas hasta que no se puedan hacer más inferencias. 3 El hecho de que una cabeza no se haga verdadera al aplicar una regla no implica que ésta sea falsa. Podrı́a haber otra regla (o varias) que, al ser aplicadas, la hicieran verdadera. Por lo tanto, una cabeza sólo se hará falsa si no es verdadera según ninguna regla. 8 CAPÍTULO 1. PRELIMINARES Convenciones en Datalog Los programas Datalog utilizan normalmente una serie de convenciones léxicas para facilitar su interpretación: 1. Todas las variables comienzan con letra mayúscula. 2. El resto de elementos comienzan con letra minúscula u otros sı́mbolos como los dı́gitos. Estos elementos incluyen predicados y constantes. Otra convención en los programas Datalog es distinguir los predicados entre: 1. EDB, del ingés Extensional Database, o Base de Datos Extensional, predicados definidos a priori. Esto es, los hechos verdaderos de estos predicados se dan en una relación o tabla, o se dan mediante el significado del predicado (como serı́a el caso para un predicado de comparación). 2. IDB, del inglés Intensional Database, o Base de Datos Intensional, predicados que están definidos mediante reglas. Un predicado debe ser IDB o EDB, y puede ser solamente uno de estos. En consecuencia, cualquier predicado que aparezca en la cabeza de una o más reglas debe ser un predicado IDB. Los predicados que aparecen en el cuerpo pueden ser IDB o EDB. Al usar programas Datalog para expresar análisis de flujo de datos, los predicados EDB se computan a partir del grafo de flujo mismo. Los predicados IDB se definen mediante las reglas, y el problema de flujo de datos se resuelve infiriendo todos los posibles hechos IDB a partir de las reglas y los hechos EDB dados. Ejecución de programas Datalog Todo conjunto de reglas Datalog define relaciones para sus predicados IDB como una función de las relaciones dadas por sus predicados EDB. Se comienza con la suposición de que las relaciones IDB están vacı́as, esto es, que los predicados IDB son falsos para todos sus posibles argumentos. Entonces, se aplican las reglas repetidamente, infiriendo nuevos hechos cuando las reglas los requieran. Cuando el proceso converge, se finaliza, y las relaciones IDB resultantes forman la salida del programa. Este proceso se formaliza en el Algoritmo 1: 1.2. DATALOG: UNA REPRESENTACIÓN LÓGICA DEL FLUJO DE DATOS 9 Algoritmo 1 Evaluación simple de programas Datalog. ENTRADA: Un programa Datalog y un conjunto de hechos para cada predicado EDB. SALIDA: Un conjunto de hechos para cada predicado IDB. MÉTODO: Para cada predicado p en el programa, sea Rp la relación de hechos que son verdaderos para ese predicado. Si p es un predicado EDB, entonces Rp es el conjunto de hechos dados para ese predicado. Si p es un predicado IDB, debemos computar Rp ejecutando el Algoritmo 2. Algoritmo 2 Cómputo de hechos IDB. Para todo (predicado p ∈ IDB) hacer Rp := ∅; Fin Para Mientras (Ocurran cambios a cualquier Rp ) hacer Considera todas las posibles sustituciones de constantes por variables en todas las reglas. Determina para cada sustitución, si todos los subobjetivo del cuerpo son verdaderos usando los Rp s actuales para determinar la verdad de los predicados EDB e IDB. Si (Una sustitución hace el cuerpo de una regla verdadero) Entonces Añadir la cabeza a Rq si q es la cabeza del predicado. Fin Si Fin Mientras Evaluación incremental de programas Datalog Hay una mejora de eficiencia posible para el algoritmo anterior. Nótese que un hecho IDB nuevo puede ser descubierto en la iteración i si se cumple que es el resultado de una sustitución de constantes en una regla tal que al menos uno de los subobjetivos ha sido descubierto en la iteración i − 1. La prueba para esta observación es que, si todos los hechos entre los subobjetivos fueran conocidos en la iteración i − 2, entonces los hechos “nuevos” se habrı́an descubierto al hacerse la sustitución de constantes en la iteración i − 1. Para explotar esta observación, se introduce para cada predicado IDB p un predicado nuevoP que contendrá solamente los hechos de p descubiertos en la iteración anterior. Cada regla que tenga uno o más predicados IDB entre sus subobjetivos se reemplazará por una colección de reglas. Cada regla de la colección se forma reemplazando exactamente una ocurrencia de algún predicado IDB q en el cuerpo, por un predicado nuevoQ. Finalmente, para todas las reglas, se reemplazan los predicados h de la cabeza por nuevoH. Las reglas resultantes se dice que están en forma incremental. Las relaciones para cada predicado IDB p acumulan todos los hechos-p. En una iteración: 10 CAPÍTULO 1. PRELIMINARES 1. Se aplican las reglas para evaluar los predicados nuevoP . 2. Entonces, se substrae p de nuevoP para asegurar que los hechos en nuevoP son realmente nuevos. 3. Se añaden los hechos de nuevoP a p. 4. Se inicializan todas las relaciones nuevoX a ∅ para la siguiente iteración. Estas ideas se formalizan en el Algoritmo 3. Algoritmo 3 Evaluación incremental de programas. ENTRADA Un programa Datalog y un conjunto de hechos para cada predicado EDB. SALIDA Un conjunto de hechos para cada predicado IDB. MÉTODO Para cada predicado p en el programa, sea Rp la relación de hechos que son verdaderos para ese predicado. Si p es un predicado EDB, entonces Rp es el conjunto de hechos dados para ese predicado. Si p es un predicado IDB, debemos computar Rp . Además, para cada predicado IDB p, sea RnuevoP la relación de hechos “nuevos” para el predicado p. 1. Modifar las reglas a su forma incremental según las explicaciones dadas anteriormente. 2. Ejecutar el Algoritmo 4. Reglas Datalog problemáticas Hay ciertas reglas Datalog que técnicamente no tienen sentido y, por lo tanto, no deberı́an usarse. Los riesgos importantes son los siguientes: 1. Reglas inseguras: aquellas que tienen una variable en la cabeza que no aparece en el cuerpo. En este caso, la variable en cuestión no está restringida y puede tomar cualquier valor de su dominio. 2. Programas no estratificados: conjuntos de reglas que tienen recursión en la que interviene una negación. Para evitar las reglas inseguras cualquier variable que aparezca en la cabeza de una regla debe también aparecer en el cuerpo. Además, la aparición no puede ser únicamente en un átomo negado, en un operador de comparación, o en los dos. La razón para esta polı́tica es evitar reglas que nos permitan inferir un número infinito de hechos. 1.2. DATALOG: UNA REPRESENTACIÓN LÓGICA DEL FLUJO DE DATOS 11 Algoritmo 4 Cómputo incremental de hechos IDB. Para todo ( predicado p ∈ IDB) hacer Rp := ∅ RnuevoP := ∅ Fin Para Repite Considera todas las posibles sustituciones de constantes por variables en todas las reglas. Determina para cada sustitución si todos los subobjetivos del cuerpo son verdaderos, usando los Rp s y RnuevoP s actuales para determinar la verdad de los predicados EDB e IDB. Si (Una sustitución hace el cuerpo de una regla verdadero) Entonces Añadir la cabeza a Rn uevoH si h es la cabeza del predicado. Fin Si Para todo ( predicado p ) hacer RnuevoP = RnuevoP − Rp Rp = Rp ∪ RnuevoP Fin Para Hasta (∀RnuevoP = ∅) Para que un programa esté estratificado, la recursión y la negación han de separarse. El requisito formal es el siguiente. Debe ser posible dividir los predicados IDB en estratos, tal que si hay una regla con un predicado p en la cabeza y un subobjetivo de la forma N OT q(· · ·), entonces q es, o bien un predicado EDB, o es un predicado IDB en un estrato más bajo que el de p. Mientras se satisfaga esta regla, se podrá evaluar el estrato más bajo mediante un algoritmo convencional y, entonces, tratar las relaciones para los predicados IDB de ese estrato como si fueran EDB para la computación de un estrato más alto. No obstante, si violamos esa regla, entonces el algoritmo iterativo podrá no converger. 1.2.2. Un algoritmo de análisis de punteros sencillo En esta sección se introducirá un algoritmo muy sencillo de análisis de punteros insensible al flujo de datos asumiendo que no hay llamadas a procedimiento. Más adelante se extenderá para que tenga en cuenta llamadas a procedimiento de forma sensible e insensible al contexto. La sensibilidad al flujo añade mucha complejidad y es menos importante que la sensibilidad al contexto para lenguajes como Java donde los métodos tienden a ser pequeños. La pregunta fundamental del análisis de punteros es si dados un par de punteros, éstos pueden ser aliados. Una forma de responder a esta pregunta es computando para cada puntero la respuesta a esta otra pregunta: “¿a qué objetos puede apuntar este puntero?”. Si dos punteros pueden apuntar al mismo objeto, entonces los punteros podrı́an estar aliados. 12 CAPÍTULO 1. PRELIMINARES ¿Por qué el análisis de punteros es difı́cil? El análisis de punteros para programas en C es particularmente difı́cil porque los programas C pueden realizar computaciones arbitrarias sobre los punteros. De hecho, se puede leer un entero y asignarlo a un puntero, lo que convertirı́a este puntero en un potencial alias para todas las otras variables puntero del programa. Los punteros en Java, conocidos como referencias, son mucho más simples. No se permite realizar aritmética con ellos y sólo pueden apuntar al comienzo de un objeto. El análisis de punteros debe ser interprocedimental. Sin análisis interprocedimental, uno debe asumir que cualquier método que sea llamado puede cambiar el contenido de todas las variables puntero accesibles, haciendo inefectivo cualquier análisis intraprocedimental. Los lenguajes que permiten llamadas a función indirectas representan un reto adicional para el análisis de punteros. En C, se puede invocar una función indirectamente llamando a un puntero a función. Es necesario conocer a qué puede apuntar el puntero a función antes de que se pueda analizar la función llamada. Y, además, después de analizar la función llamada, pueden descubrirse más funciones a las que el puntero a función podı́a apuntar, y, como consecuencia de esto, el proceso necesita ser iterativo. Mientras que la mayor parte de las funciones se llaman directamente en C, los métodos virtuales en Java causan que muchas invocaciones sean indirectas. Dada una invocación x.m() en un programa Java, puede haber muchas clases a las que el objeto x podrı́a pertenecer y que tienen un método llamado m. Cuanto más preciso sea nuestro conocimiento del tipo real de x, más preciso será nuestro grafo de llamadas. Idealmente, podrı́amos determinar en tiempo de compilación la clase exacta de x y, ası́, saber exactamente a qué método se refiere m. Es posible aplicar aproximaciones que reduzcan el número de objetivos posibles de una llamada. Por ejemplo, se puede determinar estáticamente cuáles son todos los tipos de objetos creados, y podemos limitar el análisis a esos tipos. Pero podemos ser más precisos si descubrimos el grafo de llamadas on-the-fly 4 , basándonos en el análisis de punteros obtenido de forma simultánea. El aumento de la precisión del grafo de llamadas lleva no sólo a resultados más precisos, sino a reducir también el tiempo de análisis. Como ya hemos dicho, el análisis de punteros es complicado. A medida que se descubren nuevos valores posibles para un puntero, todas las instrucciones que asignan los contenidos de ese puntero a otro puntero deben analizarse otra vez. 4 Se puede traducir como “al vuelo” o “bajo demanda”. 1.2. DATALOG: UNA REPRESENTACIÓN LÓGICA DEL FLUJO DE DATOS 13 Un modelo para punteros y referencias Supongamos que nuestro lenguaje tiene las siguientes formas de representar y manipular referencias: 1. Algunas variables de programa son del tipo “puntero a T ” o “referencia a T ,” donde T es un tipo. Estas variables son, o bien estáticas o bien viven en la pila de ejecución. Las llamaremos simplemente variables. 2. Hay un heap de objetos. Todas las variables apuntan a objetos del heap y no a otras variables. Estos objetos serán llamados “objetos del heap”. 3. Un objeto del heap puede tener campos, y el valor de un campo puede ser una referencia a un objeto del heap (pero no a una variable). Java se puede modelar adecuadamente con esta estructura. C se modela peor ya que las variables puntero pueden apuntar a otras variables puntero y, en principio, cualquier valor puede ser visto como un puntero. Dado que estamos realizando un análisis insensible al flujo, solamente tenemos que afirmar que una variable v puede apuntar a un objeto h del heap dado. Ası́ pues, no tenemos que preocuparnos del problema de en qué lugar del programa v puede apuntar a h, o en qué contextos v puede apuntar a h. Las variables serán identificadas mediante sus nombres completos —que podrı́a incluir el nombre del paquete, clase, método y bloque en el que está definida—, permitiendo ası́ distinguir entre variables con el mismo identificador. Los objetos del heap no tienen nombre. Una convención para referirse a estos objetos es mediante la instrucción en la que fueron creados. Como una instrucción puede ejecutarse varias veces creando múltiples objetos, una afirmación como “v puede apuntar a h” en realidad quiere decir “v puede apuntar a uno o más de los múltiples objetos creados en la instrucción etiquetada con h”. El objetivo del análisis es determinar a qué puede apuntar cada variable y cada campo de cada objeto del heap. Nos referimos a esto como “análisis de punteros” (en inglés points-to analysis); dos punteros están aliados si la intersección de sus conjuntos points-to 5 es no vacı́a. El análisis que se describirá será inclusion-based (basado en la inclusión); esto es, una instrucción como v = w causa que una variable v pueda apuntar a todos los objetos a los que puede apuntar w pero no al revés. Aunque esta aproximación parezca evidente, existen otras alternativas como el análisis equivalence-based (basado en la equivalencia) en el que la 5 Por comodidad, usaremos la denominación en inglés, conjunto points-to, para referirnos al conjunto de objetos del heap al que puede apuntar un puntero. 14 CAPÍTULO 1. PRELIMINARES instrucción v = w convertirı́a a v y w en una clase de equivalencia, permitiendo ası́ que cada una de ellas apunte a todos los objetos a los que puedan apuntar las dos. A pesar de que esta última formulación no aproxima bien las alianzas de punteros, provee una rápida, y a menudo buena, respuesta a la pregunta de qué variables apuntan al mismo tipo de objetos. Insensibilidad al flujo Un análisis sensible al flujo se guı́a por el flujo de control del programa añadiendo la información de cada instrucción requerida para el análisis pero, a la vez, teniendo en cuenta los efectos que la nueva información tiene sobre la que se tenı́a antes. Un análisis insensible al flujo ignora el flujo de control, lo que, en esencia, asume que toda instrucción del programa podrı́a ser ejecutada en cualquier orden. Computa un mapa points-to indicando a qué puede apuntar cada variable en cualquier punto de ejecución del programa. Si una variable puede apuntar a dos objetos diferentes en un programa, registramos que puede apuntar a ambos objetos. En otras palabras, en un análisis insensible al flujo, una asignación no “mata” ninguna relación points-to, sino que “genera” más relaciones points-to. Para computar los resultados insensibles al flujo, se añaden repetidamente los efectos points-to de cada instrucción sobre la relación points-to hasta que no se encuentren nuevas relaciones. Claramente, la falta de sensibilidad al flujo hace que los resultados del análisis sean más pobres pero tiende a reducir el tamaño de la representación de los resultados y hace que el algoritmo converja más rápidamente. La formulación en Datalog Ahora se propondrá la formalización en Datalog del análisis de punteros insensible al flujo discutido anteriormente. De momento se ignorarán las llamadas a procedimiento y nos concentraremos en los cuatro tipos de instrucción que pueden afectar a los punteros: 1. Creación de un objeto “h : T v = new T ();”: Esta instrucción crea un nuevo objeto del heap, y la variable v puede apuntar a él. 2. Instrucción de copia “v = w;”: Aquı́, v y w son variables. La instrucción hace a v apuntar a todo objeto del heap al que w pueda apuntar en ese momento. 3. Almacenamiento en un campo “ v.f = w;”: El tipo de objeto al que apunta v debe tener un campo f , y este campo debe ser de algún tipo referencia. Si v es un puntero a un objeto del heap h, y w apunta a g, esta instrucción hace que el campo f de h ahora apunte a g. Nótese que la variable v se deja inalterada. 1.2. DATALOG: UNA REPRESENTACIÓN LÓGICA DEL FLUJO DE DATOS 15 4. Cargar de un campo “v = w.f ;”: Aquı́, w es una variable apuntando a algún objeto del heap que tiene un campo f , y f apunta a algún objeto del heap h. La instrucción hace que la variable v apunte a h. Nótese que accesos compuestos a campos en el código fuente, como v = w.f.g, se pueden desglosar en varios accesos más primitivos como: v1 = w.f ; v = v1.g; Sólo queda expresar formalmente el análisis en reglas de Datalog. Primero, definiremos dos tipos de predicados IDB: 1. pts(V, H) quiere decir que la variable V puede apuntar al objeto del heap H. 2. hpts(H, F, G) quiere decir que el campo F del objeto del heap H puede apuntar al objeto del heap G. Las relaciones EDB se construyen a partir del programa mismo. Dado que la localización de las instrucciones en un programa es irrelevante cuando el análisis es insensible al flujo, sólo debe incluirse en la base de datos EDB la existencia de instrucciones que tengan ciertas formas. En lo que sigue se hará una simplificación. En lugar de definir las relaciones EDB para que guarden la información extraı́da del programa, usaremos una instrucción entre comillas para sugerir la relación o relaciones EDB que representen la existencia de tal instrucción. Por ejemplo, “H : T V = new T ” es un hecho EDB que afirma que en la instrucción en la posición H hay una asignación que hace que la variable V apunte a un nuevo objeto de tipo T . Asumiremos que, en la práctica, habrá una relación EDB correspondiente que contendrá átomos básicos, uno para cada instrucción de esta forma en el programa. Con esta convención todo lo que necesitamos para escribir el programa Datalog es una regla para cada uno de los cuatro tipos de instrucción: En la Figura 1.1 la regla (1) dice que la variable V puede apuntar al objeto del heap H si la instrucción H es una asignación del nuevo objeto a V . La regla (2) dice que si hay una instrucción de copia V = W , y W puede apuntar a H, entonces V puede apuntar a H. La regla (3) dice que si hay una instrucción de la forma V.F = W , W puede apuntar a G, y V puede apuntar a H, entonces el campo F de H puede apuntar a G. Finalmente, la regla (4) dice que si hay una instrucción de la forma V = W.F , W puede apuntar a G, y el campo F de G puede apuntar a H, entonces V puede apuntar a H. Nótese que pts y hpts son mutuamente recursivos, pero este programa Datalog puede ser evaluado por cualquiera de los algoritmos iterativos expuestos previamente ya que cumple las condiciones. 16 CAPÍTULO 1. PRELIMINARES 1) pts(V, H) : − “H : T V = new T ” 2) pts(V, H) : − “V = W ” & pts(W, H) 3) hpts(H, F, G) : − “V.F = W ” & pts(W, G) & pts(V, H) 4) pts(V, H) : − “V = W.F ” & pts(W, G) & hpts(G, F, H) Figura 1.1: Programa Datalog para el análisis de punteros insensible al flujo. Usando la información sobre tipos Como Java tiene un sistema de tipos seguro, las variables pueden apuntar solamente a tipos que sean compatibles con los tipos declarados. Una asignación que no sea segura generará una excepción en tiempo de ejecución. Por ello introduciremos en nuestro análisis predicados EDB que reflejen la importante información de tipos del código a analizar: 1. vT ype(V, T ) dice que la variable V se ha declarado como de tipo T . 2. hT ype(H, T ) dice que el objeto de heap H se creó con el tipo T . El tipo de un objeto creado puede no ser conocido de forma precisa si, por ejemplo, el objeto es devuelto por un método nativo. Esos tipos son modelados conservativamente como “todos los posibles tipos”. 3. assignable(T, S) dice que un objeto del tipo S puede ser asignado a una variable del tipo T . Esta información generalmente se extrae de la declaración de subtipos en el programa, pero también incorpora información sobre las clases predefinidas del lenguaje. El predicado assignable(T, T ) siempre es verdadero. Podemos modificar las reglas propuestas anteriormente en la Figura 1.1 para permitir inferencias sólo si la variable asignada recibe un objeto del heap de un tipo asignable. Las reglas se muestran en la Figura 1.2. La primera modificación es para la regla (2). Los últimos tres subobjetivos dicen que podemos concluir que V puede apuntar a H solamente si: V tiene tipo T , H tiene tipo S y si los objetos de tipo S pueden ser asignados a las variables de tipo T . La regla (4) ha sido modificada de forma similar. Nótese que no hay restricción adicional en la regla (3) porque 1.2. DATALOG: UNA REPRESENTACIÓN LÓGICA DEL FLUJO DE DATOS 1) pts(V, H) : − “H : T V = new T ” 2) pts(V, H) : − “V = W ” & pts(W, H) & vT ype(V, T ) & hT ype(H, S) & assignable(T, S) 17 3) hpts(H, F, G) : − “V.F = W ” & pts(W, G) & pts(V, H) 4) pts(V, H) : − “V = W.F ” & pts(W, G) & hpts(G, F, H) vT ype(V, T ) & hT ype(H, S) & assignable(T, S) Figura 1.2: Restricciones de tipo añadidas al análisis de punteros insensible al flujo. todas los almacenamientos deben ir a través de variables. Cualquier restricción de tipo sólo captarı́a un caso extra, cuando el objeto base fuera la constante “null”. 1.2.3. Análisis interprocedimental insensible al contexto Ahora consideraremos las invocaciones a método. Primero explicaremos cómo se puede usar el análisis de punteros para computar un grafo de llamadas preciso que sea útil a la hora de obtener resultados precisos del análisis de punteros. Después se formalizará el cálculo del grafo de llamadas on-the-fly y se mostrará cómo se puede usar Datalog para describir sucintamente los análisis. Efectos de una invocación a método Los efectos de una llamada a método como x = y.n(z) en Java sobre las relaciones points-to pueden ser computados como sigue: 1. Se determina el tipo del objeto receptor, que es el objeto al que apunta y. Supongamos que su tipo es t. Sea m el método llamado n en la superclase de t más cercana que tenga un método llamado n. Nótese que, en general, el método que se invocará sólo podrá ser determinado dinámicamente. 18 CAPÍTULO 1. PRELIMINARES 2. Los parámetros formales de m reciben la asignación de los objetos apuntados por los parámetros actuales. Los parámetros actuales incluyen no sólo los parámetros pasados directamente sino también el objeto receptor mismo. Toda invocación a método asigna a la variable (implı́cita) this el objeto receptor. Nos referiremos a las variables this como al parámetro formal número 0 de un método. En x = y.n(z) hay dos parámetros formales: el objeto apuntado por y es asignado a la variable this y el objeto apuntado por z es asignado al primer parámetro formal declarado para m. 3. Se asigna el objeto devuelto por m a la variable del lado izquierdo de la instrucción de asignación. En el análisis insensible al contexto los parámetros y los valores devueltos se modelan mediante instrucciones de copia. La pregunta interesante que queda por contestar es cómo determinar el tipo del objeto receptor. Podemos, de forma conservadora, determinar el tipo de acuerdo con la declaración de la variable; por ejemplo, si la variable declarada tiene el tipo t, entonces sólo métodos llamados n en subtipos de t pueden ser invocados. Desafortunadamente, si la variable declarada tiene el tipo Object, entonces todos los métodos con nombre n son destinos potenciales. En programas cotidianos que usan intensivamente jerarquı́as de objetos e incluyen librerı́as enormes, una aproximación tal puede resultar en muchos destinos espurios, haciendo el análisis lento e impreciso. Necesitamos conocer a qué pueden apuntar las variables para calcular los destinos de las llamadas; pero a menos que conozcamos los destinos de las llamadas, no podremos descubrir a qué pueden apuntar todas las variables. Esta relación recursiva requiere que descubramos los destinos on-the-fly mientras computamos las relaciones points-to. El análisis continúa hasta que no hay nuevos destinos de llamada ni nuevas relaciones de tipo points-to. Descubrimiento del grafo de llamadas en Datalog Para formular las reglas Datalog para el análisis interprocedimental insensible al contexto, introducimos tres predicados EDB, cada uno de los cuales se obtiene fácilmente a partir del código fuente: 1. actual(S, I, V ) dice que V es el I-ésimo parámetro actual usado en el punto de llamada S. 2. f ormal(M, I, V ) dice que V es el I-ésimo parámetro formal declarado en el método M . 3. cha(T, N, M ) dice que M es el método llamado cuando N es invocado sobre un objeto receptor del tipo T —cha viene del inglés “class hierarchy analysis”, análisis de la jerarquı́a de clases. 1.2. DATALOG: UNA REPRESENTACIÓN LÓGICA DEL FLUJO DE DATOS 19 1) invokes(S, M ) : − ‘‘S : V.N (. . .)’’ & pts(V, H) & hT ype(H, T ) & cha(T, N, M ) 2) pts(V, H) : − invokes(S, M ) & f ormal(M, I, V ) & actual(S, I, W ) & pts(W, H) 3) pts(V, H) : − ‘‘S : V = W.N (. . .)’’ & invokes(S, M ) & returns(M, X) & pts(X, H) Figura 1.3: Programa Datalog para el descubrimiento del grafo de llamadas Cada arco del grafo de llamadas está representado por un predicado IDB invokes. Mientras descubrimos más arcos del grafo de llamadas, se crean más relaciones points-to al introducir parámetros en el procedimiento y extraer valores de retorno. Este efecto se resume en las reglas mostradas en la Figura 1.3. La primera regla computa el destino de una invocación del punto de llamada. Esto es, “S : V.N (. . .)” dice que hay un punto de llamada etiquetado como S que invoca a un método llamado N sobre el objeto receptor apuntado por V . Los subobjetivos dicen que si V puede apuntar al objeto del heap H, que se creó con el tipo T , y M es el método usado cuando N es invocado sobre objetos del tipo T , entonces el punto de llamada S puede invocar al método M. La segunda regla dice que si el punto de llamada S puede llamar al método M , entonces cada parámetro formal de M puede apuntar donde pueda apuntar su correspondiente parámetro actual de la llamada. Si combinamos estas reglas con las explicadas en la Sección 1.2.2 tenemos un análisis de punteros insensible al contexto que usa un grafo de llamadas que se calcula on-the-fly. Este análisis tiene el efecto lateral de crear un grafo de llamadas usando un análisis de punteros insensible al flujo y al contexto. No obstante, este grafo de llamadas es significativamente más preciso que el que se podrı́a computar basándose sólo en declaraciones de tipo y análisis sintáctico. 20 CAPÍTULO 1. PRELIMINARES Carga dinámica y reflexión Lenguajes como Java permiten la carga dinámica de clases. En este caso es imposible analizar todo el código que puede ejecutar un programa, y, por lo tanto, es imposible proporcionar estáticamente aproximaciones conservadoras de los grafo de llamadass o de los análisis de punteros. El análisis estático sólo puede proporcionar una aproximación basándose en el código analizado. Incluso si asumimos que se pretende analizar todo el código a ejecutar, hay una complicación adicional que imposibilita un análisis conservador: la reflexión. La reflexión permite a un programa determinar dinámicamente los tipos de los objetos a crear, los nombres de los métodos a invocar, y los nombres de los campos a los que se desea acceder. Los nombres de tipo, método y campo pueden calcularse o bien ser datos de entrada de usuario, ası́ que, por lo general, la única aproximación es la de asumir el universo. A pesar de que un gran número de enormes programas en Java usan reflexión, éstos suelen usar ciertas convenciones. En particular, mientras que la aplicación no redefina el cargador de clases, se puede conocer la clase del objeto si se conoce su nombre. Un análisis de punteros podrı́a permitirnos encontrar dicho nombre si está determinado estáticamente o, al menos, podrı́a permitirirnos conocer en qué lugar del programa se define el mismo si es que se construye con datos del usuario. De forma similar, se pueden usar otras construcciones de los lenguajes como, por ejemplo, los castings para aproximar el tipo de los objetos creados dinámicamente, siempre y cuando no se redefina en el programa el comportamiento de dichas construcciones. 1.2.4. Análisis de punteros sensible al contexto Al principio de la Sección 1.1.2, se expuso cómo la sensibilidad al contexto puede aumentar considerablemente la precisión de los análisis interprocedimentales. Se habló de dos aproximaciones al análisis interprocedimental, una basada en la clonación y otra basada en los resúmenes. El cálculo de resúmenes de información points-to es difı́cil. Primero, los resúmenes son grandes: el resumen de cada método debe incluir el efecto de todas las actualizaciones en función de los parámetros de entrada que la función, incluyendo los métodos llamados por ésta, puede realizar. Esto es, un método puede cambiar la información points-to de todos los datos alcanzables mediante variables estáticas, parámetros de entrada y todos los objetos creados por el mismo método y sus métodos invocados. Pese a las muchas propuestas que ha habido para realizar esta aproximación, ninguna ha podido escalar para analizar programas grandes. 1.2. DATALOG: UNA REPRESENTACIÓN LÓGICA DEL FLUJO DE DATOS 21 Por lo tanto, se presentará una análisis sensible al contexto basado en clonación. Este tipo de análisis clona los métodos para cada uno de los contextos de interés. Después, aplica un análisis insensible al contexto sobre el grafo clonado. Aunque esta aproximación pueda parecer sencilla, tiene la dificultad de la gestión de un número enorme de clones. En un aplicación Java tı́pica es común encontrar 1014 contextos, cuya representación supone un reto. La discusión del presente análisis se realizará en dos partes: 1. ¿Cómo manejar lógicamente la sensibilidad al contexto? 2. ¿Cómo representar el número exponencial de contextos? Como se mostrará, esta aproximación es un ejemplo de la importancia de la abstracción. Primero se eliminará la complejidad algorı́tmica debida a la gestión de información de la especificación del análisis, que quedará reflejado en sólo algunas lı́neas en Datalog. Luego en esta sección, se revisará una representación genérica de un programa Datalog en diagramas de decisión binarios que permita su cómputo eficiente. Nuestra propuesta de representación de un programa Datalog será expuesta en el Capı́tulo 2. Los beneficios de seguir esta aproximación son numerosos: 1. Se podrá reutilizar todo el conocimiento aplicado a estructuras de datos altamente eficientes a cualquier análisis especificado en Datalog. 2. La implementación del análisis será automática (una traducción de la especificación) y, por lo tanto, es más probable su corrección. 3. Se podrán reutilizar los resultados de un análisis para mejorar otros. 4. Se podrá manipular las especificaciones de los análisis más fácilmente, permitiendo crear nuevos análisis más complejos. Contextos y cadenas de llamadas El análisis de punteros sensible al contexto que se describirá asumirá que se posee un grafo de llamadas ya computado. Este paso es necesario para poder representar de una forma compacta la enorme cantidad de contextos que surgirán. Para obtener el grafo de llamadas, primero se ejecutará un análisis de punteros insensible al contexto que compute el grafo de llamadas on-the-fly, como se mostró en la Sección 1.2.3. A continuación, se mostrará como crear un grafo de llamadas clonado. Un contexto es la representación de la cadena de llamadas que forma la historia de las llamadas a función activas. Otra forma de ver un contexto es como un resumen de la secuencia 22 CAPÍTULO 1. PRELIMINARES de llamadas cuyos registros de activación están, en ese momento, en la pila de ejecución. Si no hay funciones recursivas en la pila, entonces la cadena de llamadas —la secuencia de posiciones desde las que las llamadas activas se hicieron— es una representación completa. También es una representación aceptable en el sentido de que sólo hay una número finito de contextos diferentes, aunque esa cifra pueda ser exponencial con el número de funciones en el programa. Sin embargo, si hay funciones recursivas en el programa, entonces el número de cadenas de llamadas posibles es infinito, y no se pueden considerar todas las cadenas de llamadas como contextos diferentes. Hay diversas formas de limitar el número de contextos distintos. Se optará por usar un esquema sencillo, en el que se capturará la historia de las funciones no recursivas, mientras que las recursivas se considerarán demasiado complicadas para desenmarañar. Primero se buscan todos los conjuntos de funciones mutuamente recursivas en el programa. Para ello, se calcula el conjunto de componentes fuertemente conexas (CFCs) del grafo obtenido a partir del programa en el que: 1. Los nodos son las funciones. 2. Existe un arco del nodo p al q si la función p llama a la función q. Cada componente fuertemente conexa es un conjunto de funciones mutuamente recursivas. Se considerará CFC no-trivial, a aquella que tenga más de un miembro (mutuamente recursivos), o si tiene un solo miembro recursivo. Un CFC con un único miembro no recursivo será considerado un CFC trivial. La limitación que se impondrá al número de contextos distintos posibles seguirá la siguiente regla: dada una cadena de llamadas, se eliminará la ocurrencia del punto de llamada s si 1. s está en una función p. 2. La función q es llamada en el punto de llamada s (q = p es posible). 3. p y q están en la misma componente fuertemente conexa, esto es, si p y q son mutuamente recursivos, o si p = q y p es recursivo). El resultado de aplicar esta regla es que, cuando se invoca un miembro de una CFC notrivial S, el punto de llamada para esa llamada forma parte del contexto, pero las llamadas realizadas dentro de S a otras funciones de la misma CFC no forman parte del contexto. Finalmente, cuando se realiza una llamada afuera de S, se registra su punto de llamada como parte del contexto. 1.2. DATALOG: UNA REPRESENTACIÓN LÓGICA DEL FLUJO DE DATOS 23 1) pts(V, C, H) : − “H : T V = new T ()” & CSinvokes(H, C, , ) 2) pts(V, C, H) : − “V = W ” & pts(W, C, H) 3) hpts(H, F, G) : − “V.F = W ” & pts(W, C, G) & pts(V, C, H) 4) pts(V, C, H) : − “V = W.F ” & pts(W, C, G) & hpts(G, F, H) 5) pts(V, D, H) : − CSinvokes(S, C, M, D) & f ormal(M, I, V ) & actual(S, I, W ) & pts(W, C, H) Figura 1.4: Programa Datalog para análisis de punteros sensible al contexto. Ahora se describirá cómo derivar el grafo clonado. Cada método clonado está identificado por el método en el programa M y un contexto C. Los arcos se definen añadiendo los correspondientes contextos a cada uno de los arcos del grafo de llamadas original. En el original, existirá un arco enlazando el punto de llamada S con el método M si el predicado invokes(S, M ) es verdadero. Con vistas a añadir contextos para identificar métodos en el grafo de llamadas clonado, podemos definir un predicado CSinvokes tal que CSinvokes(S, C, M, D) sea verdadero si el punto de llamada S en el contexto C llama al contexto D del método M . Añadiendo contexto a las reglas Datalog Para realizar un análisis sensible al contexto, podemos aplicar el mismo análisis insensible al contexto al grafo de llamadas clonado. Pero, ya que un método en el grafo de llamadas clonado se representa mediante el método original y su contexto, revisaremos todas las reglas de Datalog para tener esto en cuenta. Por simplicidad, las reglas mostradas en la Figura 1.4 no incluirán restricciones de tipo, y los “ ” representarán a cualquier variable nueva. Se debe notar la necesidad de definir un argumento adicional que represente el contexto en el predicado pts. pts(V, C, H) dice que la variable V en el contexto C puede apuntar al objeto del heap H. Las reglas son auto-explicativas salvo, quizás, la número 5. La regla 5 dice que si el punto de llamada S en el contexto C llama al método M con contexto D, entonces los parámetros formales en el método M con contexto D podrı́an apuntar a los objetos apuntados 24 CAPÍTULO 1. PRELIMINARES por sus correspondientes parámetros actuales en el contexto C. Observaciones adicionales sobre la sensibilidad La formulación de sensibilidad al contexto descrita en esta sección ha resultado lo suficientemente práctica como para manejar programas Java reales (usando la implementación que se describirá en la siguiente sección). No obstante, este algoritmo aún no puede analizar las aplicaciones Java más grandes. Los objetos del heap en esta formulación se nombran a partir de su punto de llamada, pero sin sensibilidad al contexto. Esta simplificación puede causar problemas. Por ejemplo, con el patrón factorı́a de objetos, en el que los objetos del mismo tipo son creados por la misma rutina. El esquema actual harı́a que todos los objetos de esa clase compartieran el mismo nombre. Serı́a fácil resolver esa situación desplegando el código de creación en lugar de tenerlo en una rutina. En general, es deseable aumentar la sensibilidad al contexto en la nominación de los objetos. Aunque es fácil añadir sensibilidad al contexto a los objetos en la formulación en Datalog, no lo es conseguir que dicho análisis escale bien a programas grandes. Otro tipo de sensibilidad importante es la sensibilidad objetual. Una técnica sensible a los objetos puede distinguir entre métodos invocados sobre distintos objetos receptores, haciendo el análisis más preciso. En el hipotético caso en el que en un punto de llamada la variable sobre la que se invoca el método pueda apuntar a dos objetos distintos de la misma clase —por lo tanto los campos de dichos objetos podrı́an apuntar a objetos distintos. Si no se distingue entre un objeto u otro durante la llamada, una copia de campos sobre los mismos con la referencia implı́cita this crearı́a relaciones espurias, esto es, disminuirı́a la precisión del análisis. Para ciertos análisis la sensibilidad objetual puede ser más útil que la sensibilidad al contexto. 1.2.5. Implementación de Datalog mediante DDBs Los diagramas de decisión binarios (DDBs) son un formalismo para representar funciones n booleanas mediante grafos. Ya que hay 22 funciones booleanas de n variables, ningún tipo de representación va a resultar compacta para todas ellas. No obstante, las funciones booleanas que aparecen en la práctica tienden a ser muy regulares. Por lo tanto, es común encontrar una representación compacta con DDBs para funciones de interés. Es un hecho que las funciones booleanes descritas por los programas Datalog que especifican análisis no son una excepción. La aproximación mediante DDBs obtiene representaciones compactas de la información de análisis y supera a los sistemas de gestión de bases de datos 1.2. DATALOG: UNA REPRESENTACIÓN LÓGICA DEL FLUJO DE DATOS 25 convencionales porque, estos últimos, están diseñados para patrones de datos más irregulares, tı́picos de la información comercial. A continuación se introducirá la notación DDB. Luego, se sugerirá cómo representar datos relacionales como DDBs, y cómo manipular estos DDBs para reflejar las operaciones relacionales realizadas en la ejecución de un programa Datalog. Finalmente, se describirá el modo de representar un número exponencial de contextos con DDBs, que es el factor clave para el éxito del uso de DDBs en el análisis sensible al contexto. Diagramas de decisión binarios Un DDB representa una función booleana mediante un grafo dirigido acı́clico (GDA) con raı́z. Cada uno de los nodos del interior del GDA está etiquetado con una de las variables de la función representada. En el extremo del grafo hay dos hojas, una etiquetada con un 0 y la otra con un 1. Cada nodo interior tiene dos arcos a sus hijos; estos arcos se llaman low y high. El arco low está asociado con el caso en el que la variable del nodo origen de dicho arco tiene valor 0, y el arco high está asociado con el caso en el que la variable anterior tiene valor 1. Dada una asignación de valores de verdad a las variables, se puede comenzar en la raı́z y en cada nodo etiquetado con una x, seguir el arco low o high, dependiendo de si el valor de verdad para x es 0 o 1, respectivamente. Si se llega a una hoja etiquetada 1, entonces la función representada es verdadera para esta asignación de valores de verdad; en otro caso, la función es falsa. Pese a no ser un requisisto necesario, es conveniente restringir el uso de los DDBs a DDBs ordenados ya que es más fácil operar sobre los mismos. En un DDB ordenado, hay un orden x1 , x2 , . . . , xn de las variables, y cada vez que haya un arco de un nodo padre etiquetado xi a un nodo hijo etiquetado xj , se debe cumplir que i < j. En lo sucesivo, se asumirá que todos los DDBs están ordenados. Transformaciones sobre DDBs Hay dos simplificaciones sobre los DDBs que ayudan a compactarlos: 1. Cortocircuito: Si un nodo N tiene arcos high y low que van al mismo nodo M , entonces podemos eliminar N . Los arcos que antes incidı́an sobre N pasan a incidir sobre M . 2. Fusión de nodos: Si dos nodos N y M tienen arcos low que van al mismo nodo, y también tienen arcos high que van al mismo nodo, entonces podemos fusionar N con 26 CAPÍTULO 1. PRELIMINARES M . Los arcos que incidı́an sobre N o M pasan a incidir sobre el nodo resultado de la fusión. Representando relaciones mediante DDBs Las relaciones con las que tratamos tienen componentes extraı́dos de “dominios”. Un dominio para una componente de una relación es el conjunto de los posibles valores que pueden tener las tuplas en ese componente. Por ejemplo, la relación pts(V, H) tiene el dominio de todas las variables del programa para su primera componente y el dominio de todas las instrucciones de creación de objetos para la segunda componente. Si un dominio tiene más de 2n−1 valores posibles pero no más de 2n valores, entonces requiere n bits o variables booleanas para representar valores en ese dominio. Luego una tupla en una relación puede ser vista como una asignación de valores de verdad a las variables que representan los valores de los dominios para cada una de las componentes de la tupla. Una relación, entonces, podrı́a ser vista como una función booleana que devuelva verdadero para todas las asignaciones de valores de verdad que representan tuplas en la relación. Operaciones relacionales como operaciones sobre DDBs Sabiendo cómo representar relaciones con DDBs, falta conocer el modo de manipularlos para reflejar las operaciones relacionales que se llevan a cabo para implementar, por ejemplo, el algoritmo de evaluación incremental de programas Datalog visto anteriormente (Algoritmo 3). Las principales operaciones sobre relaciones que necesitan realizarse son: 1. Inicialización: Se necesita crear un DDB que represente solamente una tupla de una relación. Luego, podremos juntarlos con otros DDB para representar relaciones más grandes realizando la operación de unión. 2. Unión: Para realizar la unión de relaciones, aplicamos la operación lógica OR de las funciones booleanas que representan las relaciones. Esta operación se usará para construir las relaciones iniciales, para combinar los resultados de distintas reglas con el mismo sı́mbolo de predicado en la cabeza, y para agregar hechos nuevos al conjunto de hechos viejos, como en el Algoritmo 3. 3. Proyección: Al evaluar el cuerpo de una regla, se necesita construir la relación de la cabeza como consecuencia lógica de las tuplas ciertas del cuerpo. En términos de un DDB que representa una relación, se deben eliminar los nodos que están etiquetados 1.2. DATALOG: UNA REPRESENTACIÓN LÓGICA DEL FLUJO DE DATOS 27 con aquellas variables booleanas que no representan componentes de la cabeza. Podrı́a ser necesario, también, renombrar las variables en el DDB para hacerlas corresponder con las variables booleanas de los componentes de la relación de la cabeza. 4. Join 6 (o concatenación natural ): Para encontrar asignaciones de valores a variables que hagan que el cuerpo de una regla sea verdadero, se necesita realizar la concatenación natural de las relaciones correspondientes a cada uno de los subobjetivos. Por ejemplo, si tuviéramos dos subobjetivos r(A, B) & s(B, C). La concatenación natural de las relaciones para estos subobjetivos es el conjunto (a, b, c) de tripletas tales que (a, b) es una tupla en la relación para r, y (b, c) es una tupla en la relación para s. En términos de DDBs, esta operación se reduce a renombrar las variables booleanas en los DDBs tal que los componentes para las dos B concuerden en nombres de variable y aplicarles la operación lógica AND. DDBs para tuplas singulares. Para inicializar una relación, se necesita una forma de construir DDBs para funciones booleanas que sean verdaderas únicamente para una sola asignación de valores de verdad. Sean las variables booleanas x1 , x2 , . . . , xn y la asignación de valores de verdad a1 , a2 , · · · , an donde cada ai es o bien 0, o bien 1. El DDB tendrá un nodo Ni para cada xi . Si ai = 0, entonces el arco high con origen en Ni conducirá a la hoja 0, y el arco low conducirá al nodo Ni+1 si i < n, o a la hoja 1 si i = n. Por el contrario, si ai = 1, entonces el arco low de Ni conducirá a la hoja 0, y el arco high conducirá a al nodo Ni+1 si i < n, o a la hoja 1 si i = n. Esta estrategia proporciona un DDB que permite comprobar si cada xi tiene el valor correcto, para i = 1, 2, . . . , n. Tan pronto se encuentre un valor incorrecto, se salta directamente a la hoja 0. Sólo se alcanza la hoja 1 si todas las variables tienen su valor correcto. El uso de DDBs para análisis de punteros Hacer que un análisis de punteros insensible al contexto funcione ya es una tarea complicada. La ordenación de las variables del DDB puede variar enormemente el tamaño de la representación. Se necesitan muchas consideraciones, incluida la prueba y error, para llegar a una ordenación que permita que el análisis acabe rápido. El análisis de punteros sensible al contexto es aun más difı́cil por el número exponencial de contextos en un programa. En particular, si se asignan arbitrariamente números para 6 Join es el término en inglés para la palabra en castellano “juntar”, pero, en el dominio de las relaciones, es una operación claramente distinta a la unión. Hay distintos tipos de “join” sobre relaciones. El aquı́ presentado tiene una denominación en castellano de uso común: “concatenación natural ”. 28 CAPÍTULO 1. PRELIMINARES representar los contextos en un grafo de llamadas, no se podrı́an analizar ni siquiera programas Java pequeños. Es importante que los contextos se numeren tal que la codificación binaria del análisis de punteros pueda ser muy compacta. Dos contextos del mismo método con cadenas de llamadas parecidas comparten mucha información, por lo que es deseable numerar los n contextos de un método consecutivamente. De forma similar, ya que los pares invocanteinvocado de un punto de llamada comparten mucha información, serı́a deseable numerar los contextos tal que la diferencia numérica entre cada pareja invocante-invocado fuera siempre una constante. Incluso con un esquema de numeración inteligente para los contextos de llamada, es difı́cil analizar programas Java grandes de manera eficiente. El aprendizaje automático activo ha demostrado ser útil para encontrar una ordenación de variables lo suficientemente eficiente para manejar grandes aplicaciones. 1.3. Pbes: un formalismo para analizar programas Dados un conjunto de variables booleanas X y un conjunto de términos constructores (o términos dato) D, un Sistema de Ecucaciones Booleana Parametrizadas [Mat98] (o Pbes del inglés Parameterised Boolean Equation System) B = (x0 , M1 , ..., Mn ) es un conjunto de n bloques Mi , cada uno de los cuales contiene pi ecuaciones de punto fijo de la forma σi ~ i,j ) = φi,j xi,j (d~i,j : D con j ∈ [1..pi ] y σi ∈ {µ, ν}, que es llamado el signo de la ecuación i, pudiendo ser el menor (µ) o mayor (ν) operador de punto fijo. Todo xi,j es una variable booleana de X que enlaza cero o más términos constructores di,j del tipo Di,j que pueden aparecer en la fórmula booleana φi,j (de un conjunto Φ de fórmulas booleanas). En lo sucesivo, y con vistas a simplificar la descripción, consideraremos que sólo puede haber como máximo un parámetro constructor d : D enlazado por una variable booleana. La variable x0 ∈ X , definida en el bloque M1 , es una variable booleana cuyo valor es de interés en el contexto de la metodologı́a de resolución local. Las fórmulas booleanas φi,j se definen formalmente como sigue. Definición 1.3.1 Fórmula Booleana Una boolean formula φ, definida sobre un alfabeto de variables booleanas (parametrizadas) X ⊆ X y términos constructores D ⊆ D, tiene la siguiente sintaxis: φ, φ1 , φ2 ::= true | false | φ1 ∧ φ2 | φ1 ∨ φ2 | X(e) | ∀d ∈ D. φ | ∃d ∈ D. φ 1.3. PBES: UN FORMALISMO PARA ANALIZAR PROGRAMAS 29 en la que las constantes y operadores booleanos tienen su definición usual, e es un término constructor (una constante o variable del tipo D), X(e) denota la llamada a la variable booleana X con el parámetro e, y d es un término del tipo D. Un entorno booleano δ ∈ ∆ es una función parcial que asigna un predicado δ(x) : X → (D → B), con B = {true, false}, a cada variable booleana (parametrizada) x(d : D). Las constantes booleanas true y false abrevian la conjunción y disyunción vacı́as (∧∅ y ∨∅) respectivamente. Un entorno constructor ε ∈ E es una función parcial que asigna un valor ε(e) : D → D, que forma lo que comúnmente se llama soporte de ε y es escrito supp(ε), a cada término constructor e del tipo D. Nótese que ε(e) = e cuando e es un término constructor constante. La actualización de ε1 por ε2 se define como (ε1 ε2 )(x) = if x ∈ supp(ε2 ) then ε2 (x) else ε1 (x). La función de interpretación [[φ]]δε, en la que [[.]] : Φ → ∆ → E → B, proporciona el valor de verdad de una fórmula booleana φ en el contexto de δ y ε, donde todas las variables booleanas libres x se evalúan mediante δ(x), y todos los términos constructores libres d se evalúan mediante E(d). Definición 1.3.2 (Semántica de una Fórmula Booleana) Sean δ : X → (D → B) y ε : D → D un entorno booleano y un entorno de datos respectivamente. La semántica de una fórmula booleana φ se define inductivamente mediante la siguiente función de interpretación: [[true]]δε [[false]]δε [[φ1 ∧ φ2 ]]δε [[φ1 ∨ φ2 ]]δε [[x(e)]]δε [[∀d ∈ D. φ]]δε [[∃d ∈ D. φ]]δε = = = = = = = true false [[φ1 ]]δε ∧ [[φ2 ]]δε [[φ1 ]]δε ∨ [[φ2 ]]δε (δ(x))(ε(e)) ∀ v ∈ D, [[φ]]δ(ε [v/d]) ∃ v ∈ D, [[φ]]δ(ε [v/d]) Definición 1.3.3 (Semántica de un Bloque de Ecuaciones) Dados un Pbes B = (x0 , M1 , ..., Mn ) y un entorno booleano δ, la solución [[Mi ]]δ a un bloque Mi = {xi,j (di,j : σ Di,j ) =i φi,j }j∈[1,pi ] (i ∈ [1..n]) se define como sigue: σ [[{xi,j (di,j : Di,j ) =i φi,j }j∈[1,pi ] ]]δ = σi Ψiδ donde Ψiδ : (Di,1 → B) × . . . × (Di,pi → B) → (Di,1 → B) × . . . × (Di,pi → B) es una función vectorial definida como Ψiδ (g1 , ..., gpi ) = (λvi,j : Di,j .[[φi,j ]](δ [g1 /xi,1 , ..., gpi /xi,pi ])[vi,j /di,j ])j∈[1,pi ] en la que gi : Di → B, i ∈ [1..pi ]. 30 CAPÍTULO 1. PRELIMINARES Un Pbes está libre de alternancia si no hay recursión mutua entre variables booleanas definidas mediante ecuaciones booleanas de punto fijo menor (σi = µ) y mayor (σi = ν). En este caso, los bloques de ecuaciones pueden ser ordenados topológicamente tal que la resolución de un bloque Mi sólo depende de variables definidas en un bloque Mk con i < k. Un bloque es cerrado cuando la resolución de todas sus fórmulas booleanas φi,j sólo depende de variables booleanas xi,k de Mi . Definición 1.3.4 (Semántica de un PBES libre de alternancia) Dados un Pbes libre de alternancia B = (x0 , M1 , ..., Mn ) y un entorno booleano δ, la semántica [[B]]δ para B es el valor de su variable principal x0 dado por la semántica de M1 , esto es, δ1 (x0 ), donde los contextos δi se calculan como sigue: δn δi = = [[Mn ]][] (el contexto está vacı́o porque Mn es cerrado) ([[Mi ]]δi+1 ) δi+1 para i ∈ [1, n − 1] en el que cada bloque Mi se interpreta en el contexto de todos los bloques Mk con i < k. Capı́tulo 2 De Datalog a Bes Gracias al uso de variables booleanas tipadas, los Pbes sirven como mecanismo para lograr una representación intermedia elegante y directa de una consulta Datalog. En este capı́tulo, se presenta una reformulación del problema de la evaluación de consultas Datalog en términos de la resolución de Pbes y viceversa. Las transformaciones se realizan en tiempo lineal con una representación adecuada del problema. Una vez obtenido el Pbes a partir del programa Datalog, se transforma on-the-fly a un Bes sin parámetros, optimizando ası́ su resolución. Igual que en [WACL05], se asume que los programas Datalog tienen negación estratificada (no hay recursión a través de la negación) y dominios finitos totalmente ordenados, pero carecen de operadores de comparación. 2.1. Representación de una consulta Datalog Empezaremos por formalizar la definición informal de Datalog que se dio en la Sección 1.2. Una regla Datalog es una cláusula de Horn libre de funciones sobre un alfabeto de sı́mbolos de predicado (por ejemplo nombres de relaciones o predicados aritméticos, como <) cuyos argumentos son variables o sı́mbolos constantes. Un programa Datalog R es un conjunto finito de reglas Datalog. Definición 2.1.1 (Sintaxis de las Reglas) Sean P un conjunto de sı́mbolos de predicado, V un conjunto finito sı́mbolos variables, y C un conjunto de sı́mbolos constantes. Una regla Datalog r, también llamada cláusula, definida sobre un alfabeto finito P ⊆ P y argumentos de V ∪ C,V ⊆ V, C ⊆ C, tiene la siguiente sintaxis: p0 (a0,1 , . . . , a0,n0 ) : − p1 (a1,1 , . . . , a1,n1 ), . . . , pm (am,1 , . . . , am,nm ). donde cada pi es un sı́mbolo de predicado de aridad ni con argumentos ai,j ∈ V ∪ C (j ∈ [1..ni ]). 32 CAPÍTULO 2. DE DATALOG A BES El átomo p0 (a1,0 , . . . , an0 ,0 ) en el lado izquierdo de la cláusula es la cabeza de la regla, donde p0 no es ni un predicado aritmético ni está negado. La conjunción finita de subobjetivos en el lado derecho de la fórmula es el cuerpo de la regla, i.e., átomos que pueden ser opcionalmente aritméticos o negados, y contiene todas las variables que aparecen en la cabeza. Siguiendo la terminologı́a de la programación lógica, llamamos hecho a una regla con un cuerpo vacı́o, mientras que un objetivo será una regla con la cabeza vacı́a. Para hacer la explicación más sencilla, restringiremos la sintaxis a sı́mbolos de predicado de aridad 1. Un objeto sintáctico (argumento, átomo o regla) que no contiene variables es llamado básico. El Universo de Herbrand de un programa Datalog R definido sobre P , V y C, denotado por UR , es el conjunto finito de todos los argumentos básicos, i.e., constantes de C. La Base de Herbrand de R, denotada por BR , es el conjunto finito de todos los átomos básicos que pueden ser construidos asignado elementos de UR a los sı́mbolos de predicado de P . Una Interpretación de Herbrand de R, denotada I (de un conjunto I de interpretaciones de Herbrand, I ⊆ BR ), es un conjunto de átomos básicos. Definición 2.1.2 (Semántica de punto fijo) Sea R un programa Datalog. El menor modelo de Herbrand de R es una interpretación de Herbrand I de R definida como el menor punto fijo de un operador monótono y continuo TR : I → I conocido como el operador de consecuencias inmediatas y definido por: TR (I) = {h ∈ BR | h : −b1 , ..., bm es una instancia básica de una regla en R, con bi ∈ I, i = 1..m, m ≥ 0} Nótese que TR computa todos los átomos básicos derivados a partir de las reglas aplicables, llamados base de datos intensional (o idb), e instancias básicas de todas las reglas con cuerpo vacı́o (m = 0), también llamadas base de datos extensional (edb). La elección del modelo mı́nimo como la semántica de un programa Datalog está justificada por la asunción de que todos los hechos que no están en la base de datos son falsos. El número de modelos de Herbrand para un programa Datalog R es finito, por lo que siempre existe un menor punto fijo para TR , denotado µTR , el cual es el menor modelo de Herbrand de R. En la práctica, uno está generalmente interesado en la computación de algunos átomos especı́ficos, llamados consultas, con argumentos determinados y no con todos los átomos de la base de datos. De ahı́ que las consultas puedan ser usadas para prevenir la computación de hechos que no son relevantes para los átomos de interés, i.e., hechos que no se derivan de la consulta. Definición 2.1.3 (Evaluación de una Consulta) Una consulta Datalog q es un par hG, Ri en el que: 33 2.1. REPRESENTACIÓN DE UNA CONSULTA DATALOG • R es un programa Datalog definido sobre P , V y C, • G es un conjunto de objetivos. Dada una consulta q, su evaluación consiste en la computación de µT{q} , siendo {q} la extensión de un programa Datalog R con las reglas Datalog de G. La evaluación deduce a partir de un programa Datalog aumentado con un conjunto de objetivos todas las combinaciones de constantes que, al ser asignadas a las variables en los objetivos, hacen a alguna de las cláusulas objetivo verdadera, i.e., todos los átomos bi en el cuerpo se satisfacen. Proponemos una transformación de una consulta Datalog como un Pbes más una variable booleana parametrizada de interés que se evaluará posteriormente usando una técnica directa. Proposición 2.1.4 Sea q = hG, Ri una consulta Datalog, definida sobre P , V y C, y Bq = (x0 , M1 ), con σ1 = µ, un sistema de ecuaciones booleanas parametrizadas definido sobre un conjunto X de variables booleanas (en correspondencia uno a uno con los sı́mbolos de predicado de P ) más una variable especial x0 , un conjunto D de términos constructores (en correspondencia uno a uno con los sı́mbolos de variables y constantes de V ∪ C), y M1 el bloque que contiene exactamente las siguientes ecuaciones: µ x0 = _ m ^ pi (di ) (2.1.1) :− p1 (d1 ), ..., pm (dm ). ∈G i:=1 µ {p(d : D) = _ m ^ pi (di ) | p ∈ P } (2.1.2) p(d) :− p1 (d1 ),... pm (dm ). ∈R i:=1 Entonces q se satisface si y sólo si x0 = true. La variable booleana x0 representa el conjunto de objetivos Datalog G, mientras que las variables booleanas (parametrizadas) p(d : D) codifican el conjunto de reglas Datalog R. La dirección inversa de reducibilidad consiste en la transformación de una variable booleana parametrizada de interés, definida en un Pbes, en la correspondiente relación de interés, expresada como una consulta Datalog, que pudiera ser evaluada usando técnicas de evaluación Datalog tradicionales. Proposición 2.1.5 Sea B = (x0 , M1 ), con σ1 = µ, un sistema de ecuaciones booleanas parametrizadas definido sobre un conjunto X de variables booleanas y un conjunto D de términos constructores, y qB = hG, Ri, una consulta Datalog definida sobre un conjunto P de sı́mbolos de predicado (en corrrespondencia uno a uno con las variables booleanas de X ), un conjunto 34 CAPÍTULO 2. DE DATALOG A BES V ∪ C de sı́mbolos de variables y constantes (en correspondencia uno a uno con los términos constructores de D), y hG, Ri que contiene exactamente las siguientes reglas Datalog: G = :− R = y1,1 (d1,1 ), . . . , .. . y1,nj (d1,nj )., µ nj ni ^ _ yi,j (di,j ) ∈ M1 x0 = i=1 j=1 : − yni ,1 (dni ,1 ), . . . , yni ,nj (dni ,nj ). x(d) : − y1,1 (d1,1 ), . . . , y1,nj (d1,nj )., nj ni ^ _ µ .. yi,j (di,j ) ∈ M1 x(d) = . i=1 j=1 x(d) : − y (d ), . . . , y (d ). ni ,1 ni ,1 ni ,nj ni ,nj Entonces x0 = true si y sólo si qB = hG, Ri se satisface. Ejemplo 2.1.6 El siguiente ejemplo sencillo ilustra el método de reducción de Datalog a Pbes. Sea q = hG, Ri la siguiente consulta Datalog: :- superior (mary, Y). supervise (mary, alice). supervise (alice, mark). superior (X, Y) :- supervise (X, Y). superior (X, Y) :- supervise (X, Z), superior (Z, Y). Mediante el uso de la Proposición 2.1.4, se obtiene el siguiente Pbes: x0 supervise(mary : D, alice : D) supervise(alice : D, mark : D) superior(X : D, Y : D) µ = µ = µ = µ = superior(mary, Y ) true true supervise(X, Y ) ∨ (supervise(X, Z) ∧ superior(Z, Y )) A continuación seguiremos desarrollando el uso de Pbess para la resolución de consultas Datalog. 2.2. Instanciación a un BES sin parámetros Entre las diferentes técnicas que se conocen para resolver un Pbes, como la eliminación Gaussiana con aproximación simbólica y el uso de patrones, sub/sobre aproximaciones, o invariantes, en este trabajo consideramos el método de resolución basado en la transformación del Pbes en un sistema de ecuaciones booleanas sin parámetros (Bes) que puede ser resuelto con algoritmos con tiempo y memoria lineales [Mat98, DPW08] cuando los dominios de los datos son finitos. 35 2.2. INSTANCIACIÓN A UN BES SIN PARÁMETROS Definición 2.2.1 (Sistema de Ecuaciones Booleanas) Un Sistema de Ecuaciones Booleanas (Bes) B = (x0 , M1 , ..., Mn ) es un Pbes en el que las variables booleanas no dependen de parámetros constructores. Por consiguiente, no hay dominios de datos, y las variables booleanas se consideran proposicionales. Para la obtención de una transformación directa a un Bes sin parámetros, en primer lugar se debe describir el Pbes en un formato más simple. Este paso de simplificación consiste en introducir nuevas variables de forma que cada fórmula al lado derecho de una ecuación booleana contenga como mucho un operador. Por lo tanto, las fórmulas booleanas se restringen a fórmulas puramente disyuntivas o conjuntivas. Dada un consulta Datalog q = hG, Ri, si se aplica la simplificación al Pbes de la proposición 2.1.4, se obtiene el siguiente Pbes: µ _ µ :− p1 (d1 ),...,pm (dm ). ∈G m ^ x0 = gp1 (d1 ),...,pm (dm ) = gp1 (d1 ),...,pm (dm ) pi (di ) i:=1 µ _ µ p(d) :− p1 (d1 ),...,pm (dm ). ∈R m ^ p(d : D) = rp1 (d1 ),...,pm (dm ) = rp1 (d1 ),...,pm (dm ) pi (di ) i:=1 Por último, si se aplica el algoritmo de instanciación de Mateescu [Mat98], se obtiene finalmente un Bes sin parámetros, en el que todos los posibles valores de cada término constructor tipado ha sido enumerado sobre su correspondiente dominio finito de datos. El Bes implı́cito y sin parámetros que resulta de este proceso se define como sigue, donde Dm representa D × D × . . . m veces. µ _ x0 = gp1 (d1 ),...,pm (dm ) (2.2.1) :− p1 (d1 ),...,pm (dm ). ∈G µ _ gp1 (d1 ),...,pm (dm ) = µ gpc1 (e1 ),...,pm (em ) = {e1 ,...,em m ^ gpc1 (e1 ),...,pm (em ) (2.2.2) }∈Dm piei (2.2.3) i:=1 µ _ pd = rp1 (d1 ),...,pm (dm ) (2.2.4) p(d) :− p1 (d1 ),...,pm (dm ). ∈R µ rp1 (d1 ),...,pm (dm ) = _ {e1 , ..., em }∈Dm rpc1 (e1 ),...,pm (em ) (2.2.5) 36 CAPÍTULO 2. DE DATALOG A BES µ rpc1 (e1 ),...,pm (em ) = m ^ piei (2.2.6) i:=1 Obsérvese que la ecuación 2.1.1 se ha transformado en un conjunto de ecuaciones sin parámetros (2.2.1, 2.2.2, 2.2.3). Primero, la ecuación 2.2.1 describe el conjunto de objetivos parametrizados gp1 (d1 ),...,pm (dm ) de la consulta. Luego, la ecuación 2.2.2 representa la instanciación de cada parámetro di a todos los posibles valores de su dominio. Por último, la ecuación 2.2.3 establece que cada objetivo instanciado gpc1 (e1 ),...,pm (em ) se satisface siempre que los valores ej hagan a todos los predicados pi del objetivo true. De modo similar, la ecuación 2.1.2 (que describe las reglas Datalog) se codifica mediante un conjunto de ecuaciones sin parámetros (2.2.4, 2.2.5, 2.2.6). 2.2.1. Optimizaciones El Bes sin parámetros descrito arriba es ineficiente ya que adopta una aproximación por fuerza bruta que, en los primeros pasos de la computación (Ecuación 2.2.2), enumera todas las posibles tuplas (sobre Dm ) de la consulta. Es bien sabido que un programa Datalog tiene una complejidad temporal O(nk ), en la que k es el máximo número de variables presentes en una única regla, y n es el número de constantes en los hechos y las reglas. De forma parecida, para una consulta sencilla como :- superior(X,Y)., con X e Y siendo elementos de un dominio D de tamaño 10 000, la Ecuación 2.2.2 generará D2 , i.e., 108 , variables booleanas representando todas las posibles combinaciones de valores X e Y en la relación superior. Normalmente, para cada átomo de un programa Datalog, el número de hechos que se dan o se infieren mediante las reglas Datalog es mucho más pequeño que la talla del dominio elevada a la potencia de la aridad del átomo. Idealmente, la evaluación de una consulta Datalog deberı́a enumerar hechos (dados o inferidos) sólo bajo demanda. Entre las optimizaciones existentes para la evaluación top-down de consultas Datalog, está la técnica Query-Sub-Query [Vie86] que consiste en minimizar el número de tuplas derivadas mediante un proceso de reescritura del programa que se basa en la propagación de enlaces. Básicamente, el método está dirigido a mantener los enlaces de las variables que pueden usarse para cada átomo p(a) en una regla. En nuestra técnica de evaluación Datalog basada en Bes, adoptamos una aproximación similar: dos nuevas ecuaciones booleanas (las Ecuaciones 2.2.2 y 2.2.5 ligeramente modificadas) sólo enumeran los valores de los argumentos cuyas variables estén compartidas, en otro caso los argumentos se mantienen sin cambios. Además, si el átomo p(a) es parte de la Base de Datos Extensional, los únicos posibles valores de sus argumentos variables son valores presentes en algún hecho del programa Datalog. Llamaremos Dip al subdominio de D que contiene todos los posibles valores del argumento 37 2.2. INSTANCIACIÓN A UN BES SIN PARÁMETROS variable i-ésimo de p si p está en la Base De Datos Extensional, en otro caso Dip = D. De esta forma, es más fácil que la resolución del Bes resultante procese menos hechos y que, por lo tanto, sea más eficiente que la aproximación mediante fuerza bruta. Siguiendo esta técnica de optimización, se puede derivar directamente un Bes sin parámetros a partir de la representación Bes anterior, lo cual puede ser formalizado como sigue: µ _ x0 = gp1 (d1 ),...,pm (dm ) (2.2.7) :− p1 (d1 ),...,pm (dm ). ∈G µ _ gp1 (d1 ),...,pm (dm ) = p {a1 , ..., am }∈({V ∪D1 1 }×...×{V ∪D1pm }) | gppc1 (a1 ),...,pm (am ) if (∃ j ∈ [1..m], j 6= i | di = dj ∧ di ∈ V ) p µ gppc1 (a1 ),...,pm (am ) = µ pa = then ai ∈ D1 i ∧ (∀ j ∈ [1..m], di = dj | aj := ai ) else ai := di m ^ piai i:=1 pfa ∨ pra (2.2.8) (2.2.9) (2.2.10) µ _ pfa = (e:=a ∧ a∈C) ∨ pce (e∈D1p ∧ a∈V ) | (2.2.11) p(e). ∈ R µ pce = true (2.2.12) µ _ pra = rp1 (d1 ),...,pm (dm ) (2.2.13) p(a) :− p1 (d1 ),...,pm (dm ). ∈R µ _ rp1 (d1 ),...,pm (dm ) = p {a1 , ..., am }∈({V ∪D1 1 }×...×{V ∪D1pm }) | rppc1 (a1 ),...,pm (am ) if (∃ j ∈ [1..m], j 6= i | di = dj ∧ di ∈ V ) p µ rppc1 (a1 ),...,pm (am ) = then ai ∈ D1 i ∧ (∀ j ∈ [1..m], di = dj | aj := ai ) else ai := di m ^ piai (2.2.14) (2.2.15) i:=1 Obsérvese que las Ecuaciones 2.2.7, 2.2.9, 2.2.13 y 2.2.14 corresponden respectivamente a las Ecuaciones 2.2.1, 2.2.3, 2.2.4 y 2.2.6 de la definición previa de Bes con un ligero renombrado de las variables booleanas generadas. La novedad importante es que, en lugar de enumerar todos los posibles valores del dominio e.g., {e1 , . . . , em } ∈ Dm como se hace en la Ecuación 2.2.2, su correspondiente nueva Ecuación 2.2.8 sólo enumera los valores de los argumentos variables que se repiten en el cuerpo de una regla, de otra forma los argumentos variables se dejan inalterados i.e., ai := di . Es más, las variables booleanas generadas gppc1 (a1 ),...,pm (am ) pueden referirse aún a relaciones que contengan argumentos variables. De esta manera se evita en este punto la explosión combinatoria de tuplas posibles, la cual se retrasa hasta pasos futuros. La Ecuación 2.2.10 genera dos sucesores booleanos para la varia- 38 CAPÍTULO 2. DE DATALOG A BES ble pa : pfa si p es una relación que forma parte de Base de Datos Extensional, y pra cuando p es definida mediante reglas Datalog. En la Ecuación 2.2.11, cada valor de a (variable o constante) que lleve a un hecho inicial p(e). del programa, genera una nueva variable booleana pce , que es true por la definición de hecho. La Ecuación 2.2.13 infiere reglas Datalog cuyas cabezas sean pa . Nótese que las Ecuaciones 2.2.8, 2.2.11, y 2.2.14 enumeran todos los posibles valores de los subdominios D1pi en lugar del dominio completo D. Con el programa Datalog descrito en el Ejemplo 2.1.6, esta restricción consistirı́a en usar dos nuevos subdominios D1supervise = {mary, alice} y D2supervise = {alice, mark} en lugar del dominio D = {mary, alice, mark} para los valores de cada argumento variable en la relación supervise. 2.2.2. Extracción de soluciones Considerando el Bes sin parámetros optimizado definido arriba, el problema de la satisfabilidad de una consulta se reduce a la resolución local de la variable booleana x0 . El valor (true o false) computado par x0 indica si existe al menos un objetivo satisfacible en G. Remarcamos que el Bes que representa la evaluación de una consulta Datalog se compone únicamente de un bloque de ecuaciones que contiene dependencias alternantes entre variables disyuntivas y conjuntivas. Por ese motivo, puede ser resuelta mediante una estrategia de búsqueda primero en profundidad optimizada para ese tipo de bloque de ecuaciones. No obstante, como dicha estrategia puede concluir solamente la existencia de una solución a la consulta computando el mı́nimo número de variables booleanas, es necesario el uso de una estrategia en amplitud para computar todas las posibles soluciones de la consulta Datalog. Dicha estrategia forzará la resolución de todas las variables booleanas que estén en la cola de evaluación, incluso si la satisfacibilidad de la consulta ya ha sido demostrada en el camino. Por consiguiente, el motor de resolucion computará todas las posibles variables booleanas pce , que son soluciones potenciales para la consulta. Cuando se termine la resolución del Bes (asegurada por el uso de dominios finitos y una exploración basada en tablas hash), las soluciones a la consulta, i.e., las combinaciones de valores variables {e1 , . . . , em }, uno para cada átomo de la consulta, que llevan a una consulta true, se extraen de todas las variables booleanas pce que son alcanzables desde la variable booleana x0 a través de un camino true de variables booleanas. En el siguiente capı́tulo vamos a describir la estructura de la herramienta que implementa la versión optimizada de la transformación y resolución del programa Datalog presentada en la Sección 2.2.1. Previamente, se desarrolló un prototipo que implementaba la solución sin optimizar (siguiendo las ecuaciones 2.2.1-2.2.6) y que, por lo tanto, tenı́a problemas de escalabilidad. Capı́tulo 3 Arquitectura de la aplicación En el presente capı́tulo se expondrá la estructura conceptual de la aplicación DATALOG SOLVE y se darán detalles del funcionamiento interno de DATALOG SOLVE. 3.1. Vista general En la Figura 3.1 puede verse la estructura general de DATALOG SOLVE y el entorno en el que actúa para la realización de análisis estáticos sobre código Java siguiendo la aproximación introducida en la Sección 1.2. Análisis (.datalog) dominios finitos var (.map) heap (.map) vP0 (.tuples) hP0 (.tuples) resolución (+diagnóstico) (.class) Compilador Joeq + hechos Datalog predefinidos BES implı́cito Programa Java Biblioteca Cæsar Solve (Cadp) Y/N (satisfacibilidad de la consulta) Datalog Solve vP (.tuples) assign (.tuples) hP (.tuples) : entrada/salida hechos Datalog : proporciona Tuplas de salida (respuestas a la consulta) Figura 3.1: El sistema DATALOG SOLVE y su entorno de operación. En el marco de análisis propuesto, un análisis queda especificado mediante un programa lógico Datalog. Dicho programa lógico es un conjunto de hechos, reglas y consultas. Las reglas y las consultas dependen del tipo de análisis que se desea hacer (por ejemplo, un análisis de punteros) y se concretan en un fichero “.datalog”. Por el contrario, el conjunto de hechos es dependiente del programa Java concreto a analizar. Dicho conjunto ası́ como la extensión de 40 CAPÍTULO 3. ARQUITECTURA DE LA APLICACIÓN los dominios sobre los que se forman los hechos se extraen a partir del bytecode del programa Java mediante el compilador Joeq (ficheros .map, .tuples en la Figura 3.1). El objetivo de DATALOG SOLVE es resolver el programa lógico, que está representando un análisis estático concreto, y devolver los hechos inferidos a partir del mismo. Para ello, DATALOG SOLVE realiza una traducción del programa Datalog a un Pbes (siguiendo la Proposición 2.1.4) que se instancia al vuelo en un Bes sin parámetros —siguiendo las Ecuaciones 2.2.1 o 2.2.7— para que el motor de resolución de Bes de la biblioteca Cæsar Solve pueda buscar las soluciones. Queremos remarcar el hecho de que DATALOG SOLVE es un motor de evaluación de programas Datalog y, por lo tanto, su aplicación no está limitada exclusivamente al campo del análisis estático de programas. En las siguientes secciones ahondaremos un poco más en las distintas partes de la herramienta. 3.2. Una visión externa Externamente, DATALOG SOLVE es un motor de resolución de programas Datalog. Independientemente de su uso (para realizar análisis estáticos o no) es un programa con una entrada, una salida y unas opciones (o modos) generales de ejecución. En los siguientes apartados se darán detalles de estos aspectos externos del programa. 3.2.1. Entrada del programa Tal y como se explicó al comienzo del presente capı́tulo, la entrada a DATALOG SOLVE está formada por un conjunto de hechos, una serie de reglas y consultas, y los dominios sobre los que se construyen todos ellos. Estas entradas están contenidas en distintos tipos de fichero que analizaremos a continuación. Ficheros de dominio Un fichero de dominio representa un único dominio del programa Datalog. Como se puede ver en la Figura 3.2, es un fichero de texto plano en el que cada cadena de caracteres que forma una lı́nea representa un elemento del dominio. Cada elemento del dominio (cada lı́nea del fichero) está identificado por el número de lı́nea1 (comenzando en 0) que ocupa en el fichero de dominio. 1 Los números que encabezan cada lı́nea de la Figura 3.2 no están incluidos en el fichero original, sino que están implı́citamente determinados por el número de lı́nea que ocupa cada elemento del dominio en el fichero. 3.2. UNA VISIÓN EXTERNA 0 1 2 3 4 . . . 41 null java.io.BufferedInputStream java.io.PrintStream byte[] java.nio.charset.CodingErrorAction Figura 3.2: Un fichero de dominio con los tipos usados en un programa Java. Los ficheros de dominio a leer se especifican en el fichero “.datalog” que se describirá más adelante. En el ámbito del análisis de programas Java, este tipo de fichero será generado por el compilador Joeq automáticamente a partir del programa objeto del análisis. Ficheros con hechos Un fichero de hechos o .tuples representa todos los hechos de un predicado determinado. Dicho fichero ha de llamarse “nombre del predicado.tuples” para que DATALOG SOLVE pueda acceder a él. En las siguientes lı́neas, cuando hablemos de argumento o de aridad siempre estaremos haciendo referencia a los argumentos o la aridad del predicado cuyos hechos representa un fichero determinado. Como se puede ver en la Figura 3.3, un fichero de hechos es un fichero de texto plano que consta de una cabecera, que ocupa solamente la primera lı́nea, y del resto de lı́neas, que constarán de valores numéricos separados por espacios en blanco. Cada lı́nea del fichero (excepto la cabecera) representa un hecho del predicado asociado al fichero. El número i-ésimo de una lı́nea representa al elemento del dominio2 que constituye el argumento i-ésimo en el hecho representado por dicha lı́nea. Por lo tanto, la cantidad de números presentes en cada lı́nea será la misma para todo el fichero y será igual a la aridad del predicado de interés. # H0:12 F0:10 H1:12 0 0 48 8 3 25 24 3 31 36 3 39 . . . Figura 3.3: Un fichero de hechos (.tuples) asociado a un predicado. 2 Se entiende que es el dominio asociado al argumento i-ésimo del predicado. 42 CAPÍTULO 3. ARQUITECTURA DE LA APLICACIÓN La cabecera del fichero empieza con el sı́mbolo # al que le siguen un número de pares de cadenas del tipo nombre de columna:número de bits igual a la aridad del predicado. Estos pares informan de la estructura del predicado de interés. El nombre de columna i-ésimo se compone del nombre del dominio asociado al argumento i-ésimo al que se le concatena un número natural. Este natural es el número de la ocurrencia del dominio anterior en la estructura del predicado. El rango de estos números comienza en el 0 y las ocurrencias del dominio se cuentan de izquierda a derecha. El número de bits contiene el número de bits necesarios para codificar el dominio asociado3 . En el ámbito del análisis de programas Java, este tipo de fichero será generado por el compilador Joeq4 automáticamente a partir del programa objeto del análisis. Ficheros .datalog Los ficheros “.datalog” contendrán las reglas y las consultas Datalog que expresarán un tipo de análisis determinado. Como se puede ver en la Figura 3.4, un fichero “.datalog” contiene tres secciones bien diferenciadas porque el nombre de cada una de ellas va precedido de la cadena “###”: DOMAINS: la sección de declaración de dominios, RELATIONS: la sección de declaración de relaciones (o predicados), y RULES: la sección de definición de reglas. En la sección de declaración de dominios, cada lı́nea es una declaración de un dominio. Una declaración de dominio consta de un nombre de dominio, el tamaño del mismo y el nombre del fichero que contendrá sus elementos (representados con cadenas de caracteres), todos ellos separados por espacios en blanco. En la sección de declaración de relaciones, cada lı́nea declara un predicado distinto. Una declaración de predicado consta del nombre del predicado, una secuencia de pares nombre de argumento : nombre de dominio, que especifican las naturaleza de los argumentos, y la cadena inputtuples u outputtuples (sólo una de las dos). La cadena inputtuples en la declaración de un predicado indica que dicho predicado tiene hechos iniciales que DATALOG SOLVE tendrá que cargar5 . La cadena outputtuples indicará que, en el caso de que 3 Pese a que DATALOG SOLVE no usa estos números, son interesantes si se usan otras representaciones compactas de los hechos. 4 En realidad usamos una versión de Joeq modificada ligeramente por nosotros para que extraiga ficheros .tuples y no los DDBs que extrae la versión original. 5 DATALOG SOLVE siempre cargará los hechos de un predicado p de un fichero llamado p.tuples. 3.2. UNA VISIÓN EXTERNA 43 ### DOMAINS V 23 var.map H 4 heap.map ### RELATIONS vP0 (variable: V, heap: H) inputtuples assign (dest: V, source: V) inputtuples vP (variable: V, heap: H) outputtuples ### RULES vP (v,h) :- vP0(v,h). vP (v1,h) :- assign(v1,v2), vP(v2,h). Figura 3.4: Un fichero “.datalog” con las reglas y las consultas Datalog a evaluar. se le indique a DATALOG SOLVE que extraiga todas las soluciones, se genere un fichero con todos los hechos que se hayan inferido para dicho predicado6 . En la sección de definición de reglas, cada lı́nea define una regla con la misma sintaxis introducida en el Sección 1.2.1, pero sustiyendo los “&” por “,” y acabando con un “.”. La versión actual de DATALOG SOLVE no permite especificar consultas en el fichero “.datalog”. Por defecto, se generan tantas consultas del tipo :- p(x0 , x1 , . . . )7 como predicados hayan sido declarados con la opción outputtuples. 3.2.2. Salida del programa DATALOG SOLVE genera distintos tipos de salida dependiendo de su modo de funcionamiento, que se explicará en la Sección 3.2.3. En el modo de satisfacibilidad, la salida es un mensaje indicando true o false dependiendo de si se ha encontrado una solución para la consulta realizada o no. En el modo de búsqueda exhaustiva de soluciones, la salida estará formada por ficheros con todas las tuplas inferidas por el programa Datalog. El formato de estos ficheros de salida es análogo al de los fichero .tuples comentados anteriormente, esto es, los ficheros de entrada con hechos. 6 DATALOG SOLVE siempre nombrará p.tuples al fichero que genere con los hechos inferidos para un predicado p. 7 x0 , x1 , etc. son variables distintas. 44 CAPÍTULO 3. ARQUITECTURA DE LA APLICACIÓN 3.2.3. Modos de ejecución DATALOG SOLVE tiene dos modos de funcionamiento: el modo de satisfacibilidad y el modo exhaustivo. En el modo de satisfacibilidad, DATALOG SOLVE busca soluciones a la consulta planteada. La ejecución termina en el momento en el que encuentra la primera solución, avisando de que la consulta planteada se satisface —tiene al menos una solución. Por el contrario, en el modo exhaustivo, DATALOG SOLVE encuentra todas y cada una de las soluciones posibles a la consulta y las vuelca a ficheros de salida. A partir del comportamiento de la herramienta en ambos modos, se puede deducir que DATALOG SOLVE nunca será más rápido en modo exhaustivo que en modo satisfacibilidad. 3.3. Una visión interna DATALOG SOLVE está compuesto de 120 lı́neas de Lex, 380 lı́neas de Bison y 3500 lı́neas de código C. Es un traductor y evaluador de programas Datalog que opera en tres fases a la hora de resolver programas. En la primera fase, el programa Datalog se analiza y se almacena en memoria una representación del mismo que facilite su posterior manipulación. En la segunda fase, se resuelve el Bes que representa el programa utilizando la biblioteca CÆSAR SOLVE 1, incluida en OPEN/CÆSAR. En la tercera y última fase, se extraen las soluciones deseadas de la traza de resolución del Bes que se ha obtenido en la segunda fase. 3.3.1. Primera fase: traducción En esta primera fase, el programa Datalog se lee de los ficheros de entrada8 , se analiza y se almacena en memoria. Esta primera fase a nivel de implementación consta de dos fases bien definidas: 1. análisis y almacenamiento del programa Datalog en memoria de forma directa 9 , y 2. generación de estructuras de datos a partir del la representación directa del programa en memoria que codifiquen implı́citamente el Bes subyacente al programa Datalog según el formalismo desarrollado. A continuación explicaremos con mayor detalle en qué consisten estas dos fases y qué tipo de estructuras de datos utilizan. 8 9 Ficheros de dominios, de hechos y el fichero “.datalog”. Por directa se entiende que tiene la misma estructura que el programa Datalog original. 45 3.3. UNA VISIÓN INTERNA Análisis y almacenamiento directo del programa Datalog El analizador de programas Datalog incluido en DATALOG SOLVE tiene la estructura tı́pica del front-end de un compilador habitual. Se realiza un análisis sintáctico que realiza peticiones de tokens al analizador léxico. El analizador sintáctico se ha especificado en BISON y el léxico en FLEX. A continuación proporcionamos la gramática usada para el análisis sintáctico: program → domain section header domains relation section header relations rule section header rules domains → domain | domains domain domain → domain id number filename relations → relation relations | relation → identifier ( parameter list ) io nature parameter list → identifier :: identifier parameter tail parameter tail → , parameter list | rules → clause rules | clause → atom tail . atom → identifier parenthesized list parenthesized list → ( argument list ) argument list → term argument tail argument tail → , argument list | term → identifier 46 CAPÍTULO 3. ARQUITECTURA DE LA APLICACIÓN tail → :- literal list | literal list → literal literal tail literal → identifier parenthesized list literal tail → , literal list | A partir del análisis dirigido por la grámatica anterior, se construye una representación directa del programa Datalog consistente en la creación de unas estructuras de datos enlazadas que representen los no-terminales más importantes de la gramática. Hemos considerado como no-terminales más importantes los siguientes: domain relation rule atom (aunque éste no está considerado en la gramática) literal Ası́, cada uno de estos no-terminales lleva asociado un struct en el que se guarda la información asociada al mismo que nos proporcionan sus tokens. A continuación se ilustrará este proceso mediante unos cuantos ejemplos. typedef struct CAESAR STRUCT RULE { /* Head of rule */ CAESAR TYPE ATOM HEAD; /* Tail of rule */ CAESAR TYPE LITERAL LIST TAIL; } CAESAR BODY RULE, *CAESAR TYPE RULE; Figura 3.5: Estructura de datos que representa una regla Datalog de manera directa. En la Figura 3.5, se puede observar la estructura de una regla Datalog. Consiste en una estructura de tipo átomo como cabeza y una lista de literales como cuerpo. En la Figura 3.6, podemos ver la representación de un átomo Datalog. Consiste en una estructura de tipo relación RELATION y una lista de variables RULE VARIABLE LIST 3.3. UNA VISIÓN INTERNA 47 typedef struct CAESAR STRUCT ATOM { /* Relation */ CAESAR TYPE RELATION RELATION; /* Rule-variable list */ CAESAR TYPE RULE VARIABLE LIST RULE VARIABLE LIST; } CAESAR BODY ATOM, *CAESAR TYPE ATOM; Figura 3.6: Estructura de datos que representa un átomo Datalog de manera directa. asociadas al mismo. De forma semejante, en la Figura 3.7, podemos ver la estructura de un literal que es análoga ya que no estamos teniendo en cuenta la negación dentro de nuestro evaluador Datalog. typedef struct CAESAR STRUCT LITERAL { /* Relation */ CAESAR TYPE RELATION RELATION; /* Rule-variable list */ CAESAR TYPE RULE VARIABLE LIST RULE VARIABLE LIST; } CAESAR BODY LITERAL, *CAESAR TYPE LITERAL; Figura 3.7: Estructura de datos que representa un literal Datalog de manera directa. Por último, en la Figura 3.8 está descrita la estructura de una relación Datalog. Ésta posee un nombre NAME que la identifica, una aridad ARITY, un código que indica si es una relación de entrada o de salida, y una lista, DOMAIN LIST, con los dominios de sus argumentos. Mediante este tipo de representación, al final de la primera fase tenemos una representación del programa Datalog mediante tres listas de estructuras interconectadas: una de dominios, otra de relaciones y otra de reglas. Esta disposición nos permitirá posteriormente navegar por el programa y manipularlo. Traducción a un Bes implı́cito en memoria La traducción del programa Datalog a un Bes es dependiente de los distintos tipos de variables booleanas considerados en la formalización realizada en el Capı́tulo 2. A nivel de implementación se ha optado por darles nombres más sencillos a los distintos tipos de variables booleanas que los adoptados en el formalismo. A continuación presentamos una correspondencia entre la forma general de un tipo de variable según nuestro formalismo y el nombre dado al tipo de variable en la implementación (sin argumentos). 48 CAPÍTULO 3. ARQUITECTURA DE LA APLICACIÓN typedef struct CAESAR STRUCT RELATION { /* Name of the relation */ CAESAR TYPE STRING NAME; /* Arity */ CAESAR TYPE NATURAL ARITY; /* Origin and format of information */ CAESAR TYPE IONATURE IONATURE; /* Arguments’ domain list */ CAESAR TYPE DOMAIN LIST DOMAIN LIST; } CAESAR BODY RELATION, *CAESAR TYPE RELATION; Figura 3.8: Estructura de datos que representa una relación Datalog de manera directa. x0 → X0 : variable inicial gp1 (d1 ),...,pm (dm ) → X1 : consulta parametrizada (parameterised query o pquery) gppc1 (a1 ),...,pm (am ) → X2 : consulta parcialmente instanciada (partially instantiated query o piquery) pa → X3 : predicado parametrizado (parameterised predicate) pfa → X4 : predicado parcialmente instanciado (partially instantiated predicate o pipredicate) pce → X6 : hecho (fact) pra → X5 : predicado parcialmente instanciado (partially instantiated predicate o pipredicate). rp1 (d1 ),...,pm (dm ) → X7 : regla parametrizada (parameterised rule o prule) rppc1 (a1 ),...,pm (am ) → X8 : regla parcialmente instanciada (partially instantiated rule o pirule) A continuación explicaremos una por una las estructuras de datos que se crean para traducir el programa Dataloga un Bes. Tabla de dominios A partir de la lista de dominios del programa Datalog analizado se crean varias tablas. Primero, se crea una tabla de dominios, en la cual cada entrada representa un dominio de interés. Una entrada, cuya estructura de puede ver en la Figura 3.9, contiene el nombre que identifica al dominio. También contiene el cardinal o tamaño del dominio, esto es, el número de elementos 3.3. UNA VISIÓN INTERNA 49 que contiene el mismo. Asimismo, la entrada almacena también una tabla con todos los elementos del dominio, esto es, con todas las cadenas de caracteres que los representan10 y que se obtienen a partir del fichero asociado al dominio especificado en el programa Datalog. Por último, con vistas a la optimización, se almacena también una tabla que contiene únicamente los elementos del dominio que forman parte de algún hecho inicial. Estos dominios reducidos se utilizan en la evaluación porque, en el caso de que un elemento no participe en ningún hecho, no tiene sentido considerarlo ya que no podrá existir en ninguna conclusión extraı́da del programa. Esto se debe a que todo elemento que forme parte de alguna solución del programa ha de haber sido propagado, en última instancia, desde un hecho. typedef struct CAESAR STRUCT DOMAIN TABLE ENTRY { /* Name */ CAESAR TYPE STRING NAME; /* Cardinal */ CAESAR TYPE NATURAL CARDINAL; /* Indexes of the ELEMENTS table which are in at least one fact */ CAESAR TYPE TABLE 1 FILTERED ELEMENTS; /* Table with all the elements (strings) of the domain */ CAESAR TYPE TABLE 1 ELEMENTS; } CAESAR BODY DOMAIN TABLE ENTRY, *CAESAR TYPE DOMAIN TABLE ENTRY; Figura 3.9: Entrada de la tabla de dominios. Tabla de relaciones La tabla de relaciones se construye a partir de la lista de relaciones de nuestra representación del programa Datalog. Cada entrada representará una relación (o predicado) del mismo. Los campos básicos de toda entrada de esta tabla son el nombre, la aridad y un vector de dominios que especifican una relación, tal y como se puede ver en la Figura 3.10. Para que la ejecución del Bes subyacente sea más eficiente temporalmente se han añadido una serie atributos adicionales (Figura 3.10) que enumeramos a continuación: COMBINATIONS Es una tabla de combinaciones 11 , esto es, una tabla hash que contendrá todas las combinaciones posibles de variables y constantes que serán exploradas 10 El identificador real de un elemento de un dominio es el número de la lı́nea en la que aparece su string representativo dentro de su fichero de dominio correspondiente. 11 Explicaremos en detalle las tablas de combinaciones más adelante. 50 CAPÍTULO 3. ARQUITECTURA DE LA APLICACIÓN typedef struct CAESAR STRUCT RELATION TABLE ENTRY { /* Relation name */ CAESAR TYPE STRING NAME; /* Relation arity */ CAESAR TYPE NATURAL ARITY; /* Domain indexes */ CAESAR TYPE INDEX TABLE 1 *DOMAINS; /* Fact table */ CAESAR TYPE TABLE 1 FACTS; /* Rule table */ CAESAR TYPE TABLE 1 RULES; /* Combination table */ CAESAR TYPE TABLE 1 COMBINATIONS; /* Filtered domains */ CAESAR TYPE TABLE 1 *FILTERED DOMAINS; } CAESAR BODY RELATION TABLE ENTRY, *CAESAR TYPE RELATION TABLE ENTRY; Figura 3.10: Entrada de la tabla de relaciones. durante la ejecución para dicha relación. Su misión es detectar ciclos (impidiendo una ejecución infinita) y ahorrar espacio en memoria (las combinaciones pueden formar parte de la representación de distintas variables booleanas pero sólo se almacenarán una vez). FACTS Es una tabla de combinaciones que guardará exclusivamente combinaciones de constantes. Servirá para consultar si una combinación de la respectiva relación es un hecho, esto es, verdadera o no. Dado que sólo las relaciones extensionales tienen hechos, este campo sólo existirá para dichas relaciones. FILTERED DOMAINS Es un vector que contendrá, siguiendo el orden de los argumentos de dicha relación, las tablas en las que están representados los dominios filtrados correspondientes. La versión filtrada de un dominio asociado al argumento i-ésimo de una relación es el conjunto de elementos de dicho dominio que participa en al menos un hecho de la misma relación y en la posición i-ésima. RULES Es una tabla que contendrá las reglas en cuya cabeza se encuentre la relación des- 3.3. UNA VISIÓN INTERNA 51 crita. Tablas de combinaciones Una combinación es el conjunto de argumentos aplicados a un sı́mbolo de predicado para obtener un predicado. Estos argumentos pueden ser variables o constantes. Según la traducción propuesta, estos argumentos de predicados pasan a ser parámetros de las variables booleanas por lo que deben almacenarse (directa o indirectamente) con cada variable booleana generada. Se ha optado por una representación de las combinaciones en forma de bloque de memoria de tantas palabras como argumentos tenga el predicado y una serie de bits finales que codifiquen la naturaleza (variable o constante) de cada argumento. Las tablas de combinaciones son tablas hash en las que cada entrada representa una combinación. Se usan tablas hash para evitar eficientemente la creación de entradas duplicadas. Toda relación del programa tiene asociada una tabla de combinaciones que dará cuenta de los predicados que ya han sido explorados, permitiendo a DATALOG SOLVE evitar su entrada en ciclos infinitos. Tabla de reglas La tabla de reglas se construye a partir de la lista de reglas de nuestra representación directa del programa Datalog. Cada entrada representará una regla del mismo y su estructura puede verse en la Figura 3.11 En última instancia, una regla nos ha de permitir transformar un objetivo en un conjunto de subobjetivos propagando adecuadamente la información de la que disponemos, esto es, realizar una sustitución. Para optimizar la sustitución, se analizan todas las reglas descubriendo sus variables libres y ligadas, y se almacena la información de cada ocurrencia de las mismas. Los atributos de una regla son: LENGTH Es la longitud del cuerpo de la regla, esto es, el número de predicados que encontramos en el mismo. RELATION Es el predicado de la cabeza de la regla. PCRULE TABLE Es una tabla de reglas parcialmente instanciadas. No tienen nada que ver con la sustitución pero, ubicarla aquı́, facilita la navegación a la hora de generar sucesores de ciertas variables booleanas. FREE VARIABLE MAP LIST Es una lista de mappings 12 para las variables libres del programa. 12 Más adelante se explicará en qué consiste un mapping y su estructura de datos asociada. 52 CAPÍTULO 3. ARQUITECTURA DE LA APLICACIÓN typedef struct CAESAR STRUCT RULE TABLE ENTRY { /* Size of the rule tail */ CAESAR TYPE NATURAL LENGTH; /* Relation head index */ CAESAR TYPE INDEX TABLE 1 RELATION; /* PCRule table */ CAESAR TYPE TABLE 1 PCRULE TABLE; /* Maps for free variables */ CAESAR TYPE VARIABLE MAP LIST FREE VARIABLE MAP LIST; /* Maps for linked variables */ CAESAR TYPE VARIABLE MAP LIST LINKED VARIABLE MAP LIST; /* Packed combinations in the tail of the rule */ CAESAR TYPE POINTER TAIL TO CONCRETIZE; /* Display for seeking the correct combination in the packed tail */ CAESAR TYPE NATURAL *TAIL TO CONCRETIZE DISPLAY; /* Indices of the relations in the tail */ CAESAR TYPE INDEX TABLE 1 *TAIL TO CONCRETIZE RELATIONS; } CAESAR BODY RULE TABLE ENTRY, *CAESAR TYPE RULE TABLE ENTRY; Figura 3.11: Entrada de la tabla de reglas. LINKED VARIABLE MAP LIST Es una lista de mappings para las variables enlazadas del programa. TAIL TO CONCRETIZE Es un bloque de combinaciones que representa el cuerpo de la regla. Sobre él se realiza la sustitución y la instanciación de forma eficiente. TAIL TO CONCRETIZE DISPLAY Es un vector que permite situarnos en el posición de predicado deseada dentro del bloque de combinaciones del punto anterior. TAIL TO CONCRETIZE RELATIONS Es un vector que nos permite obtener un predicado a partir de su posición en el cuerpo de la regla. Sustitución e instanciación 53 3.3. UNA VISIÓN INTERNA Para realizar la sustitución e instanciación rápidamente se necesita un acceso eficiente a los argumentos de los literales que deseamos modificar. Para ello, se trabaja sobre un bloque contiguo e indexado (mediante una estructura de tipo display ya introducida en el punto 3.3.1) de combinaciones en memoria. Además de esto, se han creado unas estructuras de datos que almacenan en listas las posiciones en las que ocurre una variable, tanto en la cabeza como en el cuerpo. Son las siguientes: CAESAR STRUCT VARIABLE POSITION Está formado por el número de literal y el número de argumento como se puede ver en la Figura 3.12. typedef struct CAESAR STRUCT VARIABLE POSITION { /* Literal number */ CAESAR TYPE NATURAL LITERAL INDEX; /* Argument number */ CAESAR TYPE NATURAL ARGUMENT INDEX; } CAESAR BODY VARIABLE POSITION, *CAESAR TYPE VARIABLE POSITION; Figura 3.12: Posición de una variable. CAESAR STRUCT VARIABLE MAP Está formado por una variable13 , la cardinalidad del dominio filtrado asociado a dicha variable, una posición asociada al predicado de la cabeza de la regla, y una lista de posiciones asociadas a los predicados del cuerpo de la regla (Figura 3.13). Esto es, la estructura representa un mapping de una variable a todas sus ocurrencias en una regla. Mediante el uso de las estructuras de datos anteriores, la sustitución e instanciación parcial de una regla se reduce a realizar: 1. una única sustitución sobre el bloque de combinaciones del cuerpo recorriendo los mappings de las variables ligadas, y 2. las instanciaciones necesarias sobre el mismo bloque de combinaciones recorriendo los mappings de las variables libres a instanciar, esto es, aquéllas que tienen más de una ocurrencia en el cuerpo de la regla. 13 Una variable se representa como un ı́ndice de una tabla de variables previamente inicializada con todas las variables del programa. Los detalles meramente técnicos de la construcción de dicha tabla se han omitido con vistas a simplificar la exposición. 54 CAPÍTULO 3. ARQUITECTURA DE LA APLICACIÓN typedef struct CAESAR STRUCT VARIABLE MAP { /* Variable */ CAESAR TYPE INDEX TABLE 1 VARIABLE INDEX; /* Size of the filtered domain associated to the variable */ CAESAR TYPE NATURAL CARDINAL; /* Position in the head where it is found the variable */ CAESAR TYPE VARIABLE POSITION HEAD POSITION; /* Positions in the tail where it is found the variable */ CAESAR TYPE VARIABLE POSITION LIST TAIL POSITION LIST; } CAESAR BODY VARIABLE MAP, *CAESAR TYPE VARIABLE MAP; Figura 3.13: Mapping de una variable y sus posiciones. Gestión de las variables booleanas El Bes generado se compone de tres bloques de ecuaciones como se puede ver en la Figura 3.14. El motivo de dividir el Bes en distintos bloques de ecuaciones es poder usar el modo de resolución óptimo para cada variable según el tipo de modo de ejecución de DATALOG SOLVE, respetando la condición de Cæsar Solve que impone que el Bes esté libre de alternancia. Desde el punto de vista de los algoritmos de resolución del Bes subyacente al programa Datalog, una variable booleana es un número natural de 32 bits como se puede observar en la Figura 3.15. Una variable booleana en DATALOG SOLVE tiene unos campos a nivel de bit que la identifican perfectamente: BLOCK Es el número de bloque del sistema de ecuaciones donde se encuentra ese tipo de variable. TYPE Es, junto al número de bloque, el campo que especifica el tipo de la variable booleana (X0, X1, X2, . . . ). No tiene sentido por sı́ mismo, su única utilidad es diferenciar tipos de variable de un mismo bloque. ENTRY Es el número de entrada que representa a la variable booleana (y que, por lo tanto, contiene toda su información) en la tabla hash de variables booleanas de su tipo. Esta representación se eligió por ser compacta y sencilla. La repartición que hace del espacio de direccionamiento para cada tipo variable es equitativo. No todos los tipos de variable necesitan la misma cantidad de direcciones, no obstante, hasta el momento, la cantidad de direcciones asignadas ha mostrado ser suficientemente amplia. 55 3.3. UNA VISIÓN INTERNA BLOCK 1 BLOCK 0 x0 x5 x1 x3 x8 x7 x2 x4 x6 F V BLOCK 2 Figura 3.14: Distribución de las variables booleanas en los distintos bloques. typedef struct CAESAR STRUCT BOOLEAN VARIABLE { /* Block index */ CAESAR TYPE NATURAL BLOCK :2; /* Variable type */ CAESAR TYPE NATURAL TYPE :2; /* Index */ CAESAR TYPE NATURAL ENTRY :28; } CAESAR TYPE BOOLEAN VARIABLE; Figura 3.15: Variable booleana en DATALOG SOLVE. Toda variable booleana generada durante la resolución del Bes implı́cito tendrá una entrada en su correspondiente tabla hash según su tipo. Debido a que algunas variables booleanas siempre contienen información idéntica o muy parecida, se ha decidido a nivel de implementación hacer que compartan sus tablas hash. El uso de tablas hash es muy importante porque, al realizar una inserción, ésta nos per- 56 CAPÍTULO 3. ARQUITECTURA DE LA APLICACIÓN mite detectar eficientemente si la variable booleana está repetida, permitiendo evitar la reexploración de la misma y de todas sus sucesoras. Las tablas hash utilizadas para la gestión de las variables booleanas son las siguientes: QUERY TABLE Esta tabla contiene consultas parametrizadas y parcialmente instanciadas, esto es, variables booleanas X1 y X2. PREDICATE TABLE Esta tabla contiene predicados parametrizados e instanciados parcialmente y hechos, esto es, variables booleanas X3, X4, X5 y X6. PRULE TABLE Esta tabla contiene reglas parametrizadas, esto es, variables booleanas X7. PCRULE TABLE Esta tabla contiene reglas parcialmente instanciadas, esto es, variables booleanas X8. Todas las tablas que gestionan variables booleanas trabajan directa o indirectamente con tablas de combinaciones para guardar el estado completo de una variable. La organización de estas tablas está siendo revisada con vistas a su optimización. En el código fuente se puede encontrar información detallada sobre la estructura actual de estas tablas. Es destacable la ausencia de una tabla para la variable inicial X0, pero ésta es única y, por lo tanto, no procede almacenarla en ningún sitio y de hecho su valor está pre-establecido. 3.3.2. Segunda fase: resolución del Bes En la segunda fase, se resuelve el Bes subyacente al programa Datalog. Este Bes es implı́cito, lo que quiere decir que está definido mediante una única variable inicial X0 y una función sucesor que genera un conjunto de variables booleanas sucesoras a partir de otra, la antecesora. /* Initial boolean variable */ CAESAR TYPE BOOLEAN VARIABLE X0; X0.BLOCK = 0; /* Block 0 */ X0.TYPE = 0; /* Type 0 */ X0.ENTRY = 0; /* No entry */ Figura 3.16: Variable booleana inicial, X0, de DATALOG SOLVE. 3.3. UNA VISIÓN INTERNA 57 La inicialización de X0 puede verse en la Figura 3.17. Analizaremos el prototipo de la función sucesor, que puede verse en la Figura 3.17, con vistas a explicar su funcionamiento. Éste contiene: void CAESAR VARIABLE ITERATE( CAESAR TYPE POINTER VARIABLE 1, CAESAR TYPE POINTER VARIABLE 2, void (*CAESAR LOOP)()); Figura 3.17: Prototipo de la función sucesor de DATALOG SOLVE. VARIABLE 1 Es un puntero (del tipo CAESAR TYPE POINTER) a la variable booleana antecesora cuyos sucesores se desea generar. VARIABLE 2 Es un puntero (del tipo CAESAR TYPE POINTER) a la posición de memoria donde CÆSAR SOLVE 1 espera recibir cada variable booleana sucesora generada. CAESAR LOOP Es una función que ha de ser invocada cada vez que se coloque un variable booleana sucesora en VARIABLE 2 para que CÆSAR SOLVE 1 la considere en su exploración del Bes. La función sucesora analiza la VARIABLE 1 para ver qué tipo de variable booleana es. Según su tipo, genera las variables booleanas sucesoras del tipo adecuado en VARIABLE 2 (propagando información de la antecesora y añadiendo una nueva) y, nada más generar una variable, invoca a CAESAR LOOP para que CÆSAR SOLVE 1 tome nota de ella. Como resultado de la aplicación reiterada de la función sucesor, se pueden generar los distintos tipos de variables booleanas siguien el esquema de la Figura 3.18. Hay que destacar que una variable X4 concreta puede generar una variable false (F en el gráfico), o bien n variables X6, pero no ambos tipos de variable. Una situación análoga ocurre con X5. Como se puede observar la generación de los distintos tipos de variable es cı́clica. El ciclo se cierra gracias a las variables X3 que son sucesoras de X2 y de X8. La existencia del ciclo de generación de variables X3 − X5 − X7 − X8 − X3 (y, por lo tanto, de una dependencia cı́clica) requiere que estas variables se encuentren en el mismo bloque del Bes. Si no fuera ası́, existirı́a una dependencia cı́clica entre bloques, lo que implicarı́a que el sistema no estarı́a libre de alternancia y, por lo tanto, no podrı́amos usar el motor de resolución de Cæsar Solve. 58 CAPÍTULO 3. ARQUITECTURA DE LA APLICACIÓN x0 x1 ... x1 ... x1 x2 ... x2 ... x2 x3 ... x3 ... x3 x4 F x6 x5 ...x6 ...x6 V F x7 ...x7 ...x7 x8 ... x8 ... x8 x3 ... x3 ... x3 Figura 3.18: Grafo que ilustra el orden de generación de las variables booleanas. 3.3.3. Tercera fase: extracción de respuestas En la tercera fase se extraen las soluciones a la consulta a partir de la traza de resolución del Bes que se ha obtenido en la segunda fase. Para ello, se utiliza una funcionalidad ofrecida por CÆSAR SOLVE 1 para extraer información de la traza de resolución de un Bes. Esta funcionalidad recibe una función CAESAR VARIABLE ITERATE como la de Figura 3.19 y un número de bloque de ecuaciones booleanas, y se itera sobre todas las variables booleanas. Para cada una de las variables anteriores, CÆSAR SOLVE 1 invoca a la función STABLE VARIABLE ITERATE pasándole toda la información de la misma: el Bes a la que pertenece, el bloque en el que se ubica, el identificador de la variable y el valor de verdad de la misma (si es true o false). El proceso seguido por la función STABLE VARIABLE ITERATE es sencillo: 1. se comprueba que el tipo de la variable booleana contiene una solución a la consulta. En caso afirmativo, 3.4. DATALOG SOLVE: EJECUTABLE 59 void STABLE VARIABLE ITERATE( CAESAR TYPE SOLVE 1 BES, CAESAR TYPE NATURAL BLOCK, CAESAR TYPE POINTER VARIABLE, CAESAR TYPE BOOLEAN *VALUE); Figura 3.19: Prototipo del iterador sobre variables booleanas de DATALOG SOLVE. 2. se extrae la solución de la tabla asociada a la variable (posiblemente consultando otras variables, y 3. se añade la solución al fichero de soluciones especificado al invocar a DATALOG SOLVE. 3.3.4. Versiones Desde el comienzo del trabajo en nuestra propuesta se han desarrollado dos versiones de DATALOG SOLVE. La primera versión sirvió como prueba de concepto de nuestra transformación implementando el algoritmo de instanciación de las ecuaciones 2.2.1-2.2.6. Soportaba los dos modos de ejecución (satisfacibilidad y exhaustivo) de DATALOG SOLVE. Pero, como se expuso en el Capı́tulo 2, intrı́nsecamente adolecı́a de problemas de rendimiento (explosión de estados). Aprovechando el trabajo realizado con esta primera versión se comenzó el desarrollo de la segunda versión de DATALOG SOLVE. La segunda versión de DATALOG SOLVE mejora la eficiencia de su antecesor apoyándose en un nuevo esquema de instanciación que sigue las ecuaciones 2.2.7-2.2.15. Esta nueva forma de instanciación evita la temprana explosión de estados que padecı́a la primera versión de nuestro motor de resolución, pero añade una mayor complejidad a la extracción de las soluciones. La visión interna dada en el presente capı́tulo corresponde a la de esta nueva revisión de DATALOG SOLVE. El desarrollo completo de esta nueva versión está en proceso, habiéndo concluido la implementación del modo de ejecución de satisfacibilidad. Pretendemos disfrutar de esta segunda versión con toda su funcionalidad en breve. 3.4. DATALOG SOLVE: Ejecutable Nombre datalog solve - un evaluador de consultas Datalog bajo demanda 60 CAPÍTULO 3. ARQUITECTURA DE LA APLICACIÓN Sinopsis datalog solve [opciones generales] fichero de entrada[.datalog] Opciones generales -tuples Ejecuta el programa en modo de búsqueda exhaustiva de soluciones. Al finalizar la ejecución se generan los ficheros de salida .tuples que contendrán dichas soluciones. -stats Muestra estadı́sticas sobre el Bes subyacente tras la ejecución del programa. -understandable (Futura Opción) Genera ficheros (adicionales) con las soluciones expresadas según la representación en cadenas de caracteres de los elementos de dominio. Esta opción asume, aunque el usuario no lo haya escrito explı́citamente, que la opción ‘‘-tuples’’ está activada. Estado de salida El estado de salida es 0 si no ha habido errores en la ejecución del programa, 1 en caso contrario. Conclusiones y trabajo futuro Se han implementado dos motores de resolución de programas Datalog. El primero fue una prueba de concepto que adolecı́a del problema de la explosión de estados y su funcionalidad como solver (modo de satisfacibilidad y modo exhaustivo) se completó totalmente. El segundo motor, que es una revisión del primero con vistas a la optimización espacial, funciona en modo de satisfacibilidad y sigue siendo objeto de trabajo en estos momentos. Este segundo motor es el que centra nuestro interés ya que avala nuestra aproximación ofreciendo una mayor eficiencia. Es en este motor en el que basamos la evaluación de nuestro trabajo que podemos encontrar en el punto C.2 de estas conclusiones. C.1. Trabajo relacionado La descripción de data-flow analysis como una consulta a bases de datos fue aplicada por primera vez por Ullman [Ull89] y Reps [Rep94], quienes utilizaron la implementación bottomup con magic-sets de Datalog para derivar automáticamente una implementación local. Recientemente se han usado Bess con parámetros tipados [Mat98], llamados Pbes, para codificar algunos problemas de verificación complejos como el problema de model-checking usando el µ-cálculo modal de primer orden orientado a datos [MT08], o la comprobación de equivalencia de varias bisimulaciones [CPvW07] sobre sistemas de transiciones etiquetados que podrı́an ser infinitos. No obstante, los Pbess aún no han sido utilizados para computar análisis interprocedimentales de programas complejos en los que se lidie con objetos creados dinámicamente. El trabajo más estrechamente relacionado con este proyecto propone el uso de Grafos de Dependencia (Dgs, del inglés Dependency Graphs) para representar problemas de satisfacibilidad, incluyendo la satisfacibilidad de Cláusulas de Horn y la resolución de un Bes [LS98]. En él se describe un algoritmo de tiempo lineal para la satisfacibilidad de Cláusulas de Horn como la menor solución en un sistema de ecuaciones Dg. Éste corresponde a un Bes sin alternancia, que puede lidiar solamente con problemas de la lógica proposicional. La extensión del trabajo de Liu y Smolka [LS98] a la evaluación de consultas Datalog no es sencilla. Una muestra de 62 CONCLUSIONES Y TRABAJO FUTURO ello es la codificación de la lógica temporal basada en datos en sistemas de ecuaciones con parámetros en [MT08], en el que cada variable booleana puede depender de múltiples términos constructores. Sin embargo, los Dgs no son lo suficientemente expresivos para representar tales dependencias de datos en cada vértice. De ahı́ que sea necesario trabajar a un nivel más alto, directamente en la representación del Pbes. Recientemente se ha desarrollado el sistema Bddbddb [WACL05], que es un marco de análisis de programas especificados con Datalog basado en diagramas de decisión binarios que escala a programas grandes y es competitivo respecto a las aproximaciones imperativas tradicionales. La resolución de la consulta se logra mediante una computación de punto fijo que comienza con los hechos del programa Datalog. Las reglas Datalog se aplican de manera bottom-up hasta que se llega a la saturación, por lo que finalmente se computan todas las soluciones que satisfacen toda relación del programa Datalog. Estos conjuntos de soluciones se usan para responder consultas complejas. En contraste, nuestra aproximación opera con técnicas bajo demanda para resolver un conjunto de consultas sin ninguna computación a priori de los átomos derivables. Recientemente, Zheng y Rugina [ZR08] demostraron que el algoritmo de Cfl-alcanzabilidad con lista de trabajos se puede comparar favorablemente con una solución exhaustiva, especialmente en términos de consumo de memoria. Nuestra técnica para resolver programas Datalog basada en resolución local de Bess va en la misma dirección pero ofrece una aproximación novedosa a los análisis de programas bajo demanda. C.2. Evaluación experimental Hemos aplicado nuestra herramienta Datalog Solve14 en su versión más reciente y optimizada al análisis de punteros insensible al contexto de programas Java. Especı́ficamente, se ha realizado el análisis de punteros descrito en la Figura C.20. Más adelante presentamos los resultados de la evaluación de DATALOG SOLVE. Con la intención de indagar en la escalabilidad y aplicabilidad de la transformación propuesta, hemos aplicado nuestra técnica a 4 de los 100 proyectos Java más populares en Sourceforge que se puedan compilar como una aplicación autónoma. Estos proyectos también fueron usados como benchmarks por el sistema Bddbddb [WACL05], uno de los motores de bases de datos deductivas más eficientes, basado en diagramas de decisión binarios (en inglés binary decision diagrams o BDDs), que escala a programas Java grandes. Los benchmarks son todos de aplicaciones reales con decenas de miles de usuarios. Los proyectos varı́an en el número de clases, métodos, bytecodes, variables y asignaciones de espacio en el heap (“heap allocations”). 14 Disponible en http://www.dsic.upv.es/users/elp/datalog_solve/. 63 EVALUACIÓN EXPERIMENTAL ### Domains V H F 262144 65536 16384 variable.map heap.map field.map ### Relations vP_0 store load assign vP hP (variable : V, heap : H) (base : V, field : F, source : V) (base : V, field : F, dest : V) (dest : V, source : V) (variable : V, heap : H) (base : H, field : F, target : H) inputtuples inputtuples inputtuples inputtuples outputtuples outputtuples ### Rules vP vP hP vP (v, h) (v1, h) (h1, f, h2) (v2, h2) ::::- vP_0 (v, h). assign(v1, v2), vP (v2, h). store(v1, f, v2), vP (v1, h1), vP (v2, h2). load (v1, f, v2), vP (v1, h1), hP (h1, f, h2). Figura C.20: Datalog specification of a context-insensitive points-to analysis La información detallada que se muestra en la Tabla C.1 se obtiene de forma automática por medio del compilador Joeq. Tabla C.1: Descripción de los proyectos Java usados como benchmarks. Nombre freets nfcchat jetty joone Descripción speech synthesis system scalable, distributed chat client server and servlet container Java neural net framework Clases 215 283 309 375 Métodos 723 993 1160 1531 Bytecodes 46K 61K 66K 92K Variables 8K 11K 12K 17K Heap allocations 3K 3K 3K 4K Todos los experimentos se realizaron usando una máquina virtual Java JRE 1.5, Joeq versión 20030812, sobre un procesador Intel Core 2 T5500 1.66GHz con 3 Gigabytes de RAM, ejecutando Linux Kubuntu 8.04. El tiempo y el consumo de memoria de nuestros análisis se muestran en la Tabla C.2. Estos resultados ilustran la escalabilidad de nuestra resolución Bes sobre ejemplos reales. Datalog Solve comprueba la satisfacibilidad de la consulta por defecto para todos los benchmarks en unos pocos segundos. El resultado de los análisis se verificó comparándolo con las soluciones computadas por el sistema Bddbddb para el mismo análisis y los mismos programas. La computación exhaustiva y la extracción de todas las soluciones de un programa Datalog 64 CONCLUSIONES Y TRABAJO FUTURO Tabla C.2: El tiempo (en segundos) y los picos de uso de memoria (en Megabytes) para la consecución del análisis sobre cada benchmark. Nombre freets nfcchat jetty joone tiempo (seg.) 10 8 73 4 memoria (MB.) 61 59 70 58 es el trabajo en curso sobre la versión más reciente de DATALOG SOLVE. Además, pretendemos aprovechar algunas herramientas construidas para los Bess. En particular, la que es más atractiva para su aplicación en la práctica es el uso de algoritmos distribuidos de resolución de Bess. La falta de paralelización es el punto débil de los motores de resolución Datalog existentes, por lo que nuestra aproximación podrı́a tener impacto en ese aspecto. Apéndice A DATALOG SOLVE API La versión actual de DATALOG SOLVE se ofrece como una aplicación autónoma para resolver programas Datalog. No obstante, como se pensó en su posible integración en un entorno de verificación como Cadp, se estructuró su funcionalidad de forma modular. De esta manera, DATALOG SOLVE puede verse como una librerı́a que ofrece la siguiente Api: CAESAR TYPE BOOLEAN CAESAR READ DATALOG 1( CAESAR TYPE SOLVE 1 *DATALOG BES, CAESAR TYPE BOOLEAN, SATISFABILITY MODE, CAESAR TYPE STRING DATALOG FILENAME); Lee el programa Datalog del fichero DATALOG FILENAME creando las estructuras en memoria que lo representan para su posterior evaluación. También se indica el tipo de evaluación que se deseará del programa Datalog mediante el parámetro SATISFABILITY MODE. Si éste último es igual a CAESAR TRUE, la resolución del programa Datalog parará nada más encontrar la primera solución posible a la consulta. Devuelve un puntero al Bes que representa el programa Datalog mediante el parámetro DATALOG BES. Devuelve CAESAR TRUE si se ha tenido éxito en la generación de la representación interna del programa Datalog, o CAESAR FALSE en caso contrario. CAESAR TYPE BOOLEAN CAESAR COMPUTE DATALOG 1( CAESAR TYPE SOLVE 1 DATALOG BES, CAESAR TYPE BOOLEAN *SOLUTION EXISTS); 66 APÉNDICE A. DATALOG SOLVE API Evalúa el programa Datalog representado por el Bes pasado por parámetro. Devuelve un valor igual a CAESAR TRUE mediante el parámetro *SOLUTION EXISTS si la resolución del Bes ha obtenido al menos una solución. Devuelve CAESAR TRUE si la resolución se ha producido sin problemas, o CAESAR FALSE si no se ha podido llevar a cabo por falta de memoria o el incumplimiento de algún invariante. CAESAR TYPE BOOLEAN CAESAR WRITE SOLUTION DATALOG 1( CAESAR TYPE SOLVE 1 DATALOG BES, CAESAR TYPE FILE FILE); Escribe los resultado de un programa Datalog en sus respectivos ficheros. Si la consulta a resolver es la que se hace por defecto el parámetro FILE habrá de ser igual a NULL. Si no, los resultados de la consulta se escribirán en el fichero cuyo manejador se pase mediante el parámetro FILE. void CAESAR WRITE BES DATALOG 1( CAESAR TYPE SOLVE 1 DATALOG BES, CAESAR TYPE FILE FILE); Escribe el Bes resultado de la evaluación del programa Datalog en el fichero cuyo manejador se pase mediante el parámetro FILE. void CAESAR WRITE STATISTICS DATALOG 1( CAESAR TYPE SOLVE 1 DATALOG BES, CAESAR TYPE FILE FILE); Esta función ofrece estadı́sticas sobre la resolución del Bes considerado mediante el parámetro DATALOG BES. Bibliografı́a [BJP05] F. Besson, T. Jensen, and D. Pichardie. A pcc architecture based on certified abstract interpretation. RR 5751, INRIA Rennes, November 2005. I.1 [CPvW07] T. Chen, B. Ploeger, J. van de Pol, and T. A. C. Willemse. Equivalence Checking for Infinite Systems Using Parameterized Boolean Equation Systems. In Proc. 18th Int’l Conf. on Concurrency Theory CONCUR’07, volume 4703, pages 120– 135. Springer LNCS, 2007. C.1 [DPW08] A. van Dam, B. Ploeger, and T.A.C. Willemse. Instantiation for Parameterised Boolean Equation Systems. In Proc. 5th Int’l Colloquium on Theoretical Aspects of Computing ICTAC’08. Springer LNCS, 2008. 2.2 [HHI+ 01] Joseph Y. Halpern, Robert Harper, Neil Immerman, Phokion G. Kolaitis, Moshe Y. Vardi, and Victor Vianu. On the unusual effectiveness of logic in computer science, 2001. I.1 [JV05] Dale W. Jorgenson and Khuong Vu. Information technology and the world economy. Scandinavian Journal of Economics, 107:631–650, Dec 2005. I.1 [LS98] X. Liu and S. A. Smolka. Simple Linear-Time Algorithms for Minimal Fixed Points. In Proc. 25th Int’l Colloquium on Automata, Languages, and Programming ICALP’98, volume 1443, pages 53–66. Springer LNCS, 1998. C.1 [Mat98] R. Mateescu. Local Model-Checking of an Alternation-Free Value-Based Modal Mu-Calculus. In Proc. 2nd Int’l Workshop on Verication, Model Checking and Abstract Interpretation VMCAI’98, 1998. 1.3, 2.2, 2.2, C.1 [MT08] R. Mateescu and D. Thivolle. A Model Checking Language for Concurrent ValuePassing Systems. In Proc. 15th Int’l Symp. on Formal Methods FM’08, volume 5014. Springer LNCS, 2008. C.1 68 [MW85] APÉNDICE . BIBLIOGRAFÍA Z. Manna and R. Waldinger. The Logical Basis for Computer Programming. Addison-Wesley, 1985. I.1 [NL96] G. C. Necula and P. Lee. Safe kernel extensions without runtime checking. In Proc. 2nd USENIX Symp. OSDI’96, pages 229 – 243. ACM Press, 1996. I.1 [NR01] G.C. Necula and S.P. Rahul. Oracle-based checking of untrusted software. In Proc. 28th ACM SIGPLAN-SIGACT Annual Symp. POPL 2001, pages 142 – 154, New York, NY, USA, 2001. ACM Press. I.1 [Rep94] T. W. Reps. Solving Demand Versions of Interprocedural Analysis Problems. In Proc. 5th Int’l Conf. on Compiler Construction CC’94, volume 786, pages 389– 403. Springer LNCS, 1994. C.1 [Ull89] J. D. Ullman. Principles of Database and Knowledge-Base Systems, Volume I and II, The New Technologies. Computer Science Press, 1989. C.1 [Vie86] L. Vieille. Recursive Axioms in Deductive Databases: The Query/Subquery Approach. In Proc. 1st Int’l Conf. on Expert Database Systems EDS’86, pages 253– 267, 1986. 2.2.1 [VT06] Jordi Vilaseca i Requena and Joan Torrent i Sellens. Tic, conocimiento y crecimiento economico: Un análisis empı́rico, agregado e internacional sobre las fuentes de la productividad. Economı́a Industrial, 360:41–60, 2006. I.1 [WACL05] J. Whaley, D. Avots, M. Carbin, and M. S. Lam. Using Datalog with Binary Decision Diagrams for Program Analysis. In Proc. Third Asian Symp. on Programming Languages and Systems APLAS’05, volume 3780, pages 97–118. Springer LNCS, 2005. I.1, 2, C.1, C.2 [ZR08] X. Zheng and R. Rugina. Demand-driven alias analysis for C. In Proc. 35th ACM SIGPLAN-SIGACT Symp. on Principles of Programming Languages POPL’08, pages 197–208. ACM Press, 2008. C.1