
Introduccion
Le doy la bienvenida, interesado en leer a los desarrolladores en cualquier idioma en el que oriente estos artículos y cuyo apoyo y opiniones valoro.
Para empezar, de acuerdo con las tradiciones establecidas, proporcionaré enlaces a artículos anteriores:
Parte 1: escribir un lenguaje VMParte 2: presentación intermedia de programasPara formar en su cabeza una comprensión completa de lo que escribimos en estos artículos, debe familiarizarse con las partes anteriores de antemano.
Además, debería publicar inmediatamente un enlace a un artículo sobre un proyecto que escribí anteriormente y en el que se basa todo este informe:
Clack syudy . Quizás valga la pena conocerlo primero.
Y un poco sobre el proyecto:
→
Pequeño sitio del proyecto→
Repositorio de GitHubBueno, también diré de inmediato que todo está escrito en Object Pascal, es decir, en FPC.
Entonces comencemos.
El principio de funcionamiento de la mayoría de los traductores.
En primer lugar, vale la pena entender que no podría escribir nada que valga la pena sin antes familiarizarme con un montón de material teórico y varias estatuas. Describiré lo principal en un par de palabras.
La tarea del traductor es, en primer lugar, preparar el código para el análisis (por ejemplo, eliminar comentarios) y dividir el código en tokens (un token es el conjunto de caracteres mínimo significativo para el idioma).
Luego, analizándolo y transformándolo, necesitamos analizar el código en una representación intermedia determinada y luego ensamblar la aplicación lista para su ejecución o ... ¿Qué necesita recolectar en general?
Sí, realmente no dije nada con este montón de texto, sin embargo, ahora la tarea se divide en varias subtareas.
Omitamos cómo se prepara el código para la ejecución, porque Es demasiado aburrido describir un proceso. Supongamos que tenemos un conjunto de tokens listos para el análisis.
Análisis de código
Es posible que haya escuchado sobre la construcción de un árbol de código y su análisis, o incluso cosas más abstrusas. Como siempre, esto no es más que abarrotar los terribles términos simples. Por análisis de código, me refiero a un conjunto de acciones mucho más simple. La tarea es revisar la lista de tokens y analizar el código, cada uno de sus componentes.
Como regla general, en lenguajes imperativos, el código ya se presenta en forma de un árbol a partir de estructuras.
Debe admitir que no es aceptable comenzar el ciclo "A" en el cuerpo del ciclo "B" y finalizarlo fuera del cuerpo del ciclo "B". Un código es una estructura que consiste en un conjunto de construcciones.
¿Y qué tiene cada diseño? Así es, el principio y el final (y tal vez algo más en el medio, pero no el punto).
En consecuencia, el análisis de código puede hacerse de una sola pasada, realmente sin construir un árbol.
Para hacer esto, necesita un bucle que se ejecutará a través del código y una gran caja de interruptores que realizará el análisis y análisis del código principal.
Es decir corremos a través de los tokens, tenemos un token (por ejemplo, que sea ...) "if" - Realmente dudo que tal token pueda estar en el código así -> este es el comienzo de if..then [.. else] .. end construction!
Analizamos todos los tokens posteriores, respectivamente, para la construcción de condiciones en nuestro idioma.
Un poco sobre errores de código
En la etapa de análisis de estructuras y ejecución a lo largo del código, es mejor no puntuar el procesamiento de errores. Esta es una funcionalidad útil para el traductor. Si se produce un error durante el análisis de la estructura, entonces es lógico: la estructura no está construida correctamente y debe notificar al desarrollador.
Ahora sobre Mash. ¿Cómo se analiza el lenguaje?
Arriba, describí un concepto generalizado de un dispositivo traductor. Ahora es el momento de hablar sobre mi trabajo.
De hecho, el traductor resultó ser muy similar al descrito anteriormente. Pero para mí no divide el código en un montón de tokens para su posterior análisis.
Antes de comenzar el análisis, el código se presenta en una forma más bella. Los comentarios se eliminan y todas las construcciones se combinan en líneas largas si se describen en varias líneas.
Por lo tanto, en cada línea tomada por separado hay una construcción del lenguaje o su parte. Esto es genial, ahora podemos analizar cada línea en nuestra gran caja de interruptores, en lugar de buscar estas construcciones en el conjunto de tokens. Además, la ventaja aquí es que la línea tiene un final y es más fácil determinar errores en la construcción con este enfoque.
En consecuencia, el análisis de estructuras individuales se realiza por métodos separados, que devuelven una representación intermedia del código de estructuras o sus partes.
P.S. En un artículo anterior, describí la construcción de un traductor de un idioma intermedio para bytecode para una VM. En realidad, este lenguaje intermedio es una representación intermedia.
Debe entenderse que las estructuras pueden consistir en varias estructuras más simples. Porque Dado que analizamos cada estructura por métodos separados, podemos llamarlas fácilmente entre sí al analizar cada estructura.

