Crea tu propio compilador – Parte 10 – Las primeras instrucciones

En el capítulo anterior vimos cómo implementar un analizador de expresiones sencillo para nuestro compilador. Pero no llegamos a usarlo, porque no podemos aún procesar instrucciones. Y aunque se pueden compilar algunas expresiones elementales, como 1 + 1, estas expresiones en sí, no constituyen instrucciones válidas en nuestro lenguaje de programación.

En esta entrega veremos cómo implementar el procesamiento de una de las instrucciones más básicas como es la asignación de valores a variables. Un ejemplo de esta instrucción sería:

x = 1

Antes de iniciar con la implementación de las asignaciones, vamos a repasar la definición de almacenamiento.

Almacenamiento de Operadores

Este es otro de los conceptos que manejaremos en nuestro compilador.

El almacenamiento determina la forma en que se guarda un operando y puede tener muchos tipos. Por ejemplo puede ser que el operando sea una constante y no use espacio de memoria RAM, o puede ser que el operando esté en la RAM en una zona estática o en la pila, etc.

El almacenamiento (al igual que el tipo de un operando) es importante porque proporciona la información que necesita el compilador para poder generar las instrucciones apropiadas para los operaciones solicitadas.

Si bien el almacenamiento puede ser muy diverso, en nuestro compilador, solo consideraremos 4 casos:

//0 -> Constante
//1 -> Variable
//2 -> Expresión
//1000 -> Sin almacenamiento

Un almacenamiento de tipo 0, se refiere a un literal numérico o de cadena, como por ejemplo:

123
"Saludos".

Este almacenamiento es uno de los más básicos y también los más deseables, porque no ocupan espacio de memoria en el programa, y solo se cargan en el programa cuando son requeridos.

El almacenamiento de tipo 1, indica que el operando está en alguna zona de la memoria y se ubicará por su dirección. Es el almacenamiento preferido para manejar variables simples. Este almacenamiento es también fácil de usar porque para acceder al valor del operando solo basta con referirse a una zona de la memoria donde reside la variable.

Los siguientes operandos pertenecen a este tipo de almacenamiento:

variable_numérica
variable_cadena
x

El almacenamiento de tipo 2, indica que el valor del operando se debe guardar en alguna otra parte, puesto que no es una constante ni una variable. Los compiladores implementan diversos métodos para trabajar con expresiones, pero en nuestro caso, almacenaremos las expresiones en registros de trabajo.

Para las expresiones numéricas, usaremos el registro EAX.

Para las expresiones cadena usaremos una zona de RAM, reservada por el compilador, llamada “_regstr” que es una zona de 256 bytes.

En resumen, de acuerdo con su almacenamiento, los operandos tienen su valor en:

Tipo de operando   Ubicación de su valor
------------------ --------------------
Constante          No existe hasta que se compila
Variable           Dirección fija de la RAM
Expresión          Registro de la CPU o RAM. 

Compilando una expresión

La compilación de una expresión consiste en convertir la expresión en una secuencia de instrucciones en ensamblador.

Consideremos que queremos compilar la siguiente expresión:

10 + x

De estos dos operandos, solo el segundo existe previamente almacenado (se supone que en alguna parte de la RAM). El primer operando es una constante (almacenamiento 0) y no existía hasta el momento en que se compila esta rutina. Dicho de otra forma, el operando “10” no ha estado ocupando en la memoria.

Si generamos código ensamblador para esta sencilla operación, tendríamos algo como:

mov eax, x    ; Carga operando con almacenamiento “Variable”.
add eax, 10   ; Suma a operando con almacenamiento “Constante”.

Que consiste en cargar en el registro EAX, el valor de la variable “x”, para luego sumarle el valor 10 al registro EAX.

El resultado (de almacenamiento Expresión) queda en el registro EAX, que es como el registro para retornar resultados. Es común, en los compiladores, usar varios registros para devolver resultados de expresiones. Luego estos resultados pueden ser guardados en otra variable o usado para calcular nuevas expresiones.

