Cómo haría para crear un lenguaje de programación?

El título de este artículo refleja una pregunta que escucho una y otra vez en los foros o en los correos electrónicos que recibo.

Creo que todos los desarrolladores curiosos la han formulado al menos una vez. Es normal sentirse fascinado por el funcionamiento de los lenguajes de programación. Por desgracia, la mayoría de las respuestas que leemos son muy académicas o teóricas. Otras contienen demasiados detalles de implementación. Después de leerlas seguimos preguntándonos cómo funcionan las cosas en la práctica.

Así que vamos a responderla. Sí, vamos a ver cuál es el proceso para crear tu propio lenguaje completo con un compilador para él y lo que no.

La visión general

La mayoría de las personas que quieren aprender a «crear un lenguaje de programación» están efectivamente buscando información sobre cómo construir un compilador. Quieren entender la mecánica que permite ejecutar un nuevo lenguaje de programación.
Un compilador es una pieza fundamental del puzzle pero hacer un nuevo lenguaje de programación requiere más que eso:

1) Un lenguaje tiene que ser diseñado: el creador del lenguaje tiene que tomar algunas decisiones fundamentales sobre los paradigmas a utilizar y la sintaxis del lenguaje
2) Hay que crear un compilador
3) Hay que implementar una biblioteca estándar
4) Hay que proporcionar herramientas de apoyo como editores y sistemas de compilación

Veamos más en detalle lo que implica cada uno de estos puntos.

Diseñar un lenguaje de programación

Si sólo quieres escribir tu propio compilador para aprender cómo funcionan estas cosas, puedes saltarte esta fase. Puedes simplemente tomar un subconjunto de un lenguaje existente o idear una simple variación del mismo y empezar. Sin embargo, si tiene planes para crear su propio lenguaje de programación, tendrá que pensarlo un poco.

Pienso que el diseño de un lenguaje de programación se divide en dos fases:

  1. La fase de visión general
  2. La fase de refinamiento

En la primera fase respondemos a las preguntas fundamentales sobre nuestro lenguaje.

  • ¿Qué paradigma de ejecución queremos utilizar? Será imperativo o funcional? O quizás basado en máquinas de estado o reglas de negocio?
  • ¿Queremos tipado estático o tipado dinámico?
  • ¿Qué tipo de programas serán los más adecuados para este lenguaje? ¿Se utilizará para pequeños scripts o para grandes sistemas?
  • ¿Qué nos importa más: el rendimiento? Legibilidad?
  • ¿Queremos que sea similar a un lenguaje de programación existente? ¿Estará dirigido a los desarrolladores de C o será fácil de aprender para quien venga de Python?
  • ¿Queremos que funcione en una plataforma específica (JVM, CLR)?
  • ¿Qué tipo de capacidades de metaprogramación queremos soportar, si es que hay alguna? ¿Macros? ¿Plantillas? ¿Reflexión?

En la segunda fase seguiremos evolucionando el lenguaje a medida que lo usemos. Nos encontraremos con problemas, con cosas que son muy difíciles o imposibles de expresar en nuestro lenguaje y acabaremos evolucionándolo. Puede que la segunda fase no sea tan glamurosa como la primera, pero es la fase en la que seguimos afinando nuestro lenguaje para que sea utilizable en la práctica, así que no debemos subestimarla.

Construir un compilador

Construir un compilador es el paso más emocionante para crear un lenguaje de programación. Una vez que tenemos un compilador podemos realmente dar vida a nuestro lenguaje. Un compilador nos permite empezar a jugar con el lenguaje, usarlo e identificar lo que nos falta en el diseño inicial. Permite ver los primeros resultados. Es difícil superar la alegría de ejecutar el primer programa escrito en nuestro flamante lenguaje de programación, por muy sencillo que sea ese programa.

¿Pero cómo construimos un compilador?

Como todo lo complejo lo hacemos por pasos:

  1. Construimos un parser: el parser es la parte de nuestro compilador que toma el texto de nuestros programas y entiende qué comandos expresan. Reconoce las expresiones, las declaraciones, las clases y crea estructuras de datos internas para representarlas. El resto del parser trabajará con esas estructuras de datos, no con el texto original
  2. (opcional) Traducimos el árbol de parseo a un Árbol de Sintaxis Abstracto. Normalmente las estructuras de datos producidas por el analizador sintáctico son un poco de bajo nivel ya que contienen muchos detalles que no son cruciales para nuestro compilador. Por ello, con frecuencia queremos reordenar las estructuras de datos en algo de nivel un poco más alto
  3. Resolvemos los símbolos. En el código escribimos cosas como a + 1. Nuestro compilador necesita averiguar a qué se refiere a. ¿Es un campo? ¿Es una variable? ¿Es un parámetro de un método? Examinamos el código para responder a eso
  4. Validamos el árbol. Tenemos que comprobar que el programador no ha cometido errores. ¿Está intentando sumar un booleano y un int? O accediendo a un campo inexistente? Necesitamos producir mensajes de error apropiados
  5. Generamos el código máquina. En este punto traducimos el código en algo que la máquina pueda ejecutar. Puede ser código máquina propio o bytecode para alguna máquina virtual
  6. (opcional) Realizamos el linking. En algunos casos necesitamos combinar el código máquina producido para nuestros programas con el código de las bibliotecas estáticas que queremos incluir, para generar un único ejecutable

