Files
j-lang/docs/roadmap.md
Jose Luis Montañes Ojados 74b426e12d first commit
2026-02-15 20:48:01 +01:00

11 KiB

j-lang: Roadmap detallado

Guia de implementacion fase por fase. Cada fase construye sobre la anterior.

src/
  memory/     -> Fase 1 y 5
  objects/    -> Fase 2
  frontend/   -> Fase 3
  vm/         -> Fase 4
  main.c      -> Punto de entrada (une todo)

Fase 1: Gestion de Memoria

Objetivo: Tener un malloc y free propios que gestionen un heap simulado con metadatos compactos.

Archivos: src/memory/allocator.h

Estado actual: Ya existe una implementacion en mem-heap/src/allocator.h con:

  • CMA_metadata (struct con size y in_use)
  • CMA_CreateAllocator() - crea un heap de 1KB
  • CMA_malloc() - asigna bloques (solo append, no reutiliza huecos)
  • CMA_used() - calcula bytes usados
  • CMA_last_free() - encuentra el primer bloque libre
  • CMA_visualize() - dibuja el heap en consola con colores

Tareas

1.1 Copiar allocator al proyecto

  • Copiar mem-heap/src/allocator.h a src/memory/allocator.h
  • Verificar que compila desde main.c con #include "memory/allocator.h"

1.2 Implementar CMA_free

Liberar un bloque marcandolo como no-usado.

// Firma
void CMA_free(void *allocator, void *ptr);

// Logica:
// 1. ptr apunta al payload, retroceder sizeof(CMA_metadata) para llegar al header
// 2. Poner header->in_use = 0
// 3. Opcionalmente: poner a cero los bytes del payload (memset)

1.3 Reutilizar bloques liberados (first-fit)

Actualmente CMA_malloc solo hace append al final. Hay que recorrer el heap buscando bloques libres que quepan.

// En CMA_malloc, antes de ir al final:
// 1. Recorrer bloques desde el inicio
// 2. Si encuentras uno con in_use == 0 && size >= requested_size -> reutilizarlo
// 3. Si no hay ninguno, seguir con el append actual

1.4 Compactar la cabecera (opcional, mejora)

El readme menciona compactar Size + Marked + InUse en menos bytes. La idea:

  • Usar bitfields o empaquetar en un uint32_t:
    • Bits 0-29: size (hasta ~1GB)
    • Bit 30: marked (para el GC, Fase 5)
    • Bit 31: in_use
typedef struct {
    uint32_t header; // size:30 | marked:1 | in_use:1
} CMA_metadata_compact;

#define META_SIZE(h)    ((h).header >> 2)
#define META_MARKED(h)  (((h).header >> 1) & 1)
#define META_INUSE(h)   ((h).header & 1)

Criterio de terminado

  • CMA_malloc asigna bloques correctamente
  • CMA_free libera bloques
  • CMA_malloc reutiliza bloques liberados
  • CMA_visualize muestra bloques libres vs ocupados correctamente
  • Test: malloc -> free -> malloc reutiliza el mismo espacio

Fase 2: Modelo de Objetos

Objetivo: Definir como se representan los valores del lenguaje (numeros, strings, listas) en memoria C.

Archivos: src/objects/object.h

Dependencias: Fase 1 (los objetos se crean con CMA_malloc)

Tareas

2.1 Definir los tipos

typedef enum {
    OBJ_INT,
    OBJ_FLOAT,
    OBJ_STRING,
    OBJ_LIST,
    OBJ_NONE     // equivalente a None/null
} ObjectType;

2.2 Definir el struct Object

Enfoque con tagged union:

typedef struct Object {
    ObjectType type;
    union {
        int int_val;           // OBJ_INT
        double float_val;      // OBJ_FLOAT
        struct {               // OBJ_STRING
            char *chars;
            int length;
        } string_val;
        struct {               // OBJ_LIST
            struct Object **items;
            int count;
            int capacity;
        } list_val;
    } data;
} Object;

2.3 Funciones constructoras

Cada tipo necesita una funcion que cree el objeto usando el allocator:

Object* obj_new_int(void *allocator, int value);
Object* obj_new_string(void *allocator, const char *str);
Object* obj_new_list(void *allocator);

2.4 Funcion de impresion

