compiladores - w3.ualg.ptw3.ualg.pt/~jmcardo/ensino/comp04/guiacompiladores.pdf · caderno de...

62
Caderno de exercícios e trabalhos práticos para a disciplina de: COMPILADORES Julho de 2003 - Julho de 2004 João M. P. Cardoso Universidade do Algarve Faculdade de Ciências e Tecnologia Campus de Gambelas 8000-117 Faro Email: [email protected] URL: http://w3.ualg.pt/~jmcardo Curso de Licenciatura em Engenharia de Sistemas e Informática Curso de Licenciatura em Ensino de Informática Curso de Licenciatura em Informática (ramo tecnológico e ramo de gestão)

Upload: buitram

Post on 07-Nov-2018

225 views

Category:

Documents


0 download

TRANSCRIPT

Caderno de exercícios e trabalhos práticos para a disciplina de:

COMPILADORES

Julho de 2003 - Julho de 2004

João M. P. Cardoso Universidade do Algarve Faculdade de Ciências e Tecnologia Campus de Gambelas 8000-117 Faro Email: [email protected] URL: http://w3.ualg.pt/~jmcardo Curso de Licenciatura em Engenharia de Sistemas e Informática Curso de Licenciatura em Ensino de Informática Curso de Licenciatura em Informática (ramo tecnológico e ramo de gestão)

CADERNO DE EXERCÍCIOS E TRABALHOS PRÁTICOS PARA A DISCIPLINA DE COMPILADORES

©João M. P. Cardoso 2

Índice

FICHA 1. Utilização da ferramenta make ..............................................................5

1.1 Verificar a diferença entre os tempos de execução de uma aplicação na

presença ou ausência de opções de optimização de compilação. ..............................5

1.2 Criação de uma makefile ...............................................................................6

FICHA 2. Optimizações de código.........................................................................8

2.1 Verificar a diferença entre o tempo de execução utilizando o interpretador e

utilizando o compilador JIT .......................................................................................8

2.2 Optimizações..................................................................................................9

2.3 Propagação de constantes (Constant Propagation) ......................................10

2.4 Constant folding (Constant-Expression Evaluation) ...................................11

2.5 Desenrolamento de ciclos (Loop Unrolling)................................................12

2.6 Simplificação algébrica (Algebraic simplification) .....................................12

2.7 Desenrolamento de ciclos (Loop Unrolling)................................................13

2.8 Simplificações algébricas + constant folding ..............................................14

2.9 Scalar replacement .......................................................................................14

2.10 Simplificação algébrica................................................................................15

2.11 Eliminação de código ou de declarações não utilizadas ..............................16

2.12 Strength reduction........................................................................................16

2.13 Depois de simplificações algébricas e reassociação ....................................17

2.14 Depois de CSE (eliminação de sub-expressões comuns): ...........................17

2.15 Sumário ........................................................................................................18

CADERNO DE EXERCÍCIOS E TRABALHOS PRÁTICOS PARA A DISCIPLINA DE COMPILADORES

©João M. P. Cardoso 3

FICHA 3. Introdução ao Processamento de Linguagens ......................................19

3.1 Exercícios.....................................................................................................19

FICHA 4. Introdução aos Algoritmos Iterativos...................................................21

4.1 Algoritmo Iterativo para Determinar Funções Recursivas ..........................21

4.2 Transitive Closure........................................................................................22

4.3 Algoritmo Iterativo para Transitive Closure................................................22

4.4 Exercício 1 ...................................................................................................23

4.5 Código Java..................................................................................................23

4.6 Classe Graph.java ........................................................................................23

4.7 Classe TransitiveClosure.java......................................................................25

4.8 Trabalho para Casa ......................................................................................26

4.9 Créditos ........................................................................................................26

FICHA 5. Exercícios sobre Expressões Regulares e Autómatos Finitos..............27

5.1 Exercícios com Expressões Regulares.........................................................27

5.2 Exercícios com Autómatos Finitos ..............................................................27

FICHA 6. Implementação manual de um analisador lexical ................................30

6.1 Exercícios.....................................................................................................30

6.2 Notas ............................................................................................................31

FICHA 7. Exercícios sobre Gramáticas................................................................32

FICHA 8. Implementação manual de um analisador sintáctico descendente .......35

FICHA 9. Implementação de Analisadores Gramaticais Utilizando o JavaCC (1ª

parte) 36

CADERNO DE EXERCÍCIOS E TRABALHOS PRÁTICOS PARA A DISCIPLINA DE COMPILADORES

©João M. P. Cardoso 4

9.1 Exemplo .......................................................................................................36

9.2 Exercícios.....................................................................................................39

9.3 Referências de Apoio...................................................................................39

FICHA 10. Implementação de Analisadores Gramaticais Utilizando o JavaCC (2ª

parte) 40

10.1 Exercício ......................................................................................................40

FICHA 11. Implementação de Analisadores Gramaticais Utilizando o JavaCC (3ª

parte) 41

11.1 Exemplo .......................................................................................................41

11.2 Referências de Apoio...................................................................................52

FICHA 12. Trabalho Final ......................................................................................53

12.1 Exercício ......................................................................................................53

12.2 Gramática Inicial da Linguagem..................................................................56

12.3 Algumas Dicas .............................................................................................57

12.4 Documentação e Ferramentas de Suporte....................................................57

Anexo B: Enunciado de um Exame .............................................................................59

CADERNO DE EXERCÍCIOS E TRABALHOS PRÁTICOS PARA A DISCIPLINA DE COMPILADORES

©João M. P. Cardoso 5

FICHA 1. Utilização da ferramenta make1

Duração: 3 horas

Esta aula tem como objectivo a utilização de makefiles. Será demonstrada a sua

utilidade na compilação de uma aplicação, fazendo variar algumas opções de

compilação. A criação de uma makefile será estudada para um programa exemplo.

1.1 Verificar a diferença entre os tempos de execução

de uma aplicação na presença ou ausência de

opções de optimização de compilação. Procedimentos:

Após copiar o ficheiro JASPA_1.0.tar.gz para a área de trabalho deve descompactá-lo

(sugestão: tar xvfz ficheiro).

Mudando para o directório JASPA, com um editor à escolha, comente a 4ª linha do

ficheiro MakeFile que determina a compilação usando F90. Edite o ficheiro

common.make e remova a secção respeitante ao F90.

Para verificar a correcta instalação da aplicação, emita os seguintes comandos:

$ make

$ cd matrices

$ java jaspa memplus.mtx

$ jaspa_c memplus.mtx

Copie para o directório matrices, o ficheiro m48.mtx.

1 Exercício concebido pela Eng.ª Margarida Moura (assistente da disciplina no ano

lectivo de 2002/2003)

CADERNO DE EXERCÍCIOS E TRABALHOS PRÁTICOS PARA A DISCIPLINA DE COMPILADORES

©João M. P. Cardoso 6

Voltando para o directório JASPA, edite o ficheiro common.make e comente as linhas

de CFLAGS e JAVAFLAGS.

Emita os comandos:

$ make

$ cd matrices

$ java jaspa m48.mtx

$ java jaspa m48.mtx

$ java jaspa m48.mtx

Anote os resultados obtidos.

$ jaspa_c m48.mtx

$ jaspa_c m48.mtx

$ jaspa_c m48.mtx

Anote os resultados obtidos.

Voltando para o directório JASPA, edite o ficheiro common.make e retire a marca de

comentário das linhas de CFLAGS e JAVAFLAGS. Repita os comandos anteriores e

anote os resultados obtidos.

1.2 Criação de uma makefile Esta secção tem como objectivo relembrar a utilização de makefiles.

Sugere-se a leitura inicial de Criando Makefiles: Um Mini Tutorial

(http://www.gazetadolinux.com/pr/lg/issue83/heriyanto.html).

Para uma descrição detalhada, recomenda-se a leitura no manual do GNU make que

pode ser encontrado em http://www.gnu.org/software/make/manual/make.html

Procedimentos iniciais:

CADERNO DE EXERCÍCIOS E TRABALHOS PRÁTICOS PARA A DISCIPLINA DE COMPILADORES

©João M. P. Cardoso 7

Transfira para a sua área o ficheiro exer2.zip, e descompacte-o. Compile os

programas executando o comando $ make

Execute os programas com os comandos: $ prog0 dados_bbig.dat 370000 result.dat

$ prog1 dados_bbig.dat 370000 result.dat

Crie, na sua área, a estrutura de directórios da figura abaixo.

Figura 1. Árvore de directórios

Organize os programas por directórios de forma que:

file_func.c fique em gerais

exemplo1.c, exemplo2.c e exemplo3.c fiquem em func

prog0.c e prog1.c fiquem em src

a) Elabore uma makefile em Corrente que proceda à compilação dos programas,

guardando os executáveis em bin.

b) Modifique a makefile de forma que esta provoque a execução sucessiva dos dois

programas.

c) Modifique novamente a makefile de forma a que a linha emitida por um programa

seja enviada para um ficheiro com o nome do programa e a extensão log. Acrescente

uma regra que, usando o diff permita comparar os dois ficheiros.

CADERNO DE EXERCÍCIOS E TRABALHOS PRÁTICOS PARA A DISCIPLINA DE COMPILADORES

©João M. P. Cardoso 8

FICHA 2. Optimizações de código

Duração: 3 horas

Esta aula tem como objectivo a utilização de algumas das optimizações de código

realizadas pelos compiladores. Algumas dessas optimizações não dependem do

processador alvo para garantirem sempre bons resultados. As optimizações do tipo

anterior e cujo impacto será avaliado são:

• Propagação de constantes (Constant propagation);

• Avaliação de expressões com constantes (Constant folding ou Constant-

Expression evaluation);

• Simplificações algébricas (Algebraic Simplifications)

• Substituição por escalares (Scalar replacement)

• Redução do custo (Strength reduction)

• Eliminação de sub-expressões comuns (Common-Subexpression elimination -

CSE)

• Eliminação de código e de declarações não utilizadas

• Desenrolamento de ciclos (Loop unrolling)

Após leitura desta ficha e verificação das melhorias no tempo de execução, descreva

cada uma das optimizações anteriores.

2.1 Verificar a diferença entre o tempo de execução

utilizando o interpretador e utilizando o

compilador JIT O programa exemplo que vamos utilizar é formado pelas classes em bytecodes e por

uma classe para a qual é dada o ficheiro Java original. Para executar o programa deve

primeiro compilar o ficheiro Filter.java e depois executar um dos comandos seguintes:

Utilizando o interpretador: Java –Xint DrawImage

Utilizando o compilador JIT (por omissão): Java DrawImage

CADERNO DE EXERCÍCIOS E TRABALHOS PRÁTICOS PARA A DISCIPLINA DE COMPILADORES

