Creación de lenguajes JVM utilizables: una descripción general
Publicado: 2022-03-11Hay varias razones posibles para crear un lenguaje, algunas de las cuales no son inmediatamente obvias. Me gustaría presentarlos junto con un enfoque para crear un lenguaje para la Máquina Virtual Java (JVM) reutilizando las herramientas existentes tanto como sea posible. De esta forma, reduciremos el esfuerzo de desarrollo y proporcionaremos una cadena de herramientas familiar para el usuario, lo que facilitará la adopción de nuestro nuevo lenguaje de programación.
En este artículo, el primero de la serie, presentaré una descripción general de la estrategia y varias herramientas involucradas en la creación de nuestro propio lenguaje de programación para JVM. en artículos futuros, profundizaremos en los detalles de implementación.
¿Por qué crear su lenguaje JVM?
Ya hay una infinidad de lenguajes de programación. Entonces, ¿por qué molestarse en crear uno nuevo? Hay muchas respuestas posibles a eso.
En primer lugar, hay muchos tipos diferentes de lenguajes: ¿quieres crear un lenguaje de programación de propósito general (GPL) o uno específico de dominio? El primer tipo incluye lenguajes como Java o Scala: lenguajes destinados a escribir soluciones lo suficientemente decentes para un gran conjunto de problemas. En cambio, los lenguajes específicos de dominio (DSL) se enfocan en resolver muy bien un conjunto específico de problemas. Piense en HTML o Latex: podría dibujar en la pantalla o generar documentos en Java pero sería engorroso, con estos DSL en cambio puede crear documentos muy fácilmente pero están limitados a ese dominio específico.
Entonces, tal vez haya un conjunto de problemas en los que trabaja muy a menudo y para los cuales podría tener sentido crear un DSL. Un lenguaje que te haría muy productivo mientras resuelves el mismo tipo de problemas una y otra vez.
Quizás, en cambio, desee crear una GPL porque tiene algunas ideas nuevas, por ejemplo, para representar las relaciones como ciudadanos de primera clase o representar el contexto.
Finalmente, es posible que desee crear un nuevo idioma porque es divertido, genial y porque aprenderá mucho en el proceso.
El hecho es que si apunta a la JVM puede obtener un lenguaje utilizable con un esfuerzo reducido, eso es porque:
- Solo necesita generar bytecode y su código estará disponible en todas las plataformas donde hay una JVM
- Podrá aprovechar todas las bibliotecas y marcos existentes para la JVM
Por lo tanto, el costo de desarrollar un lenguaje se reduce considerablemente en la JVM y podría tener sentido crear nuevos lenguajes en escenarios que serían antieconómicos fuera de la JVM.
¿Qué necesitas para que sea utilizable?
Hay algunas herramientas que absolutamente necesita para usar su idioma: un analizador y un compilador (o un intérprete) se encuentran entre estas herramientas. Sin embargo, esto no es suficiente. Para que su lenguaje sea realmente utilizable en la práctica, debe proporcionar muchos otros componentes de la cadena de herramientas, posiblemente integrándose con las herramientas existentes.
Idealmente, usted quiere ser capaz de:
- Administre referencias al código compilado para la JVM desde otros lenguajes
- Edite archivos de origen en su IDE favorito con resaltado de sintaxis, identificación de errores y finalización automática
- Desea poder compilar archivos utilizando su sistema de compilación favorito: maven, gradle u otros
- Desea poder escribir pruebas y ejecutarlas como parte de su solución de integración continua
Si puedes hacer eso, adoptar tu idioma será mucho más fácil.
Entonces, ¿cómo podemos lograr eso? En el resto de la publicación, examinamos las diferentes piezas que necesitamos para hacer esto posible.
Análisis y compilación
Lo primero que debe hacer para transformar sus archivos fuente en un programa es analizarlos, obteniendo una representación Abstract-Syntax-Tree (AST) de la información contenida en el código. En ese momento, deberá validar el código: ¿hay errores sintácticos? ¿Errores semánticos? Debe encontrarlos todos e informarlos al usuario. Si todo va bien, aún necesita resolver los símbolos. Por ejemplo, ¿"Lista" se refiere a java.util.List o java.awt.List ? Cuando invoca un método sobrecargado, ¿cuál está invocando? Finalmente, necesita generar un código de bytes para su programa.
Entonces, desde el código fuente hasta el código de bytes compilado, hay tres fases principales:
- Construyendo un AST
- Analizando y transformando el AST
- Producir el bytecode del AST
Veamos esas fases en detalle.
Construir un AST : el análisis es una especie de problema resuelto. Existen muchos marcos, pero le sugiero que use ANTLR. Es bien conocido, está bien mantenido y tiene algunas características que facilitan la especificación de gramáticas (maneja reglas menos recursivas; no es necesario que lo entienda, ¡pero agradezca que lo haga!).
Analizar y transformar el AST : escribir un sistema de tipos, validación y resolución de símbolos podría ser un desafío y requerir bastante trabajo. Este tema solo requeriría una publicación separada. Por ahora, considere que esta es la parte de su compilador en la que va a dedicar la mayor parte del esfuerzo.
Producir el código de bytes del AST : esta última fase en realidad no es tan difícil. Debería haber resuelto los símbolos en la fase anterior y preparado el terreno para que, básicamente, pueda traducir los nodos individuales de su AST transformado a una o unas pocas instrucciones de código de bytes. Las estructuras de control pueden requerir algo de trabajo extra porque vas a traducir tus bucles for, interruptores, ifs, etc. en una secuencia de saltos condicionales e incondicionales (sí, debajo de tu hermoso lenguaje todavía habrá un montón de gotos). Debe aprender cómo funciona la JVM internamente, pero la implementación real no es tan difícil.
Integración con otros idiomas
Cuando haya obtenido la dominación mundial para su idioma, todo el código se escribirá usándolo exclusivamente. Sin embargo, como paso intermedio, su idioma probablemente se usará junto con otros idiomas de JVM. Quizás alguien comience a escribir un par de clases o pequeños módulos en su idioma dentro de un proyecto más grande. Es razonable esperar poder mezclar varios lenguajes JVM. Entonces, ¿cómo afecta sus herramientas de lenguaje?
Es necesario considerar dos escenarios diferentes:
- Tu idioma y los demás viven en módulos compilados por separado
- Su idioma y los demás viven en los mismos módulos y se compilan juntos
En el primer escenario, su código solo necesita usar código compilado escrito en otros idiomas. Por ejemplo, algunas dependencias como Guava o módulos en el mismo proyecto se pueden compilar por separado. Este tipo de integración requiere dos cosas: primero, debe poder interpretar los archivos de clase producidos por otros lenguajes para resolver los símbolos y generar el código de bytes para invocar esas clases. El segundo punto es similar al primero: es posible que otros módulos deseen reutilizar el código escrito en su idioma después de haberlo compilado. Ahora, normalmente eso no es un problema porque Java puede interactuar con la mayoría de los archivos de clase. Sin embargo, aún podría escribir archivos de clase que sean válidos para JVM pero que no se puedan invocar desde Java (por ejemplo, porque usa identificadores que no son válidos en Java).

