Esquemas de Tradução
Prof. Alexandre Monteiro
Baseado em material cedido pelo Prof. Euclides Arcoverde
Recife
‹#›
Contatos

Prof. Guilherme Alexandre Monteiro Reinaldo

Apelido: Alexandre Cordel

E-mail/gtalk: [email protected]
[email protected]

Site: http://www.alexandrecordel.com.br/fbv

Celular: (81) 9801-1878
Traduções Dirigidas por Sintaxe



As produções de uma gramática são suas
“regras sintáticas” e definem construções
relevantes da linguagem
Traduções Dirigidas por Sintaxe associam
regras semânticas a cada uma dessas regras
sintáticas, com algum objetivo
As regras semânticas definem a saída
correspondente a cada produção
3
Traduções Dirigidas por Sintaxe

Possíveis aplicações
•Criação da árvore sintática em memória
•Verificação de tipos
•Geração de código intermediário
•Etc.

Enfim, ela pode ser usada em todo o restante das etapas do
front-end...
4
Traduções Dirigidas por Sintaxe

Existem dois tipos
• Definição Dirigida por Sintaxe – assumindo que
cada nó da árvore possui atributos, define os
valores que eles devem receber
• Esquemas de Tradução – associam código qualquer
a cada produção, para ser executado durante a
análise sintática (parsing)

A distinção é um pouco confusa...
5
Traduções Dirigidas por Sintaxe

“Definição Dirigida por Sintaxe” é uma
descrição mais abstrata e formal de uma
tradução
• Especifica a tradução

Já um “Esquema de Tradução” é a descrição
concreta do código que vai ser executado para
cada produção
• Implementa uma definição dirigida por sintaxe
• Usa trechos de código
6
Definição Dirigida por Sintaxe
Definição Dirigida por Sintaxe

É a gramática livre de contexto da linguagem
acrescida de
• Descrição dos atributos que cada símbolo (terminal
ou não-terminal) possui
• Regras semânticas, associadas a cada produção,
para definir os valores dos atributos

Chamaremos SDD (Syntax-Directed Definition)
8
Exemplo 1

Considere a seguinte gramática:
L
E
E
T
T
F
→
→
→
→
→
→
E ;
T + E1
T
F * T1
F
digit
9
Exemplo 1

Com base em uma gramática de expressões, o objetivo do
exemplo é avaliar o valor numérico das expressões
•Para os não-terminais L, E, T e F, vamos
considerar que possuem o atributo “val”
•O símbolo terminal digit terá um atributo
“lexval”, com o seu valor numérico
•O símbolo terminal ; (ponto-e-vírgula) não
tem importância nesta tradução
10
Exemplo 1
Produção
Regra Semântica
L→E;
L.val = E.val
E → T + E1
E.val = T.val + E1.val
E→T
E.val = T.val
T → F * T1
T.val = F.val * T1.val
T→F
T.val = F.val
F → digit
F.val = digit.lexval
11
Exemplo 2

Considere a seguinte gramática:
E
E
E
T
T
T
→
→
→
→
→
→
E1 + T
E1 - T
T
( E )
num
identifier
12
Exemplo 2


Este exemplo usa uma gramática de expressões com soma
e subtração apenas
O objetivo da SDD neste exemplo é construir a árvore
sintática
•Cada não-terminal tem um atributo “node”
que representa o nó da árvore que
representa aquela ocorrência do nãoterminal
13
Exemplo 2
Produção
Regra Semântica
E → E1 + T
E.node = new Op(‘+’, E1.node, T.node)
E → E1 - T
E.node = new Op(‘-’, E1.node, T.node)
E→T
E.node = T.node
T→(E)
T.node = E.node
T → num
T.node = new Leaf(num)
T → identifier
T.node = new Leaf(identifier)
14
Definição Dirigida por Sintaxe


Não se preocupa com detalhes, como a ordem de definição
dos atributos
Sua principal aplicação é para especificar traduções mais
simples
•Mais abstrata
15
Esquema de Tradução
Esquema de Tradução

É uma extensão do conceito de SDD
• Possui trechos de código como regras semânticas
(que passam a ser chamadas ações semânticas)
• Controla a ordem de execução das ações
semânticas

Implementação da tradução (ou da SDD)
• Mais concreta
17
Esquema de Tradução

Assim, um Esquema de Tradução é uma
gramática livre acrescida de
• Descrição dos atributos que cada símbolo (terminal
ou não-terminal) possui
• Ações semânticas, associadas a cada produção
- Descritas na forma de trechos de código de uma linguagem
de programação real
- Posicionadas dentro da produção como se fossem um
símbolo, para indicar o momento exato de executar a ação
18
Esquema de Tradução

