
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.
Dejar una contestacion