Escribimos nuestro lenguaje de programación, parte 2: representación intermedia de programas

imagen

Introduccion


Saludos a todos los que vinieron a leer mi próximo artículo.

Repito, describo la creación de un lenguaje de lenguaje de programación basado en trabajos previos, cuyos resultados se describen en esta publicación .

En la primera parte (enlace: habr.com/post/435202 ) describí las etapas de diseño y escritura de un lenguaje VM que ejecutará nuestras aplicaciones futuras en nuestro idioma futuro.
En este artículo, planeo describir las etapas principales de la creación de un lenguaje de programación intermedio que se ensamblará en un bytecode abstracto para ejecución directa en nuestra VM.

Creo que no está de más proporcionar enlaces de inmediato al sitio web del proyecto y su repositorio.

Sitio
Repositorio

Debo decir de inmediato que todo el código está escrito en FPC y daré ejemplos al respecto.

Entonces, comenzamos nuestra iluminación.

¿Por qué rendimos el lenguaje intermedio?


Vale la pena entender que convertir un programa de un lenguaje de alto nivel inmediatamente a un bytecode ejecutable, que consiste en un conjunto limitado de instrucciones, es tan trivial que es mejor simplificarlo en un orden de magnitud agregando un lenguaje intermedio al proyecto. Es mucho mejor simplificar el código gradualmente que presentar inmediatamente expresiones, estructuras y clases matemáticas con un conjunto de códigos de operación. Por cierto, esta es la forma en que funcionan la mayoría de los traductores y compiladores de terceros.

En mi artículo anterior, escribí sobre cómo implementar un lenguaje VM. Ahora necesitamos implementar un lenguaje similar al ensamblador para él y funcionalidad para seguir escribiendo el traductor. En estas etapas, sentamos las bases para el futuro proyecto. Vale la pena entender que cuanto mejor sea la base, más empinada será la construcción.

Damos el primer paso para realizar este milagro.


Para empezar, vale la pena establecer una meta. ¿Qué escribiremos realmente? ¿Qué características debe tener el código final y qué debe hacer?

Puedo crear una lista de las principales partes funcionales en las que debe consistir esta parte del proyecto:

  • Ensamblador simple. Convierte instrucciones simples en un conjunto de códigos de operación para máquinas virtuales.
  • La implementación básica del funcional para implementar variables.
  • La implementación básica de lo funcional para trabajar con constantes.
  • Funcionalidad para admitir puntos de entrada a métodos y calcular sus direcciones en la etapa de traducción.
  • Quizás un par de bollos más funcionales.

La ilustración anterior muestra un fragmento de código en un lenguaje intermedio que se convierte en código para una VM por un traductor primitivo, que se discutirá.

Entonces, los objetivos están establecidos, procedamos a la implementación.

Escribir un ensamblador simple


Nos preguntamos qué es ensamblador?

De hecho, este es un programa que realiza la sustitución de códigos de operación en lugar de sus descripciones textuales.

Considera este código:

push 0 push 1 add peek 2 pop 

Después de procesar el código del ensamblador, obtenemos el código ejecutable para la VM.

Vemos que las instrucciones pueden ser monosilábicas y bisilábicas. No más instrucciones complicadas para la VM apilada.

Necesitamos un código que pueda extraer tokens de una cadena (tenemos en cuenta que puede haber cadenas entre ellos).

Lo escribimos:

 function Tk(s: string; w: word): string; begin Result := ''; while (length(s) > 0) and (w > 0) do begin if s[1] = '"' then begin Delete(s, 1, 1); Result := copy(s, 1, pos('"', s) - 1); Delete(s, 1, pos('"', s)); s := trim(s); end else if Pos(' ', s) > 0 then begin Result := copy(s, 1, pos(' ', s) - 1); Delete(s, 1, pos(' ', s)); s := trim(s); end else begin Result := s; s := ''; end; Dec(w); end; end; 

Ok, ahora necesitamos implementar algo así como una construcción de mayúsculas y minúsculas para cada declaración, y nuestro ensamblador simple está listo.

Variables


Recuerde que nuestra VM tiene una variedad de punteros para admitir variables y, en consecuencia, direccionamiento estático. Esto significa que lo funcional para trabajar con variables se puede representar como una TStringList, en la que las cadenas son los nombres de las variables y sus índices son sus direcciones estáticas. Debe entenderse que la duplicación de nombres de variables en esta lista es inaceptable. Creo que puedes imaginar el código necesario y / o incluso escribirlo tú mismo.

Si desea ver la implementación finalizada, entonces es bienvenido: /lang/u_variables.pas

Constantes


El principio aquí es el mismo que con las variables, pero hay una cosa. Para optimizar, es mejor vincular no a los nombres de las constantes, sino a sus valores. Es decir cada valor constante puede tener una TStringList, que servirá para almacenar los nombres de las constantes con este valor.
Para las constantes, debe especificar el tipo de datos y, en consecuencia, para agregarlos al idioma, deberá escribir un pequeño analizador.

Implementación: /lang/u_consts.pas

Método Puntos de entrada


Para implementar el bloqueo de código, soporte para diferentes diseños, etc. El soporte para esta funcionalidad debe implementarse a nivel de ensamblador.

Considere un ejemplo de código:

 Summ: peek 0 pop peek 1 pop push 0 new peek 2 mov push 2 push 0 add jr 

Lo anterior es un ejemplo de traducción del método Summ:

 func Summ(a, b): return a + b end 

Debe entenderse que no hay códigos de operación para los puntos de entrada. ¿Qué es un punto de entrada al método Summ? Este número primo es el desplazamiento del siguiente punto de entrada del código de operación. (el desplazamiento del código de operación es el número del código de operación relativo al comienzo del código de bytes abstracto ejecutable). Ahora tenemos una tarea: necesitamos calcular este desplazamiento en la etapa de compilación y, como opción, declarar la constante Summ como este número.

Escribimos para esto un cierto contador de peso para cada operador. Tenemos operadores monosilábicos simples, por ejemplo, "pop". Ocupan 1 byte. Hay otros más complejos, por ejemplo, "push 123": ocupan 5 bytes, 1 para el código de operación y 4 para el tipo int sin signo.

La esencia del código para agregar soporte para el ensamblador de puntos de entrada:

  1. Tenemos un contador, digamos i = 0.
  2. Revisamos el código, si tenemos una construcción del tipo "push 123", luego le agregamos 5, si el código de operación simple es 1. Si tenemos un punto de entrada, elimínelo del código y declare la constante correspondiente con el valor del contador y el nombre del punto de entrada.

Otra funcionalidad


Esto, por ejemplo, es una conversión de código simple antes del procesamiento.

Resumen


Hemos implementado nuestro pequeño ensamblador. Lo necesitaremos para implementar un traductor más complejo basado en él. Ahora podemos escribir pequeños programas para nuestra VM. En consecuencia, en otros artículos se describirá el proceso de escribir un traductor más complejo.

Gracias por leer hasta el final si lo hiciste.

Si algo no está claro para usted, entonces estoy esperando sus comentarios.

Source: https://habr.com/ru/post/es435258/


All Articles