La descarga está en progreso. Por favor, espere

La descarga está en progreso. Por favor, espere

1 Agregando primitivas 2 Introducción Ahora presentaremos cómo agregar operaciones primitivas a la máquina G. Entenderemos como operaciones primitivas.

Presentaciones similares


Presentación del tema: "1 Agregando primitivas 2 Introducción Ahora presentaremos cómo agregar operaciones primitivas a la máquina G. Entenderemos como operaciones primitivas."— Transcripción de la presentación:

1

2 1 Agregando primitivas

3 2 Introducción Ahora presentaremos cómo agregar operaciones primitivas a la máquina G. Entenderemos como operaciones primitivas a las aritméticas y a los operadores relacionales. Usaremos la operación de suma como ejemplo paradigmático. Introduciremos entonces una instrucción Add, la que suma dos números que residen en el heap, y a su vez retorna el resultado en un nuevo nodo del mismo.

4 3 La instrucción Add Las direcciones de los dos argumentos se encontrarán en el tope del stack, y es a su vez en el tope donde quedará almacenada la dirección del nodo resultante. La instrucción tiene la siguiente regla de transición: Add : i a 0 : a 1 : s h[a 0 : NNum n 0, a 1 : NNum n 1 ] m => i a : s h[a : NNum (n 0 + n 1 )] m Es correcta esta especificación de Add ? La regla es aplicable solamente cuando los elementos en el tope del stack se encuentran en WHNF.

5 4 La instrucción Add (2) Como estamos implementando una máquina perezosa no es correcto asumir que los argumentos de una operación primitiva están en WHNF. En el algoritmo de reducción especificado anteriormente el proceso efectuaba este chequeo antes de evaluar un operador, y en el caso de que los argumentos no estuvieran normalizados se invocaba recursivamente al evaluador. En G la idea es mantener las instrucciones lo más simples posibles, por lo tanto usaremos la instrucción Add sólo cuando estemos seguros que los argumentos están efectivamente en WHNF.

6 5 Eval Lo que haremos es aumentar el conjunto de instrucciones de la máquina con la instrucción Eval. Esta instrucción satisface los siguiente requerimientos: Asumamos que nos encontramos en el estado Eval : i a : s h m Cuando sea que comience la ejecución de la secuencia de instrucciones i, el estado será i a : s h´ m y el item en el tope del stack se encontrará en WHNF. Será posible también que Eval no termine, ésto podrá suceder si el nodo en el heap apuntado desde el tope del stack no tiene WHNF.

7 6 Eval (2) Si el nodo cuya dirección está en el tope del stack ya está en WHNF, entonces Eval no hace nada. Si es posible efectuar una reducción, entonces la acción de Eval es efectuar una reducción a WHNF. Si esta llamada a Eval termina, entonces la ejecución continúa con el heap modificado como único resultado. Esto es muy similar a la estructura de llamado a subrutinas y retorno que se usa tradicionalmente en la implementación de lenguajes de programación. Recordamos que la solución clásica para implementar este comportamiento es usar un stack. En el stack se salvará la información necesaria para poder continuar la ejecución una vez que la subrutina invocada termina de ejecutarse.

8 7 Dump En la versión Mark 4 de la máquina G el stack previamente mencionado será llamado Dump, y es un stack de pares, cuyo primer componente es una secuencia de instrucciones, y el segundo componente un stack (de G). type GmState = (GmCode, GmStack, GmDump, GmHeap, GmGlobals) type GmDump = [GmDumpItem] type GmDumpItem = (GmCode, GmStack)

9 8 Instrucciones de Mark 4 data Instruction = Slide Int | Alloc Int | Update Int | Pop Int | Unwind | Pushglobal Name | Pushint Int | Push Int | Mkap | Eval | Add | Sub | Mul | Div | Neg | Eq | Ne | Lt | Le | Gt | Ge | Cond GmCode GmCode

10 9 Nuevas transiciones Instrucciones del evaluador. Las instrucciones que manipulan el dump son pocas. Por un lado Eval, que crea un nuevo item del dump cada vez que el nodo que está en el tope del stack no está WHNF. También tendremos que introducir una modificación para la instrucción Unwind, la que tendrá que popear un item del dump cuando la evaluación es terminada. Cuando la expresión al tope del stack está en WHNF, Unwind puede recomponer el viejo contexto usando el dump, alojando la dirección al tope del stack en el tope del stack previo, que es recuperado.

