Estoy planeando enseñar un curso de invierno sobre un número variable de temas, uno de los cuales será compiladores. Ahora, me encontré con este problema mientras pensaba en tareas para dar durante el trimestre, pero me tiene perplejo, por lo que podría usarlo como un ejemplo.
public class DeadCode {
public static void main(String[] args) {
return;
System.out.println("This line won't print.");
}
}
En el programa anterior, es obvio que la declaración de impresión nunca se ejecutará debido a return
. Los compiladores a veces dan advertencias o errores sobre el código muerto. Por ejemplo, el código anterior no se compilará en Java. Sin embargo, el compilador javac no detectará todas las instancias de código muerto en cada programa. ¿Cómo probaría que ningún compilador puede hacerlo?
BigInteger i = 0; while(isCollatzConjectureTrueFor(i)) i++; printf("Hello world\n");
Respuestas:
Todo proviene de la indecidibilidad del problema de detención. Supongamos que tenemos una función de código muerto "perfecto", algo de Turing Machine M y algo de cadena de entrada x, y un procedimiento que se parece a esto:
Si M se ejecuta para siempre, entonces eliminamos la declaración de impresión, ya que nunca la alcanzaremos. Si M no se ejecuta para siempre, entonces debemos mantener la declaración de impresión. Por lo tanto, si tenemos un eliminador de código muerto, también nos permite resolver el problema de detención, por lo que sabemos que no puede haber tal eliminador de código muerto.
La forma de evitar esto es mediante una "aproximación conservadora". Entonces, en mi ejemplo anterior de Turing Machine, podemos suponer que la ejecución de M en x podría terminar, por lo que jugamos a lo seguro y no eliminamos la declaración de impresión. En su ejemplo, sabemos que no importa qué funciones se detengan o no, no hay forma de llegar a esa declaración impresa.
Por lo general, esto se realiza mediante la construcción de un "gráfico de control de flujo". Hacemos suposiciones simplificadoras, como "el final de un ciclo while está conectado al principio y la declaración posterior", incluso si se ejecuta para siempre o se ejecuta solo una vez y no visita ambos. Del mismo modo, suponemos que una declaración if puede alcanzar todas sus ramas, incluso si en realidad algunas nunca se usan. Este tipo de simplificaciones nos permiten eliminar el "código obviamente muerto" como el ejemplo que da, sin dejar de ser decidible.
Para aclarar algunas confusiones de los comentarios:
Como dice Raphael, en mi ejemplo, consideramos la máquina de Turing como una entrada. La idea es que, si tuviéramos un algoritmo DCE perfecto, podríamos construir el fragmento de código que doy para cualquier máquina de Turing , y tener un DCE resolvería el problema de detención.
Para el problema que plantea njzk2: tiene toda la razón, en este caso puede determinar que no hay forma de que se llegue a una declaración después de la devolución. Esto se debe a que es lo suficientemente simple como para que podamos describir su inalcanzabilidad utilizando restricciones de gráficos de flujo de control (es decir, no hay bordes salientes de una declaración de retorno). Pero no hay un eliminador de código muerto perfecto, que elimina todo el código no utilizado.
Para TomášZato: no es realmente una prueba dependiente de entrada. Por el contrario, interpretarlo como un "forall". Funciona de la siguiente manera: supongamos que tenemos un algoritmo DCE perfecto. Si me das una Turing Machine M arbitraria e ingresas x, puedo usar mi algoritmo DCE para determinar si M se detiene, al construir el fragmento de código anterior y ver si la declaración de impresión se elimina. Esta técnica, de dejar un parámetro arbitrario para probar una declaración general, es común en matemáticas y lógica.
No entiendo completamente el punto de TomášZato sobre que el código es finito. Seguramente el código es finito, pero un algoritmo DCE perfecto debe aplicarse a todo el código, que es un conjunto infinito. Del mismo modo, aunque el código en sí es finito, los conjuntos potenciales de entrada son infinitos, al igual que el tiempo de ejecución potencial del código.
En cuanto a considerar la rama final no muerta: es segura en términos de la "aproximación conservadora" de la que hablo, pero no es suficiente para detectar todas las instancias de código muerto como lo solicita el OP.
Considere un código como este:
Claramente podemos eliminar
print "goodbye"
sin cambiar el comportamiento del programa. Por lo tanto, es un código muerto. Pero si hay una llamada a una función diferente en lugar de(true)
lawhile
condición, entonces no sabemos si podemos eliminarla o no, lo que lleva a la indecidibilidad.Tenga en cuenta que no voy a pensar en esto por mi cuenta. Es un resultado bien conocido en la teoría de los compiladores. Se discute en The Tiger Book . (Es posible que pueda ver de dónde hablan en los libros de Google .
fuente
Este es un giro en la respuesta de jmite que evita la posible confusión sobre la no terminación. Daré un programa que siempre se detiene, puede tener un código muerto pero no podemos (siempre) algorítmicamente decidir si lo tiene.
Considere la siguiente clase de entradas para el identificador de código muerto:
Desde
M
yx
son fijos,simulateMs
tiene un código muerto conreturn 0
if y only ifM
no se detienex
.Esto nos da inmediatamente una reducción del problema de detención a la verificación de código muerto: dado TM como instancia de problema de detención, cree el programa anterior con el código de : tiene código muerto si y solo si no se detiene por sí solo código.M MM M M
x
Por lo tanto, la verificación de código muerto no es computable.
En caso de que no esté familiarizado con la reducción como técnica de prueba en este contexto, le recomiendo nuestro material de referencia .
fuente
Una manera simple de demostrar este tipo de propiedad sin atascarse en detalles es usar el siguiente lema:
Lema: Para cualquier compilador C para un lenguaje completo de Turing, existe una función
undecidable_but_true()
que no toma argumentos y devuelve el verdadero booleano, de modo que C no puede predecir siundecidable_but_true()
devuelve verdadero o falso.Tenga en cuenta que la función depende del compilador. Dada una función
undecidable_but_true1()
, un compilador siempre se puede aumentar con el conocimiento de si esta función devuelve verdadero o falso; pero siempre hay alguna otra funciónundecidable_but_true2()
que no se cubrirá.Prueba: según el teorema de Rice , la propiedad "esta función devuelve verdadero" es indecidible. Por lo tanto, cualquier algoritmo de análisis estático no puede decidir esta propiedad para todas las funciones posibles.
Corolario: dado un compilador C, el siguiente programa contiene código muerto que no se puede detectar:
Una nota sobre Java: el lenguaje Java exige que los compiladores rechacen ciertos programas que contienen código inalcanzable, al tiempo que exige que el código se proporcione en todos los puntos accesibles (por ejemplo, el flujo de control en una función no vacía debe terminar con una
return
declaración). El lenguaje especifica exactamente cómo se realiza el análisis de código inalcanzable; Si no fuera así, sería imposible escribir programas portátiles. Dado un programa de la formaes necesario especificar en qué casos el código inalcanzable debe ser seguido por algún otro código y en qué casos no debe ser seguido por ningún código. Un ejemplo de un programa Java que contiene código que es inalcanzable, pero no de una manera que los compiladores de Java puedan notar, aparece en Java 101:
fuente
day_of_week
es inalcanzable.La respuesta de jmite se aplica a si el programa alguna vez saldrá de un cálculo, solo porque es infinito, no llamaría al código después de que esté muerto.
Sin embargo, hay otro enfoque: un problema para el cual hay una respuesta pero se desconoce:
Esta rutina, sin duda, no contiene código muerto - la función devolverá una respuesta que ejecuta un camino, pero no el otro. ¡Buena suerte para encontrarlo! Mi memoria es que ninguna computadora teórica puede resolver esto dentro de la vida útil del universo.
Con más detalle:
La
Evaluate()
función calcula qué lado gana un juego de ajedrez si ambos lados juegan perfectamente (con la máxima profundidad de búsqueda).Los evaluadores de ajedrez normalmente miran hacia adelante en cada movimiento posible a una profundidad específica y luego intentan anotar el tablero en ese punto (a veces expandir ciertas ramas más lejos como mirar a la mitad de un intercambio o similar puede producir una percepción muy sesgada). son 17695 movimientos a medias, la búsqueda es exhaustiva, atravesará todos los juegos de ajedrez posibles. Dado que todos los juegos terminan, no hay problema en tratar de decidir qué tan buena es la posición de cada tablero (y, por lo tanto, no hay razón para mirar la lógica de evaluación del tablero, nunca se llamará), el resultado es una victoria, una pérdida o un empate. Si el resultado es un empate, el juego es justo, si el resultado no es un empate, es un juego injusto. Para expandirlo un poco obtenemos:
Tenga en cuenta, además, que será prácticamente imposible para el compilador darse cuenta de que Chessboard.Score () es un código muerto. El conocimiento de las reglas del ajedrez nos permite a los humanos resolver esto, pero para saberlo, debes saber que MakeMove nunca puede aumentar el conteo de piezas y que Chessboard.Draw () volverá verdadero si el conteo de piezas permanece estático durante demasiado tiempo. .
Tenga en cuenta que la profundidad de búsqueda es en medio movimientos, no en movimientos completos. Esto es normal para este tipo de rutina de IA, ya que es una rutina O (x ^ n): agregar una capa de búsqueda más tiene un efecto importante sobre cuánto tiempo tarda en ejecutarse.
fuente
¡Creo que en un curso de informática, la noción de código muerto es interesante en el contexto de comprender la diferencia entre el tiempo de compilación y el tiempo de ejecución!
Un compilador puede determinar cuándo tiene un código que no se puede atravesar en ningún momento de tiempo de compilación, pero no puede hacerlo en tiempo de ejecución. un simple while-loop con entrada del usuario para la prueba de loop-break lo muestra
Si un compilador realmente puede determinar el código muerto de tiempo de ejecución (es decir, discernir que Turing está completo), entonces hay un argumento de que el código nunca necesita ejecutarse, ¡porque el trabajo ya está hecho!
Por lo menos, la existencia de código que pasa las comprobaciones de código muerto en tiempo de compilación ilustra la necesidad de una verificación pragmática de los límites en las entradas y la higiene general de la codificación (en el mundo real de los proyectos reales).
fuente