©João M. P. Cardoso 9

Verifique os tempos de execução de cada um dos algoritmos apresentados.

A Figura 2 apresenta o resultado da execução do referido programa.

Figura 2. Appet Java utilizada para que os alunos possam verificar o impacto no

desempenho da utilização de determinadas transformações (optimizações) ao

nível do código da aplicação.

2.2 Optimizações Vamos considerar a função doFIR apresentada em baixo e que é um método da classe

Filter.java. Este método, como pôde constatar anteriormente implementa um

algoritmo de processamento de imagem que reduz o ruído de uma imagem. De

seguida vamos realizar manualmente um conjunto de optimizações, que integram ou

constam como opções de alguns compiladores e que permitem melhorar o

desempenho.

final public static short[] doFIR(short[] IN) {

short[] K = {1, 2, 1,

2, 4, 2,

1, 2, 1};

int DIM = 350;

short[] OUT = new short[DIM*DIM];

CADERNO DE EXERCÍCIOS E TRABALHOS PRÁTICOS PARA A DISCIPLINA DE COMPILADORES

©João M. P. Cardoso 10

for (int row=0; row < DIM-3+1; row++) {

for (int col = 0; col< DIM-3+1; col++) {

int sumval = 0;

for (int wrow=0; wrow < 3; wrow++) {

for (int wcol = 0; wcol<3; wcol++) {

sumval += IN[(row +wrow)*DIM+(col+wcol)]*K[wrow*3+wcol];

}

}

sumval = sumval / 16;

OUT[row * DIM + col] = (short) sumval;

}

}

return OUT;

}

2.3 Propagação de constantes (Constant Propagation) final public static short[] doFIR(short[] IN) {

short[] K = {1, 2, 1,

2, 4, 2,

1, 2, 1};

short[] OUT = new short[350*350];

for (int row=0; row < 350-3+1; row++) {

for (int col = 0; col< 350-3+1; col++) {

int sumval = 0;

for (int wrow=0; wrow < 3; wrow++) {

for (int wcol = 0; wcol<3; wcol++) {

sumval += IN[(row +wrow)*350+(col+wcol)]*K[wrow*3+wcol];

}

}

sumval = sumval / 16;

OUT[row * 350 + col] = (short) sumval;

}

}

return OUT;

}

CADERNO DE EXERCÍCIOS E TRABALHOS PRÁTICOS PARA A DISCIPLINA DE COMPILADORES

©João M. P. Cardoso 11

2.4 Constant folding (Constant-Expression

Evaluation) final public static short[] doFIR(short[] IN) {

short[] K = {1, 2, 1,

2, 4, 2,

1, 2, 1};

short[] OUT = new short[350*350];

for (int row=0; row < 350-3+1; row++) {

for (int col = 0; col< 350-3+1; col++) {

int sumval = 0;

for (int wrow=0; wrow < 3; wrow++) {

for (int wcol = 0; wcol<3; wcol++) {

sumval+= IN[(row +wrow)*350+(col+wcol)]*K[wrow*3+wcol];

}

}

sumval = sumval / 16;

OUT[row * 350 + col] = (short) sumval;

}

}

return OUT;

}

Depois de aplicar constant folding:

final public static short[] doFIR(short[] IN) {

short[] K = {1, 2, 1,

2, 4, 2,

1, 2, 1};

short[] OUT = new short[122500];

for (int row=0; row < 348; row++) {

for (int col = 0; col< 348; col++) {

int sumval = 0;

for (int wrow=0; wrow < 3; wrow++) {

for (int wcol = 0; wcol<3; wcol++) {

sumval+= IN[(row +wrow)*350+(col+wcol)]*K[wrow*3+wcol];

}

}

sumval = sumval / 16;

OUT[row * 350 + col] = (short) sumval;

122500

348

CADERNO DE EXERCÍCIOS E TRABALHOS PRÁTICOS PARA A DISCIPLINA DE COMPILADORES

©João M. P. Cardoso 12

}

}

return OUT;

}

2.5 Desenrolamento de ciclos (Loop Unrolling) for (int wcol = 0; wcol<3; wcol++) {

sumval+= IN[(row +wrow)*350+(col+wcol)]*K[wrow*3+wcol];

}

Obtém-se:

final public static short[] doFIR(short[] IN) {

short[] K = {1, 2, 1,

2, 4, 2,

1, 2, 1};

short[] OUT = new short[122500];

for (int row=0; row < 348; row++) {

for (int col = 0; col< 348; col++) {

int sumval = 0;

for (int wrow=0; wrow < 3; wrow++) {

sumval+= IN[(row +wrow)*350+(col+0)]*K[wrow*3+0];

sumval+= IN[(row +wrow)*350+(col+1)]*K[wrow*3+1];

sumval+= IN[(row +wrow)*350+(col+2)]*K[wrow*3+2];

}

sumval = sumval / 16;

OUT[row * 350 + col] = (short) sumval;

}

}

return OUT;

}

2.6 Simplificação algébrica (Algebraic simplification) final public static short[] doFIR(short[] IN) {

short[] K = {1, 2, 1,

2, 4, 2,

1, 2, 1};

short[] OUT = new short[122500];

for (int row=0; row < 348; row++) {

CADERNO DE EXERCÍCIOS E TRABALHOS PRÁTICOS PARA A DISCIPLINA DE COMPILADORES

©João M. P. Cardoso 13

for (int col = 0; col< 348; col++) {

int sumval = 0;

for (int wrow=0; wrow < 3; wrow++) {

sumval+= IN[(row +wrow)*350+col])*K[wrow*3];

sumval+= IN[(row +wrow)*350+(col+1)])*K[wrow*3+1];

sumval+= IN[(row +wrow)*350+(col+2)])*K[wrow*3+2];

}

sumval = sumval / 16;

OUT[row * 350 + col] = (short) sumval;

}

}

return OUT;

}

2.7 Desenrolamento de ciclos (Loop Unrolling) for (int wrow=0; wrow < 3; wrow++) {

sumval+= IN[(row +wrow)*350+col]*K[wrow*3];

sumval+= IN[(row +wrow)*350+(col+1)]*K[wrow*3+1];

sumval+= IN[(row +wrow)*350+(col+2)]*K[wrow*3+2];

}

Obtém-se:

final public static short[] doFIR(short[] IN) {

short[] K = {1, 2, 1,

2, 4, 2,

1, 2, 1};

short[] OUT = new short[122500];

for (int row=0; row < 348; row++) {

for (int col = 0; col< 348; col++) {

int sumval = 0;

sumval+= IN[(row +0)*350+col]*K[0*3];

sumval+= IN[(row +0)*350+(col+1)]*K[0*3+1];

sumval+= IN[(row +0)*350+(col+2)]*K[0*3+2];

sumval+= IN[(row +1)*350+col]*K[1*3];

sumval+= IN[(row +1)*350+(col+1)]*K[1*3+1];

sumval+= IN[(row +1)*350+(col+2)]*K[1*3+2];

sumval+= IN[(row +2)*350+col]*K[2*3];

sumval+= IN[(row +2)*350+(col+1)]*K[2*3+1];

sumval+= IN[(row +2)*350+(col+2)]*K[2*3+2];

CADERNO DE EXERCÍCIOS E TRABALHOS PRÁTICOS PARA A DISCIPLINA DE COMPILADORES

©João M. P. Cardoso 14

sumval = sumval / 16;

OUT[row * 350 + col] = (short) sumval;

}

}

return OUT;

}

2.8 Simplificações algébricas + constant folding final public static short[] doFIR(short[] IN) {

short[] K = {1, 2, 1,

2, 4, 2,

1, 2, 1};

short[] OUT = new short[122500];

for (int row=0; row < 348; row++) {

for (int col = 0; col< 348; col++) {

int sumval= IN[row*350+col]*K[0];

sumval+= IN[row*350+(col+1)]*K[1];

sumval+= IN[row*350+(col+2)]*K[2];

sumval+= IN[(row +1)*350+col]*K[3];

sumval+= IN[(row +1)*350+(col+1)]*K[4];

sumval+= IN[(row +1)*350+(col+2)]*K[5];

sumval+= IN[(row +2)*350+col]*K[6];

sumval+= IN[(row +2)*350+(col+1)]*K[7];

sumval+= IN[(row +2)*350+(col+2)]*K[8];

sumval = sumval / 16;

OUT[row * 350 + col] = (short) sumval;

}

}

return OUT;

}

2.9 Scalar replacement final public static short[] doFIR(short[] IN) {

short[] K = {1, 2, 1,

2, 4, 2,

1, 2, 1};

short[] OUT = new short[122500];

for (int row=0; row < 348; row++) {

for (int col = 0; col< 348; col++) {

CADERNO DE EXERCÍCIOS E TRABALHOS PRÁTICOS PARA A DISCIPLINA DE COMPILADORES

©João M. P. Cardoso 15

int sumval= IN[row*350+col]*1;

sumval+= IN[row*350+(col+1)]*2;

sumval+= IN[row*350+(col+2)]*1;

sumval+= IN[(row +1)*350+col]*2;

sumval+= IN[(row +1)*350+(col+1)]*4;

sumval+= IN[(row +1)*350+(col+2)]*2;

sumval+= IN[(row +2)*350+col]*1;

sumval+= IN[(row +2)*350+(col+1)]*2;

sumval+= IN[(row +2)*350+(col+2)]*1;

sumval = sumval / 16;

OUT[row * 350 + col] = (short) sumval;

}

}

return OUT;

}

2.10 Simplificação algébrica final public static short[] doFIR(short[] IN) {

short[] K = {1, 2, 1,

2, 4, 2,

1, 2, 1};

short[] OUT = new short[122500];

for (int row=0; row < 348; row++) {

for (int col = 0; col< 348; col++) {

int sumval= IN[row*350+col];

sumval+= IN[row*350+col+1]*2;

sumval+= IN[row*350+col+2];

sumval+= IN[(row +1)*350+col]*2;

sumval+= IN[(row +1)*350+col+1]*4;

sumval+= IN[(row +1)*350+col+2]*2;

sumval+= IN[(row +2)*350+col];

sumval+= IN[(row +2)*350+col+1]*2;

sumval+= IN[(row +2)*350+col+2];

sumval = sumval / 16;

OUT[row * 350 + col] = (short) sumval;

}

}

return OUT;

}

CADERNO DE EXERCÍCIOS E TRABALHOS PRÁTICOS PARA A DISCIPLINA DE COMPILADORES

©João M. P. Cardoso 16

2.11 Eliminação de código ou de declarações não