El segundo escenario es más complicado: suponga que tiene una clase A definida en código Java y una clase B escrita en su idioma. Supongamos que las dos clases se refieren entre sí (por ejemplo, A podría extender B y B podría aceptar A como parámetro para el mismo método). Ahora, el punto es que el compilador de Java no puede procesar el código en su idioma, por lo que debe proporcionarle un archivo de clase para la clase B. Sin embargo, para compilar la clase B, debe insertar referencias a la clase A. Entonces, lo que debe hacer es tener una especie de compilador parcial de Java, que dado un archivo fuente de Java puede interpretarlo y producir un modelo del mismo que puede usar para compilar su clase B. Tenga en cuenta que esto requiere que pueda analizar el código Java (usando algo así como JavaParser) y resolver símbolos. Si no tiene idea de por dónde empezar, eche un vistazo a java-symbol-solver.
Herramientas: Gradle, Maven, Test Frameworks, CI
La buena noticia es que puede hacer que el hecho de que están usando un módulo escrito en su idioma sea totalmente transparente para el usuario al desarrollar un complemento para gradle o maven. Puede indicar al sistema de compilación que compile archivos en su lenguaje de programación. El usuario seguirá ejecutando mvn compilar o gradle ensamblar y no notará ninguna diferencia.
La mala noticia es que escribir complementos de Maven no es fácil: la documentación es muy pobre, no es inteligible y en su mayoría está desactualizada o simplemente es incorrecta . Sí, no suena reconfortante. Todavía no he escrito complementos de Gradle, pero parece mucho más fácil.
Tenga en cuenta que también debe considerar cómo se pueden ejecutar las pruebas con el sistema de compilación. Para las pruebas de soporte, debe pensar en un marco muy básico para las pruebas unitarias y debe integrarlo con el sistema de compilación, de modo que la ejecución de la prueba maven busque pruebas en su idioma, las compile y las ejecute informando el resultado al usuario.
Mi consejo es mirar los ejemplos disponibles: uno de ellos es el complemento Maven para el lenguaje de programación Turín.
Una vez que lo haya implementado, todos deberían poder compilar fácilmente archivos fuente escritos en su idioma y usarlos en servicios de integración continua como Travis.
Complemento IDE
Un complemento para un IDE será la herramienta más visible para sus usuarios y algo que afectará en gran medida la percepción de su idioma. Un buen complemento puede ayudar al usuario a aprender el idioma al proporcionar autocompletado inteligente, errores contextuales y refactorizaciones sugeridas.
Ahora, la estrategia más común es elegir un IDE (típicamente Eclipse o IntelliJ IDEA) y desarrollar un complemento específico para él. Esta es probablemente la pieza más compleja de su cadena de herramientas. Este es el caso por varias razones: en primer lugar, no puede reutilizar razonablemente el trabajo que dedicará a desarrollar su complemento para un IDE para los demás. Su Eclipse y su complemento IntelliJ van a estar totalmente separados. El segundo punto es que el desarrollo de complementos IDE es algo poco común, por lo que no hay mucha documentación y la comunidad es pequeña. Significa que tendrás que pasar mucho tiempo resolviendo las cosas por ti mismo. Personalmente desarrollé complementos para Eclipse y para IntelliJ IDEA. Mis preguntas en los foros de Eclipse permanecieron sin respuesta durante meses o años. En los foros de IntelliJ tuve mejor suerte y, a veces, recibí una respuesta de los desarrolladores. Sin embargo, la base de usuarios de los desarrolladores de complementos es más pequeña y la API es muy bizantina. Prepárate para sufrir.
Hay una alternativa a todo esto, y es utilizar Xtext. Xtext es un marco para desarrollar complementos para Eclipse, IntelliJ IDEA y la web. Ha nacido en Eclipse y se ha ampliado recientemente para admitir otras plataformas, por lo que no hay mucha experiencia en eso, pero podría ser una alternativa digna de ser considerada. Permítanme aclarar esto: la única forma de desarrollar un complemento muy bueno es desarrollarlo utilizando la API nativa de cada IDE. Sin embargo, con Xtext puede tener algo razonablemente decente con una fracción del esfuerzo: solo le da la sintaxis de su idioma y obtiene errores de sintaxis/finalización de forma gratuita. Aún así, debe implementar la resolución de símbolos y las partes difíciles, pero este es un punto de partida muy interesante; sin embargo, las partes difíciles son la integración con las bibliotecas específicas de la plataforma para resolver los símbolos de Java, por lo que esto realmente no resolverá todos sus problemas.
Conclusiones
Hay muchas formas en las que podría perder usuarios potenciales que mostraron interés en su idioma. Adoptar un nuevo idioma es un desafío porque requiere aprenderlo y adaptar nuestros hábitos de desarrollo. Al reducir tanto como sea posible el desgaste y aprovechar el ecosistema ya conocido por sus usuarios, puede evitar que los usuarios se rindan antes de aprender y enamorarse de su idioma.
En el escenario ideal, su usuario podría clonar un proyecto simple escrito en su idioma y construirlo usando las herramientas estándar (Maven o Gradle) sin notar ninguna diferencia. Si desea editar el proyecto, puede abrirlo en su editor favorito y el complemento lo ayudará a señalarle los errores y brindarle terminaciones inteligentes. Este es un escenario muy diferente a tener que descubrir cómo invocar su compilador y editar archivos usando el bloc de notas. El ecosistema en torno a su idioma realmente puede marcar la diferencia, y hoy en día se puede construir con un esfuerzo razonable.
Mi consejo es que seas creativo en tu lenguaje, pero no en tus herramientas. Reduzca las dificultades iniciales que las personas deben enfrentar para adoptar su idioma utilizando estándares familiares.
¡Feliz diseño de lenguaje!
Lecturas adicionales en el blog de ingeniería de Toptal:
- Cómo abordar la escritura de un intérprete desde cero