Para la compilación de expresiones, usaremos rutinas sencillas de dos operandos y un operador. Las expresiones más complejas se descompondrán en expresiones sencillas.

Las asignaciones en Titan

Las asignaciones son probablemente las instrucciones más usadas dentro de un lenguaje de programación. Sirven primordialmente para inicializar variables, para modifica el contenido de una variable  o para guardar el resultado de expresiones.

Muchos lenguajes, como el C o C++,consideran a las asignaciones como simples expresiones que hacen uso del operador de asignación (=) y que pueden también devolver valores. De allí que se pueda hacer códigos como:

x = (y = 1);

Sin embargo otros lenguajes, como Pascal, consideran a las asignaciones como instrucciones especiales, que no devuelven valores. Es decir que no se manejan como expresiones.

En nuestro lenguaje Titan, consideraremos a las expresiones como instrucciones especiales así que no formarán parte de una expresión como tal. Esto ayudará a simplificar el análisis de expresiones.

Las asignaciones son de la forma:

a = b

Para el reconocimiento de este tipo de “expresiones”, necesitamos primero una rutina para reconocer a las variables. Esta rutina solo explorará el arreglo varNames[] en busca del nombre de la variable buscada:

procedure FindVariable;
{Busca la variable con el nombre que está en "srcToken", y actualiza las variables:
"curVarName", "curVarType", y "curVarArSiz".
Si no encuentra, devuelve cadena vacía en "curVarName".}
var
  tmp: string;
  contin: integer;
  curVar    : integer;
begin
  curVar := 0;
  tmp := varNames[curVar];
  contin := ord(tmp <> srcToken);
  while contin=1 do begin
    inc(curVar);
    if curVar = 256 then break;
    tmp := varNames[curVar];
    contin := ord(tmp <> srcToken);
  end;
  //Verifica si encontró
  if contin=0 then begin
    curVarName := varNames[curVar];
    curVarType := varType[curVar];
    curVarArSiz := varArrSiz[curVar];
    exit;  //"curVar" contiene el índice
  end;
  //No encontró
  curVarName := '';
end;

FindVariable() devuelve una cadena nula en  “curVarName” cuando no encuentra la variable buscada.

Ahora que tenemos como identificar a una variable, nos toca implementar el procedimiento para realizar la asignación en sí. Este procedimiento, algo extenso, consiste en ir extrayendo los tokens de una expresión, que deben tener la forma:

<variable> = <variable o constante>

Considerando además que la variable a ser asignada puede ser también un arreglo:

<variable>[índice] = <variable o constante>

Por otro lado, solo consideraremos por ahora el caso de constantes o variables, como elementos que pueden ser asignados.

El código en cuestión es:

