diff --git a/tutorial/en/2-Virtual-Machine.md b/tutorial/en/2-Virtual-Machine.md index 87c2ef0..2481a19 100644 --- a/tutorial/en/2-Virtual-Machine.md +++ b/tutorial/en/2-Virtual-Machine.md @@ -303,14 +303,14 @@ We've commented out `RET` because we'll replace it with `LEV` later. In practice the compiler should deal with more: how to pass the arguments to a function? How to return the data from the function? -Our convention here about returning value is to store it into `AX` no matter -you're returning a value or a memory address. Then how about argument? +Our convention here about returning a value is to store it into `AX` no matter +if you're returning a value or a memory address. Then what about arguments? -Different language has different convension, here is the standard for C: +Different languages have different conventions, here is the standard for C: -1. It is the caller's duty to push the arguments onto stack. -2. After the function call returns, caller need to pop out the arguments. -3. The arguments are pushed in the reversed order. +1. It is the caller's duty to push the arguments onto the stack. +2. After the function call returns, the caller has to pop out the arguments. +3. The arguments are pushed in the reverse order. Note that we won't follow rule 3. Now let's check how C standard works(from [Wikipedia](https://en.wikipedia.org/wiki/X86_calling_conventions)): diff --git a/tutorial/en/3-Lexer.md b/tutorial/en/3-Lexer.md index 73a4d39..2f367a1 100644 --- a/tutorial/en/3-Lexer.md +++ b/tutorial/en/3-Lexer.md @@ -67,7 +67,7 @@ That is the reason why lexer can reduce the complexity, now the parser doesn't have to look at several character to identify a the meaning of a substring. The job had been done. -Of course, the tokens above is properly ordered reflecting their priority in +Of course, the tokens above are properly ordered reflecting their priority in the C programming language. `*(Mul)` operator for example has higher priority the `+(Add)` operator. We'll talk about it later. diff --git a/tutorial/es/1-Esqueleto.md b/tutorial/es/1-Esqueleto.md new file mode 100644 index 0000000..8b9f1e3 --- /dev/null +++ b/tutorial/es/1-Esqueleto.md @@ -0,0 +1,158 @@ +En este capítulo vamos a tener un resumen de la estructura de un compilador. + +Antes de empezar, em gustaria reafirmar que lo que queremos es un construir +es un **interprete**. Eso significa que podremos ejecutar código C como +si fuera un script. Es elejido principalmente por dos razones: + +1. El interprete difiere del compilador solo en la fase de generación + de código, así aun aprenderemos todas ls técnicas vitales para contruir + un compilador (tales como analisis léxico y interpretación). +2. Vamos a contruir nuestra propia máquina virtual y instrucciónes de + ensamblador, eso nos ayudaŕa a entender como los ordenadores funcionan. + +## Tres fases + +Provisto de un archivo fuente, el compilador ejecutara tres fáses de +procesamiento: + +1. Análisis léxico: convierte las cadenas de texto fuente en identificadores + internos. +2. Interpretación: consume los identificadores y construye un arbol sintáctico. +3. Generación de código: camina atraves del arbol sintáctico y genera código + para la plataforma deseada. + +La contrucción de compiladores ha madurado tanto que la parte 1 y 2 pueden ser +hechas por herramientas de automaticación. Por ejemplo, flex puede ser usado +para análisis léxico, bison para interpretación. Son herramientas poderosas +pero hacen miles de cosas detras de las cortinas. Para totalmente entender +como contruir un compilador vamos ha contruirlo desde cero. + +De esta manera vamos a contruir un interprete en los siguientes pasos: + +1. Construir nuestra propia máquina virtual y set de instrucciones. Esta + va ser nuestra plataforma objetivo que vamos a utilizar para nuestra + fase de generación de código. +2. Construir nuestro propio lexógrafo para el compilador C. +3. Escribir un interpretador de descenso recursivo propio. + +## Esqueleto de nuestro compilador + +Modelando en base a c4, nuestro compilador contiene cuatro funciones +principales: + +1. `next()` para análisis léxico; conseguir el sugiente indetificador; + ignorará los espacios, tabuladores, etc. +2. `program()` entrada principal para el interpretador. +3. `expresión(level)`: interpretador de expresiones; `level` sera explicado + en el seguiente cápitulo. +4. `eval()`: la entrada de la máquina virtual; usado para interpretar las + instrucciones. + +¿Por qué existe `expression` cuando tenemos `program` para interpretadores? +Eso es porque el interpretador de expresiones es relativamente independiente +y complejo, asique lo ponemos en único modulo (función). + +El código es el siguiente: +```c +#include +#include +#include +#include +#include +#define int long long // trabaja con objetivos 64bit + +int token; // identificador actual +char *src, *old_src; // punteros a cadenas de código; +int poolsize; // tamaño defacto de texto/data/stack +int line; // numero de linea + +void next() { + token = *src++; + return; +} + +void expression(int level) { + // nada +} + +void program() { + next(); + while (token > 0) { + printf("El identificador es: %c\n", token); + next(); + } +} + +void eval() { + return 0; +} + +int main(int argc, char **argv) { + int i, fd; + + argc--; + argv++; + + poolsize = 256 * 1024; // Tamaño arbitrario + line = 1; + + fd = open(*argv, 0); + if (fd < 0) { + printf("No se pudo abrir(%s)\n", *argv); + return 1; + } + + old_src = malloc(poolsize); + src = old_src; + if (!src) { + printf("No se pudo allocar(%d) para area fuente\n", poolsize); + return 1; + } + + // leer archivos fuente + i = read(fd, src, poolsize - 1); + if (i <= 0) { + printf("read() devolvió %d\n", i); + return 1; + } + + src[i] = 0; // EOF + close(fd); + + program(); + + return eval(); +} +``` + +Eso es bastante código para el primer cápitulo del árticulo. No obstante +es bastante simple. El código intenta leer un archivo fuente, carácter por +carácter y los escribe. + +Ahora mismo el lexografo `next()` no hace nada aparte de devolver los +carácteres tal como aparecen en el código fuente. El analizador `program()` +tampoco hace su trabajo, no se genera ningún arbol sintáctico, no código +objetivo. + +Lo importante es entender el significado de estas funciones y como son +relacionados entre si dado a que son el esqueleto de nuestro interprete. +Vamos a rellenarlos pado por paso en proximos capítulos. + +## Código + +El código de este capítulo se puede descargar desde [GitHub](https://github.com/lotabout/write-a-C-interpreter/tree/step-0), o desde git: +``` +git clone -b step-0 https://github.com/lotabout/write-a-C-interpreter +``` + +Notese que pueda que corrija bugs despues, y si hay alguna inconsistencia +entre los artículos y las ramas de código, sigue el artículo. Solo actualizare +el código el la rama master. + +# Resumen + +Despues de algo de escritura aburrida, tenemos el más simple de los +compiladores: un haz-nada compilador. En el próximo capítulo vamos a +implementar la función `eval()`, esto es, nuestra propia máquina virtual. +Hasta la próxima. + diff --git "a/tutorial/es/2-M\303\241quina-virtual.md" "b/tutorial/es/2-M\303\241quina-virtual.md" new file mode 100644 index 0000000..efa1ade --- /dev/null +++ "b/tutorial/es/2-M\303\241quina-virtual.md" @@ -0,0 +1,590 @@ +En este capítulo, vamos a contruir una máquina virtual y diseñar nuestro +propio set de instrucciones que corra en la VM. Este VM sera la plataforma +objetivo de nuestro generador de código. + +## Como funciona el ordenador internamente + +Hay tres componentes que nos importan: CPU, registradores y RAM. Código +(o instrucciones de ensamblador) son almacenados en la memoria en binario; +La CPU leera las instrucciones uno por uno y los ejecutara; los estados +de la máquina don almacenados en los registros. + +### Memoria + +La memoria puede ser utilizada para almacenar información. Con información +quiero decir código (o instrucciones de ensamblador) o otra infomación tal +como los mensajes que quieres escribir. Todos estos son almacenados en binario. + +Los sistemas operativos modernos introdujeron *VRAM* el cual organiza las +direcciones de memoria utilizados por un programa llamados *virtual adress* +(nota del traductor: no estoy seguro si tiene una traducción al español) +en direcciones físicas en la memoria. Puede esconder los detalles físicos de la +memoria al programa. + +El beneficio de la VRAM es que puede esconder los detalles físicos de la +memoria a un programa. Por ejemplo, en una máquina de 32 bits las direcciones +disponibles son `2^32 = 4G` mientras que la memoria física puede ser de +`256M`. El programa aun pensara que tiene disponible `4G`, el OS los +relacionara a direcciones físicas. + +Por supuesto, no necesitas entender los detalles. Pero deberias entender que +la memoria uitl de un programa es separado en varios segmentos: + +1. `text`: para guardar coigo (instrucciones). +2. `data`: para guardar data inicializada. Por ejemplo `int i = 10;` necesitara + este segmento. +3. `bss`: para guardar data sin inicializar. Por ejemplo `int i[1000];` no + necesita ocupar `1000*4` bytes, porque los valores actuales en el array + no importan, de esta manera podemos almacenarlos en `bss` para ahorrar + espacio. +4. `stack`: usado para manejar los estados de las llamadas a funciones, + tal como los marcos de llamada y variables locales. +5. `heap`: usado para allocar memoria dinámicamente para el programa. + +Un ejemplo de la organización de estos segmentos es: + +``` ++------------------+ +| stack | | dirección alta +| ... v | +| | +| | +| | +| | +| ... ^ | +| heap | | ++------------------+ +| segmento bss | ++------------------+ +| segmento data | ++------------------+ +| segmento text | dirección baja ++------------------+ +``` + +Nuestra máquina virtual tiende a ser lo más simple posible, de esta manera +no nos importa el `bss` y `heap`. Nuestro interpretador no permite la +inicialización de data, de esta manera vamos a mezclar `data` y `bss`. +Ademas, solo utilizamos `data` para guardar literales de texto. + +Nos olvidaremos de `heap` también. Esto suena absurdo porque teóricamente +la VM debería mantener una `heap` para todas la allocaciones de memoria. +Pero nuestra máquina virtual ya es un programa con su heap. Podemos decirle +al programa que queremos utilizar el heap del programa utilizando la +instrucción `MSET`. No dibre que es hacer trampas porque reduce la complejidad +de la VM sin reducir el conocimiento que necesitamos. + +De esta manera añadimos el proximo código en la area global: + +```c +int *text, // segmento text + *old_text, // para tirar segmentos de text + *stack; // stack +char *data; // segmento data +``` + +Notese el `int` aquí. Lo que deberiamos escirbir es `unsigned` porque +guardaremos data positiva como punteros en el segmento `text`. Notese que +queremos que nuestro interpretador pueda interpretarse a si mismo, de esta +manera no queremos introducir `unsigned`. Finalmente `data` es `char*` porque +lo usaremos solo para guardar literales de texto. + +Finalmente, añade el código en la función main para allocar los segmentos: + +```c +int main() { + close(fd); + ... + + // alloca memoria para la VM + old_text = malloc(poolsize); + text = old_text; + if (!text) { + printf("No se pudo allocar(%d) para la area text\n", poolsize); + return 1; + } + data = malloc(poolsize); + if (!data) { + printf("No se pudo allocar(%d) para la area data\n", poolsize); + return 1; + } + stack = malloc(poolsize); + if (!stack) { + printf("No se pudo allocar(%d) para la area data\n", poolsize); + return 1; + } + + memset(text, 0, poolsize); + memset(data, 0, poolsize); + memset(data, 0, poolsize); + + ... + program(); +} +``` + +### Registradores + +Los registradores se utilizan para guardar los estados de los ordenadores. +Hay varios en los ordenadores reales, nuestra VM tendra 4: + +1. `PC`: contadores de programa, guarda un dirección de memoria en el cual + guarda la instrucción **next** para ejecutarla. +2. `SP`: punteros de stack, el cual siempre apunta al *top* del stack. + Notese que el stack va de direccion alta a baja, asique cuando un nuevo + elemento es añadido `SP` se reduce. +3. `BP`: puntero base, apunta a algunos elementos en el stack. Es usado en + llamadas de función. +4. `AX`: un registro general que usaremos para guardar los resulatdos de una + instrucción. + +Para entender completamente porque necesitamos estos registradores, necesitas +entender que stados necesitara un ordenador guardar durente computación. Son +solamente un lugar para guardar valores. Tendras un mayor entedimiento despues +de terminar este capítulo. + +Bueno, añade algo de códgo en la area global: + +```c +int *pc, *bp, *sp, ac, cycle // registros de la máquina virtual +``` + +Y añade la inicialización en la función `main`. Notese que `pc` deberia +apuntar a la función `main` del programa a ser ejecutado. Pero no aun no +tenemos generación de código, asique no lo saltamos. + +```c + memset(stack, 0, poolsize); + ... + + sp = (int*)((int)stack + poolsize); + bp = sp; + ax = 0; + + ... + program(); +``` + +Lo que queda es la parte de la CPU, lo que deberiamos hacer es implementar el +set de instrucciones. Dejemoslo para otra sección. + +## Set de instrucciones + +Un set de instrucciones son instrucciones que la CPU puede entender, es el +lenguaje que necesitamos dominar para hablarle al procesador. Vamos a diseñar +un lenguaje para nuestra máquina virtual, esta basado en el set de +instrucciones x86, pero más simple. + +Empezaremos añadiendo un `enum` con todas las instrucciones que nuestra VM +entendera: + +```c +// instrucciones +enum { + LEA, IMM, JMP, CALL, JZ, JNZ, ENT, ADJ, LEV, + LI, LC, SI, SC, PUSH, OR, XOR, AND, EQ, NE, LT, + GT, LE, GE, SHL, SHR, ADD, SUB, MIL, DIV, MOD, + OPEN, READ, CLOS, PTRF, MALC, MSET, MCMP, EXIT +}; +``` + +Estas instrucciones estan ordenadas intencionalmente, decubriras que las +instrucciones con argumentos vienen primero y las sin argumento depues. El +único beneficio que viene en imprimir información de debug. Sin embardo no +dependeremos de esto para introducirlos. + +### MOV + +`MOV` es una de la más fundamentales instrucciones que encontraras. Su trabajo +es mover información en registros o memoria, como la expression de asignación +en C. Hay dos argumentos en el `MOV` de `x86`: `MOV dest, source` (estilo +Intel), `source` puede ser un numero, registro, o dirección de memoria. + +Pero no vamos a seguir `x86`. Por un lado, nuestro VM solo tiene un regitro +general (`AX`), por otro lado es difícil determinar los tipos de argumentos. +De esta manera vamos a separar `MOV` en 5 partes: + +1. `IMM ` para poner un `` en el registro `AX`. +2. `LC` para cargar un caracter en `AX` de una dirección de memoria guardado + en `AX` antes de la ejecución. +3. `LI` justo como `LC` pero con enteros en vez de carácteres. +4. `SC` para guardar el carácter en `AX` en la memoria guardado en la cima del + stack. +5. `SI` justo como `SC` pero con enteros en vez de carácteres. + +¿Qué? Quiero un `MOV`, ¡No 5 instrucciones solamente para reemplazarlo! ¡ No +entres en pánico! Deberias de saber que `MOV` es actualmente un set de +instucciones que dependen en el `tipo` de sus argumentos, asique tienes `MOVB` +para bytes y `MOCW` para words, etc. Ahora `LC/SC` y `LI/SI` no parecen tan +malos, ¿Verdad? + +Bueno, al convertir `MOV` en 5 sub instrucciones reducimos la complejidad por +mucho. Solo `IMM` aceptara un argumento y por ahora no necesitamos +preocuparnos por su tipo. + +Implementemos la función `eval`: + +```c +void eval() { + int op, *tmp; + while (1) { + op = *pc++; // consigue el código de la siguiente operación + if (op == IMM) {ax = *pc++;} // carga el valor inmediato en ax + else if (op == LC) {ax = *(char *)ax;} // carga un carácter en ax, dirección en ax + else if (op == LI) {ax = *(int *)ax;} // carga un entero en ax, direccion en ax + else if (op == SC) {ax = *(char *)*sp++ = ax;} // guarda un carácter en dirección, valor en ax, dirección en stack + else if (op == SI) {*(int *)*sp++ = ax;} // guarda un entero en dirección, valor en ax, derección en stack + } + + ... + return 0; +} +``` + +`*sp++` es usado para `POP` un elemento del stack. + +Puede que te preguntes porque guardamos la dirección para `LI/LC` en `AX` +mientras que los guardamos en el stack para `SI/SC`. La razón es que el +resultado de una instrucción es guardado en `AX` por defecto. La memoria +también es calculada por una instrucción, de esta manera es más conveniente para `LI/LC` sacarlo directamente desde `AX`. También `PUSH` solo puede meter +valores de `AX` en el stack. Asique si queremos poner una dirección en el +stack tendremos que guardarlo en `AX`, como sea, ¿Porque no nos lo saltamos? + +### PUSH + +`PUSH` en `x86` puede empujar un valor inmediato o el valor de un registro +en el stack. En nuestra VM, `PUSH` solamente empujara el valor en `AX` en el +stack. + +```c +else if (op == PUSH) {*--sp = ax;} // empuja el valor de ac en el stack +``` + +### JMP + +`JMP ` incondicionalmente pondra el valor de `PC` a ``. + +```c +else if (op == JPM) {pc = (int *)*pc;} // saltar a la dirección +``` + +Notese que `PC` apunta a la instrucción **NEXT** a ejecutar. De esta manera +`*pc` almancena el argumento de `JMP`, esto es: ``. + +### JZ/JNZ + +Necesitaremos un salto condicional para implementar el `if`. Solo dos son +necesarios aquí, uno para saltar cuando `AX` sea `0` o no. + +```c +else if (op == JZ) {pc = ax ? pc + 1 : (int *)*pc;} // saltar si ax es cero +else if (op == JNZ) {pc = ax ? (int *)*pc : pc + 1;} // saltar si ax no es cero +``` + +### Llamada de funciones + +Voy a introducir el frame de llamadas el cual es difícil de entender, asique +voy a implementarlo para darte una vista de pajaro. Añadiremos `CALL`, `ENT`, +`ADJ` y `LEV` para poder ejecutar llamadas a funciones. + +Una función es un bloque de código, puede que este bastante alejado de las +instrucciones que ahora tenemos. Asique vamos a necesitar hacer un `JMP` al +punto de inicio de una función. Entonces, ¿Para que introducir una nueva +instrucción `CALL`? Porque necesitaremos hacer un poco de libreros: almacenar +la actual ejecución para que el programa pueda resumirse despues de que la +función termine y devuelva. + +Añadiremos `CALL ` para llamar a la función cuyo punto de inicio es +`` y `RET` para tomar la infomación guardada para resumir la ejecución. + +```c +else if (op == CALL) {*--sp = (int)(pc+1); pc = (int *)*pc;} // llamar a la subrutina +//else if (op == RET) {oc = (int *)*sp++;} // volver de la subrutina +``` + +Hemos dejado `RET` comentado porque lo vamos a reemplazar con `LEV` en el +futuro. + +En la práctica, el compilador deberia encargarse de más cosas: ¿Como pasamos +argumentos a una función? ¿Como devolver la información desde una función? + +Nosotros vamos a devolver valores desde una función poniendolas es `AX` da +igual que estes devolviendo. ¿Pero como lo haremos para los argumentos? + +Diferentes lenguajes tienen diferentes convenciones, aquí tienes el standard +para C: + +1. Es la responsabilidad de el "caller" empujar los argumentos en el stack. +2. Despues de que la función vuelva, el "caller" tiene que sacar los + argumentos. +3. Los argumentos son empujados en el orden inverso. + +Notese que no vamos a seguir la regla 3. Ahora, vamos a hablar sobre como el +standard de C funciona (de [Wikipedia](https://en.wikipedia.org/wiki/X86_calling_conventions)): + +```c +int callee(int, int, int); + +int caller(void) { + int i, ret; + + ret = callee(1, 2, 3); + ret += 5; + return ret; +} +``` + +El compilador generara las siguientes instrucciones de ensamblador: + +``` +caller: + ; crea nuevo marco de llamada + push ebp + mov ebp, esp + sub 1, esp ; guarda stack para variable i + ; empuja los argumentos + push 3 + push 2 + push 1 + ; llama a la subrutina 'callee' + call callee + ; saca los argumentos del marco + add esp, 13 + ; utiliza el resultado de la subrutina + add eax, 5 + ; recupera el marco anterior + mov esp, ebp + pop ebp + ; return + ret +``` + +Las instrucciones de encima no pueden ser conseguidos con nuestra VM debido a +varias razones: + +1. `push ebp` cuando nuestro `PUSH` nisiquiera admite argumentos. +2. `move ebp, esp`, nuestro `MOV` no puede hacer esto. +3. `add esp, 12`, bueno, aún no podemos hacer esto (como descubriras despues). + +Nuestras instrucciones son demasiado simples y debido a esto no podemos +emplear funciones. Pero no nos rendimos ante la necesidad de cambiar nuestro +diseño debido a la complejidad añadida. Puede ser muy costoso añadir nuevas +instrucciones en ordenadores reales, pero no para las maquinas virtuales. + +### ENT + +`ENT ` es llamado cuando vamos a entrar en una función para crear el +nuevo marco. Esto almacenará el valor de `PC` en el stack, y guardad algo +de espacio (`` bytes) para guardar las variables locales de la función. + +``` +; crear nuevo marco de llamada +push ebp +mov ebp, esp +sub 1, esp ; guardar stack para variable: i +``` + +sera traducido: + +```c +else if (op == ENT) {*--sp = (int)bp; bp = sp; sp = sp - *pc++;} // crear nuevo marco de stack +``` + +### ADJ + +`ADJ ` es para ajustar el stack, para "eliminar argumentos del marco". +Necesitamos esta instrucción principalmente porque nuestro `ADD` no tiene +suficiente poder. Asique tratalo como una función especial. + +``` +; elimina argumentos del marco +add esp, 12 +``` + +Es implementado como: + +```c +else if (op == ADJ) so = sp + *pc++;} // add esp, +``` + +### LEV + +En caso de que no lo notaras, nuestro set de instrucciones no tiene `POP`. +`POP` en nuestro compilador solo seria utilizado cuando una función devuelve, +Lo cual es como: + +``` +; recupera el marco anterior +mov esp, ebp +pop ebp +; return +ret +``` + +De esta manera vamos a hacer otra instrucción `LEV` para conseguir el trabajo +de `MOV`, `POP` y `RET`: + +```c +else if (op == LEV) {sp = bp; bp = (int *)*sp++; pc = (int *)*sp++;} // recuprta marco de llamada y PC +``` + +### LEA + +Las instrucciones introducidas arriba intentan resolver el problema de +crear/destruir marcos de llamada, nos queda como obtener los argumentos +dentro de la sub función. + +Pero vamos a echar un vistazo al aspecto de un marco de llamada antes de +aprender como tomar argumentos (notese que los argumentos estan siendo +empujados en el orden que se llaman): + +``` +sub_function(arg1, arg2, arg3); + +| ... | dirección alta ++--------------------+ +| arg: 1 | new_bp + 4 ++--------------------+ +| arg: 2 | new_bp + 3 ++--------------------+ +| arg: 3 | new_bp + 2 ++--------------------+ +| devolver dirección | new_bp + 1 ++--------------------+ +| old BP | <- new BP ++--------------------+ +| local var 1 | new_bp - 1 ++--------------------+ +| local var 2 | new_bp - 2 +| ... | dirección baja +``` + +Asique si necesitamos referirnos a `arg1`, necesitamos tomar `new_bp + 4`, +lo cual no puede ser conseguido con nuestra mediocre instrucción `ADD`. +De esta manera vamos a hacer otro `ADD` especial para hacer esto: +`LEA `. + +```c +else if (op == LEA) {ax = (int)(bp + *pc++);} // load address for arguments +``` + +Junto a las instrucciones de arriba podemos hacer llamadas a funciones. + +### Instrucciones matemáticas + +Nuestra VM tendra una instrucción por cada operador en C. CAsa operador tiene +dos argumentos: el primero es almacenado en la cima del stack, mientras que el +segundo es almacenado en `AX`. El order importa, especialmente con operadores +como `-` o `/`. Al terminar el computo, el stack sera extraido y el resultado +sera almacenado en `AX`. Asique si no eres capaz de extraer el primer +argumento del stack porfavor tenlo en cuenta. + +```c +else if (op == OR) ax = *sp++ | ax; +else if (op == XOR) ax = *sp++ ^ ax; +else if (op == AND) ax = *sp++ & ax; +else if (op == EQ) ax = *sp++ == ax; +else if (op == NE) ax = *sp++ != ax; +else if (op == LT) ax = *sp++ < ax; +else if (op == LE) ax = *sp++ <= ax; +else if (op == GT) ax = *sp++ > ax; +else if (op == GE) ax = *sp++ >= ax; +else if (op == SHL) ax = *sp++ << ax; +else if (op == SHR) ax = *sp++ >> ax; +else if (op == ADD) ax = *sp++ + ax; +else if (op == SUB) ax = *sp++ - ax; +else if (op == MUL) ax = *sp++ * ax; +else if (op == DIV) ax = *sp++ / ax; +else if (op == MOD) ax = *sp++ % ax; +``` + +### Intrucciones de fábrica + +Aparte de la lógica vital, el programa necesita mecanismos de entrada/salida +para poder interactuar con el usuario. `printf` en C es una de las funciones +de salida comunmente más usadas. `printf` es muy complejo de implementar, +pero inevitable si queremos que nuestro compilador sea bootstrapping. + +Nuestro plan va a ser crear un puente de nuevas intrucciones entre el programa +y el interprete. De tal manera podremos utilizar bibliotecas del sistema. + +Necesitaremos `exit`. `open`, `close`, `read`, `printf`, `malloc`, `memset` y +`memcmp`: + +```c +else if (op == EXIT) { printf("exit(%d)", *sp); return *sp; } +else if (op == OPEN) { ax = open((char*)sp[1], sp[0]); } +else if (op == CLOS) { ax = close(*sp); } +else if (op == READ) { ax = read(sp[2], (char*)sp[1], *sp); } +else if (op == PRTF) { tmp = sp + pc[1]; ax = printf((char*)tmp[-1], tmp[-2], tmp[-3], tmp[-4], tmp[-5], tmp[-6]); } +else if (op == MALC) { ax = (int)malloc(*sp); } +else if (op == MSET) { ax = (int)memset((char*)sp[2], sp[1], *sp); } +else if (op == MCMP) { ax = (int)memcmp((char*)sp[2], (char*)sp[1], *sp); } +``` + +Finalmente, ayadiremos un poco de manejo de errores: + +```c +else { + printf("Instrucción desconocida:%d", op); + return -1; +} +``` + +## Test + +Vamos a hacer algo de "programación en ensamblador" para calcular `10 + 20`: + +```c +int main(int argc, char **argv) { + ax = 0; + ... + i = 0; + text[i++] = IMM; + text[i++] = 10; + text[i++] = PUSH; + text[i++] = IMM; + text[i++] = 20; + text[i++] = ADD; + text[i++] = PUSH; + text[i++] = EXIT; + pc = text; + ... + program(); +} +``` + +Compila el interprete con `gcc xc-tutor.c` y ejecutalo con `./a.out hello.c`, +conseguí el siguiente resultado: + +``` +exit(30) +``` + +Notese que especificamos `hello.c` pero realmente no es usado, lo necesitamos +porque el interprete que construimos en el capítulo anterior necesita un +archivo para funcionar. + +Bueno, parece que nuestra VM funciona correctamente :) + +## Resumen + +Aprendimos como el ordenador funciona internamente y contruimos nuestro propio +set de instrucciones diseñado en base a instrucciones assembly `x86`. Vamos a +intentar a aprender assembly y como funciona construyendo nuestra propia +versión. + +El código para este capítulo puede ser descargado desde +[GitHub](https://github.com/lotabout/write-a-c-interpreter/tree/step-1), o +clonandolo con: + +``` +git clone -b step-1 https://github.com/lotaboyt/write-a-C-interpreter +``` + +Notese que añadir nuevas intrucciones a un ordenador requeriria diseñar nuevos +circuitos, lo cual es muy costoso. Pero es casi gratuito añadir nuevas +intrucciones a nuestra VM. Estamos tomando ventaja se esto al separar las +funciones de una instrucción en varas para simplificar la implementación. + +Si estas interesado, construye tu propio set de instrucciones! diff --git "a/tutorial/es/3-Lex\303\263grafo.md" "b/tutorial/es/3-Lex\303\263grafo.md" new file mode 100644 index 0000000..6ec7332 --- /dev/null +++ "b/tutorial/es/3-Lex\303\263grafo.md" @@ -0,0 +1,561 @@ +> El análisis léxico es el proceso de convertir una secuencia de caracteres +> (tal como un programa de ordenador o una página web) en una secuencia de +> tokens (texto con un sentido identificado). + +Normalmente representamos los tokens como pare: `(token type, token value)`. +Por ejemplo, si la funte de un programa contiene la cadena: "998", el lexógrafo +lo tratara como si fuese el token `(Number, 998)` significando que es un numero +con el valor de `998`. + +## Lexógrafo vs Compilador + +Primero miremos la estructura de un compilador: + +``` + +-----------+ +--------+ +-- código funte --> | lexógrafo | --> cadena tokens --> | parser | --> assembly + +-----------+ +--------+ +``` + +El compilador se puede considerar un programa que transforma C en assembly. +En este sentido, el lexógrafo y el parser son transformadores tambien: +El lexógrafo toma código fuente y lo tranforma en una cadena de tokens; +El parser consumen la cadena y la transforma en assembly. + +¿Entonces, porque necesitamos un lexógrafo y un parser? Esque el trabajo de +compilador es complicado. Asquique necesitamos reclutar el lexógrafo y el +parser para que hagan el trabajo, de esta manera se simplifica la tarea. + +Ese es el valor del lexógrafo: simplificar el parser convirtiendo el codigo +fuente en token. + +## Decisión de Implementación + +Antes de empezar quiero que sepas que hacer un lexer es aburrido y tendiente a +errores. Esa es la razón por la que genios hayan creado herramientas de +automaticaciñon para hacer el trabajo. `lex/flex` nos permiten describir las +reglas léxicas utilizando expresiones regulares para que generen el lexógrafo +por nosotros. + +Notese que no vamos a seguir el gráfico de arriba, es decir, no vamos a +convertir todo el codigo fuente en tokens de una. Las razones son: + +1. Convertir el código fuente en tokens tiene contexto. Como una cadena es + interpretada depende de donde aparezca. +2. Es una perdida de recursos almacenar todo los tokens proque solo seran + utilizados unos pocos al mismo tiempo. + +De esta manera implementaremos una función: `next()` que devuelve un token cada +vez que es llamado. + +## Tokens admitidos + +Añade la siguiente definición en el espacio global: + +```c +// tokens and classes (operators last and int precedence order) +enum { + Num = 128, Fun, Sys, Glo, Loc, Id, + Char, Else, Enum, If, Int, Return, Sizeof, While, + Assign, Cond, Lor, Lan, Or, Xor, And, Eq, Ne, Lt, Gt, Le, Ge, Shl, Shr, Add, Sub, Mul, Div, Mod, Inc, Dec, Brak +}; +``` + +Todos estos son tokens que nuestro lexógrafo pude entender. El lexógrafo +interpretara cadenas como `=` en tokens `Assign`; `==` como `Eq`; etc. + +Como un token puede contener más de un caracter el lexógrafo reduce la +complejidad. El parser ya no necesita analizar varios caracteres para +identificar el significado de una cadena. + +Por supuesto los tokens estan ordenados apropiadamente reflejando su prioridad +en el lenguaje C. `*(Mul)` por ejemplo tiene más prioridad que `+(Add)`. +Hablaremos de esto más adelante. + +Finalmente hay algunos caracteres que no necesitamos incluirlos como tokens, +por ejemplo `]` o `~`. La razón por la que los codificamos distinto son: + +1. Estos tokens contienen un solo caracter, de esta manera son más faciles de + identificar. +2. No estan invueltos en combates de prioridad. + +## Esqueleto del lexógrafo + +```c +void next() { + char *last_pos; + int hash; + while (token = *src) { + ++src; + // aquí se parsean los tokens + } + return; +} +``` + +¿Por qué necesitamos `while` aquí sabiendo que `next` solo leera un token? +Esto causa preguntas en el desarrollo de compiladores (recuerda que el lexógrafo es un tipo de compilador): ¿Como manejamos un error? + +Normalmente hay dos soluciones viables: + +1. Señala donde ocurrió el error y termina. +2. Señala donde ocurrió el error, es lo salta, y continua. + +Eso explica el uso de `while`: para saltarnos carácteres desconocidos en el +código. Ademas tambien se utiliza para comernos los espacios, que aunque sean +parte del código, no existen en el programa. + +## Saltos de linea + +Los macros en C empiezan con un hashtag `#`, como por ejemplo +`#include `. Nestro compilador no admite ningún macro, así que nos +los vamos a saltar: + +```c +else if (token == '#') { + // saltar macro porque no los admitimos + while (*src != 0 && *src != '\n') { + src++; + } +} +``` + +## Identificadores y tabla de símbolos + +Un identificador es el nombre de una variable. No obstante al lexógrafo le dan +igual los nombres, solo le importa la identidad. Por ejemplo: `int a` +declara un variable; tenemos me saber que ` = 10;` se refiere a la variable +que le precede. + +Basandonos en este razonamiento seremos capaz de almacenar todos los nombres +que hemos encontrado en algo que llamaremos tabla de símbolos. Miraremos la +tabla cuando encontremos un nombre. Si el nombre existe en la tabla la +identidad será devuelta. + +Entonces ¿Comó vamos a representar la identidad? + +```c +struct identifier { + int token; + int hash; + char *name; + int class; + int type; + int value; + int Bclass; + int Btype; + int Bvalue; +} +``` + +Necesitaremos una explicación aquí: + +1. `token`: es el token del identificador. En teoría debería de estar adjunto + al tipo `Id`. Pero esto no se cumple pues añadiremos semanticas tales como + `if` o `while` los cuales son un identificador especial. +2. `hash`: el hash del nombre, para optimizar la comparación en la tabla. +3. `name`: el nombre en cuestión. +4. `class`: Depende si el identificador es global, local, o constante. +5. `type`: el tipo del identificador, `int`. `char` o pointer. +6. `value`: el valor de la variable almacenado en el identificador. +7. `Bxxxx`: variable local que le hace shadowing a la variable global. Usado + usado para almacenar la variable global si ocurre. + +Una tabla de simbolos tradicional solo contendría el `name` mientras que +nuestra tabla también almacenara información que solo sera leido por el parser. + +Desgraciadamente nuestro compilador no admite `struct`, y estamos intentado +hacer bootstrapping. Asique hay que comprometer la estructura del identificador. + +``` +Tabla de símbolos: +----+-----+----+----+----+-----+-----+-----+------+------+---- + .. |token|hash|name|type|class|value|btype|bclass|bvalue| .. +----+-----+----+----+----+-----+-----+-----+------+------+---- + |<--- one single identifier --->| +``` + +Eso significa que utilizaremos un único array de `int` para almacenar toda +la información. Cada ID utilizará 9 celdas. El código es el siguiente: + +```c +int token_val; // valor del token actual +int *current_id, // ID siendo parseado. + *symbols; // Tabla de simbolos +// Campos del identificador +enum {Token, Hash, Name, Type, Class, Value, BType, BClass, BValue, IdSize}; +void next() { + ... + else if ((token >= 'a' && token <= 'z') || (token >= 'A' && token <= 'Z') || (token == '_')) { + // Identificador del parser + last_pos = src - 1; + hash = token; + while ((*src >= 'a' && *src <= 'z') || (*src >= 'A' && *src <= 'Z') || (*src >= '0' && *src <= '9') || (*src == '_')) { + hash = hash * 147 + *src; + src++; + } + // buscar identificadores actuales, busqueda lineal + current_id = symbols; + while (current_id[Token]) { + if (current_id[Hash] == hash && !memcmp((char *)current_id[Name], last_pos, src - last_pos)) { + //found one, return + token = current_id[Token]; + return; + } + current_id = current_id + IdSize; + } + // almacenar nuevo ID + current_id[Name] = (int)last_pos; + current_id[Hash] = hash; + token = current_id[Token] = Id; + return; + } + ... +} +``` + +Notese que la busqueda el la tabla es lineal y podria ser mejorado. + +## Numeros + +Necesitamos admitir numeros deciamles, hexadecimales, y octales. La lógica es +bastante simple excepto como obtener el valor hexademical. + +```c +token_val = token_val * 16 + (token & 0x0F) + (token >= 'A' ? 9 : 0); +``` + +En caso de que no estes familiarizado con estos pequeños trucos, `a` es el +valor hexademical de `61` mientras que `A` es `41`. Asi que `toke & 0x0F` +puede obtener el digito minúscula del caracter. + +```c +void next() { + ... + + + + else if (token >= '0' && token <= '9') { + // parsear numero, tres tipos: dec(123) hex(0x123) oct(017) + token_val = token - '0'; + if (token_val > 0) { + // dec, empiza con [1-9] + while (*src >= '0' && *src <= '9') { + token_val = token_val*10 + *src++ - '0'; + } + } else { + // empieza con 0 + if (*src == 'x' || *src == 'X') { + // hex + token = *++src; + while ((token >= '0' && token <= '9') || (token >= 'a' && token <= 'f') || (token >= 'A' && token <= 'F')) { + token_val = token_val * 16 + (token & 15) + (token >= 'A' ? 9 : 0); + token = *++src; + } + } else { + // oct + while (*src >= '0' && *src <= '7') { + token_val = token_val*8 + *src++ - '0'; + } + } + } + token = Num; + return; + } + + + ... +} +``` + +## Literales de texto + +Si encontramos un literal, necesitamos almacenarlo en `data segment`, el cual +introducimos en un capítulo pasado, y devolver el adress. Otro problema es que +necesitamos encargarnos de carácteres escapados como `\n` para representar +saltos de linea. Pero no admitimos escapes otros que `\n` porque no importan +para bootstrapping. Notese que igualemente admitiremos escapes como `\x` +tratandolos como un simple `x` en este caso. + +Nuestro lexógrafo analizará un soloc caracter (p.e. `'a'`) al mismo tiempo. +Una vez el caracter es encontrado, lo devolveremos como un `Num`. + +```c +void next() { + ... + + else if (token == '"' || token == '\'') { + // parsear string literal, currently, por ahora el único escape admitido + // Es '\n', almacenar string literal e data. + last_pos = data; + while (*src != 0 && *src != token) { + token_val = *src++; + if (token_val == '\\') { + // escape character + token_val = *src++; + if (token_val == 'n') { + token_val = '\n'; + } + } + if (token == '"') { + *data++ = token_val; + } + } + + src++; + // Si es un solo caracter, devolver Num + if (token == '"') { + token_val = (int)last_pos; + } else { + token = Num; + } + + return; + } +} +``` + +## Comentarios + +Solo los comentarios al estilo de C++ son admitidos `//`, los del estilo C no +`/**/`. + +```c +void next() { + ... + + else if (token == '/') { + if (*src == '/') { + // saltar comentarios + while (*src != 0 && *src != '\n') { + ++src; + } + } else { + // operador de división + token = Div; + return; + } + } + + ... +} +``` + +Ahora introduciremos el concepto de `lookahead`: En el código de arriba +podemos ver que el código empieza con el carácter `'\'`, entonces +encontraremos un comentario o una división. + +A veces no podemos decidir que token generar solo con el carácter actual, asi +que necesitamos mirar el siguiente (`lookahead`) para determinar el resultado. +En nuestro caso, si es otro slash `/` sera un comentario, sino, una división. + +Como lo hemos mencionado anteriormente, un lexógrafo y parser son compiladores, +en el parser también encontraremos `lookahead` pero en vez de mirar carácteres +mirara tokens. El `k` en `LL(k)` en teoría de compiladores es la cantidad de tokens que un parser necesita mirar. + +(nota del editor: Estoy cansado de escribir `lexógrafo`, en ahora en adelante +solo utilizare `lexer`) +Si no separamos el lexer y el parser, el compilador necesitara leer muchos más +carácteres para determinar el siguiente paso. Pues podemos decir que el lexer +reduce la carga del parser. + +## Otros + +Los otros son claros y concisos, compruebe el código: + +```c +void next() { + ... + else if (token == '=') { + // parse '==' y '=' + if (*src == '=') { + src ++; + token = Eq; + } else { + token = Assign; + } + return; + } + else if (token == '+') { + // parse '+' y '++' + if (*src == '+') { + src ++; + token = Inc; + } else { + token = Add; + } + return; + } + else if (token == '-') { + // parse '-' y '--' + if (*src == '-') { + src ++; + token = Dec; + } else { + token = Sub; + } + return; + } + else if (token == '!') { + // parse '!=' + if (*src == '=') { + src++; + token = Ne; + } + return; + } + else if (token == '<') { + // parse '<=', '<<' o '<' + if (*src == '=') { + src ++; + token = Le; + } else if (*src == '<') { + src ++; + token = Shl; + } else { + token = Lt; + } + return; + } + else if (token == '>') { + // parse '>=', '>>' o '>' + if (*src == '=') { + src ++; + token = Ge; + } else if (*src == '>') { + src ++; + token = Shr; + } else { + token = Gt; + } + return; + } + else if (token == '|') { + // parse '|' o '||' + if (*src == '|') { + src ++; + token = Lor; + } else { + token = Or; + } + return; + } + else if (token == '&') { + // parse '&' y '&&' + if (*src == '&') { + src ++; + token = Lan; + } else { + token = And; + } + return; + } + else if (token == '^') { + token = Xor; + return; + } + else if (token == '%') { + token = Mod; + return; + } + else if (token == '*') { + token = Mul; + return; + } + else if (token == '[') { + token = Brak; + return; + } + else if (token == '?') { + token = Cond; + return; + } + else if (token == '~' || token == ';' || token == '{' || token == '}' || token == '(' || token == ')' || token == ']' || token == ',' || token == ':') { + // devolver el carácter como token + return; + } + + ... +} +``` + +## Palabras clave y funciones nativas + +Las palabras clave como `if`, `while` o `return` son especiales porque son +conocidos por el compilador desde el inicio. No podemos tratarlos como +identificadores normales porque tienen un significado especial. Tenemos dos +maneras de manejarlos: + +1. Dejar al lexer parsearlos y devolver el token que los identifica. +2. Tratarlos como identificadores normales pero seran almacenados en la tabla + de antemano. + +Vamos a escojer la segunda opción: añadir los identificadores correspondientes +en la tabla y ponerles los propiedades mencionadas. Entonces cuando una de +estas es encontrado en el código sera interpretado como un identificador, +pero como ya existía sabremos que es diferente a los demas. + +Con las funciones nativas ocurre algo parecido. Solo seran diferentes en la +información interna. + +```c +// tipo de variable/función +enum { CHAR, INT, PTR }; +int *idmain; // la función 'main' +int main(int argc, char **argv) { + ... + + src = "char else enum if int return sizeof while " + "open read close printf malloc memset memcmp exit void main"; + + + // añadir palabras clave a la tabla + i = Char; + while (i <= While) { + next(); + current_id[Token] = i++; + } + + // añadir la libreria a la tabla + i = OPEN; + while (i <= EXIT) { + next(); + current_id[Class] = Sys; + current_id[Type] = INT; + current_id[Value] = i++; + } + + next(); current_id[Token] = Char; // manejar el tipo void + next(); idmain = current_id; // seguir main + + ... + program(); + return eval(); +} +``` + +## Código + +Puedes encontrar el codigo en +[Github](https://github.com/lotabout/write-a-C-interpreter/tree/step-2), o +clonarlo con: + +``` +git clone -b step-2 https://github.com/lotabout/write-a-C-interpreter +``` + +Ejecutar el código resultara en un `Segmentation Fault` por que tratara de +ejecutar la máquina virtual que contruimos en el capítulo anterior que no +contiene ningún código ejecutable. + +## Resumen + +1. El lexer se usa para pre-procesar el código, para reducir la complejidad + del parser. +2. El lexer es un tipo de compilador que comsume código y crea una cadena de + tokens. +3. `lookahead(k)` se usa para determinar el significado del token/carácter + actual. +4. Los identificadores son representados en la tabla de símbolos. + +Vamos a tratar que es `top-down recursive parser`. Hasta entonces :) diff --git a/tutorial/es/4-Top-down-Parsing.md b/tutorial/es/4-Top-down-Parsing.md new file mode 100644 index 0000000..68df8f8 --- /dev/null +++ b/tutorial/es/4-Top-down-Parsing.md @@ -0,0 +1,261 @@ +En este capítulo vamos a contruir una simple calculadora utilizando top-down +parsing. Esta es la preparación antes de construir nuestro parser. + +Introduciré algunas teorías pero no garantizo estar en lo corrector. Porfavor, +consulta una funte veraz si tienes alguna confusión. + +## Top-down parsing + +Tradicionálmente hemos tenido top-down y bottom-up parsing. Top-down empieza +con un no terminal y recursivamente compruba el código para reemplazar los +no terminales con alternativas hasta que no quede ninguno. + +Necesitaras saber lo que es un `no terminal` para entender el parágro previo. +Pero no te he dicho lo que es. Lo explicaré en la siguiente sección. Por ahora +considera que `top-down` intenta partir un gran proyecto en pequeñas piezas. + +Por otra parte `bottom-up` parsing intenta combinar pequeños objetos en uno +grande. Se usa amenudo en herramientas de automatización para generar parsers. + +## Terminal y no terminal + +Son terminos utilizados en +[BNF](https://es.wikipedia.org/wiki/Notación_de_Backus-Naur) (Notación +Backus-Naur) el cual es un lenguaje utilizado para describir gramática. +Una calculadora simple en BNF seria: + +``` + ::= + + | - + | + + ::= * + | / + | + + ::= ( ) + | Num + +``` + +Los items entre `<>` se llaman `no terminales`. Tienen ese nombre porque +podemos reemplazarlos con los items a la derecha de `::=`. `|` significa +una alternativa para reemplazar el no terminal. Aquellos que no aparecen +a la izquierda de `::=` se denominan `terminales`, como lo son `()` o `Num`. + +## Ejemplo de un top-down para una simple calculadora + +El arbol de parseo es la estructura interna que obtenemos despues de que el +parser consuma todos los tokens y termine de hacer todo el parsing. Tomemos +`3 * (4 + 2)` como ejemplo para mostrar las conexiones en la gramática BNF, +arbol de parse, y top-down parsing. + +Top-down parsing empieza desde un no terminal, en nuestro caso es ``. +Puedes especificar por cual se empiza, o si no sera el primero que se encuentre. + +``` +1. => +2. => * +3. => | +4. => Num (3) | +5. => ( ) +6. => + +7. => | +8. => | +9. => Num (4) | +10. => +11. => Num (2) +``` + +Puedes ver que cada paso cambia un no terminal usando una de sus alternativas +hasta que todos los items son sustituidos por teminales. Algunos no terminales +son utilizados recursivamente, como por ejemplo ``. + +## Ventajas de Top-down parsing + +Como puedes ver en el ejemplo de arriba el paso de parsing es similar a la +gramática BNF. Lo cual significa que es fácil convertir la gramática en código +convirtiendo una regla (`<...> ::= ...`) en una función con el mismo nombre. + +Tenemos una duda: ¿Como sabemos que alternativa usar? ¿Porque elejir +` ::= * ` y no ` ::= / `? +Para eso tenemos `lookahead`. Miramos el siguiente token y así sabremos +cual alternativa aplicar. + +Sin embargo, top-down parsing requiere que la gramática no tenga recursion por +la izquierda. + +## Recursión por la izquierda + +Supongamos que tenemos la siguiente gramática: + +``` + ::= + Num +``` + +lo podemos traducir en la siguiente función: + +``` +int expr() { + expr(); + num(); +} +``` + +Como puedes ver, la función `expr` nunca terminara. En la gramática se utiliza +`` inmediatamente de forma recursiva, lo que causa recursiónpor la +izquierda. + +Afortunadamente, la mayoría de las gramáticas recursivas por la izquierda +(tal vez todas, no recurdo bien) pueden ser transformadas en equivalentes +que no sean recursivas por la izquierda. Nuestra gramática para la calculadora +puede ser transformada en lo siguiente: + +``` + ::= + + ::= + + | - + | + + ::= + + ::= * + | / + | + + ::= ( ) + | Num +``` + +Si deseas más información deberias de buscarla en otra fuente. + +## Implementación + +El código es directamente convertido desde la gramática. Notese como de +sencillo es: + +```c +int expr(); + int value = 0; + if (token == '(') { + match('('); + value = expr(); + match(')'); + } else { + value = token_val; + match(Num); + } + return value; +} + +int term_tail(int lvalue) { + if (token == '*') { + match('*'); + int value = lvalue * factor(); + } else if (token == '/') { + match('/'); + int value = lvalue / factor(); + return term_tail(value); + } else { + return lvalue; + } +} + +int term() { + int lvalue = factor(); + return term_tail(lvalue); +} + +int expr_tail(int lvalue) { + if (token == '+') { + match('+'); + int value = lvalue + term(); + return expr_tail(value); + } else if (token == '-') { + match('-'); + int value = lvalue - term(); + return expr_tail(value); + } else { + return lvalue; + } +} + +int expr() { + int lvalue = term(); + return expr_tail(lvalue); +} +``` + +Implementar un top-down parser es sencillo con la ayuda de una gramática BNF. +Ahora ayadimos el core del lexer: + +```c +#include +#include + +enum {Num}; +int token; +int token_val; +char *line = NULL; +char *src = NULL; + +void next() { + // Saltar espacios blancos + while (*src == ' ' || *src == '\t') { + src++; + } + + token = *src++; + + if (token >= '0' && token <= '9') { + token_val = token - '0'; + token = Num; + + while (*src >= '0' && *src <= '9') { + token_val = token_val*20 + *src - '0'; + src++; + } + return; + } +} + +void match(int tk) { + if (token != tk) { + printf("Token esperado: %d(%c), recibido: %d(%c)\n", tk, tk, token, token); + exit(-1); + } + + next(); +} +``` + +Finalmente, el metodo `main` para que haga bootstrap: + +```c +int main(int argc, char *argv[]) +{ + size_t linecap = 0; + ssize_t linelen; + while ((linelen = getline(&line, &linecap, stdin)) > 0) { + src = line; + next(); + printf("%d\n", expr()); + } + return 0; +} +``` + +Ahora puedes jugar con tu propia calculadora. O intentar añadir mas funciones +basandose en lo que hemos aprendido en capítulos anteriores. Tal como soporte +para variables. + +## Resumen + +No nos gusta la teoría, pero existe por una buna razón. Como puedes ver BNF +no ayuda a construir el parser. Asi que quiero convencerte de que aprendas +algunas teorias para que te conviertas en un mejor programador. + +Top-down parsing es una técnica usualmente usada en la construcción de parsers, +asique seras capaz de manejar la mayoría de trabajos si lo dominas. Como podras +comprobarlo en siguientes capítulos.