Análisis Semántico

El análisis semántico es una fase crítica en el proceso de compilación que sigue al análisis sintáctico. Su objetivo principal es verificar que las sentencias y expresiones del programa tengan significado lógico y coherente de acuerdo con las reglas del lenguaje de programación. Mientras que el análisis sintáctico asegura que las frases estén correctamente estructuradas, el análisis semántico se asegura de que las operaciones realizadas sobre los datos sean válidas y consistentes.


Funciones del Análisis Semántico

El análisis semántico se encarga de varias tareas clave en la compilación:

  1. Verificación de Tipos: Se asegura de que los tipos de datos usados en las operaciones sean compatibles. Por ejemplo, verificar que una variable de tipo entero no sea asignada a una cadena de texto.

  2. Chequeo de Alcance: Verifica que las variables y funciones utilizadas hayan sido declaradas previamente y estén dentro del ámbito (scope) adecuado.

  3. Compatibilidad de Argumentos: Asegura que las funciones sean llamadas con el número y tipo correctos de argumentos, según su definición.

  4. Conversión de Tipos: En algunos casos, es necesario convertir los tipos de datos para que las operaciones sean válidas. El análisis semántico se asegura de que estas conversiones sean correctas y seguras.

  5. Manejo de Errores Semánticos: Si hay errores de tipo semántico, como una operación inválida o una variable no definida, el compilador genera advertencias o errores.

Ejemplo:

Si en un programa C se declara:

int x;
x = "hola";

El análisis semántico detectaría un error porque se está intentando asignar una cadena de caracteres a una variable de tipo entero.


Tabla de Símbolos

Durante el análisis semántico, se utiliza una tabla de símbolos, que es una estructura de datos que almacena información sobre los identificadores del programa (variables, funciones, etc.). Para cada identificador, la tabla de símbolos guarda:

  • El tipo de dato asociado (entero, flotante, cadena, etc.).
  • El ámbito en el que fue declarado.
  • Información adicional como el método de paso de parámetros o el tipo de retorno de una función.

La tabla de símbolos es utilizada tanto por el analizador léxico, sintáctico y semántico, y se actualiza conforme se procesan las declaraciones y definiciones.


Tipos de Errores Semánticos

1. Errores de Tipo:

  • Ocurren cuando se intenta realizar operaciones entre tipos incompatibles.
  • Ejemplo: sumar un entero con una cadena de texto.

2. Uso de Variables No Declaradas:

  • Ocurre cuando se utiliza una variable que no ha sido previamente declarada en el programa.

3. Incompatibilidad de Argumentos:

  • Se produce cuando una función es llamada con el número incorrecto de parámetros o con tipos de datos incompatibles.

4. Asignaciones Inválidas:

  • Se detectan cuando se asigna un valor a una variable que no coincide con su tipo de dato.

5. Control de Flujo Incoherente:

  • Ocurre cuando las sentencias de control como if o while contienen condiciones que no son válidas o no tienen sentido en el contexto.

Ejemplo de Proceso de Análisis Semántico

Considera el siguiente fragmento de código en C:

int suma(int a, int b) {
    return a + b;
}
 
int main() {
    int resultado;
    resultado = suma(5, "diez");
}

Durante el análisis semántico, el compilador detecta que se está pasando una cadena de texto (“diez”) a la función suma, que espera dos enteros. Esto generará un error de incompatibilidad de tipos.


Representación Intermedia

Una vez finalizado el análisis semántico, se puede generar una representación intermedia del programa que sea más fácil de optimizar y de convertir en código máquina. Esta representación se basa en el árbol sintáctico construido durante el análisis sintáctico, pero ahora incluye información semántica, como los tipos de datos y las conversiones necesarias entre ellos.


Esquemas canónicos y de traducción

Esquemas canónicos y de traducción

Los esquemas canónicos y los esquemas de traducción son herramientas fundamentales en la fase de análisis y síntesis de los compiladores. Los esquemas canónicos proporcionan una forma estandarizada de presentar las gramáticas y las estructuras del lenguaje, mientras que los esquemas de traducción se utilizan para definir cómo transformar esas estructuras del lenguaje fuente en una representación destino.


Esquemas Canónicos

Los esquemas canónicos son una representación normalizada de las gramáticas formales que facilitan la comprensión y el procesamiento de un lenguaje. Están estrechamente relacionados con la teoría de autómatas y gramáticas formales, y se utilizan en el análisis sintáctico para imponer reglas claras y consistentes en el procesamiento de las cadenas de entrada.

Ejemplo:

Un ejemplo clásico de un esquema canónico es la Forma Normal de Chomsky (CNF). En esta forma, cada regla de producción tiene una de dos formas posibles:

  • , donde , y son símbolos no terminales.
  • , donde es un símbolo terminal.

Esta forma es utilizada principalmente para simplificar el análisis sintáctico en compiladores, ya que permite una representación más eficiente de las reglas gramaticales. Además, facilita la implementación de algoritmos como el análisis CYK para gramáticas libres de contexto.

Ventajas:

  • Simplificación de las reglas gramaticales.
  • Facilidad de implementación de algoritmos de análisis sintáctico.
  • Reducción de la complejidad en la verificación de la validez gramatical de las cadenas.

Esquemas de Traducción

Los esquemas de traducción definen cómo un compilador convierte una estructura gramatical en una estructura equivalente en un lenguaje destino. Esta fase se lleva a cabo durante el análisis semántico y la generación de código, donde las construcciones del lenguaje fuente son transformadas en una representación intermedia o en código ejecutable.

Ejemplo:

Consideremos una gramática para expresiones aritméticas:

<expresión> ::= <expresión> + <término> | <término>
<término> ::= <término> * <factor> | <factor>
<factor> ::= id | num

El siguiente es un esquema de traducción asociado:

  • Para <expresión> ::= <expresión> + <término>, la acción es agregar un nodo para la operación + en el árbol de expresión y combinar los operandos.
  • Para <término> ::= <término> * <factor>, se agrega un nodo para la operación * y se combinan los factores.
  • Para <factor> ::= id, se registra el identificador como una hoja del árbol.

Esquemas de Traducción Dirigidos por la Sintaxis (SDT)

Un tipo particular de esquema de traducción es el Esquema de Traducción Dirigido por la Sintaxis (SDT). En este tipo de esquema, se añaden acciones semánticas a las reglas de producción de la gramática. Estas acciones se ejecutan cuando las reglas son aplicadas durante el análisis sintáctico.

Ejemplo con SDT:

<expresión> ::= <expresión> + <término> { print("+"); }
<término> ::= <término> * <factor> { print("*"); }
<factor> ::= id { print(id.lexema); }

Este esquema traduce una expresión aritmética, generando el código que representa la operación realizada. Si la entrada es a + b * c, el esquema generaría la salida a b c * +.

Ventajas:

  • Permite la integración de acciones semánticas directamente en las reglas gramaticales.
  • Facilita la generación de código a medida que se realiza el análisis sintáctico.
  • Es útil para realizar traducciones eficaces y optimizadas del código fuente a código intermedio o código máquina.
Link to original