¿Cómo se recupera exactamente un compilador de un error de tipo?

10

He leído varios artículos, artículos y la sección 4.1.4, capítulo 4 de Compiladores: Principios, Técnicas y Herramientas (2ª Edición) (también conocido como "El Libro del Dragón") que tratan sobre el tema de la recuperación de errores del compilador sintáctico. Sin embargo, después de experimentar con varios compiladores modernos, he visto que también se recuperan de errores semánticos , así como de errores sintácticos.

Entiendo bastante bien los algoritmos y técnicas detrás de los compiladores que se recuperan de errores relacionados sintácticamente, sin embargo, no entiendo exactamente cómo un compilador puede recuperarse de un error semántico.

Actualmente estoy usando una ligera variación del patrón de visitante para generar código a partir de mi árbol de sintaxis abstracta. Considere mi compilador compilando las siguientes expresiones:

1 / (2 * (3 + "4"))

El compilador generaría el siguiente árbol de sintaxis abstracta:

      op(/)
        |
     -------
    /       \ 
 int(1)    op(*)
             |
          -------
         /       \
       int(2)   op(+)
                  |
               -------
              /       \
           int(3)   str(4)

La fase de generación de código luego usaría el patrón de visitante para recorrer recursivamente el árbol de sintaxis abstracta y realizar la verificación de tipo. El árbol de sintaxis abstracta se atravesaría hasta que el compilador llegara a la parte más interna de la expresión; (3 + "4"). Luego, el compilador verifica cada lado de las expresiones y ve que no son semánticamente equivalentes. El compilador genera un error de tipo. Aquí es donde radica el problema. ¿Qué debería hacer ahora el compilador ?

Para que el compilador se recupere de este error y continúe verificando el tipo de las partes externas de las expresiones, tendría que devolver algún tipo ( into str) de la evaluación de la parte más interna de la expresión, a la siguiente parte más interna de la expresión. Pero simplemente no tiene un tipo para devolver . Como se produjo un error de tipo, no se dedujo ningún tipo.

Una posible solución que he postulado es que si se produce un error de tipo, se debe generar un error, y un valor especial que significa que se produjo un error de tipo, debe devolverse a las llamadas transversales de árbol de sintaxis abstracta anteriores. Si las llamadas transversales anteriores encuentran este valor, saben que se produjo un error de tipo más profundo en el árbol de sintaxis abstracta y deben evitar intentar deducir un tipo. Si bien este método parece funcionar, parece ser muy ineficiente. Si la parte más interna de una expresión está profunda en el árbol de sintaxis abstracta, entonces el compilador tendrá que hacer muchas llamadas recursivas solo para darse cuenta de que no se puede hacer un trabajo real, y simplemente regresar de cada una.

Se utiliza el método que describí anteriormente (lo dudo). Si es así, ¿no es eficiente? Si no, ¿cuáles son exactamente los métodos utilizados cuando los compiladores se recuperan de los errores semánticos?

Christian Dean
fuente
3
Estoy bastante seguro de que eso es lo que se usa, y ¿por qué no crees que es lo suficientemente eficiente? Para hacer la verificación de tipo, el compilador tiene que recorrer todo el árbol de todos modos . Una falla semántica es más eficiente ya que le permite al compilador eliminar una rama una vez que se encuentra el error.
Telastyn el

Respuestas:

8

Su idea propuesta es esencialmente correcta.

La clave es que el tipo de nodo AST se calcula solo una vez y luego se almacena. Cada vez que se necesita el tipo nuevamente, simplemente recupera el tipo almacenado. Si la resolución termina en un error, se almacena un tipo de error en su lugar.

Winston Ewert
fuente
3

Un enfoque interesante es tener un tipo especial para errores. Cuando se encuentra un error de este tipo, se registra un diagnóstico y el tipo de error se devuelve como el tipo de la expresión. Este tipo de error tiene algunas propiedades interesantes:

  • Cualquier operación que se realice en él tiene éxito (para evitar una cascada de mensajes de error causados ​​por la misma falla original)
  • El resultado de cualquier operación realizada en un objeto con tipo de error también tiene un tipo de error
  • Si un tipo de error llega a la generación de código, el generador de código detecta el uso y genera código que falla (por ejemplo, arroja una excepción, aborta o lo que sea apropiado para su idioma)

Con esta combinación, puede compilar correctamente el código que contiene errores de tipo, y mientras ese código no se use realmente, no se producirá ningún error de tiempo de ejecución. Esto puede ser útil, por ejemplo, para permitirle ejecutar pruebas unitarias para partes del código que no se vean afectadas.

Jules
fuente
Gracias por la respuesta Jules. Curiosamente, este es el método exacto que terminé usando. Grandes mentes piensan igual, ¿eh? ;-)
Christian Dean
2

Si hay un error semántico, se emite un mensaje de error de compilación que indica que se ha enviado al usuario.

Una vez hecho esto, está bien abortar la compilación ya que el programa de entrada está en error; no es un programa legal en el lenguaje, por lo que simplemente puede ser rechazado.

Sin embargo, eso es bastante duro, por lo que hay alternativas más suaves. Anule cualquier generación de código y generación de archivos de salida, sin embargo, continúe buscando algo para buscar más errores.

Por ejemplo, simplemente puede abortar cualquier análisis de tipo adicional para el árbol de expresión actual y continuar procesando expresiones de declaraciones posteriores.

Erik Eidt
fuente
2

Supongamos que su idioma permite agregar enteros y permite la concatenación de cadenas con el +operador.

Como int + stringno está permitido, la evaluación del +resultado dará como resultado un error. El compilador podría simplemente regresar errorcomo el tipo. O podría ser más inteligente, ya que int + int -> inty string + string -> stringestán permitidos, podría devolver "error, podría ser int o string".

Luego viene el *operador, y asumiremos que solo int + intestá permitido. Luego, el compilador puede decidir que +se suponía que debía regresar int, y el tipo devuelto para *entonces sería int, sin ningún mensaje de error.

gnasher729
fuente
Creo que te sigo, @gnasher, pero ¿qué quieres decir exactamente con el "" operador ? ¿Era ese error tipográfico?
Christian Dean el
@ChristianDean hay un asterisco en las comillas que se interpreta como marcado Markdown en lugar de representarse.
JakeRobb
He enviado una edición a la respuesta que resolverá el problema tan pronto como mi edición sea revisada por pares.
JakeRobb