Crea tu propio compilador – Parte 3 – Inventando un lenguaje

El lenguaje

El siguiente paso en la creación de nuestro compilador, será la creación del lenguaje. Para ello podemos plantear 2 opciones:

  • Usar un lenguaje ya existente, como C  o Pascal.
  • Crear un lenguaje nuevo desde cero.

Implementar un lenguaje ya existente, es una tarea descomunal, aún si elegimos un lenguaje sencillo como Basic o C, porque la complejidad de su sintaxis y la cantidad de instrucciones que manejan, va en contra de nuestro objetivo de sencillez. Ni que decir de lenguajes como C++ u Object Pascal.

Crear un lenguaje nuevo nos da la ventaja de poder definir una sintaxis básica y acorde a nuestros requerimientos. Por ello, es que iremos por esa opción.

Ahora, para mayor simplicidad, definiremos los siguientes requerimientos para nuestro lenguaje:

  • Imperativo. Nada de características funcionales complejas.
  • Tipado. Así se facilita el análisis de expresiones y detección de errores.
  • Con solo dos tipos de datos. Lo necesario para poder ser autocontenido.
  • Reducida cantidad de instrucciones. Para no complicar la implementación.
  • Soporte a manejo de archivos.

Sin embargo, el lenguaje debe tener la suficiente potencia como para poder escribir un compilador simple .

Estos dos objetivos se contraponen; por un lado se debe hacer al lenguaje lo mínimamente complejo, pero por otro lado, se debe tener la capacidad de desarrollar un compilador en este lenguaje, lo que implica un nivel de potencia considerable.

Como queremos que el lenguaje pueda compilar un código fuente, lo más lógico es que este este código se encuentre en un archivo, así que el lenguaje debe tener funciones para leer archivos, aunque sea de texto.

Otra de las funcionalidades que debería tener el lenguaje, dado que deberá poder implementar un analizador léxico, será que debe disponer de funciones y operadores para manipulación de cadenas, y por supuesto debe tener el tipo de dato cadena.

Ahora, es posible que el lenguaje no implemente el tipo cadena (lo cual sería un alivio), y manejara todo como arreglos de caracteres, al mismo estilo de C, pero esto implicaría hacer al compilador aún más complejo, porque seguramente debería implementar punteros y su aritmética respectiva.

El soporte del lenguaje para números es mínimo y solo nos basta con manejar 1 tipo de dato numérico entero, que nos puede servir a su vez como carácter (de forma similar al tipo “char” del C) o como valor booleano, si restringimos su valor a 0 o 1.

Otra de las características que debe tener el lenguaje, es poder soportar el uso de funciones o procedimientos, porque es lo mínimo que se puede pedir para hacer a un programa, algo modular. De momento no se tiene pensado implementar librerías externas o algún tipo de estructura modular, para no complicar el proyecto.

A pesar de las restricciones para la simplicidad del lenguaje, esta parte es la más divertida del proyecto, porque aquí podemos soltar nuestra creatividad considerablemente.

El lenguaje que yo propongo aquí, y al que llamaré “Titan”, es uno que nace de mi experiencia y creatividad, pero usted puede definir sus propias estructuras o palabras reservadas.

EL CÓDIGO FUENTE

Por cuestiones de simplicidad, nuestro lenguaje será sensible a la caja, es decir, que diferenciará mayúsculas de minúsculas. Hacer al lenguaje insensible a mayúscula/minúscula, es algo sencillo, pero implica un trabajo adicional (como implementar las funciones de transformación), y considerando que nuestro objetivo inicial es simplificar el lenguaje, lo más que se pueda, prescindiremos de esta característica.

Otro detalle adicional, para simplificar nuestro compilador, es que solo manejaremos un tipo de datos cadena, y para simplificar su implementaciones, las definiremos de un tamaño fijo, de 255 caracteres, al mismo estilo de Pascal, en sus primeras versiones.

Debido a esta limitación, tampoco se podrán tener, en el código fuente, líneas de más de 255 caracteres, porque no habría forma de guardarlas de forma sencilla.

La sintaxis que voy a definir tiene como objetivos:

  • Permitir una implementación rápida y simple del analizador léxico.
  • Ser un lenguaje de alto nivel. Así que no se parecerá mucho a C.
  • Facilitar la escritura al programador, evitando el uso de muchos símbolos.

Para cumplir el último objetivo, se está prescindiendo del uso del punto y coma como delimitador de instrucciones, (aunque es posible que se implemente posteriormente) y se está evitando el uso de paréntesis cuando sea posible. Se tratará de usar el fin de línea como delimitador, al igual que se hace en Visual Basic.

Sin embargo, esta sintaxis es experimental, así que es posible que hagamos cambios posteriores.

Un detalle adicional, es que no incluiremos rutinas de verificación de errores en algunas instrucciones. De esta forma simplificaremos el código, pero hay que dejar en claro que un buen diseño, implica siempre, manejar rutinas de protección de errores.

El lenguaje que defino aquí, usa palabras del alfabeto inglés, por comodidad, pero bien se puede usarse español o cualquier otro idioma.

PALABRAS RESERVADAS

Se definen aquí las palabras que no pueden usarse como identificadores  o algún otro elemento del lenguaje.

Por ahora, solo manejaremos las siguientes palabras reservadas:

