Vamos construir um compilador

Parte 12: Miscelâneas

Autor: Jack W. Crenshaw, Ph.D. (05/06/1989)
Tradução e adaptação: Felipo Soranz (26/05/2002)

Este capítulo é sobre outra daquelas incursões laterais que não parecem se encaixar em outro lugar nesta série. Como eu mencionei da última vez, foi quando eu estava escrevendo este capítulo que percebi que mudanças deveriam ser feitas na estrutura do compilador. Portanto tive que desviar um pouco do nosso curso padrão para desenvolver a nova estrutura e mostrá-la pra você.

Agora que já passamos por isto, podemos trabalhar naquilo que eu havia mencionado. Isto não deve levar muito tempo, e logo podemos voltar ao nosso normal.

Muitas pessoas me perguntaram a respeito de coisas que outras linguagens tem, mas que eu ainda não tratei nesta série. As duas maiores são ponto-e-vírgula e comentários. Talvez você tenha pensado a respeito deles, também, e imaginou como as coisas mudariam se tivéssemos que tratar deles. Apenas para prosseguirmos com o que está por vir, sem nos preocuparmos com o incômodo sentimento de que alguma está faltando, vamos tratar destas questões aqui.

Ponto-e-vírgula

Desde a introdução de Algol, o ponto-e-vírgula tem sido parte de quase toda linguagem moderna. Todos nos acostumamos com o lugar onde eles supostamente deveriam estar. Aliás, eu suspeito que mais erros de compilação ocorreram por causa de ponto-e-vírgula mal-posicionado ou omitido que qualquer outra causa. E se ganhássemos um centavo para cada tecla extra pressionada que os programadores se acostumaram a digitar, poderíamos pagar a dívida externa.

Tendo vindo da linguagem FORTRAN, levou tempo para que eu me acostumasse a usar ponto-e-vírgula, e pra dizer a verdade eu nunca soube muito bem por que eles eram necessários. Como eu programo em Pascal, e como o tratamento de ponto-e-vírgula de Pascal é particularmente complexo, este pequeno caractere ainda é uma das minhas maiores fontes de erro.

Quando eu comecei a desenvolver KISS, eu resolvi questionar CADA construção das outras linguagens, e tentar evitar a maioria dos problemas comuns que ocorrem nelas. Isto coloca o ponto-e-vírgula nas primeiras posições da minha lista.

Para entender o papel do ponto-e-vírgula, é preciso olharmos um pouco para a história.

As primeiras linguagens de programação era orientadas a linha. Em FORTRAN, por exemplo, diversas partes de um comando tinham colunas e campos específicos onde deveriam aparecer. Como alguns comandos eram longos demais para uma linha, um mecanismo de “cartão de continuação” foi provido para deixar o compilador decidir que um determinado cartão era ainda parte da linha anterior. O mecanismo sobrevive até hoje, mesmo que cartões perfurados sejam agora coisas de um passado distante.

Quando outras linguagens surgiram, elas também adotaram vários mecanismos para tratar de comandos de múltiplas linhas. BASIC é um bom exemplo. É importante reconhecermos, porém, que o mecanismo de FORTRAN não era tão necessário para a orientação por linha da linguagem, como também a orientação por coluna. Nas versões de FORTRAN onde entrada de forma livre é permitida, estas orientações não são mais necessárias.

Quando os pais de Algol introduziram a nova linguagem, eles queriam se livrar dos programas orientados a linha, como FORTRAN e BASIC, e permitir que a entrada tivesse uma forma livre. Isto inclui a possibilidade de colocar múltiplos comandos em sequência em uma única linha, como em:

a=b; c=d; e=e+1;

Em casos como este, o ponto-e-vírgula é quase necessário. A mesma linha sem o uso de ponto-e-vírgula ficaria “esquisita”:

a=b c=d e=e+1

Eu suspeito que esta é a maior… talvez a ÚNICA… razão para o ponto-e-vírgula: evitar que os programas pareçam “esquisitos”.

Mas a ideia de colocar diversos comandos em sequência juntos na mesma linha é bastante duvidosa na melhor das hipóteses. Não é um estilo de programação muito bom, e parece nos levar aos dias em que era considerado importante conservar cartões. Nestes dias de monitores coloridos e código indentado, a clareza dos programas é muito melhor alcançada mantendo os comandos separados. É claro que é bom ter a OPÇÃO de múltiplos comandos, mas parece ser uma vergonha manter os programadores escravizados com o ponto-e-vírgula, apenas para evitar que aquele caso raro pareça “esquisito”.

Quando comecei com KISS, eu procurei manter a mente aberta. Eu decidi que eu usaria o ponto-e-vírgula quando ele se tornasse necessário para o analisador, mas não antes disso. Eu achei que isto aconteceria assim que eu adicionasse a habilidade de espalhar comandos em múltiplas linhas. Mas como você pôde ver, nunca aconteceu realmente. O compilador TINY está completamente feliz em analisar os comandos mais complicados, espalhados em diversas linhas, sem ponto-e-vírgula.

Porém, há pessoas que usaram o ponto-e-vírgula por tanto tempo, que se sentem nuas sem eles. Eu sou uma delas. Uma vez que eu defini KISS suficientemente bem, eu comecei a escrever alguns programas de exemplo na linguagem. Eu descobri, para meu completo horror, que eu continuava colocando o ponto-e-vírgula lá de qualquer forma. Então eu estou encarando uma NOVA onda de erros de compilação, causados por um ponto-e-vírgula INDESEJADO. Sinistro!

Talvez alguns leitores já estejam projetando suas próprias linguagens, que podem incluir ponto-e-vírgula, ou que desejam usar as técnicas deste tutorial para compilar linguagens convencionais como C. Em todo caso, temos que estar aptos a tratar do ponto-e-vírgula.

