Baixe o app para aproveitar ainda mais
Prévia do material em texto
Linguagens de Programação: Módulo 5 Organização de LP´s: Sintaxe, Semântica e Pragmática Parte 0 - Conceitos Básicos Métodos de Implementação Qualquer programa precisa ser convertido para linguagem de máquina para ser executado; Tradutor descreve como uma entrada em código-fonte será traduzido em código de máquina; Basicamente três métodos: Compilação Interpretação pura Híbrido Métodos de Implementação Compilação (Cobol, C, Pascal) Transformação de programas de alto nível em código de máquina; Requer apenas o executável para execução; Execução mais rápida do programa; Boa parte das verificações de erro já pode ser feita durante a tradução; Nada precisa ser traduzido Transformação lenta; Falta de portabilidade Compilação Interpretação Pura Não existe transformação, os programas são interpretados Execução de 10 a 100 vezes mais lenta que programas compilados Ocupa mais espaço de memória: tabela de símbolos tem que ser carregada na interpretação; Ex.: .BAT, .SH Interpretação Híbrida Meio termo entre compilados e interpretados Tradução de programas em linguagem de alto nível em linguagem intermediária Visa combinar execução eficiente com portabilidade Custo de transformação baixo Tempo de execução razoável a muito bom Ex.: JAVA Interpretação Híbrida Um Modelo de Implementação Código fonte Árvore de parse e tabela de símbolos Translação ou parsing Sintaxe Código objeto intermediário Geração de código Semântica Código objeto binário Ligação ou linking Pragmática Interpretador Semântica Hardware Atribuições de uma LP • Reconhecer programas legais e ilegais • Gerar código correto • Gerenciar armazenamento de código e variáveis • Informar o usuário sobre problemas com o código LP em Duas Partes Código fonte Árvore de parsing e tabela de símbolos Código objeto intermediário Front end (sintaxe) Back end (semântica e geração de codigo) Representação Intermediária Representação Final RI RF Translação ou parsing Geração de código Parte 1 - Front end e Sintaxe Definições Básicas em Sintaxe (1) Alfabeto ou vocabulário é um conjunto de símbolos usados em uma linguagem. Representado pela letra grega 1 = {0,1} 2= {a..z, A..Z, 0..9} String, sentença, ou palavra é uma seqüência de zero ou mais símbolos de um vocabulário. Ex. 00001001011 é uma sentença do vocabulário 1 Definições Básicas em Sintaxe (2) Dicionário é o conjunto de todas as sentenças que eu posso formar sobre um vocabulário. Representado por * Linguagem L definida sobre é qualquer subconjunto de * Definindo uma linguagem sobre um alfabeto Existem muitos meios para se definir uma linguagem sobre um alfabeto. O mais básico é enumerar todas as sentenças possíveis de uma linguagem. Por exemplo = {0,1}, L = {, 0, 1, 01, 10, 111} Definindo a Sintaxe de uma Linguagem de Programação Evidentemente o método de enumeração de sentenças não é viável para uma linguagem de programação. Normalmente combina-se dois métodos para definir o que é aceitável numa LP. Um método é usado para analisar lexicamente a seqüência de caracteres o código fonte e identificar quais são os bastões (tokens) existentes naquele código. Ex.: palavras chaves, operadores, variáveis, etc. Outro método é usado para checar se os bastões formam uma gramática aceitável na LP, isto é chamado de parsing. Processamento Sintático em LPs Desta forma, compiladores e interpretadores usam uma combinação de processadores de linguagem regulares e processadores de gramáticas para produzir uma tabela de símbolos e uma árvore de parsing. Estas duas estruturas serão então usadas para se produzir código intermediário “interpretável” (como em Java) ou “linkável” em código objeto (como em C). Front end scanner ou analisador léxico parser Erros Código fonte RI tokens Desta forma o front end de uma LP é normalmente composto de dois módulos. Funções do Front End • Reconhecer se o código fonte é legal • Reportar erros de sintaxe • Produzir RI (representação intermediária) • Criar mapeamento preliminar de armazenamento • Arrumar o código para o back end Parte 1.1 - Análise Léxica Analisador Léxico scanner ou analisador léxico parser Erros Código fonte RI tokens Função do Analisador Léxico • Mapear caracteres em tokens eliminando caracteres inúteis tais como tabs, blanks, comentários, etc. Por exemplo: A expressão “x = x + y;” torna-se, <id,x> = <id,x> + <id,y>; Tokens são a unidade básica da sintaxe. Exemplos típicos de tokens são: literal-número, identificador, +, -, /, do, end token Analisador Léxico Tokens seguem para o parser Métodos para Definição de Analisadores Léxicos Analisadores léxicos (scanners) normalmente usam métodos que enumeram os padrões de caracteres aceitáveis pela LP. Entre estes métodos estão as expressões regulares. Expressões Regulares (1) Método de definição de uma linguagem que enumera todos os padrões legais aceitos na linguagem, exemplo: Concatenação de x com y: xy Potência xxx ... x , n vezes: xn Sentença vazia, : x0 Fechamento de Kleene: x* = {x0, x1, x2, x3, ...} x+ = {x1, x2, x3, x4, ...}, fechamento positivo Expressões Regulares (2) Exemplo 1 Representando a linguagem L de todas as sentenças em {a,b} onde todos os as sempre precedem os bs L = {w, w = a*b*} Expressões Regulares (3) Continuação: Opção de x ou y: x | y Seleção (não padronizada) : [ x y ] Intervalo de a até g: a-g Qualquer caractere: . Começo de linha: ^ à esquerda Fim de linha: $ à direita Caracteres de controle: \, escape ( ), agrupamento Expressões Regulares (4) Exemplo 2: Nomes válidos de variáveis em uma LP: {w, w = [a-zA-Z][a-zA-Z0-9]*} Exemplo 3: Conjunto de todas as sentenças no alfabeto {a, b, c} em que todos os as sempre precedem os bs e os cs vão em qualquer lugar: L = {w, w = (a|c)*(b|c)*} Expressões Regulares (5) Exemplo “quase” real de utilização de expressões regulares: números válidos em uma LP: digito [0-9] inteiro ( \+ | \- | )(0|[1-9]digito*) decimal inteiro \. (digito*) real (inteiro | decimal) E (\+ | \- | ) digito* Uso de Linguagem Regulares Scanners (analisadores léxicos) transformam linguagens regulares em máquinas de estados finitos que transformam o código fonte em uma seqüência de tokens ou bastões. Por exemplo: if (indice < 0) { indice := indice +1; } <if> <beginExpr> <id 23> <opLessThan> <literal 0> <endExpr> <then> <beginBlock> <beginStmt> <id 23> <attrb> <id 23> <opPlus> <literal 1> <endStmt> <endBlock> Alguns problemas de sintaxe (1) PL/I não tem palavra reservada: if then then then = else; else else = then; FORTRAN e ALGOL ignoravam os blanks, e reconheciam caracteres ambíguos: integerVA do 10 i = 1,25,2 do 10 i = 1.25,2 derrubou uma sonda espacial Alguns problemas de sintaxe (2) FORTRAN66 e as primeiras versões de C tinham um fechamento transitivo limitadoque limitava o número de caracteres em nomes de variáveis a 6 caracteres. As linguagens de hoje ainda têm problemas para tratar caracteres especiais dentro de uma string. Por exemplo: newline, tab, quote, delimitadores de comentários, etc. Limites de linguagens regulares • Nem todas as linguagens são regulares. Neste caso, não se pode construir máquinas de estados finitos (DFA) para reconhecer este tipo de linguagem. Por exemplo: L = {pkqk} DFAs não conseguem contar! Parte 1.2 - Gramáticas Livres de Contexto e Parsers Parser scanner ou analisador léxico parser Erros Código fonte RI tokens Função do Parser • Reconhecer a sintaxe livre de contexto formada pelos tokens produzidos pelo analisador léxico para construir as representações intermediárias para geração de código objeto. • Produzir mensagens de erros úteis ao usuário e quando possível corrigir estes erros. Geradores de parsers podem automatizar boa parte deste trabalho. => YACC, Cup, JavaCC Gramáticas Generativas Sintaxe livre de contexto são interpretadas por gramáticas generativas que determinam as palavras legais de uma linguagem através de regras de produção, por exemplo Gramáticas BNF (Backus Naur Form) G = <, N, P, S>, onde: é um conjunto de símbolos terminais (o alfabeto) N é um conjunto de símbolos não terminais P é um conjunto de regras de produção S é o símbolo de partida Gramáticas BNF (1) Regras de produção são regras da forma A::= , onde A é um símbolo não terminal e é uma seqüência de símbolos terminais e não terminais. Exemplo 1: regras de produção <corpo> ::= BEGIN <declaração> END <declaração> ::= <palavra> ; <declaração> <declaração> ::= <palavra> Gramáticas BNF (2) Exemplo 2: Linguagem com alfabeto {a,b} onde os as sempre precedem os bs . G = <, N, P, S>, onde: = {a,b}, alfabeto N = {b}, conjunto de símbolos não terminais P = {b ::= ab, b ::= bb, b ::= }, conjunto de regras de produção S = b, símbolo de partida L(G) é a linguagem gerada a partir de G Gramáticas BNF (3) Exemplo 2 (continuação): Para gerar a palavra aaabb na linguagem L(G) com alfabeto {a,b} podemos usar a seguinte derivação. S = b b ab aab aaab aaabb aaabbb aaabb aaabb Também escrito como: b * aaabb , ou seja, b deriva aaabb com zero ou mais operações de derivação Gramáticas BNF: Definições Forma Sentencial (FS) é uma seqüência válida de símbolos terminais e não terminais que podem ser obtidos a partir das regras de produção de uma gramática. Ex.: aab; aaabbb; e aaabb Operação de derivação (OD) é ato de aplicar uma regra de produção a uma FS com símbolos não terminais para se obter uma outra FS válida. Ex.: aaabb aaabbb Derivação é uma seqüência de ODs que leva do símbolo de partida b a uma palavra válida (conjunto de símbolos terminais) na linguagem L(G). Exemplo de BNF Expressões simples sobre números e identificadores: <meta> ::= <expr> <expr> ::= <expr><op><expr> | num | id <op> ::= + | - | * | / símbolo de partida não terminais terminais Análise Léxica versus Parsing Expressões regulares: • são usadas para reconhecer tokens, isto é classificar identificadores, números, palavras chaves, etc. • são mais concisas e mais fáceis de entender • tornam fácil construir analisadores léxicos a partir de DFA Gramáticas livres de contextos: • são usadas para reconhecer a gramática de LPs • podem contar: (), begin ... end, e if ... then ... else, etc. • são usadas para construir estruturas intermediárias (árvores de parsing e tabelas de símbolos) A gramática da linguagem C tem cerca de 200 regras de produção. Exemplo de Derivação à Direita Expressão “x + 2 - y” regra 1: <meta> regra 2: <expr> regra 4: <expr> <op> <expr> regra 6: <expr> <op> <id,y> regra 1: <expr> - <id,y> regra 3: <expr> <op> <expr> - <id,y> regra 5: <expr> <op> <num,2> - <id,y> regra 4: <expr> + <num,2> - <id,y> final: <id, x> + <num,2> - <id,y> derivação à direita (rightmost derivation) 1) <meta> ::= <expr> 2) <expr> ::= <expr><op><expr> 3) | num 4) | id 5) <op> ::= + 6) | - 7) | * 8) | / Árvore de Parsing meta op expr expr expr expr op expr + - <id,y> <num,2> <id,x> Problema da Precedência Considere a seguinte expressão “x + 2 * y”, sua árvore de parsing será: meta op expr expr expr expr op expr + * <id,y> <num,2> <id,x> Uma avaliação desta árvore produzirá (x+2)*y. Isto está errado. Nossa gramática não tem a noção de precedência na avaliação das regras de produção. A precedência deve ser embutida na própria gramática. Considere esta outra gramática ... Solucionando o Problema 1) <meta> ::= <expr> 2) <expr> ::= <expr><op><expr> 3) | num 4) | id 5) <op> ::= + 6) | - 7) | * 8) | / 1) <meta> ::= <expr> 2) <expr> ::= <expr> + <termo> 3) | <expr> - <termo> 4) | <termo> 5) <termo> ::= <termo> * <fator> 6) | <termo> / <fator> 7) | <fator> 8) <fator> ::= num 9) | id A gramática força a precedência pois “termos” derivados de expressões multiplicativas são sempre derivados de expressões de soma e subtração. Problema da Ambigüidade Quando uma gramática tem mais de uma derivação para uma forma sentencial há um problema de ambigüidade. Considere o clássico problema do “if”: (A) <stmt> ::= if <expr> then <stmt> (B) | if <expr> then <stmt> else <stmt> (C) | outras declarações Considere a forma setencial “if E1 then if E2 then S1 else S2”. Esta FS tem duas derivações: (A)(B) e (B)(A). A correta seria a primeira pois normalmente o “else” casa com o “then” mais próximo. Solucionando o Problema Como no caso da precedência, a ambigüidade deve ser resolvida pela própria gramática. A) <stmt> ::= <casado> B) | <solto> C) <casado> ::= if <expr> then <casado> else <casado> D) | outras declarações E) <solto> ::= if <expr> then <stmt> F) | if <expr> then <casado> else <solto> Esta gramática gera a mesma linguagem da anterior mas sempre "casa" cada else com o then mais próximo. Como os Parsers são Construídos gerador de parser (yacc, bison, javaCC) parser gramática tokens RI Curiosidade: yacc, bison e JavaCC funcionam como linguagens funcionais Exemplo YACC file : program | module ; program : program_heading semicolon block DOT ; program_heading : PROGRAM identifier | PROGRAM identifier LPAREN identifier_list RPAREN ; identifier_list : identifier_list comma identifier | identifier ; block : label_declaration_part constant_definition_part type_definition_part variable_declaration_part procedure_and_function_declaration_partstatement_part ; Exemplo de trecho de um arquivo YACC para a linguagem Pascal. Arquivo completo está no FTP do curso. Exemplo JavaCC void IfStatement(): /* * The disambiguating algorithm of JavaCC automatically binds dangling * else's to the innermost if statement. The LOOKAHEAD specification * is to tell JavaCC that we know what we are doing. */ {} { "if" "(" Expression() ")" Statement() [ LOOKAHEAD(1) "else" Statement() ] } void WhileStatement(): {} { "while" "(" Expression() ")" Statement() } void DoStatement(): {} { "do" Statement() "while" "(" Expression() ")" ";" } Exemplo de trecho de um arquivo JavaCC para a linguagem Java. Tipos de Parsers Top-down (de cima para baixo): • começa na raiz da árvore de derivação • pega uma produção e tenta encaixar ela a entrada (tokens) • pode requerer backtracking Bottom-up (de baixo para cima): • começa nas folhas da árvore de derivação • começa num estado aceitável de tokens de entrada • à medida que os tokens são consumidos muda de estado que codificam as novas possibilidades (reconhece prefixos válidos) • usa uma pilha para armazenar tanto o estado como a forma sentencial atual O YACC usa um parser LALR de baixo para cima. O JavaCC usa um parser de cima para baixo. Discussão de técnicas de Parsing estão fora o escopo deste curso. Este é um assunto extenso e algo complexo. Descrição destas técnicas podem ser encontradas em qualquer bom livro de compiladores.
Compartilhar