Usando Esquemas de
Tradução
“Na presença de Deus e de Cristo Jesus, que há de julgar os vivos e os mortos, por ocasião da sua manifestação pessoal e mediante seu Reino"
Revisão
Esquemas de Tradução são um tipo de
Tradução Dirigida por Sintaxe
Associam ações semânticas (trechos de código) às
produções
Podem ser realizados de duas maneiras
Junto com a análise sintática
Veremos no CUP e em parsers descendentes
Sumário da Aula
1. Esquemas de Tradução no CUP
2. Esquemas de Tradução em Parsers
Descendentes
3. Construção da Árvore
1. Esquemas de Tradução no
CUP
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
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!"); :} ;
Tradução Usando o CUP
Em alguns casos, é possível colocar a ação semântica
em meio aos símbolos
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 ação deve ser executada
comand ::= expr
{: System.out.println("Expressão!"); :} PT_VIRG
Tradução Usando o CUP
A tradução permitida pelo CUP pode ser
entendida como se
Cada símbolo tivesse um único atributo
O tipo desse atributo é uma classe de sua escolha
Funcionam como atributos sintetizados
(valores do pai definidos a partir dos filhos)
Não permite atributos herdados (filhos definidos a
Tradução Usando o CUP
Parar associar classes aos símbolos terminais e
não-terminais:
Indicar na seção de declaração desses símbolos
Ocorrências do símbolo terminal NUMERO serão associadas a
um valor String (como se fosse um atributo de NUMERO)
Ocorrências do não-terminal expr serão associadas a um valor
Integer
terminal String NUMERO; non terminal Integer expr;
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”
Ok, pois yytext() retorna uma String
[0-9]+ {
return new Symbol(sym.NUMERO, yytext()); }
Tradução Usando o CUP
Para ter acesso aos valores (atributos) dos
símbolos filhos, basta colocar dois pontos seguido de um nome qualquer
Esse nome conterá o valor (atributo) desejado
É como se val fosse o atributo de expr
comand ::= expr:val PT_VIRG
{: System.out.println("Valor: " + val); :} ;
Tradução Usando o CUP
Para definir (“setar”) o valor associado ao
não-terminal pai, use a variável RESULT
RESULT é como se fosse o valor do atributo do nó
pai expr (lado esquerdo da produção)
expr ::= expr:e1 MAIS expr:e2
{: int soma = e1.intValue() + e2.intValue(); RESULT = new Integer(soma);
Tradução Usando o CUP
Para um exemplo completo de uso do CUP junto
com ações semânticas, vejam o projeto de
exemplo disponível no site
Implementa um Esquema de Tradução para
interpretar expressões
Veremos agora como usar um Esquema de
2. 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
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...
Tradução em Parsers de
Descida Recursiva
...o método parseComando() ficaria assim
void parseComand() { parseExpr();
acceptToken(PT_VIRG);
printf("Expressão!");
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...
Tradução em Parsers de
Descida Recursiva
...o resultado é este
void parseComand() { parseExpr();
printf("Expressão!");
PT_VIRG }
Tradução em Parsers de
Descida Recursiva
Se tiver mais de uma produção...
comand ::= "while" {printf("While!");} "(" expr ")" comand
| "do" comand {printf(“Do-While!");} "while (" expr ");" | ...
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!"); ... } }
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
Tradução em Parsers de
Descida Recursiva
Como trabalhar com atributos para os símbolos?
Por exemplo, implementar a tradução abaixo
Primeiro veremos como associar um objeto a
um símbolo (representando o atributo val)
expr ::= termo + expr1 {expr.val = termo.val + expr1.val}
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
Para os terminais, é só retornar um atributo da
classe desejada junto com o Token
Integer parseExpr() {
... }
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 (toke.getType() == MAIS) { acceptToken();
Integer expr1Val = parseExpr(); return termoVal + expr1Val;
}
return termoVal; }
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, na gramática obtida
Tradução em Parsers de
Descida Recursiva
Gramática original
Seria fácil criar uma tradução para calcular o valor de
expressões aqui...
expr ::= expr “*” termo | termo
Tradução em Parsers de
Descida Recursiva
Gramática sem recursão
É preciso passar o valor do “termo” do lado esquerdo
para o “restoExpr” fazer a multiplicação
expr ::= termo restoExpr
restoExpr ::= “*” termo restoExpr | ε
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; }
Construção da Árvore
Na verdade, podemos falar de árvores de dois tipos
Árvore de derivação – guarda todos os símbolos, inclusive
sinais especiais, de cada produção
Árvore sintática – guarda só o que é relevante de cada
produção
Por exemplo, não precisa guardar tokens de ponto-e-vírgula,
“begin”, “end”, etc.
Geralmente, é mais adequado construir a árvore
Exemplo
Gramática para os exemplos a seguir
expressão ::= expressão + expressão | expressão - expressão | expressão * expressão | expressão / expressão | ( expressão ) | IDENTIFICADOR | INTEIRO-LITERAL
Árvore de Derivação
Árvore de derivação de “x*(i+i)”
expressão expressão x * i + x expressão expressão expressão ( expressão )
Árvore Sintática
Árvore sintática de “x*(i+i)”
produto x i x var soma var var
Construção da Árvore
Sintática
Não são árvores homogêneas, como as que são
vistas em Algoritmos e Estruturas de Dados
Recomendo criar, para cada nó dessa árvore,
um tipo diferente
Classes específicas para cada símbolo não-terminal Classes específicas para cada produção
Construção da Árvore
1. 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
Criar classe Atribuicao com o atributo identificador (do tipo
String) e o atributo expressao (do tipo que for associado a esse não-terminal)
Construção da Árvore
1. 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
Construção da Árvore
2. Não-terminal com mais de uma produção
Criar uma classe abstrata ou interface para
representar o não-terminal da esquerda
Criar uma subclasse para cada produção Exemplos de produções no CUP
expr ::= expr MAIS expr | expr VEZES expr | NUM
Construção da Árvore
2. Não-terminal com mais de uma produção
(cont.)
Criar interface Expr
Criar classes filhas Soma, Produto e ExprNum
Expr
Construção da Árvore
2. Não-terminal com mais de uma produção
(cont.)
Ações 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
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...
4. Esquemas de Tradução na
Árvore de Derivação
Tradução na Análise Sintática
Vimos antes 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
(diretamente) apenas atributos sintetizados
Parser descendentes permitem atributos sintetizados
Tradução na Árvore
Outra maneira de se usar Esquemas de
Tradução é usando a árvore já construída
Essa estratégia é mais geral do que traduções
que acontecem durante a análise sintática
Permite implementar qualquer tradução, mesmo com
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
Esquema de Tradução na
Árvore de Derivação
Uma maneira simples de implementar uma
tradução é criando métodos em cada classe da árvore
Tudo inicia chamando o método do nó raiz
Na ordem adequada, cada classe da árvore
Realiza ações
E chama métodos dos seus atributos (conceitualmente,
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
Vamos supor que a árvore tenha sido criada
com as classes Soma, Produto e ExprNum, todas filhas de Expr
expr → { print("+"); } expr + expr | { print("*"); } expr * expr | NUM { print(NUM.lexval); }
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);
}
Exemplo 1: Tradução para a
Forma Pré-fixada
E assim
Fazer análogo na classe Produto
class Soma extends Expr {
private Expr expr1, expr2;
public void toPrefixed() { print("+");
expr1.toPrefixed(); expr2.toPrefixed(); }
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
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
Projeto
Fazer um Esquema de Tradução durante a
análise sintática apenas para construir a árvore
Baseie-se no projeto base criado
Fazer dois Esquemas de Tradução na árvore,
para realizar:
a análise semântica