utilizadas final public static short[] doFIR(short[] IN) {

short[] OUT = new short[122500];

for (int row=0; row < 348; row++) {

for (int col = 0; col< 348; col++) {

int sumval= IN[row*350+col];

sumval+= IN[row*350+col+1]*2;

sumval+= IN[row*350+col+2];

sumval+= IN[(row +1)*350+col]*2;

sumval+= IN[(row +1)*350+col+1]*4;

sumval+= IN[(row +1)*350+col+2]*2;

sumval+= IN[(row +2)*350+col];

sumval+= IN[(row +2)*350+col+1]*2;

sumval+= IN[(row +2)*350+col+2];

sumval = sumval / 16;

OUT[row * 350 + col] = (short) sumval;

}

}

return OUT;

}

2.12 Strength reduction final public static short[] doFIR(short[] IN) {

short[] OUT = new short[122500];

for (int row=0; row < 348; row++) {

for (int col = 0; col< 348; col++) {

int sumval= IN[row*350+col];

sumval+= IN[row*350+col+1]<<1;

sumval+= IN[row*350+col+2];

sumval+= IN[(row +1)*350+col]<<1;

sumval+= IN[(row +1)*350+col+1]<<2;

sumval+= IN[(row +1)*350+col+2]<<1;

sumval+= IN[(row +2)*350+col];

sumval+= IN[(row +2)*350+col+1]<<1;

sumval+= IN[(row +2)*350+col+2];

sumval = sumval >> 4;

OUT[row * 350 + col] = (short) sumval;

CADERNO DE EXERCÍCIOS E TRABALHOS PRÁTICOS PARA A DISCIPLINA DE COMPILADORES

©João M. P. Cardoso 17

}

}

return OUT;

}

2.13 Depois de simplificações algébricas e reassociação

final public static short[] doFIR(short[] IN) {

short[] OUT = new short[122500];

for (int row=0; row < 348; row++) {

for (int col = 0; col< 348; col++) {

int sumval=IN[row*350+col];

sumval+=IN[row*350+col+1]<<1;

sumval+=IN[row*350+col+2];

sumval+=IN[350*row +350+col]<<1;

sumval+=IN[350*row +351+col]<<2;

sumval+=IN[350*row +352+col]<<1;

sumval+=IN[350*row +700+col];

sumval+=IN[350*row +701+col]<<1;

sumval+=IN[350*row +702+col];

sumval = sumval >> 4;

OUT[row * 350 + col] = (short) sumval;

}

}

return OUT;

}

2.14 Depois de CSE (eliminação de sub-expressões

comuns): final public static short[] doFIR(short[] IN) {

short[] OUT = new short[122500];

for (int row=0; row < 348; row++) {

for (int col = 0; col< 348; col++) {

int row_350_col = row*350 + col;

int sumval= IN[row_350_col];

sumval+= IN[row_350_col + 1]<<1;

sumval+= IN[row_350_col + 2];

CADERNO DE EXERCÍCIOS E TRABALHOS PRÁTICOS PARA A DISCIPLINA DE COMPILADORES

©João M. P. Cardoso 18

sumval+= IN[row_350_col + 350]<<1;

sumval+= IN[row_350_col + 351]<<2;

sumval+= IN[row_350_col + 352]<<1;

sumval+= IN[row_350_col + 700];

sumval+= IN[row_350_col + 701]<<1;

sumval+= IN[row_350_col + 702];

sumval = sumval >> 4;

OUT[row_350_col] = (short) sumval;

}

}

return OUT;

}

2.15 Sumário As optimizações ilustradas nesta ficha permitem, sempre que haja potencial para a sua

aplicação, melhorar o desempenho do código. No exemplo ilustrado foi necessário

aplicar algumas das optimizações mais do que uma vez, em virtude da aplicação de

certas optimizações potenciar a aplicação de outras (ver por exemplo a aplicação do

desenrolamento de ciclos).

CADERNO DE EXERCÍCIOS E TRABALHOS PRÁTICOS PARA A DISCIPLINA DE COMPILADORES

©João M. P. Cardoso 19

FICHA 3. Introdução ao Processamento de Linguagens

Duração: 3 horas

Exercício iniciais com programação em Java. Esta aula serve para adquirir alguns

conhecimentos em programação em Java com dois exemplos que envolvem o

tratamento de texto.

3.1 Exercícios 1. Escreva um programa em Java que leia um ficheiro de texto e apresente no

ecrã o número de ocorrências de cada caracter (letra, número, ou símbolo). O

programa deverá ser invocado na linha de comandos como:

Java ContaSimbolos <nome do ficheiro>

2. Escreva um programa que leia um ficheiro com um texto e escreva num

ficheiro especificado pelo utilizador o texto transformado pelas regras da

paródia: “The Official Language of Europe”2 identificadas a negrito na caixa

de texto abaixo (considere a precedência na aplicação das regras pela ordem

com que são descritas no texto). O programa deverá ser invocado na linha de

comandos como:

Java Parodia <nome do ficheiro de entrada> <nome do ficheiro de saída>

The Official Language of Europe

The European Commission has just announced an agreement whereby English will be the

official language of the EU rather than German which heavily lobbied to be the official

language and was the other possibility. As part of the negotiations, Her Majesty's

Government conceded that English spelling had some room for improvement and has

accepted a 5-year phase-in plan that would be known as "EuroEnglish."

2 Paródia divulgada na Internet de fonte desconhecida.

CADERNO DE EXERCÍCIOS E TRABALHOS PRÁTICOS PARA A DISCIPLINA DE COMPILADORES

©João M. P. Cardoso 20

In the first year, "s" will replace the soft "c" . . . Sertainly, this will make the sivil

servants jump with joy. The hard "c" will be dropped in favor of the "k." This should

klear up konfusion and keyboards kan have 1 less letter.

There will be growing publik enthusiasm in the sekond year, when the troublesome "ph"

will be replased with the "f." This will make words like "fotograf" 20% shorter.

In the 3rd year, publik akseptanse of the new spelling kan be expekted to reach the

stage where more komplikated changes are possible. Governments will enkourage the

removal of double letters, which have always ben a deterent to akurate speling. Also, al

wil agre that the horible mess of the silent "e's" in the language is disgraseful, and they

should go away.

By the 4th yar, peopl wil be reseptiv to steps such as replasing "th" with "z" and the "w"

with "v."

During the fifz yar, ze unesesary "o" kan be dropd from vords kontaining "ou" and

similar changes vud of kors be aplid to ozer kombinations of leters. After ze fifz yar, ve

vil hav a realy sensibl vriten styl. Zer vil be no mor trubls or difikultis and evryvun vil

find it easy tu understand each ozer.

ZE DREM VIL FINALI KUM TRU!

CADERNO DE EXERCÍCIOS E TRABALHOS PRÁTICOS PARA A DISCIPLINA DE COMPILADORES

©João M. P. Cardoso 21

FICHA 4. Introdução aos Algoritmos Iterativos

Duração: 3 horas

4.1 Algoritmo Iterativo para Determinar Funções Recursivas

Esta aula pretende representar uma classe de algoritmos muito utilizada em algumas

fases de um compilador. Estes algoritmos designam-se por iterativos, pois vão

alcançando a solução final por iterações.

Pretende-se determinar se um conjunto de chamadas a funções existente num dado

programa origina recursividade. Para tal é normalmente criado um grafo chamado de

Call Graph3 (grafo de chamadas a funções) que representa a estrutura de chamadas a

funções de um programa. Considere os pedaços de código seguintes (as reticências

indicam enunciados no programa que não contêm chamadas a instruções): Void P() {... Q(); … S(); …}

Void Q() {...R(); … T(); …}

Void R() {…P(); …}

Void S() {…}

Void T() {...}

Cada nó do Call Graph representa a chamada a uma função ou procedimento, e cada

laço entre dois nós representa que a função de onde sai a seta chama a função para

onde a seta aponta. O Call Graph que representa o programa é (poderia ser construído

com base na análise do código do programa):

P

Q

R T

S

3 O Call Graph é utilizado na compilação em outros contextos que não apenas o de recursividade.

CADERNO DE EXERCÍCIOS E TRABALHOS PRÁTICOS PARA A DISCIPLINA DE COMPILADORES

©João M. P. Cardoso 22

Vamos supor que um compilador pretendia determinar quais são as funções

recursivas.

4.2 Transitive Closure Uma das propriedades que se pode aplicar ao grafo é a propriedade transitiva:

● Se existe uma seta do nó A para o nó B e do nó B para o nó C então vamos fazer

com que exista também uma seta entre o nó A e o nó C.

Como os enunciados ‘A chama A directa ou indirectamente e ‘A é recursiva’ são

equivalentes a aplicação da regra anterior permite determinar automaticamente as

funções recursivas num determinado programa.

Após aplicação da propriedade transitiva teríamos o grafo seguinte:

P

Q

R T

S

Que mostra claramente que as funções P, Q, e R são funções recursivas.

4.3 Algoritmo Iterativo para Transitive Closure O seguinte algoritmo iterativo pode ser utilizado para aplicar a propriedade transitiva

a um Call Graph:

SET flag of Something changed TO TRUE;

WHILE something changed {

SET flag of Something changed TO FALSE;

FOR EACH node A IN Graph {

FOR EACH node B IN Descendants of Node A {

FOR EACH node C IN Descendants of Node B {

IF there is no arrow from Node A to Node C {

Add an arrow from Node A to Node C;

SET flag Something changed TO TRUE;

}

}

CADERNO DE EXERCÍCIOS E TRABALHOS PRÁTICOS PARA A DISCIPLINA DE COMPILADORES

©João M. P. Cardoso 23

}

}

}

4.4 Exercício 1 Com base no código Java (ver a secção seguinte) formado pelas classes Graph e

TransitiveClosure, escreva o método da classe Graph, designado por

transitiveClosure, que deve implementar o algoritmo apresentado anteriormente. De

seguida realize os passos seguintes:

a) Compile as classes java fazendo: javac *.java

b) Execute o programa fazendo: java TransitiveClosure

4.5 Código Java No código Java, o Call Graph original é representado por um array bidimensional,

designado por Edges, de 5×5 elementos booleanos. O grafo original poderá ser

representado pela seguinte matriz, em que os nós 0, 1, 2, 3, e 4 representam as funções

P, Q, R, S, e T, respectivamente:

J Edges[i][j]

0 1 2 3 4

0 False true false true false

1 False False true False true

2 true False False False False

3 False False False False False

I

4 False False False False False

Por exemplo, o valor true no elemento edges[0][1] indica que existe um laço entre o

nó 0 e o nó 1 com sentido de 0 para 1.

4.6 Classe Graph.java /**

This is the class of the calling graph structure.

It includes the transitive closure algorithm.

CADERNO DE EXERCÍCIOS E TRABALHOS PRÁTICOS PARA A DISCIPLINA DE COMPILADORES

©João M. P. Cardoso 24

the lines below tells the javadoc tool who is the author of this class and which is the

version (try: javadoc graph.java and then view the generated html files)

@author João M. P. Cardoso

@version v0.1

*/

