Sintaxe de uma Linguagem n n Vantagens de uma Gramática Cada LP possui regras que descrevem a estrutura sintática dos programas. Especificada através de uma gramática livre de contexto, BNF (Backus-Naur Form). Uma gramática dá uma especificação precisa e fácil de entender da sintaxe de uma linguagem de programação. Para algumas classes de gramáticas, é possível gerar automaticamente um parser eficiente. Além disso as ferramentas usadas indicam ambiguidades e outras construções difíceis de serem reconhecidas e que passariam desapercebidas. Tem relação direta com a estrutura da linguagem usada para tradução/compilação. Fácil de ser estendida/atualizada. n n n n 1 2 Analisador Sintático - Parser Parse tree token programa fonte Analisador Léxico Parser Papel do Analisador Sintático Representação intermediária Resto do front-end n n pede próximo token n n Tabela de Símbolos 3 4 Tipos de Parsers para Gramáticas n n n Métodos de parsing universais: funcionam para qualquer gramática, mas são muito ineficientes (inviáveis para uso prático). Top down: constroem as parse trees a partir da raiz em direção às folhas. Bottom-up: constroem as parse trees a partir das folhas. 5 Obter uma cadeia de tokens do Scanner e verificar se pode ser gerada pela gramática. Relatar quaisquer erros de sintaxe de uma forma inteligível. Deve se recuperar de erros que ocorram mais comumente. Assumimos que a saída de um Parser seja uma representação de uma árvore gramatical. Parsers Top-down ou Bottom-up n n Em parsers top-down ou bottom-up a leitura do arquivo é sempre feita da esquerda para a direita, um símbolo de cada vez. Só funcionam para um subconjunto de gramáticas, que na prática são suficientes para a grande maioria das linguagens de programação. 6 1 Tratamento de Erros de Sintaxe n n n n Tratamento de Erros de Sintaxe n Léxicos: erro de grafia. Sintáticos: desbalanceamento de expressões. Semânticos: operador aplicado a um operando incompatível. Lógicos: chamada infinitamente recursiva. n n Relatar presença de erros clara e acuradamente. Recuperar-se do erro rapidamente, para possibilitar a detecção de erros subseqüentes. Não deve gerar overhead significativo para programas corretos. 7 8 Estratégias de Recuperação de Erros n n n n Gramáticas Livres de Contexto n Modalidade do desespero Nível de frase Produções de erro Correção global n n n n cmd → if expr then cmd else cmd Um conjunto de símbolos terminais (tokens). Um conjunto de símbolos não-terminais: “variáveis”. Um conjunto de produções, cada produção consiste de um não-terminal, uma seta, e uma seqüência de tokens e/ou não terminais. Um não-terminal designado como símbolo inicial. 9 Exemplo 1 10 Exemplo 1 (notação abreviada) expr → expr op expr expr → ( expr ) expr → - expr expr → id op → + op → op → * op → / op → ↑ expr → expr op expr | ( expr ) | - expr | id op → + | - | * | / | ^ 11 12 2 Exemplo 2 Parse Trees n bloco → begin cmd_opc end cmd_opc → lista_cmd | ∈ lista_cmd → lista_cmd ; cmd | cmd n Mostra graficamente como o símbolo inicial de uma gramática deriva uma string da linguagem. Para uma produção A → XYZ A X Y Z 13 14 Derivações n n n Ambigüidade Visão alternativa para verificar a validade de uma entrada para uma dada gramática. Fornece uma precisa descrição da construção top-down da árvore gramatical. Ex: derivar a cadeia –(id+id) n n Parse-tree gera uma string, mas uma string pode possuir várias parse-trees, se a gramática for ambígüa. Solução: usar sempre gramáticas nãoambígüas, ou gramáticas ambígüas com informações adicionais sobre como resolver ambigüidades. 15 16 Gramática para Expr. Aritméticas Exemplo: Duas Parse Trees E → E + E | E * E | - E | ( E ) | id Ex: -(id + id) Ambigüidade: id + id * id E E E id 17 + * E id E E E + E id id E * id E id 18 3 Expressões Regulares × Gramáticas Livres de Contexto n Exemplo Tudo que pode ser descrito por uma expressão regular pode ser descrito com gramáticas livres de contexto, mas: n n n n As regras léxicas são especificadas mais simplesmente com expressões regulares Expressões Regulares geralmente são mais concisas e simples de entender Podem ser gerados analisadores léxicos mais eficientes a partir de expressões regulares Estrutura/modulariza o front-end do compilador n n n (a | b)*abb A 0 → aA0 | bA 0 | aA 1 A 1 → bA2 A 2 → bA3 A3 → ∈ Conseqüentemente, pode-se converter um AFN em uma gramática. 19 20 Expressões Regulares × Gramáticas Livres de Contexto n n Eliminando Ambigüidades Expressões regulares são convenientes para especificar a estrutura de construções léxicas, como identificadores, constantes, palavras chave, etc. Em geral usa-se gramáticas para especificar estruturas aninhadas, como parenteses, begin-end, if-then-else, etc. n n cmd → if expr then cmd | if expr then cmd else cmd | outro Nas linguagens de programação normalmente a regra é que o else casa com o then mais próximo. 21 22 Eliminando Ambigüidades n Eliminando Ambigüidades if E1 then S1 else if E2 then S2 else S3 n 23 if E1 then if E 2 then S1 else S2 24 4 Solução Recursão à Esquerda cmd → cmd_associado | cmd_não_associado cmd_associado → if expr then cmd_associado else cmd_associado | outro cmd_não_associado → if expr then cmd | if expr then cmd_associado else cmd_não_associado n É possível um analisador gramatical descendente recursivo rodar para sempre. expr → expr + termo | termo A → Aα | β 25 26 Eliminação da Recursão à Esquerda n n Eliminação da Recursão à Esquerda Uma gramática é recursiva à esquerda se possui um não-terminal A tal que exista uma derivação de A que gera Aα, para alguma cadeia α. Existem técnicas/algoritmos para eliminar a recursão à esquerda. n n A → Aα | β pode ser substituído por A → βA’ A’ → αA’ | ∈ E →E + T |T pode ser substituído por E → TE’ E’ → +TE’ | ∈ 27 28 Eliminação da Recursão à Esquerda Exemplo n n Gramática para expressões aritméticas: E→ E+T|T T→ T*F|F F → (E) | id Eliminando a recursão imediata à esquerda: E → TE’ E’ → +TE’ | ∈ T → FT’ T’ → *FT’ | ∈ F → (E) | id n n n 29 Não importa quantas produções -A existam, podemos eliminar a recursão imediata das mesmas pela seguinte técnica: A → Aα1 | Aα2 | ... | Aαm | β1 | β2 | ... | βn Onde nenhum βi começa por um A. Em seguida: A → β1 A’ | β2 A’ | ... | βn A’ A’ → α1 A’ | α2 A’ | ... | αmA’ | ∈ Eliminar recursão envolvendo derivações em vários passos: S → Aa | b A → Ac | Aad | bd | ∈ ⇒ A → Ac | Sd | ∈ S → Aa | b A → bdA’ | A’ A’ → cA’ | adA’ | ∈ 30 5 Fatoração à Esquerda n n n Fatoração à Esquerda Técnica de transformação gramatical usada para produzir uma gramática adequada à análise sintática preditiva. Combina os casos em que há mais de uma alternativa possível a partir do reconhecimento de um token. Existem técnicas/algoritmos para fazer a fatoração à esquerda. Ex: cmd → if expr then cmd else cmd | if expr then cmd Para cada não-terminal A, encontrar o mais longo prefixo α comum a duas ou mais alternativas. Substituir todas as produções de A: A → αβ1 | αβ2 | ... | αβn | γ por: A → αA’ | γ A’ → β1 | β2 | ... | βn Ex: S → iEtS | iEtSeS | a S → iEtSS’ | a ⇒ E→b S’ → eS | ∈ E→b n n n 31 32 Análise Gramatical Preditiva n n n n Análise Sintática Top-Down Análise gramatical descendente recursiva é um método top-down de análise sintática. Não-terminal de uma G ⇒ Procedimento. Análise gramatical preditiva é uma forma especial de análise gramat. desc. recursiva: lookahead. A seqüência de procedimentos chamada no processamento da entrada ⇒ árvore gramatical. Tentativa de se encontrar uma derivação mais à esquerda para uma cadeia de entrada. Análise sintática de descendência recursiva: técnica de análise sintática top-down que suporta retrocesso. Ex: S → cAd A → ab | a entrada: w = cad Análise sintática preditiva: caso especial de análise sintática de descendência recursiva sem retrocesso. Relativamente fáceis de escrever à mão. Só reconhecem alguns tipos de gramáticas: não suportam recursão à esquerda, precisa fazer fatoração à esquerda. n n n n n 33 Analisadores Sintáticos Preditivos n n n Escrevendo-se cuidadosamente uma gramática, eliminando-se a recursão à esquerda e fatorandose à esquerda a gramática resultante ⇒ ASP. Exemplo: reconhecer cmd → if expr then cmd else cmd | while expr do cmd | begin lista_cmds end Pode fazer uso na implementação de diagramas de transição, ou ser implementado recursivamente. 35 34 Diagramas de Transições para Analisadores Sintáticos Preditivos n Gramática para expressões aritméticas sem recursão à esquerda: E → TE’ E’ → +TE’ | ∈ T → FT’ T’ → *FT’ | ∈ F → (E) | id 36 6 Diagramas de Transições Simplificados Análise Sintática Preditiva Não-Recursiva n ⇓ n 1. 2. ⇓ 3. ⇒ Faz-se uso explicitamente de uma pilha, ao invés de implicitamente através de chamadas recursivas. Possibilidades: X=a=$ X=a≠$ X é um nãoterminal. 37 38 Programa de Análise Sintática Preditiva faça ip apontar para o primeiro símbolo de w$ ; repetir seja X o símbolo ao topo da pilha e a o símbolo apontado por ip ; se X for um terminal ou $ então se X = a então remover X da pilha e avançar i p senão // X é um não-terminal se M[X, a] = X→Y1Y2...Yk então início remover X da pilha; empilhar Yk,Yk-1,...,Y1, com Y1 ao topo da pilha; escrever a produção X →Y1Y2...Yk fim senão erro( ); até que X = $ // a pilha está vazia PRIMEIRO e SEGUINTE n n n n n 39 Auxilia a construção de um analisador sintático preditivo. Permitem preencher as entradas de uma tabela sintática preditiva. Os conjuntos de tokens produzidos pela função SEGUINTE podem ser usados como tokens de sincronização. PRIMEIRO(α) ⇒ conjunto de terminais que começam as cadeias derivadas de α. SEGUINTE(A) ⇒ conjunto de terminais a que figuram à direita do não-terminal A. 40 Tabela Sintática NãoTerminal PRIMEIRO e SEGUINTE Símbolo de Entrada id E E’ E→TE’ T T’ F T → FT’ + * ( ) $ E’ → ∈ E’ → ∈ T’ → ∈ T’ → ∈ E → TE’ E’ → +TE’ T → FT’ T’ → ∈ T’ → *FT’ F → id 1. F →(E) 2. Pilha Entrada $E $E’ T $E’ T’ F $E’ T’ id $E’ T’ $E’ $E’ T + $E’ T $E’ T’ F $E’ T’ id $E’ T’ $E’ T’ F * $E’ T’ F $E’ T’ id $E’ T’ $E’ $ id + id * id $ id + id * id $ id + id * id $ id + id * id $ id + id * id $ id + id * id $ id + id * id $ id + id * id $ id + id * id $ id + id * id $ id + id * id $ id + id * id $ id + id * id $ id + id * id $ id + id * id $ id + id * id $ id + id * id $ Saída E → TE’ T → FT’ F → id T’ → ∈ E’ → +TE’ T → FT’ F → id T’ → *FT’ Gramática: 3. E → TE’ E’ → +TE’ | ∈ T → F T’ T’ → * F T’ | ∈ F → (E) | id F → id T’ → ∈ E’ → ∈ Entrada: id + id * id 41 Regras para computar PRIMEIRO(X): Se X for um terminal, então PRIMEIRO (X) é {X}. Se X → ∈ for uma produção, adicionar ∈ a PRIMEIRO(X). Se X for um não-terminal e X → Y1 Y2 ...Yk uma produção, colocar a em PRIMEIRO(X) se, para algum i, a estiver em PRIMEIRO(Yi ) e ∈ estiver em todos PRIMEIRO(Y1 ), ..., PRIMEIRO(Yi - 1 ); tudo o que estiver em PRIMEIRO(Y1 ) estará certamente em PRIMEIRO(X). 42 7 PRIMEIRO e SEGUINTE 1. 2. 3. Exemplo E → TE’ E’ → +TE’ | ∈ T → FT’ T’ → *FT’ | ∈ F → (E) | id PRIMEIRO(E) = PRIMEIRO(T) = PRIMEIRO(F) = { (, id} PRIMEIRO(E’) = {+, ∈} PRIMEIRO(T’) = {*, ∈} SEGUINTE(E) = SEGUINTE(E’) = { ), $} SEGUINTE(T) = SEGUINTE(T’) = {+, ), $} SEGUINTE(F) = {+, *, ), $} Regras para computar SEGUINTE(A): Colocar $ em SEGUINTE(S), onde S é o símbolo de partida e $ o marcador de fim de entrada à direita. Se existir uma produção A → αBβ, então tudo em PRIMEIRO(β), exceto ∈, é colocado em SEGUINTE(B). Se existir uma produção A → αB ou uma produção A → αBβ onde PRIMEIRO( β) contém ∈ (isto é, β ⇒ ∈), então, tudo em SEGUINTE(A) está em SEGUINTE(B). 43 44 Tabela Sintática Gramática para Tipos de Pascal tipo → tipo_simples | ↑ id | array [ tipo_simples ] of tipo tipo_simples → integer | char | num pontoponto num S → iEtSS’ | a S’ → eS | ∈ E→b NãoTerminal a Símbolo de Entrada S S →a b i t $ S → iEtSS’ S’ → ∈ S’ → eS S’ E e S’ → ∈ n E→b Ex: considere a entrada array [1..5] of integer 45 46 Pseudocódigo para um Analisador Gramatical Preditivo void tipo ( ) { if (lookahead está em { integer, char, num }) tipo_simples ( ); else if (lookahead = = ‘↑’) { reconhecer (‘↑’); reconhecer (id); } else if (lookahead = = array) { reconhecer (array); reconhecer (‘[’); tipo_simples ( ); reconhecer (‘]’); reconhecer (of); tipo ( ); } else error ( ); } void tipo_simples ( ) { if (lookahead = = integer) reconhecer (integer) ; else if (lookahead = = char) reconhecer (char); else if (lookahead = = num) { reconhecer (num); reconhecer (pontoponto); reconhecer (num); } else error ( ); } void reconhecer (token t) { if (lookahead = = t) lookahead = proximo_token ( ); else error ( ); } 47 8