📚 Guía de Examen

Compiladores &
Traductores

Teoría · Código Real · VM · object.bin · Bugs · Examen hoy

01

Traductores y Compiladores

Un traductor convierte código fuente en código objeto. Facilita detección de errores, aumenta productividad y brinda portabilidad.

Intérprete

Traduce y ejecuta instrucción por instrucción. No genera archivo objeto.

🏗️
Compilador

Traduce todo el programa antes de ejecutar. Genera ejecutable.

🔗
Enlazador (Linker)

Une módulos objeto. Estático o dinámico (DLL).

📦
Otros

Precompiladores, Cargadores, Ensambladores (1:1 con máquina).

Fases del Compilador

FASE 1Análisis LéxicoScanner · Tokens
FASE 2Análisis SintácticoParser · Gramática
FASE 3Análisis SemánticoTipos · Significado
FASE 4Cód. IntermedioRepr. temporal
FASE 5OptimizaciónEficiencia
FASE 6Generación Cód.Lenguaje objetivo
📌 Clave
Fases 1-3 = ANÁLISIS. Fases 4-6 = SÍNTESIS. En el proyecto: Scanner + Parser RDCP/LL + GC() implementan todo esto directamente en C.
02

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
};
💡 Lista enlazada de tokens
El scanner produce 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
03

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
📌 Las 14 reglas
1) Program→Instr;Program2 | 2) Program2→Instr;Program2 | 3) Program2→ε
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 TerminalFIRSTFOLLOW
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}{;}
04

El Lenguaje Eiichi

Robot en tablero 8×8. Debe recoger un objeto y salir evitando chocar.

Instrucciones → Código generado

Instrucción EiichiCódigo ensamblador generadoFunción VM
TRIGHT;TRIGHTi_tright() — gira E→S→W→N→E
TLEFT;TLEFTi_tleft() — gira en sentido contrario
ADVANCE N;MOV #N,AC → label: MOV AC,ACC → ADV → DEC → MOV ACC,AC → JNZ labelBucle: avanza N veces decrementando AC
GOBACK;Solo Expect(T_GOBACK) — sin GC()⚠️ Sin implementación
PICKUP;PICKUPi_pickup() — recoge objeto si está en misma celda
PUTDOWN;PUTDOWNi_putdown() — deposita objeto
IF_CRASH;PUSH CRC → PUSH #0 → CMP → JEZ [label] → TLEFTSi CRC=0 salta; si CRC>0 gira izquierda
💡 IF_CRASH — TLEFT hardcodeado
El 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.
05

Arquitectura y Entornos de Ejecución

Máquina Multinivel

L0Lógica DigitalCircuitos físicos — único nivel 100% real
L1MicroprogramaciónMicrocódigo que controla la ALU
L2Lenguaje de MáquinaInstrucciones binarias (ISA)
L3Sistema OperativoLlamadas al sistema
L4EnsambladorMnemónicos → instrucciones de máquina
L5Lenguaje de Alto NivelC — donde está escrito el compilador Eiichi
L6Máquina Virtual / BytecodeLa VM de Eiichi opera aquí — interpreta object.bin

Modos de Direccionamiento en Eiichi

ModoDefineEjemplo en códigoSignificado
InmediatoAM_IMMEDT = 1GC(AM_IMMEDT); GC(3);El valor 3 viene literal en la instrucción
Directo a RegistroAM_DIRREG = 2GC(AM_DIRREG); GC(R_AC);El dato está en el registro AC
Directo a MemoriaAM_DIRMEM = 3(definido, no usado en ejemplos)El dato está en una dirección de memoria
06

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ódigoMnemónicoParámetrosUsa modo dir.Descripción
1MOV2✅ SíMueve un valor de origen a destino
2ADV0❌ NoAvanza el robot 1 posición
3DEC0❌ NoDecrementa ACC en 1
4JNZ1❌ NoSalta si ACC ≠ 0
5JEZ1❌ NoSalta si ACC = 0
6PUSH1✅ SíEmpuja valor al stack
7CMP0❌ NoCompara 2 valores del stack (resta, actualiza flags)
8END0❌ NoTermina el programa
9TRIGHT0❌ NoGira el robot a la derecha
10TLEFT0❌ NoGira el robot a la izquierda
11PICKUP0❌ NoRecoge el objeto
12PUTDOWN0❌ NoDeposita el objeto

Registros de la VM

PC — Program Counter

Apunta a la siguiente instrucción en mem[]. Inicia en 0.

AC — Advance Counter

Contador del bucle ADVANCE N. Se carga con N y decrementa hasta 0.

ACC — Accumulator

Registro general. DEC lo decrementa; CMP deposita ahí el resultado de la resta.

