Crea tu propio compilador – Parte 12 – Funciones del sistema

En el capítulo anterior usamos nuestro evaluador de expresiones, que aunque es bastante simple, nos permitió generar código para distintas expresiones numéricas y de cadena, que son los dos únicos tipos de datos que maneja nuestro compilador.

En este capítulo, y por primera vez, le permitiremos a nuestro compilador, poder escribir mensajes en pantalla. Esto nos permitirá, al fin, poder escribir un verdadero «Hola mundo», que es tan importante como cuando escuchamos a un bebé decir sus primeras palabras.

Un alto en el camino

Hasta el momento, el código fuente de nuestro compilador ha ido creciendo y adquiriendo más capacidades. En este punto conviene recordar que nuestro diseño de compilador es bastante simplista en comparación con lo que sería un verdadero compilador, y que solo lo creamos con fines didácticos, pero aún así mantiene un diseño, el mismo que describiremos en las siguientes líneas.

Para empezar tenemos la sección de declaración de variables globales, que son la mayoría de variables, porque se evita usar variables locales para tener un código bastante simplificado.

Luego tenemos la sección de funciones del analizador léxico:

  • function EndOfLine: integer;
  • function EndOfFile: integer;
  • procedure NextLine;
  • procedure ReadChar;
  • procedure NextChar;
  • function NextCharIsSlash: integer;
  • function IsAlphaUp: integer;
  • function IsAlphaDown: integer;
  • function IsNumeric: integer;
  • procedure ExtractIdentifier;
  • procedure ExtractSpace;
  • procedure ExtractNumber;
  • procedure ExtractString;
  • procedure ExtractComment;
  • procedure NextToken;

Estas funciones son la que reconocen y extraen los tokens del archivo fuente de entrada. El token actual se almacena en la variable «srcToken» y el tipo del token se guarda en «srcToktyp».

Luego incluímos un conjunto de funciones que incluyen el análisis sintáctico y semántico:

  • procedure TrimSpaces;
  • procedure GetLastToken;
  • procedure CaptureChar(c: integer);
  • procedure ParserVar;
  • procedure FindVariable;
  • procedure ReadArrayIndex;
  • procedure ProcessAssigment;

Recordemos que, por simplicidad, no estamos incluyendo un módulo de análisis sintáctico independiente, porque haría demasiado complejo a nuestro compilador, que es lo que queremos evitar. En nuestra implementación se podría decir que realizamos el análisis sintáctico y semántico a la vez. E inclusive, la generación de código, o síntesis, la realizamos mientras hacemos el análisis sintáctico-semántico.

La generación de código, se hace principalmente en la función de evalaución de expesiones, que se implementa con diversas funciones:

  • GetOperand;
  • procedure GetOperand1;
  • procedure GetOperand2;
  • procedure DeclareConstantString(constStr: string);
  • procedure OperAdd;
  • procedure OperSub;
  • procedure EvaluateExpression;

Creando nuestra función «print»

Hasta ahora nuestro compilador solo ha podido procesar declaraciones y expresiones. Pero ahora necesitamos que reconozca nuestra primera función del sistema. Esta primera función será la función «print», la que nos permitirá escribir mensajes en pantalla.

Para implementar esta función, podemos usar diferentes métodos dentro del MASM32, pero en este caso he elegido usar una macro que permite escribir mensajes por pantalla. Esto nos ayudará, porque de otra forma tendriamos que lidiar con oscuras llamadas a la BIOS o sus equivalentes.

La macro «print» del MASM32 es fácil de usar, como se muestra en el siguiente código ensamblador:

    .code
start:
    print "Hola"
    exit
end start

Así que para imprimir un mensaje en pantalla solo debemos detectar cuando se llama a la función «print» y escribir la instrucción «print» del ensamblador con el parámetro correspondiente. La parte difícil será considerar todos los casos que puede tener el parámetro, que para nuestro caso puede ser:

  • Constantes (literales) de tipo numérico o cadena, como: 123 u «Hola»
  • Variables numéricas o de texto como: mi_variable
  • Resultado de expresiones, como: 1+x

Si tan solo tuviéramos que imprimir constantes, la implementación de nuestra función «print» sería algo como esto:

procedure processPrint(ln: integer);
begin
  NextToken; //Pasa del "print"
  EvaluateExpression;
  if MsjError<>'' then exit;
  if resType = 1 then begin
    //Imprime constante Entera
    write(outFile, ' print "');
    write(outFile, resCteInt, '"');
  end else if resType = 2 then begin
    //Imprime constante cadena
    write(outFile, ' print "'+resCteStr+'"');
  end;
end;

Que escribe la instrucción ensamblador «print» con el parámetro adecuado (En realidad «print» es una macro del MASM32 ).

Ya luego tan solo deberíamos llamar a esta función para cuando encontremos una llamada a la función «print» en nuestro programa en Titan. Es decir, nuestra instrucción «print» en Titan, se convertirá en la macro «print» del MASM32. La similitud de nombres es solo una coincidencia. Bien pudimos haber llamado con cualquier otro nombre a nuestra instrucción para imprimir en pantalla. Le hemos puesto «print» cuando definimos el lenguaje, pero pudimos haber usado «escribir», «imprimir» o algo similar.

Pero como debemos considerar que podemos imprimir no solo constantes, debemos usar las facilidades de la macro «print» para recibir la dirección de la cadena que deseamos imprimir.

