Como construir um compilador utilizando ferramentas Java Aula 6 – Análise Sintática Prof. Márcio Delamaro [email protected] Como construir um compilador utilizando ferramentas Java – p. 1/2 Ascendente × descendente Dada uma GLC, a análise sintática baseada nessa GLC pode ser ascendente ou descendente. Um analisador ascendente agrupa os símbolos da entrada para formar os símbolos não terminais mais distantes do símbolo inicial da GLC ou que estão em níveis inferiores na estrutura da linguagem. Na análise descendente, parte-se do símbolo inicial da GLC e busca-se identificar na entrada as construções que correspondem às produções da GLC. Como construir um compilador utilizando ferramentas Java – p. 2/2 Descendente recursiva Um analisador descendente recursivo utiliza essa última estratégia. Recursivo, pois a cada símbolo não terminal da GLC corresponde um método (muitas vezes recursivo). Cada método deve “ler” a entrada e reconhecer a estrutrutura do não-terminal. Como construir um compilador utilizando ferramentas Java – p. 3/2 Exemplo hclassdecl i → ′′ class′′ ′′ ident′′ [ ′′ extends′′ ′′ ident′′ ] hclassbodyi Como construir um compilador utilizando ferramentas Java – p. 4/2 Exemplo hclassdecl i → ′′ class′′ ′′ ident′′ [ ′′ extends′′ ′′ ident′′ ] hclassbodyi verificar se o próximo token da entrada é class. Como construir um compilador utilizando ferramentas Java – p. 4/2 Exemplo hclassdecl i → ′′ class′′ ′′ ident′′ [ ′′ extends′′ ′′ ident′′ ] hclassbodyi verificar se o próximo token da entrada é class. verificar se o próximo token é ident. Como construir um compilador utilizando ferramentas Java – p. 4/2 Exemplo hclassdecl i → ′′ class′′ ′′ ident′′ [ ′′ extends′′ ′′ ident′′ ] hclassbodyi verificar se o próximo token da entrada é class. verificar se o próximo token é ident. verificar se o próximo token é extends. Se não for, deve continuar a análise, pois essa parte da produção é opcional. Se for, deve verificar se o token seguinte é um ident. Se for, deve continuar com a análise. Como construir um compilador utilizando ferramentas Java – p. 4/2 Exemplo hclassdecl i → ′′ class′′ ′′ ident′′ [ ′′ extends′′ ′′ ident′′ ] hclassbodyi verificar se o próximo token da entrada é class. verificar se o próximo token é ident. verificar se o próximo token é extends. Se não for, deve continuar a análise, pois essa parte da produção é opcional. Se for, deve verificar se o token seguinte é um ident. Se for, deve continuar com a análise. verificar se o resto da entrada corresponde ao não-terminal classboby. Isso não é feito diretamente pelo método, mas, sim, invocando o método correspondente àquele não-terminal. Como construir um compilador utilizando ferramentas Java – p. 4/2 Exemplo void classdecl() { if ( curToken.type == CLASS) // token corrente é "class" analex(); // chama AL. curToken recebe novo token else SintaticError(); // ocorreu erro sintático if ( curToken.type == IDENT) // token corrente é identificador analex(); else SintaticError(); if ( curToken.type == EXTENDS) // token corrente é "extends" { analex(); if ( curToken.type == IDENT) // token é identificador analex(); else SintaticError(); } classbody(); } Como construir um compilador utilizando ferramentas Java – p. 5/2 Gramáticas LL(1) Cuidado na escolha da grámática. Como construir um compilador utilizando ferramentas Java – p. 6/2 Gramáticas LL(1) Cuidado na escolha da grámática. O primeiro L significa left to right e indica que a leitura da entrada é feita da esquerda para a direita. Como construir um compilador utilizando ferramentas Java – p. 6/2 Gramáticas LL(1) Cuidado na escolha da grámática. O primeiro L significa left to right e indica que a leitura da entrada é feita da esquerda para a direita. O segundo L significa left linear e indica que sempre o símbolo não terminal que estiver mais à esquerda será reconhecido primeiro. Como construir um compilador utilizando ferramentas Java – p. 6/2 Gramáticas LL(1) Cuidado na escolha da grámática. O primeiro L significa left to right e indica que a leitura da entrada é feita da esquerda para a direita. O segundo L significa left linear e indica que sempre o símbolo não terminal que estiver mais à esquerda será reconhecido primeiro. E o número 1 indica que o número de símbolos de lookahead necessários é 1. Como construir um compilador utilizando ferramentas Java – p. 6/2 Gramática LL(1)? hclassdecl i → ′′ class′′ ′′ ident′′ [ ′′ extends′′ ′′ ident′′ ] hclassbodyi Como construir um compilador utilizando ferramentas Java – p. 7/2 Gramática LL(1)? hclassdecl i → ′′ class′′ ′′ ident′′ [ ′′ extends′′ ′′ ident′′ ] hclassbodyi hclassdecl i → ′′ class′′ ′′ ident′′ hclassbodyi | ′′ class′′ ′′ ident′′ ′′ extends′′ ′′ ident′′ hclassbodyi Como construir um compilador utilizando ferramentas Java – p. 7/2 Tabela preditiva Indica qual “caminho” seguir S → aSAb | bAa A → bAb | c Terminais Não-terminal S A a b c S → aSAb S → bAa A → bAb A → c As posições vazias ou mais do que uma produção. Como construir um compilador utilizando ferramentas Java – p. 8/2 Recursão à esquerda Um caso típico de problemas ao construir a tabela preditiva é relativo à a existência de recursão à esquerda. ∗ B ⇒ Bα A partir de B podemos gerar uma forma sentencial em que o próprio B aparece como primeiro símbolo. Ao tentar reconhecer B, o AS descendente recursivo invoca o método correspondente ao não-terminal. Olhando para o token da entrada, esse método pode escolher um caminho que leve ao aparecimento de outro B, sem que nenhum token tenha sido consumido – o que caracteriza a recursão à esquerda. Como construir um compilador utilizando ferramentas Java – p. 9/2 Construção da tabela preditiva À primeira vista, pode parecer fácil calcular a tabela preditiva de uma GLC. Basta olhar quais são os tokens que iniciam as produções do não-terminal B e colocar na linha de B, na coluna desses tokens, as produções que eles iniciam. Porém, existem diversas situações que podem complicar essa tarefa. Como construir um compilador utilizando ferramentas Java – p. 10/2 Dificuldade I Se temos B → Aα | Cβ , então é preciso saber quais são os tokens que iniciam as produções de A e C para decidir qual produção utilizar. E essa situação pode se propagar, uma vez que A e C podem também iniciar com não-terminais. hprogi → ′′ class′′ ′′ ident′′ [ hclassbodyi ] hclassbodyi → hfuncbodyi ′′ .′′ | hvardecl i ′′ ;′′ Como construir um compilador utilizando ferramentas Java – p. 11/2 Dificuldade II Se temos B → A1 A2 ...An aα, precisamos saber se A1 ,... An podem gerar a cadeia vazia. Se isso acontecer, então o token a também deve ser utilizado para decidir qual produção de B utilizar. hprogi → ′′ class′′ ′′ ident′′ [ hclassbodyi ] hclassbodyi → hfuncbodyi hvardecl i ′′ ;′′ hfuncbodyi → [ halgumacoisai ] hvardecl i → [ houtracoisai ] Como construir um compilador utilizando ferramentas Java – p. 12/2 Dificuldade III ∗ Se temos A → aBb; B → α e α ⇒ λ, então b deve ser considerado como um token que deve ser utilizado para decidir pela produção B → α ao tentar reconhecer B. hprogi → ′′ class′′ ′′ ident′′ hclassbodyi ′′ ;′′ hclassbodyi → hfuncbodyi |hvardecl i ′′ .′′ hfuncbodyi → [ halgumacoisai ] hvardecl i → [ houtracoisai ] Como construir um compilador utilizando ferramentas Java – p. 13/2 Algoritmo para construir a TP Adicionar $, que aparece no fim da cadeia Computar FIRST: Seja α uma forma sentencial da gramática. Então FIRST(α) é definido como o conjunto ∗ de todos os símbolos terminais a tal que α ⇒ aβ , ou seja, o conjunto de terminais que iniciam alguma cadeia derivada a partir de α. Computar FOLLOW: Seja B um não-terminal da gramática e S o seu símbolo inicial. O conjunto FOLLOW(B) é formado pelos terminais a tal que ∗ S ⇒ αBaβ , ou seja, existe uma forma sentencial derivável a partir do símbolo inicial em que a aparece imediatamente à direita de B. Como construir um compilador utilizando ferramentas Java – p. 14/2 FIRST para o terminal a, FIRST(a) = {a}; para o não-terminal B, tal que B → A1 A2 ...An , onde Ai são símbolos terminais ou não terminais, fazemos: inicialmente FIRST(B) = {} repetimos FIRST(B) = FIRST(B) ∪ FIRST(Ai ), para i = 1, 2, ... até que encontremos algum i tal que Ai não deriva λ; para um string α = A1 A2 ...An , onde Ai são símbolos terminais ou não terminais, fazemos: repetimos FIRST(α) = FIRST(α) ∪ FIRST(Ai ), para i = 1, 2, ... até que encontremos algum i tal que Ai não deriva λ. Como construir um compilador utilizando ferramentas Java – p. 15/2 Exemplo expression → term expression′ expression′ → −term [expression′ ] | +term [expression′ ] | λ term → f actor term′ term′ → ∗f actor [term′ ] | /f actor [term′ ] | λ f actor → ident | constant | (expression) Como construir um compilador utilizando ferramentas Java – p. 16/2 FIRST: exemplo FIRST($) = {$} Como construir um compilador utilizando ferramentas Java – p. 17/2 FIRST: exemplo FIRST($) = {$} FIRST(+) = {+} Como construir um compilador utilizando ferramentas Java – p. 17/2 FIRST: exemplo FIRST($) = {$} FIRST(+) = {+} FIRST(-) = {-} Como construir um compilador utilizando ferramentas Java – p. 17/2 FIRST: exemplo FIRST($) = {$} FIRST(+) = {+} FIRST(-) = {-} FIRST(*) = {*} Como construir um compilador utilizando ferramentas Java – p. 17/2 FIRST: exemplo FIRST($) = {$} FIRST(+) = {+} FIRST(-) = {-} FIRST(*) = {*} FIRST(/) = {/ } Como construir um compilador utilizando ferramentas Java – p. 17/2 FIRST: exemplo FIRST($) = {$} FIRST(+) = {+} FIRST(-) = {-} FIRST(*) = {*} FIRST(/) = {/ } FIRST(ident) = {ident} Como construir um compilador utilizando ferramentas Java – p. 17/2 FIRST: exemplo FIRST($) = {$} FIRST(+) = {+} FIRST(-) = {-} FIRST(*) = {*} FIRST(/) = {/ } FIRST(ident) = {ident} FIRST(constant) = {constant} Como construir um compilador utilizando ferramentas Java – p. 17/2 FIRST: exemplo FIRST($) = {$} FIRST(+) = {+} FIRST(-) = {-} FIRST(*) = {*} FIRST(/) = {/ } FIRST(ident) = {ident} FIRST(constant) = {constant} FIRST(( ) = {( } Como construir um compilador utilizando ferramentas Java – p. 17/2 FIRST: exemplo FIRST($) = {$} FIRST(+) = {+} FIRST(-) = {-} FIRST(*) = {*} FIRST(/) = {/ } FIRST(ident) = {ident} FIRST(constant) = {constant} FIRST(( ) = {( } FIRST(factor) = {ident, constant, ( } Como construir um compilador utilizando ferramentas Java – p. 17/2 FIRST: exemplo FIRST(term’) = {*, / } Como construir um compilador utilizando ferramentas Java – p. 18/2 FIRST: exemplo FIRST(term’) = {*, / } FIRST(term) = {ident, constant, ( } Como construir um compilador utilizando ferramentas Java – p. 18/2 FIRST: exemplo FIRST(term’) = {*, / } FIRST(term) = {ident, constant, ( } FIRST(expression’) = {+, -} Como construir um compilador utilizando ferramentas Java – p. 18/2 FIRST: exemplo FIRST(term’) = {*, / } FIRST(term) = {ident, constant, ( } FIRST(expression’) = {+, -} FIRST(expression) = {ident, constant, ( } Como construir um compilador utilizando ferramentas Java – p. 18/2 FOLLOW adicionar o indicador de fim de cadeia $ ao conjunto FOLLOW(S), onde S é o símbolo inicial da GLC; dada a produção B → αAβ , fazemos: FOLLOW(A) = FOLLOW(A) ∪ FIRST(β) ∗ se β ⇒ λ, então fazemos FOLLOW(A) = FOLLOW(A) ∪ FOLLOW(B). Como construir um compilador utilizando ferramentas Java – p. 19/2 FOLLOW: exemplo FOLLOW(expression) = {$, )} FOLLOW(expression′ ) = {$, )} FOLLOW(term) = {+, -, $, )} FOLLOW(term′ ) = {+, -, $, )} FOLLOW(f actor) = {*, /, +, -, $, )} Como construir um compilador utilizando ferramentas Java – p. 20/2 Finalizando a tabela dada a produção B → α, para todo terminal a ∈ FIRST(α), devemos colocar essa produção na posição (B, a) da tabela; ∗ dada a produção B → α, onde α ⇒ λ, para todo terminal a ∈ FOLLOW(B), devemos colocar essa produção na posição (B, a) da tabela. Como construir um compilador utilizando ferramentas Java – p. 21/2 Resultado Terminais Não terminais ident constant +, - *, / ( ) $ expression 1 1 1 expression’ 2 3 3 term 4 4 4 term’ 6 5 6 6 factor 7 8 9 Como construir um compilador utilizando ferramentas Java – p. 22/2 Resultado (1)expression → term expression′ (2)expression′ → +term expression′ (3)expression′ → λ (4)term → f actor term′ (5)term′ → ∗f actor term′ (6)term′ → λ (7)f actor → ident (8)f actor → constant (9)f actor → (expression) Como construir um compilador utilizando ferramentas Java – p. 23/2 Como usar a TP A tabela preditiva pode ser usada, além da verificação da gramática, para construir o AS. Para implementar o método de um não-terminal, podemos olhar a tabela e determinar para cada terminal válido quais são as seqüências de ações a tomar, como, por exemplo, consumir um terminal ou invocar os métodos de outros não-terminais. Como construir um compilador utilizando ferramentas Java – p. 24/2 Como usar a TP A tabela preditiva pode ser usada, além da verificação da gramática, para construir o AS. Para implementar o método de um não-terminal, podemos olhar a tabela e determinar para cada terminal válido quais são as seqüências de ações a tomar, como, por exemplo, consumir um terminal ou invocar os métodos de outros não-terminais. Porém, se a linguagem é complexa, o cálculo da tabela preditiva e a construção do AS de forma manual são tarefas árduas. Ainda mais se estivermos trabalhando com uma gramática na BNF, pois, como vimos, o cálculo dos conjuntos FIRST e FOLLOW consideram apenas produções normais, sem os operadores da BNF. Como construir um compilador utilizando ferramentas Java – p. 24/2 Como usar a TP Nesse ponto entram os programas como o JavaCC, que, baseado na definição da linguagem em BNF, gera todos os métodos correspondentes aos não-terminais e verifica possíveis problemas, avisando ao implementador os pontos da gramática que os originaram. Como construir um compilador utilizando ferramentas Java – p. 25/2 Exercícios Dê exemplo de uma gramática que gera uma tabela preditiva com mais de uma entrada em alguma posição. Dê exemplo de uma gramática com recursão à esquerda. Elimine a recursão à esquerda. Dê exemplos, retirados da gramática de X ++ que mostrem as três dificuldades na construção da tabela preditiva. Compute o FIRST e FOLLOW de alguns símbolos de X ++ . Como construir um compilador utilizando ferramentas Java – p. 26/2