Bitácora de Código

Creación de un lenguaje: el evaluador

25 OCT 2026

Lenguajes y compiladores
codexivolenguaje de programaciónintérpretes

Cómo funciona el evaluador de un intérprete que recorre el árbol: entornos, procedimientos que recuerdan dónde nacieron, cómo sale un regresa y errores en español.

El parser ya entrega un árbol que dice quién va con quién. Falta la parte más satisfactoria: que ese árbol haga algo. De eso se encarga el evaluador.

Parte 4: el parser

Creación de un lenguaje: el parser

Qué hace un parser, cómo decide un parser Pratt quién se queda con cada valor y cómo construí el de Codexivo: precedencias, llamadas y el pero_si.

Recorrer el árbol

El evaluador recorre el árbol y, en cada nodo, hace lo que ese nodo significa. Un número se evalúa a sí mismo. Una suma evalúa su lado izquierdo, luego el derecho, y suma los resultados. Un si evalúa su condición y, según lo que salga, evalúa una rama o la otra.

Como un nodo contiene otros nodos, el evaluador se llama a sí mismo. Es una cadena de casos, uno por cada tipo de nodo (simplificada):

ts
export const evaluate = (node, env) => {
  if (node instanceof ast.Program) {
    return evaluateProgram(node, env);
  } else if (node instanceof ast.Number) {
    return new NumberObj(node.value);
  } else if (node instanceof ast.Infix) {
    const left = evaluate(node.left, env);
    const right = evaluate(node.right, env);
    return evaluateInfixExpression(node.operator, left, right);
  }
  // … un caso por cada tipo de nodo …
  else {
    return NULL;
  }
};

Es la forma más directa de escribir un intérprete, y funciona. Lo primero que evaluó fueron números y booleanos; después los operadores, el si, los procedimientos. Hoy ya calcula fib(10) con recursión, y da 55.

Un solo tipo de número

Hasta aquí el lexer solo leía enteros (INT). Al agregar decimales tenía que decidir: dos tipos de número, como Python o Pascal, o uno solo, como JavaScript.

Elegí uno solo: el token pasó a llamarse NUM. En muchos cursos el primer lenguaje es C o Java, donde 7 / 2 da 3 porque los dos son enteros: una regla con una buena razón que todavía no se entiende cuando uno empieza. En Codexivo, 7 / 2 da 3.5. Quien está aprendiendo no debería pelear con el lenguaje antes de llegar a la lógica, ni preguntarse por qué 3 y 3.0 son cosas distintas. Si algún día hace falta la división entera, puede ser una operación con nombre propio.

Dónde viven las variables

Una variable no vive en el árbol. Vive en un entorno: un diccionario de nombres a valores que, si no encuentra un nombre, le pregunta al entorno de afuera.

ts
get(name: string): Object | undefined {
  const obj = this.store.get(name);
  if (obj) {
    return obj;
  }
  if (this.outer) {
    return this.outer.get(name);
  }
  return undefined;
}

Cada vez que se llama a un procedimiento, el evaluador crea un entorno nuevo con sus parámetros. La pregunta importante es a qué entorno apunta ese nuevo entorno hacia afuera. La respuesta de Codexivo: al del lugar donde se creó el procedimiento, no al de donde se llama. Por eso esto funciona:

codexivo
variable contador = procedimiento(inicio) {
  regresa procedimiento(paso) {
    regresa inicio + paso;
  };
};
variable desde_diez = contador(10);
desde_diez(5);

Da 15. Cuando se llama a desde_diez, contador ya terminó, pero el procedimiento de adentro todavía recuerda su entorno, y en ese entorno inicio vale 10. Para esto no hubo que escribir nada especial: salió solo de guardar, junto a cada procedimiento, el entorno donde nació.

Cómo sale un regresa

El regresa fue más difícil de lo que parecía. En este procedimiento:

codexivo
variable a = procedimiento(n) {
  si (n > 0) {
    si (n > 1) { regresa 10; }
    regresa 1;
  }
};
a(5);

el regresa 10 está dos bloques adentro. Cuando el evaluador lo encuentra, no basta con terminar el bloque del si de adentro: tiene que salir del si de afuera y del procedimiento completo.

La solución es envolver el valor. regresa 10 no produce un 10, sino un objeto Return que contiene el 10. Cada bloque, al ver un Return, deja de ejecutar sus siguientes instrucciones y lo pasa hacia arriba sin abrirlo. Solo la llamada al procedimiento lo desenvuelve. a(5) da 10, y el regresa 1 nunca se ejecuta.

Errores en español

Los errores siguen la misma idea: son un valor más. Si una suma recibe un número y un booleano, no lanza una excepción de JavaScript; regresa un objeto de error con su mensaje, en español, con línea y columna:

text
ERROR: tipo de operando desconocido: NUMBER + BOOLEAN en la linea 1 columna 3

Los nombres de los tipos todavía están en inglés y el mensaje no dice qué hacer, pero ya dice dónde.

Con eso entraron también las cadenas y el primer procedimiento de la biblioteca, longitud. longitud("año") da 3: la ñ es una letra, no dos bytes.

Lo que se queda

El evaluador es donde el lenguaje significa algo. El lexer decide qué es una palabra, el parser decide qué va con qué, pero aquí se decide qué pasa: que 7 / 2 sea 3.5, que un procedimiento recuerde dónde nació, que un regresa salga de todo.

Cada una de esas decisiones es pequeña, y todas van hacia lo mismo: que quien aprende pelee con la lógica de su problema y no con el lenguaje. Un número que se comporta como en la escuela, un regresa que sale de donde uno espera, un error que dice dónde está.

Pruébalo

Este es el intérprete de Codexivo con el procedimiento que recuerda dónde nació. La vista de memoria muestra los entornos: avanza paso a paso y fíjate en qué entorno sigue vivo cuando contador ya terminó.

La memoria, en vivo

El entorno que no se borra.

Abrir en el sandbox
variable contador = procedimiento(inicio) {
  regresa procedimiento(paso) {
    regresa inicio + paso
  }
}
variable desde_diez = contador(10)
imprimir(desde_diez(5))

Incrustado desde codexivo.urielcuriel.dev. Avanza paso a paso para ver cada entorno y sus variables.

En la siguiente entrega: los arreglos, que ya empecé.