¿Necesitamos siempre un compilador? No. Podemos sustituirlo por otros medios para ejecutar el código:

  • Podemos escribir un intérprete: un intérprete es sustancialmente un programa que hace los pasos 1-4 de un compilador y luego ejecuta directamente lo especificado por el Árbol de Sintaxis Abstracto
  • Podemos escribir un transpilador: un transpilador hará lo especificado en los pasos 1-4 y luego dará salida a algún código en algún lenguaje para el que ya tengamos un compilador (por ejemplo C++ o Java)

Estas dos alternativas son perfectamente válidas y con frecuencia tiene sentido elegir una de estas dos porque el esfuerzo requerido suele ser menor.

Escribimos un artículo explicando cómo escribir un transpilador. Échale un vistazo si quieres ver un ejemplo práctico, con código.

En este artículo explicamos con más detalle la diferencia entre un compilador y un intérprete.

Una biblioteca estándar para tu lenguaje de programación

Cualquier lenguaje de programación necesita hacer unas cuantas cosas:

  • Imprimir en la pantalla
  • Acceder al sistema de archivos
  • Utilizar conexiones de red
  • Crear GUIs

Estas son las funcionalidades básicas para interactuar con el resto del sistema. Sin ellas un lenguaje es básicamente inútil. ¿Cómo proporcionamos estas funcionalidades? Creando una biblioteca estándar. Esta será un conjunto de funciones o clases que pueden ser llamadas en los programas escritos en nuestro lenguaje de programación pero que serán escritos en algún otro lenguaje. Por ejemplo, muchos lenguajes tienen bibliotecas estándar escritas al menos parcialmente en C.

Una biblioteca estándar puede entonces contener mucho más. Por ejemplo, clases para representar las principales colecciones como listas y mapas, o para procesar formatos comunes como JSON o XML. A menudo contendrá funcionalidades avanzadas para procesar cadenas y expresiones regulares.

En otras palabras, escribir una biblioteca estándar es mucho trabajo. No es glamuroso, no es conceptualmente tan interesante como escribir un compilador, pero sigue siendo un componente fundamental para hacer viable un lenguaje de programación.

Hay formas de evitar este requisito. Una es hacer que el lenguaje corra en alguna plataforma y que sea posible reutilizar la biblioteca estándar de otro lenguaje. Por ejemplo, todos los lenguajes que se ejecutan en la JVM pueden simplemente reutilizar la biblioteca estándar de Java.

Herramientas de apoyo para un nuevo lenguaje de programación

Para que un lenguaje sea utilizable en la práctica, a menudo necesitamos escribir algunas herramientas de apoyo.

La más obvia es un editor. Un editor especializado con resaltado de sintaxis, comprobación de errores en línea y autocompletado es hoy en día algo imprescindible para que cualquier desarrollador sea productivo.

Pero hoy en día los desarrolladores están mimados y esperan todo tipo de herramientas de apoyo. Por ejemplo, un depurador podría ser realmente útil para hacer frente a un error desagradable. O un sistema de construcción similar a maven o gradle podría ser algo que los usuarios pedirán más adelante.

Al principio un editor podría ser suficiente, pero a medida que su base de usuarios crece también la complejidad de los proyectos crecerá y se necesitarán más herramientas de apoyo. Esperemos que en ese momento haya una comunidad dispuesta a ayudar a construirlas.

Resumen

Crear un lenguaje de programación es un proceso que parece misterioso para muchos desarrolladores. En este artículo tratamos de mostrar que es sólo un proceso. Es fascinante y no es fácil, pero se puede hacer.

Usted puede querer construir un lenguaje de programación por una variedad de razones. Una buena razón es por diversión, otra es para aprender cómo funcionan los compiladores. Tu lenguaje puede acabar siendo muy útil o no, dependiendo de muchos factores. Sin embargo, si te diviertes y/o aprendes mientras lo construyes, entonces vale la pena invertir algo de tiempo en esto.

Y, por supuesto, podrás presumir con tus compañeros desarrolladores.

Si quieres aprender más sobre la creación de un lenguaje echa un vistazo a los otros recursos que hemos creado: aprender a construir lenguajes.

También te pueden interesar algunos de nuestros artículos:

Deja una respuesta

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