void obj_print(Object *obj);
// OBJ_INT -> printf("%d", obj->data.int_val)
// OBJ_STRING -> printf("%s", obj->data.string_val.chars)
// OBJ_LIST -> printf("["); ...cada elemento...; printf("]")

Criterio de terminado

  • Se pueden crear objetos de cada tipo con el allocator
  • obj_print los muestra correctamente
  • Test: crear int(42), string("hola"), lista con ambos, imprimir todo

Fase 3: Front-End (Lexer y Parser)

Objetivo: Convertir texto fuente (.j) en un AST que la VM pueda ejecutar.

Archivos: src/frontend/lexer.h, src/frontend/parser.h

Dependencias: Fase 2 (el parser crea nodos AST que referencian tipos de Object)

Tareas

3.1 Lexer (Tokenizador)

Convierte texto en una lista de tokens.

typedef enum {
    // Literales
    TOK_INT,        // 42
    TOK_STRING,     // "hola"

    // Identificadores y keywords
    TOK_ID,         // x, foo, mi_var
    TOK_PRINT,      // print (keyword)
    TOK_IF,         // if
    TOK_WHILE,      // while

    // Operadores
    TOK_ASSIGN,     // =
    TOK_PLUS,       // +
    TOK_MINUS,      // -
    TOK_STAR,       // *
    TOK_SLASH,      // /
    TOK_EQ,         // ==
    TOK_NEQ,        // !=
    TOK_LT,         // <
    TOK_GT,         // >

    // Delimitadores
    TOK_LPAREN,     // (
    TOK_RPAREN,     // )
    TOK_COLON,      // :
    TOK_NEWLINE,    // \n (significativo, como en Python)
    TOK_INDENT,     // aumento de indentacion
    TOK_DEDENT,     // reduccion de indentacion

    TOK_EOF
} TokenType;

typedef struct {
    TokenType type;
    char *value;    // texto literal del token
    int line;       // linea del fuente (para errores)
} Token;

// Firma principal
Token* tokenize(const char *source, int *token_count);

Empezar simple: solo TOK_INT, TOK_ID, TOK_ASSIGN, TOK_NEWLINE, TOK_EOF. Ir agregando tokens conforme se necesiten.

3.2 Parser (AST)

Convierte tokens en un arbol.

typedef enum {
    NODE_INT_LIT,     // literal entero: 42
    NODE_STRING_LIT,  // literal string: "hola"
    NODE_VAR,         // referencia a variable: x
    NODE_ASSIGN,      // asignacion: x = expr
    NODE_BINOP,       // operacion binaria: a + b
    NODE_PRINT,       // print(expr)
    NODE_IF,          // if cond: bloque
    NODE_WHILE,       // while cond: bloque
    NODE_BLOCK,       // secuencia de statements
} NodeType;

typedef struct ASTNode {
    NodeType type;
    union {
        int int_val;                          // NODE_INT_LIT
        char *string_val;                     // NODE_STRING_LIT, NODE_VAR
        struct { char *name; struct ASTNode *value; } assign;       // NODE_ASSIGN
        struct { char op; struct ASTNode *left; struct ASTNode *right; } binop;  // NODE_BINOP
        struct { struct ASTNode *expr; } print;                     // NODE_PRINT
        struct { struct ASTNode **stmts; int count; } block;        // NODE_BLOCK
    } data;
} ASTNode;

// Firma principal
ASTNode* parse(Token *tokens, int token_count);

3.3 Gramatica minima inicial

Empezar con un subconjunto muy pequeno:

programa  = { statement NEWLINE }
statement = ID "=" expr        (asignacion)
          | "print" expr       (impresion)
expr      = term { ("+" | "-") term }
term      = INT | ID

Esto permite ejecutar cosas como:

x = 10
y = 20
z = x + y
print z

Criterio de terminado

  • El lexer tokeniza x = 10\nprint x correctamente
  • El parser genera un AST valido a partir de esos tokens
  • Test: tokenizar y parsear simple.j, imprimir el arbol resultante

Fase 4: Evaluador / VM

Objetivo: Recorrer el AST y ejecutar las instrucciones.

Archivos: src/vm/eval.h

Dependencias: Fase 2 (crea Objects), Fase 3 (recibe un AST)

Tareas

4.1 Tabla de variables (Environment)