// import from the API the class to use System.out.println

import java.io.*;

public class Graph {

// array which represents the edges between nodes

boolean[][] edges;

/**

add an edge between two nodes

@param source represents the source of the directed edge

@param sink represents the destination node of the directed edges

*/

public void addEdge(int source, int sink) {

edges[source][sink] = true;

}

/**

Print the edges between nodes.

*/

public void print() {

System.out.println("Graph edges: ");

int N = edges.length;

for(int i=0; i<N; i++)

for(int j=0; j<N; j++)

if(edges[i][j])

System.out.println(i+" -> "+j);

}

/**

Constructor of the graph.

The line below defines the argument numNodes to be used by the javadoc tool

@param numNodes the number of nodes in the graph

*/

public Graph(int numNodes) {

CADERNO DE EXERCÍCIOS E TRABALHOS PRÁTICOS PARA A DISCIPLINA DE COMPILADORES

©João M. P. Cardoso 25

// create the array with size = numNodes x numNodes

edges = new boolean[numNodes][numNodes];

// initiate all the elements equal to false: none edges

for(int i=0; i<numNodes; i++)

for(int j=0; j<numNodes; j++)

edges[i][j] = false;

}

/**

Show the functions of the calling graph which are recursive.

*/

public void printRecursiveFunctions() {

int numNodes = this.edges.length;

System.out.print("Recursive Functions in nodes: {");

for(int k=0; k<numNodes; k++)

// if there is an edge with source=sink then

// the node represents a recursive function

if(edges[k][k])

System.out.print(" "+k);

System.out.println(" }");

}

/**

Algorithm to perform the transitive closure on the original graph.

*/

public void transitiveClosure() {

// your code must be here!!!

}

}

4.7 Classe TransitiveClosure.java /**

Class where the calling graph is created and the transitive closure is applied.

*/

public class TransitiveClosure {

public static void main(String args[]) {

// create a calling graph with 5 nodes

CADERNO DE EXERCÍCIOS E TRABALHOS PRÁTICOS PARA A DISCIPLINA DE COMPILADORES

©João M. P. Cardoso 26

Graph callGraph = new Graph(5);

// add the 5 edges

callGraph.addEdge(0, 1);

callGraph.addEdge(1, 2);

callGraph.addEdge(2, 0);

callGraph.addEdge(0, 3);

callGraph.addEdge(1, 4);

// print the original graph

callGraph.print();

// call the transitive closure algorithm over the callGraph

callGraph.transitiveClosure();

// print the graph after applying the transitive closure algorithm

callGraph.print();

// show the nodes which represent recursive functions

callGraph.printRecursiveFunctions();

}

}

4.8 Trabalho para Casa Modifique o método main da classe TransitiveClosure de modo a determinar as

funções recursivas do seguinte Call Graph:

P

Q

R T

S

U

4.9 Créditos A aula é baseada no exemplo apresentado no livro:

Dick Grune, Henri E. Bal, Ceriel J. H. Jacobs, and Koen G. Langendoen, Modern

Compiler Design, John Wiley & Sons, Ltd, 2000.

CADERNO DE EXERCÍCIOS E TRABALHOS PRÁTICOS PARA A DISCIPLINA DE COMPILADORES

©João M. P. Cardoso 27

FICHA 5. Exercícios sobre Expressões Regulares e Autómatos Finitos

Duração: 3 horas

5.1 Exercícios com Expressões Regulares 1.1 Escreva expressões regulares para os seguintes exemplos:

(a) números binários representando números inteiros;

(b) [TPC] URLs da forma: http://www.ualg.pt (em que o campo intermédio

“ualg” é o único campo variável).

(c) [TPC] IPs da forma: 140.192.33.37 (considere que todos os IPs têm o mesmo

número de dígitos por cada campo).

(d) Representação binária de números inteiros sem utilizarem zeros supérfluos;

(e) Strings sobre o alfabeto {a, b, c} com número ímpar de a’s;

(f) [TPC] Strings sobre o alfabeto {a, b, c} em que o primeiro “a” precede o

primeiro “b”;

(g) Números binários cuja divisão por 4 dá resto zero;

(h) [TPC] Números binários maiores do que 101001;

(i) [TPC] A linguagem das constantes em vírgula flutuante (notação utilizada em

Java).

1.2 Acha que é possível escrever expressões regulares para os seguintes exemplos?

No caso de ser possível apresente uma solução:

(j) números binários que começam e acabam com o mesmo digito;

(k) [TPC] sequências de algarismos que formam capicuas;

5.2 Exercícios com Autómatos Finitos 2.1 Considere o autómato finito seguinte:

CADERNO DE EXERCÍCIOS E TRABALHOS PRÁTICOS PARA A DISCIPLINA DE COMPILADORES

©João M. P. Cardoso 28

0 1

0

0

2

0

3

1

1

1

(a) O autómato é determinista ou não determinista?

(b) Qual é o seu estado de início? Quais são os estados de aceitação (finais)?

(c) Este autómato aceita a sequência 110100? Qual a sequência de estados

visitados no reconhecimento desta String?

(d) Qual é a String mais pequena que o autómato aceita?

(e) Pode indicar a String maior que o autómato aceita?

(f) Por palavras, qual é a linguagem que o autómato aceita?

2.2 Desenhe os NFAs para as expressões regulares seguintes. Depois converta cada

um deles para DFA.

(a) [01]

(b) 1+

(c) (0 | 1) [0 | 1]+

2.3 Converta os NFAs seguintes para DFAs:

(a) 0, 1

x y z0 1

(b)

(c)

CADERNO DE EXERCÍCIOS E TRABALHOS PRÁTICOS PARA A DISCIPLINA DE COMPILADORES

©João M. P. Cardoso 29

2.4 Um analisador lexical baseado num interpretador de um DFA utiliza duas tabelas:

● edges: indexada pelo número do estado e símbolo de entrada, retorna o número do

estado, e

● final: indexada pelo número do estado, retorna 0 ou um número representativo da

acção a realizar.

Considerando a seguinte especificação:

(aba)+ ⇒ acção número 1

(a(b*)a) ⇒ acção número 2

(a | b) ⇒ acção número 3

Apresente as tabelas edges e final para o analisador lexical.

CADERNO DE EXERCÍCIOS E TRABALHOS PRÁTICOS PARA A DISCIPLINA DE COMPILADORES

©João M. P. Cardoso 30

FICHA 6. Implementação manual de um analisador lexical

Duração: 3 horas

Exercício sobre expressões regulares, NFAs, DFAs, e implementação manual de

analisadores lexicais baseados em DFAs.

6.1 Exercícios Pretende-se implementar um programa capaz de reconhecer números inteiros e

números reais introduzidos via teclado.

As expressões regulares que definem os números são:

Inteiro → [0-9]+

Real → [0-9]*.[0-9]+

(a) Desenhe o NFA que integre as duas expressões regulares anteriores;

(b) Transforme o NFA num DFA equivalente;

(c) Desenvolva um programa em Java que leia via teclado um conjunto de caracteres

e reconheça os dois tipos de números. Considere uma implementação do DFA

baseada na construção Java switch.

Pode utilizar o algoritmo seguinte, com a ressalva de que os caracteres não são lidos

de um ficheiro mas sim via teclado:

State = state_begin;

Char c = read char;

While c ≠ EOF do {

State = trans(State, c);

c = read next char;

}

if State in Set_of_final_states then

return “yes”;

else

return “no”;

CADERNO DE EXERCÍCIOS E TRABALHOS PRÁTICOS PARA A DISCIPLINA DE COMPILADORES

©João M. P. Cardoso 31

(d) O que teria de mudar nas expressões regulares para que todos os números com

algarismos à esquerda do ponto e sem algarismos à direita do ponto fossem

considerados números reais? Apresente os novos NFA e DFA.

(e) Elabore um programa Java que implemente o reconhecedor da alínea anterior,

utilizando uma ou mais tabelas para determinar as transições de estados.

(f) TPC (depois de ser utilizado o JavaCC): Implemente o supracitado programa

utilizando o gerador de analisadores léxicos e sintácticos JavaCC.

6.2 Notas O programa é constituído pela classe RecNumber e deve responder da seguinte forma

para os casos apresentados (o símbolo ↵ representa a tecla ENTER):

C:\java_code>java RecNumber↵

123↵

Integer number

C:\java_code>java RecNumber↵

12.3↵

Real number

C:\java_code>java RecNumber↵

.34↵

Real number

C:\java_code>java RecNumber↵

1. ↵

Unrecognized input

C:\java_code>java RecNumber

12a↵

Unrecognized input

CADERNO DE EXERCÍCIOS E TRABALHOS PRÁTICOS PARA A DISCIPLINA DE COMPILADORES

©João M. P. Cardoso 32

FICHA 7. Exercícios sobre Gramáticas

Duração: 3 horas

1. Especificar utilizando a representação BNF gramáticas correspondentes às

expressões regulares:

(a) [0-9]+

(b) [0-9]*

2. A gramática seguinte produz expressões constituídas de dígitos 0...9 separados

pelos sinais + ou -: Expr → Expr “+” Expr | Expr “-“ Expr | Digit

Digit → 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9

(a) Considere as expressões 9-5+2 e 3+7-4-2. Desenhe árvores sintácticas

concretas que derivem cada uma destas expressões;

(b) Pode derivar -5+2 da gramática especificada? Se achar que sim desenhe a

árvore sintáctica, caso contrário diga porque não.

(c) É a gramática ambígua? No caso de ser, tente encontrar uma gramática que

possa produzir a mesma linguagem (i.e., o mesmo conjunto de expressões) mas

que não seja ambígua.

(d) Diga se cada uma das gramáticas seguintes é equivalente (i.e., produz a mesma

linguagem) à gramática anterior? (i) Expr → Expr “+” Expr

Expr → Expr “-“ Expr

Expr → Digit

Digit → 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9

(ii) DIGIT = [0-9]

Expr → Expr “+” Expr

Expr → Expr “-“ Expr

Expr → DIGIT

(iii) Expr → (Expr “+” Expr) | (Expr “-“ Expr) | Digit

Digit → 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9

(iv) OP = “+“ | “-“

Expr → (Expr OP Expr) | Digit

Digit → 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9

3. Considere as gramáticas seguintes:

CADERNO DE EXERCÍCIOS E TRABALHOS PRÁTICOS PARA A DISCIPLINA DE COMPILADORES

©João M. P. Cardoso 33

DIGIT = [0-9]

(i) Aexpr → “-“ | DIGIT | Aexpr DIGIT

