¿Cómo programar un programa para programar programas?

El aula: años explicando lo mismo, dos veces, en dos idiomas de código distintos

Durante varios años como docente universitario tuve la oportunidad de impartir, entre otras, dos asignaturas que terminarían siendo el origen de este proyecto: Fundamentos de Programación en C# y Fundamentos de Programación en Python.

Ambas asignaturas, aunque en lenguajes distintos, seguían un camino casi idéntico. Empezábamos por lo más básico: qué es un algoritmo, qué es una variable, qué significa "programar". De ahí avanzábamos a estructuras condicionales, estructuras cíclicas, tipos de datos, y más adelante a estructuras de datos más exigentes —listas, pilas, colas, árboles binarios— hasta llegar finalmente al punto donde todo ese conocimiento teórico tenía que aterrizar en código real, escrito en C# o en Python según correspondiera.

Ese último paso —pasar de la idea al código— es, en mi experiencia, donde más estudiantes se quedan atascados. Y no siempre por la razón que uno esperaría.

La barrera silenciosa: no es solo lógica, también es idioma

Mis grupos eran heterogéneos, como probablemente lo son la mayoría de los grupos de una universidad: estudiantes más jóvenes junto a otros con más trayectoria, algunos ya con experiencia profesional previa en otras áreas, y —el punto que más me marcó— niveles de inglés muy distintos entre ellos.

Aquí está la parte que a veces se nos olvida a quienes llevamos años programando: 

ifwhileforfunctionreturntruefalsenull...

para nosotros son palabras que dejaron de "significar" hace mucho tiempo, se volvieron símbolos puros de una estructura lógica. Pero para un estudiante que apenas está aprendiendo a pensar de forma algorítmica y al mismo tiempo tiene que decodificar en qué idioma está escrita la instrucción, son dos cargas cognitivas apilándose una sobre la otra.

No es que mis estudiantes no pudieran razonar la lógica de un algoritmo. Muchas veces la razonaban perfectamente en español, en el pizarrón, en voz alta. El tropiezo aparecía al momento de traducir ese razonamiento a un código cuyas palabras clave estaban en un idioma que todavía no dominaban del todo. Y ese tropiezo, aunque parezca menor, es suficiente para que un estudiante empiece a dudar de si "sirve" para programar, cuando en realidad lo único que le faltaba era una palabra en inglés que no conocía.

Me hice la pregunta muchas veces, de distintas formas, después de clase: ¿habrá una manera de quitarle esa capa extra de dificultad a quienes recién empiezan?

La pregunta que no me dejó en paz

Un día la pregunta se volvió más concreta: ¿sería posible crear un lenguaje de programación simple, cuyas palabras reservadas y sentencias estuvieran en español? No para reemplazar a C# o Python —esos siguen siendo, y seguirán siendo, el destino final—, sino como un peldaño intermedio: un lenguaje pensado exclusivamente para enseñar los fundamentos, sin que el idioma fuera un obstáculo antes incluso de llegar a la lógica.

Y de ahí salió una segunda pregunta, mucho más ingenua y mucho más divertida en retrospectiva: ¿cómo se hace, exactamente, un lenguaje de programación? ¿Cómo se programa un programa... para programar programas?

Esa pregunta, dicha así, suena casi a trabalenguas. Pero es literalmente el punto de partida de este proyecto.

Aprendiendo cómo se construye un intérprete

Me puse a investigar en serio cómo funcionan los lenguajes de programación por dentro, y descubrí que, más allá de las diferencias superficiales entre unos y otros, casi todos comparten una estructura básica bastante consistente. Un intérprete necesita, como mínimo:

  1. Leer el código fuente — el archivo de texto plano que escribió la persona que programa.
  2. Analizarlo léxicamente — dividir ese texto plano en piezas con significado propio (tokens): palabras reservadas, identificadores, números, cadenas de texto, símbolos.
  3. Analizarlo sintácticamente — tomar esa secuencia de tokens y darle una estructura jerárquica que refleje la gramática del lenguaje (un árbol de sintaxis abstracta, o AST).
  4. Interpretarlo y ejecutarlo — recorrer esa estructura y, por cada instrucción, producir el efecto que se supone que debe producir: sumar, imprimir, decidir, repetir.

Con esa idea en la cabeza —lectura, análisis léxico, análisis sintáctico, interpretación— empecé a escribir lo que hoy es Básico Lang, un intérprete tree-walking (es decir, que ejecuta el árbol de sintaxis directamente, sin compilarlo antes a un bytecode intermedio) escrito en C# sobre .NET 8.

Cómo está construido Básico Lang, por dentro

Voy a intentar explicar la arquitectura del proyecto de la misma forma en que me hubiera gustado que alguien me la explicara a mí cuando empecé a investigar todo esto.

El pipeline: cuatro etapas, una detrás de otra

Todo el proceso, desde que se ejecuta dotnet run hasta que el programa .basico termina de correr, pasa por cuatro etapas encadenadas:

Código fuente → Carga → Análisis léxico → Parsing → Interpretación