Imprimir una variable, numérica, implica que tenemos que convertir primero el valor numérico a su representación en una cadena. Esto es necesario porque, por ejemplo, el valor numérico 123, almacenado en una variable, es solo un conjunto de bits, que nada tiene que ver con los caracteres ASCII «1», «2», y «3» que deseamos mostrar en la consola.

Para realizar la conversión, nos ayudaremos de otra de las funciones que nos ofrece el MASM32 y se llama «dwtoa». Así que en nuestro compilador, las instrucciones de tipo «print variable_numerica» se deben convertir primero en una llamada a «dwtoa» y luego una llamada a la macro «print», algo como esto:

invoke dwtoa, variable_numerica , addr _regstr';
print addr _regstr;

Notar que el resultado en texto de la conversion de nuestra variable numérica, se almacenará en nuestra zona temporal de memoria «_regstr», que nos cae muy bien en este caso.

El caso de impresión de expresiones es también algo similar al caso de variables.

Finalmente, nuestra función que implementa la generación de código para la instrucción «print», sería esta:

procedure processPrint(ln: integer);
{Implementa las instrucciones "print" y "println". Si "ln" = 0 se compila "print",
de otra forma se compila "println".}
begin
  NextToken;  //Pasa del "print"
  EvaluateExpression;
  if MsjError<>'' then exit;
  if resStorag = 0 then begin
    //Almacenamiento en Constantes
    if resType = 1 then begin
      //Imprime constante Entera
      write(outFile, '    print "');
      write(outFile, resCteInt, '"');
    end else if resType = 2 then begin
      //Imprime constante cadena
      write(outFile, '    print "'+resCteStr+'"');
    end;
  end else if resStorag = 1 then begin
    //Almacenamiento en variable
    if resType = 1 then begin
      //Imprime variable entera
      writeln(outFile, '    invoke dwtoa,' + resVarNam +', addr _regstr');
      write(outFile, '    print addr _regstr');
    end else if resType = 2 then begin
      //Imprime constante cadena
      write(outFile, '    print addr ' + resVarNam);
    end;
  end else if resStorag = 2 then begin
    //Almacenamiento en expresión
    if resType = 1 then begin
      //Imprime variable entera
      writeln(outFile, '    invoke dwtoa, eax, addr _regstr');
      write(outFile, '    print addr _regstr');
    end else if resType = 2 then begin
      //Imprime constante cadena
      write(outFile, '    print addr _regstr');
    end;
  end else begin
    MsjError := 'Almacenamiento no soportado';
    exit;
  end;
  if ln=0 then writeln(outFile, '') else writeln(outFile, ',13,10');
end;

Observar que se separan todos los casos posibles, tal como hemos comentado.

Además se está aprovechando para implementar otra instrucción de nuestro lenguaje Titan, que es «println» que hace lo mismo que «print» pero que agrega un salto de línea al final. Para ello se debe poner el parámetro «ln» en 1.

Una vez agregada esta función ya tenemos soporte para imprimir en pantalla con «print» y «println», pero aún debemos interceptar las llamadas a estas instrucciones desde dentro de nuestro código fuente. Eso lo hacemos en el cuerpo principal del código del compilador, agregando las siguientes instrucciones:

...
  //**** Aquí procesamos instrucciones.
  //y ya no se deben permitir más declaraciones.
  if srcToken = 'print' then begin
     processPrint(0);
     if MsjError<>'' then break;
  end else if srcToken = 'println' then begin
     processPrint(1);
     if MsjError<>'' then break;
  end else if srcToktyp = 2 then begin
    //Es un identificador, puede ser una asignación
...

Y ya con esto tenemos soporte para «print» y «println» y ya finalmente podremos mandar mensajes por pantalla.

Hagamos la prueba con nuestro primer «Hola mundo». Creemos el siguiente programa en Titan, en el archivo input.tit:

print «Hola»

Y ejecutemos nuestro compilador, desde Lazarus.

Si todo salió bien, debemos obtener en la salida (archivo «input.asm») el siguiente código:

    include \masm32\include\masm32rt.inc
    .data
    _regstr DB 256 dup(0)
    .code
    .code
start:
    print "Hola"
    exit
end start

Y ahora sí, podemos ensamblar y ejecutar el programa, con nuestro archivo «test.bat», y veremos un hermoso «Hola» en nuestra consola:

Este es nuestro primer programa que imprime un mensaje en pantalla. Observar que el mensaje no imprime el salto de línea por lo que el prompt sale en la misma línea.

Probemos ahora con el famoso «Hola Mundo»:

Observar como ahora se incuye el salto de línea al final de la cadena.

Pero nuestra rutina no solo imprime literales de cadena. Es capaz de imprimir expresiones numéricas también:

Observar la llamada previa a «dwtoa» y el uso de «_regstr» al momento de imprimir variables numéricas.

Aún más, podemos dejar que nuestro evaluador de expresiones trabaje en conjunto para imprimir expresiones sencillas:

Una revisión del código ASM generado nos ayuda a entender cómo es que se logra la magia de poder imprimir expresiones. El truco está en que primero se llama al evaluador de expresiones y luego a la rutina processPrint().

Hasta ahora hemos logrado un gran avance con nuestro compilador. En el siguiente artículo estaremos revisando y completando algunas funciones que quedan pendientes.

1 Trackback / Pingback

  1. Crea tu propio compilador – Parte 1 – Introducción – Blog de Tito Hinostroza

Dejar una contestacion

Tu dirección de correo electrónico no será publicada.


*