Parseo de Expression Statements en Platzi Parser

Clase 23 de 58Curso de Creación de Lenguajes de Programación: Intérpretes

Contenido del curso

Construcción del lexer o tokenizador

Construcción del parser o analizador sintáctico

Evaluación o análisis semántico

Resumen

Construir un parser capaz de reconocer expresiones es uno de los pasos más importantes al desarrollar un intérprete. Aquí se aborda cómo enseñarle al parser a reconocer expression statements e identificadores, conectando tokens con funciones específicas mediante la técnica conocida como Pratt Parser. Si ya sabes parsear let statements y return statements, este es el siguiente paso natural para que tu intérprete cobre vida.

¿Qué es un expression statement y cómo se parsea?

Dentro del lenguaje de programación que se está construyendo, toda instrucción es una de tres cosas: un let statement (declaración de variable), un return statement (retorno de valor) o un expression statement [01:00]. No hay otra opción. Cuando el parser encuentra algo que no es let ni return, automáticamente intenta parsearlo como un expression statement.

Para implementarlo, se modifica la función parse_statement. Donde antes se regresaba None cuando no se encontraba ni let ni return, ahora se llama a una nueva función: parse_expression_statement [01:12].

La función parse_expression_statement sigue estos pasos:

  • Verifica que el current_token no sea None (programación defensiva).
  • Crea una nueva instancia de ExpressionStatement pasándole el token actual.
  • Llama a parse_expression con la precedencia más baja.
  • Si el siguiente token es un punto y coma (semicolon), avanza al siguiente token con advance_tokens.
  • Regresa el expression statement construido.

La verificación de que current_token no sea None es fundamental. Si se omite, herramientas como mypy señalarán un error porque el parámetro token no acepta valores opcionales [02:28]. Esto es programación defensiva: trabajar con tipos garantiza que el programa sea correcto y falle de forma controlada si algo inesperado ocurre.

¿Cómo funcionan las precedencias en un intérprete?

La precedencia determina el orden en que se evalúan las operaciones. Se implementa como un IntEnum importado del módulo enum de Python [03:42]. A diferencia de un Enum regular donde se usa auto(), aquí el valor numérico importa porque define qué operación se ejecuta primero.

Las precedencias, de menor a mayor, son:

  • Lowest (1): la precedencia más baja.
  • Equals: comparaciones de igualdad.
  • Less greater: comparaciones de menor o mayor.
  • Sum: operaciones de suma.
  • Product: multiplicación y división.
  • Prefix: operadores como el signo menos o not.
  • Call: llamadas a función, que tienen la precedencia más alta [04:40].

Este orden sigue la lógica matemática que puedes recordar con el acrónimo PEMDAS [03:20]. Por ejemplo, en 5 * 5 + 5, primero se ejecuta la multiplicación porque product tiene mayor precedencia que sum.

¿Cómo se registra y parsea un identificador en el Pratt Parser?

La función parse_expression recibe una precedencia y regresa opcionalmente una expresión [05:18]. Su lógica central consiste en buscar dentro del diccionario prefix_parse_functions una función asociada al token_type del token actual. Si no encuentra una función registrada, captura un KeyError y regresa None.

Para que el parser reconozca identificadores, se registra la función parse_identifier dentro de register_prefix_functions [07:05]:

python self._prefix_parse_functions[TokenType.IDENTIFIER] = self.parse_identifier

Nótese que no se hace la llamada a la función (sin paréntesis); solo se pasa una referencia. La función parse_identifier es extremadamente sencilla [07:22]:

python def parse_identifier(self) -> Identifier: assert self._current_token is not None return Identifier( token=self._current_token, value=self._current_token.literal )

Simplemente toma el token actual y usa su literal como valor del identificador.

¿Por qué refactorizar parse let statement?

Una vez que existe parse_identifier, tiene sentido usarla también dentro de parse_let_statement [08:20]. Antes, esa función construía el identificador manualmente. Ahora se reemplaza por una llamada a parse_identifier, logrando consistencia y un solo punto de fallo. Los tests confirman que el refactoring no rompió nada.

¿Qué hace especial al Pratt Parser?

El Pratt Parser se distingue porque liga tokens a funciones [09:00]. Cada tipo de token tiene asociada una función que sabe cómo parsearlo. Toda la infraestructura ya está lista: para parsear enteros, booleanos u otras expresiones en el futuro, solo hay que definir la función correspondiente y registrarla en el diccionario. Este patrón hace que el parser sea extensible y elegante.

El enfoque de test driven development también demuestra su valor aquí: al refactorizar, los tests existentes verifican de inmediato que todo sigue funcionando correctamente [09:15]. Si parse_identifier fallara en algún contexto, la suite de tests lo detectaría al instante.

Si estás construyendo tu propio intérprete, recuerda leer el código de arriba hacia abajo, de izquierda a derecha, siguiendo cómo cada función llama a la siguiente. Comparte tus avances y experiencias con la comunidad.