(ii) Bexpr → DIGIT Bexpr | Bexpr DIGIT | “.”

(a) Descreva as linguagens produzidas por cada uma das gramáticas.

(b) Ambas as gramáticas são recursivas. Apresente gramáticas equivalentes não-

recursivas utilizando a notação EBNF;

4. Considere a gramática seguinte: NUMLIT = [0-9]+

IDENT = [a-zA-Z][a-zA-Z0-9]*

BOOLLIT = “true” | “false”

Seq → Comando { “;” Seq }

Comando → “if” Expr “then” Comando [“else” Comando]

| IDENT “:=” Expr

| “{“ Seq “}”

Expr → NUMLINT | BOOLLIT | IDENT

(a) Desenhe a árvore sintáctica concreta para a sequência de código: y := 0;

if x then y := 1

else {y := 0}

(b) Desenhe uma possível AST para a árvore sintáctica concreta anterior.

5. Dada a gramática: NUM = [0-9]+

ID = [A-Za-Z][0-9A-Za-z]*

Expr → Expr “+” Term | Expr “–” Term | Term

Term → Term “*” Factor | Term “/” Factor | Factor

Factor → Primary “^” Factor | Primary

Primary → “-” Primary | Element

Element → “(“ Expr “)” | NUM | ID

Quais as árvores sintácticas para:

(a) 5-2*3

(b) y^3

6. Pretende-se especificar uma gramática que permita produzir expressões aritméticas

com inteiros atendendo a que:

CADERNO DE EXERCÍCIOS E TRABALHOS PRÁTICOS PARA A DISCIPLINA DE COMPILADORES

©João M. P. Cardoso 34

(i) as operações aritméticas que podem ser utilizadas são (por ordem de

precedência):

• ^

• /, *

• +, -

(ii) a gramática deve ser não-ambígua e deve respeitar a prioridade das

operações aritméticas;

(iii) deve poder produzir expressões aritméticas com parêntesis;

Especifique a gramática utilizando a notação EBNF.

CADERNO DE EXERCÍCIOS E TRABALHOS PRÁTICOS PARA A DISCIPLINA DE COMPILADORES

©João M. P. Cardoso 35

FICHA 8. Implementação manual de um analisador sintáctico descendente

Duração: 3 horas mais trabalho extraordinário

Trabalho de avaliação referente à implementação manual em Java de um analisador

sintáctico descendente.

Dada a gramática seguinte: S → E

E → T “+” E

E → T

T → “x”

(a) Implemente o analisador sintáctico descendente que implemente a gramática

considerada. Suponha que o analisador recebe via teclado a sequência de símbolos

terminais identificados por códigos. O analisador deve imprimir uma mensagem

dizendo se a sequência de símbolos terminais pertence ou não à gramática.

Nota: os códigos atribuídos a cada símbolo terminal são: “x” (código 1), “+”

(código 2). Assim, a expressão: x + x é introduzida no analisador como: 1 2 1.

(b) Implemente outra versão do analisador de modo a que este construa a árvore

sintáctica concreta. No final da construção da árvore sintáctica o analisador deve

imprimir a estrutura da árvore.

CADERNO DE EXERCÍCIOS E TRABALHOS PRÁTICOS PARA A DISCIPLINA DE COMPILADORES

©João M. P. Cardoso 36

FICHA 9. Implementação de Analisadores Gramaticais Utilizando o JavaCC (1ª parte)

Duração: 3 horas

Utilização do JavaCC [3] para gerar parsers. Esta aula tem como objectivo o início da

aprendizagem do gerador de analisadores sintácticos JavaCC. Esta tarefa requer a

aplicação dos conhecimentos até agora adquiridos na disciplina, principalmente no

que respeita aos conceitos sobre análise sintáctica e sobre analisadores sintácticos

descendentes.

O JavaCC utiliza um ficheiro com extensão .jj onde se encontra descrita a gramática

para o parser e gera uma classe Java com o nome do parser e outras classes Java de

suporte4.

nome.jj Parser.java ParserTojenManager.java ParserConstants.java Token.java ParseError.java …

JavaCC

Figura 3. Entradas e saídas do JavaCC supondo que foi especificado “Parser”

como nome do parser no ficheiro nome.jj.

9.1 Exemplo Suponhamos que desejamos implementar um analisador sintáctico que reconheça

expressões aritméticas com apenas um número inteiro positivo ou com uma adição ou

uma subtracção de dois números inteiros positivos (por exemplo: 2+3, 1-4, 5, etc.). De

seguida apresenta-se uma gramática em EBNF com a especificação do símbolo

terminal utilizando uma expressão regular:

4 À medida que forem percebendo o JavaCC vão também tomando conhecimento do significado das

classes de suporte.

CADERNO DE EXERCÍCIOS E TRABALHOS PRÁTICOS PARA A DISCIPLINA DE COMPILADORES

©João M. P. Cardoso 37

INTEGER = [0-9]+

Aritm → Integer [(“+” | “-“) Integer]

Para implementar um parser que implemente esta gramática utilizando o JavaCC

temos de criar um ficheiro com extensão .jj (vamos chamar-lhe: Exemplo.jj).

O ficheiro é constituído por:

1. Lista de opções (opcional): é onde pode ser definido o nível de lookahead, por

exemplo.

2. Unidade de compilação Java (PARSER_BEGIN(nome) ...

PARSER_END(nome))

3. Lista de produções da gramática (as produções aceitam os símbolos +, *, e ?

com o mesmo significado, aquando da utilização em expressões regulares)

Por exemplo para a gramática anterior, poderemos criar o ficheiro Exemplo.jj

seguinte:

PARSER_BEGIN(Exemplo)

// código Java que invoca o parser

public class Exemplo {

public static void main(String args[]) throws ParseException {

// criação do objecto utilizando o constructor com argumento para

// ler do standard input (teclado)

Exemplo parser = new Exemplo(System.in);

Exemplo.Aritm();

}

}

PARSER_END(Exemplo)

// símbolos que não devem ser considerados na análise

SKIP :

{

“ “ | “\t” | “\r”

}

// definição dos tokens (símbolos terminais)

TOKEN :

{

< INTEGER : ([“0” – “9”])+ >

| < EOL : “\n” >

CADERNO DE EXERCÍCIOS E TRABALHOS PRÁTICOS PARA A DISCIPLINA DE COMPILADORES

©João M. P. Cardoso 38

}

// definição da produção

void Aritm() : {}

{

<INTEGER> ( (“+” | “-“) <INTEGER> )? (<EOF> | <EOL>)

}

Em seguida deve fazer-se: Javacc Exemplo.jj

Compilar o código Java gerado: Javac *.java

Executar o analisador sintáctico: Java Exemplo

Poderemos associar às funções referentes aos símbolos não-terminais pedaços de

código Java. Por exemplo, as modificações apresentadas em seguida permitem

escrever no ecrã mensagens a indicar os números que são lidos pelo parser:

void Aritm : {Token t1, t2;}

{

t1=<INTEGER> {

System.out.println(“Integer = “+t1.image);

}

( (“+” | “-“) t2=<INTEGER> {

System.out.println(“Integer = “+t2.image);

}

)? (<EOF> | <EOL>)

}

Por cada símbolo terminal INTEGER, foi inserida uma linha de código Java que

imprime no ecrã o valor do token lido (o método image da classe Token retorna uma

String representativa do valor do token5)

Insira estas modificações no ficheiro Exemplo.jj e volte a repetir o processo até à

execução do parser. Verifique o que acontece.

5 Outros métodos serão apresentados posteriormente.

CADERNO DE EXERCÍCIOS E TRABALHOS PRÁTICOS PARA A DISCIPLINA DE COMPILADORES

©João M. P. Cardoso 39

Como poderíamos adicionar também a escrita do sinal (+ ou -) lido?

9.2 Exercícios 1. Considere a ficha Nº 5. No exercício pretendia-se implementar um programa para

reconhecer números inteiros e números reais introduzidos via teclado. As expressões

regulares que definem os números são: Inteiro → [0-9]+

Real → [0-9]*.[0-9]+

Agora, pretende-se implementar o programa utilizando o gerador de analisadores

léxicos e sintácticos: JavaCC.

Antes de iniciar as implementações referentes aos exercícios apresentados de seguida,

deve ler com atenção o tutorial [3]. Tenha atenção aos símbolos que devem ser

utilizados nas produções da gramática no JavaCC. Note que o JavaCC requer a

definição de gramáticas não-ambíguas de modo a poder fornecer apenas uma árvore

sintáctica concreta para uma dada frase.

2. Implemente utilizando o JavaCC um analisador sintáctico que permita reconhecer

expressões aritméticas com as operações *, +, e -. O analisador deve também aceitar a

utilização de parêntesis;

3. Com base no analisador anterior, adicione o código Java necessário para

implementar uma calculadora de expressões aceites pela gramática.

9.3 Referências de Apoio 3. JavaCC: https://javacc.dev.java.net/

4. Oliver Enseling, “Build your own languages with JavaCC”, Copyright © 2003

JavaWorld.com, an IDG company, http://www.javaworld.com/javaworld/jw-

12-2000/jw-1229-cooltools_p.html (cópia local em pdf:

http://w3.ualg.pt/~jmcardo/ensino/PS2003/AulaTP7/jw-1229-cooltools.pdf)

CADERNO DE EXERCÍCIOS E TRABALHOS PRÁTICOS PARA A DISCIPLINA DE COMPILADORES

©João M. P. Cardoso 40

FICHA 10. Implementação de Analisadores Gramaticais Utilizando o JavaCC (2ª parte)

Duração: 3 horas

Implementação de uma calculadora utilizando o JavaCC sem a criação da árvore

sintáctica.

10.1 Exercício Considere a gramática seguinte:

EOL=”\n”

INTEGER=[0-9]+

Start → Expr EOL

Expr → Term {(“+” Expr) | (“-“ Expr)}

Expr → “(“ Expr “)”

Term → Unary {(“*” Term) | (“/” Term)}

Term → “(“ Expr “)”

Unary → “-“ INTEGER

Unary → INTEGER

a) É a gramática ambígua?

b) A gramática respeita as precedências (prioridades) das operações aritméticas?

c) Acha que a gramática tem recursividade à esquerda?

d) Para que não seja necessário backtracking qual o nível mínimo de lookahead

necessário?

e) Implemente esta gramática utilizando o JavaCC. Utilize o nível de lookahead que

achar necessário.

f) Modifique o ficheiro do JavaCC da gramática de modo a calcular o valor das

expressões (funcionamento como calculadora).

CADERNO DE EXERCÍCIOS E TRABALHOS PRÁTICOS PARA A DISCIPLINA DE COMPILADORES

©João M. P. Cardoso 41

