Universidad Simón Bolívar Departamento de Computación y Tecnología de la Información CI3641 – Lenguajes de Programación I Septiembre-Diciembre 2008 Carnet: Nombre: Examen parcial I (40 puntos) Antes de empezar, revise bien el examen. Pregunta 0 Pregunta 1 Total 30 puntos 10 puntos 40 puntos Pregunta 0 - 30 puntos Esta pregunta consta de quince (15) subpreguntas de selección, numeradas de 0.0 a 0.14. Cada una de las subpreguntas viene acompañada de cuatro posibles respuestas (a,b,c,d ), entre las cuales sólo una es correcta. Ud. deberá marcar la opción que considere correcta o, si desea no contestar la subpregunta, marcar la última opción (e) que indica que Ud. prefiere omitir la respuesta. Cada una de las quince (15) subpreguntas tiene un valor de dos (2) puntos. Tres subpreguntas malas eliminan una buena. Las subpreguntas omitidas (marcadas en la opción (e)) no suman ni restan puntos. ◦ 0.0. Suponga que estamos trabajando con uno de los dialectos de Fortran pre-90, en los que no se permite que las subrutinas sean recursivas, ni directa ni indirectamente. Sabemos entonces que la reserva de espacio para las variables locales de una subrutina puede ser realizada tanto estáticamente como en pila. Suponga que contamos con el grafo de llamadas potenciales de un programa escrito en este dialecto de Fortran pre-90. Este grafo tiene como nodos a las subrutinas del programa, incluyendo el programa principal, y como aristas dirigidas (P, Q) aquéllas en las que el texto de la subrutina origen P contiene una llamada a la subrutina destino Q. Suponga además que los nodos de este grafo están decorados con la cantidad de espacio requerida por cada subrutina para sus variables locales. En el caso de que la reserva de espacio local para las subrutinas sea realizada estáticamente, indique qué característica del grafo de llamadas potenciales puede ser utilizada para determinar la cantidad mínima de espacio total requerida por el programa para sus variables: a) La sumatoria del peso de todos los nodos del grafo. b) El peso mínimo de todos los caminos que empiezan en el programa principal, entendiendo por peso de un camino la sumatoria de los pesos de sus nodos. c) El peso máximo de todos los caminos que empiezan en el programa principal, entendiendo por peso de un camino la sumatoria de los pesos de sus nodos. d ) El peso del nodo correspondiente al programa principal. e) No sabe / No contesta. 0.1. Continuando con la pregunta 0.0 sobre Fortran pre-90, en el caso de que la reserva de espacio local para las subrutinas sea realizada en pila, indique qué característica del grafo de llamadas potenciales corresponde a la cantidad mínima de espacio total que podría utilizar el programa para sus variables: a) La sumatoria del peso de todos los nodos del grafo. b) El peso mínimo de todos los caminos que empiezan en el programa principal, entendiendo por peso de un camino la sumatoria de los pesos de sus nodos. c) El peso máximo de todos los caminos que empiezan en el programa principal, entendiendo por peso de un camino la sumatoria de los pesos de sus nodos. d ) El peso del nodo correspondiente al programa principal. e) No sabe / No contesta. 0.2. Continuando con la pregunta 0.0 sobre Fortran pre-90, en el caso de que la reserva de espacio local para las subrutinas sea realizada en pila, indique qué característica del grafo de llamadas potenciales corresponde a la cantidad máxima de espacio total que podría utilizar el programa para sus variables: a) La sumatoria del peso de todos los nodos del grafo. b) El peso mínimo de todos los caminos que empiezan en el programa principal, entendiendo por peso de un camino la sumatoria de los pesos de sus nodos. c) El peso máximo de todos los caminos que empiezan en el programa principal, entendiendo por peso de un camino la sumatoria de los pesos de sus nodos. d ) El peso del nodo correspondiente al programa principal. e) No sabe / No contesta. 0.3. Continuando con la pregunta 0.0 sobre Fortran pre-90, la ventaja de realizar la reserva de espacio local para las subrutinas estáticamente en lugar de en pila es: a) Mejor eficiencia en cuanto a tiempo de ejecución de los programas escritos en el lenguaje, aunque podría utilizarse más espacio para las variables. b) Mejor eficiencia en cuanto a espacio utilizado durante la ejecución de los programas escritos en el lenguaje. c) Mejor eficiencia tanto de tiempo de ejecución como de espacio utilizado para las variables de los programas escritos en el lenguaje. d ) Mejor eficiencia de compilación de los programas escritos en el lenguaje. e) No sabe / No contesta. 0.4. Considere el siguiente programa escrito en pseudocódigo: var x: integer; /* variable global */ procedure set_x (n: integer) x := n endp procedure print_x print (x) endp procedure foo (S, P: procedure; n: integer) var x: integer := 7; if (n = 3) set_x (n) else S (n) endif; if (n = 3) P else print_x endif endp begin /* programa principal */ set_x (1); foo (set_x, print_x, 3); print_x; set_x (1); foo (set_x, print_x, 5); print_x end Suponga que el pseudolenguaje utilizado maneja alcance dinámico. Indique entonces cuál es la salida de este programa en el caso de que la asociación (binding) de ambientes no-locales de subprogramas pasados como parámetro sea superficial (shallow ): a) 3 3 5 5. b) 7 3 1 1. c) 3 1 5 1. d ) 1 1 7 5. e) No sabe / No contesta. 0.5. Continuando con la pregunta 0.4, continúe suponiendo que el pseudolenguaje utilizado maneja alcance dinámico, e indique ahora cuál sería la salida del programa en el caso de que la asociación (binding) de ambientes no-locales de subprogramas pasados como parámetro sea profunda (deep): a) 3 3 5 5. b) 7 3 1 1. c) 3 1 5 1. d ) 1 1 7 5. e) No sabe / No contesta. 0.6. La definición del lenguaje Java establece que el orden de evaluación de los operandos de un operador binario siempre debe iniciarse con el operando izquierdo, lo cual incluye evaluar todos sus efectos secundarios o de borde (side effects), y luego continuar con el operando derecho. Esto incluye el caso particular del operador de asignación “=” como operador binario. (Nota: La afirmación general del párrafo anterior es un poco simplista, pues no considera ciertos detalles de algunos casos particulares de Java. A los efectos de esta pregunta, esta simplificación no nos afecta.) Por otra parte, aunque la definición de Java también establece que la mayoría de sus operadores binarios asocian a la izquierda, en el caso del operador binario de asignación se establece que éste asocia a la derecha. En relación con esto, considere el siguiente programa en Java: class C { public static void main(String[] args) { int a[] = new int[5]; int i; for (int k = 0; k < 5; k++) a[k] = 10 * k + 7; i = 1; a[i++] = a[++i] = i++; } } Indique cuál es el estado final de la variable i: a) 2. b) 3. c) 4. d ) 5. e) No sabe / No contesta. 0.7. Continuando con la pregunta anterior 0.6, indique cuál es el estado final del arreglo a: a) 7 17 27 37 47. b) 7 3 3 37 47. c) 7 4 27 4 47. d ) 7 3 27 3 47. e) No sabe / No contesta. 0.8. Continuando con la pregunta anterior 0.6, algunas personas podrían señalar que luce como una aparente contradicción el que el operador de asignación asocie a la derecha mientras el orden de evaluación de sus operandos empieza por la izquierda. ¿Qué se puede decir de tal afirmación? a) No hay contradicción alguna. b) La asociación hacia la derecha es una excepción de la regla de evaluación empezando por la izquierda. c) La asignación debería asociar a la izquierda. d ) No tiene sentido que el orden de evaluación de los operandos de la asignación empiece por la izquierda; debería empezar por la derecha. e) No sabe / No contesta. 0.9. La ventaja de contar con una instrucción de selección condicional del estilo switch/case es que éstas se pueden implementar mediante tablas de salto en lugar de la correspondiente secuencia de if anidados. Sobre estas tablas de salto, podemos decir que: a) Siempre se puede y conviene que se implementen como tablas lineales/directas de salto. b) Conviene que se implementen como tablas lineales/directas de salto cuando el conjunto de etiquetas de la instrucción es denso y no contiene grandes rangos. c) Conviene que se implementen como tablas de hash cuando el conjunto de etiquetas de la instrucción contiene grandes rangos. d ) Conviene que se implementen mediante búsqueda binaria únicamente cuando el conjunto de etiquetas de la instrucción es muy pequeño. e) No sabe / No contesta. 0.10. Considere los iteradores del lenguaje Clu. A continuación presentamos un iterador X construido en pseudo-Clu, esto es, pseudocódigo similar al del lenguaje Clu en el que, para evitar las complicaciones de las estructuras de datos provistas por Clu, utilizaremos la sencilla versión de tipos recursivos provistos por el lenguaje Haskell: data Arbol = hoja Int | nodo (Arbol,Arbol) X = iter (a: Arbol) yields (Arbol) if (a is nodo(izq,der)) { for b in X(izq) yield b endfor; yield a; for b in X(der) yield b endfor } else yield a endif enditer En el cuerpo de X, se usa el predicado a is nodo(izq,der) para determinar si el árbol a fue construido como nodo y, en caso afirmativo, asignar a izq y der las componentes de tal nodo. Considere ahora la iteración for z: Arbol in X (nodo (hoja 3, nodo (hoja 5, hoja 7))) do ... endfor ¿Cuál es el primer árbol procesado por esta iteración? a) hoja 3. b) hoja 5. c) nodo (hoja 5, hoja 7). d ) Ninguno de los anteriores. e) No sabe / No contesta. 0.11. Continuando con la pregunta anterior 0.10, ¿cuál es el segundo árbol procesado por la iteración dada? a) hoja 3. b) hoja 5. c) nodo (hoja 5, hoja 7). d ) Ninguno de los anteriores. e) No sabe / No contesta. 0.12. Continuando con la pregunta anterior 0.10, ¿cuál es el tercer árbol procesado por la iteración dada? a) hoja 3. b) hoja 5. c) nodo (hoja 5, hoja 7). d ) Ninguno de los anteriores. e) No sabe / No contesta. 0.13. Para manejar el problema de posible no-inicialización de variables, Scott en el libro de texto considera, entre otras posibilidades, el uso de la estrategia de “definite assignment” y el uso de verificaciones dinámicas. Java y algunos otros lenguajes utilizan la estrategia de “definite assignment”, que podría traducirse como “inicialización definitiva” o “inicialización estáticamente clara”. Bajo esta estrategia se verifica estáticamente que las variables sean inicializadas bajo todo posible flujo del control de la ejecución. Por otra parte, bajo la estrategia de verificaciones dinámicas (dynamic checks) se analiza la posibilidad de no-inicialización durante la ejecución. Comparando estas dos alternativas, la ventaja de la inicialización definitiva sobre la verificación dinámica es: a) Eficiencia de ejecución de los programas escritos en el lenguaje. b) Eficiencia de mantenimiento de los programas escritos en el lenguaje. c) Portabilidad de los programas escritos en el lenguaje. d ) El hecho de que hay programas sin error de no-inicialización de variables que son rechazados bajo la estrategia de inicialización definitiva, mientras sí pueden ser procesados exitosamente bajo la estrategia de verificación dinámica. e) No sabe / No contesta. 0.14. Continuando con la pregunta anterior 0.13, la ventaja de la verificación dinámica sobre la inicialización definitiva es: a) Eficiencia de ejecución de los programas escritos en el lenguaje. b) Eficiencia de mantenimiento de los programas escritos en el lenguaje. c) Portabilidad de los programas escritos en el lenguaje. d ) El hecho de que hay programas sin error de no-inicialización de variables que son rechazados bajo la estrategia de inicialización definitiva, mientras sí pueden ser procesados exitosamente bajo la estrategia de verificación dinámica. e) No sabe / No contesta. ◦ Pregunta 1 - 10 puntos Se desea que Ud. construya, utilizando el lenguaje Haskell, un pequeño fragmento de un compilador de un lenguaje L que verifica “definite assignment”, esto es, “inicialización definitiva”. Tal como fue descrito en la pregunta 0.13, bajo esta estrategia se verifica estáticamente que las variables sean inicializadas bajo todo posible flujo del control de la ejecución. Para construir el pequeño fragmento deseado, suponemos que ya el compilador analizó sintácticamente el programa de entrada escrito en L, y tiene reflejada toda la información de éste en una estructura del tipo Instr siguiente: data Instr = | | | asign String Expr sec [Instr] cond Expr Instr Instr iter Expr Instr data Expr = ... Así, el tipo Instr corresponde a una estructura de árbol de instrucciones, mientras el otro tipo mencionado Expr permite reflejar la información de una expresión. En Instr, el constructor asign corresponde a asignación (cuyo lado izquierdo acepta sólo identificadores de variables simples, razón por la cual se usa el tipo String), el constructor sec corresponde a secuenciación de una lista de instrucciones, el constructor cond corresponde a un típico condicional if/else, y por último el constructor iter corresponde a una típica iteración while. Por claridad, asumiremos que el lenguaje L hace una clara distinción entre instrucciones y expresiones, y no permite híbridos instrucción/expresión como lo hacen otros lenguajes (de la forma de instrucciones con valor o expresiones con efectos de borde). Por otra parte, el tipo Expr abarca toda clase de expresiones, sean éstas booleanas, enteras, o de cualquier otro tipo. Con esta infraestructura, el fragmento de compilador de L que Ud. debe construir en Haskell es una función inicializada :: String -> Instr -> Resultado que determina si el identificador de variable correspondiente a su primer argumento fue “definitivamente” (“definitely”) inicializado en la instrucción/programa correspondiente a su segundo argumento. El resultado será del tipo data Resultado = Bien | Mal | Neutro donde Bien significa que la variable fue bien inicializada, Mal significa que la variable fue utilizada sin estar “definitivamente” (“definitely”) inicializada, y Neutro significa que la variable no fue inicializada adecuadamente pero tampoco se intentó utilizar su valor. Asuma que: (i) dispone de una función ocurre :: String ->Expr ->Bool que permite determinar si un identificador de variable es utilizado en una expresión, (ii) ya se realizaron todas las verificaciones de declaración de identificadores y no hay problemas de tal naturaleza, y (iii) también fueron realizadas ya todas las verificaciones de tipo sin haber encontrado errores. Por último, si así Ud. lo requiere, puede construir funciones auxiliares que le permitan construir su función inicializada. Nota: Sólo en caso de que Ud. haya aprobado en algún trimestre anterior al actual el Laboratorio de Lenguajes de Programación I, puede Ud. opcionalmente responder a esta pregunta utilizando Java en lugar de Haskell, según Ud. prefiera. (Espacio adicional)