Números

Vamos a construir un pequeño compilador que puede entender enteros.

No es un lenguaje muy interesante, pero el compilador tiene todos los razgos principales de un compilador de verdad, y podemos ir construyendo sobre el.

Lo primero es construir un proyecto de rust para nuestro compilador. Yo lo voy a llamar just-numbers, para recordarme que solo puede compilar numeros.

Ve a un directorio donde quieras almacenar el proyecto y corre:

cargo new just-numbers

esto crea un nuevo directorio just-numbers con varios archivos, incluyendo Cargo.toml y src/main.rs. Cambiense a este nuevo directorio en la consola.

La funcion main solo tiene un hello world. Vamos a modificarlo para leer un numero de un archivo, e imprimir ensamblador de x86_64 para almacenar el numero en el registro rax.

use std::fs;
use std::env;
use std::str::FromStr;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let args: Vec<String> = env::args().collect();
    let filename = &args[1];

    let program_text = fs::read_to_string(filename)?;
    // println!("Input = {}", program_text);

    let program = i64::from_str(&program_text.trim())?;
    println!("section .text\n\
global our_code_starts_here\n\
our_code_starts_here:\n\
\tmov RAX, {}\n\
\tret\n", program);
    Ok(())
}

Si tuvieramos un archivo como 123.jn que tiene adentro solo el numero 123 podrimamos compilarlo con las siguientes instrucciones:

cargo build
cargo run 123.jn > 123.s

Y el archivo 123.s contendria las instrucciones en assembly para devolver 123.

section .text
global our_code_starts_here
our_code_starts_here:
	mov RAX, 123
	ret

La etiqueta our_code_starts_here dejaria 123 en el registro rax.

Ahora necesitamos un programa que reciba este resultado y haga algo util. En nuestro caso, vamos a hacer un programa en C que imprima el resultado.

#include <stdio.h>
#include <stdint.h>

extern int64_t our_code_starts_here() asm("our_code_starts_here");

int main(int argc, char** argv) {
  int64_t result = our_code_starts_here();
  printf("%ld\n", result);
  return 0;
}

Este programa declara our_code_starts_here como una funcion externa en assembly, y la llama, guardando el resultado en result. Luego imprime el resultado.

Ahora estamos listos para combinar el programa en C (nuestro "runtime") con el programa en assembly para hacer un programa completo.

nasm -f elf64 -o 123.o 123.s
gcc -g -o 123 main.c 123.o

*En la Mac, pueden ensamblar sustituyendo -f elf64 por -f macho64.

Si todo salio bien, si corren ./123 debe salir 123 en pantalla.

Conclusión

graph TB;
    jn[/123.jn/]-->rs[cargo run];
    rs-->s[/123.s/];
    s-->nasm[nasm];
    nasm-->obj[/123.o/];
    obj-->cc[gcc];
    main[/main.c/]-->cc;
    cc-->exe[/123/];

¡Hicimos un compilador! No parece mucho, y es algo frágil, pero tiene todos los componentes principales: lee un programa en ASCII, lo convierte a una representacion binaria, y lo traduce a lenguage de máquina. Hicimos varias trampas y usamos algunos trucos, pero funciona, y no es complicado (cabe en 19 lineas de rust).

En los siguientes capitulos haremos mas compiladores, y utilizaremos herramientas mas sofisticadas.