“Açúcar Sintático”

Toda esta discussão nos leva à questão de “açúcar sintático” (syntactic sugar)… construções que são adicionadas à linguagem, não por serem necessárias, mas para ajudar para que o programa pareça correto para o programador. Afinal, é bom term um compilador pequeno e simples, mas seria de pouco uso se a linguagem resultante fosse enigmática ou difícil de programar. A linguagem Forth vêm à mente (sim, eu estou ciente que talvez alguns de vocês reclamem). Se podemos adicionar características à linguagem que faz com que o programa seja mais fácil de ler e entender, e se estas características ajudam o programador a evitar os erros, então, devemos fazê-lo. Particularmente se as construções não adicionam muita complexidade à linguagem ou seu compilador.

O ponto-e-vírgula pode ser considerado um exemplo, mas existem muitos outros, como o THEN do comando IF, o DO, do comando WHILE, e mesmo o comando PROGRAM. Nenhum destes tokens adicionam muita coisa à sintaxe da linguagem… o compilador pode descobrir o que está acontecendo sem eles. Mas algumas pessoas acreditam que eles realmente melhoram a legibilidade dos programas, e isto pode ser muito importante.

Há duas escolas de pensamento neste assunto, que são bem representadas por duas das linguagens muito populares, C e Pascal.

Para os minimalistas, todo este açúcar deve ser deixada de fora. Eles argumentam que tudo isto abarrota a linguagem e obriga o programador a digitar mais do que o necessário. Talvez mais importante que isto, cada token extra ou palavra-chave representa uma armadilha esperando para pegar o programador desatento. Se você esquecer de um token, trocá-lo de lugar, ou escrevê-lo de forma errada, o compilador vai pegar você. Então estas pessoas acham que o melhor é se livrar deste tipo de coisa. Em geral eles tendem a gostar de linguagem C e suas descendentes, que procuram usar o mínimo necessário de palavras-chave e pontuação.

Aquelas da outra linha de pensamento geralmente gostam de Pascal. Eles argumentam que ter que escrever alguns caracteres a mais é um preço pequeno a ser pago por legibilidade. Afinal, os humanos tem que ler os programas, também. (E muitos de nós nunca lembram disso…) O melhor argumento é que cada construção é uma oportunidade de dizer ao compilador que você realmente quis dizer aquilo que você disse para ser feito. Os tokens de “açúcar” servem como demarcações úteis para nos ajudar a encontrar nosso caminho.

As diferenças são bem representadas pelas duas linguagens. A reclamação mais comum da linguagem C é que ela é muito permissiva. Quando você comete um erro em C, o código errado é frequentemente uma outra construção válida em C. Então o compilador simplesmente continua compilando, e deixa você encontrar o erro durante a depuração. Eu acho que é por isto que depuradores são tão populares entre os programadores C.

Por outro lado, se um programa Pascal compilar, você pode ter certeza de que o programa faz o que você queria que ele fizesse. Se houver um erro em tempo de execução, ele provavelmente será de projeto.

O melhor exemplo de “açúcar” é o próprio ponto-e-vírgula. Considere o seguinte fragmento:

a=1+(2*b+c) b...

Como não há operadores conectando o token “b” com o resto do comando, o compilador conclui que a expressão terminou no “)”, e que “b” é o princípio de um novo comando. Mas suponha que eu simplesmente tenha esquecido do operador desejado, e realmente quis dizer:

a=1+(2*b_c)*b...

Neste caso, o compilador emitiria um erro, tudo bem, mas não seria muito compreensível, já que ele estaria esperando um operador “=” depois do “b” que realmente deveria estar lá.

Se, por outro lado, eu incluir um ponto-e-vírgula depois do “b”, ENTÃO não haveria dúvida sobre onde eu quero que o comando termine. “Açúcar sintático”, portanto, pode ter um propósito muito útil em adicionar alguma segurança para nos mantermos na linha.

Eu me encontro mais ou menos no meio de tudo isso. Em geral, eu gosto da visão dos programadores Pascal.. eu prefiro encontrar meus bugs em tempo de compilação ao invés de em tempo de execução. Mas também odeio incluir “falatório” sem razão aparente, como em COBOL. Até aqui eu estive deixando a maior parte do “açúcar” de lado em KISS/TINY. Mas certamente eu também não sou inflexível e não vejo nenhum mal em espalhar um pouco de “açúcar” aqui e ali apenas pela segurança extra que ele traz. Se você prefere esta última abordagem, saiba que coisas assim são simples de adicionar. Apenas lembre-se que, assim como o ponto-e-vírgula, cada item de “açúcar” é algo que pode potencialmente causar um erro de compilação com sua omissão.

Tratando do ponto-e-vírgula

Há duas formas distintas em que o ponto-e-vírgula pode ser usado em linguagens populares. Em Pascal, o ponto-e-vírgula é considerado como um SEPARADOR de comandos. Nenhum ponto-e-vírgula é necessário depois do último comando de um bloco. A sintaxe é:

<block> ::= <statement> ( ';' <statement> ) *

<statement> ::= <assignment> | <if> | <while> ... | null

(O comando “null” ou vazio é IMPORTANTE!)

Pascal também define alguns outros locais onde podem haver ponto-e-vírgula, como depois do comando PROGRAM.

Em C e Ada, por outro lado, o ponto-e-vírgula é considerado um TERMINADOR de comandos, e segue todos os comandos (com algumas exceções confusas e embaraçosas). A sintaxe para isto é simples:

<block> ::= ( <statement> ';' )*