11 10 Instrucciones del evaluador Esto último se ve claramente en la siguiente regla de transición: [Unwind] a : s : d h[a : NNum n] m => i a : s d h m La expresión con dirección a está en WHNF porque es un entero, en- tonces la vieja secuencia i´ de instrucciones es retomada y el stack pa- sa a ser el viejo stack pero con a en el tope. Esta regla se aplica sólo cuando el dump no es vacío, si no la máquina ha terminado de evaluar. Todas las restantes transiciones para Unwind quedan igual a como fueron definidas en Mark 3 (excepto que ahora también tendrán el com- ponente dump). Ahora podemos especificar la regla de transición para Eval.

12 11 Instrucciones del evaluador (2) La instrucción Eval salva el resto del stack ( s ) y el resto de las instrucciones ( i ) como un item del dump. La nueva secuencia de instrucciones es un unwinding y el nuevo stack contiene solamente la dirección a. Eval : i a : s d h m => [Unwind] [a] : d h m

13 12 Instrucciones aritméticas La ejecución de cualquiera de los operadores binarios aritméticos puede especificarse por medio de una regla de transición de la siguiente forma: op : i a 0 : a 1 : s d h[a 0 : NNum n 0, a 1 : NNum n 1 ] m => i a : s d h[a : NNum (n 0 op n 1 )] m La instrucción Neg, por otro lado, niega el número que se encuentra en el tope del stack, por lo que tiene la siguiente regla de transición: Neg : i a : s d h[a : NNum n] m => i a : s d h[a : NNum (- n)] m

14 13 Comparación Los operadores de comparación también pueden especificarse con una regla de transición genérica: op : i a 0 : a 1 : s d h[a 0 : NNum n 0, a 1 : NNum n 1 ] m => i a : s d h[a : NNum (n 0 op n 1 )] m Notar que esta regla es similar a la de los operadores aritméticos. La diferencia está en que la operación ==, por ejemplo, retorna un booleano y no un entero. Para arreglar esto asignaremos valores enteros a las constantes True (1) y False (0).

15 14 La instrucción Cond Finalmente, para compilar la instrucción if implementaremos la instrucción Cond, con las siguientes reglas de transición Cond i 1 i 2 : i a : s d h[a : NNum 1] m => i 1 ++ i s d h m Cond i 1 i 2 : i a : s d h[a : NNum 0] m => i 2 ++ i s d h m

16 15 El compilador Introduciremos los menores cambios posibles al compilador para poder usar las instrucciones aritméticas, relacionales y condicionales que hemos agregado. La forma más simple de extender el compilador es agregar código-G para cada una de las nuevas funciones primitivas: compiledPrimitives :: [GmCompiledSC] compiledPrimitives = [(+, 2, [Push 1, Eval, Push 1, Eval, Add, Update 2, Pop 2, Unwind]),... (negate, 1, Push 0, Eval, Neg, Update 1, Pop 1, Unwind]),... (= =, 2, [Push 1, Eval, Push 1, Eval, Eq, Update 2, Pop 2, Unwind]),... (if, 3, [Push 0, Eval, Cond [Push 1] [Push 2], Update 2, Pop 2, Unwind])]

17 16 Un mejor manejo de aritmética La forma en que G es implementada fuerza a que cada operador primitivo es invocado usando una de las primitivas compiladas. Podemos mejorar esta situación, teniendo en cuenta que a menudo podemos invocar al operador primitivo directamente. Consideremos por ejempo el siguiente programa: main = 3 + 4 * 5 Al ser compilado genera el siguiente código [Pushint 5, Pushint 4, Pushglobal *, Mkap, Mkap, Pushint 3, Pushglobal +, Mkap, Mkap, Eval] (33 pasos, 11 nodos) Uno podría pensar en usar directamente las instrucciones Add y Mul en lugar de invocar a la funciones + y *, generando el siguiente código: [Pushint 5, Pushint 4, Mul, Pushint 3, Add] (5 pasos, 5 nodos)

18 17 Un problema Un posible problema surge al considerar el siguiente ejemplo: main = K 1 (1/0) que genera el siguiente código: [Pushint 0, Pushint 1, Pushglobal /, Mkap, Mkap, Pushint 1, Pushglobal K, Mkap, Mkap, Eval] Si seguimos el pattern del ejemplo anterior podríamos tratar de generar el código: [Pushint 0, Pushint 1, Div, Pushint 1, Pushglobal K, Mkap, Mkap, Eval] El problema es que división por 0 es aplicada antes que K, un compilador correcto debiera generar código sin este tipo de errores. Lo que pasa es que nuestro código es demasiado estricto. Un problema similar ocurre con expresiones que no terminan y son inadvertidamente evaluadas.

