Saltar al contenido
cajulang

Cómo funciona Caju

De un archivo .caju a un programa en ejecución: las etapas del compilador, cómo se guardan los valores en memoria, el recolector de basura, las llamadas y los panics, y cómo el compilador acaba ejecutándose en tu navegador. Todo lo que se muestra aquí sale del compilador real.

Del código fuente al ejecutable

El compilador es una cadena de etapas, cada una escrita en Caju (compiler/*.caju). Sigue un programa pequeño a través de ellas: elige una etapa para ver lo que produce.

02

Analizador léxico

lexer.caju corta el texto en tokens: palabras clave, nombres, operadores, números, textos. Cada token guarda su línea y su columna, y así cada error posterior señala el lugar correcto. Los saltos de línea se convierten en tokens NEWLINE: terminan las sentencias.

El programa de ejemplo · main.caju
fn add(a: int, b: int) -> int {
return a + b
}
print(add(2, 3))
Lo que produce esta etapa: tokens
  1. fnKEYWORD 1:1
  2. addNAME 1:4
  3. (OP 1:7
  4. aNAME 1:8
  5. :OP 1:9
  6. intNAME 1:11
  7. ,OP 1:14
  8. bNAME 1:16
  9. :OP 1:17
  10. intNAME 1:19
  11. )OP 1:22
  12. ->OP 1:24
  13. intNAME 1:27
  14. {OP 1:31
  15. ↵NEWLINE 1:32
  16. returnKEYWORD 2:3
  17. aNAME 2:10
  18. +OP 2:12
  19. bNAME 2:14
  20. ↵NEWLINE 2:15
  21. }OP 3:1
  22. ↵NEWLINE 3:2
  23. printNAME 5:1
  24. (OP 5:6
  25. addNAME 5:7
  26. (OP 5:10
  27. 2NUMBER 5:11
  28. ,OP 5:12
  29. 3NUMBER 5:14
  30. )OP 5:15
  31. )OP 5:16
  32. ↵NEWLINE 5:17
  33. ∎EOF 6:1

Un compilador que se compila a sí mismo

El compilador de Caju está escrito en Caju. Para obtener el primero, se usa una vez un pequeño compilador escrito en JavaScript; a partir de ahí, el compilador compila su propio código fuente.

  1. 1

    Etapa 0

    bootstrap/*.js: el compilador original, en JavaScript, ejecutado con Node.

  2. 2

    Etapa 1

    La etapa 0 compila compiler/main.caju: un caju nativo, hecho por el compilador de JavaScript.

  3. 3

    Etapa 2

    La etapa 1 compila el mismo código fuente: ahora es un compilador hecho por un compilador de Caju.

  4. 4

    Etapa 3

    La etapa 2 lo compila otra vez. Las etapas 2 y 3 deben ser idénticas byte a byte: el compilador se reproduce a sí mismo.

  5. 5

    caju.wasm

    Con --target wasm, el compilador se compila a sí mismo a WebAssembly; después, caju.wasm también se compila a sí mismo a los mismos bytes.

cmp build/caju2 build/caju3 && echo okstage 2 == stage 3

Cada cambio en el compilador se comprueba así, además de con la batería de pruebas: si las etapas 2 y 3 difieren, algo en el compilador cambió el significado del propio compilador.

Valores en memoria

Cada valor cabe en una palabra de 64 bits. Los números y los booleanos son la propia palabra; todo lo demás es un puntero a un objeto creado por el runtime.

s := "caju"
s
●
str
len
4
byte
c
byte
a
byte
j
byte
u
NUL
0

Un puntero a [longitud][bytes UTF-8][0]. Los textos nunca cambian: + crea uno nuevo, y todo el programa comparte el mismo literal.

El recolector de basura

El runtime asigna los objetos en páginas de 64 KB y los recolecta con marcado y barrido, escrito en Caju (rt/gc.caju). Pruébalo: las raíces son la pila y las variables globales del programa.

Cada objeto es alcanzable desde una raíz o no lo es. Pulsa Marcar.

RaícesHeapstack: liststack: userglobal: cache[]intarray*User"Ana"mapmap"old"structclosure
  • aún no visitado
  • marcado (alcanzable)
  • basura
  • liberado

El recolector es conservador: cualquier palabra en la pila o en un objeto que parezca un puntero a un objeto lo mantiene vivo. Por eso un puntero nunca debe estar solo en un registro durante una llamada, y por eso la versión WebAssembly guarda en una pila en memoria cada valor que debe sobrevivir a una llamada. Una recolección empieza cuando la memoria asignada desde la anterior supera lo que sobrevivió a ella (como mínimo 8 MB).

Llamadas, defer y panic

Cada llamada apila sus argumentos y cada función guarda sus variables locales en un marco. defer, panic y recover forman parte del runtime.

↑ llamante
locales
sum
list ●
argumentos
a = 2
b = 3
marco ($fp) →
↓ llamada
locales
x
y ●
temporales
t ●
puntero de pila ($sp) →

Una llamada

Quien llama apila los argumentos (y la closure, si es un valor función) y hace la llamada; la función llamada encuentra el argumento i de n en un desplazamiento fijo y reserva espacio para sus variables locales. Los resultados 2, 3... viajan en g.ret.

El límite de profundidad

Un contador de llamadas restantes empieza en 10.000 y cada llamada gasta una: la llamada anidada número 10.001 se detiene con "Too much recursion" en lugar de bloquear el programa.

defer

Una función con defer enlaza un registro en una lista (rt_dtop); cada defer añade un nodo con la función y sus argumentos ya evaluados. Se ejecutan del último al primero cuando la función retorna.

panic y recover

panic ejecuta las llamadas diferidas del registro más reciente al más antiguo. Si una de ellas llama a recover(), la ejecución salta de vuelta a la función que la difirió, que retorna con normalidad. En WebAssembly, ese salto es una excepción de wasm.

Caju en tu navegador

El playground ejecuta el compilador real, compilado a WebAssembly, sin servidor.

  1. 1

    La versión

    Cada versión en GitHub incluye caju-wasm.tar.gz: el compilador como caju.wasm, la biblioteca estándar y el runtime como texto, y un pequeño host en JavaScript.

  2. 2

    El worker

    La página inicia un Web Worker que carga caju.wasm y un sistema de archivos en memoria con la biblioteca.

  3. 3

    Compilar

    Tus archivos van a ese sistema de archivos y caju build --target wasm se ejecuta dentro de caju.wasm, igual que en Linux: el host responde a sus llamadas al sistema (open, read, write...).

  4. 4

    Ejecutar

    El nuevo módulo se ejecuta en el mismo worker; lo que escribe llega a la página a medida que se escribe. Detener termina el worker.

Playground →

Alrededor del compilador

caju test

Busca los archivos *_test.caju y los ejecuta: describe, it y expect de std/testing.

caju pkg

Los paquetes son repositorios de GitHub; selección de versión mínima y un caju.lock con hashes SHA-256.

caju mcp

Un servidor de Model Context Protocol: los asistentes de IA comprueban, ejecutan y prueban código Caju con resultados estructurados.