Das duas sintaxes, a do Pascal parece ser a mais racional, mas a experiência tem mostrada que ela permite algumas dificuldade estranhas. As pessoas se acostumam a digitar um ponto-e-vírgula depois de todo comando digitado e acabam fazendo isto no último comando de um bloco, também. Isto normalmente não causa nenhum mal… isto é simplesmente tratado como se houvesse um comando “null” em seguida. Muitos programadores Pascal, incluindo este aqui, fazem isto. Mas existe um lugar em que NÃO se deve digitar um ponto-e-vírgula, e é justamente antes de um ELSE. Este pequeno detalhe já me custou muitas compilações extra, particularmente quando o ELSE é adicionado a código existente. Portanto a escolha de C/Ada acaba sendo melhor. Aparentemente, Niklaus Wirth também acha isto: em Modula-2, ele abandonou a abordagem de Pascal.

Dada qualquer uma destas sintaxes, é simples (agora que reorganizamos o analisador!) adicionar estas características ao nosso compilador. Vamos tratar do último caso primeiro, já que é mais simples.

Pra começar, eu tornei as coisas mais fáceis, introduzindo um novo reconhecedor:

/* Reconhece um ponto-e-vírgula */
void Semicolon()
{
    MatchString(";");
}

Esta rotina funciona de forma muito parecida com à antiga Match(). Ela insiste em achar um ponto e vírgula como o próximo token. Tendo encontrado, ela passa para o próximo token.

Como um ponto-e-vírgula segue um comando, a rotina Block() é praticamente a única que deve ser alterada:

/* Analisa e traduz um bloco de comandos estilo "C/Ada" */
void Block()
{
    int follow = 0;

    do {
        Scan();
        switch (Token) {
            case 'i':
                DoIf();
                break;
            case 'w':
                DoWhile();
                break;
            case 'R':
                DoRead();
                break;
            case 'W':
                DoWrite();
                break;
            case 'x':
                Assignment();
                break;
            case 'e':
            case 'l':
                follow = 1;
                break;
        }
        if (!follow)
            Semicolon();
    } while (!follow);
}

Repare cuidadosamente na pequena alteração no comando switch. A chamada a Assignment() agora é garantida com um teste em token. Isto é para evitar a chamada a Assignment() quando o token é o ponto-e-vírgula (que pode acontecer se o comando for vazio).

Como as declarações são comandos também, também devemos adicionar uma chamada a Semicolon() dentro de TopDeclarations():

/* Analisa e traduz declarações */
void TopDeclarations()
{
    Scan();
    while (Token == 'v') {
        do {
            Declaration();
        } while (Token == ',');
        Semicolon();
        Scan();
    }
}

Finalmente, precisamos de mais uma para o comando PROGRAM.

/* Programa principal */
int main()
{
    Init();

    MatchString("PROGRAM");
    Semicolon();
    AsmHeader();
    TopDeclarations();
    MatchString("BEGIN");
    AsmProlog();
    Block();
    MatchString("END");
    AsmEpilog();

    return 0;
}

É simples assim. Teste com uma cópia de TINY e veja se você gosta.

A versão Pascal é um pouco mais complicada, mas ainda assim ela requer apenas mudanças menores, e somente para a rotina Block() . Para manter as coisas o mais simples possível, vou separar a rotina em duas partes. A rotina seguinte trata de apenas um comando:

/* Analisa e traduz um único comando */
void Statement()
{
    Scan();
    switch (Token) {
        case 'i':
            DoIf();
            break;
        case 'w':
            DoWhile();
            break;
        case 'R':
            DoRead();
            break;
        case 'W':
            DoWrite();
            break;
        case 'x':
            Assignment();
            break;
    }
}

Usando a rotina, podemos agora reescrever Block() :

/* Analisa e traduz um bloco de comandos estilo "Pascal" */
void Block()
{
    Statement();
    while (Token == ';') {
        NextToken();
        Statement();
    }
    Scan();
}

Isto certamente não machucou, certo? Agora podemos processar ponto-e-vírgula da mesma forma que em Pascal.

Um Meio-Termo

Agora que sabemos como tratar de ponto-e-vírgula, isto significa que vamos colocá-los em KISS/TINY? Bem, sim e não. Eu gosto do açúcar sintático e da segurança extra que acompanha o fato de sabermos com certeza onde o fim dos comandos estão. Mas eu não mudei a minha ideia sobre erros de compilação associados ao ponto-e-vírgula.

Então eu tenho algo em mente que parece ser um bom meio-termo: fazê-los OPCIONAIS!

Considere a seguinte versão de “semicolon”:

/* Reconhece um ponto-e-vírgula opcional */
void Semicolon()
{
    if (Token == ';')
        NextToken();
}

Esta rotina vai aceitar o ponto-e-vírgula sempre que for chamada, mas não vai insistir em encontrar um. Isto significa que quando você decidir usar ponto-e-vírgula, o compilador vai usar a informação extra para se manter na linha. Mas se você omitir um (ou omitir a todos) o compilador não vai reclamar. O melhor dos dois mundos.

Coloque esta nova rotina na primeira versão do seu programa (a com a sintaxe C/Ada), e você terá o princípio de TINY Versão 1.2.

Comentários

Até aqui eu estive cuidadosamente evitando a questão dos comentários. Você pensaria que isto deve ser uma questão fácil… afinal, o compilador em si não tem que tratar dos comentários; ele simplesmente deve ignorá-los. Bem, algumas vezes isto é verdade.

Comentários podem ser tão fáceis ou tão difíceis quanto você quiser fazê-los. Em um extremo, podemos fazer as coisas de forma que os comentários são interceptados quase que no mesmo instante que eles entram no compilador. No outro extremo, podemos tratá-los como elementos léxicos. As coisas começam a ficar interessantes, quando você considera coisas como delimitadores contidos dentro de strings entre aspas.

