Qué hace el Analizador Semántico
Misión
El análisis semántico es la última de las fases de análisis (fases encargadas de detectar errores). Esto quiere decir que, en esta fase, se deben ya detectar todos los errores que queden en el programa de entrada. Las fases que vienen tras el semántico supondrán que la entrada es completamente válida y se centrarán así en su trabajo.
Sin embargo, cuando se dice que tiene que detectar todos los errores que quedan, no se está siendo del todo exacto, ya que no todo error se puede detectar en un analizador semántico. Por tanto, la misión real del analizador semántico es detectar todo error... que se pueda detectar de manera estática (lo cual se verá a continuación).
Tipos de Errores
Hay dos tipos de errores:
- Los que se pueden detectar sólo cuando el programa se está ejecutando.
- Los que se pueden detectar, también, antes de ejecutarse el programa (analizando su código).
Dicho de otra manera, todos pueden detectarse en tiempo de ejecución y algunos de ellos también antes de la ejecución.
Un ejemplo clásico de error cuya detección sólo puede hacerse durante la ejecución sería el acceso a un elemento de un array.
int v[10];
void f(int i) {
print v[i];
}
En el ejemplo anterior no se puede saber si el acceso será válido o no sin ejecutar el programa, ya que se necesita saber el valor de i. Si no es un valor entre 0 y 9, se estará ante un error.
Otros ejemplos de comprobaciones que sólo se pueden hacer durante la ejecución serían el acceso a un objeto a través de una referencia null (el familiar NullPointerException) o la división por cero.
En definitiva, no se pueden detectar antes de su ejecución todos aquellos errores cuyas comprobaciones requieran de información que no se obtenga hasta que se ejecute el programa. En el caso del array, esta información sería el valor del índice; en el caso de la división por cero, sería el valor del divisor.
Tipos de Comprobaciones
El código que detecta un determinado error, es decir, el código que realiza la comprobación de su existencia, puede ejecutarse en dos momentos.
- Si se realiza antes de la ejecución del programa (por ejemplo, al compilar), se denomina comprobación estática.
- Si se realiza durante la ejecución del programa, se denomina comprobación dinámica.
El código de las comprobaciones dinámicas, como estas tienen que realizarse durante la ejecución, estará entre el código del programa a generar, es decir, será código adicional que genere el compilador. Por ejemplo, en el siguiente fragmento se muestra cómo el compilador añade el código de la comprobación dinámica para detectar los accesos inválidos a un array. Esto tendría que hacerlo en cada acceso a un array.
Código del programador | Código que se genera |
---|---|
|
|
El código de las comprobaciones estáticas, en cambio, estará dentro del compilador (concretamente, en el analizador semántico) y serán éstas comprobaciones las que se realicen sobre el árbol. Sólo este tipo de comprobaciones serán las que se traten en estos apuntes.
Comprobaciones y Lenguajes
La relación entre los tipos de errores posibles y el momento de su detección es la siguiente:
- Para detectar los errores que sólo se pueden detectar en ejecución sólo se pueden usar, evidentemente, comprobaciones dinámicas.
- Para detectar el resto de los errores, se pueden usar cualquiera de los dos tipos de comprobaciones. La decisión que se tome aquí es un rasgo que caracteriza a los distintos lenguajes de programación.
En función de qué errores detectan y cuándo los detectan, los lenguajes de programación se pueden clasificar en tres categorías.
Los que realizan sólo comprobaciones dinámicas. Hay lenguajes que, dado que hay que hacer comprobaciones dinámicas de todas formas, deciden ya hacer todas las comprobaciones de esa manera (incluidas las de los errores que pudieran ser detectados estáticamente). A estos lenguajes se les conoce como dinámicos precisamente por este motivo. Lenguajes de este tipo son Javascript, Python y Ruby.
Estos lenguajes, por ejemplo, no detectan el uso de una variable que no haya sido definida hasta el momento en el que se use durante la ejecución del programa.
if (a > 10) print(b) // Sólo comprueba 'b' si 'a > 10'
Los que realizan sólo comprobaciones estáticas. En estos lenguajes, los errores que sólo pueden ser detectados en tiempo de ejecución, al no generar el código de las comprobaciones dinámicas, pasan desapercibidos. Ejemplos de estos lenguajes son C, Pascal y Fortran.
Si, por ejemplo, en C se realiza una escritura fuera de rango en un array, el valor se escribirá en memoria desconocida sin que nada avise en el momento de ejecutar dicha sentencia errónea (y el efecto de esto será indeterminado). Estos lenguajes asumen que es responsabilidad del programador que esto no ocurra.
v[i] = 99 // ¿pantallazo azul?
Los que realizan ambos tipos de comprobaciones. Estos lenguajes suelen usar sólo la comprobación dinámica de un determinado error cuando no sea posible una comprobación estática. Ejemplos de estos lenguajes son Java, C#, Go y Rust.
Estos lenguajes detectarían que la variable b del ejemplo anterior no ha sido definida antes de ejecutar el programa y avisarían también del error en la línea con el acceso al array fuera de rango durante la ejecución.
Una vez establecida la misión del analizador semántico, que es detectar errores mediante comprobaciones estáticas, se verá en los capótulos posteriroes cómo se hace esto.