CRC — Crash Counter

Se incrementa cuando Eiichi va a chocar. Si choca dos veces con el mismo objeto → destruido.

SP — Stack Pointer

Índice en stack[]. PUSH escribe en stack[SP++]. CMP lee con stack[--SP].

IR1 / IR2

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
📌 Conexión directa parser → VM
El parser RDCP no solo valida la gramática — genera código ejecutable al mismo tiempo. Cada llamada a 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
💡 Backpatching
Al generar el 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.
07

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 byteEjemplo
1 = MOVAM (1=inmediato, 2=registro) + valor, luego AM + destino1 1 3 2 6 → MOV #3, AC
4 = JNZ / 5 = JEZDirección de salto (1 valor)4 5 → JNZ 5
6 = PUSHAM + valor6 2 5 → PUSH CRC
2,3,7,8,9,10,11,12Sin parámetros — el siguiente es ya un opcode2 → ADV

Desensamblado real del object.bin del proyecto

Este es exactamente el output que produce GC_VerifyGeneratedBinaryFile() al ejecutar la VM:

AddrInstrucciónBytesOrigen Eiichi
0MOV #3, AC01 01 03 02 06ADVANCE 3;
5MOV AC, ACC01 02 06 02 07
10ADV02
11DEC03
12MOV ACC, AC01 02 07 02 06
17JNZ 5 ↑04 05
19PICKUP11PICKUP;
20TRIGHT09TRIGHT;
21MOV #3, AC01 01 03 02 06ADVANCE 3;
26MOV AC, ACC01 02 06 02 07
31ADV02
32DEC03
33MOV ACC, AC01 02 07 02 06
38JNZ 26 ↑04 26
40PUSH CRC06 02 05IF_CRASH;
43PUSH #006 01 00
46CMP07
47JEZ 50 ↓ (backpatch)05 50
49TLEFT (hardcoded)0A
50MOV #5, AC ← JEZ salta aquí01 01 05 02 06ADVANCE 5;
55MOV AC, ACC01 02 06 02 07
60ADV02
61DEC03
62MOV ACC, AC01 02 07 02 06
67JNZ 55 ↑04 37
69END08fin del programa
📌 Programa Eiichi compilado
El programa fuente que generó este binario es:

ADVANCE 3; → avanza 3 casillas
PICKUP; → recoge el objeto
TRIGHT; → gira a la derecha
ADVANCE 3; → avanza 3 más
IF_CRASH; → si va a chocar, gira izquierda
ADVANCE 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

LÓGICAvm originalGetNextValue() declarada void pero retorna int
❌ void GetNextValue() { ... return intRes; }
✅ int GetNextValue() { ... return intRes; }
TYPOvm originalintREs en caso R_IR2
❌ intREs = IR2;
✅ intRes = IR2;
LÓGICAvm originali_jnz() sin cuerpo
❌ if (FLAG_GREATER_THAN_ZERO || FLAG_LESS_THAN_ZERO) } // cierra la función sin hacer nada
✅ if (FLAG_GREATER_THAN_ZERO || FLAG_LESS_THAN_ZERO) { PC = intNextAddress; }
LÓGICAvm originali_cmp() con sintaxis inválida
❌ int intMinuend = stack[ == SP];
✅ int intMinuend = stack[--SP];
SEMÁNTICOvm original= en lugar de == en i_pickup() e i_putdown()
❌ if (e_object.status = OS_GROUND) // asigna en lugar de comparar
✅ if (e_object.status == OS_GROUND)
LÓGICAvm originalD_WEST verifica y+1 en lugar de y-1
❌ if (!crash_with_obst(x, y + 1)) // dirección equivocada
✅ if (!crash_with_obst(x, y - 1))
LÓGICAvm originalD_SOUTH avanza con x-- en lugar de x++
❌ if (!crash_with_obst(x + 1, y)) x--; // debería incrementar
✅ x++;
FALTANTEvm originalmain() llama i_right() — función inexistente
❌ case A_TRIGHT: i_right(); break;
✅ case A_TRIGHT: i_tright(); break;
FALTANTEvm originalA_JNZ sin implementación en main()
❌ case A_JNZ: break; // ADVANCE nunca regresa al bucle
✅ case A_JNZ: i_jnz(); break;

Bugs pendientes en la VM v2 (corregida)

