Compiladores Análise Sintática
Análise Sintática Vejamos um exemplo, seja a seguinte instrução de atribuição: posicao = inicial + incremento * 60 Na análise sintática tenta-se construir uma frase correta com os tokens produzidos pela fase anterior. É usual construir uma estrutura em árvore para representar a frase obtida. A partir dos tokens cria uma representação intermediária tipo árvore (árvore sintática) mostra a estrutura gramatical da sequência de tokens
Análise sintática Regras Sintáticas Especificam as sequências de símbolos que constituem estruturas sintáticas válidas; Estas regras permitem o reconhecimento de expressões e comandos; Exemplo: Pascal: atribuição a:=b; C: atribuição a=b;
Análise sintática As linguagens de programação possuem regras precisas para descrever a estrutura sintática de programas bem formados; Exemplo: Linguagem C Funções declaração e comando Comando expressões A estrutura sintática das construções de uma linguagem de programação é especificada pelas regras gramaticais de uma gramática livre de contexto
Análise sintática Benefícios para projetistas de linguagens: Uma gramática provê uma especificação sintática precisa e fácil de entender para as linguagens de programação; A partir de determinadas classes gramaticais, podemos construir automaticamente um analisador sintático eficiente; Durante o processo de construção do analisador, podem ser detectadas ambiguidades sintáticas; Uma gramática permite o desenvolvimento de uma linguagem iterativamente, possibilitando lhe acrescentar novas construções para realizar novas tarefas;
Análise sintática Utiliza os tokens produzidos pela análise léxica e verifica a formação do programa com o uso de GLC (Gramáticas Livres de Contexto) A partir dos tokens é criada uma representação intermediária da árvore sintática mostra a estrutura gramatical da sequência de tokens;
Análise Sintática posicao = inicial + incremento * 60 <id, 1>, <=>, <id, 2>, <+>, <id, 3>, <*>, <numero, 60> Analisador Sintático <=> Árvore Sintática <id, 1> <+> <id,2> <*> <id,3> 60
Análise sintática O analisador sintático recebe do analisador léxico uma cadeia de tokens representando o programa fonte e verifica se essa cadeia de tokens pertence à linguagem gerada pela gramática.
Análise sintática O analisador sintático constrói uma árvore de derivação e a passa ao restante do front-end do compilador para processamento. Obs: na prática não é necessário construir a árvore de derivação explicitamente, pois as ações de verificação e tradução podem ser implementados em um único módulo.
Análise sintática Existem 3 estratégias gerais de análise sintática para o processamento de gramáticas: Universal, Descendente (Top Down) e Ascendente (Bottom Up). Em ambas as estratégias a entrada do analisador sintático é consumida da esquerda para a direita, um símbolo de cada vez Os analisadores criados à mão normalmente utilizam gramáticas LL Os analisadores sintáticos para a maioria de gramáticas LR geralmente são construídos utilizando ferramentas automatizadas
Análise sintática Para descrever uma linguagem é necessário uma série de regras gramaticais; As regras são formadas por uma única estrutura do lado esquerdo seguida do metasímbolo ::= e por uma sequência de itens do lado direito (símbolos ou estruturas); Estruturas entre <> são chamadas de não terminais; Símbolos como garota e cachorro são chamados de terminais; As regras gramaticais são as produções.
Análise sintática Exemplo de uma gramática para expressões aritméticas de adição e multiplicação: <exp>::= <exp>+<exp> <exp>*<exp> (exp) <num> <num> ::= <num><digito> <digito> <digito> ::= 0 1 2 3 4 5 6 7 8 9
Análise sintática BNF Sentenças simples consistem de uma frase nominal e de uma frase verbal seguida de um ponto, da seguinte maneira: <sentence> ::= <frase_nominal><frase_verbal>. Deve-se saber descrever a estrutura de uma frase nominal e de uma frase verbal: <frase_nominal> ::= <artigo><substantivo> <artigo> ::= um a <substantivo> ::= garota cachorro <frase_verbal> ::= <verbo> <frase_nominal> <verbo> ::= viu abraça
Análise sintática Cada regra gramatical apresentada consiste de uma string colocada entre < e >, esta string é o nome da estrutura que está sendo descrita; O símbolo ::= pode ser lido como consiste de ou é o mesmo que ; Após o símbolo ::=, temos uma sequência de outros nomes e símbolos;
Análise sintática Construção de uma sentença legal: Inicia-se com o símbolo <sentença> e prossegue-se trocando o lado esquerdo por alternativas do lado direito nas regras; Este processo criará uma derivação na linguagem; Desta forma, podemos construir a sentença: A garota viu um cachorro ;
Análise sintática Montando a derivação da sentença: a garota viu um cachorro <sentença> <frase_nominal><frase_verbal>. <artigo><substantivo><frase_verbal>. a <substantivo><frase_verbal>. a garota <frase_verbal>. a garota <verbo><frase_nominal>. a garota viu <frase_nominal>. a garota viu <artigo><substantivo>. a garota viu um <substantivo>. a garota viu um cachorro. Pode-se começar com a sentença a garota viu um cachorro, e voltar até <sentença> para provar que é uma sentença válida da linguagem.
Análise sintática - Extensão da BNF EBNF (Extend BNF) Definição EBNF para uma calculadora Definição de sintaxe para uma linguagem Definição EBNF para uma linguagem de programação simples
Análise sintática Recursão a Esquerda Na gramática a seguir, o não-terminal E representa expressões consistindo em termos separados pelo operador +; T representa termos consistindo em fatores separados pelo operador *; e F representa fatores que podem ser expressos entre parênteses ou identificadores. E E+T T T T*F F F (E) id Essa gramática não pode ser usada com o método de análise descendente pois é recursiva a esquerda.
Análise sintática Recursão a Esquerda Gramáticas são recursivas à esquerda se possui um não-terminal A para o qual existam derivações do tipo A Aα para uma cadeia α. Para o par de produções recursivas à esquerda A Aα β A substituição abaixo elimina a recursão imediata à esquerda: A βa A αa ε Nenhuma outra modificação é requerida a partir de A.
Análise sintática Recursão a Esquerda Gramática para expressões simples: E E + T T T T * F F F ( E ) id Aplicando transformação na Primeira Regra E E + T T é do tipo A Aα β Obtemos: A βa E TE A αa ε E +TE ε
Análise sintática Recursão a Esquerda Gramática para expressões simples: E E + T T T T * F F F ( E ) id Aplicando transformação na Segunda Regra E T * F F é do tipo A Aα β Obtemos: A βa T FT A αa ε E *FT ε
Análise sintática Recursão a Esquerda Assim, obtemos a partir de: E E + T T T T * F F F ( E ) id A gramática equivalente sem recursão à esquerda: E TE E +TE T FT T *FT ε F (E) id
Análise sintática Recursão a Esquerda Exemplo 2: A Aa b Para o par de produções recursivas à esquerda A Aα β Exemplo 3: S SS+ SS* a Exemplo 4: S Sa B B Bb c Considere para eliminar a recursão A βa A αa ε
Análise sintática Recuperação de erro O recuperador de erros em um analisador sintático possui objetivos simples, mas desafiadores: Informar a presença de erros de forma clara e precisa; Recuperar-se de cada erro com rapidez suficiente para detectar erros subsequentes; Acrescentar um custo mínimo no processamento de programas corretos. Como um recuperador de erro deve informar a presença de um erro? No mínimo ele precisa informar o local no programa fonte onde o erro foi detectado, pois existe uma boa chance de que o local exato do erro seja em um dos tokens anteriores.
Análise sintática Recuperação de erro Recuperação em nível de frase Ao detectar um erro, o analisador sintático pode realizar a correção local sobre o restante da entrada. Uma correção local típica compreende a substituição de uma vírgula por um ponto-e-vírgula, exclusão de um ponto-e-vírgula desnecessário. A escolha da correção local fica a critério do projetista do compilador.
Análise sintática Recuperação de erro Produções de Erro Nesta estratégia de recuperação de erro podemos estender a gramática da linguagem em mãos com produções que geram construções erradas, antecipando assim os erros mais comuns.
Análise sintática Não é possível enumerar a sintaxe de todos os programas das mais diferentes linguagens; É necessário uma maneira de definir um conjunto infinito usando uma descrição finita: A sintaxe deuma linguagem é definida através deuma gramática; Gramática: conjunto de regras que definem todos os construtores que podem ser aceitos na linguagem.
Análise sintática Fortran foi definido através da especificação de algumas regras em inglês; Algol 60 foi definido através de uma gramática livre de contexto desenvolvida por Jonh Backus; Essa gramática ficou conhecida como BNF (Backus-Naur Form); BNF foi utilizada posteriormente na definição de várias linguagens como C, Pascal e Ada; BNF é uma metalinguagem pois consiste numa linguagem para descrição de outras linguagens.
Análise sintática Observe os dois trechos de código a seguir, sendo o código a. em C e o código b. em Pascal a. b. while(x!=y) while x<>y do { begin...... } end Ambas possuem a mesma estrutura conceitual, porém, diferem na aparência léxica; Quando duas construções diferem apenas no nível léxico, se diz que elas seguem a mesma sintaxe abstrata e diferem na sintaxe contreta.
Análise sintática Com tudo isso, é possível concluir que a descrição sintática de uma linguagem: Ajuda o programador a saber como escrever um programa sintaticamente correto; Pode ser usada para determinar se um programa está sintaticamente correto este é exatamente o trabalho do compilador!
Análise sintática - Análise Top-Down Como reconhecer se uma sentença está de acordo com uma gramática? Pode-se implementar reconhecedores de sentença Recursivamente, com retrocesso Com mecanismo preditivo First e Follow Para usar os reconhecedores, primeiramente deve-se transformar a Gramática Livre de Contexto Eliminação de produções vazias Eliminação de recursividade a esquerda Fatoração de uma gramática
Análise sintática Conjuntos First First(α) Definição informal: conjunto de todos os terminais que começam com qualquer sequência derivável de α Definição formal Se existe um t T e um β V* tal que α * t β então t First(α) Se α * ε então ε First(α) A B C D first(a) = {b, c, d} B b C c D d first(b)= {b} first(c)= {c} first(d)= {d}
Análise sintática Conjuntos First Para determinar o FIRST(A): Se a é terminal, então o first(a) = a; Se A é não terminal e A aα é uma produção, então se acrescenta a ao conjunto de first de A, logo: first(a)=a; Se A ε é uma produção ε, logo first(a)=ε; Se A Y1Y2...Yk é uma produção, então todo i tal que todos Y1...Yi-1 são não terminais e FIRST(Yj) contém ε, onde j=1,2...i-1. acrescente todo símbolo diferente de ε de FIRST(Yj) a FIRST(A). Se ε FIRST(A), para todo i=1,2..k. então acrescente ε a FIRST(A).
Análise sintática Conjuntos First E TE E +TE ε T FT T *FT ε F (E) id First(E) = {? } First(E ) = {? } First(T) = {? } First(T ) = {? } First(F) = {? }
Análise sintática Conjuntos First E TE E +TE ε T FT T *FT ε F (E) id First(E) = First(T) = First(F) ={ (,id } First(E ) = { +, ε} First(T) = First(F) = { (, id } First(T ) = { *, ε } First(F) = { (, id }
Análise sintática Conjuntos First E TE E +TE ε T FT T *FT ε First(E) = First(T) = First(F) ={ (,id } First(E ) = { +, ε} First(T) = First(F) = { (, id } First(T ) = { *, ε } F (E) id First(F) = { (, id } Se F derivasse em ε seria preciso incluir o first(t ) em first(t)
Análise sintática Conjuntos First E TE E +TE ε T FT H E T T *FT ε F (E) id First(E) = First(T) = First(F) ={ (,id } First(E ) = { +, ε} First(T) = First(F) = { (, id } First(H) = {? } First(T ) = { *, ε } First(F) = { (, id }
Análise sintática Conjuntos First E TE E +TE ε T FT H E T T *FT ε F (E) id First(E) = First(T) = First(F) ={ (,id } First(E ) = { +, ε} First(T) = First(F) = { (, id } First(H) = { First(E ) U First(T) } First(T ) = { *, ε } First(F) = { (, id }
Análise sintática Conjuntos First E TE E +TE ε T FT H E T T *FT ε F (E) id First(E) = First(T) = First(F) ={ (,id } First(E ) = { +, ε} First(T) = First(F) = { (, id } First(H) = { +, (, id } First(T ) = { *, ε } First(F) = { (, id }
Análise sintática Conjuntos Follow Se A é um não-terminal, o follow(a) é o conjunto de terminais imediatamente seguintes (à direita) de A
Análise sintática Conjuntos Follow Para determinar follow(a) 1. Colocar $ em follow(s) se S é o símbolo de partida. $ é o marcador de fim de entrada durante análise 2. Se existe uma produção A αbβ e β ε então tudo que estiver em first(β), exceto ε, deve ser adicionado em follow(b) 3. Se existe uma produção A αb ou A αbβ onde first(β) contem ε (β ε), então tudo que está em follow(a) está em follow(b)
Análise sintática Conjuntos Follow E TE E +TE ε T FT T *FT ε F (E) id First(E) = First(T) = First(F) ={ (,id } First(E ) = { +, ε} First(T) = First(F) = { (, id } First(T ) = { *, ε } First(F) = { (, id } Follow(E) = { ), $ } Follow(E ) = Follow(E) = { ), $ } Follow(T) = First(E ) U Follow(E) U Follow(E ) = { +, ), $} Follow(T ) = Follow(T) = {+, ), $} Follow(F) = First(T ) U Follow(T) U Follow(T ) = { *, +, ), $}
Análise sintática Conjuntos Follow Regra 2 e regra 1 E TE E +TE ε T FT T *FT ε F (E) id First(E) = First(T) = First(F) ={ (,id } First(E ) = { +, ε} First(T) = First(F) = { (, id } First(T ) = { *, ε } First(F) = { (, id } Follow(E) = { ), $ } Follow(E ) = Follow(E) = { ), $ } Follow(T) = First(E ) U Follow(E) U Follow(E ) = { +, ), $} Follow(T ) = Follow(T) = {+, ), $} Follow(F) = First(T ) U Follow(T) U Follow(T ) = { *, +, ), $}
Análise sintática Conjuntos Follow Regra 3 E TE E +TE ε T FT T *FT ε F (E) id First(E) = First(T) = First(F) ={ (,id } First(E ) = { +, ε} First(T) = First(F) = { (, id } First(T ) = { *, ε } First(F) = { (, id } Follow(E) = { ), $ } Follow(E ) = Follow(E) = { ), $ } Follow(T) = First(E ) U Follow(E) U Follow(E ) = { +, ), $} Follow(T ) = Follow(T) = {+, ), $} Follow(F) = First(T ) U Follow(T) U Follow(T ) = { *, +, ), $}
Análise sintática Conjuntos Follow Regra 2 e Regra 3 E TE E +TE ε T FT T *FT ε F (E) id First(E) = First(T) = First(F) ={ (,id } First(E ) = { +, ε} First(T) = First(F) = { (, id } First(T ) = { *, ε } First(F) = { (, id } Follow(E) = { ), $ } Follow(E ) = Follow(E) = { ), $ } Follow(T) = First(E ) U Follow(E) U Follow(E ) = { +, ), $} Follow(T ) = Follow(T) = {+, ), $} Follow(F) = First(T ) U Follow(T) U Follow(T ) = { *, +, ), $}
Análise sintática Conjuntos Follow Regra 3 E TE E +TE ε T FT T *FT ε F (E) id First(E) = First(T) = First(F) ={ (,id } First(E ) = { +, ε} First(T) = First(F) = { (, id } First(T ) = { *, ε } First(F) = { (, id } Follow(E) = { ), $ } Follow(E ) = Follow(E) = { ), $ } Follow(T) = First(E ) U Follow(E) U Follow(E ) = { +, ), $} Follow(T ) = Follow(T) = {+, ), $} Follow(F) = First(T ) U Follow(T) U Follow(T ) = { *, +, ), $}
Análise sintática Conjuntos Follow Regra 2 e Regra 3 E TE E +TE ε T FT T *FT ε F (E) id First(E) = First(T) = First(F) ={ (,id } First(E ) = { +, ε} First(T) = First(F) = { (, id } First(T ) = { *, ε } First(F) = { (, id } Follow(E) = { ), $ } Follow(E ) = Follow(E) = { ), $ } Follow(T) = First(E ) U Follow(E) U Follow(E ) = { +, ), $} Follow(T ) = Follow(T) = {+, ), $} Follow(F) = First(T ) U Follow(T) U Follow(T ) = { *, +, ), $}
Análise sintática First Follow S AB first(s)={c} follow(s)={ $ } A c ε first(a)={c, ε} follow(a)={ c } B cbb ca first(b)={c} follow(b)={ $ }
Análise sintática Analisador Top-Down A análise top-down é realizada da raiz para as folhas Parte-se de um não-terminal que é o símbolo inicial da gramática em direção aos terminais
Análise sintática preditiva não-recursiva O símbolo da cadeia de entrada, em análise, é suficiente para determinar qual regra de produção deve ser escolhida São construídos utilizando gramáticas LL ( 1 ) Cadeia de entrada é analisada da esquerda para a direita ( Left-toright) A derivação das produções é feita mais a esquerda ( Leftmost) A cada passo é observado um ( 1) símbolo a frente para determinar que ação deve ser tomada
Análise sintática preditiva não-recursiva Condições Eliminar a recursividade a esquerda Fatorar a gramática Construir o conjunto first e follow
Análise sintática preditiva não-recursiva Construção da tabela preditiva Dimensão1: não terminal X Dimensão2: símbolo de entrada (terminal) t A entrada (X, t) contém a regra da produção a aplicar obtida a partir dos conjuntos first e follow S caa first(s)={c} follow(s)={ $ } A cb B first(a)={b, c, ε} follow(a)={ a } B bcb ε first(b)={b, ε} follow(b)={ a } Não Terminal Símbolo de Entrada a b c S erro erro S caa A A B A B A cb B B ε B bcb erro
Análise Sintática Tabela Preditiva Topo da pilha S S deriva em c? Sim: S caa Não Terminal Símbolo de Entrada a b c S erro erro S caa A A B A B A cb B B ε B bcb erro Entrada: cbca Pilha Entrada Ação S$ cbca$
Análise Sintática Tabela Preditiva S caa substitui na pilha o S por caa Não Terminal Símbolo de Entrada a b c S erro erro S caa A A B A B A cb B B ε B bcb erro Entrada: cbca Pilha Entrada Ação S$ cbca$ S caa caa$ cbca$
Análise Sintática Tabela Preditiva O topo da pilha é igual ao valor do topo de entrada Não Terminal Símbolo de Entrada a b c S erro erro S caa A A B A B A cb B B ε B bcb erro Entrada: cbca Pilha Entrada Ação S$ cbca$ S caa caa$ cbca$ casar(c)
Análise Sintática Tabela Preditiva A deriva em b? Sim: A B Não Terminal Símbolo de Entrada a b c S erro erro S caa A A B A B A cb B B ε B bcb erro Entrada: cbca Pilha Entrada Ação S$ cbca$ S caa caa$ cbca$ casar(c) Aa$ bca$
Análise Sintática Tabela Preditiva Substitui o A na pilha por B Não Terminal Símbolo de Entrada a b c S erro erro S caa A A B A B A cb B B ε B bcb erro Entrada: cbca Pilha Entrada Ação S$ cbca$ S caa caa$ cbca$ casar(c) Aa$ bca$ A B Ba$ bca$
Análise Sintática Tabela Preditiva B deriva em b? Sim: B bcb Não Terminal Símbolo de Entrada a b c S erro erro S caa A A B A B A cb B B ε B bcb erro Entrada: cbca Pilha Entrada Ação S$ cbca$ S caa caa$ cbca$ casar(c) Aa$ bca$ A B Ba$ bca$
Análise Sintática Tabela Preditiva B deriva em b? Sim: B bcb Não Terminal Símbolo de Entrada a b c S erro erro S caa A A B A B A cb B B ε B bcb erro Entrada: cbca Pilha Entrada Ação S$ cbca$ S caa caa$ cbca$ casar(c) Aa$ bca$ A B Ba$ bca$ B bcb
Análise Sintática Tabela Preditiva O topo da pilha é igual ao valor do topo de entrada Não Terminal Símbolo de Entrada a b c S erro erro S caa A A B A B A cb B B ε B bcb erro Entrada: cbca Pilha Entrada Ação S$ cbca$ S caa caa$ cbca$ casar(c) Aa$ bca$ A B Ba$ bca$ B bcb bcba$ bca$ casar(b)
Análise Sintática Tabela Preditiva O topo da pilha é igual ao valor do topo de entrada Não Terminal Símbolo de Entrada a b c S erro erro S caa A A B A B A cb B B ε B bcb erro Entrada: cbca Pilha Entrada Ação S$ cbca$ S caa caa$ cbca$ casar(c) Aa$ bca$ A B Ba$ bca$ B bcb bcba$ bca$ casar(b) cba$ ca$ casar(c)
Análise Sintática Tabela Preditiva B deriva em a? SIM: quando B ε Não Terminal Símbolo de Entrada a b c S erro erro S caa A A B A B A cb B B ε B bcb erro Entrada: cbca Pilha Entrada Ação S$ cbca$ S caa caa$ cbca$ casar(c) Aa$ bca$ A B Ba$ bca$ B bcb bcba$ bca$ casar(b) cba$ ca$ casar(c) Ba$ a$ B ε
Análise Sintática Tabela Preditiva O topo da pilha é igual ao valor do topo de entrada Não Terminal Símbolo de Entrada a b c S erro erro S caa A A B A B A cb B B ε B bcb erro Entrada: cbca Pilha Entrada Ação S$ cbca$ S caa caa$ cbca$ casar(c) Aa$ bca$ A B Ba$ bca$ B bcb bcba$ bca$ casar(b) cba$ ca$ casar(c) Ba$ a$ B ε a$ a$ casar(a)
Análise Sintática Tabela Preditiva O topo da pilha é igual ao valor do topo de entrada Não Terminal Símbolo de Entrada a b c S erro erro S caa A A B A B A cb B B ε B bcb erro Entrada: cbca Pilha Entrada Ação S$ cbca$ S caa caa$ cbca$ casar(c) Aa$ bca$ A B Ba$ bca$ B bcb bcba$ bca$ casar(b) cba$ ca$ casar(c) Ba$ a$ B ε a$ a$ casar(a) $ $ aceita
Análise sintática Analisador Bottom-up A análise top-down é realizada das folhas para a raiz Parte-se dos símbolos terminais em direção ao símbolo inicial da gramática
Análise sintática Analisador Bottom-up id * id F * id T * id T * F T E E E + T T T T * F F F (E) id id F id F id id T * F F id id T T * F F id Entrada: id*id id
Análise sintática Analisador Bottom-up O processo de análise sintática ascendente pode ser encarado como um processo de reduzir uma cadeia w para o símbolo inicial da gramática Redução : operação de substituição do lado direito de uma produção pelo nãoterminal correspondente do lado esquerdo Para a regra A α, α pode ser reduzido em A
Análise sintática Analisador Bottom-up Analisadores sintáticos Bottom-up Analisadores conhecidos como empilha-reduz (shift-reduce) Etapas do reconhecimento: determinar quando reduzir e determinar a produção a ser utilizada para que a análise prossiga
Análise sintática Analisador Bottom-up Componentes do analisador bottom-up Pilha, onde os símbolos a serem reduzidos são empilhados Tabela sintática, que guia o processo de shift e reduce Processo de reconhecimento de uma sentença 1. Empilhar símbolos da cadeia de entrada 2. Quando um lado direito apropriado de uma produção aparece, ele é reduzido (substituído) pelo lado esquerdo da produção 3. Se a análise tiver sucesso, esse processo ocorre até que os símbolos da cadeia de entrada sejam todos consumidos e a pilha fique apenas com o símbolo inicial da gramática
Análise sintática Analisador Bottom-up Gramática: S (L) a L L+S S Entrada: a+a Pilha Cadeia Regra $ (a+a)$ $ (a+a)$ shift ( $( a+a)$ shift a $(a +a)$ reduce S a $(S +a)$ reduce L S $(L +a)$ shift + $(L+ a)$ shift a $(L+a )$ reduce S a $(L+S )$ reduce L L+S $(L )$ shift ) $(L) $ reduce S (L) $S $ aceita
Análise sintática Analisador Bottom-up Operações durante a análise: Shift: coloca-se no topo da pilha o primeiro símbolo da cadeia de entrada Reduce: substitui-se o lado direito do handle pelo seu lado esquerdo Aceita: a cadeia de entrada é reconhecida Erro: a cadeia de entrada não é reconhecida
Referências SEBESTA, Robert W. Conceitos de linguagens de programação. 9ª ed. Porto Alegre: Bookman, 2011. 792 p. ISBN 978-85-7780-791-8. Notas de aula Professora Isabel Harb Manssour