Delimitadores de Um Caractere

Aqui está um exemplo. Podemos usar o padrão Turbo Pascal e usar chaves para os comentários. Neste caso, temos delimitadores de um caractere, então o processamento é um pouco mais fácil.

Uma abordagem é eliminar os comentários no instante em que os encontramos na entrada; isto é, na rotina NextChar(). Para fazer isto, primeiro altere o nome de NextChar() para algo como NextCharX(). (Aviso: isto será uma mudança temporária, portanto é melhor não fazer isto com sua única cópia de TINY. Eu presumo que você entenda que todas estas experiências devem ser feitas em uma cópia do programa, e não no próprio.)

Agora, vamos precisar de uma rotina que pule os comentários. Então, entre com esta nova rotina:

/* Pula um campo de comentário */
void SkipComment()
{
    while (Look != '}') {
        NextCharX();
    }
    NextCharX();
}

Evidentemente, o que está rotina faz é simplesmente ler e descartar caracteres da entrada, até encontrar uma chave direita (“}”). Então ele lê um caractere a mais e o coloca em “Look” como esperado.

Agora podemos escrever uma nova versão de NextChar() que usa “skipComment” para remover os comentários:

/* Lê próximo caractere da entrada e pula quaisquer comentários */
void NextChar()
{
    NextCharX();
    if (Look == '{')
        SkipComment();
}

Entre com este código e faça um teste. Você vai descobrir que é possível colocar comentários de fato em qualquer lugar que você quiser. Os comentários nem sequer chegam ao analisador sintático… toda chamada a NextChar() retorna apenas caracteres que NÃO são parte de um comentário.

Na verdade, embora esta abordagem faça o trabalho corretamente, e talvez seja perfeitamente satisfatória pra você, ela faz o trabalho bem DEMAIS. Primeiro, a maioria das linguagens de programação especificam que o comentário deve ser tratado como um espaço, de forma que comentários não são permitidos digamos, no meio dos nomes de variáveis (exemplo: “VAR{comentário}IÁVEL”). Esta versão atual simplesmente não liga para ONDE você coloca os comentários.

