313 lines
9.8 KiB
Markdown
313 lines
9.8 KiB
Markdown
|
|
# TODO: Backend mycpu_v2 para j-lang
|
|||
|
|
|
|||
|
|
Roadmap de implementación del generador de código para el CPU v2 de 16-bit.
|
|||
|
|
El objetivo es que `gencode.h` tome el AST del frontend y produzca:
|
|||
|
|
- **Texto ensamblador** legible (para debug)
|
|||
|
|
- **Binario** (array de bytes para cargar en PROM)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Referencia rápida del CPU v2
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
Instrucción: 8 bytes = [OPCODE:16][PARAM1:16][PARAM2:16][TARGET:16]
|
|||
|
|
PC cuenta en words de 16-bit → instrucción N está en PC = N × 4
|
|||
|
|
|
|||
|
|
Registros libres: REG0-REG11 (12 registros, 16 bits cada uno)
|
|||
|
|
Registros especiales: REG12(RAM_VAL), REG13(RAM_ADDR), REG14(PC), REG15(I/O)
|
|||
|
|
|
|||
|
|
Modos de direccionamiento (codificados en el opcode):
|
|||
|
|
base + 0x00 = registro, registro
|
|||
|
|
base + 0x40 = inmediato, registro
|
|||
|
|
base + 0x80 = registro, inmediato
|
|||
|
|
base + 0xC0 = inmediato, inmediato
|
|||
|
|
|
|||
|
|
RAM:
|
|||
|
|
Leer: Escribir dirección en REG13 → REG12 se actualiza automáticamente
|
|||
|
|
Escribir: REG12 = valor, REG13 = addr → RSTR (0x18)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Pseudo-instrucciones útiles
|
|||
|
|
|
|||
|
|
```asm
|
|||
|
|
MOV #valor, REGn → ADD #valor, #0, REGn (opcode 0xC0)
|
|||
|
|
MOV REGa, REGb → ADD REGa, #0, REGb (opcode 0x80)
|
|||
|
|
JMP #addr → EQ #0, #0, addr (opcode 0xD0, siempre true)
|
|||
|
|
NOP → ADD #0, #0, REG0 (opcode 0xC0)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Convención de registros
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
REG0-REG3 → Temporales para evaluación de expresiones (expression stack)
|
|||
|
|
REG4-REG5 → Auxiliares (spill de expresiones profundas)
|
|||
|
|
REG6-REG11 → Libres / reserva futura (frame pointer, etc.)
|
|||
|
|
REG12 → RAM VALUE (especial, no tocar directamente)
|
|||
|
|
REG13 → RAM ADDR (especial, no tocar directamente)
|
|||
|
|
REG14 → PC (especial)
|
|||
|
|
REG15 → I/O (especial)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## Almacenamiento de variables
|
|||
|
|
|
|||
|
|
Todas las variables en **RAM**. Tabla nombre→dirección en tiempo de compilación.
|
|||
|
|
|
|||
|
|
```asm
|
|||
|
|
; Leer variable 'x' (dirección addr) → REGn
|
|||
|
|
ADD #addr, #0, REG13 ; REG13 = addr
|
|||
|
|
ADD REG12, #0, REGn ; REGn = RAM[addr] (lectura automática)
|
|||
|
|
|
|||
|
|
; Escribir variable 'x' (valor en REGn, dirección addr)
|
|||
|
|
ADD #addr, #0, REG13 ; REG13 = addr
|
|||
|
|
ADD REGn, #0, REG12 ; REG12 = valor
|
|||
|
|
RSTR ; RAM[addr] = REG12
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Fase 0: Infraestructura del emisor
|
|||
|
|
|
|||
|
|
**Objetivo**: Estructuras y funciones base para emitir instrucciones.
|
|||
|
|
|
|||
|
|
- [ ] Struct `Instruction` (opcode, param1, param2, target)
|
|||
|
|
- [ ] Buffer de instrucciones (array dinámico donde se acumulan)
|
|||
|
|
- [ ] Función `emit(opcode, p1, p2, target)` — agrega instrucción al buffer
|
|||
|
|
- [ ] Tabla de variables: mapeo nombre→dirección_RAM (compilación)
|
|||
|
|
- [ ] Función `lookupOrCreateVar(name)` — busca o asigna dirección RAM
|
|||
|
|
- [ ] Sistema de labels/backpatching:
|
|||
|
|
- [ ] `emitPlaceholder()` → emite instrucción con target=0, retorna índice
|
|||
|
|
- [ ] `patchTarget(index, target)` → rellena el target de instrucción emitida
|
|||
|
|
- [ ] `currentAddr()` → posición actual (nº de instrucción)
|
|||
|
|
- [ ] Output ASM: recorrer buffer → texto legible con mnemonicos
|
|||
|
|
- [ ] Output binario: recorrer buffer → array de bytes (8 bytes/instrucción)
|
|||
|
|
|
|||
|
|
**Criterio**: Emitir instrucciones hardcoded, ver texto ASM y binario generados.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Fase 1: Constantes, asignaciones y print
|
|||
|
|
|
|||
|
|
**Objetivo**: Compilar `x = 42` y `print x`.
|
|||
|
|
|
|||
|
|
- [ ] Compilar `NODE_INT_LIT` → cargar inmediato en REG[depth]
|
|||
|
|
```asm
|
|||
|
|
ADD #42, #0, REG0 ; MOV #42, REG0
|
|||
|
|
```
|
|||
|
|
- [ ] Compilar `NODE_ASSIGN` → evaluar expr → REG0, store en RAM
|
|||
|
|
```asm
|
|||
|
|
; (resultado ya en REG0)
|
|||
|
|
ADD #addr, #0, REG13 ; REG13 = dirección de variable
|
|||
|
|
ADD REG0, #0, REG12 ; REG12 = valor
|
|||
|
|
RSTR ; RAM[addr] = valor
|
|||
|
|
```
|
|||
|
|
- [ ] Compilar `NODE_VAR` → leer de RAM a REG[depth]
|
|||
|
|
```asm
|
|||
|
|
ADD #addr, #0, REG13 ; REG13 = dirección
|
|||
|
|
ADD REG12, #0, REG0 ; REG0 = RAM[addr]
|
|||
|
|
```
|
|||
|
|
- [ ] Compilar `NODE_PRINT` → evaluar expr → REG0, copiar a I/O
|
|||
|
|
```asm
|
|||
|
|
ADD REG0, #0, REG15 ; OUTPUT = REG0
|
|||
|
|
```
|
|||
|
|
- [ ] Compilar `NODE_BLOCK` → iterar y compilar cada statement
|
|||
|
|
|
|||
|
|
**Test**: `simple.j` (x = 10). `print 10` → escribe 10 en REG15.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Fase 2: Expresiones aritméticas
|
|||
|
|
|
|||
|
|
**Objetivo**: Compilar `x = 10 + 20 * 3`.
|
|||
|
|
|
|||
|
|
**Estrategia**: Register depth counter. Cada sub-expresión deposita resultado en `REG[depth]`.
|
|||
|
|
|
|||
|
|
- [ ] Variable `int reg_depth = 0` para tracking
|
|||
|
|
- [ ] Compilar `NODE_BINOP`:
|
|||
|
|
```
|
|||
|
|
compilar left → resultado en REG[depth]
|
|||
|
|
depth++
|
|||
|
|
compilar right → resultado en REG[depth]
|
|||
|
|
depth--
|
|||
|
|
emit OP REG[depth], REG[depth+1], REG[depth]
|
|||
|
|
```
|
|||
|
|
- [ ] Manejar profundidad > 4 → PUSH/POP al stack (spill)
|
|||
|
|
- [ ] Mapeo de operadores:
|
|||
|
|
- `+` → ADD (0x00)
|
|||
|
|
- `-` → SUB (0x01)
|
|||
|
|
- `*` → MUL (0x02)
|
|||
|
|
- `/` → DIV (0x03)
|
|||
|
|
|
|||
|
|
**Test**: `sum.j`, `resta.j`. Verificar que `2 + 3 * 4` da 14.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Fase 3: Comparaciones y control de flujo
|
|||
|
|
|
|||
|
|
**Objetivo**: Compilar `if` y `while`.
|
|||
|
|
|
|||
|
|
### if
|
|||
|
|
|
|||
|
|
- [ ] Compilar `NODE_IF`:
|
|||
|
|
```
|
|||
|
|
compilar condición left → REG0
|
|||
|
|
compilar condición right → REG1
|
|||
|
|
emit CONDICIONAL_INVERSO REG0, REG1, [placeholder]
|
|||
|
|
compilar bloque then
|
|||
|
|
patch placeholder → currentAddr() × 4
|
|||
|
|
```
|
|||
|
|
- [ ] Mapeo de condicionales **inversos** (saltar si la condición es FALSA):
|
|||
|
|
- `==` en AST → emit `NEQ` (0x11)
|
|||
|
|
- `!=` en AST → emit `EQ` (0x10)
|
|||
|
|
- `<` en AST → emit `GRE` (0x15) — saltar si >=
|
|||
|
|
- `>` en AST → emit `LSE` (0x13) — saltar si <=
|
|||
|
|
|
|||
|
|
### while
|
|||
|
|
|
|||
|
|
- [ ] Compilar `NODE_WHILE`:
|
|||
|
|
```
|
|||
|
|
loop_start = currentAddr()
|
|||
|
|
compilar condición left → REG0
|
|||
|
|
compilar condición right → REG1
|
|||
|
|
emit CONDICIONAL_INVERSO REG0, REG1, [placeholder_exit]
|
|||
|
|
compilar cuerpo
|
|||
|
|
emit EQ #0, #0, (loop_start × 4) ; JMP incondicional
|
|||
|
|
patch placeholder_exit → currentAddr() × 4
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Recordar
|
|||
|
|
- **PC = instrucción_index × 4** (cada instrucción = 4 words de 16-bit)
|
|||
|
|
- El salto incondicional es `EQ #0, #0, target` (0xD0, siempre true)
|
|||
|
|
|
|||
|
|
**Test**: `if.j`, `while.j`. While que cuenta de 0 a 10.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Fase 4: Funciones (CALL/RET)
|
|||
|
|
|
|||
|
|
**Objetivo**: Compilar `fn` definitions y llamadas.
|
|||
|
|
|
|||
|
|
### Convención de llamada
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
1. Caller pushea argumentos al stack (derecha a izquierda)
|
|||
|
|
2. Caller ejecuta CALL #dirección (pushea PC+1 al stack, salta)
|
|||
|
|
3. Callee popea argumentos → variables locales en RAM
|
|||
|
|
4. Callee ejecuta cuerpo
|
|||
|
|
5. Callee deja resultado en REG0
|
|||
|
|
6. Callee ejecuta RET (popea PC del stack, salta)
|
|||
|
|
7. Caller usa REG0 como valor de retorno
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Tareas
|
|||
|
|
|
|||
|
|
- [ ] Compilar `NODE_FN_DEF`:
|
|||
|
|
```
|
|||
|
|
emit JMP [placeholder_skip] ; saltar sobre el cuerpo
|
|||
|
|
fn_addr = currentAddr()
|
|||
|
|
para cada param (de derecha a izq):
|
|||
|
|
emit POP → REGn
|
|||
|
|
store REGn → RAM[param_addr]
|
|||
|
|
compilar cuerpo
|
|||
|
|
emit RET
|
|||
|
|
patch placeholder_skip → currentAddr() × 4
|
|||
|
|
registrar fn_name → fn_addr en tabla de funciones
|
|||
|
|
```
|
|||
|
|
- [ ] Compilar `NODE_CALL`:
|
|||
|
|
```
|
|||
|
|
para cada argumento (de izq a der):
|
|||
|
|
compilar argumento → REG0
|
|||
|
|
emit PUSH REG0
|
|||
|
|
emit CALL #(fn_addr × 4)
|
|||
|
|
; resultado queda en REG0
|
|||
|
|
```
|
|||
|
|
- [ ] Compilar `NODE_RETURN`:
|
|||
|
|
```
|
|||
|
|
compilar expresión → REG0
|
|||
|
|
emit RET
|
|||
|
|
```
|
|||
|
|
- [ ] Resolver scope de variables locales:
|
|||
|
|
- **Opción simple**: cada función tiene su propio rango de RAM
|
|||
|
|
- **Opción avanzada**: frame pointer (registro base + offset para locales)
|
|||
|
|
|
|||
|
|
**Test**: `functions.j`, `custom_fn.j`.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Fase 5: Strings y objetos (avanzado)
|
|||
|
|
|
|||
|
|
**Objetivo**: Soportar strings, clases, campos e instancias.
|
|||
|
|
|
|||
|
|
- [ ] Strings en RAM — caracteres consecutivos, variable apunta a dirección base
|
|||
|
|
- [ ] Print de strings — loop: leer cada char de RAM → escribir en REG15
|
|||
|
|
- [ ] Instancias — bloque de RAM con campos, variable apunta a base
|
|||
|
|
- [ ] Campos — offset fijo desde base de instancia
|
|||
|
|
- [ ] Métodos — funciones con `self` (dirección de instancia) como primer arg
|
|||
|
|
- [ ] Constructor — reservar espacio en RAM, llamar a `init`
|
|||
|
|
|
|||
|
|
**Nota**: Requiere un allocator en runtime para reservar memoria dinámica en RAM.
|
|||
|
|
|
|||
|
|
### Estrategia de allocator en runtime
|
|||
|
|
|
|||
|
|
Hay dos opciones, de menor a mayor complejidad:
|
|||
|
|
|
|||
|
|
**Opción A: Bump allocator (recomendado para empezar)**
|
|||
|
|
|
|||
|
|
La más simple. Una dirección de RAM fija (ej: `RAM[0x00FF]`) actúa como "heap pointer" que empieza al final de las variables estáticas. Cada asignación avanza el pointer. No tiene `free`.
|
|||
|
|
|
|||
|
|
```asm
|
|||
|
|
; alloc(size) — size en REG1, retorna dirección en REG0
|
|||
|
|
ADD #0x00FF, #0, REG13 ; REG13 = dirección del heap_ptr
|
|||
|
|
ADD REG12, #0, REG0 ; REG0 = heap_ptr actual (dirección a retornar)
|
|||
|
|
ADD REG12, REG1, REG12 ; REG12 = heap_ptr + size
|
|||
|
|
RSTR ; guardar nuevo heap_ptr en RAM[0x00FF]
|
|||
|
|
; REG0 = dirección del bloque asignado
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
~4 instrucciones. Suficiente para strings literales y concatenaciones simples.
|
|||
|
|
|
|||
|
|
**Opción B: Allocator con metadata (como `allocator.h`, pero en ASM del CPU v2)**
|
|||
|
|
|
|||
|
|
Mismo diseño conceptual que `src/memory/allocator.h` pero implementado como rutina en ensamblador del CPU v2:
|
|||
|
|
- Cada bloque en RAM: `[size:16][in_use:16][payload...]`
|
|||
|
|
- Loop que recorre bloques con comparaciones + saltos (first-fit)
|
|||
|
|
- `free` marca `in_use = 0`
|
|||
|
|
- ~30-50 instrucciones del CPU v2
|
|||
|
|
|
|||
|
|
Solo necesario si se van a liberar strings (reasignar variables string, concatenaciones temporales).
|
|||
|
|
|
|||
|
|
**Recomendación**: Empezar con bump allocator. Si más adelante se necesita `free`, implementar opción B usando el diseño de `allocator.h` como referencia conceptual.
|
|||
|
|
|
|||
|
|
**Test**: `str.j`, `classes.j`.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Diagrama de dependencias
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
Fase 0 (infraestructura)
|
|||
|
|
│
|
|||
|
|
└── Fase 1 (constantes, asignación, print)
|
|||
|
|
│
|
|||
|
|
└── Fase 2 (aritmética)
|
|||
|
|
│
|
|||
|
|
└── Fase 3 (if/while)
|
|||
|
|
│
|
|||
|
|
└── Fase 4 (funciones)
|
|||
|
|
│
|
|||
|
|
└── Fase 5 (strings/objetos)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## Verificación por fase
|
|||
|
|
|
|||
|
|
| Fase | Archivos de test |
|
|||
|
|
|------|-----------------|
|
|||
|
|
| 1 | `simple.j` |
|
|||
|
|
| 2 | `sum.j`, `resta.j` |
|
|||
|
|
| 3 | `if.j`, `while.j` |
|
|||
|
|
| 4 | `functions.j`, `custom_fn.j` |
|
|||
|
|
| 5 | `str.j`, `classes.j` |
|
|||
|
|
|
|||
|
|
Comparar output generado (ASM + binario) con lo que la VM produce para la misma entrada.
|