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 consizeyin_use)CMA_CreateAllocator()- crea un heap de 1KBCMA_malloc()- asigna bloques (solo append, no reutiliza huecos)CMA_used()- calcula bytes usadosCMA_last_free()- encuentra el primer bloque libreCMA_visualize()- dibuja el heap en consola con colores
Tareas
1.1 Copiar allocator al proyecto
- Copiar
mem-heap/src/allocator.hasrc/memory/allocator.h - Verificar que compila desde
main.ccon#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_mallocasigna bloques correctamenteCMA_freelibera bloquesCMA_mallocreutiliza bloques liberadosCMA_visualizemuestra 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_printlos 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 xcorrectamente - 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.jconx = 1se ejecuta sin errores- Un script con
x = 10\ny = 20\nprint x + yimprime30 - 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_visualizemuestra 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