Cambia completamente tu forma de pensar
Programación literaria & IA
Literate Driven
Development
Temo Pacheco
La metodología más poderosa para desarrollar con IA.
No necesitas:
Vibe coding
Spec Driven Development
Harness Engineering
Agentic loops
La LDD resuelve 5 problemas que enfrenta el desarrollo asistido con IA.
La industria no ha logrado resolver el primero.
Entender
1
Complejidad
2
Rapidez
3
Economía
4
Obras de arte
5
Programación literaria
Inteligencia artificial
Literate Driven Development
Programación literaria
Donald Knuth
Matemático y programador
1984
1993
La conocí
2026
“Cambiemos nuestra actitud tradicional hacia la construcción de programas.
En lugar de imaginar que nuestra tarea principal es instruir a una computadora sobre qué hacer,
concentrémonos más bien en explicarles a los seres humanos qué queremos que haga una computadora.”
Donald Knuth · Literate Programming, 1984
El orden de siempre
1°
para que lo ejecute la máquina
2°
para que lo lean y entiendan otras personas
Knuth propuso darle la vuelta
1°
para que lo lean y entiendan otras personas
2°
para que lo ejecute la máquina
Dar prioridad a la explicación a los humanos.
Esto es la Programación Literaria.
Te libera de la estructura que impone la máquina
CARPETAS Y ARCHIVOS
src/
└ main.c
└ cobrar.c
└ boot.asm
include/
└ pago.h
scripts/
└ deploy.sh
└ gen.py
.c
.h
.asm
.sh
.py
COMPILADOR
cc · ld
compila cada archivo
ordena los símbolos
enlaza el binario
Nada de eso está mal. Así es como los programas están organizados.
Te libera de la estructura que impone la máquina
Para qué
existe esto
01
Cómo
se usa
02
Cómo
funciona
03
Los rincones
más internos
04
Te libera de la estructura que impone la máquina
Para qué
existe esto
01
Cómo
funciona
03
Cómo
se usa
02
Los rincones
más internos
04
Te libera de la estructura que impone la máquina
Cómo
se usa
02
Los rincones
más internos
04
Para qué
existe esto
01
Cómo
funciona
03
tú
+
IA
lexer.c
emit.asm
int es_digito(char c) {
return c >= '0' && c <= '9';
}
Token leer_numero(const char *s, int *i) {
int v = 0;
while (es_digito(s[*i]))
v = v*10 + (s[(*i)++] - '0');
return (Token){ TOK_NUM, v };
}
Token siguiente(const char *s, int *i) {
while (s[*i] == ' ') (*i)++;
if (es_digito(s[*i]))
return leer_numero(s, i);
if (s[*i] == '+') { (*i)++;
return (Token){ TOK_MAS, 0 };
}
return (Token){ TOK_FIN, 0 };
emitir_suma:
ldr x0, [sp], #16
ldr x1, [sp], #16
add x0, x0, x1
str x0, [sp, #-16]!
ret
emitir_num:
mov x0, #0
ldr x0, [x19], #8
str x0, [sp, #-16]!
emitir_fin:
mov x0, #0
ldp x29, x30, [sp], #16
ret
int es_digito(char c) {
return c >= '0' && c <= '9';
}
Token leer_numero(const char *s, int *i) {
int v = 0;
while (es_digito(s[*i]))
v = v*10 + (s[(*i)++] - '0');
return (Token){ TOK_NUM, v };
}
Token siguiente(const char *s, int *i) {
while (s[*i] == ' ') (*i)++;
if (es_digito(s[*i]))
return leer_numero(s, i);
if (s[*i] == '+') { (*i)++;
return (Token){ TOK_MAS, 0 };
}
return (Token){ TOK_FIN, 0 };
emitir_suma:
ldr x0, [sp], #16
ldr x1, [sp], #16
add x0, x0, x1
str x0, [sp, #-16]!
ret
emitir_num:
mov x0, #0
ldr x0, [x19], #8
str x0, [sp, #-16]!
emitir_fin:
mov x0, #0
ldp x29, x30, [sp], #16
ret
La explicación viaja entretejida con el código
<Reconocer un dígito>
int es_digito(char c) {
return c >= '0' && c <= '9';
}
<Reconocer un número>
Token leer_numero(const char *s, int *i) {
int v = 0;
while (es_digito(s[*i]))
v = v*10 + (s[(*i)++] - '0');
return (Token){ TOK_NUM, v };
}
<El siguiente token>
Token siguiente(const char *s, int *i) {
while (s[*i] == ' ') (*i)++;
if (es_digito(s[*i]))
return leer_numero(s, i);
<Reconocer un operador>
if (s[*i] == '+') { (*i)++;
return (Token){ TOK_MAS, 0 };
}
return (Token){ TOK_FIN, 0 };
<Emitir una suma>
emitir_suma:
ldr x0, [sp], #16
ldr x1, [sp], #16
<Apilar el resultado>
add x0, x0, x1
str x0, [sp, #-16]!
ret
<Emitir un número>
emitir_num:
mov x0, #0
ldr x0, [x19], #8
str x0, [sp, #-16]!
<Emitir el final>
emitir_fin:
mov x0, #0
ldp x29, x30, [sp], #16
ret
La explicación viaja entretejida con el código
<Reconocer un dígito>
int es_digito(char c) {
return c >= '0' && c <= '9';
}
<Reconocer un número>
Token leer_numero(const char *s, int *i) {
int v = 0;
while (es_digito(s[*i]))
v = v*10 + (s[(*i)++] - '0');
return (Token){ TOK_NUM, v };
}
<El siguiente token>
Token siguiente(const char *s, int *i) {
while (s[*i] == ' ') (*i)++;
if (es_digito(s[*i]))
return leer_numero(s, i);
<Reconocer un operador>
if (s[*i] == '+') { (*i)++;
return (Token){ TOK_MAS, 0 };
}
return (Token){ TOK_FIN, 0 };
<Emitir una suma>
emitir_suma:
ldr x0, [sp], #16
ldr x1, [sp], #16
<Apilar el resultado>
add x0, x0, x1
str x0, [sp, #-16]!
ret
<Emitir un número>
emitir_num:
mov x0, #0
ldr x0, [x19], #8
str x0, [sp, #-16]!
<Emitir el final>
emitir_fin:
mov x0, #0
ldp x29, x30, [sp], #16
ret
La explicación viaja entretejida con el código
Reconocer un número
Acumula dígitos mientras los haya y devuelve el número.
<Reconocer un número>
Token leer_numero(const char *s, int *i) {
int v = 0;
while (es_digito(s[*i]))
v = v*10 + (s[(*i)++] - '0');
return (Token){ TOK_NUM, v };
}
Se apoya en el predicado del folio siguiente.
01
<Reconocer un dígito>
int es_digito(char c) {
return c >= '0' && c <= '9';
}
<El siguiente token>
Token siguiente(const char *s, int *i) {
while (s[*i] == ' ') (*i)++;
if (es_digito(s[*i]))
return leer_numero(s, i);
<Reconocer un operador>
if (s[*i] == '+') { (*i)++;
return (Token){ TOK_MAS, 0 };
}
return (Token){ TOK_FIN, 0 };
<Emitir una suma>
emitir_suma:
ldr x0, [sp], #16
ldr x1, [sp], #16
<Apilar el resultado>
add x0, x0, x1
str x0, [sp, #-16]!
ret
<Emitir un número>
emitir_num:
mov x0, #0
ldr x0, [x19], #8
str x0, [sp, #-16]!
<Emitir el final>
emitir_fin:
mov x0, #0
ldp x29, x30, [sp], #16
ret
La explicación viaja entretejida con el código
Reconocer un número
Acumula dígitos mientras los haya y devuelve el número.
<Reconocer un número>
Token leer_numero(const char *s, int *i) {
int v = 0;
while (es_digito(s[*i]))
v = v*10 + (s[(*i)++] - '0');
return (Token){ TOK_NUM, v };
}
Se apoya en el predicado del folio siguiente.
01
Reconocer un dígito
La pregunta más pequeña del compilador: ¿esto es un dígito?
<Reconocer un dígito>
int es_digito(char c) {
return c >= '0' && c <= '9';
}
Un predicado que todo lo demás reutiliza.
02
<El siguiente token>
Token siguiente(const char *s, int *i) {
while (s[*i] == ' ') (*i)++;
if (es_digito(s[*i]))
return leer_numero(s, i);
<Reconocer un operador>
if (s[*i] == '+') { (*i)++;
return (Token){ TOK_MAS, 0 };
}
return (Token){ TOK_FIN, 0 };
<Emitir una suma>
emitir_suma:
ldr x0, [sp], #16
ldr x1, [sp], #16
<Apilar el resultado>
add x0, x0, x1
str x0, [sp, #-16]!
ret
<Emitir un número>
emitir_num:
mov x0, #0
ldr x0, [x19], #8
str x0, [sp, #-16]!
<Emitir el final>
emitir_fin:
mov x0, #0
ldp x29, x30, [sp], #16
ret
La explicación viaja entretejida con el código
Reconocer un número
Acumula dígitos mientras los haya y devuelve el número.
<Reconocer un número>
Token leer_numero(const char *s, int *i) {
int v = 0;
while (es_digito(s[*i]))
v = v*10 + (s[(*i)++] - '0');
return (Token){ TOK_NUM, v };
}
Se apoya en el predicado del folio siguiente.
01
Reconocer un dígito
La pregunta más pequeña del compilador: ¿esto es un dígito?
<Reconocer un dígito>
int es_digito(char c) {
return c >= '0' && c <= '9';
}
Un predicado que todo lo demás reutiliza.
02
<El siguiente token>
Token siguiente(const char *s, int *i) {
while (s[*i] == ' ') (*i)++;
if (es_digito(s[*i]))
return leer_numero(s, i);
<Reconocer un operador>
if (s[*i] == '+') { (*i)++;
return (Token){ TOK_MAS, 0 };
}
return (Token){ TOK_FIN, 0 };
<Emitir una suma>
emitir_suma:
ldr x0, [sp], #16
ldr x1, [sp], #16
<Apilar el resultado>
add x0, x0, x1
str x0, [sp, #-16]!
ret
<Emitir un número>
emitir_num:
mov x0, #0
ldr x0, [x19], #8
str x0, [sp, #-16]!
<Emitir el final>
emitir_fin:
mov x0, #0
ldp x29, x30, [sp], #16
ret
La explicación viaja entretejida con el código
Reconocer un dígito
La pregunta más pequeña del compilador: ¿esto es un dígito?
<Reconocer un dígito>
int es_digito(char c) {
return c >= '0' && c <= '9';
}
Un predicado que todo lo demás reutiliza.
02
Emitir un número
Antes de seguir leyendo, veamos qué se hace con un número.
<Emitir un número>
emitir_num:
mov x0, #0
ldr x0, [x19], #8
str x0, [sp, #-16]!
Un literal no se calcula: se carga y se apila.
03
<El siguiente token>
Token siguiente(const char *s, int *i) {
while (s[*i] == ' ') (*i)++;
if (es_digito(s[*i]))
return leer_numero(s, i);
<Reconocer un operador>
if (s[*i] == '+') { (*i)++;
return (Token){ TOK_MAS, 0 };
}
return (Token){ TOK_FIN, 0 };
<Emitir una suma>
emitir_suma:
ldr x0, [sp], #16
ldr x1, [sp], #16
<Apilar el resultado>
add x0, x0, x1
str x0, [sp, #-16]!
ret
<Emitir el final>
emitir_fin:
mov x0, #0
ldp x29, x30, [sp], #16
ret
La explicación viaja entretejida con el código
Reconocer un dígito
La pregunta más pequeña del compilador: ¿esto es un dígito?
<Reconocer un dígito>
int es_digito(char c) {
return c >= '0' && c <= '9';
}
Un predicado que todo lo demás reutiliza.
02
Emitir un número
Antes de seguir leyendo, veamos qué se hace con un número.
<Emitir un número>
emitir_num:
mov x0, #0
ldr x0, [x19], #8
str x0, [sp, #-16]!
Un literal no se calcula: se carga y se apila.
03
<El siguiente token>
Token siguiente(const char *s, int *i) {
while (s[*i] == ' ') (*i)++;
if (es_digito(s[*i]))
return leer_numero(s, i);
<Reconocer un operador>
if (s[*i] == '+') { (*i)++;
return (Token){ TOK_MAS, 0 };
}
return (Token){ TOK_FIN, 0 };
<Emitir una suma>
emitir_suma:
ldr x0, [sp], #16
ldr x1, [sp], #16
<Apilar el resultado>
add x0, x0, x1
str x0, [sp, #-16]!
ret
<Emitir el final>
emitir_fin:
mov x0, #0
ldp x29, x30, [sp], #16
ret
La explicación viaja entretejida con el código
Emitir un número
Antes de seguir leyendo, veamos qué se hace con un número.
<Emitir un número>
emitir_num:
mov x0, #0
ldr x0, [x19], #8
str x0, [sp, #-16]!
Un literal no se calcula: se carga y se apila.
03
El siguiente token
Salta los espacios y decide qué clase de token viene.
<El siguiente token>
Token siguiente(const char *s, int *i) {
while (s[*i] == ' ') (*i)++;
if (es_digito(s[*i]))
return leer_numero(s, i);
Un solo punto de entrada para todo el flujo.
04
<Reconocer un operador>
if (s[*i] == '+') { (*i)++;
return (Token){ TOK_MAS, 0 };
}
return (Token){ TOK_FIN, 0 };
<Emitir una suma>
emitir_suma:
ldr x0, [sp], #16
ldr x1, [sp], #16
<Apilar el resultado>
add x0, x0, x1
str x0, [sp, #-16]!
ret
<Emitir el final>
emitir_fin:
mov x0, #0
ldp x29, x30, [sp], #16
ret
La explicación viaja entretejida con el código
Emitir un número
Antes de seguir leyendo, veamos qué se hace con un número.
<Emitir un número>
emitir_num:
mov x0, #0
ldr x0, [x19], #8
str x0, [sp, #-16]!
Un literal no se calcula: se carga y se apila.
03
El siguiente token
Salta los espacios y decide qué clase de token viene.
<El siguiente token>
Token siguiente(const char *s, int *i) {
while (s[*i] == ' ') (*i)++;
if (es_digito(s[*i]))
return leer_numero(s, i);
Un solo punto de entrada para todo el flujo.
04
<Reconocer un operador>
if (s[*i] == '+') { (*i)++;
return (Token){ TOK_MAS, 0 };
}
return (Token){ TOK_FIN, 0 };
<Emitir una suma>
emitir_suma:
ldr x0, [sp], #16
ldr x1, [sp], #16
<Apilar el resultado>
add x0, x0, x1
str x0, [sp, #-16]!
ret
<Emitir el final>
emitir_fin:
mov x0, #0
ldp x29, x30, [sp], #16
ret
La explicación viaja entretejida con el código
El siguiente token
Salta los espacios y decide qué clase de token viene.
<El siguiente token>
Token siguiente(const char *s, int *i) {
while (s[*i] == ' ') (*i)++;
if (es_digito(s[*i]))
return leer_numero(s, i);
Un solo punto de entrada para todo el flujo.
04
Emitir una suma
Con el operador ya reconocido, toca emitirlo.
<Emitir una suma>
emitir_suma:
ldr x0, [sp], #16
ldr x1, [sp], #16
Dos saques de la pila antes de sumar.
05
<Reconocer un operador>
if (s[*i] == '+') { (*i)++;
return (Token){ TOK_MAS, 0 };
}
return (Token){ TOK_FIN, 0 };
<Apilar el resultado>
add x0, x0, x1
str x0, [sp, #-16]!
ret
<Emitir el final>
emitir_fin:
mov x0, #0
ldp x29, x30, [sp], #16
ret
La explicación viaja entretejida con el código
El siguiente token
Salta los espacios y decide qué clase de token viene.
<El siguiente token>
Token siguiente(const char *s, int *i) {
while (s[*i] == ' ') (*i)++;
if (es_digito(s[*i]))
return leer_numero(s, i);
Un solo punto de entrada para todo el flujo.
04
Emitir una suma
Con el operador ya reconocido, toca emitirlo.
<Emitir una suma>
emitir_suma:
ldr x0, [sp], #16
ldr x1, [sp], #16
Dos saques de la pila antes de sumar.
05
<Reconocer un operador>
if (s[*i] == '+') { (*i)++;
return (Token){ TOK_MAS, 0 };
}
return (Token){ TOK_FIN, 0 };
<Apilar el resultado>
add x0, x0, x1
str x0, [sp, #-16]!
ret
<Emitir el final>
emitir_fin:
mov x0, #0
ldp x29, x30, [sp], #16
ret
La explicación viaja entretejida con el código
Emitir una suma
Con el operador ya reconocido, toca emitirlo.
<Emitir una suma>
emitir_suma:
ldr x0, [sp], #16
ldr x1, [sp], #16
Dos saques de la pila antes de sumar.
05
Apilar el resultado
El resultado vuelve a la pila para quien lo necesite arriba.
<Apilar el resultado>
add x0, x0, x1
str x0, [sp, #-16]!
ret
La pila es el contrato entre las dos mitades.
06
<Reconocer un operador>
if (s[*i] == '+') { (*i)++;
return (Token){ TOK_MAS, 0 };
}
return (Token){ TOK_FIN, 0 };
<Emitir el final>
emitir_fin:
mov x0, #0
ldp x29, x30, [sp], #16
ret
La explicación viaja entretejida con el código
Emitir una suma
Con el operador ya reconocido, toca emitirlo.
<Emitir una suma>
emitir_suma:
ldr x0, [sp], #16
ldr x1, [sp], #16
Dos saques de la pila antes de sumar.
05
Apilar el resultado
El resultado vuelve a la pila para quien lo necesite arriba.
<Apilar el resultado>
add x0, x0, x1
str x0, [sp, #-16]!
ret
La pila es el contrato entre las dos mitades.
06
<Reconocer un operador>
if (s[*i] == '+') { (*i)++;
return (Token){ TOK_MAS, 0 };
}
return (Token){ TOK_FIN, 0 };
<Emitir el final>
emitir_fin:
mov x0, #0
ldp x29, x30, [sp], #16
ret
La explicación viaja entretejida con el código
Apilar el resultado
El resultado vuelve a la pila para quien lo necesite arriba.
<Apilar el resultado>
add x0, x0, x1
str x0, [sp, #-16]!
ret
La pila es el contrato entre las dos mitades.
06
Reconocer un operador
Si no es un número, puede ser un operador — o el final.
<Reconocer un operador>
if (s[*i] == '+') { (*i)++;
return (Token){ TOK_MAS, 0 };
}
return (Token){ TOK_FIN, 0 };
Aquí termina el trabajo del analizador léxico.
07
<Emitir el final>
emitir_fin:
mov x0, #0
ldp x29, x30, [sp], #16
ret
La explicación viaja entretejida con el código
Apilar el resultado
El resultado vuelve a la pila para quien lo necesite arriba.
<Apilar el resultado>
add x0, x0, x1
str x0, [sp, #-16]!
ret
La pila es el contrato entre las dos mitades.
06
Reconocer un operador
Si no es un número, puede ser un operador — o el final.
<Reconocer un operador>
if (s[*i] == '+') { (*i)++;
return (Token){ TOK_MAS, 0 };
}
return (Token){ TOK_FIN, 0 };
Aquí termina el trabajo del analizador léxico.
07
<Emitir el final>
emitir_fin:
mov x0, #0
ldp x29, x30, [sp], #16
ret
La explicación viaja entretejida con el código
Reconocer un operador
Si no es un número, puede ser un operador — o el final.
<Reconocer un operador>
if (s[*i] == '+') { (*i)++;
return (Token){ TOK_MAS, 0 };
}
return (Token){ TOK_FIN, 0 };
Aquí termina el trabajo del analizador léxico.
07
Emitir el final
Al final se restaura el marco y se devuelve el control.
<Emitir el final>
emitir_fin:
mov x0, #0
ldp x29, x30, [sp], #16
ret
El libro cierra donde cierra el programa.
08
La explicación viaja entretejida con el código
Reconocer un operador
Si no es un número, puede ser un operador — o el final.
<Reconocer un operador>
if (s[*i] == '+') { (*i)++;
return (Token){ TOK_MAS, 0 };
}
return (Token){ TOK_FIN, 0 };
Aquí termina el trabajo del analizador léxico.
07
Emitir el final
Al final se restaura el marco y se devuelve el control.
<Emitir el final>
emitir_fin:
mov x0, #0
ldp x29, x30, [sp], #16
ret
El libro cierra donde cierra el programa.
08
Reconocer un número
El analizador léxico no entiende de números, sino de dígitos: los acumula uno a uno mientras el flujo se lo permita. Cada dígito nuevo desplaza al anterior un lugar hacia la izquierda —multiplicar por diez es exactamente eso— y se suma su valor. La invariante vale en cada vuelta: v contiene el número formado por los dígitos leídos hasta ahora.
v′ = 10 · v + d, 0 ≤ d ≤ 9
<Reconocer un número>
Token leer_numero(const char *s, int *i) {
int v = 0;
while (es_digito(s[*i]))
v = v*10 + (s[(*i)++] - '0');
return (Token){ TOK_NUM, v };
}
Al salir, el puntero queda sobre el primer carácter que ya no es un dígito: el lector no consume de más y el llamador sigue sin retroceder.
Aho, Lam, Sethi & Ullman (2006). Compilers: Principles, Techniques and Tools, 2.ª ed., §3.3.
01
Emitir una suma
Del otro lado del compilador el árbol se recorre en postorden, de modo que cuando un nodo se emite sus dos hijos ya dejaron su resultado en la pila. La suma no necesita saber nada de ellos: saca dos palabras, las suma y empuja una.
v′ = 10 · v + d, 0 ≤ d ≤ 9
<Reconocer un número>
Token leer_numero(const char *s, int *i) {
int v = 0;
while (es_digito(s[*i]))
v = v*10 + (s[(*i)++] - '0');
return (Token){ TOK_NUM, v };
}
Al salir, el puntero queda sobre el primer carácter que ya no es un dígito: el lector no consume de más y el llamador sigue sin retroceder.
Aho, Lam, Sethi & Ullman (2006). Compilers: Principles, Techniques and Tools, 2.ª ed., §3.3.
01
Emitir una suma
Del otro lado del compilador el árbol se recorre en postorden, de modo que cuando un nodo se emite sus dos hijos ya dejaron su resultado en la pila. La suma no necesita saber nada de ellos: saca dos palabras, las suma y empuja una.
Instrucción
Pila antes
Pila después
ldr x0, [sp], #16
a · b
a
add x0, x0, x1
—
—
str x0, [sp, #-16]!
—
a+b
<Emitir una suma>
emitir_suma:
ldr x0, [sp], #16
ldr x1, [sp], #16
add x0, x0, x1
str x0, [sp, #-16]!
ret
Al salir, el puntero queda sobre el primer carácter que ya no es un dígito: el lector no consume de más y el llamador sigue sin retroceder.
Aho, Lam, Sethi & Ullman (2006). Compilers: Principles, Techniques and Tools, 2.ª ed., §3.3.
01
Emitir una suma
Del otro lado del compilador el árbol se recorre en postorden, de modo que cuando un nodo se emite sus dos hijos ya dejaron su resultado en la pila. La suma no necesita saber nada de ellos: saca dos palabras, las suma y empuja una.
Instrucción
Pila antes
Pila después
ldr x0, [sp], #16
a · b
a
add x0, x0, x1
—
—
str x0, [sp, #-16]!
—
a+b
<Emitir una suma>
emitir_suma:
ldr x0, [sp], #16
ldr x1, [sp], #16
add x0, x0, x1
str x0, [sp, #-16]!
ret
Tres instrucciones por nodo, sin registros reservados ni asignación global: emitir cuesta tiempo lineal en el número de nodos.
Arm Limited (2024). Arm Architecture Reference Manual for A-profile, §C6.2.
02
Del otro lado del compilador el árbol se recorre en postorden, de modo que cuando un nodo se emite sus dos hijos ya dejaron su resultado en la pila. La suma no necesita saber nada de ellos: saca dos palabras, las suma y empuja una.
Instrucción
Pila antes
Pila después
ldr x0, [sp], #16
a · b
a
add x0, x0, x1
—
—
str x0, [sp, #-16]!
—
a+b
<Emitir una suma>
emitir_suma:
ldr x0, [sp], #16
ldr x1, [sp], #16
add x0, x0, x1
str x0, [sp, #-16]!
ret
Tres instrucciones por nodo, sin registros reservados ni asignación global: emitir cuesta tiempo lineal en el número de nodos.
Arm Limited (2024). Arm Architecture Reference Manual for A-profile, §C6.2.
02
El siguiente token
Pedir el siguiente token cuesta tiempo constante amortizado: cada carácter del flujo se mira una sola vez. De ahí que el análisis completo sea lineal en la longitud de la entrada.
T(n) = Θ(n)
<El siguiente token>
Token siguiente(const char *s, int *i) {
while (s[*i] == ' ') (*i)++;
if (es_digito(s[*i]))
return leer_numero(s, i);
<Emitir una suma>
emitir_suma:
ldr x0, [sp], #16
ldr x1, [sp], #16
add x0, x0, x1
str x0, [sp, #-16]!
ret
Tres instrucciones por nodo, sin registros reservados ni asignación global: emitir cuesta tiempo lineal en el número de nodos.
Arm Limited (2024). Arm Architecture Reference Manual for A-profile, §C6.2.
02
El siguiente token
Pedir el siguiente token cuesta tiempo constante amortizado: cada carácter del flujo se mira una sola vez. De ahí que el análisis completo sea lineal en la longitud de la entrada.
T(n) = Θ(n)
<El siguiente token>
Token siguiente(const char *s, int *i) {
while (s[*i] == ' ') (*i)++;
if (es_digito(s[*i]))
return leer_numero(s, i);
El bucle de espacios es el único retroceso posible, y no lee nada dos veces: avanza o devuelve.
Lexema
Token
Valor
42
TOK_NUM
42
+
TOK_MAS
—
fin de cadena
TOK_FIN
—
Arm Limited (2024). Arm Architecture Reference Manual for A-profile, §C6.2.
02
El siguiente token
Pedir el siguiente token cuesta tiempo constante amortizado: cada carácter del flujo se mira una sola vez. De ahí que el análisis completo sea lineal en la longitud de la entrada.
T(n) = Θ(n)
<El siguiente token>
Token siguiente(const char *s, int *i) {
while (s[*i] == ' ') (*i)++;
if (es_digito(s[*i]))
return leer_numero(s, i);
El bucle de espacios es el único retroceso posible, y no lee nada dos veces: avanza o devuelve.
Tiempo de análisis contra longitud de la entrada: una recta.
Lexema
Token
Valor
42
TOK_NUM
42
+
TOK_MAS
—
fin de cadena
TOK_FIN
—
Knuth, D. E. (1984). Literate Programming. The Computer Journal, 27(2), 97–111.
03
Y sigue
<El siguiente token>
Token siguiente(const char *s, int *i) {
while (s[*i] == ' ') (*i)++;
if (es_digito(s[*i]))
return leer_numero(s, i);
El bucle de espacios es el único retroceso posible, y no lee nada dos veces: avanza o devuelve.
Tiempo de análisis contra longitud de la entrada: una recta.
Lexema
Token
Valor
42
TOK_NUM
42
+
TOK_MAS
—
fin de cadena
TOK_FIN
—
Knuth, D. E. (1984). Literate Programming. The Computer Journal, 27(2), 97–111.
03
Y sigue
Cuarenta y una páginas más, todas iguales a éstas. El capítulo del enlazador tiene catorce; el del preprocesador, nueve.
<Los capítulos que faltan>
07 El preprocesador → 9 páginas
08 El enlazador → 14 páginas
09 El soporte x86-64 → 18 páginas
Ninguna de ellas es documentación. Todas son el programa.
Tiempo de análisis contra longitud de la entrada: una recta.
Knuth, D. E. (1984). Literate Programming. The Computer Journal, 27(2), 97–111.
03
Y sigue
Cuarenta y una páginas más, todas iguales a éstas. El capítulo del enlazador tiene catorce; el del preprocesador, nueve.
<Los capítulos que faltan>
07 El preprocesador → 9 páginas
08 El enlazador → 14 páginas
09 El soporte x86-64 → 18 páginas
Ninguna de ellas es documentación. Todas son el programa.
04
Fácil de entender. De compartir. De mejorar.
Y sigue
Cuarenta y una páginas más, todas iguales a éstas. El capítulo del enlazador tiene catorce; el del preprocesador, nueve.
<Los capítulos que faltan>
07 El preprocesador → 9 páginas
08 El enlazador → 14 páginas
09 El soporte x86-64 → 18 páginas
Ninguna de ellas es documentación. Todas son el programa.
04
Fácil de entender. De compartir. De mejorar.
PROSA
El qué
PROSA
El porqué
CÓDIGO
El cómo
¿Cómo se escribe un documento literario?
Dos lenguajes. Al mismo tiempo.
PARA EXPLICAR
Un lenguaje de formato
LaTeX
Markdown
HTML
PARA EJECUTAR
Uno o varios de programación simultáneos
C
Dart
JavaScript
Go
UN METALENGUAJE
weft
Un lenguaje sencillo por encima de los otros dos.
Enlaza prosa y código en una sola fuente de la verdad.
El metalenguaje para que humanos e IA escriban software juntos — como coautores.
UN SENCILLO COMANDO
Un origen, dos salidas
EL ORIGEN
saludo.weft
prosa + código · la fuente de la verdad
weft
tangle
por defecto
weave
con -w
EL PROGRAMA
saludo.c · util.c
código real, se compila como siempre
EL DOCUMENTO
.md · .tex · .html
un PDF que se lee y se comparte
El código es un subproducto. El documento literario es la fuente.
Un archivo weft
Las directivas de weft
@o salida.c @{ … @}
Archivo de salida: el tejido escribe ahí ese bloque
@d Concepto: algo @{ … @}
Fragmento con nombre, reutilizable
@<Concepto: algo@>
Invocación: expande aquí ese fragmento
@i otro.weft
Incluye otro archivo fuente
@l go
Fija el lenguaje del bloque que sigue
@| Nombre Otro @}
Nombres que el fragmento exporta al índice
@% comentario
Comentario dentro de un bloque: no se teje
@@
Una arroba literal: decoradores, correos…
Los comandos de weft
weft libro.weft
Teje el código: escribe los archivos de salida
weft -w tex libro.weft
Teje el libro: LaTeX, Markdown o HTML
weft -m --prose libro.weft
Mapa JSON: cada fragmento, su prosa y el grafo
weft -e NOMBRE --callers
Un fragmento, sus dependencias y quién lo usa
weft --lint libro.weft
Referencias inventadas y fragmentos huérfanos
weft --diff libro.weft
Qué cambiaría al re-tejer, sin escribir nada
make 2>&1 | weft --errors
Errores del compilador, traducidos a la línea .weft
weft -R salida.c:47
De una línea del código generado a su fuente
weft --help
Todo, cuando se pierde el contexto
Literate Driven
Development
¿Qué se requiere?
a)
Archivos literarios .weft
Las fuentes narrativas donde la prosa y el código viven como un solo documento.
b)
weft, la herramienta de línea de comandos
Teje la fuente en un documento para leer, y la enreda en el código para ejecutar.
c)
El skill literate-driven-development
Para que la IA escriba en el paradigma junto a ti.
Y una forma completamente distinta de programar.
Flujo de trabajo
1
2
3
4
5
6
1
2
3
4
5
6
1
2
3
4
5
6
Entendimiento
Complejidad
25
microservicios
API GraphQL
4
apps móviles
2
portales públicos
Un portal administrativo
Un portal contable
Un portal financiero
Y un sistema nuevo que se está integrando
Complejidad
25 microservicios
API GraphQL
4 apps móviles
2 portales públicos
Un portal administrativo
Un portal contable
Un portal financiero
Y un sistema nuevo que se está integrando
Complejidad
API GraphQL
25 microservicios
4 apps móviles
2 portales públicos
Un portal administrativo
Un portal contable
Un portal financiero
Y un sistema nuevo que se está integrando
Rapidez
Economía
Claude Code
100 USD
suscripción mensual
Obras de arte de la programación
Sistemas críticos
Auditables
Evolutivos
Resolver problemas complejos
Estabilidad
Nuevos lenguajes
La IA y el humano como autores