FICHA 11. Implementação de Analisadores Gramaticais Utilizando o JavaCC (3ª parte)

Duração: 3 horas

Esta ficha pretende que aprendam a utilizar o JJTree em conjunto com o JavaCC [3]

para gerar parsers e árvores sintácticas.

Após a utilização do JavaCC nas duas últimas aulas, é agora tempo de aprenderem

como se pode gerar automaticamente a árvore sintáctica. Para tal utilizaremos o

JJTree (é uma ferramenta integrada no pacote de software JavaCC). O JJTree é uma

ferramenta de pré-processamento que permite gerar um ficheiro para o JavaCC que,

para além da descrição da gramática, integra código Java para a geração da árvore

sintáctica. O ficheiro de entrada do JJTree é um ficheiro que especifica a gramática do

mesmo modo que para o JavaCC e que adicionalmente inclui directivas para a geração

dos nós da árvore (é utilizado jjt como extensão do ficheiro origem).

nome.jjt JJTree

nome.jj Node.java …

Figura 4. Entradas e saídas do JJTree.

11.1 Exemplo Vamos considerar a gramática da FICHA 8 e vamos supor que pretendemos realizar

um programa que calcule as mesmas expressões, desta vez gerando a árvore sintáctica

e efectuando os cálculos sobre a árvore.

Para gerar a árvore sintáctica vamos alterar a extensão do ficheiro da gramática

original para jjt e vamos adicionar as directivas que indicam ao JJTree para criar a

árvore. O ficheiro apresentado de seguida contém as modificações introduzidas (é

utilizado o método dump() para imprimir no ecrã a árvore gerada por cada expressão

introduzida).

CADERNO DE EXERCÍCIOS E TRABALHOS PRÁTICOS PARA A DISCIPLINA DE COMPILADORES

©João M. P. Cardoso 42

Ficheiro Calculator.jjt:

options

{

LOOKAHEAD=2;

}

PARSER_BEGIN(Calculator)

public class Calculator

{

public static void main(String args[]) throws ParseException {

Calculator parser = new Calculator(System.in);

int i=0;

while (true) {

// the function will return the root node of the Syntax Tree

SimpleNode rootNode = parser.parseOneLine();

//print the Syntax Tree

rootNode.dump("Syntax Tree "+(i++)+": ");

}

}

}

PARSER_END(Calculator)

SKIP :

{

" "

| "\r"

| "\t"

}

TOKEN:

{

< INTEGER: (["0"-"9"])+ >

| < EOL: "\n" >

}

SimpleNode parseOneLine() #Root : {} // diz ao JJTree para criar o nó Root

{

expr() <EOL> {return jjtThis;} // retorna o nó da árvore construído neste

procedimento

| <EOL>

CADERNO DE EXERCÍCIOS E TRABALHOS PRÁTICOS PARA A DISCIPLINA DE COMPILADORES

©João M. P. Cardoso 43

| <EOF> { System.exit(-1); }

}

void expr():

{

}

{

term()

(

"+" expr() #Add(2) // diz ao JJTree para criar um nó Add que tem dois filhos

| "-" expr() #Sub(2) // diz ao JJTree para criar um nó Sub que tem dois filhos

)*

| "(" expr() ")"

}

void term(): {}

{

unary()

(

"*" term() #Mult(2) // diz ao JJTree para criar um nó Mult que tem dois filhos

| "/" term() #Div(2) // diz ao JJTree para criar um nó Div que tem dois filhos

)*

| "(" expr() ")"

}

void unary(): {}

{

"-" <INTEGER>

| <INTEGER>

}

Em seguida deve fazer-se: jjtree Calculator.jjt

Gerar o código Java com o JavaCC: javacc Calculator.jj

Compilar o código Java gerado: Javac *.java

Executar o analisador sintáctico: Java Calculator

CADERNO DE EXERCÍCIOS E TRABALHOS PRÁTICOS PARA A DISCIPLINA DE COMPILADORES

©João M. P. Cardoso 44

Verifique de seguida as árvores geradas para um conjunto de expressões introduzidas.

Contudo, a criação da árvore sintáctica não é suficiente para nós. Primeiro, a árvore

gerada não armazena os valores dos números inteiros lidos. Segundo, é necessário

programar o procedimento que atravessa a árvore e calcula o valor da expressão

aritmética definida pela árvore.

Para resolvermos o primeiro obstáculo teremos que fazer algumas alterações. O

JJTree vai gerar uma classe Java para o SimpleNode (nome da classe definido como

retorno do procedimento: parseOneLine()). Esta classe, é utilizada para representar os

nós da árvore sintáctica, e pode ser personalizada para implementar as

funcionalidades necessárias. A classe inclui os seguintes métodos:

Alguns métodos da classe representativa dos nós da árvore sintáctica:

Descrição:

public Node jjtGetParent() Retorna o nó pai do nó actual public Node jjtGetChild(int i) Retorna o nó filho nº i public int jjtGetNumChildren() Retorna o número de filhos do nó Public void dump() Escreve no ecrã a árvore sintáctica

a partir do nó

Depois da classe SimpleNode ser gerada pela primeira vez pelo JJTree podemos

adicionar-lhe métodos ou campos. Neste momento interessa-nos acrescentar à classe

um campo que permita armazenar o valor do inteiro (no caso das folhas da árvore).

Foram ainda adicionados dois métodos que permitem aceder ao campo (atribuir e

retornar o seu valor). A classe é apresentada de seguida com as alterações efectuadas

(a negrito).

/* Generated By:JJTree: Do not edit this line. SimpleNode.java */

public class SimpleNode implements Node {

protected Node parent;

protected Node[] children;

protected int id;

protected Calculator parser;

int value;

public void setValue(int a) {

this.value = a;

}

CADERNO DE EXERCÍCIOS E TRABALHOS PRÁTICOS PARA A DISCIPLINA DE COMPILADORES

©João M. P. Cardoso 45

public int getValue() {

return this.value;

}

public SimpleNode(int i) {

id = i;

}

public SimpleNode(Calculator p, int i) {

this(i);

parser = p;

}

public void jjtOpen() {

}

public void jjtClose() {

}

public void jjtSetParent(Node n) { parent = n; }

public Node jjtGetParent() { return parent; }

public void jjtAddChild(Node n, int i) {

if (children == null) {

children = new Node[i + 1];

} else if (i >= children.length) {

Node c[] = new Node[i + 1];

System.arraycopy(children, 0, c, 0, children.length);

children = c;

}

children[i] = n;

}

public Node jjtGetChild(int i) {

return children[i];

}

public int jjtGetNumChildren() {

return (children == null) ? 0 : children.length;

}

/* You can override these two methods in subclasses of SimpleNode to

customize the way the node appears when the tree is dumped. If

your output uses more than one line you should override

toString(String), otherwise overriding toString() is probably all

you need to do. */

public String toString() { return CalculatorTreeConstants.jjtNodeName[id]; }

public String toString(String prefix) { return prefix + toString(); }

CADERNO DE EXERCÍCIOS E TRABALHOS PRÁTICOS PARA A DISCIPLINA DE COMPILADORES

©João M. P. Cardoso 46

/* Override this method if you want to customize how the node dumps

out its children. */

public void dump(String prefix) {

System.out.print(toString(prefix));

if (children != null) {

for (int i = 0; i < children.length; ++i) {

SimpleNode n = (SimpleNode)children[i];

if (n != null) {

System.out.println();

n.dump(prefix + " ");

if(n.id == CalculatorTreeConstants.JJTUNARY)

System.out.println(" ["+n.getValue()+"]");

}

}

}

}

}

O novo ficheiro que descreve a gramática, que indica ao JJTree para gerar a árvore

sintáctica e que calcula o valor das expressões aritméticas introduzidas é apresentado

de seguida. Durante o atravessamento da árvore é importante que se possa verificar de

que tipo é um determinado nó. Tal pode ser feito utilizando o campo id de cada nó da

árvore (ver nos exemplos node.id). Para cada tido de nó da árvore o JJTree gera um

ficheiro em que são especificados os identificadores atribuídos (ver ficheiro

CalculatorTreeConstants.java). O tipo de nós corresponde aos procedimentos da

gramática e/ou aos nós especificados nas directivas para o JJTree.

Novo ficheiro Calculator.jjt:

options

{

LOOKAHEAD=2;

}

PARSER_BEGIN(Calculator)

public class Calculator {

public static void main(String args[]) throws ParseException {

Calculator parser = new Calculator(System.in);

int i=0;

while (true) {

// the function will return the root node of the Syntax Tree

CADERNO DE EXERCÍCIOS E TRABALHOS PRÁTICOS PARA A DISCIPLINA DE COMPILADORES

©João M. P. Cardoso 47

SimpleNode rootNode = parser.parseOneLine();

//print the Syntax Tree

rootNode.dump("Syntax Tree "+(i++)+": ");

// call the evaluation method with the ROOT node

System.out.println("Result: "+parser.eval(rootNode));

}

}

/*

Método recursivo que realiza o cálculo da expressão percorrendo a árvore sintáctica.

*/

int eval(SimpleNode node) {

// each node contains an id field identifying its type.

int id = node.id;

//System.out.println("VALUE "+node.getValue()+" "+id);

if(id == CalculatorTreeConstants.JJTUNARY) // node with integer value

return node.getValue();

else if(node.jjtGetNumChildren() == 1) // only one child

return this.eval((SimpleNode) node.jjtGetChild(0));

// nodes with two childs

SimpleNode lhs = (SimpleNode) node.jjtGetChild(0); //left child

SimpleNode rhs = (SimpleNode) node.jjtGetChild(1); // right child

switch(id) {

case CalculatorTreeConstants.JJTADD : return eval( lhs ) + eval( rhs );

case CalculatorTreeConstants.JJTSUB : return eval( lhs ) - eval( rhs );

case CalculatorTreeConstants.JJTMULT : return eval( lhs ) * eval( rhs );

case CalculatorTreeConstants.JJTDIV : return eval( lhs ) / eval( rhs );

default : // abort

System.out.println("Operador ilegal!");

System.exit(1);

}

return 0;

}

}

PARSER_END(Calculator)

CADERNO DE EXERCÍCIOS E TRABALHOS PRÁTICOS PARA A DISCIPLINA DE COMPILADORES

©João M. P. Cardoso 48

SKIP :

{

" "

| "\r"

| "\t"

}

TOKEN:

{

< INTEGER: (["0"-"9"])+ >

| < EOL: "\n" >

}

SimpleNode parseOneLine() #Root : {} // diz ao JJTree para criar o nó Root

{

expr() <EOL> {return jjtThis;} // retorna o nó da árvore construído neste

procedimento

| <EOL>

| <EOF> { System.exit(-1); }

}

void expr(): {}