Um Esquema de Tradução pode ser
implementado junto com a análise sintática
• Ações semânticas disparadas sempre que o parser
identifica uma produção

Ou percorrendo a árvore de derivação
• Primeiro, a árvore é construída por um Esquema de
Tradução realizado durante a análise sintática
• Depois, a árvore é percorrida, disparando as ações
semânticas no momento adequado
19
Usando Esquemas de Tradução
Introdução

Esquemas de Tradução são um tipo de Tradução Dirigida
por Sintaxe
•Ações semânticas (trechos de código)
associados às produções

Pode ser realizada de duas maneiras
•Junto com a análise sintática
- Veremos no CUP e em parsers descendentes
•Percorrendo a árvore de derivação
21
Sumário

Esquemas de Tradução no CUP

Esquemas de Tradução em Parsers Descendentes

Construção da Árvore

Esquemas de Tradução na Árvore Sintática
22
Tradução na Análise Sintática


As ações semânticas são disparadas sempre que o parser
identifica uma produção
Veremos como é feita em dois casos
•Esquema de tradução em parsers ascedentes
(como o CUP)
•Esquema de tradução em parsers de descida
recursiva
23
Esquemas de Tradução no CUP
Tradução Usando o CUP

Em geral, basta colocar um trechos de código em meio às
produções delimitando-os por {: e :}
comand ::= expr PT_VIRG
{: System.out.println("Expressão!"); :}
;
expr ::= ...
25
Tradução Usando o CUP

Em alguns casos, é possível colocar a ação
semântica em meio aos símbolos
comand ::= expr
{: System.out.println("Expressão!"); :}
PT_VIRG
;
• Ação executada antes de reconhecer o PT_VIRG

Em outros casos, o CUP pode indicar que é
incapaz de identificar o momento em que a
26
ação deve ser executada
Tradução Usando o CUP


O CUP permite associar classes aos símbolos
(terminais ou não-terminais)
Pensando no conceito de Definição Dirigida por
Sintaxe, é como se
• Cada símbolo tivesse um único atributo
• O tipo desse atributo é a classe em questão

Funcionam como atributos sintetizados
• Não permite atributos herdados
27
Tradução Usando o CUP

Para associar classes aos símbolos terminais e
não-terminais, basta indicá-las na seção de
declaração de símbolos
terminal String NUMERO;
non terminal Integer expr;
• Ocorrências do símbolo terminal NUMERO serão
representadas por objetos da classe String
• Ocorrências do não-terminal expr representadas
pela classe Integer
28
Integração Lexer/CUP

Atenção: o tipo com que você declara um
símbolo terminal deve ser o mesmo tipo
retornado pelo lexer no segundo parâmetro do
construtor de “Symbol”
[0-9]+ {
return new Symbol(sym.NUMERO, yytext());
}
• Ok, pois yytext() retorna uma String
29
Tradução Usando o CUP

Para ter acesso aos objetos que representam
os atributos dos símbolos filhos, basta colocar
dois pontos seguido de um nome qualquer
• A ação poderá usar esse nome como uma variável
daquele tipo
comand ::= expr:val PT_VIRG
{: System.out.println("Valor: " + val); :}
;
• É como se val fosse o atributo de expr
30
Tradução Usando o CUP

Para atribuir valor ao objeto que representa o
não-terminal pai (lado esquerdo), é preciso
usar a variável RESULT
expr ::= expr:e1 MAIS expr:e2
{: int soma = e1.intValue() + e2.intValue();
RESULT = new Integer(soma);
:}
• O RESULT é como um atributo sintetizado (que
depende apenas dos filhos) de expr 31
Tradução Usando o CUP

Para um exemplo completo de uso do CUP
junto com ações semânticas será
disponibilizado no oro-aro
• Implementa um Esquema de Tradução para
interpretar expressões, retornando seus valores

Veremos agora como usar um Esquema de
Tradução com um parser de descida recursiva
32
Esquemas de Tradução em Parsers
Descendentes
Tradução em Parsers de Descida
Recursiva

Relembrando a técnica
• O reconhecimento de cada não-terminal X está
associado a um procedimento parseX()
• Dentro de parseX() é feita a discriminação entre as
diferentes produções de X

Como fazer para associar ações semânticas a
cada produção?
34
Tradução em Parsers de Descida
Recursiva


O caso mais simples é se o não-terminal tem uma só
produção e a ação deve ser executada no final da produção
Por exemplo, para essa tradução...
comand ::= expr ";" { System.out.println("Expressão!"); }
35
Tradução em Parsers de Descida
Recursiva

...o método parseComando() ficaria assim
void parseComand() {
parseExpr();
acceptToken(PT_VIRG);
System.out.println("Expressão!");
}
36
Tradução em Parsers de Descida
Recursiva