procedure ProcessAssigment;
begin
  NextToken;
  TrimSpaces;
  if srcToken = '[' then begin
    ReadArrayIndex;  //Actualiza idxStorag, idxCteInt, y idxVarNam.
    //Valida si la variable es arreglo
    if curVarArSiz = 0 then begin
      MsjError := 'Esta variable no es un arreglo.';
      exit;
    end;
  end else begin
    idxStorag := 1000;  //Sin almacenamiento
  end;
  TrimSpaces;
  if srcToken<>'=' then begin
    MsjError := 'Se esperaba "=".';
    exit;
  end;
  NextToken;  //Toma "="
  //Evalúa expresión
  EvaluateExpression;
  if MsjError<>'' then begin
    exit;
  end;
  //Codifica la asignación
  if resType = 1 then begin
    //Integer
    if asgVarType<>1 then begin
      MsjError := 'No se puede asignar un entero a esta variable.';
      exit;
    end;
    if resStorag = 0 then begin
      //Constante
      if idxStorag = 1000 then begin  //Sin arreglo
        WriteLn(outFile, '    mov DWORD PTR ',asgVarName,', ', resCteInt);  //
      end else if idxStorag = 0 then begin  //Indexado por constante
        WriteLn(outFile, '    mov DWORD PTR [',asgVarName,'+',idxCteInt*4,'], ', resCteInt);
      end else begin
        MsjError := 'No se soporta este tipo de expresión.';
        exit;
      end;
    end else if resStorag = 1 then begin
      //Variable
      if idxStorag = 1000 then begin  //Sin arreglo
        WriteLn(outFile, '    mov eax, ', resVarNam);
        WriteLn(outFile, '    mov ' + asgVarName + ', eax');
      end else begin
        MsjError := 'No se soporta este tipo de expresión.';
        exit;
      end;
    end else begin
      //Expresión. Ya está en EAX
      if idxStorag = 1000 then begin  //Sin arreglo
        WriteLn(outFile, '    mov ' + asgVarName + ', eax');
      end else begin
        MsjError := 'No se soporta este tipo de expresión.';
        exit;
      end;
    end;
  end else begin
    //String
    if asgVarType<>2 then begin
      MsjError := 'No se puede asignar una cadena a esta variable.';
    end;
    if resStorag = 0 then begin
      //<variable> <- Constante
      if idxStorag = 1000 then begin  //Sin arreglo
        DeclareConstantString(resCteStr);
        WriteLn(outFile, '    invoke szCopy,addr '+constr+', addr '+ asgVarName);
      end else if idxStorag = 0 then begin  //Indexado por constante
        DeclareConstantString(resCteStr);
        WriteLn(outFile, '    invoke szCopy,addr '+constr+', addr ',asgVarName, ' + ',
                         idxCteInt*256);
      end else begin
        MsjError := 'No se soporta este tipo de expresión.';
        exit;
      end;
    end else if resStorag = 1 then begin
      //<variable> <- Variable
      WriteLn(outFile, '    invoke szCopy,addr '+resVarNam+', addr '+ asgVarName);
    end else begin
      //Expresión. Ya está en "_regstr"
      WriteLn(outFile, '    invoke szCopy,addr _regstr'+', addr '+asgVarName);
    end;
  end;
end;

Esta rutina incluye generación de código ensamblador para realizar la asignación. La asignación de números es fácil porque se hace con instrucciones simples de transferencia MOV, pero la asignación de cadenas se implementa llamando a la macro szCopy, que nos proporciona MASM32, y que permite mover bytes de una dirección a otra.

Como la asignación considera el caso de asignación de arreglos, se usa la bandera idxStorag, que es actualizada con el procedimiento ReadArrayIndex():

procedure ReadArrayIndex;
{Lee el índice de un arreglo. Es decir, lo que va entre corchetes: [].
Devuelve información en las variables: idxStorag, idxCteInt, y idxVarNam.
No genera código y no usa ningún registro adicional, porque restringe que el
índice sea una constante o una variable simple.
Se asume que el token actual es '['.
Si encuentra algún error, devuelve el mensaje en "MsjError"}
begin
  //Es acceso a arreglo
  NextToken;  //Toma '['
  EvaluateExpression;
  if MsjError<>'' then exit;
  if resStorag = 2 then begin
    {Se restringe el uso de expresiones aquí, por simplicidad, para no complicar la
    generación de código. Así solo tendremos constantes o variables como índice.}
    MsjError := 'No se permiten expresiones aquí.';
    exit;
  end;
  if resStorag = 1 then begin
    //Es variable. Solo puede ser entera.
    if resType <> 1 then begin
      MsjError := 'Se esperaba varaible entera.';
      exit;
    end;
  end;
  CaptureChar(ord(']'));
  if MsjError<>'' then exit;
  //Sí, es un arreglo. Guarda información sobre el índice.
  //Solo puede ser entero o variable entera.
  idxStorag := resStorag;   //Guarda almacenamiento del índice.
  idxCteInt := resCteInt;   //Valor entero
  idxVarNam := resVarNam;   //Nombre de varaible
end;

Esta rutina nos permitirá leer el índice de un arreglo, es decir, lo que va entre los corchetes []. Y devuelve información adicional en algunas variables globales. Una de ellas, idxStorag, nos indica si el índice es una constante o una variable.

Ahora que ya tenemos una rutina para buscar variables FindVariable(), podemos ya completar nuestra rutina GetOperand(), que quedó incompleta en el artículo anterior:

