Crea tu propio compilador – Parte 11 – Probando el evaluador de expresiones

En el capítulo anterior explicamos el concepto de almacenamiento para los operandos, a la vez que mejoramos nuestro analizador de expresiones y el reconocimiento de operandos.

Además agregamos diversas rutinas para la generación de código para las operaciones de asignación y operaciones simples, pero no llegamos a probarlas.

En esta entrega, probaremos las asignaciones y algunas expresiones sencillas para comprobar que el proceso de generación de código es correcto. Esta será la primera vez, en este artículo, que generaremos código, verdaderamente ejecutable.

Procesamiento de expresiones

De acuerdo con nuestra implementación, las expresiones se procesan en el cuerpo del programa principal:

begin
...
  while EndOfFile<>1 do begin
    NextToken;
    writeln(srcToken);
    if srcToktyp = 0 then begin
      //Salto de línea, no se hace nada
    end else if srcToktyp = 1 then begin
      //Espacio, no se hace nada
    end else if srcToktyp = 5 then begin
      ExtractComment; //Comentario
    end else if srcToken = 'var' then begin
      //Es una declaración
      if InVarSec = 0 then begin
        //Estamos fuera de un bloque de variables
        WriteLn(outFile, '    .data');
        InVarSec := 1;  //Fija bandera
      end;
      //*** Aquí procesamos variables
      ParserVar;
    end else begin
      //Debe ser una instrucción. Aquí debe empezar la sección de código
      if InVarSec = 1 then begin
        //Estamos dentro de un bloque de variables
        WriteLn(outFile, '    .code');
        InVarSec := 0;  //Fija bandera
      end;
      if FirstCode=1 then begin
        //Primera instrucción
        WriteLn(outFile, '    .code');
        WriteLn(outFile, 'start:');
        FirstCode := 0;  //Activa bandera
      end;
      //**** Aquí procesamos instrucciones.
      //y ya no se deben permitir más declaraciones.
      if srcToktyp = 2 then begin
        //Es un identificador, puede ser una asignación
        FindVariable;
        if curVarName = '' then begin
          MsjError := 'Se esperaba variable: ' + srcToken;
          break;
        end;
        //Debe ser una asignación
        ProcessAssigment;
        if MsjError<>'' then break;
      end else begin
        MsjError := 'Instrucción desconocida: ' + srcToken;
        break;
      end;
    end;
  end;
...
end.

Las expresiones se procesan después de descartar los casos de líneas en blanco, espacios, comentarios o declaraciones.

Además  notar que las expresiones deben empezar con un identificador. Además, se asumen que siempre serán de tipo asignación, es decir, de la forma: “a = b”.

Si por ejemplo escribimos nuestro programa (input.tit) con la siguiente expresión:

Al ejecutar Nuestro compilador, desde Lazarus, veremos que se queja con un mensaje de error:

Notar que el mensaje de error es preciso y además indica correctamente la posición del error.

Lo que indica el error es que se espera que el operador, después de la “x”, sea el operador de asignación, o sea, que esperaba una expresión de tipo “x = …”, porque se ha definido en ProcessAssigment() que solo se acepten expresiones de esta forma.

Una expresión de asignación, sin embargo, será procesada correctamente:

¡Eureka! Este es nuestro primer código generado por el compilador.

La parte de la derecha, corresponde al archivo ensamblador generado a partir de nuestro código fuente de la izquierda. La primera vez que se observa este código puede parecer magia, pues se está generando código a partir de código, y es una de las partes más gratificantes de crear compiladores.

Este código generado (archivo input.asm) puede ser ensamblado correctamente en un ejecutable, usando nuestro script “test.bat”, como ya explicamos en entregas anteriores de este artículo. El ejecutable, sin embargo, no mostrará nada pero si ejecutará la instrucción solicitada.

La instrucción generada “mov DWORD PTR x, 1” proviene del procedimiento ProcessAssigment() de nuestro compilador:

Que se ejecuta cuando se detecta que se asigna una constante “integer” a una variable “integer”. Las variables “curVarType” y “curVarName” mantienen información de la variable a la que se le está asignando algún valor.

Las instrucciones en ensamblador, las escribimos en el archivo de salida, usando writeln(), y direccionando la salida al archivo *.asm.