19 18 La solución La solución al problema anterior es controlar el contexto en que una expresión ocurre. Distinguiremos dos contextos: Estricto: el valor de la expresión es requerido en WHNF. Perezoso: el valor de la expresión podría o no ser requerido en WHNF. Para cada contexto tendremos un esquema de compilación correspondiente. En el contexto estricto el esquema de compilación será el esquema E. En el contexto perezoso usaremos el esquema C. El objetivo es tratar de encontrar tantos contextos estrictos como sea posible, ya que el código generado es más eficiente.

20 19 La solución (2) Observación: cuando sea que se quiera instanciar un supercombinador es porque interesa reducir su valor a WHNF. Entonces un SC puede ser siempre evaluado en un contexto estricto. Hay también algunas expresiones en las que sabemos que algunas sub-expresiones serán evaluadas a WHNF si la expresión lo es. La clase de expresiones en contextos estrictos (CE) puede ser descripta recursivamente:

21 20 Expresiones en contextos estrictos La expresión en el cuerpo de un SC está en un CE. If e 0 op e 1 ocurre en un CE, donde op es un operador aritmético o de comparación, entonces las expresiones e 0 y e 1 están también en un CE. Si negate e ocurre en un CE, también lo hace e. Si la expresión if e 0 e 1 e 2 ocurre en un CE, también lo hacen las expresiones e 0, e 1 y e 2. Si let(rec) D in e ocurre en un CE entonces la expresión e también lo está Veamos un ejemplo:

22 21 Expresiones en contextos estrictos (2) f x y = (x + y) + g (x * y) Las expresiones (x + y) y g (x * y) serán evaluadas en CE (porque el cuerpo de f lo es). El operador + propaga el CE a x e y. En la segunda expresión la presencia de un SC definido por el usuario implica que la sub-expresión x * y será compilada asumiendo que la expresión podría no ser evaluada.. El compilador E será definido recursivamente. Como el cuerpo de cada SC es evaluado en un CE necesitamos invocar al esquema E desde el esquema R. Esto satisface el primer requerimiento. Para propagar la información de contexto E será recursivamente invocado.

23 22 El esquema de compilación E E [ e ] compila código que evalúa una expresión e a WHNF en el environment, dejando un puntero a la expresión en el tope del stack. E [ i ] = [Pushint i] E [ let x 1 = e 1 ;... ; x n = e n in e ] = C [ e 1 ] ++... ++ C [ e n ] n ++ E [ e ] ++ [Slide n] = n [ x 1 |-> n-1,..., x n |-> 0] E [ let rec x 1 = e 1 ;... ; x n = e n in e ] = [Alloc n] ++ C [ e 1 ] ++ [Update n-1] ++... ++ C [ e n ] ++ [Update 0] E [ e ] ++ [Slide n] = n [ x 1 |-> n-1,..., x n |-> 0]

24 23 El esquema de compilación E (2) E [ e 0 + e 1 ] = E [ e 1 ] ++ E [ e 0 ] +1 ++ [Add] y similarmente para todos los operadores primitivos binarios E [ negate e] = E [ e ] ++ [Neg] E [ if e 0 e 1 e 2 ] = E [ e 0 ] ++ [Cond (E [ e 1 ] ) (E [ e 2 ] )] E [ e ] = C [ e ] ++ [ Eval ] otherwise El esquema R ahora se define como sigue: R [ e ] n = E [ e ] ++ [Update n, Pop n, Unwind]

25 24 Modificación de Unwind La máquina podría fallar al usar el nuevo compilador. Es entonces necesario introducir una nueva regla de transición para la instrucción Unwind : [Unwind] [a 0,..., a k ] : d h[a 0 : NGlobal n c] m => i a k : s d h m si k < m Esta nueva definición nos permite usar Eval para evaluar cualquier objeto a WHNF y no solamente números.

26 25 Nota La forma en que se implementó el contexto estricto en el compilador es simplemente una aplicación de la noción de herencia de atributos tratada en teoría de la compilación. No es posible en general determinar en tiempo de compilación si una expresión debe ser compilada en un contexto estricto o no. Acá se asumió un compromiso, tratando un conjunto limitado de expresiones - expresiones aritméticas - en forma especial.


Descargar ppt "1 Agregando primitivas 2 Introducción Ahora presentaremos cómo agregar operaciones primitivas a la máquina G. Entenderemos como operaciones primitivas."

Presentaciones similares


Anuncios Google