next up previous contents
Next: Geração de código Up: Geração de código e Previous: Geração de código e   Sumário

Análise semântica

Embora a análise sintática consiga verificar se uma expressão obedece às regras de formação de uma dada gramática, seria muito difícil expressar através de gramáticas algumas regras usuais em linguagem de programação, como ``todas as variáveis devem ser declaradas'' e situações onde o contexto em que ocorre a expressão ou o tipo da variável deve ser verificado.

O objetivo da análise semântica é trabalhar nesse nível de inter-relacionamento entre partes distintas do programa. As tarefas básicas desempenhada durante a análise semântica incluem a verificação de tipos, a verificação do fluxo de controle e a verificação da unicidade da declaração de variáveis. Dependendo da linguagem de programação, outros tipos de verificações podem ser necessários.

Considere o seguinte exemplo de código em C:

int f1(int a, float b) {
  return a%b;
}
A tentativa de compilar esse código irá gerar um erro detectado pelo analisador semântico, mais especificamente pelas regras de verificação de tipos, indicando que o operador módulo % não pode ter um operador real. No compilador gcc, essa mensagem é
In function `f1': 
...: invalid operands to binary %

Em alguns casos, o compilador realiza a conversão automática de um tipo para outro que seja adequado à aplicação do operador. Por exemplo, na expressão em C

  a = x - '0';
a constante do tipo caráter '0' é automaticamente convertida para inteiro para compor corretamente a expressão aritmética na qual ela toma parte; todo char em uma expressão é convertido pelo compilador para um int. Esse procedimento de conversão de tipo é denominado coerção (cast). Em C, a seguinte seqüência de regras determina a realização automática de coerção em expressões aritméticas:
  1. char e short são convertidos para int, float para double;
  2. se um dos operandos é double, o outro é convertido para double e o resultado é double;
  3. se um dos operandos é long, o outro é convertido para long e o resultado é long;
  4. se um dos operandos é unsigned, o outro é convertido para unsigned e o resultado é unsigned;
  5. senão, todos os operandos são int e o resultado é int.

Em outras situações, a conversão deve ser indicada explicitamente pelo programador através do operador de molde, com o nome do tipo entre parênteses na frente da expressão cujo resultado deseja-se converter. Por exemplo, com as declarações

  int a;
  int *p;
a expressão
  a = p;
geraria a seguinte mensagem do compilador:
  warning: assignment makes integer from pointer without a cast
Porém, se o programador indicar que sabe que está fazendo uma conversão ``perigosa'' através do operador de molde,
  a = (int) p;
então nenhuma mensagem é gerada.

Algumas linguagens de programação, como C++, permitem definir comportamentos diferenciados a operadores segundo o tipo de argumento que recebem. Por exemplo, na expressão

  c << x;
o operador << será interpretado como o comando de deslocamento de bits à esquerda se c e x forem inteiros, mas será uma operação de saída se c for uma referência para um arquivo. Esse mecanismo de adequar o comportamento do operador segundo o tipo de seus operandos é denominado sobrecarga de operadores. Em geral, essas linguagens permitem também aplicar o mesmo tipo de mecanismo a rotinas. Através da sobrecarga de funções, o compilador seleciona entre rotinas que têm o mesmo nome aquela cuja quantidade e lista de tipos estão adequadas à forma de invocação.

Outro exemplo de erro detectado pela análise semântica, neste caso pela verificação de fluxo de controle, é ilustrado pelo código

void f2(int j, int k) {
  if (j == k)
    break;
  else
    continue;
}
Nesse caso, o compilador gera as mensagens:
In function `f2':
...: break statement not within loop or switch
...: continue statement not within a loop
ou seja, ele reconhece que o comando break só pode ser usado para quebrar a seqüência de um comando de iteração (within loop) ou para indicar o fim de um case (within switch). Da mesma forma, um comando continue só pode ser usado em um comando de iteração.

A verificação de unicidade detecta situações tais como duplicação em declarações de variáveis, de componentes de estruturas e em rótulos do programa. Por exemplo, a compilação do seguinte código

void f3(int k) {
  struct {
    int a;
    float a;
  } x;
  float x;
  switch (k) {
  case 0x31: x.a = k;
  case '1': x = x.a;
  }
}
causaria a geração das seguintes mensagens de erro pelo compilador gcc:
In function `f3':
...: duplicate member `a'
...: previous declaration of `x'
...: duplicate case value
A primeira mensagem detecta que dois membros de uma mesma definição de estrutura recebem o mesmo nome, a, o que não é permitido. Na segunda mensagem a indicação refere-se às duas variáveis de mesmo nome, x. A terceira mensagem indica que dois cases em uma expressão switch receberam o mesmo rótulo, o que também não é permitido. Observe que, mesmo embora a forma de expressar o valor nos cases tenha sido diferente, o compilador verificou que 0x31 e '1' referiam-se ao mesmo valor e acusou a situação de erro.


next up previous contents
Next: Geração de código Up: Geração de código e Previous: Geração de código e   Sumário
Ivan L. M. Ricarte 2003-02-14