Subsecciones

Análisis Semántico

Hay quien dice que el análisis semántico es la determinación de aquellas propiedades que, siendo dependientes del contexto, pueden ser computadas estáticamente en tiempo de compilación para cualquier programa correcto. Entre estas propiedades están: la comprobación de que las variables son declaradas, la compatibilidad de tipos en las expresiones, el correcto uso de las llamadas a función asi como el ámbito y visibilidad de las variables. La fase de análisis semántico puede verse como una fase de ``adornado'' o ``etiquetado'' del AAA, en la cual los atributos de los nodos del AAA son computados.

Aunque la veamos como una fase separada del análisis sintáctico, puede en numerosas ocasiones llevarse a cabo al mismo tiempo que se construye el árbol. Así lo hacemos en este ejemplo: incrustamos la acción semántica en la correspondiente rutina de análisis sintáctico. Así, en la rutina term, una vez que hemos obtenido los dos operandos, comprobamos que son de tipo numérico llamando (línea 8) a

Semantic::Analysis::check_type_numeric_operator:

Observe como aparece un nuevo atributo TYPE decorando el nodo creado (línea 9):

 1 sub term() {
 2   my ($t, $t2);
 3 
 4   $t = factor;
 5   if ($lookahead eq '*') {
 6     match('*');
 7     $t2 = term;
 8     my $type = Semantic::Analysis::check_type_numeric_operator($t, $t2, '*');
 9     $t = TIMES->new( LEFT => $t, RIGHT => $t2, TYPE => $type);
10   }
11   return $t;
12 }

En el manejo de errores de tipo, un tipo especial $err_type es usado para indicar un error de tipo:

  sub check_type_numeric_operator {
    my ($op1, $op2, $operator) = @_;

    my $type = numeric_compatibility($op1, $op2, $operator);
    if ($type == $err_type) {
      Error::fatal("Operandos incompatibles para el operador $operator") 
    }
    else {
      return $type;
    }
  }
La subrutina numeric_compatibility comprueba que los dos operandos son de tipo numérico y devuelve el correspondiente tipo. Si ha ocurrido un error de tipo, intenta encontrar un tipo conveniente para el operando:

  sub numeric_compatibility {
    my ($op1, $op2, $operator) = @_;

    if (($op1->TYPE == $op2->TYPE) and is_numeric($op1->TYPE)) {
      return $op1->TYPE; # correct
    }
    ... # código de recuperación de errores de tipo
  }

  sub is_numeric {
    my $type = shift;

    return ($type == $int_type); # añadir long, float, double, etc.
  }

Es parte del análisis semántico la declaración de tipos:

sub declaration() {
  my ($t, $class, @il);

  if (($lookahead eq 'INT') or ($lookahead eq 'STRING')) {
    $class = $lookahead;
    $t = &type();
    @il = &idlist();
    &Semantic::Analysis::set_types($t, @il);
    &Address::Assignment::compute_address($t, @il);
    return $class->new(TYPE => $t, IDLIST => \@il);
  }
  else {
    Error::fatal('Se esperaba un tipo');
  }
}
Para ello se utiliza una tabla de símbolos que es un hash %symbol_table indexado en los identificadores del programa:

sub set_types {
  my $type = shift;
  my @vars = @_;

  foreach my $var (@vars) {
    if (!exists($symbol_table{$id})) { $symbol_table{$var}->{TYPE} = $type; }
    else { Error::error("$id declarado dos veces en el mismo ámbito"); }
  }
}

Cada vez que aparece una variable en el código, bien en un factor o en una asignación, comprobamos que ha sido declarada:

sub factor() {
  my ($e, $id, $str, $num);

  if ($lookahead eq 'NUM') { ... }
  elsif ($lookahead eq 'ID') {
    $id = $value;
    match('ID');
    my $type = Semantic::Analysis::check_declared($id);
    return ID->new( VAL => $id, TYPE => $type);
  }
  elsif ($lookahead eq 'STR') { ... }
  elsif ($lookahead eq '(') { ... }
  else { Error::fatal("Se esperaba (, NUM o ID"); }
}

La función check_declared devuelve el atributo TYPE de la correspondiente entrada en la tabla de símbolos.

sub check_declared {
  my $id = shift;

  if (!exists($symbol_table{$id})) {
    Error::error("$id no ha sido declarado!");
    # auto-declaración de la variable a err_type
    Semantic::Analysis::set_types($err_type, ($id));
  }
  return $symbol_table{$id}->{TYPE};
}


Práctica: Declaraciones Automáticas

Modifique la subrutina check_declared para que cuando una variable no haya sido declarada se declare ``sobre la marcha''. ¿Puede utilizar información dependiente del contexto para decidir cual es la mejor forma de declararla?


Práctica: Análisis Semántico

Extienda el código de la práctica 33.9.5 para comprobar la compatibilidad de tipos.



1 p $ \rightarrow$ b
2 b $ \rightarrow$ ds ss
3 b $ \rightarrow$ ss
4 ds $ \rightarrow$ d ';' ds
5 ds $ \rightarrow$ d ';'
6 d $ \rightarrow$ INT il
7 d $ \rightarrow$ STRING il
8 ss $ \rightarrow$ s ';' ss
9 ss $ \rightarrow$ s
10 s $ \rightarrow$ ID '=' e
11 s $ \rightarrow$ '{' b '}'
12 s $ \rightarrow$ P e
13 s $ \rightarrow \epsilon$
14 e $ \rightarrow$ e1 '+' t
15 e $ \rightarrow$ e1 '-' t
16 e $ \rightarrow$ t
17 t $ \rightarrow$ t1 '*' f
18 t $ \rightarrow$ t '/' f
19 t $ \rightarrow$ f
20 f $ \rightarrow$ '(' e ')'
21 f $ \rightarrow$ ID
22 f $ \rightarrow$ NUM
23 f $ \rightarrow$ STR
24 il $ \rightarrow$ ID ',' il
25 il $ \rightarrow$ ID


En cuanto a las sentencias de bloque, se pretende que el ámbito y visibilidad de las variables sea como en el lenguaje C, esto es, las declaraciones mas internas con el mismo identificador ocultan las mas externas. Así:

int a;
a = 4;
{
  int a;
  a = 5;
  p a
}; /* el ; es necesario */
p a

Imprimiría 5 y 4. Para traducir esta sentencia es necesario usar una lista/pila de referencias a tablas de símbolos. Cada sentencia compuesta o bloque tendrá su propia tabla de símbolos. Los identificadores se búscan en la lista de referencias a tablas de símbolos, primero en la última tabla de símbolos insertada y sino se encuentra se busca en la penúltima insertada, etc.

Guarde como un atributo del identificador ($ SYMTABLE$) la referencia a la tabla de símbolos a la que pertenece. Guarde como un atributo del nodo bloque ($ BLOCK$) la referencia a la tabla de símbolos asociada.

Casiano Rodríguez León
2013-04-23