procedure GetOperand;
{Extrae un operando. Acatualiza variables "resXXX".}
var
  n: integer;
begin
  TrimSpaces;
  //Captura primer operando, asumiendo que es el único
  if srcToktyp = 3 then begin  //Literal Número
    n := StrToInt(srcToken);  //Falta verifiación de error
    resStorag := 0; //Constante
    resType := 1;   //Integer
    resCteInt := n; //Valor
    NextToken;
  end else if srcToktyp = 4 then begin  //Literal Cadena
    resStorag := 0; //Constante
    resType := 2;   //Integer
    resCteStr := copy(srcToken,2,length(srcToken)-2); //Valor
    NextToken;
  end else if srcToktyp = 2 then begin  //Identificador
    //Verifica función del sistema
    if srcToken = 'length' then begin
      NextToken;
      CaptureChar(ord('('));
      if MsjError<>'' then exit;
      EvaluateExpression;  //Llamada recursiva
      if MsjError<>'' then exit;
      CaptureChar(ord(')'));
      if MsjError<>'' then exit;
      if resType<>2 then begin
        MsjError := 'Se esperaba una cadena.';
        exit;
      end;
      if resStorag = 0 then begin
        //Constante cadena
        resType := 1;  //Devuelve constante numérica
        resCteInt := length(resCteStr);
      end else if resStorag = 1 then begin
        //Variable cadena
        WriteLn(outFile, '    invoke szLen, addr '+resVarNam);
        resType := 1;  //Devuelve número en EAX
        resStorag := 2;  //Expresión
      end else if resStorag = 2 then begin
        //Expresión cadena
        WriteLn(outFile, '    invoke szLen, addr _regstr');
        resType := 1;  //Devuelve número en EAX
        resStorag := 2;  //Expresión
      end else begin
        MsjError := 'Almacenamiento no soportado';
        exit;
      end;
    end else begin
      //Busca variable
      FindVariable;
      if curVarName = '' then begin
        MsjError := 'Identificador desconocido: ' + srcToken;
        exit;
      end;
      //Es una variable. Podría ser un arreglo.
      NextToken;
      TrimSpaces;
      if srcToken = '[' then begin
        //Es acceso a arreglo
        ReadArrayIndex;  //Actualiza idxStorag, idxCteInt, y idxVarNam.
        //Valida si la variable es arreglo
        if curVarArSiz = 0 then begin
          MsjError := 'Esta variable no es un arreglo.';
          exit;
        end;
        //Extraemos valor y devolvemos como expresión
        resStorag := 2; //Expresión
        resType := curVarType;  //Devuelve el mismo tipo que la variable.
        if resType = 1 then begin
          //Arreglo de enteros
          WriteLn(outFile, '    mov eax, DWORD PTR [',curVarName,'+',idxCteInt*4,']');
        end else begin
          //Arreglo de cadenas
          WriteLn(outFile, '    invoke szCopy,addr '+curVarName+'+',idxCteInt*256,', addr _regstr');

        end;
      end else begin
        //Es una variable común
        resStorag := 1; //Variable
        resType := curVarType;   //Tipo
        resVarNam := curVarName;
      end;
    end;
  end else begin
    MsjError := 'Error de sintaxis: ' + srcToken;
    exit;
  end;
end;

Esta implementación incluye también el procesamiento de la función length() que nuestro compilador reconocerá y devolverá (con almacenamiento Expresión) la longitud de una cadena.

Ahora que tenemos ya a GetOperand() más completo, le daremos más potencia a EvaluateExpression() para que considere también expresiones con variables.

Hay que notar que EvaluateExpression() y GetOperand() se llaman mutuamente entre ellas, así que implementan lo que se llama “Recursividad mutua”.

Finalmente ya podemos modificar nuestro programa principal para que finalmente procese a las operaciones de asignación y la evaluación de expresiones sencillas.

      ...
      //**** 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;
      ...

En el siguiente artículo, probaremos el código e implementaremos una rutina sencilla para mostrar valores en pantalla.

 

Puntuación: 4 / Votos: 1

Deja un comentario

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