{

term()

(

"+" expr() #Add(2) // diz ao JJTree para criar um nó Add que tem dois filhos

| "-" expr() #Sub(2) // diz ao JJTree para criar um nó Sub que tem dois filhos

)*

| "(" expr() ")"

}

void term(): {}

{

unary()

(

"*" term() #Mult(2) // diz ao JJTree para criar um nó Mult que tem dois filhos

| "/" term() #Div(2) // diz ao JJTree para criar um nó Div que tem dois filhos

)*

| "(" expr() ")"

}

void unary(): {Token t;}

CADERNO DE EXERCÍCIOS E TRABALHOS PRÁTICOS PARA A DISCIPLINA DE COMPILADORES

©João M. P. Cardoso 49

{

"-" (t=<INTEGER> {

// diz ao JJTree para colocar o valor do inteiro num dos campos deste nó

// (para tal é necessário adicionar à classe Java SimpleNode o método setValue() e

// o campo que armazena o valor inteiro

jjtThis.setValue(-Integer.parseInt(t.image)6); })

| (t=<INTEGER> {

// diz ao JJTree para colocar o valor do inteiro num dos campos deste nó

// (para tal é necessário adicionar à classe Java SimpleNode o método setValue() e

// o campo que armazena o valor inteiro

jjtThis.setValue(Integer.parseInt(t.image));

})

}

Acha que a árvore sintáctica gerada pelos ficheiros jjt apresentados é uma árvore

sintáctica concreta ou uma AST (árvore sintáctica abstracta)?

Para que não seja gerado um nó por cada procedimento na gramática utiliza-se a

directiva #void a seguir ao nome do procedimento para o qual não se quer nó na

árvore. Este método permite gerar as ASTs automaticamente sem por isso ser

necessário transformar a árvore concreta numa AST. O ficheiro seguinte apresenta

uma versão da calculadora em que são geradas ASTs. Verifique a colocação da

directiva a indicar os símbolos não-terminais para os quais não será representado

qualquer nó na árvore.

Novo ficheiro Calculator.jjt:

options

{

LOOKAHEAD=2;

}

PARSER_BEGIN(Calculator)

public class Calculator

6 Como o valor de um Token é guardado como String, é necessário converter a representação em String

para inteiro. Tal pode ser conseguido utilizando métodos existentes nos API do Java. Por exemplo,

Integer.parseInt(t.image) retorna o inteiro representado pela String t.image.

CADERNO DE EXERCÍCIOS E TRABALHOS PRÁTICOS PARA A DISCIPLINA DE COMPILADORES

©João M. P. Cardoso 50

{

public static void main(String args[]) throws ParseException {

Calculator parser = new Calculator(System.in);

int i=0;

while (true) {

// the function will return the root node of the Syntax Tree

SimpleNode rootNode = parser.parseOneLine();

//print the Syntax Tree

rootNode.dump("Syntax Tree "+(i++)+": ");

// call the evaluation method with the ROOT node

System.out.println("Result: "+parser.eval(rootNode));

}

}

int eval(SimpleNode node) {

// each node contains an id field identifying its type.

// we switch on these.

// enum values such as JJTUNARY come from the interface file

// SimpleParserTreeConstants, which SimpleParser implements.

// This interface file is one of several auxilliary Java sources

// generated by JJTree. JavaCC contributes several others.

int id = node.id;

//System.out.println("VALUE "+node.getValue()+" "+id);

if(id == CalculatorTreeConstants.JJTUNARY) // node with integer value

return node.getValue();

else if(node.jjtGetNumChildren() == 1) // only one child

return this.eval((SimpleNode) node.jjtGetChild(0));

SimpleNode lhs = (SimpleNode) node.jjtGetChild(0); //left child

SimpleNode rhs = (SimpleNode) node.jjtGetChild(1); // right child

switch(id) {

case CalculatorTreeConstants.JJTADD : return eval( lhs ) + eval( rhs );

case CalculatorTreeConstants.JJTSUB : return eval( lhs ) - eval( rhs );

case CalculatorTreeConstants.JJTMULT : return eval( lhs ) * eval( rhs );

case CalculatorTreeConstants.JJTDIV : return eval( lhs ) / eval( rhs );

default : // abort

System.out.println("Operador ilegal!");

CADERNO DE EXERCÍCIOS E TRABALHOS PRÁTICOS PARA A DISCIPLINA DE COMPILADORES

©João M. P. Cardoso 51

System.exit(1);

}

return 0;

}

}

PARSER_END(Calculator)

SKIP :

{

" "

| "\r"

| "\t"

}

TOKEN:

{

< INTEGER: (["0"-"9"])+ >

| < EOL: "\n" >

}

SimpleNode parseOneLine() #Root : {}

{

expr() <EOL> {return jjtThis;}

| <EOL>

| <EOF> { System.exit(-1); }

}

void expr() #void : {}

{

term()

(

"+" expr() #Add(2)

| "-" expr() #Sub(2)

)*

| "(" expr() ")"

}

void term() #void : {}

{

unary()

(

"*" term() #Mult(2)

CADERNO DE EXERCÍCIOS E TRABALHOS PRÁTICOS PARA A DISCIPLINA DE COMPILADORES

©João M. P. Cardoso 52

| "/" term() #Div(2)

)*

| "(" expr() ")"

}

void unary() : {Token t;}

{

"-" (t=<INTEGER> {

jjtThis.setValue(-Integer.parseInt(t.image));

})

| (t=<INTEGER> {

jjtThis.setValue(Integer.parseInt(t.image));

})

}

11.2 Referências de Apoio 5. JavaCC: http://www.experimentalstuff.com/Technologies/JavaCC/index.html

6. Documento de introdução ao JJTree incluído na distribuição da ferramenta:

http://w3.ualg.pt/~jmcardo/ensino/PS2003/jjtree.intro

CADERNO DE EXERCÍCIOS E TRABALHOS PRÁTICOS PARA A DISCIPLINA DE COMPILADORES

©João M. P. Cardoso 53

FICHA 12. Trabalho Final

Duração: 3 aulas de 3 horas mais trabalho extraordinário

Esta ficha apresenta o enunciado do último trabalho prático que será realizado com o

acompanhamento proporcionado por 3 aulas práticas e com esforços extra aulas.

12.1 Exercício Pretende-se implementar um interpretador ou um compilador para uma linguagem de

programação muito simples, designada por ualg. Nesta linguagem todas as variáveis e

constantes são do tipo inteiro com sinal (representado em 32 bits). A linguagem aceita

expressões aritméticas e operações relacionais, construções condicionais do tipo if e

if-else, e construções while. Os programas na linguagem ualg são constituídos por

uma única função (a função tem de retornar sempre uma variável e pode ter ou não

argumentos), não existem chamadas a funções, e os argumentos são sempre passados

por valor. A linguagem ualg não deve diferenciar minúsculas de maiúsculas.

São apresentados na Figura 5 exemplos e algumas restrições da linguagem. A

gramática de uma linguagem similar encontra-se representada em EBNF na secção 2.

Tenha contudo em atenção que a gramática apresentada só permite o encadeamento

de construções condicionais e cíclicas do tipo de if-while ou while-if. Deve por isso

proceder às alteraçoes necessárias que permitam ao compilador/interpretador não ter

restrições no encademaneto deste tipo de construtores. A Figura 6 apresenta exemplos

de programas em ualg.

Tipo Instruções e operações

Exemplo

Operações aritméticas e lógicas *, /, +, -, <<, >>, &, |, ^

a=2*b; /* RHS com apenas uma operação */

Relacionais (Apenas utilizados como teste nas construções if, if-else, while)

==, !=, >, <, >=, <=

If(a >= b) { ... } /* Apenas utilizados no teste da condição pelas construções if, if-else, e while */

Construções condicionais If, if-else If(a == b) { c=2; }

Construções cíclicas while While(a != 2) { a = a-1; }

Atribuição = A=2; A=b;

CADERNO DE EXERCÍCIOS E TRABALHOS PRÁTICOS PARA A DISCIPLINA DE COMPILADORES

©João M. P. Cardoso 54

Imprimir a string e o valor da variável no ecrã (com mudança de linha)

print Print(“a: ”, a);

Ler inteiro do ecrã read A=read;

Figura 5. Operações e construções suportadas pela linguagem.

Pretende-se que implemente um dos seguintes programas:

1. Interpretador para esta linguagem;

2. Compilador para esta linguagem que gere uma rotina em código MIPS

(utilização do simulador spim [4] para validar os resultados) [5];

Count(word) { cont=0; n = 0; While(n < 32) { teste = word & 1; If(teste == 1) { cont = cont +1; } word = word >> 1; n = n + 1; } return cont; }

sqrt(vsqn) { vsq=vsqn; asq=0; a=0; tvsq=0; I=0; While(I < 6) { nasq1 = asq + a; nasq2 = nasq1 << 2; nasq = nasq2 | 1; sa = a << 1; tvsq1 = tvsq << 2; Vsq1 = vsq >> 10; Vsq2 = vsq1 & 3; tvsq = tvsq1 | vsq2; vsq = vsq << 2; if(nasq <= tvsq) { a = sa | 1; asq = nasq; } else { a = sa ; asq = asq << 2 ; } I = i+1; } return a; }

Max() { A=Read; B=Read; Max=a; If(max < b) { Max = b; } Print(“max: “, max); Return max; } (a) Max(a, b) { Max=a; If(max < b) { Max = b; } Return max; } (b)

Conta nº de bits com valor “1” no conteúdo da variável passada como argumento.

Calcula a raiz quadrada de um número inteiro sem sinal representado por 12 bits. Retorna o valor inteiro da raiz quadrada representado por 6 bits.

Dois programas para determinar o valor máximo de duas variáveis: (a) versão com leitura/escrita de dados; (b) versão sem leitura/escrita de dados.

Figura 6.Exemplo de programas na linguagem ualg.

Compilador: Para executar o compilador deve ser introduzido o comando: java ualg

[-o] programa.ualg. A opção –o deve ser considerada pelos alunos que consigam

optimizar a geração do código:

CADERNO DE EXERCÍCIOS E TRABALHOS PRÁTICOS PARA A DISCIPLINA DE COMPILADORES

©João M. P. Cardoso 55

● Utilização do menor número possível de espaço em memória para armazenar

variáveis (atribuir o maior número possível de variáveis a registos do

microprocessador);

Interpretador: Para executar o interpretador deve introduzido o comando: java ualg

[-p|-b] programa.ualg. As opções –p e –b devem ser incorporadas para permitir a

interpretação linha-a-linha (opção: –p) e a interpretação até encontrar uma instrução

write (opção: –b). As funções com argumentos devem originar que o interpretador

solicite ao utilizador a inserção via teclado do valor para cada um dos argumentos. O