Calentamiento ejecutado en el código
Para empezar, el traductor debe familiarizarse rápidamente con el código, revisarlo y prestar atención a algunos diseños.
En este punto, puede lidiar con variables globales, usar construcciones, así como importaciones, procedimientos y funciones, y construcciones OOP.
Es mejor generar una vista intermedia en varios objetos para el almacenamiento, de modo que
pegue el código para las variables globales después de la inicialización, pero antes del inicio de main ().
El código para las construcciones OOP se puede insertar al final.
Diseños sofisticados
Ok, descubrimos los diseños simples. Ahora es el momento de lo complicado. No creo que hayas logrado olvidar el ejemplo con dos ciclos. Como sabemos, las estructuras generalmente vienen en forma de una especie de árbol. Esto significa que podemos analizar estructuras complejas usando la pila.
¿Qué tiene que ver la pila con eso? Por otra parte
Primero, describimos la clase que empujaremos a la pila. Al analizar construcciones complejas, podemos generar una representación intermedia para el principio y el final de este bloque, por ejemplo, analizamos el ciclo for, while, hasta, si construye, métodos y, de hecho, todo en el lenguaje Mash.
Esta clase necesita campos para almacenar representaciones intermedias, metainformación (para algunas construcciones variables) y, por supuesto, para almacenar un tipo de bloque.
Solo daré el código completo, porque no hay mucho aquí:
unit u_prep_codeblock; {$mode objfpc}{$H+} interface uses Classes, SysUtils; type TBlockEntryType = (btProc, btFunc, btIf, btFor, btWhile, btUntil, btTry, btClass, btSwitch, btCase); TCodeBlock = class(TObject) public bType: TBlockEntryType; mName, bMeta, bMCode, bEndCode: string; constructor Create(bt: TBlockEntryType; MT, MC, EC: string); end; implementation constructor TCodeBlock.Create(bt: TBlockEntryType; MT, MC, EC: string); begin inherited Create; bType := bt; bMeta := MT; bMCode := MC; bEndCode := EC; end; end.
Bueno, la pila es un simple TList, reinventar la rueda aquí es simplemente estúpido.
Por lo tanto, analizando la construcción, digamos lo mismo mientras el bucle se ve así:
function ParseWhile(s: string; varmgr: TVarManager): string; var WhileNum, ExprCode: string; begin Delete(s, 1, 5);
Acerca de las expresiones matemáticas
Es posible que no haya notado esto, pero las expresiones matemáticas / lógicas también son código estructurado.
Implementé su análisis de forma apilada. Primero, todos los elementos individuales de la expresión se insertan en la pila, luego, en varias pasadas, se genera el código para la representación intermedia.
Varias veces, porque Hay operaciones matemáticas prioritarias, como la multiplicación.
No veo el punto aquí, porque Hay muchos y aburridos.
P.S. /lang/u_prep_expressions.pas: aquí está completamente expuesto para su revisión.
Resumen
Entonces, hemos implementado un traductor que puede convertir ... Por ejemplo, este es el código:
proc PrintArr(arr): for(i ?= 0; i < len(arr); i++): PrintLn("arr[", i, "] = ", arr[i]) end end proc main(): var arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] PrintArr(arr) InputLn() end
¿Qué falta en nuestro idioma? Derecha, apoyo OOP. Hablaremos de esto en mi próximo artículo.
Gracias por leer hasta el final si lo hiciste.
Si algo no está claro para usted, entonces estoy esperando sus comentarios. O preguntas en el
foro , sí ... Sí, lo reviso a veces.
Y ahora una pequeña encuesta (para que la vea y disfrute de la importancia de mis artículos):