Se o não-terminal tem apenas uma produção e a ação tiver
que ser executada no meio da produção...
comand ::= expr {System.out.println("Expressão!"); } ";"
37
Tradução em Parsers de Descida
Recursiva

...o resultado é este
void parseComand() {
parseExpr();
System.out.println("Expressão!");
PT_VIRG
}
38
Tradução em Parsers de Descida
Recursiva

Se tiver mais de uma produção...
comand ::= "while" {System.out.println("While!");}
"(" expr ")" comand
| "do" comand {System.out.println(“Do-While!");}
"while (" expr ");"
| ...
39
Tradução em Parsers de Descida
Recursiva

...basta posicionar a ação semântica dentro do
bloco que trata a produção desejada
void parseLoopComand() {
if (token.getType() == WHILE) {
acceptToken();
System.out.println(“While!");
...
} else if (token.getType() == DO) {
acceptToken();
parseComand();
System.out.println(“Do-While!");
...
}
}
40
Tradução em Parsers de Descida
Recursiva



Em alguns casos, pode não ser possível posicionar a ação
adequadamente, pois ainda não se sabe qual é a produção
Por exemplo: se tiver mais de uma produção e a ação de
alguma delas acontece antes do primeiro símbolo
Mesma limitação do CUP
41
Tradução em Parsers de Descida
Recursiva

Como trabalhar com atributos para os
símbolos?
• Por exemplo, implementar a tradução abaixo
expr ::= termo + expr1 {expr.val = termo.val + expr1.val}
| termo
{expr.val = termo.val}

Primeiro veremos como associar um objeto a
um símbolo (representando o atributo val)
42
Tradução em Parsers de Descida
Recursiva

Para associar uma classe ao não-terminal X,
basta fazer o método parseX() retornar a
classe
Integer parseExpr() {
...
}

Para os terminais, é só retornar um atributo da
classe desejada junto com o Token
43
Tradução em Parsers de Descida
Recursiva

Para acessar os valores dos filhos basta
receber (e guardar) os valores de retorno
• Atributos sintetizados
Integer parseExpr() {
Integer termoVal = parseTermo();
if (token.getType() == MAIS) {
acceptToken();
Integer expr1Val = parseExpr();
return termoVal + expr1Val;
}
}
44
Tradução em Parsers de Descida
Recursiva

Em alguns casos, pode ser necessário passar valores por
parâmetro
•Atributos herdados

Isso acontece, por exemplo, em uma gramática recursiva à
esquerda, após transformá-la em uma sem a recursão
45
Tradução em Parsers de Descida
Recursiva

Gramática original
expr ::= expr “*” termo
| termo
termo ::= NUMBER
• Seria fácil criar uma tradução para calcular o valor
de expressões aqui...
46
Tradução em Parsers de Descida
Recursiva

Gramática sem recursão
expr ::= termo restoExpr
restoExpr ::= “*” termo restoExpr
| ε
termo ::= NUMBER
• É preciso passar o valor do “termo” do lado
esquerdo para o “restoExpr” fazer a multiplicação
47
Tradução em Parsers de Descida
Recursiva

A tradução ficaria assim
Integer parseExpr() {
Integer termoVal = parseTermo();
parseRestoExpr(termoVal);
}
Integer parseRestoExpr(Integer esq) {
if (token.getType() == VEZES) {
Integer dir = parseTermo();
Integer produto = esq * dir;
return parseRestoExpr(produto);
} else {
return esq;
}
}
48
Construção da Árvore
Construção da Árvore

Relembrando...
• Árvore de derivação – guarda todos os símbolos,
inclusive sinais especiais, de cada produção
- Feita com base na gramática concreta
• Árvore sintática – guarda só o que é importante de
cada produção
- Feita com base na gramática abstrata

É mais adequado construir a árvore sintática
50
Construção da Árvore


Não são árvores homogêneas, como as que são vistas nas
disciplinas de estruturas de dados
Cada nó dessa árvore pode ser de um tipo diferente
•Classes diferentes para cada símbolo nãoterminal
•Classes diferentes para cada produção
51
Construção da Árvore

Se o não-terminal tiver uma só produção
• Usar uma só classe e colocar como atributos dela os
símbolos importantes que aparecem no lado direito
• Exemplo de produção no CUP
atrib ::= IDENTIFIER EQ expr
- Criar classe Atribuicao com o atributo identificador (do
tipo String) e o atributo expressao (do tipo que for
associado a esse não-terminal)
52
Construção da Árvore

Se o não-terminal tiver uma só produção (cont.)
•Por fim, colocar a ação no CUP para
instanciar esse objeto
atrib ::= IDENTIFIER:id EQ expr:e
{: RESULT = new Atribuicao(id,e); :}
53
Construção da Árvore

Não-terminal com mais de uma produção
• Criar uma classe abstrata ou interface para
representar o não-terminal
• Criar uma classe (herdada) para cada produção
• Exemplos de produções no CUP
expr ::= expr MAIS expr
| expr VEZES expr
| NUM
54
Construção da Árvore

Não-terminal com mais de uma produção (cont.)
•Criar interface Expr
•Criar classes filhas Soma, Produto e
ExprNum
Expr
Soma
Produto
ExprNum
55
Construção da Árvore

Não-terminal com mais de uma produção
(cont.)
• Colocar as ações no CUP para instanciar as classes
expr ::= expr:e1 MAIS expr:e2
{: RESULT = new Soma(e1,e2); :}
| expr:e1 VEZES expr:e2
{: RESULT = new Produto(e1,e2); :}
| NUM:n
{: RESULT = new ExprNum(n); :}
56
Construção da Árvore

Em alguns casos, uma mesma classe pode ser
usada para mais de uma produção
• Uma só classe para "if" sem "else" e para "if" com
"else"
• Uma só classe para todas as expressões binárias
• Uma só classe para todas as expressões unárias
• “Usar o bom senso...”
57
Esquemas de Tradução na Árvore
de Derivação
Tradução na Análise Sintática


Já vimos como usar Esquemas de Tradução
junto com a análise sintática
Esquemas desse tipo têm algumas limitações
• Parsers ascendentes (como o CUP) permitem apenas
atributos sintetizados
• Parser descendentes permitem atributos
sintetizados e herdados, mas com algumas
restrições
59
Tradução na Árvore


Outra maneira de se usar Esquemas de Tradução é usando a
árvore
Essa estratégia é mais geral do que traduções que
acontecem durante a análise sintática
•Permite implementar qualquer tradução,
mesmo com atributos herdados
60
Esquema de Tradução na Árvore de
Derivação

A primeira etapa é construir a árvore sintática
•Acabamos de ver como fazer isso durante a
análise sintática...

Em seguida, basta percorrer a árvore, executando as ações
semânticas no momento desejado
61
Esquema de Tradução na Árvore de
Derivação

Uma maneira simples de implementar uma tradução é criar
métodos em cada classe da árvore que representar uma
produção (ou um não-terminal)
•Tudo inicia chamando o método do nó raiz
•No momento adequado, cada nó chama os
métodos equivalentes dos filhos
62
Exemplo 1: Tradução para a Forma
Pré-fixada

Este é o Esquema de Tradução desejado
• Visa passar a expressão para a forma prefixada
expr →
{ print("+"); } expr + expr
| { print("*"); } expr * expr
| NUM { print(NUM.lexval); }

Vamos supor que a árvore tenha sido criada
com as classes Soma, Produto e ExprNum,
todas filhas de Expr
63
Exemplo 1: Tradução para a Forma
Pré-fixada

A tradução é feita assim
class ExprNum extends Expr {
int lexval;
public void toPrefixed() {
print(lexval);
}
}
64
Exemplo 1: Tradução para a Forma
Pré-fixada

E assim
class Soma extends Expr {
private Expr expr1, expr2;
public void toPrefixed() {
print("+");
expr1.toPrefixed();
expr2.toPrefixed();
}
}
•Fazer análogo na classe Produto
65
Exemplo 2: Verificação Semântica




Criar um método verificaSemantica() em cada classe
da árvore
O início seria no método verificarSemantica() da
classe Program
Esse método iria chamar o método
verificarSemantica() da classe ListaDecl e, depois, o
da classe Bloco
O método da classe ListaDecl iria chamar o método
66
verificarSemantica() de cada objeto Declaracao
Esquema de Tradução na Árvore de
Derivação




Outra maneira de percorrer a árvore é usando
o design pattern "Visitor"
A árvore implementa apenas as chamadas a
métodos de um Visitor genérico
Classes Visitor de próposito específico podem
ser criadas para implementar uma tradução
Ver
http://en.wikipedia.org/wiki/Visitor_pattern
67
Visitor


Visitor é um padrão de projeto catalogado
pelos “quatro” mosqueteiros da engenharia de
software, Gamma, Helm, Johnson e Vlissides
(Gang of Four - GoF), como um padrão de
projeto comportamental
Seu objetivo principal é permitir que sejam
adicionadas novas funcionalidades a classes
existentes, de maneira plugável, sem que haja
a necessidade de se alterar a hierarquia dessas
classes
68
Bibliografia

AHO, A., LAM, M. S., SETHI, R., ULLMAN, J. D.,
Compiladores: princípios, técnicas e ferramentas. Ed.
Addison Wesley. 2a Edição, 2008 (Capítulo 4)
69
Download

Aula 9 (08/04/2015) - Esquemas de Tradução