Capítulo 1. 1. Linguagens 1.1. Definição Definição : Uma linguagem L sobre um alfabeto, também designado com frequência por vocabulário, V, é um conjunto de frases, em que cada frase é uma sequência de símbolos pertencentes a V (vocábulos ou palavras). Uma linguagem não admite todas as combinações possíveis dos vocábulos (símbolos do alfabeto). Apenas certas combinações é que dão origem a frases válidas. Normalmente, a linguagem é um conjunto infinito, o que torna a sua enumeração impossível. A definição de uma linguagem é feita em dois passos : 1º) Estabelecer qual o alfabeto a usar; 2º) Indicar as regras que restringem, das combinações possíveis, aquelas que de facto são frases correctas. Relativamente às regras referidas no 2º passo, estas dividem-se em dois conjuntos : As regras sintácticas que definem as formas correctas, estabelecendo as combinações, ou agrupamentos, de símbolos (palavras) possíveis; As regras semânticas definem as condições que têm de ser respeitadas pelos símbolos para que as frases sintacticamente correctas façam sentido, isto é, para que seja possível interpretá-las (compreender e executar a sua mensagem). As regras sintácticas preocupam-se com a estrutura das frases, actuando ao nível das intenções (por ex., indicam que símbolos devem ser usados e a ordem pela qual devem ser escritos). As regras semânticas trabalham ao nível dos valores intrínsecos dos símbolos, ou inferíveis a partir deles, e preocupam-se com o significado das frases (o seu conteúdo semântico), indicando como é que essas frases serão interpretadas. O significado é a informação contida numa frase e é aquilo que realmente nos interessa conhecer para que a comunicação entre dois agentes tenha algum efeito prático.
2 Processadores de linguagens 1.2. Linguagens naturais versus linguagens formais Nas linguagens naturais (línguas faladas no dia-a-dia pelos povos) as frases pertencem à linguagem por razões de facto, isto é, porque as pessoas as usam assim mesmo na sua comunicação quotidiana. As regras surgem posteriormente, com o objectivo de sistematizar e ensinar futuramente a linguagem e organizar (estruturar) essas frases. As linguagens artificiais (que são criadas com o propósito de suportar a comunicação Homem/Máquina) só começam a ser usadas depois de o vocabulário ter sido escolhido e de as regras sintácticas e semânticas terem sido estabelecidas. Além disso, as regras que definem as linguagens artificiais são pensadas de modo a garantir a não-ambiguidade dessas linguagens, o que não acontece nas linguagens naturais. 1.3. Como se especifica uma linguagem As linguagens artificiais linguagens de comandos, linguagens de programação, linguagens de interrogação, etc. têm de ter a sua sintaxe e semântica rigorosamente definidas à custa de regras apropriadas. Essas regras devem ser escritas de forma tão sucinta e objectiva quanto possível, para garantir que são interpretadas (compreendidas e usadas) com facilidade e sem ambiguidade por ambos os agentes presentes na comunicação o Emissor, que será geralmente o utilizador humano, e o Receptor, que será o computador. Neste contexto, a mensagem trocada será um comando, um programa ou uma questão. A gramática tem sido universalmente aceite como notação formal para definir linguagens, servindo o duplo propósito de : ensinar como se escrevem, ou produzem, as frases da linguagem (papel gerador); determinar como se podem analisar essas frases (papel reconhecedor). 2. Processadores de linguagens 2.1. Definição Definição : Seja L(G) a linguagem gerada pela gramática G. Um processador para essa linguagem P L(G), é um programa que, tendo conhecimento da gramática G : lê um texto (sequência de caracteres); verifica se esse texto é uma frase válida de L(G); executa uma acção qualquer em função do significado da frase reconhecida. Todos estes programas são caracterizados por serem constituídos por dois grandes módulos : um de análise, em que se faz o reconhecimento do significado do texto fonte; outro de síntese, em que se reage ao significado identificado, produzindo um determinado resultado. São exemplos de processadores de linguagens : compiladores traduzem linguagens de programação de alto nível para código máquina; assemblers traduzem linguagens de programação de baixo nível para código máquina; interpretadores executam os programas logo após o seu reconhecimento : realizam acções, em vez de traduzirem os programas para uma linguagem de baixo nível; tradutores em geral transformam textos escritos numa linguagem qualquer para outra linguagem qualquer; carregadores reconhecem descrições de dados e carregam essa informação para Bases de Dados, ou para estruturas de dados em memória central;
Processadores de linguagens 3 pesquisadores reconhecem questões ( query ) e pesquisam em Bases de Dados para mostrarem as respostas encontradas; filtros reproduzem, à saída, o texto que receberam à entrada, depois de lhe retirarem certas palavras (ou blocos), ou expandirem palavras (por ex., abreviaturas, ou comandos de inclusão de outros ficheiros), ou simplesmente transformarem palavras (por ex., convertendo para maiúsculas) um caso prático bem conhecido deste tipo de processadores de linguagens é o pré-processador de C; processadores de documentos usados para diversos tipos de manipulação de documentos como, por ex., formatação (caso dos programas LATEX e BIBTEX). Em termos cronológicos, os primeiros processadores de linguagens foram os assemblers, compiladores e interpretadores, todos destinados ao reconhecimento e tradução/execução de linguagens de programação. Actualmente, é mais frequente desenvolverem-se os outros tipos de processadores cujo campo de aplicação é cada vez maior. 2.2. Tarefas Da definição anterior resulta, desde já, a divisão do processamento em duas partes : reconhecimento e reacção. A primeira parte é sempre realizada da mesma forma (qualquer que seja o processador da linguagem) e costuma subdividir-se nas três tarefas seguintes : análise léxica responsável pela leitura sequencial dos caracteres que formam o texto fonte, pela sua separação em palavras e pelo reconhecimento dos vocábulos (símbolos terminais) representados por cada palavra; análise sintáctica encarregue de agrupar os símbolos terminais verificando se formam uma frase sintacticamente correcta, isto é, composta de acordo com as regras sintácticas da linguagem; análise semântica destinada a verificar se as regras semânticas da linguagem são satisfeitas e a calcular os valores associados aos símbolos, de modo a poder conhecer-se o significado completo da frase. A segunda parte não se costuma dividir mais, para não ter de se pormenorizar em que consiste a acção de transformação do significado. Os módulos que irão implementar cada uma das fases de análise e de síntese têm de comunicar entre si. Assim, o analisador léxico passa ao analisador sintáctico os símbolos terminais que reconheceu no texto fonte, enquanto o analisador sintáctico envia ao analisador semântico uma representação interna da forma (estrutura sintáctica) da frase (usando uma árvore de derivação para suportar essa representação); por fim, o analisador semântico passa ao transformador uma árvore de sintaxe decorada que representa o significado da frase. 2.3. Estratégias de processamento Para resolver o problema da reacção, ou transformação da frase de entrada no resultado final, existem duas grandes estratégias diferentes, que são as seguintes : Tradução Orientada pela Sintaxe; Tradução Orientada pela Semântica. A primeira é a mais antiga e a mais usada, e onde todo o processamento é controlado pelo analisador sintáctico : este módulo vai pedindo o próximo símbolo do texto fonte e, à medida que vai progredindo no reconhecimento, vai fazendo, intercaladamente, a análise semântica e a transformação. Não há uma separação nítida entre todas as tarefas e nunca se chega a construir integralmente a árvore de sintaxe decorada, nem tão pouco a árvore de derivação.
4 Processadores de linguagens A segunda abordagem tem vindo a ganhar adeptos, à medida que os recursos máquina aumentam e o seu preço baixa, tendo actualmente uma grande importância. Aqui, todas as tarefas referidas são executadas separadamente, não se distinguindo nenhuma em relação às outras. Além disso, a árvore de derivação é construída explicitamente para todas as restantes etapas trabalharem sobre a árvore de sintaxe decorada. Estas duas abordagens não diferem só na estratégia de representação da informação em memória e na técnica de desenvolvimento dos algoritmos, mas também, e sobretudo, no formalismo de especificação usado para descrever o processador de linguagem : no primeiro caso, recorre-se à gramática tradutora, na segunda, usa-se a gramática atributiva. Enquanto que a Tradução Orientada pela Sintaxe é mais simples de especificar e menos exigente em termos de requisitos de hardware e de software, a Tradução Orientada pela Semântica corresponde a um maior rigor na descrição formal do processador a desenvolver e trás vantagens do ponto de vista da programação. 2.4. Compiladores Os compiladores são ferramentas fundamentais na informática, que transcrevem um texto escrito numa linguagem fonte ( source ) para um texto equivalente escrito numa linguagem alvo ( target ). Texto fonte Compilador Texto alvo Mensagens Figura 1. O compilador visto pelo utilizador. Definição : Um compilador é um processador de linguagens construído propositadamente para reconhecer programas, escritos em linguagens de programação de alto nível, para os traduzir para código binário, ou código máquina, que é uma linguagem de programação de baixo nível directamente reconhecida e executada por um determinado computador (designado por máquina objecto ou máquina final). Definição : Um transcompilador ( cross-compiler ) aceita um texto escrito numa linguagem de alto nível e produz um texto escrito noutra linguagem de alto nível (é um caso particular dos compiladores). As vantagens dos transcompiladores está na rápida implementação do compilador. As desvantagens está em que os programas gerados são mais lentos e ocupam maior espaço de memória. Sobre os compiladores devem salientar-se os seguintes factos : O texto fonte é um programa escrito numa linguagem que permite estruturar as instruções e usar estruturas de dados; O código final (objecto) é código máquina; Existe um grande desnível entre a complexidade das instruções na linguagem fonte e na linguagem objecto. Os compiladores introduzem problemas específicos na análise semântica e na tradução que, neste caso, tem o nome próprio de geração de código.
Processadores de linguagens 5 Um compilador é especifico de um determinado computador, pois o compilador produz código binário (ou Assembly), o qual varia de marca para marca e de modelo para modelo. Sempre que se pretende traduzir uma linguagem fonte para que os programas sejam executados numa máquina diferente, é necessário adaptar ( retarget ) o gerador de código. O desnível que existe entre a complexa estrutura da linguagem fonte e a enorme simplicidade da linguagem máquina, tem como consequência uma grande dificuldade na definição dos esquemas de tradução que efectuam a correspondência entre as construções fonte e as sequências de instruções máquina. Além disso, esses esquemas de tradução são dependentes do computador escolhido como objecto da tradução. Pelas razões expostas, é normal criar-se uma representação intermédia que deve manter a mesma semântica do programa fonte e permitir algumas optimizações. A linguagem escolhida para código intermédio constitui uma representação do código final, ou seja, não engloba detalhes, como a gestão de registos e de memória, da máquina onde será executado o código final. A tradução da linguagem fonte para essa linguagem intermédia evita, assim, problemas como a necessidade de gerir registos (que aparecem nos computadores reais em número limitado e com utilizações específicas) e a necessidade de escolher entre instruções alternativas para realizar a mesma operação. Com a introdução duma linguagem intermédia, a tarefa de geração de código é dividida em duas subtarefas : geração de código intermédio em que o significado da frase de entrada é reescrito na linguagem intermédia, sendo resolvidas questões como a linearização das estruturas de controlo e a atribuição de um modo de acesso à variável, de um endereço inicial na memória e reserva do espaço necessário para guardar um valor do tipo declarado para a variável; geração de código final responsável pela tradução do código intermédio para código máquina, sendo então feita a selecção das melhores instruções máquina e a utilização/gestão dos registos efectivamente existentes no computador objecto. Para além dos requisitos especiais que a análise semântica e a geração de código têm de satisfazer, a implementação de um compilador requer o desenvolvimento de outros módulos próprios e muito importantes para o sucesso do programa final, como sejam : para fazer o tratamento dos identificadores módulo que tem de permitir recolher toda a informação associada a cada identificador que surge no programa fonte, armazenando-a no momento em que o identificador é declarado, ou surge pela primeira vez, para poder ser consultada cada vez que é usado; para fazer o tratamento de erros módulo que, depois de detectado um erro, se encarregue de o reportar ao programador e assegurar a sua correcção/recuperação; para fazer a optimização do texto fonte e do texto final (objecto) gerado dois módulos que conduzam à produção dum código máquina bastante eficiente em termos de tamanho, de rapidez de execução e de requisitos de memória. A optimização é uma etapa complementar à geração de código (intermédio e final), que actua frequentemente em simultâneo com esta, tendo por objectivo a produção de programas mais eficientes (quer em espaço de memória quer em tempo de execução), mas mantendo sempre a correcção do código optimizado. A optimização do código pode ser classificada de acordo com a área de código a optimizar : Local sendo cada instrução observada individualmente; Bloco sendo observada uma sequência de instruções denominada bloco básico; Global sendo observada uma janela do programa.
6 Processadores de linguagens Para resumir tudo quanto se disse acerca da arquitectura de um compilador e suas diferenças relativas aos processadores de linguagens gerais, apresenta-se na figura 2 um diagrama que esquematiza este processo. Programa fonte Análise léxica Análise sintáctica Análise semântica Tabela de símbolos Geração de código intermédio Geração de código Optimização de código Programa alvo Figura 2. Etapas de um compilador. 2.5. Assemblers Os assemblers são funcionalmente muito parecidos com os compiladores, na medida em que traduzem um programa fonte para código máquina. A diferença reside no facto de que o texto de entrada, nos assemblers, não é escrito numa linguagem de alto nível, mas sim numa linguagem de mnemónicas estruturalmente tão simples quanto um programa em linguagem máquina. Por isso, não só o reconhecimento das frases se torna muito fácil, como também os esquemas de tradução são simples e intuitivos, e o processo de tradução muito directo. Na verdade, o assembler pouco difere do módulo de geração de código final igual ao dos compiladores. 2.6. Interpretadores Estes processadores não geram um texto de saída, mas sim executam as instruções do programa fonte logo que o reconhecem. A execução é feita sobre a representação intermédia de código, tipicamente numa notação em árvore. O uso de interpretadores é muito conhecido pelos programadores das linguagens Basic, Lisp e Prolog, as quais constituem os exemplos mais vulgares desta técnica de processamento. As vantagens dos interpretadores são : fácil de implementar, assegura maior rapidez no ciclo edição/compilação/execução, permitem testar/alterar programas à medida que são executados. As desvantagens são : acesso limitado a rotinas, inviáveis em projectos de larga escala. O interpretador não tem de se preocupar com questões de optimização e geralmente processa linguagens de programação menos ricas estruturalmente do que o compilador.
Processadores de linguagens 7 Programa fonte Interpretador Saída Dados 2.7. Tratamento de erros Figura 3. Arquitectura do interpretador. Ao desenvolver-se processadores de linguagens reais, a capacidade destes para tratar os erros é de enorme importância para a sua aceitação, pelo utilizador final, como ferramenta de trabalho. Portanto, se forem detectados erros ao analisar uma frase, esta não deve simplesmente ser rejeitada como não pertencente à linguagem em causa e o processamento terminar por aí. Definição : No contexto do processamento de linguagens, entende-se por tratamento de erros o processo que é desencadeado pelo reconhecedor logo após a detecção de um erro na frase que está a ser analisada. A reacção compreende duas grandes tarefas : a sinalização do erro, ou notificação enviada ao utilizador alertando-o para o facto de se ter encontrado uma violação a uma das regras que fazem da sequência de símbolos fonte uma frase válida da linguagem; a correcção/recuperação que permite superar a falta detectada e prosseguir a análise, até à aceitação da frase ou até à detecção de novo erro. A detecção de um erro é uma tarefa inerente à análise, ou seja, indissociável do reconhecimento. Quando um processador está a analisar uma frase, tentando verificar se ela foi correctamente escrita, de acordo com a gramática da linguagem, são três as razões que podem levar à detecção de um erro : o aparecimento de caracteres inválidos que impedem a identificação de qualquer símbolo terminal pertencente ao alfabeto da linguagem erro léxico; o aparecimento dum símbolo terminal (válido) combinado com os seus vizinhos de forma inválida (ou por estar trocado com outro, ou por não ser permitido naquele ponto, ou ainda por omissão dum símbolo anterior), impedindo o reconhecimento de uma subfrase aceitável erro sintáctico; o aparecimento dum símbolo terminal que, apesar de ser válido e surgir numa posição correcta, infringe as regras de tipo, ou concordância, que têm de ser verificadas para se poder identificar completamente o significado da frase erro semântico. Estas situações correspondem ao desrespeito pelas regras sintácticas, ou pelas regras semânticas, ou ainda pelas regras ortográficas que definem a linguagem em causa. Note-se que uma frase pode ainda conter erros de uma outra classe erros lógicos que surgem quando a frase, apesar de estar bem formada sintacticamente e semanticamente, não traduz correctamente a ideia, ou vontade, do seu emissor (por exemplo, quando um programa implementa um algoritmo incorrectamente). Este tipo de erro não é tratado automaticamente, pois um processador de linguagens não é capaz de os detectar. A sinalização do erro é de importância crucial para que o programador possa localizar o foco de problemas, interpretá-los e corrigi-los definitivamente. Por isso, a mensagem a ser enviada deve, no mínimo, indicar : a posição (linha e coluna, em relação ao texto fonte) onde o símbolo de erro foi encontrado; o símbolo de erro; a causa provável que justifica esse erro (diagnóstico) : podem indicar-se os símbolos de que o processador de linguagens estava à espera, ou a concordância que era pretendida.