“var”, “number”, “string”, “if”, “then”, “end”, “while”, “do”, “proc”, “func” y “exit”.

Como se puede deducir, se tratará de mantener esta lista lo más pequeña posible (por simplicidad del compilador), pero es posible que vayamos ampliando esta lista conforme vayamos desarrollando el lenguaje, sin embargo, este es un buen punto de partida.

COMENTARIOS

Los comentarios se definirán con los caracteres “//” de la misma forma a como se hace en el lenguaje C.

Los comentarios serán de una sola línea. Por ahora no implementaremos comentarios de varias líneas, pero se espera usar los caracteres /* … */ al igual que en C/C++.

NÚMEROS

Solo se reconocerán números enteros de hasta 32 bits, como: 123, -10000, o 001.

No hay soporte para números en coma flotante, por ahora.

IDENTIFICADORES

Los identificadores son los nombres que se usarán para las variables. Para nuestro caso, se usarán las siguientes reglas:

  • El primer carácter, debe ser siempre una letra, sea mayúscula o minúscula o el carácter guion bajo “_”.
  • Los caracteres siguientes, si es que los hay, pueden ser cualquier letra, número o el carácter guion bajo “_”.

Para las letras solo se reconocerán las letras del alfabeto inglés.

Considerar que los identificadores diferencian mayúsculas de minúsculas y que no se pueden usar identificadores iguales a las palabras reservadas.

Otro detalle a tener en cuenta es que la longitud de los identificadores no puede ser mayor a 255 caracteres, ni contener caracteres fuera del alfabeto ASCII.

ESTRUCTURA DE UN PROGRAMA

Los programas pueden tener una estructura libre, pero se recomienda:

<declaraciones de funciones>
<declaraciones de variables>
<programa principal>

El programa “Hola mundo”, será en nuestro caso.

print "Hola mundo"

Otro ejemplo podría ser:

var x: number;
var cad: string
cad = "hola"
print cad

Notar que el punto y coma, al final, no se usa.

OPERADORES

En nuestro caso, solo implementaremos:

“=” operador de asignación de variables y de comparación de números.

“<>” operador comparación de números.

“+” suma de números o concatenación de cadenas.

“-” resta de números.

“>” comparación de números.

“<” comparación de números.

La concatenación de cadenas se hará con el mismo operador “+” que se usa para la suma aritmética. La concatenación es importante para poder realizar la manipulación de cadenas.

EXPRESIONES

Las expresiones serán simples. De dos operandos como máximo, aparte de la asignación, si es que existe.

Así, las siguientes expresiones serán válidas:

x = 1;

x = x+y

x>0

Pero las siguientes expresiones, son inválidas:

x = x + y + 1  //Más de 2 operandos después de la asignación

x>y+1 //Más de 2 operandos

Como las expresiones son simples, no hay problemas en definir la precedencia de operadores y solo asumiremos que la asignación tiene la mayor precedencia.

TIPOS DE DATOS

Solo manejaremos dos tipos de datos.

Tipo numérico -> Números con signo, de hasta 32 bits de longitud. Ejemplos: 1000, -50 Solo se permiten valores enteros.

Tipo cadena -> Cadenas de cualquier longitud, terminadas en caracter NULL. Ejemplo: “Hola”. Las cadenas tendrán una longitud máxima de 255 caracteres.

Dentro del código las cadenas solo pueden ocupar una línea.

Los tipos numéricos se podrán usar también como valores booleanos, asumiendo que todo valor diferente de cero se considera TRUE.

VARIABLES

Las variables se declaran con la palabra reservada “var”:

var x: number

var s: string

Solo se permite una declaración por cada palabra “var”.

Las declaraciones pueden hacerse en cualquier lugar del código.

No se manejará constantes, por el momento.

ARREGLOS

Se pueden crear arreglos de datos de los tipos número y cadena:

var x[10]: integer
var cad[5]: string

Esta definición indica que se está creando un arreglo, de 10 elementos: Desde x[0] hasta x[9].

Las cadenas, se podrán tratar también como arreglos:

var cad: string

cad[1] = 65    //primer caracter ‘A’

ESTRUCTURAS.

Solo se reconocerá una sentencia condicional y un bucle.

La sentencia condicional tendrá la forma:

if <expresion> then <cuerpo> end

El bucle a implementar será el “while”, y tendrá la forma:

while <expresión> do <cuerpo> end

FUNCIONES

El lenguaje incluirá funciones predefinidas para algunas tareas útiles, pero también se podrán definir funciones del usuario:

func <nombre>
<cuerpo>
end

Las funciones pueden también usar parámetros o variables locales. Todos los parámetros se pasarán por valor. Al menos en esta etapa.

Las funciones pueden devolver valores usando la instrucción “exit”.

func doble(x:integer)
  exit x+x
end

FUNCIONES DEL SISTEMA

Se implementarán algunas funciones internas, es decir, que no necesitan ser declaradas. Estas son:

print <expresion>

Permite imprimir un valor por consola.

println <expresion>

Permite imprimir un valor por consola, agregando un salto de línea al final.

exit <expresion>

Sale de una función, devolviendo un valor.

length(<expresión>)

Devuelve la longitud de una cadena, como número.

StrToInt(<expresión>)

Devuelve una cadena convertida a número.

IntToStr(<expresión>)

Convierte un número a una cadena.

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 *