
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. Usamos Notepad++ para mostrar el contenido del archivo fuente y de salida:
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 este 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.
Dejar una contestacion