LÓGICAvm_v2.cRegistros inicializados con valores incorrectos
Se inicializan con el número de su propio #define en lugar de 0. PC=1, SP=4, CRC=5, etc. hacen que la VM empiece en estado incorrecto.
❌ int PC=1; int IR1=2; int IR2=3; int SP=4; int CRC=5; int AC=6; int ACC=7;
✅ int PC=0; int IR1=0; int IR2=0; int SP=0; int CRC=0; int AC=0; int ACC=0;
LÓGICAvm_v2.cGC_VerifyGeneratedBinaryFile — variable no inicializada
boolVerifyOperand se usa en un if antes de ser inicializado — comportamiento indefinido.
❌ if (!boolVerifyOperand) printf("================================\n");
✅ printf("================================\n"); // quitar el if
FALTANTEvm_v2.ccase OS_GROUND sin break en graphBoard_DrawEiichi()
Sin break, después de dibujar la 'e' cae al default — siempre se ejecuta el caso por defecto también.
❌ case OS_GROUND: graphBoard_DrawChar_RelPos(x,y,'e',2,2); default: // ← siempre se ejecuta
✅ case OS_GROUND: graphBoard_DrawChar_RelPos(x,y,'e',2,2); break; // ← agregar esto
LÓGICAvm_v2.cLoadProgramInMemory — fclose(NULL) si fopen falla
Si el archivo no existe, ptrBinaryFile es NULL pero fclose(NULL) se llama de todas formas — comportamiento indefinido.
❌ if (ptrBinaryFile != NULL) { while(...) {...} } else printf("Error\n"); fclose(ptrBinaryFile); // ← fuera del if
✅ if (ptrBinaryFile != NULL) { while(...) { mem[PCLocal++] = (int)sshValue; } fclose(ptrBinaryFile); // ← dentro del if } else printf("Error\n");

Bugs en parser_RDCP.c

FALTANTEparser_RDCP.cGC_AddToVectorAddressesToBeSolved() nunca se llama
Sin esta llamada, todos los JEZ generados por IF_CRASH quedan con dirección 0 y nunca saltan al lugar correcto. El backpatch no se aplica.
❌ Program(); GC(A_END); fclose(ptrGCBinaryFile); // ← falta la llamada antes de cerrar
✅ Program(); GC(A_END); GC_AddToVectorAddressesToBeSolved(); // ← resolver direcciones pendientes fclose(ptrGCBinaryFile);
LÓGICAparser_RDCP.cGOBACK sin generación de código
InstrAdvance solo hace Expect(T_GOBACK) pero no genera ningún GC(). La instrucción GOBACK se acepta sintácticamente pero no produce código ejecutable.
❌ else Expect(T_GOBACK); // sin GC()
✅ else { Expect(T_GOBACK); GC(A_GOBACK); // o la secuencia de movimiento inverso }
LÓGICAparser_RDCP.cInstrAdvance lee Num antes de validarlo
El valor numérico se extrae del token antes de que Expect() valide que efectivamente es un T_Num. Si hay error, el GC(Num) ya escribió basura al binario.
❌ signed short Num = atoi(ptrCurrentToken->strTokenSourceCodeText); Expect(T_Num); GC(A_MOV); ... GC(Num); ...
✅ if (CurrentToken(T_Num)) { signed short Num = atoi(ptrCurrentToken->strTokenSourceCodeText); Expect(T_Num); GC(A_MOV); ... GC(Num); ... }
🏗️

Arquitectura General del Compilador Eiichi

Pipeline completo

COMPONENTE 1 Scanner Análisis Léxico
scanner.c
COMPONENTE 2A Parser RDCP Sintáctico + GC()
parser_RDCP.c
COMPONENTE 2B Parser LL Solo reconocimiento
parser_LL.c
COMPONENTE 3 VM Eiichi Ejecución
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

AspectoRDCPLL(1)
MecanismoFunciones recursivas, pila del sistemaPila explícita t_stack + tabla RM[][]
FIRST implementado comoCurrentTokenInFirst() — switch manualColumnas de la tabla RM (posición = terminal)
Genera código✅ Sí — llama a GC() directamente❌ No — solo reconoce y muestra reglas
Gramática usadaCon while() para la repetición de ProgramCon Program2 para eliminar la recursión
Flag de errorboolParserError detiene todointFlagSyntaxError en loop do-while
Condición de aceptaciónTermina sin error + llega al ENDintFlagAcceptedInput cuando stack top = EOF = input
📌 Para el examen — conexiones teoría-código
FIRSTCurrentTokenInFirst() en RDCP / columnas de RM en LL
FOLLOW → regla 3 (Program2→ε con EOF en columna 13 de RM)
Backpatching → IF_CRASH usa GCPtr[] + fseek() para resolver JEZ
object.bin → secuencia de signed short, cada uno es un opcode, modo de dir. o valor
VM → carga el .bin en mem[] y lo ejecuta con el ciclo Fetch-Decode-Execute
Registros → todos deben iniciar en 0 (bug crítico en la versión original)