typedef struct {
    char *name;
    Object *value;
} Variable;

typedef struct {
    Variable vars[256];  // simple, tamanio fijo por ahora
    int count;
} Environment;

// Buscar o crear variable
Object* env_get(Environment *env, const char *name);
void env_set(Environment *env, const char *name, Object *value);

4.2 Funcion eval recursiva

Object* eval(ASTNode *node, Environment *env, void *allocator);

// Logica por tipo de nodo:
// NODE_INT_LIT   -> obj_new_int(allocator, node->data.int_val)
// NODE_VAR       -> env_get(env, node->data.string_val)
// NODE_ASSIGN    -> evaluar valor, env_set(name, resultado)
// NODE_BINOP     -> evaluar left y right, operar segun op
// NODE_PRINT     -> evaluar expr, obj_print(resultado)
// NODE_BLOCK     -> evaluar cada statement en orden

4.3 Flujo completo en main.c

int main(int argc, char *argv[]) {
    // 1. Leer archivo .j
    char *source = read_file(argv[1]);

    // 2. Tokenizar
    int token_count;
    Token *tokens = tokenize(source, &token_count);

    // 3. Parsear
    ASTNode *program = parse(tokens, token_count);

    // 4. Ejecutar
    void *allocator = CMA_CreateAllocator();
    Environment env = {0};
    eval(program, &env, allocator);

    return 0;
}

Criterio de terminado

  • simple.j con x = 1 se ejecuta sin errores
  • Un script con x = 10\ny = 20\nprint x + y imprime 30
  • Las variables se almacenan y recuperan correctamente

Fase 5: Recolector de Basura (GC)

Objetivo: Liberar automaticamente objetos que ya no son accesibles.

Archivos: src/memory/gc.h

Dependencias: Fase 1 (usa CMA_free), Fase 2 (recorre Objects), Fase 4 (accede al Environment)

Tareas

5.1 Agregar bit marked al metadata

Si se compacto la cabecera en Fase 1.4, ya esta. Si no, agregar un campo:

typedef struct {
    size_t size;
    int in_use;
    int marked;   // <-- nuevo
} CMA_metadata;

5.2 Mark (Marcar)

Recorrer todos los objetos alcanzables desde las variables del Environment y marcarlos.

void gc_mark_object(Object *obj);
// 1. Obtener el header del objeto (retroceder sizeof(CMA_metadata))
// 2. Poner marked = 1
// 3. Si es OBJ_LIST, marcar recursivamente cada elemento
// 4. Si es OBJ_STRING, marcar el buffer de chars

void gc_mark(Environment *env);
// Para cada variable en env: gc_mark_object(variable.value)

5.3 Sweep (Barrer)

Recorrer todo el heap linealmente. Si un bloque tiene in_use == 1 y marked == 0, liberarlo.

void gc_sweep(void *allocator);
// 1. Empezar desde el inicio del heap
// 2. Para cada bloque:
//    - Si in_use && !marked -> CMA_free
//    - Si marked -> quitar marked (resetear para el proximo ciclo)
// 3. Avanzar al siguiente bloque con (header + sizeof(CMA_metadata) + size)

5.4 Integrar con el evaluador

// En eval(), despues de N asignaciones o cuando quede poca memoria:
void gc_collect(void *allocator, Environment *env) {
    gc_mark(env);
    gc_sweep(allocator);
}

Criterio de terminado

  • Crear objetos, perder la referencia, ejecutar GC -> la memoria se libera
  • CMA_visualize muestra bloques liberados tras el GC
  • Test: crear 100 objetos en un loop, verificar que la memoria no crece indefinidamente

Orden de implementacion recomendado

Fase 1.1 (copiar allocator)
  |
Fase 1.2 (CMA_free)
  |
Fase 1.3 (reutilizar bloques)
  |
Fase 2 (model de objetos)
  |
Fase 3.1 (lexer basico)
  |
Fase 3.2 (parser basico)
  |
Fase 4 (eval + main.c)          <- aqui ya puedes ejecutar simple.j
  |
Fase 1.4 (compactar cabecera)   <- opcional, mejora
  |
Fase 5 (GC)

Test de integracion final

Cuando todo este listo, este script deberia funcionar:

x = 10
y = 20
z = x + y
print z

Salida esperada: 30