¿Qué se necesita para hacer un compilador para C ++?

Intentaré darle una descripción general de alto nivel de cómo un compilador toma el código de alto nivel que escribe y lo convierte al lenguaje de bajo nivel que luego se ejecuta en su computadora.

Un compilador se puede dividir en 2 partes, el front-end y el back-end. Estos finales son muy diferentes de lo que los desarrolladores web y de aplicaciones suelen discutir.

El front-end es responsable de tomar el código de alto nivel que usted escribe y producir el código IR. IR es la abreviatura de Representación intermedia.

int x, y;
y = 3 + x;

Se traducirá a algo como:

var x
var y
mov x r1
addi r1 3
mov r1 y

Este proceso se realiza mediante tres bloques principales:

  1. Escáner : toma un archivo de texto sin formato, por ejemplo, main.cpp , y reconoce palabras clave, puntuación, corchetes, nombres, etc., generalmente a través de una especificación de expresiones regulares que proporciona el desarrollador.
    Flex es uno de esos programas populares que escanea en función de la expresión regular que se le proporciona.
  2. Analizador : Esto toma los tokens del escáner y localiza patrones sensibles en el código. Estos “patrones” son definidos por el desarrollador que sigue los estándares del lenguaje.
    Estos ‘patrones’ se llaman gramática del analizador sintáctico.

func_decl -> any_type id open_brace decl_stmt_list close_brace

  1. Lo anterior sería uno de esos ejemplos en los que se ha declarado un ‘patrón’ para una función.
    Bison es uno de esos programas populares que analiza según las reglas que se le proporcionan.
  2. Conversión a IR : este es un paso donde los patrones del analizador (pueden almacenarse como un árbol de sintaxis abstracta o las reglas para crear IR pueden escribirse junto con la gramática del analizador) se convierten en el IR del que hablamos. El IR depende completamente del desarrollador y no requiere el seguimiento de los estándares.
    Se puede hacer alguna optimización en esta etapa.

En este punto, tiene un código que se parece al código de la máquina, pero que no funcionará en ninguna máquina y no está optimizado por completo.
El siguiente paso es pasar de:

var x
var y
var z
mov x r1
addi r1 3
mov r1 y // y = x + 3
mov x r2
addi r2 3
mov r2 z // z = x + 3

a algo como esto (código de ensamblaje en una computadora con arquitectura MIPS):

LW $ 1, addr_x
ADDI $ 1, $ 1, 3
SW $ 1, addr_y
SW $ 1, addr_z

Cue, el back-end . Este segmento es responsable de:

  1. Optimizador : estas optimizaciones van más allá de lo que se ha hecho en un alto nivel en la etapa de conversión a IR. Algunos ejemplos de optimizaciones serían la optimización de bucle, paralelización, eliminación de código muerto, eliminación de subexpresión común y más.
    En el ejemplo anterior, verá cómo las variables y y z almacenan x + 3 pero el IR calcula los valores nuevamente. Esto no está presente en la asamblea.
  2. Generación de código : este paso básicamente convierte el código optimizado en código de ensamblaje funcional basado en la máquina. Este paso básicamente se asigna al ensamblaje mientras se ocupa de factores como los comandos específicos del ensamblaje, los registros disponibles, etc.

Los pasos anteriores son un esquema de alto nivel y en muchos compiladores, algunos pasos pueden fusionarse, saltarse o desglosarse incluso en pequeñas partes secundarias.

La razón para diferenciar el front-end del back-end es para que pueda:

  • Reutilice el back-end para varios idiomas cambiando un front-end por otro.
    • Básicamente, lo que esto significa es que podría escribir un front-end para, por ejemplo, Java y C ++ y ambos crearían un IR que puede ser utilizado por el mismo back-end.
  • Cambie el back-end por varias máquinas / arquitecturas para las que desea construir.
    • Hay varios lenguajes de ensamblaje (MIPS, ARM, PC de IBM …) y cada uno es extremadamente diferente del otro. Algunas optimizaciones pueden aplicarse para una, pero no para la otra, y obviamente, el paso de generación de código se verá muy diferente. Por lo tanto, la capacidad de cambiar y elegir back-end.

Espero que esta respuesta ayude a desmitificar un poco el compilador.
El compilador es un software maravilloso y extremadamente complicado que tiene muchas complejidades complejas que surgen de la necesidad de compilar rápidamente (y en paralelo), optimizar el código generado y garantizar que funcione para varios idiomas en varias máquinas sin fallar.

¡Así que la próxima vez que invoques a tu compilador, piensa en todo el trabajo que se ha dedicado a ayudarte a crear algo nuevo y maravilloso!

Sesos. Matemáticas. Algoritmos Analizador Analizador léxico. Lógica .. La lista es infinita para hacer un compilador perfecto para cualquier idioma, para ser honesto.

Ni siquiera está cerca de un programa regular que puedes escribir mientras trabajas en la tarea. NI SIQUIERA CERCA.

Sin embargo, para ponerlo en términos más simples, si desea construir algo que convierta su código en código de máquina y ejecute su programa (C ++?), Necesitará (al menos) un analizador léxico / analizador de texto que analice las decenas de miles de caracteres (léxicos) en su código, los organiza y los comprende de acuerdo con todas las combinaciones significativas posibles, y luego los interpreta como códigos significativos, y finalmente convierte ese significado entendido en código de máquina que puede introducirse en la máquina según los estándares establecido por su Asamblea.

Antes de que piense que es fácil, cada compañía que ha lanzado su propio compilador / intérprete / analizador ha tenido decenas (o cientos) de los cerebros más inteligentes en ese campo de trabajo de programación para construir ese compilador y otras pocas decenas de personas que lo probaron.

La perspectiva que necesita tener al escribir su propio compilador es completamente diferente de la que tendría al escribir incluso los programas más difíciles.

Recomiendo investigar algunos documentos escritos por personas que realmente han creado compiladores para Intel e IBM.

El diseño del compilador es un tema muy extenso, pero lo esencial es que el compilador toma el programa de alto nivel y lo separa en tokens.

Los tokens son de varios tipos, como nombre de variable, operador, puntuación, constante o literal, etc.

Después de identificar los tokens, el compilador usa lo que se llama la gramática del lenguaje para “analizar” el código o darle sentido.

Entonces, después de analizar, el compilador sabe lo que se está haciendo en cada declaración. Después de ver lo que se está haciendo en cada declaración, el compilador verifica si este ‘qué’ es correcto. Es decir, está agregando 2 enteros juntos o bromeando agregando un entero y una cadena.

Hasta esta etapa, si el compilador encuentra algún error, los informa y le pide que los corrija.

Después de hacerlo, convierte su código en lo que se llama ‘código intermedio’.

Finalmente, el código intermedio se convierte en lenguaje ensamblador específico para su hardware.

Y el ensamblador convierte estas instrucciones en 0s y 1s que se llama ‘Código de máquina’ y su procesador ejecuta este código de máquina.

PD: esta es una descripción general de muy alto nivel de un compilador que hace muchas suposiciones, pero creo que es una buena introducción.