Implementación de llamadas a funciones en un parser con AST

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

Resumen

¿Cómo definir la precedencia de una llamada a función?

Para resolver funciones de manera efectiva en cualquier lenguaje de programación, es esencial establecer precedencias. La precedencia define el orden de evaluación de las expresiones. En este caso, el objetivo es asignar la precedencia de llamada a un token específico, que en este contexto es el paréntesis izquierdo. Esto se debe a que indica el comienzo de una llamada de función, lo cual posee la precedencia más alta entre los operadores.

Una vez establecida, esta precedencia garantiza que las funciones se evalúen correctamente antes que otros operadores, incluidos el de multiplicación y adición. Esta técnica esencial asegura que nuestra lógica de programación se implemente de manera adecuada.

¿Cómo implementar la función de análisis parse call y parse call arguments?

En la construcción de un parser, necesitamos implementar funciones que nos permitan analizar llamadas a funciones, generando nodos que respeten la sintaxis establecida. La función parse call es la encargada de recibir una expresión y devolver una llamada correcta. La estructura básica de esta función es generar un nodo de manera que mantenga el token actual y pueda gestionar los argumentos con una función auxiliar llamada parse call arguments.

parse call

Esta función se encarga de:

  1. Verificar que el token actual no sea none.
  2. Crear un nuevo nodo que reciba el current token y la expresión como parámetros.
  3. Inicializar una lista de argumentos a través de parse call arguments.
def parse_call(self, function: Expression) -> Call:
    token = self.current_token  # Almacena el current token
    self.next_token()
    arguments = self.parse_call_arguments()
    return Call(token, function, arguments)

parse call arguments

La función parse call arguments se encarga de identificar y organizar cada argumento dentro de una lista. La implementación sigue una lógica secuencial:

  1. Inicializar una lista vacía para los argumentos.
  2. Verificar si el peeking token es un paréntesis derecho, en cuyo caso retorna una lista vacía.
  3. Usar el operador walrus para procesar cada expresión de argumento, añadiéndolo a la lista cuando no es none.
  4. Manejar múltiples argumentos separados por comas.
  5. Verificar la existencia de un paréntesis derecho para detectar cualquier error de sintaxis.
def parse_call_arguments(self) -> List[Expression]:
    arguments = []
    if self.peek_token_is(TokenType.RPAREN):
        self.next_token()
        return arguments

    self.next_token()
    while expression := self.parse_expression(Precedences.LOWEST):
        arguments.append(expression)
        if not self.peek_token_is(TokenType.COMMA):
            break
        self.next_token()  # Avanza para llegar a la coma
        self.next_token()  # Avanza hacia el nuevo argumento

    if not self.expect_peek(TokenType.RPAREN):
        return []

    return arguments

¿Cómo registrar la función de análisis en el parser?

Finalmente, para integrar la funcionalidad de análisis de llamadas a funciones, necesitamos asociar la función parse call con los operadores que manejan el paréntesis izquierdo. Esto se puede lograr a través del método register infix functions:

self.register_infix_function(TokenType.LPAREN, self.parse_call)

Con esto, cada vez que el parser encuentra un paréntesis izquierdo, sabe que es necesario analizar una llamada a función. La implementación de los tests asegura que cada parte del código cumpla con las expectativas y la sintaxis previamente definida.

Recordar: si tienes dudas o comentarios, la comunidad está lista para ayudarte. Siempre están disponibles espacios como los comentarios para generar discusiones ricas y constructivas sobre estos temas. ¡Continúa tu aprendizaje y explora más este fascinante mundo de los parsers en programación!