Segundo, já que o resto do analisador não recebe o caractere “{“, você não vai poder colocá-lo futuramente em uma string entre aspas.

Antes que você torça o nariz para esta solução simplística, eu gostaria de falar que alguns compiladores de respeito não permitem o uso do delimitador de comentário dentro de uma string. E seria possível usar comentários no meio de um identificador. Eu não consigo imaginar porque alguém faria isso, então é um ponto meio duvidoso. Mas para 99% de todas as aplicações, o que eu lhe mostrei pode funcionar perfeitamente bem.

Mas se você preferir o tratamento convencional, então temos que mover o ponto de detecção um pouco abaixo.

Para fazer isto, volte à versão anterior de NextChar() da forma como ela era antes e altere a chamada em SkipComment(). Então, vamos adicionar a chave esquerda (“{“) como um possível caractere de espaço, alterando a rotina SkipWhite():

/* Pula caracteres em branco */
void SkipWhite()
{
    while (isspace(Look) || Look == '{') {
        if (Look == '{')
            SkipComment();
        else
            NextChar();
    }
}

Repare que skipWhite foi escrito de forma que vamos pular qualquer combinação de caracteres de espaços e comentários, em uma chamada.

OK, teste este também. Você vai descobrir que é possível usar comentários para delimitar tokens também. Vale a pena mencionar que esta abordagem nos permite tratar de chaves dentro de strings, já que dentro das strings não há porque testar ou pular espaços em branco, e sim tratá-los literalmente.

Há mais um item a ser tratado: comentários aninhados. Alguns programadores gostam da ideia de aninhar comentários, já que isto permite comentar código que possui comentários durante a depuração. O código que eu mostrei aqui não permite isto, nem mesmo Pascal ou linguagem C.

Mas para arrumar isto é incrivelmente simples. Tudo o que precisamos fazer é tornar s SkipComment() recursivo:

/* Pula um campo de comentário */
void SkipComment()
{
    while (Look != '}') {
        NextChar();
        if (Look == '{') /* Trata comentários aninhados */
            SkipComment();
    }
    NextChar();
}

Aí está. Um tratamento de comentários com um nível de sofisticação suficiente pra toda vida.

Delimitadores Multi-caractere

Tudo isto está muito bom para casos onde um comentário é delimitado por caracteres simples, mas e os casos como em C ou Pascal padrão, onde 2 caracteres são necessários? Bem, os princípios são os mesmos, mas temos que mudar nossa abordagem um pouco. Eu tenho certeza que você não ficaria surpreso de saber que as coisas são um pouco mais difíceis neste caso.

Para a situação multi-caractere, a coisa mais fácil a fazer é interceptar o delimitador esquerdo de volta no estágio NextChar(). É possível “tokenizá-lo” lá mesmo, trocando-o por um simples caractere.

Vamos assumir que estamos usando os delimitadores de C “/” e “/”. Primeiro, temos que voltar a abordagem NextCharX(). Em outra cópia do compilador, renomeie NextChar() para NextCharX() e então entre com a nova versão de NextChar():

/* Lê próximo caractere e intercepta início de comentário*/
void NextChar()
{
    if (TempChar != ' ') {
        Look = TempChar;
        TempChar = ' ';
    } else {
        NextCharX();
        if (Look == '/') {
            TempChar = getchar();
            if (TempChar == '*') {
                Look = '{';
                TempChar = ' ';
            }
        }
    }
}

Como você pode perceber, o que este procedimento faz é interceptar toda ocorrência de “/”. Então ele examina o PRÓXIMO caractere de entrada. Se o caractere é um “*”, então encontramos o início de um comentário, e NextChar() vai retornar um substituto de um só caractere para ele. (Por simplicidade, estou usando o caractere “{“ como eu fiz para os comentários da versão Pascal. Se você estiver escrevendo um compilador C, sem dúvida você iria preferir escolher algum outro caractere que não é usado em nenhum outro lugar em C. Escolhe qualquer um da sua preferência… mesmo que seja o caractere ASCII 255 por exemplo, qualquer coisa que seja única.)

Se o caractere após o “/” não for um “*”, então NextChar() o coloca em “TempChar” e retorna o “/”.

Repare que é preciso declarar esta nova variável global e inicializá-la com um espaço.

char TempChar = ' ';

Agora, precisamos de uma nova versão de SkipComment():

/* Pula um campo de comentário */
void SkipComment()
{
    do {
        do {
            NextCharX();
        } while (Look != '*');
        NextCharX();
    } while (Look != '/');
    NextCharX();
}

Algumas coisas a notar: primeiro, a rotina “skipWhite” não precisa ser alterada, já que NextChar() continua retornando o token “{“. Se você alterar este caractere de token, é claro que você também deve alterar o caractere nesta rotina.

Segundo, repare que “skipComment” não chama NextChar() em seu laço, e sim NextCharX(). Isto significa que o “/” não é interceptado e é visto por SkipComment(). Terceiro, apesar de NextChar() ser a rotina que faz o trabalho, ainda podemos tratar dos caracteres de comentário dentro de uma string entre aspas, chamando NextCharX() ao invés de NextChar() enquanto estivermos dentro da string. Finalmente, repare que podemos novamente prover comentários aninhados simplesmente adicionando uma chamada recursiva em SkipComment(), da mesma forma que antes.

Comentários de Uma Linha

Até aqui eu mostrei como tratar de todo tipo de comentários delimitados pela esquerda e direita. Isto só deixa de lado os comentários de linha, como aqueles de linguagem Assembly (;), Ada (--) ou mesmo os de C++ (//), que são terminados pelo final da linha. De certa forma, este caso é até mais simples. A única rotina que deve ser alterada é SkipComment(), que agora deve terminar no final da linha:

/* Pula um campo de comentário de uma só linha*/
void SkipComment()
{
    do {
        NextCharX();
    } while (Look != '\n');
    NextChar();
}

Se o caractere de início do comentário é único, como “;” de assembly, é praticamente isto. Basta alterar SkipWhite() para usá-lo. Repare que você não pode usar o “;” propriamente dito na nossa versão atual de TINY por razões óbvias. Como alternativa, podemos experimentar “#”.

/* Pula caracteres em branco */
void SkipWhite()
{
    while (isspace(Look) || Look == '#') {
        if (Look == '#')
            SkipComment();
        else
            NextChar();
    }
}

Se for um token de dois caracteres, como “–” de Ada, precisamos apenas alterar os testes dentro de NextChar(). Nos dois casos, é um problema mais simples que o caso balanceado.

Conclusão

Neste ponto temos a habilidade de lidar com comentários e ponto-e-vírgula, como também outros tipos de “açúcar sintático”. Eu lhe mostrei diversas formas de lidar com cada um, dependendo da convenção desejada. A única questão pendente é: qual destas convenções serão usadas em KISS/TINY?

Pelas razões dadas anteriormente, vou escolher o seguinte:

  1. O ponto-e-vírgula é um TERMINADOR, não um separador
  2. O ponto-e-vírgula é OPCIONAL
  3. Comentários são delimitados por chaves
  4. Comentários podem ser aninhados

Coloque o código correspondente a estes casos em sua cópia de TINY. Agora você tem TINY Versão 1.3.

Listagem completa:

/*
Tiny 1.3

O código abaixo foi escrito por Felipo Soranz e é uma adaptação
do código original em Pascal escrito por Jack W. Crenshaw em sua
série "Let's Build a Compiler".

Este código é de livre distribuição e uso.
*/

#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#include <ctype.h>
#include <string.h>

#define SYMBOLTABLE_SIZE 1000
char *SymbolTable[SYMBOLTABLE_SIZE]; /* Tabela de símbolos */
char SymbolType[SYMBOLTABLE_SIZE]; /* Tabela de tipos de símbolos */
int SymbolCount; /* Número de entradas na tabela de símbolos */

char Look; /* O caractere lido "antecipadamente" (lookahead) */
int LabelCount; /* Contador usado pelo gerador de rótulos */

#define KEYWORDLIST_SIZE 9

/* Lista de palavras-chave */
char *KeywordList[KEYWORDLIST_SIZE] = {"IF", "ELSE", "ENDIF", "WHILE", "ENDWHILE",
                           "READ", "WRITE", "VAR", "END"};

/* Códigos de palavras-chave: a ordem deve obedecer a lista de palavras-chave */
char *KeywordCode = "ileweRWve";

#define MAXTOKEN 16
char TokenText[MAXTOKEN+1]; /* Texto do token atual */
char Token; /* Código do token atual */

/* Lê próximo caractere da entrada */
void NextChar()
{
    Look = getchar();
}

/* Pula um campo de comentário */
void SkipComment()
{
    while (Look != '}') {
        NextChar();
        if (Look == '{') /* Trata comentários aninhados */
            SkipComment();
    }
    NextChar();
}

/* Pula caracteres em branco */
void SkipWhite()
{
    while (isspace(Look) || Look == '{') {
        if (Look == '{')
            SkipComment();
        else
            NextChar();
    }
}

/* Exibe uma mensagem de erro formatada */
void Error(char *fmt, ...)
{
    va_list args;

    fputs("Error: ", stderr);

    va_start(args, fmt);
    vfprintf(stderr, fmt, args);
    va_end(args);

    fputc('\n', stderr);
}

/* Exibe uma mensagem de erro formatada e sai */
void Abort(char *fmt, ...)
{
    va_list args;

    fputs("Error: ", stderr);

    va_start(args, fmt);
    vfprintf(stderr, fmt, args);
    va_end(args);

    fputc('\n', stderr);

    exit(1);
}

/* Alerta sobre alguma entrada esperada */
void Expected(char *fmt, ...)
{
    va_list args;

    fputs("Error: ", stderr);

    va_start(args, fmt);
    vfprintf(stderr, fmt, args);
    va_end(args);

    fputs(" expected!\n", stderr);

    exit(1);
}

/* Avisa a respeito de um identificador desconhecido */
void Undefined(char *name)
{
    Abort("Undefined identifier %s\n", name);
}

/* Avisa a respeito de um identificador duplicado */
void Duplicate(char *name)
{
    Abort("Duplicated identifier %s\n", name);
}

/* Reporta um erro se Token NÃO for um identificador */
void CheckIdent()
{
    if (Token != 'x')
        Expected("Identifier");
}

/* Recebe o nome de um identificador ou palavra-chave */
void GetName()
{
    int i;

    SkipWhite();
    if (!isalpha(Look))
        Expected("Identifier or Keyword");
    for (i = 0; isalnum(Look) && i < MAXTOKEN; i++) {
        TokenText[i] = toupper(Look);
        NextChar();
    }
    TokenText[i] = '\0';
    Token = 'x';
}

/* Recebe um número inteiro */
void GetNum()
{
    int i;

    SkipWhite();
    if (!isdigit(Look))
        Expected("Integer");
    for (i = 0; isdigit(Look) && i < MAXTOKEN; i++) {
        TokenText[i] = Look;
        NextChar();
    }
    TokenText[i] = '\0';
    Token = '#';
}

/* Analisa e traduz um operador */
void GetOp()
{
    SkipWhite();
    Token = Look;
    TokenText[0] = Look;
    TokenText[1] = '\0';
    NextChar();
}

/* Pega o próximo Token de entrada */
void NextToken()
{
    SkipWhite();
    if (isalpha(Look))
        GetName();
    else if (isdigit(Look))
        GetNum();
    else
        GetOp();
}

/* Se a string de entrada estiver na tabela, devolve a posição ou -1 se não estiver */
int Lookup(char *s, char *list[], int size)
{
    int i;

    for (i = 0; i < size; i++) {
        if (strcmp(list[i], s) == 0)
            return i;
    }

    return -1;
}

/* Analisador léxico */
void Scan()
{
    int kw;

    if (Token == 'x') {
        kw = Lookup(TokenText, KeywordList, KEYWORDLIST_SIZE);
        if (kw >= 0)
            Token = KeywordCode[kw];
    }
}

/* Verifica se a string combina com o esperado */
void MatchString(char *s)
{
    if (strcmp(TokenText, s) != 0)
        Expected(s);
    NextToken();
}

/* Emite uma instrução seguida por uma nova linha */
void EmitLn(char *fmt, ...)
{
    va_list args;

    putchar('\t');

    va_start(args, fmt);
    vprintf(fmt, args);
    va_end(args);

    putchar('\n');
}

/* Reconhece operador aditivo */
int IsAddOp(char c)
{
    return (c == '+' || c == '-');
}

/* Reconhece operador multiplicativo */
int IsMulOp(char c)
{
    return (c == '*' || c == '/');
}

/* Reconhece um operador OU */
int IsOrOp(char c)
{
    return (c == '|' || c == '~');
}

/* Reconhece operadores relacionais */
int IsRelOp(char c)
{
    return (c == '=' || c == '#' || c == '<' || c == '>');
}

/* Verifica se símbolo está na tabela */
int InTable(char *name)
{
    return (Lookup(name, SymbolTable, SymbolCount) >= 0);
}

/* Retorna o endereço do identificador na tabela de símbolos */
int Locate(char *name)
{
    return Lookup(name, SymbolTable, SymbolCount);
}

/* Reporta um erro se identificador NÃO constar na tabela de símbolos */
void CheckTable(char *name)
{
    if (!InTable(name))
        Undefined(name);
}

/* Reporta um erro se identificador JÁ constar na tabela de símbolos */
void CheckDuplicate(char *name)
{
    if (InTable(name))
        Duplicate(name);
}

/* Adiciona uma nova entrada à tabela de símbolos */
void AddEntry(char *name, char type)
{
    char *newSymbol;

    CheckDuplicate(name);

    if (SymbolCount >= SYMBOLTABLE_SIZE) {
        Abort("Symbol table full!");
    }

    newSymbol = (char *) malloc(sizeof(char) * (strlen(name) + 1));
    if (newSymbol == NULL)
        Abort("Out of memory!");

    strcpy(newSymbol, name);

    SymbolTable[SymbolCount] = newSymbol;
    SymbolType[SymbolCount] = type;
    SymbolCount++;
}

/* Gera um novo rótulo único */
int NewLabel()
{
    return LabelCount++;
}

/* Emite um rótulo */
void PostLabel(int lbl)
{
    printf("L%d:\n", lbl);
}

/* Cabeçalho inicial para o montador */
void AsmHeader()
{
    printf("org 100h\n");
    printf("section .data\n");
}

/* Emite código para o prólogo de um programa */
void AsmProlog()
{
    printf("section .text\n");
    printf("_start:\n");
}

/* Emite código para o epílogo de um programa */
void AsmEpilog()
{
    EmitLn("MOV AX, 4C00h");
    EmitLn("INT 21h");
    printf("\n%%include \"tinyrtl_dos.inc\"\n");
}

/* Zera o registrador primário */
void AsmClear()
{
    EmitLn("XOR AX, AX");
}

/* Negativa o registrador primário */
void AsmNegate()
{
    EmitLn("NEG AX");
}

/* Carrega uma constante numérica no registrador primário */
void AsmLoadConst(char *num)
{
    EmitLn("MOV AX, %s", num);
}

/* Carrega uma variável no registrador primário */
void AsmLoadVar(char *name)
{
    EmitLn("MOV AX, [%s]", name);
}

/* Armazena registrador primário em variável */
void AsmStore(char *name)
{
    EmitLn("MOV [%s], AX", name);
}

/* Coloca registrador primário na pilha */
void AsmPush()
{
    EmitLn("PUSH AX");
}

/* Adiciona topo da pilha ao registrador primário */
void AsmPopAdd()
{
    EmitLn("POP BX");
    EmitLn("ADD AX, BX");
}

/* Subtrai o registrador primário do topo da pilha */
void AsmPopSub()
{
    EmitLn("POP BX");
    EmitLn("SUB AX, BX");
    EmitLn("NEG AX");
}

/* Multiplica o topo da pilha pelo registrador primário */
void AsmPopMul()
{
    EmitLn("POP BX");
    EmitLn("IMUL BX");
}

/* Divide o topo da pilha pelo registrador primário */
void AsmPopDiv()
{
    EmitLn("POP BX");
    EmitLn("XCHG AX, BX");
    EmitLn("CWD");
    EmitLn("IDIV BX");
}

/* Inverte registrador primário */
void AsmNot()
{
    EmitLn("NOT AX");
}

/* Aplica "E" binário ao topo da pilha com registrador primário */
void AsmPopAnd()
{
    EmitLn("POP BX");
    EmitLn("AND AX, BX");
}

/* Aplica "OU" binário ao topo da pilha com registrador primário */
void AsmPopOr()
{
    EmitLn("POP BX");
    EmitLn("OR AX, BX");
}

/* Aplica "OU-exclusivo" binário ao topo da pilha com registrador primário */
void AsmPopXor()
{
    EmitLn("POP BX");
    EmitLn("XOR AX, BX");
}

/* Compara topo da pilha com registrador primário */
void AsmPopCompare()
{
    EmitLn("POP BX");
    EmitLn("CMP BX, AX");
}

/* Altera registrador primário (e flags, indiretamente) conforme a comparação */
void AsmRelOp(char op)
{
    char *jump;
    int l1, l2;

    l1 = NewLabel();
    l2 = NewLabel();

    switch (op) {
        case '=': jump = "JE"; break;
        case '#': jump = "JNE"; break;
        case '<': jump = "JL"; break;
        case '>': jump = "JG"; break;
        case 'L': jump = "JLE"; break;
        case 'G': jump = "JGE"; break;
    }

    EmitLn("%s L%d", jump, l1);
    EmitLn("XOR AX, AX");
    EmitLn("JMP L%d", l2);
    PostLabel(l1);
    EmitLn("MOV AX, -1");
    PostLabel(l2);
}

/* Desvio incondicional */
void AsmBranch(int label)
{
    EmitLn("JMP L%d", label);
}

/* Desvio se falso (0) */
void AsmBranchFalse(int label)
{
    EmitLn("JZ L%d", label);
}

/* Lê um valor a partir da entrada e armazena-o no registrador primário */
void AsmRead()
{
    EmitLn("CALL READ");
}

/* Mostra valor do registrador primário */
void AsmWrite()
{
    EmitLn("CALL WRITE");
}

/* Alocação de memória para uma variável global */
void AllocVar(char *name, int value)
{
    printf("%s\tdw %d\n", name, value);
}

/* Analisa uma lista de declaração de variáveis */
void Declaration()
{
    NextToken();
    CheckIdent();
    CheckDuplicate(TokenText);
    AddEntry(TokenText, 'v');
    AllocVar(TokenText, 0);
    NextToken();
}

/* Reconhece um ponto-e-vírgula opcional */
void Semicolon()
{
    if (Token == ';')
        MatchString(";");
}

/* Analisa e traduz declarações globais */
void TopDeclarations()
{
    Scan();
    while (Token == 'v') {
        do {
            Declaration();
        } while (Token == ',');
        Semicolon();
        Scan();
    }
}

void BoolExpression(); /* Declaração adianta */

/* Analisa e traduz um fator matemático */
void Factor()
{
    if (Token == '(') {
        NextToken();
        BoolExpression();
        MatchString(")");
    } else {
        if (Token == 'x') {
            CheckTable(TokenText);
            AsmLoadVar(TokenText);
        } else if (Token == '#')
            AsmLoadConst(TokenText);
        else
            Expected("Math Factor");
        NextToken();
    }
}

/* Reconhece e traduz uma multiplicação */
void Multiply()
{
    NextToken();
    Factor();
    AsmPopMul();
}

/* Reconhece e traduz uma divisão */
void Divide()
{
    NextToken();
    Factor();
    AsmPopDiv();
}

/* Analisa e traduz um termo matemático */
void Term()
{
    Factor();
    while (IsMulOp(Token)) {
        AsmPush();
        switch (Token) {
          case '*':
            Multiply();
            break;
          case '/':
            Divide();
            break;
        }
    }
}

/* Reconhece e traduz uma adição */
void Add()
{
    NextToken();
    Term();
    AsmPopAdd();
}

/* Reconhece e traduz uma subtração*/
void Subtract()
{
    NextToken();
    Term();
    AsmPopSub();
}

/* Analisa e traduz uma expressão matemática */
void Expression()
{
    if (IsAddOp(Token))
        AsmClear();
    else
        Term();
    while (IsAddOp(Token)) {
        AsmPush();
        switch (Token) {
            case '+':
                Add();
                break;
            case '-':
                Subtract();
                break;
        }
    }
}

/* Analisa e traduz uma relação */
void Relation()
{
    char op;

    Expression();
    if (IsRelOp(Token)) {
        op = Token;
        NextToken(); /* Só para remover o operador do caminho */
        if (op == '<') {
            if (Token == '>') { /* Trata operador <> */
                NextToken();
                op = '#';
            } else if (Token == '=') { /* Trata operador <= */
                NextToken();
                op = 'L';
            }
        } else if (op == '>' && Token == '=') { /* Trata operador >= */
            NextToken();
            op = 'G';
        }
        AsmPush();
        Expression();
        AsmPopCompare();
        AsmRelOp(op);
    }
}

/* Analisa e traduz um fator booleano com NOT inicial */
void NotFactor()
{
    if (Token == '!') {
        NextToken();
        Relation();
        AsmNot();
    } else
        Relation();
}

/* Analisa e traduz um termo booleano */
void BoolTerm()
{
    NotFactor();
    while (Token == '&') {
        NextToken();
        AsmPush();
        NotFactor();
        AsmPopAnd();
    }
}

/* Reconhece e traduz um operador OR */
void BoolOr()
{
    NextToken();
    BoolTerm();
    AsmPopOr();
}

/* Reconhece e traduz um operador XOR */
void BoolXor()
{
    NextToken();
    BoolTerm();
    AsmPopXor();
}

/* Analisa e traduz uma expressão booleana */
void BoolExpression()
{
    BoolTerm();
    while (IsOrOp(Token)) {
        AsmPush();
        switch (Token) {
          case '|':
              BoolOr();
              break;
          case '~':
              BoolXor();
              break;
        }
    }
}

/* Analisa e traduz um comando de atribuição */
void Assignment()
{
    char name[MAXTOKEN+1];

    strcpy(name, TokenText);
    CheckTable(name);
    NextToken();
    MatchString("=");
    BoolExpression();
    AsmStore(name);
}

void Block();

/* Analisa e traduz um comando IF */
void DoIf()
{
    int l1, l2;

    NextToken();
    BoolExpression();
    l1 = NewLabel();
    l2 = l1;
    AsmBranchFalse(l1);
    Block();
    if (Token == 'l') {
        NextToken();
        l2 = NewLabel();
        AsmBranch(l2);
        PostLabel(l1);
        Block();
    }
    PostLabel(l2);
    MatchString("ENDIF");
}

/* Analisa e traduz um comando WHILE */
void DoWhile()
{
    int l1, l2;

    NextToken();
    l1 = NewLabel();
    l2 = NewLabel();
    PostLabel(l1);
    BoolExpression();
    AsmBranchFalse(l2);
    Block();
    MatchString("ENDWHILE");
    AsmBranch(l1);
    PostLabel(l2);
}

/* Processa um comando READ */
void DoRead()
{
    NextToken();
    MatchString("(");
    for (;;) {
        CheckIdent();
        CheckTable(TokenText);
        AsmRead();
        AsmStore(TokenText);
        NextToken();
        if (Token != ',')
            break;
        NextToken();
    }
    MatchString(")");
}

/* Processa um comando WRITE */
void DoWrite()
{
    NextToken();
    MatchString("(");
    for (;;) {
        Expression();
        AsmWrite();
        if (Token != ',')
            break;
        NextToken();
    }
    MatchString(")");
}

/* Remova o comentário para usar o estilo "Pascal" de ponto-e-vírgulas */
/* #define PASCAL_STYLE */

#ifdef PASCAL_STYLE

/* Analisa e traduz um único comando */
void Statement()
{
    Scan();
    switch (Token) {
        case 'i':
            DoIf();
            break;
        case 'w':
            DoWhile();
            break;
        case 'R':
            DoRead();
            break;
        case 'W':
            DoWrite();
            break;
        case 'x':
            Assignment();
            break;
    }
}

/* Analisa e traduz um bloco de comandos estilo "Pascal" */
void Block()
{
    Statement();
    while (Token == ';') {
        NextToken();
        Statement();
    }
    Scan();
}

#else

/* Analisa e traduz um bloco de comandos estilo "C/Ada" */
void Block()
{
    int follow = 0;

    do {
        Scan();
        switch (Token) {
            case 'i':
                DoIf();
                break;
            case 'w':
                DoWhile();
                break;
            case 'R':
                DoRead();
                break;
            case 'W':
                DoWrite();
                break;
            case 'x':
                Assignment();
                break;
            case 'e':
            case 'l':
                follow = 1;
                break;
        }
        if (!follow)
            Semicolon();
    } while (!follow);
}

#endif

/* Inicialização do compilador */
void Init()
{
    SymbolCount = 0;

    NextChar();
    NextToken();
}

/* Programa principal */
int main()
{
    Init();

    MatchString("PROGRAM");
    Semicolon();
    AsmHeader();
    TopDeclarations();
    MatchString("BEGIN");
    AsmProlog();
    Block();
    MatchString("END");
    AsmEpilog();

    return 0;
}

Download do compilador “Tiny 1.3”.

Agora que já tratamos destas questões paralelas, podemos finalmente voltar ao núcleo da série. No próximo capítulo, vamos falar sobre procedimentos e passagem de parâmetros, e vamos adicionar estas importantes características a TINY.

Vejo vocês lá.

Texto original: Copyright © 1988-1995 Jack W. Crenshaw. Todos os direitos reservados.
Tradução e adaptação: Copyright © 2002,2022 Felipo Soranz. Todos os direitos reservados.