interpretador deve escrever no ecrã o valor retornado pela função.

A nota final terá em conta os pesos dados a cada uma das tarefas a realizar. A

contribuição de cada tarefa na nota global é apresentada na Figura 7.

Cada grupo deve realizar uma apresentação do trabalho. No inicio da apresentação

deverá ser entregue um pequeno relatório a descrever as opções tidas durante o

desenvolvimento e as restrições do compilador. O relatório deve incluir em anexo o

ficheiro de entrada do JavaCC e do JJTree e a documentação do programa que deve

ser gerada automaticamente pelo javadoc.

Tarefa Percentagem na nota

Analisador sintáctico sem geração da árvore sintáctica (inclui especificação da gramática no JavaCC)

20%

Criação da árvore sintáctica anotada utilizando o JJTree (para validação o compilador/interpretador deve imprimir no ecrã a árvore sintáctica e as anotações)

20%

Geração de código/interpretação para/de sequências de instruções com expressões aritméticas. Incluir, no caso do interpretador, a leitura de valores pelo teclado dos argumentos da função e, a escrita do valor retornado pela função e a implementação das instruções read e print.

10%

Geração de código/interpretação para/de estruturas condicionais (if, if-else)

10%

Geração de código/interpretação para/de estruturas cíclicas (while) 10%Optimizações ao nível do código assembly gerado (opção do compilador –o) ou tipos de interpretação (opções –b e –p)

10%

Legibilidade e estrutura do código, apresentação do trabalho, etc. 20%Melhor compilador e melhor interpretador (o melhor compilador poderá vir a ser utilizado na disciplina de Arquitectura de Computadores)

Bónus: 10%

Figura 7. Percentagem na nota final no trabalho de cada tarefa.

CADERNO DE EXERCÍCIOS E TRABALHOS PRÁTICOS PARA A DISCIPLINA DE COMPILADORES

©João M. P. Cardoso 56

12.2 Gramática Inicial da Linguagem De seguida é apresentada a gramática inicial da linguagem (esta gramática foi escrita

de forma a ser o mais intuitiva possível e o mais próximo possível da gramática que

deverá ser especificada no JavaCC):

IDENTIFIER = [a-z][a-z0-9]*

LITERAL = [0-9]+

STRING = “\““ [a-zA-Z0-9”:”” “]+ “\““

// os operadores têm o mesmo significado e precedência do que na

linguagem Java

RELA_OP = “>“ | “<“ | “==“ | “!=“ | “>=” | “<=”

OTHER_OP = “*“ | “/“ | “&“ | “|“ | “<<” | “>>” | “^”

ADD_SUB = “+“ | “-“

LPAR = “(“

RPAR = “)”

VIRG = “,”

PVIRG = “;”

LCHAVETA = “{“

RCHAVETA = “}“

ASSIGN = “=“

WHILE = “while”

IF = “if”

ELSE = “else”

RETURN = “return”

PRINT= ”print”

READ= ”read”

Start → IDENTIFIER LPAR [Varlist] RPAR LCHAVETA Stmtlst RETURN IDENTIFIER

PVIRG RCHAVETA

Varlist → IDENTIFIER {VIRG IDENTIFIER}

Stmtlst → {Stmt}

Stmt → Expr1 | Expr3 | Expr4 | Print

Expr1 → IDENTIFIER ASSIGN Rhs PVIRG

Rhs → Term [(ADD_SUB | OTHER_OP) Term]

Term → IDENTIFIER | ([ADD_SUB] LITERAL) | READ

Expr3 → WHILE Exprtest LCHAVETA Stmtsimple RCHAVETA

Expr4 → IF Exprtest LCHAVETA Stmtsimple RCHAVETA

CADERNO DE EXERCÍCIOS E TRABALHOS PRÁTICOS PARA A DISCIPLINA DE COMPILADORES

©João M. P. Cardoso 57

Expr4 → IF Exprtest LCHAVETA Stmtsimple RCHAVETA ELSE LCHAVETA

Stmtsimple RCHAVETA

Print → PRINT LPAR STRING VIRG IDENTIFIER RPAR PVIRG

Exprtest → LPAR IDENTIFIER RELA_OP (IDENTIFIER | ([ADD_SUB] LITERAL)) RPAR

Stmtsimple → {Expr1 | Expr4 | Expr3 | Print}

12.3 Algumas Dicas Antes de começarem a especificar a gramática no JavaCC devem verificar se a

gramática é não-ambígua e não tem recursividade à esquerda. No caso de não se

verificar alguma destas propriedades, a gramática deve ser modificada e só depois é

que deve ser especificada no JavaCC.

A linguagem ualg não deve diferenciar minúsculas de maiúsculas. Para isso devem

atribuir o valor verdadeiro à opção IGNORE_CASE do JavaCC:

options {

IGNORE_CASE=true;

}

Lembrem-se que cada símbolo não-terminal da gramática pode ser um procedimento

no ficheiro descritivo da gramática.

Durante a fase de validação do analisador gramatical podem utilizar uma makefile

que execute o compilador com vários programas teste (programas em ualg que

permitam verificar se determinadas construções da linguagem estão a ser aceites pelo

analisador gramatical). Esta abordagem permite automatizar o teste da gramática e é

por isso útil para testar o analisador sempre que forem feitas alterações na descrição

JavaCC da gramática.

12.4 Documentação e Ferramentas de Suporte 1. JavaCC: Java Compiler Compiler™ (JavaCC) - The Java Parser Generator

JavaCC: https://javacc.dev.java.net/

2. Oliver Enseling, “Build your own languages with JavaCC”, Copyright © 2003

JavaWorld.com, an IDG company, http://www.javaworld.com/javaworld/jw-12-

2000/jw-1229-cooltools_p.html

3. Documento de introdução ao JJTree incluído na distribuição da ferramenta

[FICHA Nº 9].

CADERNO DE EXERCÍCIOS E TRABALHOS PRÁTICOS PARA A DISCIPLINA DE COMPILADORES

©João M. P. Cardoso 58

4. Spim: http://www.cs.wisc.edu/~larus/spim.html

5. Rever as primeiras 3 aulas teóricas (“Da linguagem alto-nível ao código

assembly”).

CADERNO DE EXERCÍCIOS E TRABALHOS PRÁTICOS PARA A DISCIPLINA DE COMPILADORES

©João M. P. Cardoso 59

Anexo B: Enunciado de um Exame

Exame de época de recurso da disciplina de Programação de

Sistemas

Ano lectivo de 2002/2003

2º Semestre

Licenciatura em Engenharia de Sistemas e Computação

Licenciatura em Ensino de Informática

Licenciatura em Informática, ramo de Gestão

Duração: 2 horas + meia hora de tolerância (total: 2H30)

1. [2 valores] Converta o NFA (autómato finito não-determinista) seguinte num

DFA (autómato finito determinista) utilizando o algoritmo de conversão que

aprendeu.

0 1

ε

4 3z

y

2 x

z x

5 a

6

a

a

ε

a

y

2. [1,5 valores] Escreva para cada estado de aceitação do autómato da figura anterior

a expressão regular que represente as palavras da linguagem aceites pelo mesmo.

3. [1,5 valores] Desenhe um autómato finito (determinista ou não-determinista) que

permita reconhecer palavras representadas por cada uma das expressões regulares

CADERNO DE EXERCÍCIOS E TRABALHOS PRÁTICOS PARA A DISCIPLINA DE COMPILADORES

©João M. P. Cardoso 60

seguintes (o autómato deve diferenciar o reconhecimento por cada uma das

expressões):

d+e?

[a-c]*[1-3]+

4. [10 valores] Considere a gramática seguinte (o número entre parêntesis identifica

o número de cada produção):

LIT = [0-9]+

OP = “+” | “-“ | “*” | “/”

Start → Expr $ (1)

Expr → Expr OP Expr (2)

Expr → LIT (3)

Expr → “(“ Expr “)” (4)

(a) Desenhe as árvores sintácticas concretas que conseguir derivar para as entradas:

(32*2)+1$ e 32*2+1$;

(b) Acha que a gramática apresentada é ambígua ou não-ambígua? Justifique a

resposta;

(c) Supondo que os tokens definidos pelo símbolo terminal OP correspondem a

operadores aritméticos, diga se a prioridade destes operadores é respeitada pela

gramática. Justifique a resposta com um exemplo. Caso não seja, indique as

modificações que teria de introduzir na gramática.

(d) Suponha que se pretende implementar um analisador sintáctico descendente sem

retrocesso (backtracking). Diga se terá de modificar a gramática para a

implementação ser possível. Justifique a resposta. No caso de ter de ser

modificada, apresente a nova gramática;

[Nota: Apresente a resolução das duas alíneas seguintes na mesma página do

teste]

(e) Considerando a gramática original desenhe o DFA para o analisador sintáctico

LR(0);

(f) Desenhe a tabela sintáctica correspondente ao DFA da alínea anterior;

CADERNO DE EXERCÍCIOS E TRABALHOS PRÁTICOS PARA A DISCIPLINA DE COMPILADORES

©João M. P. Cardoso 61

(g) Determine os conjuntos First e Follow para cada um dos símbolos não terminais

da gramática anterior;

(h) Diga se a gramática original é LR(0). Caso não seja indique se é SLR(1).

Justifique;

(i) Desenhe uma tabela (de acordo com a tabela exemplificativa indicada em baixo)

com as etapas do analisador sintáctico shift/reduce para a entrada: 3*2+4$.

Pilha de estados Pilha de símbolos Entrada Acção

3*2+4$

5. [5 valores] Na figura a seguir é apresentado um pedaço de código (os números

entre parêntesis identificam cada linha de código).

(1) void AddToArray(int[] A, int N, int C) {

(2) int i;

(3) i = 1;

(4) while(i <= N) {

(5) A[i] = C + A[i];

(6) i = i+1;

(7) }

(8) }

(j) Considerando as linhas de código (5) e (6), diga que verificações devem ser feitas

pelo analisador semântico;

(k) No caso da variável i ter sido declarada como float que conversões teriam de ser

introduzidas durante a análise semântica da linha (6);

(l) Desenhe a árvore de instruções e expressões de nível alto para a sequência de

instruções de (3) a (7), considerando as instruções: lda, sta, ldl, stl, ldp;

(m) Desenhe a representação intermédia de nível baixo para a sequência de instruções

de (3) a (7), considerando que todas as variáveis escalares locais são guardadas

em posições da pilha. Considere para a representação as instruções: cbr, lda, sta,

ld, st, ldp;

CADERNO DE EXERCÍCIOS E TRABALHOS PRÁTICOS PARA A DISCIPLINA DE COMPILADORES

©João M. P. Cardoso 62

[Nota: Considere uma memória de sistema endereçável ao byte.]