Si cambiamos, el programa fuente usando solo variables, veremos que se genera código ensamblador similar:

En este caso, se generan dos instrucciones ensamblador para realizar la asignación.

Si analizamos el comportamiento del compilador, veremos que no hay nada de mágico en su trabajo.  Solo consiste en generar correctamente, las instrucciones apropiadas, para la situación apropiada.

Quizás alguien se pregunte ¿Y cómo llegó el compilador a saber qué código generar?

A grandes rasgos, el programa principal determina, por descarte (no es declaración, espacio, línea en blanco), que la línea actual debe ser una instrucción de asignación. Luego captura información sobre la variable que va a ser asignada (en asgVarName y asgVarType) y llama a la rutina ProcessAssigment(), quien lee el operador de asignación “=” y lee la parte de la derecha de la asignación con ayuda de EvaluateExpression() quien determina el tipo del operando a asignar. Luego después de algunas validaciones de tipo, se detecta qué tipo de dato es el que se desea asignar, y se generan las instrucciones correspondientes.

Pero eso no es todo lo que puede hacer nuestro compilador. La rutina EvaluateExpression() también es capaz de generar código para operaciones de suma y resta usando los procedimientos OperAdd y OperSub. Estos procedimientos trabajan en forma similar a ProcessAssigment(), identificando los dos operandos de la expresión y generando el código necesario para evaluar la expresión:

En este caso se esta generando el código en dos partes. La primera evalúa “x + 1” con dos instrucciones ensamblador:

mov eax, x
add eax, 1

La otra parte hace la asignación del resultado, que es de tipo “Expresión” y se realiza todo en una sola instrucción ensamblador:

mov x, eax

Juntando todas las instrucciones, nos dará la idea de que el compilador hizo algo inteligente para evaluar una expresión más elaborada.

Desgraciadamente, nuestro evaluador de expresiones EvaluateExpression() solo procesará expresiones simples de uno o dos operandos.

Adicionalmente, también podemos asignar cadenas, porque nuestra rutina ProcessAssigment() también reconoce el tipo string:

Observar que nuestra rutina, primero crea un segmento de datos para escribir los bytes que corresponden a la cadena. Este segmento es delimitado por las líneas “.data” y “.code” y sirve para hacer declaraciones adicionales de datos. En nuestro caso, como se trata de un operando “string” de almacenamiento constante (literal), se escribe primero en la RAM, como si fuera una variable (ver rutina DeclareConstantString() ), para luego realizar la copia a la variable mediante la macro “szCopy”.

Esta macro genera el código necesario para copiar, byte por byte, todos los caracteres de las variables, hasta encontrar el carácter nulo, así que trabaja de forma muy similar a las rutinas del lenguaje C. Durante el desarrollo del compilador, preferiremos usar macros ya existentes para simplificar nuestro código ensamblador. Si el lector tiene problemas en entender el funcionamiento de estas macros, le recomendamos que primero revise la documentación referida a macros en MASM32.

Si la asignación de cadenas se realiza con variables, se puede deducir que el código generado es más sencillo porque no se necesitará generar una declaración adicional.

Un detalle adicional. La rutina OperAdd() incluye el procesamiento de cadenas, así que puede realizar también la concatenación de cadenas, devolviendo una expresión de tipo cadena.

Intentemos primero realizar una concatenación de dos literales de cadena:

Podemos ver que el evaluador de expresiones es bastante astuto como para poder realizar la concatenación en tiempo de compilación, en lugar de generar código para ello. Esto es un ejemplo de optimización de código y es una característica común en los compiladores modernos.

Para forzar al compilador a generar código de concatenación, necesitamos crear una variable más:

Ahora si vemos que la concatenación se hace llamando a la macro szCatStr, que es quien hace el trabajo pesado de ir juntando los bytes de las variables cadena.

Y con esta demostración, concluimos esta entrega. Por ahora no podemos ver la salida en pantalla de las acciones realizadas, pero en la siguiente entrega, escribiremos una rutina que nos permitirá mostrar el resultado de nuestras operaciones, de modo que podremos escribir nuestro primer “Hola mundo”.

Puntuación: 0 / Votos: 0

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *