Crea tu propio compilador – Parte 8 – Declaración de variables

En el artículo anterior implementamos un verdadero generador de código en ensamblador, que aunque vacío, nos servirá de base para ir implementando las funciones siguientes.

En este artículo implementaremos las rutinas que permitirán la declaración básica de variables, en nuestro lenguaje Titan. Estas rutinas funcionarán detectando los bloques de declaración de variables para poder generar directamente las instrucciones ensamblador que corresponden.

Viéndolo así, implementar la declaración de variables en nuestro compilador, consistirá en traducir la declaración de variables en lenguaje Titan, al lenguaje ASM.

Esta forma de generación de código es un tanto diferente a como trabajan la mayoría de compiladores, pero funcionará bastante bien para nuestro compilador.

En un compilador común, el reconocimiento de variables es también un proceso complejo, ya que las variables pueden ser de tipos y ser a la vez, composición de otras variables, como los arreglos y registros. Además se debe considerar que las variables pueden tener diferentes alcances (locales, globales o restringidas) y también diversos almacenamientos (estático, dinámico, en pila, …).

En nuestra implementación, sin embargo, solo trabajaremos con dos tipos simples y sus correspondientes arreglos. A pesar de ello, el código necesario es considerable.

Un código sencillo

Comenzaremos agregando una rutina que que procese la declaración de variables en nuestro compilador.  Una versión sencilla de esta rutina podría ser:

procedure ParserVar;
{Hace el análisis sintáctico para la declaración de variables.}
var
  varName, typName: String;
begin
  NextToken;  //Toma el "var"
  TrimSpaces;  //Quita espacios
  if srcToktyp<>2 then begin
    MsjError := 'Se esperaba un identificador.';
    exit;
  end;
  varName := srcToken;
  NextToken;  //Toma nombre de variable
  TrimSpaces;
  //Se espera ":"
  CaptureChar(ord(':'));
  if MsjError<>'' then exit;
  //Lee tipo
  TrimSpaces;
  typName := srcToken;
  if typName = 'integer' then begin
    GetLastToken;  //Debe terminar la línea
    if MsjError<>'' then exit;
    WriteLn(outFile, '    ' + varName + ' DD 0');
    //Registra variable
    varNames[nVars] := varName;
    varType[nVars]  := 1;  //Integer
    varArrSiz[nVars]:= 0;  //No es arreglo
    inc(nVars);
  end else if typName = 'string' then begin
    //Debe terminar la línea
    GetLastToken;  //Debe terminar la línea
    if MsjError<>'' then exit;
    WriteLn(outFile, '    ' + varName + ' DB 256 dup(0)');
    //Registra variable
    varNames[nVars] := varName;
    varType[nVars]  := 2;  //String
    varArrSiz[nVars]:= 0;  //No es arreglo
    inc(nVars);
  end else begin
    MsjError := 'Tipo desconocido: ' + typName;
    exit;
  end;
end;

Esta rutina debe ser llamada cuando se haya detectado que el token actual es “var”.  El trabajo, que viene luego, consiste en ir extrayendo los tokens de acuerdo a la sintaxis que hemos definido para nuestro lenguaje Titan:

var <nombre de variable>: <tipo>

Esta rutina es una versión simplificada que no considera el caso de arreglos y tiene poca protección de errores.

Para activar, en nuestro compilador, el reconocimiento de declaraciones, necesitamos modificar nuestro programa principal, para que pase el procesamiento de variables, a nuestra rutina recién creada:

    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

Esta construcción es lógica, porque sabemos que en nuestro lenguaje, la declaración de variables se hace con la palabra reservada “var”. La explicación de esta estructura, se hizo en el artículo anterior.

Observar que en el código de ParserVar() usado para analizar el léxico del código fuente, la secuencia de instrucciones tiene la forma:

cad := srcToken     //Hacer algo con "srcToken"
NextToken;     //Toma nombre de variable 
TrimSpaces;  //Salta los espacios que pudieran haber

Las llamadas recurrentes a “TrimSpaces” son necesarias debido a que nuestro lenguaje se ha definido de forma que sea indiferente a los espacios. Si no se incluyera, tendríamos que poner todos los tokens juntos en el código fuente.

Es altamente recomendable, e instructivo, experimentar con el código para  ver los errores que se generan o tratar de implementar sintaxis diferentes a la que se ha definido para nuestro lenguaje.

El procedimiento ParserVar() es reducido porque solo analiza la declaración de variables simples y porque solo existen dos tipos de datos en nuestro lenguaje.

A este nivel podemos experimentar compilando una declaración sencilla de variables como:

var a: integer

Al compilar este código, obtendremos en nuestro archivo de salida ASM, la declaración de variables en la sintaxis de MASM32

La instrucción ensamblador que corresponde a la declaración de la variable a es “a DD 0”. Se reservan 4 bytes porque hemos definido que las variables enteras tienen 4 bytes de longitud. Si se usa una declaración de un “string”, veremos que reserva 256 bytes de memoria.

Si se obtiene errores en el código fuente, revisar la implementación de ParserVar().

El código ensamblador generado es sintácticamente válido pero no ejecutará nada y solo reservará espacio en la memoria para nuestra variable. En los artículos siguientes, cuando generemos verdaderas instrucciones en ensamblador, usaremos estas declaraciones

Mejorando el código

Hasta el momento, la declaración de variables funciona bastante bien, pero solo se remite a variables de tipo simple. Para soportar la declaración de arreglos necesitamos ampliar este código con el procesamiento de los caracteres “[” y “]”.  La definición de espacio en RAM es sencillo, porque solo basta con multiplicar el tamaño del tipo de dato por el tamaño del arreglo.

El código siguiente es ya más completo y soporta también la declaración de arreglos de los dos tipos básicos de datos que manejamos en Titan:

procedure ParserVar;
{Hace el análisis sintáctico para la declaración de variables.}
var
  varName, typName: String;
  arrSize: integer;
begin
  NextToken;  //Toma el "var"
  TrimSpaces;  //Quita espacios
  if srcToktyp<>2 then begin
    MsjError := 'Se esperaba un identificador.';
    exit;
  end;
  varName := srcToken;
  NextToken;  //Toma nombre de variable
  TrimSpaces;
  //Lee tipo
  if srcToken = '[' then begin
    //Es un arreglo de algún tipo
    NextToken;   //Toma el token
    TrimSpaces;
    if srcToktyp<>3 then begin
      MsjError:='Se esperaba número.';
    end;
    arrSize := StrToInt(srcToken);  //Tamaño del arreglo
    NextToken;
    CaptureChar(ord(']'));
    if MsjError<>'' then exit;
    //Se espera ":"
    CaptureChar(ord(':'));
    if MsjError<>'' then exit;
    //Debe seguir tipo común
    NextToken;
    typName := srcToken;
    if typName = 'integer' then begin
      GetLastToken;  //Debe terminar la línea
      if MsjError<>'' then exit;
      WriteLn(outFile, '    ' + varName + ' DD ', arrSize, ' dup(0)');
      //Registra variable
      varNames[nVars] := varName;
      varType[nVars]  := 1;  //Integer
      varArrSiz[nVars]:= arrSize;  //Es arreglo
      inc(nVars);
    end else if typName = 'string' then begin
      //Debe terminar la línea
      GetLastToken;  //Debe terminar la línea
      if MsjError<>'' then exit;
      WriteLn(outFile, '    ' + varName + ' DB ', 256*arrSize ,' dup(0)');
      //Registra variable
      varNames[nVars] := varName;
      varType[nVars]  := 2;  //String
      varArrSiz[nVars]:= arrSize;  //Es arreglo
      inc(nVars);
    end else begin
      MsjError := 'Tipo desconocido: ' + typName;
      exit;
    end;
  end else if srcToken = ':' then begin  //Es declaración de tipo común
    NextToken;  //Toma ":"
    TrimSpaces;
    typName := srcToken;
    if typName = 'integer' then begin
      GetLastToken;  //Debe terminar la línea
      if MsjError<>'' then exit;
      WriteLn(outFile, '    ' + varName + ' DD 0');
      //Registra variable
      varNames[nVars] := varName;
      varType[nVars]  := 1;  //Integer
      varArrSiz[nVars]:= 0;  //No es arreglo
      inc(nVars);
    end else if typName = 'string' then begin
      //Debe terminar la línea
      GetLastToken;  //Debe terminar la línea
      if MsjError<>'' then exit;
      WriteLn(outFile, '    ' + varName + ' DB 256 dup(0)');
      //Registra variable
      varNames[nVars] := varName;
      varType[nVars]  := 2;  //String
      varArrSiz[nVars]:= 0;  //No es arreglo
      inc(nVars);
    end else begin
      MsjError := 'Tipo desconocido: ' + typName;
      exit;
    end;
  end else begin
    MsjError := 'Se esperaba ":" o "[".';
    exit;
  end;
end;

Esta rutina es un poco extensa, pero el código no es muy complejo, simplemente considera una ampliación para considerar las declaraciones de variables en modo arreglo como:  var x[10]: integer

Si compilamos esta declaración, obtendremos el código ensamblador que se muestra en la siguiente figura:

La instrucción ensamblador generada, en este caso, es “x DD 10 dup(0)” que corresponde a un arreglo de datos de 4 bytes, inicializados a cero.

Se pueden incluir declaraciones adicionales de variables y estas serán reconocidas correctamente.

Un detalle, que conviene analizar en ParserVar(), es el método que se usa para detectar y procesar los errores. El método es básico pero efectivo. En resumidas cuentas, consiste en analizar las opciones posibles y si es que se obtiene una condición no esperada, se detiene la compilación y se crea un mensaje de error.

En el siguiente artículo, hemos conseguido que se reconozcan las declaraciones de variables de nuestro lenguaje, pero aún no se han generado verdaderas instrucciones ejecutables.

En la siguiente parte empezaremos a reconocer expresiones y a generar código ejecutable verdadero, en ensamblador.

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 *