Notes - MIECT
Compiladores
Notes - MIECT
Compiladores
  • Compiladores
  • Compiladores, Linguagens e Gramáticas
    • Enquadramento
    • Compiladores
      • Interpretadores
      • Estrutura de um Compilador
    • Implementação de um Compilador
    • Linguagens: Definição como Conjunto
      • Conceito básicos e terminologia
      • Operações sobre palavras
      • Operações sobre linguagens
    • Introdução às gramáticas
      • Hierarquia de Chomsky
      • Autómatos
  • Introdução, Estrutura, Aplicação
    • Exemplos
      • Expr
      • Exemplo figuras
      • Exemplo visitor
      • Exemplo listener
    • Construção de gramáticas
      • Especificação de gramáticas
    • Estrutura léxica
    • Regras léxicas
      • Padrões léxicos típicos
      • Operador léxico “não ganancioso”
    • Estrutura sintática
      • Secção de Tokens
      • Acções no preâmbulo da gramática
    • Regras sintácticas
      • Padrões sintácticos típicos
      • Precedência
      • Associatividade
      • Herança de gramáticas
    • Outras funcionalidades
      • Tabelas CSV
      • Gramáticas ambíguas
      • Predicados semânticos
      • Separar analisador léxico do analisador sintáctico
      • “Ilhas” lexicais
      • Enviar tokens para canais diferentes
      • Reescrever a entrada
      • Desacoplar código da gramática - ParseTreeProperty
  • Análise Semântica
    • Estrutura de um Compilador
    • Sistema de Tipos
    • Gramáticas de Atributos
    • Tabela de símbolos
    • Instruções restringidas por contexto
    • ANTLR4: gestão de erros
  • Síntese
    • Síntese: Geração de código
    • String Template
    • Síntese: geração de código intermédio
  • Análise Lexical
    • Análise Lexical: Estrutura de um Compilador
    • Linguagens regulares
    • Gramáticas regulares
    • Expressões regulares
    • Conversão entre ER e GR
    • Reconhecimento de tokens
    • Autómatos finitos
    • Autómato finito não determinista
    • Autómato finito determinista
      • Projecto de autómato finito determinista
    • Conversão de AFND em AFD
    • Conversão de uma expressão regular num AFND
    • Autómato finito generalizado (AFG)
  • Gramática de Atributos
    • Conteúdo semântico
    • Gramática de atributos
    • Avaliação Dirigida pela Sintaxe
  • Análise Sintática Descendente
    • Análise Sintática
    • Análise Sintática Descendente
    • Analisador (parser) Recursivo-Descendente Preditivo
    • Questões a resolver
    • Fatorização à Esquerda
    • Eliminação de Recursividade á Esquerda
    • Conjuntos predict, first e follow
      • Conjunto first
      • Conjunto follow
    • Reconhecedor Descendente Preditivo
  • Análise Sintática Ascendente
    • Análise Sintática Ascendente
    • Construção de um reconhecedor ascendente
    • Tabela de decisão de um reconhecedor ascendente
    • Reconhecedor Ascendente
    • Tabela de Decisão de um Reconhecedor Ascendente
Powered by GitBook
On this page
  • Código de triplo endereço
  • TAC: Exemplo de expressões binárias
  • TAC: Endereços e instruções
  • Controlo de fluxo
  • Funções
  1. Síntese

Síntese: geração de código intermédio

PreviousString TemplateNextAnálise Lexical: Estrutura de um Compilador

Last updated 3 years ago

Código de triplo endereço

O padrão para expressões é um exemplo duma representação muito utilizada para geração de código baixo nível (em geral, intermédio, e não final), designada por codificação de triplo endereço (TAC).

Esta designação tem origem nas instruções com a forma: x = y op z.

No entanto, para além desta operação típica de expressões binárias, esta codificação contém outras instruções (ex: operações unárias e de controlo de fluxo).

No máximo, cada instrução tem três operandos (i.e. três variáveis ou endereços de memória).

Tipicamente, cada instrução TAC realiza uma operação elementar (e já com alguma proximidade com as linguagens de baixo nível dos sistemas computacionais).

TAC: Exemplo de expressões binárias

Por exemplo a expressão a + b ∗ (c + d) pode ser transformada na sequência TAC:

Esta sequência – embora fazendo uso desregrado no número de registos (o que, num compilador gerador de código máquina, é resolvido numa fase posterior de optimização) – é codificável em linguagens de baixo nível.

TAC: Endereços e instruções

Nesta codificação, um endereço pode ser:

  • Um nome do código fonte (variável, ou endereço de memória);

  • Uma constante (i.e. um valor literal);

  • Um nome temporário (variável , ou endereço de memória), criado na decomposição TAC.

As instruções típicas do TAC são:

  1. Atribuições de valor de operação binária: x = y op z;

  2. Atribuições de valor de operação unária: x = op y;

  3. Instruções de cópia: x = y;

  4. Saltos incondicionais e etiquetas: goto L e label L;

  5. Saltos condicionais: if x goto L ou ifFalse x goto L;

  6. Saltos condicionais com operador relacional: if x relop y goto L (o operador pode ser de igualdade ou ordem);

  7. Invocações de procedimentos (param x 1 · · · param x n ; call p, n ; y = call p, n ; return y);

  8. Instruções com arrays (i.e. o operador é os parêntesis rectos, e um dos operandos é o índice inteiro).

  9. Instruções com ponteiros para memória (como em C)

Controlo de fluxo

As instruções de controlo de fluxo são as instruções condicionais e os ciclos.

Em linguagens de baixo nível muitas vezes estas instruções não existem.

O que existe em alternativa é a possibilidade de dar “saltos” dentro do código recorrendo a endereços (labels) e a instruções de salto (goto, . . . ).

De forma similar podemos gerar código para ciclos:

Funções

A geração de código para funções pode ser feita recorrendo a uma estratégia tipo “macro” (i.e. na invocação da funções é colocado o código que implementa a função), ou implementando módulos algorítmicos separados.

Neste último caso (que, entre outras coisas, permite a recursividade), é necessária a definição de um bloco algorítmico separado, assim como implementar a passagem de argumentos/resultado para/de a função.

A passagem de argumentos pode seguir diferentes estratégias: passagem por valor, passagem por referência de variáveis, passagem por referência de objectos/registos.

Para termos implementações recursivas é necessário que se definam novas variáveis em cada invocação da função.

A estrutura de dados que nos permite fazer isso de uma forma muito eficiente e simples é a pilha de execução.

Esta pilha armazena os argumentos, variáveis locais à função e o resultado da função (permitindo ao código que invoca a função não só passar os argumentos à função como ir buscar o seu resultado).

Geralmente as arquitecturas de linguagens de baixo nível (CPU’s) têm instruções específicas para lidar com esta estrutura de dados.