Cada etapa recibe lo que produjo la anterior y no necesita saber nada de las demás:

  • Carga del código fuente. Lo primero es simplemente leer el archivo .basico completo y traerlo a memoria como un texto plano. Nada sofisticado todavía: solo hay que asegurarse de que el archivo exista y no esté vacío.
  • Análisis léxico (el lexer). Aquí es donde el texto plano deja de ser "un montón de caracteres" y se convierte en una secuencia de tokens: unidades con significado propio. La palabra variable se reconoce como una palabra reservada distinta de un identificador cualquiera; "Hola" se reconoce como una cadena de texto; 42 se reconoce como un número; si se reconoce como el inicio de un condicional. El lexer de Básico Lang recorre el código carácter por carácter y, en vez de generar todos los tokens de una sola vez, los va entregando uno a uno, bajo demanda, según se los va pidiendo la siguiente etapa.
  • Parsing (el parser). Con la secuencia de tokens en mano, el parser construye un árbol de sintaxis abstracta: una representación jerárquica de qué significa realmente el código. Por ejemplo, la línea variable resultado = 1 + 2 no se queda como una fila de tokens sueltos — se convierte en un nodo "declaración de variable" que contiene, como su valor, un nodo "suma" con dos nodos "número" como operandos. Básico Lang usa para esto un parser de precedencia (también llamado Pratt parser), una técnica clásica para resolver correctamente cosas como que la multiplicación se evalúe antes que la suma, sin necesidad de una gramática declarativa aparte: la prioridad de cada operador está codificada directamente en la lógica del parser.
  • Interpretación. Por último, el intérprete recorre ese árbol de arriba hacia abajo, y por cada nodo ejecuta el efecto correspondiente: si es una declaración de variable, la guarda en un entorno de variables; si es una suma, evalúa ambos lados y produce el resultado; si es un si/sino, evalúa la condición y decide qué rama seguir; si es una llamada a función, la ejecuta con sus propios parámetros. A esto se le llama un intérprete tree-walking porque literalmente "camina" el árbol en tiempo de ejecución, en contraste con otros lenguajes que primero compilan ese árbol a una representación intermedia (bytecode) antes de ejecutarlo.

Palabras reservadas en español, por diseño

El objetivo original —bajar la barrera del idioma— se refleja directamente en las palabras reservadas del lenguaje: variable para declarar variables, si/sino para condicionales, funcion para declarar funciones, devolver para retornar un valor, verdadero/falso para booleanos, nulo para la ausencia de valor, repetir y repetir_mientras para los bucles. Un ejemplo sencillo se ve así:

funcion saludar(nombre) { imprimirLinea("Hola, " + nombre + "!") } saludar("Ana")

Incluso los métodos nativos sobre cadenas, números y listas están en español: .contiene(...).a_mayusculas().es_entero().transformar(...), entre otros — la idea es que, de principio a fin, un estudiante que apenas está aprendiendo pueda leer el código en voz alta y que suene a lo que está pasando, sin tener que traducir mentalmente cada palabra clave antes de entender la instrucción.

Cómo está organizado el código

El proyecto está escrito en C#/.NET 8 y organizado siguiendo exactamente las cuatro etapas del pipeline que describí arriba, cada una en su propia carpeta y su propio namespace: Lexing/ para el análisis léxico, Parsing/ para el parser, Ast/ para las estructuras de datos que representan el árbol de sintaxis (separadas en sentencias y expresiones), y Runtime/ para el intérprete, el modelo de valores en tiempo de ejecución y la biblioteca estándar nativa (funciones como imprimir y los métodos de cadenas/números/listas). Esa organización busca que el propio código fuente del intérprete sirva también como material de estudio: cualquiera que quiera entender cómo se construye un lenguaje de programación puede abrir la carpeta correspondiente a la etapa que le interese y encontrar ahí, y solo ahí, el código relacionado con esa etapa.

Un proyecto vivo, no un producto terminado

Quiero ser honesto sobre algo: Básico Lang sigue en desarrollo activo. No es un lenguaje terminado ni production-ready — es, ante todo, un proyecto de aprendizaje, tanto para mí como (espero) para quien lo use más adelante en el aula. Hay piezas que todavía no están completas, como los bucles repetir/repetir_mientras, y hay comportamientos que todavía tengo que pulir. Documentar esos huecos abiertamente, en vez de esconderlos, es parte de la filosofía del proyecto: si va a servir para enseñar cómo se construye un lenguaje, tiene más valor mostrar el proceso completo —incluyendo lo que todavía no funciona— que solo el resultado final pulido.

Por qué esto importa más allá de mi salón de clases

Lo que empezó como una pregunta muy puntual —¿cómo ayudo a mis estudiantes a que el idioma no sea un obstáculo extra?— terminó convirtiéndose en un ejercicio mucho más profundo: entender, desde adentro, cómo funciona algo que uso todos los días como programador y que, hasta ese momento, daba por sentado. Construir Básico Lang me obligó a estudiar lexers, parsers, árboles de sintaxis y modelos de ejecución con un nivel de detalle que jamás había necesitado como simple usuario de C# o Python.


Básico Lang sigue creciendo. Si quieres ver el código, probarlo o seguir su evolución, te invito a explorar el repositorio del proyecto.

https://github.com/rafael-ceballos-xibalbalabs/Basico-Lang