Traductores y Compiladores
Un traductor convierte código fuente en código objeto. Facilita detección de errores, aumenta productividad y brinda portabilidad.
Traduce y ejecuta instrucción por instrucción. No genera archivo objeto.
Traduce todo el programa antes de ejecutar. Genera ejecutable.
Une módulos objeto. Estático o dinámico (DLL).
Precompiladores, Cargadores, Ensambladores (1:1 con máquina).
Fases del Compilador
Análisis Léxico (Scanner)
Lee el código fuente carácter a carácter y produce tokens. Cada token es un nodo de lista enlazada:
struct t_token {
int intTokenCode; // código numérico (T_TRIGHT=1, T_ADVANCE=3, etc.)
char *strTokenSourceCodeText; // texto original del fuente
int intRow; // fila en el fuente
int intColumn; // columna en el fuente
struct t_token *ptrNext; // siguiente token → lista enlazada
};
ptrTokenList. El parser la recorre con ptrCurrentToken, avanzando con ptrCurrentToken = ptrCurrentToken->ptrNext dentro de Expect().
Tokens definidos en Eiichi
#define T_TRIGHT 1 · T_TLEFT 2 · T_ADVANCE 3 · T_GOBACK 4 #define T_IF_CRASH 5 · T_PICKUP 6 · T_PUTDOWN 7 · T_Num 8 #define T_SemiColon 9 · T_EOF 13
Análisis Sintáctico (Parser)
El proyecto implementa ambos tipos Top-Down: RDCP y LL(1), sobre la misma gramática.
RDCP — funciones auxiliares clave
// Implementa FIRST: ¿puede el token actual iniciar el no terminal?
int CurrentTokenInFirst(int intNTSymbol) { ... }
// Consume el token si coincide; si no, activa boolParserError
void Expect(int intTSymbol) { ... }
// Program ::= Instruction; {Instruction;}
void Program() {
Instruction();
Expect(T_SemiColon);
while(CurrentTokenInFirst(NT_Instruction)) { // ← usa FIRST
Instruction();
Expect(T_SemiColon);
}
}
LL(1) — Tabla de Reconocimiento RM[7][13]
// RM[No Terminal - 1][Terminal - 1] = número de regla (-1 = error)
// Columnas: TR TL ADV GOB IF PU PD Nm ; Sp Tb EOL EOF
{ 1, 1, 1, 1, 1, 1, 1, -1, -1, -1, -1, -1, -1}, // NT_Program
{ 2, 2, 2, 2, 2, 2, 2, -1, -1, -1, -1, -1, 3}, // NT_Program2 (3=eps con EOF)
{ 4, 4, 5, 5, 6, 7, 7, -1, -1, -1, -1, -1, -1}, // NT_Instruction
{ 8, 9, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, // NT_InstrTurn
{-1, -1, 10, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1}, // NT_InstrAdvance
{-1, -1, -1, -1, 12, -1, -1, -1, -1, -1, -1, -1, -1}, // NT_InstrIfCrash
{-1, -1, -1, -1, -1, 13, 14, -1, -1, -1, -1, -1, -1} // NT_InstrObject
4-7) Instruction→InstrTurn/Advance/IfCrash/Object
8) →TRIGHT | 9) →TLEFT | 10) →ADVANCE Num | 11) →GOBACK
12) →IF_CRASH Instruction | 13) →PICKUP | 14) →PUTDOWN
FIRST y FOLLOW
| No Terminal | FIRST | FOLLOW |
|---|---|---|
| Program / Instruction | {TRIGHT, TLEFT, ADVANCE, GOBACK, IF_CRASH, PICKUP, PUTDOWN} | {$} / {;} |
| Program2 | {mismos + ε} | {$} → regla 3 col. EOF |
| InstrTurn | {TRIGHT, TLEFT} | {;} |
| InstrAdvance | {ADVANCE, GOBACK} | {;} |
| InstrIfCrash | {IF_CRASH} | {;} |
| InstrObject | {PICKUP, PUTDOWN} | {;} |
El Lenguaje Eiichi
Robot en tablero 8×8. Debe recoger un objeto y salir evitando chocar.
Instrucciones → Código generado
| Instrucción Eiichi | Código ensamblador generado | Función VM |
|---|---|---|
TRIGHT; | TRIGHT | i_tright() — gira E→S→W→N→E |
TLEFT; | TLEFT | i_tleft() — gira en sentido contrario |
ADVANCE N; | MOV #N,AC → label: MOV AC,ACC → ADV → DEC → MOV ACC,AC → JNZ label | Bucle: avanza N veces decrementando AC |
GOBACK; | Solo Expect(T_GOBACK) — sin GC() | ⚠️ Sin implementación |
PICKUP; | PICKUP | i_pickup() — recoge objeto si está en misma celda |
PUTDOWN; | PUTDOWN | i_putdown() — deposita objeto |
IF_CRASH; | PUSH CRC → PUSH #0 → CMP → JEZ [label] → TLEFT | Si CRC=0 salta; si CRC>0 gira izquierda |
TLEFT al final de IF_CRASH es la instrucción de recuperación fija generada siempre por InstrIfCrash(). Cualquier instrucción adicional que le siga en Eiichi se genera después, como instrucción separada.
Arquitectura y Entornos de Ejecución
Máquina Multinivel
Modos de Direccionamiento en Eiichi
| Modo | Define | Ejemplo en código | Significado |
|---|---|---|---|
| Inmediato | AM_IMMEDT = 1 | GC(AM_IMMEDT); GC(3); | El valor 3 viene literal en la instrucción |
| Directo a Registro | AM_DIRREG = 2 | GC(AM_DIRREG); GC(R_AC); | El dato está en el registro AC |
| Directo a Memoria | AM_DIRMEM = 3 | (definido, no usado en ejemplos) | El dato está en una dirección de memoria |
VM & Lenguaje Ensamblador
La VM de Eiichi es una máquina virtual de propósito específico. El parser RDCP genera código binario (a través de GC()) y la VM lo ejecuta. La relación entre ambos es directa: cada instrucción Eiichi produce una secuencia de bytes exacta.
Set de instrucciones de la VM
| Código | Mnemónico | Parámetros | Usa modo dir. | Descripción |
|---|---|---|---|---|
1 | MOV | 2 | ✅ Sí | Mueve un valor de origen a destino |
2 | ADV | 0 | ❌ No | Avanza el robot 1 posición |
3 | DEC | 0 | ❌ No | Decrementa ACC en 1 |
4 | JNZ | 1 | ❌ No | Salta si ACC ≠ 0 |
5 | JEZ | 1 | ❌ No | Salta si ACC = 0 |
6 | PUSH | 1 | ✅ Sí | Empuja valor al stack |
7 | CMP | 0 | ❌ No | Compara 2 valores del stack (resta, actualiza flags) |
8 | END | 0 | ❌ No | Termina el programa |
9 | TRIGHT | 0 | ❌ No | Gira el robot a la derecha |
10 | TLEFT | 0 | ❌ No | Gira el robot a la izquierda |
11 | PICKUP | 0 | ❌ No | Recoge el objeto |
12 | PUTDOWN | 0 | ❌ No | Deposita el objeto |
Registros de la VM
Apunta a la siguiente instrucción en mem[]. Inicia en 0.
Contador del bucle ADVANCE N. Se carga con N y decrementa hasta 0.
Registro general. DEC lo decrementa; CMP deposita ahí el resultado de la resta.
Se incrementa cuando Eiichi va a chocar. Si choca dos veces con el mismo objeto → destruido.
Índice en stack[]. PUSH escribe en stack[SP++]. CMP lee con stack[--SP].
Instruction Registers. IR1 guarda el opcode leído en el FETCH del ciclo principal.
Flags de la ALU
// Se actualizan después de DEC y CMP:
void ReviewACCFlags() {
FLAG_EQUAL_TO_ZERO = (ACC == 0) ? TRUE : FALSE;
FLAG_GREATER_THAN_ZERO = (ACC > 0) ? TRUE : FALSE;
FLAG_LESS_THAN_ZERO = (ACC < 0) ? TRUE : FALSE;
}
// JNZ usa: FLAG_GREATER_THAN_ZERO || FLAG_LESS_THAN_ZERO
// JEZ usa: FLAG_EQUAL_TO_ZERO
Ciclo de Ejecución — Fetch → Decode → Execute
PC = 0;
do {
IR1 = mem[PC++]; // FETCH: lee opcode, avanza PC
switch (IR1) { // DECODE + EXECUTE
case A_MOV: i_mov(); break; // lee 4 bytes más (AM+val AM+reg)
case A_ADV: i_adv(); break; // sin parámetros
case A_DEC: i_dec(); break; // ACC--
case A_JNZ: i_jnz(); break; // lee 1 byte: dirección de salto
case A_JEZ: i_jez(); break; // lee 1 byte: dirección de salto
case A_PUSH: i_push(); break; // lee 2 bytes (AM+val)
case A_CMP: i_cmp(); break; // sin parámetros
case A_TRIGHT: i_tright(); break; // sin parámetros
case A_TLEFT: i_tleft(); break; // sin parámetros
case A_PICKUP: i_pickup(); break; // sin parámetros
case A_PUTDOWN:i_putdown();break; // sin parámetros
case A_END: i_end(); break; // sin parámetros
}
graphBoard_DrawExecutionContext(); // dibuja tablero después de cada instrucción
} while ((mem[PC] != A_END) && (!boolSystemError) && (!boolSystemSuccess) && (keypressed != 'q'));
Cómo funciona ADVANCE N en detalle
Es el ejemplo más completo de cómo el parser RDCP genera un bucle completo en ensamblador:
// InstrAdvance() en parser_RDCP.c genera: GC(A_MOV); GC(AM_IMMEDT); GC(Num); GC(AM_DIRREG); GC(R_AC); // MOV #N, AC → carga el contador signed short label = (signed short)PC; // ← guarda posición del inicio del bucle GC(A_MOV); GC(AM_DIRREG); GC(R_AC); GC(AM_DIRREG); GC(R_ACC); // MOV AC, ACC → copia contador al acumulador (para los flags) GC(A_ADV); // ADV → avanza el robot 1 posición GC(A_DEC); // DEC → ACC-- (y actualiza flags) GC(A_MOV); GC(AM_DIRREG); GC(R_ACC); GC(AM_DIRREG); GC(R_AC); // MOV ACC, AC → actualiza el contador con el nuevo valor GC(A_JNZ); GC(label); // JNZ label → si ACC ≠ 0, vuelve al inicio del bucle
GC() escribe un signed short (2 bytes) en object.bin. La VM lee ese mismo archivo en mem[] y lo ejecuta byte a byte.
Cómo funciona IF_CRASH en detalle
// InstrIfCrash() genera: GC(A_PUSH); GC(AM_DIRREG); GC(R_CRC); // PUSH CRC → empuja crash counter GC(A_PUSH); GC(AM_IMMEDT); GC(0); // PUSH #0 → empuja cero GC(A_CMP); // CMP → ACC = 0 - CRC (si CRC=0: ACC=0) GC(A_JEZ); signed short LabelToBeSolved = (signed short)PC; // guarda posición del operando GC(0); // placeholder temporal — se resolverá con backpatch GC(A_TLEFT); // si CRC > 0: gira izquierda (instrucción de recuperación fija) // ... instrucción adicional si existe ... signed short Label = (signed short)PC; GC_AddToVectorAddressesToBeUpdate(LabelToBeSolved, Label); // backpatch
JEZ aún no se sabe la dirección de destino. Se escribe un 0 temporal y se guarda la posición en GCPtr[]. Al finalizar la instrucción de recuperación, se conoce la dirección real y fseek() regresa al archivo para sobreescribirla. Sin esta llamada al final, todos los JEZ saltan a addr 0.
El archivo object.bin
Es el programa compilado en formato binario. Cada valor es un signed short (2 bytes). Para leerlo desde Python:
import struct
with open("object.bin", "rb") as f:
data = f.read()
values = struct.unpack(f"{len(data)//2}h", data)
for i, v in enumerate(values):
print(f"{i}\t{v}")
La VM también lo desensambla automáticamente con GC_VerifyGeneratedBinaryFile(), que imprime el ensamblador legible al ejecutar.
Cómo leer el binario manualmente
Se aplica la misma tabla de #define del proyecto. El primer valor es siempre un opcode. Según el opcode, los siguientes valores son sus parámetros:
| Opcode (valor) | Tipo siguiente byte | Ejemplo |
|---|---|---|
| 1 = MOV | AM (1=inmediato, 2=registro) + valor, luego AM + destino | 1 1 3 2 6 → MOV #3, AC |
| 4 = JNZ / 5 = JEZ | Dirección de salto (1 valor) | 4 5 → JNZ 5 |
| 6 = PUSH | AM + valor | 6 2 5 → PUSH CRC |
| 2,3,7,8,9,10,11,12 | Sin parámetros — el siguiente es ya un opcode | 2 → ADV |
Desensamblado real del object.bin del proyecto
Este es exactamente el output que produce GC_VerifyGeneratedBinaryFile() al ejecutar la VM:
| Addr | Instrucción | Bytes | Origen Eiichi |
|---|---|---|---|
| 0 | MOV #3, AC | 01 01 03 02 06 | ADVANCE 3; |
| 5 | MOV AC, ACC | 01 02 06 02 07 | |
| 10 | ADV | 02 | |
| 11 | DEC | 03 | |
| 12 | MOV ACC, AC | 01 02 07 02 06 | |
| 17 | JNZ 5 ↑ | 04 05 | |
| 19 | PICKUP | 11 | PICKUP; |
| 20 | TRIGHT | 09 | TRIGHT; |
| 21 | MOV #3, AC | 01 01 03 02 06 | ADVANCE 3; |
| 26 | MOV AC, ACC | 01 02 06 02 07 | |
| 31 | ADV | 02 | |
| 32 | DEC | 03 | |
| 33 | MOV ACC, AC | 01 02 07 02 06 | |
| 38 | JNZ 26 ↑ | 04 26 | |
| 40 | PUSH CRC | 06 02 05 | IF_CRASH; |
| 43 | PUSH #0 | 06 01 00 | |
| 46 | CMP | 07 | |
| 47 | JEZ 50 ↓ (backpatch) | 05 50 | |
| 49 | TLEFT (hardcoded) | 0A | |
| 50 | MOV #5, AC ← JEZ salta aquí | 01 01 05 02 06 | ADVANCE 5; |
| 55 | MOV AC, ACC | 01 02 06 02 07 | |
| 60 | ADV | 02 | |
| 61 | DEC | 03 | |
| 62 | MOV ACC, AC | 01 02 07 02 06 | |
| 67 | JNZ 55 ↑ | 04 37 | |
| 69 | END | 08 | fin del programa |
ADVANCE 3; → avanza 3 casillasPICKUP; → recoge el objetoTRIGHT; → gira a la derechaADVANCE 3; → avanza 3 másIF_CRASH; → si va a chocar, gira izquierdaADVANCE 5; → avanza 5 hacia la salida
Cómo la VM carga y ejecuta el binario
// 1. LoadProgramInMemory() lee el .bin y lo pone en mem[]
void LoadProgramInMemory(char *strBinaryFile) {
FILE *f = fopen(strBinaryFile, "rb");
signed short sshValue;
int PCLocal = 0;
while(fread(&sshValue, sizeof(signed short), 1, f) != 0)
mem[PCLocal++] = (int)sshValue; // cada signed short → una celda de mem[]
fclose(f);
}
// 2. main() ejecuta desde mem[0]
PC = 0;
do {
IR1 = mem[PC++]; // FETCH
switch(IR1) { ... } // DECODE + EXECUTE
} while (mem[PC] != A_END && ...);
Bugs en el Código Original
Estos bugs fueron encontrados en la versión original entregada. La VM corregida (v2) ya los resuelve — pero quedan 4 pendientes.
Bugs ya corregidos en la VM v2
Bugs pendientes en la VM v2 (corregida)
#define en lugar de 0. PC=1, SP=4, CRC=5, etc. hacen que la VM empiece en estado incorrecto.boolVerifyOperand se usa en un if antes de ser inicializado — comportamiento indefinido.break, después de dibujar la 'e' cae al default — siempre se ejecuta el caso por defecto también.ptrBinaryFile es NULL pero fclose(NULL) se llama de todas formas — comportamiento indefinido.Bugs en parser_RDCP.c
JEZ generados por IF_CRASH quedan con dirección 0 y nunca saltan al lugar correcto. El backpatch no se aplica.Expect(T_GOBACK) pero no genera ningún GC(). La instrucción GOBACK se acepta sintácticamente pero no produce código ejecutable.Expect() valide que efectivamente es un T_Num. Si hay error, el GC(Num) ya escribió basura al binario.Arquitectura General del Compilador Eiichi
Pipeline completo
scanner.c
parser_RDCP.c
parser_LL.c
vm_eiichi.c
GC() — Generate Code
void GC(signed short sshByte) {
if (!boolParserError) {
fwrite(&sshByte, sizeof(signed short), 1, ptrGCBinaryFile);
PC++; // PC del compilador, no de la VM
}
}
// Cada instrucción Eiichi = múltiples llamadas a GC():
// ADVANCE 3; → GC(1) GC(1) GC(3) GC(2) GC(6) ← MOV #3,AC
// GC(1) GC(2) GC(6) GC(2) GC(7) ← MOV AC,ACC
// GC(2) GC(3) ← ADV DEC
// GC(1) GC(2) GC(7) GC(2) GC(6) ← MOV ACC,AC
// GC(4) GC(label) ← JNZ label
Diferencias clave: RDCP vs LL
| Aspecto | RDCP | LL(1) |
|---|---|---|
| Mecanismo | Funciones recursivas, pila del sistema | Pila explícita t_stack + tabla RM[][] |
| FIRST implementado como | CurrentTokenInFirst() — switch manual | Columnas de la tabla RM (posición = terminal) |
| Genera código | ✅ Sí — llama a GC() directamente | ❌ No — solo reconoce y muestra reglas |
| Gramática usada | Con while() para la repetición de Program | Con Program2 para eliminar la recursión |
| Flag de error | boolParserError detiene todo | intFlagSyntaxError en loop do-while |
| Condición de aceptación | Termina sin error + llega al END | intFlagAcceptedInput cuando stack top = EOF = input |
CurrentTokenInFirst() en RDCP / columnas de RM en LLFOLLOW → regla 3 (Program2→ε con EOF en columna 13 de RM)
Backpatching → IF_CRASH usa
GCPtr[] + fseek() para resolver JEZobject.bin → secuencia de
signed short, cada uno es un opcode, modo de dir. o valorVM → carga el .bin en
mem[] y lo ejecuta con el ciclo Fetch-Decode-ExecuteRegistros → todos deben iniciar en 0 (bug crítico en la versión original)