DATALOG SOLVE Un evaluador de consultas Datalog y su

Anuncio
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
Descargar