CES-41 COMPILADORES Capítulo II Gramáticas e Linguagens Capítulo II – Gramáticas e Linguagens 2.1 – Gramáticas e linguagens de programação 2.2 – Gramáticas livres de contexto 2.3 – GLC’s ambíguas 2.4 – GLC’s recursivas à esquerda 2.1 – Gramáticas e Linguagens de Programação As linguagens de programação conhecidas não são livres de contexto Exemplo: seja a linguagem L = {w c w | w Є (a | b)*} onde ∑ = {a, b, c} Sentença típica: aababcaabab L = {w c w | w Є (a | b)*} onde ∑ = {a, b, c} Sentença típica: aababcaabab A linguagem é uma simplificação daquelas que exigem a declaração de todo identificador antes de ser usado A primeira ocorrência de aabab representa sua declaração e a segunda, seu uso A letra c representa a separação entre declarações e comandos executáveis L = {w c w | w Є (a | b)*} onde ∑ = {a, b, c} Sentença típica: aababcaabab A Teoria de Linguagens Formais demonstra que tais linguagens não podem ser geradas por gramáticas livres de contexto (GLC’s) Ou seja, apresentam sensibilidade ao contexto No entanto não se usam gramáticas sensíveis ao contexto (GSC’s) para analisar programas escritos em linguagens de programação Gramáticas regulares são usadas na análise léxica Gramáticas livres de contexto são usadas na análise sintática A sensibilidade ao contexto das linguagens é verificada na análise semântica Exemplo: declaração de identificadores Um identificador é colocado na tabela de símbolos quando de sua declaração Sua presença nessa tabela é verificada, quando de seu uso (teste semântico) 2.2 – Gramáticas Livres de Contexto 2.2.1 – Definição Tipicamente são usadas três especificações para se definir uma linguagem de programação: Especificações sintáticas, feitas através de uma GLC Especificações léxicas, usando expressões regulares Especificações semânticas, usando restrições às construções sintáticas Em compilação, para a maioria das linguagens de programação, há uma simplificação tal que: GLC’s são usadas para guiar toda a fase de análise e a geração do código intermediário (todo o front-end do compilador) O analisador sintático é fundamentado numa GLC Ele tem como escravo o analisador léxico Ele tem como recheio o analisador semântico e o gerador do código intermediário GLC é uma entidade G contendo quatro componentes: Um conjunto finito N de símbolos não-terminais Um alfabeto, ou seja, um conjunto finito ∑ de símbolos terminais, também chamados de átomos A designação de um dos não-terminais de N para ser o símbolo inicial, referenciado muitas vezes por S Um conjunto P de produções Simbolicamente, G = {N, ∑, S, P} Forma geral de uma produção: A → α A é um não-terminal, ou seja, A Є N, e é chamado de lado-esquerdo α é um conjunto de zero ou mais terminais e/ou não- terminais, ou seja, α Є (N ∑)*, e é chamado de ladodireito De passagem, produção com sensibilidade ao contexto: βAγ→βαγ A é um não-terminal, ou seja, A Є N α, β, γ são conjuntos de zero ou mais terminais e/ou não- terminais, ou seja, α, β, γ Є (N ∑)* Pelo menos um entre β e γ não é vazio A pode ser substituído por α, se estiver entre β e γ 2.2.2 – Construção de um programa ou sentença ■ Seja a seguinte gramática G = {N, ∑, S, P} N = {S, A} ∑ = {(, )} P = {S ( A ) | S ( A ) , A ε | S} Símbolo inicial: S ■ O símbolo inicial S é o embrião ■ A construção se inicia substituindo-se o símbolo inicial pelo lado direito de uma de suas produções: SS(A) ■ Tem início a formação do feto do programa P = {S ( A ) | S ( A ) , A ε | S} ■ A seguir, lados esquerdos de produções que fazem parte desse feto vão sendo substituídos pelos lados direitos S S(A) (A)(A) ()(A) ()(S) ()((A)) ()(( )) A construção termina quando todos os nãoterminais tiverem sido substituídos O símbolo inicial e cada estado do feto são formas sentenciais do programa ou da sentença Sentença é uma forma sentencial sem nãoterminais P = {S ( A ) | S ( A ) , A ε | S} S S(A) (A)(A) ()(A) ()(S) ()((A)) ()(( )) A linguagem gerada por G é o conjunto de todas as sentenças geradas por G Simbolicamente L(G) = { w * S * w } P = {S ( A ) | S ( A ) , A ε | S} S S(A) (A)(A) ()(A) ()(S) ()((A)) ()(( )) Definição recursiva de forma sentencial de uma gramática: 1) O símbolo inicial S é uma forma sentencial 2) Seja A um não-terminal e sejam α, β, γ cadeias de símbolos terminais e/ou não terminais 3) Se βAγ for uma forma sentencial e A → α uma produção, então β α γ será também uma forma sentencial P = {S ( A ) | S ( A ) , A ε | S} S S(A) (A)(A) ()(A) ()(S) ()((A)) ()(( )) Derivação direta é a substituição, numa forma sentencial, de um não-terminal pelo lado direito de uma de suas produções O processo ao lado apresenta 6 derivações diretas P = {S ( A ) | S ( A ) , A ε | S} S S(A) (A)(A) ()(A) ()(S) ()((A)) ()(( )) Derivação de uma forma sentencial é uma sequência de zero ou mais derivações diretas para produzir essa forma, começando do símbolo inicial Simbolicamente: S * S, S * ( ) ( ( ) ) e S * ( ) ( A ) Outro símbolo: + : sequência de uma ou mais derivações diretas P = {S ( A ) | S ( A ) , A ε | S} As definições de derivação e derivação direta e S S(A) os símbolos , * e + podem, em algumas (A)(A) situações, ser aplicados a ()(A) - Não-terminais diferentes do símbolo inicial ()(S) - Sub-cadeias de sentenças ()((A)) - Sub-cadeias de formas sentenciais ()(( )) Exemplos: A S (A)( ) A + ( A ) A * ( ) Exemplo: produção de um programa na linguagem LAtrib, com as seguintes características: ■ Programas têm só o módulo principal ■ Esse módulo tem cabeçalho, declarações e comandos de atribuição ■ As variáveis podem ser inteiras e reais, escalares ■ Operadores de expressões: soma e subtração ■ Expressões podem ter parêntesis Gramática para LAtrib: ∑ = {program, var, int, real, ID, CTINT, CTREAL, ‘{’, ‘}’, ‘;’, ‘,’ , ‘=’ , ‘+’ , ‘-’ , ‘(’, ‘)’} Os átomos literais em negrito são palavras reservadas N = {Programa, Cabeçalho, Declarações, ListDecl, Declaração, Tipo, ListId, Comandos, ListCmd, CmdAtrib, Expressão, Termo} O símbolo inicial é Programa Produções da Gramática para LAtrib: Seja o programa Exemplo: Programa → Cabeçalho Declarações Comandos Cabeçalho → program ID ; program Exemplo; var int i, j; real x; { i = 2; j = 3; x = 5.5–(i+j); } Declarações → ε | var ListDecl ListDecl → Declaração | ListDecl Declaração Declaração → Tipo ListId ; Tipo → int | real Seja a construção do ListId → ID | ListId , ID programa Exemplo Comandos → { ListCmd } ListCmd → ε | ListCmd CmdAtrib CmdAtrib → ID = Expressão ; Expressão → Termo | Expressão + Termo | Expressão - Termo Termo → ID | CTINT | CTREAL | ( Expressão ) program Exemplo; var int i, j; real x; {i = 2; j = 3; x = 5.5 – (i + j);} Programa Cabeçalho Declarações Comandos + program ID(Exemplo) ; var ListDecl { ListCmd } + program ID(Exemplo) ; var ListDecl Declaração { ListCmd CmdAtrib } + program ID(Exemplo) ; var Declaração Declaração { ListCmd CmdAtrib CmdAtrib } + program ID(Exemplo) ; var Declaração Declaração { ListCmd CmdAtrib CmdAtrib CmdAtrib } program Exemplo; var int i, j; real x; {i = 2; j = 3; x = 5.5 – (i + j);} + program ID(Exemplo) ; var Declaração Declaração { CmdAtrib CmdAtrib CmdAtrib } + program ID(Exemplo) ; var Tipo ListId ; Tipo ListId ; { ID(i) = Expressão ; ID(j) = Expressão ; ID(x) = Expressão ; } + program ID(Exemplo) ; var int ListId , ID(j) ; real ID(x) ; { ID(i) = Termo ; ID(j) = Termo ; ID(x) = Expressão - Termo ; } program Exemplo; var int i, j; real x; {i = 2; j = 3; x = 5.5 – (i + j);} + program ID(Exemplo) ; var int ID(i) , ID(j) ; real ID(x) ; { ID(i) = Termo ; ID(j) = Termo ; ID(x) = Termo - ( Expressão ) ; } + program ID(Exemplo) ; var int ID(i) , ID(j) ; real ID(x) ; { ID(i) = Termo ; ID(j) = Termo ; ID(x) = Termo - ( Expressão + Termo ) ; } program Exemplo; var int i, j; real x; {i = 2; j = 3; x = 5.5 – (i + j);} + program ID(Exemplo) ; var int ID(i) , ID(j) ; real ID(x) ; { ID(i) = Termo ; ID(j) = Termo ; ID(x) = Termo - ( Termo + Termo ) ; } program Exemplo; var int i, j; real x; {i = 2; j = 3; x = 5.5 – (i + j);} + program ID(Exemplo) ; var int ID(i) , ID(j) ; real ID(x) ; { ID(i) = CTINT(2) ; ID(j) = CTINT(3) ; ID(x) = CTREAL(5.5) - (ID(i) + ID(j) ) ; } 2.2.3 – Árvores sintáticas ■ Árvore sintática é uma representação gráfica de uma derivação ■ Exemplo: árvore sintática de S * ( ) ( ( ) ) Árvore sintática do programa Exemplo program Exemplo; var int i, j; real x; { i = 2; j = 3; x = 5.5–(i+j); } 2.3 – GLC’s Ambíguas Uma GLC é ambígua, se pelo menos uma de suas sentenças possuir 2 ou mais árvores sintáticas distintas Exemplo: Seja a gramática G = {, N, P, S} tal que: = {a, b}; N = {S}; P = {S S b S a} G é ambígua pois a sentença ababa tem duas árvores sintáticas: Exemplo: a Língua Portuguesa sem pontuação teria sérias ambiguidades A frase matar o rei não é pecado teria dois sentidos: 1) matar o rei não é pecado 2) matar o rei não é pecado Em compilação, as gramáticas devem ser não-ambíguas, ou então, deve-se acrescentar regras para resolver ambiguidades Exemplo: comando if-else da Linguagem C Produções: S if B S else S if B S a1 a2 B b1 b2 S if B S else S if B S a1 a2 B b1 b2 A sentença if b1 if b2 a1 else a2 tem duas árvores sintáticas, a saber: Regra de solução: fazer o else corresponder ao último if 2.4 – GLC’s Recursivas à Esquerda Uma GLC é recursiva à esquerda, se tiver um não-terminal A tal que haja uma derivação A + A, onde (N )* Exemplo: Na gramática G = {, N, P, S} tal que: = {a, b}; N = {S}; P = {S S b S a} tem-se S SbS (recursividade imediata à esquerda) Exemplo: Na gramática da linguagem LAtrib, as seguintes produções são recursivas à esquerda: ListDecl → ListDecl Declaração ListId → ListId , ID ListCmd → ListCmd CmdAtrib Expressão → Expressão + Termo | Expressão - Termo Existe recursividade não-imediata à esquerda Exemplo: Seja a gramática G = {, N, P, S} na qual: = {a, b, c, d}; N = {S, A}; P = {S Aa b , A Sc d } Essa gramática é não-imediatamente recursiva à esquerda pois: S Aa Sca Análise sintática top-down - grande grupo de métodos muito populares de análise sintática: Não consegue tratar gramáticas recursivas à esquerda Tais gramáticas devem então ser transformadas em outras equivalentes não-recursivas à esquerda Essa transformação será vista no Capítulo V Análise sintática bottom-up - outro grande grupo de métodos muito populares de análise sintática: Trabalha mais eficientemente com gramáticas recursivas à esquerda Yacc usa análise bottom-up