8

Lab - Analisador Léxico II

Introdução8.1

No laboratório anterior, estabelecemos a infraestrutura de leitura de arquivos e identificação de símbolos simples. Porém, uma linguagem de programação real precisa de muito mais expressividade. O nosso analisador atual falharia miseravelmente ao tentar interpretar lógica booleana, comentários ou diferenciar uma atribuição (=) de uma comparação (==).

Neste capítulo, finalizaremos o Lexer implementando o suporte a Literais (strings, números), Palavras-Chave e, crucialmente, resolveremos ambiguidades léxicas usando Lookahead (olhada à frente).

Refinaremos também nossa definição de linguagem. Para treinarmos um pouco, iremos dar suporte tanto a operadores lógicos textuais (and, or) quanto aos simbólicos (&&, ||), além de um sistema robusto de comentários.

Expansão dos Tokens8.2

Antes de escrevermos a lógica, precisamos que o nosso sistema conheça os novos tipos de tokens. Analisando a especificação da linguagem, notamos que precisamos suportar operadores lógicos (&&, ||, !), literais de texto (Strings) e diferenciar claramente atribuição de comparação.

Abra o arquivo include/compiler/token.h e substitua completamente a macro TOKEN_LIST antiga pela versão abaixo. Isso garantirá que todos os novos tokens estejam presentes e nomeados corretamente.

Observe a mudança importante: TOK_ASSIGN agora representa =, enquanto TOK_EQ representa ==. Essa distinção léxica facilita imensamente a vida do Parser no futuro.

// include/compiler/token.h

#define TOKEN_LIST \
    X(TOK_LPAREN)        /* ( */ \
    X(TOK_RPAREN)        /* ) */ \
    X(TOK_LBRACE)        /* { */ \
    X(TOK_RBRACE)        /* } */ \
    X(TOK_COLON)         /* : */ \
    X(TOK_SEMICOLON)     /* ; */ \
    X(TOK_COMMA)         /* , */ \
    X(TOK_PLUS)          /* + */ \
    X(TOK_MINUS)         /* - */ \
    X(TOK_STAR)          /* * */ \
    X(TOK_SLASH)         /* / */ \
    X(TOK_ASSIGN)        /* = */ \
    X(TOK_EQ)            /* == */ \
    X(TOK_BANG_EQ)       /* != */ \
    X(TOK_GT)            /* > */ \
    X(TOK_LT)            /* < */ \
    X(TOK_GT_EQ)         /* >= */ \
    X(TOK_LT_EQ)         /* <= */ \
    X(TOK_ARROW)         /* -> */ \
    X(TOK_AND)           /* && or and */ \
    X(TOK_OR)            /* || or or */ \
    X(TOK_NOT)           /* !  or not */ \
    X(TOK_CHAR)          /* Ex: 'c' */ \
    X(TOK_NUMBER)        /* number literal */ \
    X(TOK_STRING)        /* string literal */ \
    X(TOK_IDENTIFIER)    /* identifier */ \
    X(TOK_KW_INT)        /* int */ \
    X(TOK_KW_FLOAT)      /* float */ \
    X(TOK_KW_VOID)       /* void */ \
    X(TOK_KW_BOOL)       /* bool */ \
    X(TOK_KW_CHAR)       /* char */ \
    X(TOK_KW_STRING)     /* string */ \
    X(TOK_KW_IF)         /* if */ \
    X(TOK_KW_ELSE)       /* else */ \
    X(TOK_KW_LOOP)       /* loop */ \
    X(TOK_KW_RETURN)     /* return */ \
    X(TOK_KW_FN)         /* fn */ \
    X(TOK_KW_TRUE)       /* true */ \
    X(TOK_KW_FALSE)      /* false */ \
    X(TOK_ERROR)         /* <error> */ \
    X(TOK_EOF)           /* <eof> */ \

Lookahead8.3

Temos um problema clássico na hora de escrever analisadores léxicos que é a maneira de distinguir entre / (divisão) de // (comentário) ou = (atribuição) de == (igualdade). Uma forma muito usada para solucionar esse problema é fazer nosso Lexer ser capaz de espiar o próximo caractere sem consumi-lo imediatamente.

No arquivo src/compiler/lexer.c, adicione estas funções auxiliares antes da lógica de tokens. Elas são a base para resolvermos ambiguidades.

// src/compiler/lexer.c

// Retorna o caractere atual
static char peek(Lexer *lexer) {
    return *lexer->current;
}

// Retorna o caractere seguinte (Lookahead de 2ª ordem)
// Essencial para diferenciar "/*" (início de bloco) de "/" (divisão)
static char peek_next(Lexer *lexer) {
    if (is_at_end(lexer)) return '\0';
    return lexer->current[1];
}

// Auxiliar: Verifica se o caractere atual é o esperado.
// Se for, consome-o e retorna true. Útil para operadores compostos como '!='.
static bool match(Lexer *lexer, char expected) {
    if (is_at_end(lexer)) return false;
    if (*lexer->current != expected) return false;
    
    lexer->current++; // Consome
    return true;
}

Comentários8.4

Comentários existem apenas para humanos e devem ser totalmente ignorados pelo compilador, da mesma forma que espaços em branco e quebras de linha. Por esse motivo, o tratamento de comentários não envolve a criação de tokens, mas sim o "consumo" silencioso desses trechos ainda na fase de análise léxica, garantindo que eles não cheguem às etapas posteriores do compilador.

A lógica de detecção de comentários exige atenção especial ao encontrar uma barra /. Nesse ponto, o analisador léxico não pode assumir imediatamente que se trata de um operador de divisão; é necessário observar o caractere seguinte. Se o próximo caractere também for /, o trecho corresponde a um comentário de linha, que deve ser consumido até a próxima quebra de linha. Se o caractere seguinte for *, trata-se de um comentário de bloco, que deve ser consumido até que a sequência de fechamento */ seja encontrada. Caso nenhum desses padrões seja identificado, a barra não representa comentário, e o controle deve retornar para que o lexer_next_token a trate normalmente como um operador.

Substitua sua função skip_whitespace antiga por esta versão robusta:

static void skip_whitespace(Lexer *lexer) {
    for (;;) {
        char c = peek(lexer); // Apenas espia, não consome
        switch (c) {
            case ' ':
            case '\r':
            case '\t':
                advance(lexer);
                break;
            case '\n':
                lexer->line++;
                advance(lexer);
                break;
                
            case '/':
                if (peek_next(lexer) == '/') {
                    // Comentário Inline: Consome caracteres até o fim da linha
                    while (peek(lexer) != '\n' && !is_at_end(lexer)) {
                        advance(lexer);
                    }
                } 
                else if (peek_next(lexer) == '*') {
                    // Comentário de Bloco: Consome até encontrar "*/"
                    advance(lexer); // Consome /
                    advance(lexer); // Consome *
                    
                    while (!is_at_end(lexer)) {
                        if (peek(lexer) == '\n') lexer->line++;
                        
                        // Verifica fechamento
                        if (peek(lexer) == '*' && peek_next(lexer) == '/') {
                            advance(lexer); // Consome *
                            advance(lexer); // Consome /
                            break; // Sai do loop do comentário
                        }
                        advance(lexer);
                    }
                } else {
                    return; // É uma barra de divisão (/), não é whitespace/comentário.
                }
                break;
                
            default:
                return;
        }
    }
}

Verificando Dígitos e Letras8.4.0.1

Como não queremos depender da biblioteca <ctype.h> (para manter o controle total e portabilidade estrita), criaremos nossos helpers simples.

static bool is_digit(char c) {
    return c >= '0' && c <= '9';
}

static bool is_alpha(char c) {
    return (c >= 'a' && c <= 'z') ||
           (c >= 'A' && c <= 'Z') ||
            c == '_';
}

Literais para Números e Strings8.5

Agora precisamos ensinar o Lexer a ler dados.

Agora precisamos ensinar o Lexer a ler dados, adicionando novas funções em src/compiler/lexer.c. A leitura de strings consiste em consumir caracteres até encontrar a aspa de fechamento ". Durante esse processo, a presença de um \n dentro da string não encerra a leitura; em vez disso, o analisador apenas incrementa o contador de linhas, permitindo assim o suporte a strings multi-linha.

Para números, a lógica começa consumindo uma sequência de dígitos. Caso seja encontrado um ponto . seguido de outro dígito, o lexer identifica que se trata de um número de ponto flutuante, consome o ponto e continua a leitura da parte fracionária até que não haja mais dígitos válidos.

Adicione estas funções em src/compiler/lexer.c:

static Token string(Lexer *lexer) {
    while (peek(lexer) != '"' && !is_at_end(lexer)) {
        if (peek(lexer) == '\n') lexer->line++;
        advance(lexer);
    }

    if (is_at_end(lexer)) return error_token(lexer, "String não terminada.");

    advance(lexer); // Consome a aspa de fechamento
    return make_token(lexer, TOK_STRING);
}

static Token character(Lexer *lexer) {
    if (is_at_end(lexer)) return error_token(lexer, "Caractere não terminado.");
    
    advance(lexer); // Consome o conteúdo
    
    if (peek(lexer) != '\'') {
        return error_token(lexer, "Esperado ' fechando o literal de caractere.");
    }
    
    advance(lexer); // Consome '
    return make_token(lexer, TOK_CHAR);
}

static Token number(Lexer *lexer) {
    while (is_digit(peek(lexer))) advance(lexer);

    // Parte fracionária
    if (peek(lexer) == '.' && is_digit(peek_next(lexer))) {
        advance(lexer); // Consome o ponto
        while (is_digit(peek(lexer))) advance(lexer);
    }

    return make_token(lexer, TOK_NUMBER);
}

. Palavras-Chave e Identificadores8.6

O desafio aqui é: if é uma palavra-chave, mas if_value é um identificador. Ambos começam igual.

Implementaremos uma função identifier_type que compara o texto lido com nossa lista de keywords. Além das palavras reservadas padrão (if, loop), também verificaremos aqui os operadores lógicos textuais (and, or, not), convertendo-os para seus tokens correspondentes.

Adicione estas funções:

static TokenType check_keyword(Lexer *lexer, int start, int length,
                               const char *rest, TokenType type) {
    if (lexer->current - lexer->start == start + length &&
        memcmp(lexer->start + start, rest, length) == 0) {
        return type;
    }
    return TOK_IDENTIFIER;
}

static TokenType identifier_type(Lexer *lexer) {
    // Switch no primeiro caractere para performance (Trie simplificada)
    char c = lexer->start[0];
    switch (c) {
        case 'a': return check_keyword(lexer, 1, 2, "nd", TOK_AND); // and
        case 'b': return check_keyword(lexer, 1, 3, "ool", TOK_KW_BOOL);
        case 'c': return check_keyword(lexer, 1, 3, "har", TOK_KW_CHAR);
        case 'e': return check_keyword(lexer, 1, 3, "lse", TOK_KW_ELSE);
        
        case 'f':
            if (lexer->current - lexer->start > 1) {
                switch (lexer->start[1]) {
                    case 'a': return check_keyword(lexer, 2, 3, "lse", TOK_KW_FALSE);
                    case 'l': return check_keyword(lexer, 2, 3, "oat", TOK_KW_FLOAT);
                    case 'n': return check_keyword(lexer, 2, 0, "", TOK_KW_FN);
                }
            }
            break;
            
        case 'i': // if ou int
            if (lexer->current - lexer->start > 1) {
                switch (lexer->start[1]) {
                    case 'f': return check_keyword(lexer, 2, 0, "", TOK_KW_IF);
                    case 'n': return check_keyword(lexer, 2, 1, "t", TOK_KW_INT);
                }
            }
            break;
            
        case 'l': return check_keyword(lexer, 1, 3, "oop", TOK_KW_LOOP);
        case 'n': return check_keyword(lexer, 1, 2, "ot", TOK_NOT); // not
        case 'o': return check_keyword(lexer, 1, 1, "r", TOK_OR);   // or
        case 'r': return check_keyword(lexer, 1, 5, "eturn", TOK_KW_RETURN);
        case 's': return check_keyword(lexer, 1, 5, "tring", TOK_KW_STRING);
        case 't': return check_keyword(lexer, 1, 3, "rue", TOK_KW_TRUE);
        case 'v': return check_keyword(lexer, 1, 3, "oid", TOK_KW_VOID);
    }

    return TOK_IDENTIFIER;
}

static Token identifier(Lexer *lexer) {
    while (is_alpha(peek(lexer)) || is_digit(peek(lexer))) {
        advance(lexer);
    }
    return make_token(lexer, identifier_type(lexer));
}

O Loop Principal Atualizado8.7

Finalmente, vamos reescrever a função lexer_next_token. Ela agora delega o trabalho para as funções que criamos quando encontra letras, números ou aspas.

Além disso, observe o uso intensivo da função match nos casos do switch. É aqui que resolvemos as ambiguidades dos operadores:

  • = vs ==
  • ! vs !=
  • - vs ->

Substitua a função antiga inteiramente por esta:

Token lexer_next_token(Lexer *lexer) {
    skip_whitespace(lexer); // Agora trata comentários também!
    
    lexer->start = lexer->current;

    if (is_at_end(lexer)) return make_token(lexer, TOK_EOF);

    char c = advance(lexer);

    // 1. Identificadores e Keywords (Começam com letra)
    if (is_alpha(c)) return identifier(lexer);
    
    // 2. Números (Começam com dígito)
    if (is_digit(c)) return number(lexer);

    switch (c) {
        // Delimitadores Simples
        case '(': return make_token(lexer, TOK_LPAREN);
        case ')': return make_token(lexer, TOK_RPAREN);
        case '{': return make_token(lexer, TOK_LBRACE);
        case '}': return make_token(lexer, TOK_RBRACE);
        case ';': return make_token(lexer, TOK_SEMICOLON);
        case ',': return make_token(lexer, TOK_COMMA);
        case ':': return make_token(lexer, TOK_COLON);

        // Operadores Compostos (Lookahead)
        case '!':
            // Se o próximo for '=', retorna BANG_EQ (!=), senão retorna NOT (!)
            return make_token(lexer, match(lexer, '=') ? TOK_BANG_EQ : TOK_NOT); // ! ou !=
            
        case '=':
            // Se o próximo for '=', retorna EQ (==), senão retorna ASSIGN (=)
            return make_token(lexer, match(lexer, '=') ? TOK_EQ : TOK_ASSIGN);
            
        case '<':
            return make_token(lexer, match(lexer, '=') ? TOK_LT_EQ : TOK_LT); // < ou <=
            
        case '>':
            return make_token(lexer, match(lexer, '=') ? TOK_GT_EQ : TOK_GT); // > ou >=
            
        case '-':
            // Se o próximo for '>', retorna ARROW (->), senão retorna MINUS (-)
            if (match(lexer, '>')) return make_token(lexer, TOK_ARROW);
            return make_token(lexer, TOK_MINUS);

        // Lógica Simbólica (&&, ||)
        case '&':
            if (match(lexer, '&')) return make_token(lexer, TOK_AND);
            else return error_token(lexer, "Esperado '&' após '&'");
        case '|':
            if (match(lexer, '|')) return make_token(lexer, TOK_OR);
            else return error_token(lexer, "Esperado '|' após '|'");

        // Operadores Matemáticos Simples
        case '+': return make_token(lexer, TOK_PLUS);
        case '*': return make_token(lexer, TOK_STAR);
        case '/': 
            // Como skip_whitespace já tratou comentários (// e /*), 
            // se chegamos aqui, é garantido que é uma divisão matemática.
            return make_token(lexer, TOK_SLASH);

        // Literais de Texto e Char
        case '"': return string(lexer);
        case '\'': return character(lexer);

        default:
            return error_token(lexer, "Caractere inesperado.");
    }
}

Testando a Implementação Completa8.8

Agora nosso compilador é capaz de entender a riqueza da linguagem. Vamos testar com um arquivo que usa todas as novas features: comentários, lógica textual, strings e comparações. Atualize o arquivo teste.slang para o conteúdo:

/* Exemplo Completo da Linguagem
  Testando Lexer V2
*/

x: int = 5
name: string = "Compiladores"
valid: bool = true

// Testando lógica textual e simbólica
if x >= 5 and valid {
    x = x + 1
}

if x == 6 || !valid {
    print("Sucesso")
}

fn process(v: void) -> int {
    return 0
}
Estilização do código

Observe que o código no VS Code não apresenta estilização, pois a linguagem slang não existe. Para facilitar o acompanhamento do restante da disciplina, foi desenvolvida uma extensão que pode ser instalada para aplicar a estilização adequada ao VS Code.

Faça o download da Extensão VSIX. Para instalar, no VS Code vá até Extensões, clique no ícone de três pontos (...), selecione Install from VSIX e escolha o arquivo baixado.

Caso necessário, reinicie o programa para que o código passe a aparecer corretamente estilizado.

Compile e execute. A saída deve listar corretamente TOK_STRING ("Compiladores"), ignorar os comentários, identificar TOK_AND, TOK_EQ_EQ e TOK_KW_VOID.

Com isso, encerramos o Front-end Léxico. Temos um Scanner robusto, capaz de ignorar ruídos (espaços e comentários) e categorizar corretamente a entrada para o Parser, que será nosso próximo grande módulo.

A ordem importa!!!

Lembre-se que a ordem que você define as funções no arquivo lexer.c importa, uma vez que não colocamos as definições das funções no arquivo.

Caso esteja tendo erro de importação, tente colar no arquivo src/compiler/lexer.c o seguinte código após as macros de #include.

static bool is_digit(char c);
static bool is_alpha(char c);
static bool is_at_end(Lexer *lexer);
static char advance(Lexer *lexer);
static Token make_token(Lexer *lexer, TokenType type);
static Token error_token(Lexer *lexer, const char *message);
static char peek(Lexer *lexer);
static char peek_next(Lexer *lexer);
static bool match(Lexer *lexer, char expected);
static Token string(Lexer *lexer);
static Token character(Lexer *lexer);
static Token number(Lexer *lexer);
static TokenType check_keyword(Lexer *lexer, int start, int length, const char *rest, TokenType type);
static TokenType identifier_type(Lexer *lexer);
static Token identifier(Lexer *lexer);
static void skip_whitespace(Lexer *lexer);

Próximos passos8.9

Conteúdo em desenvolvimento!

Este conteúdo ainda não foi finalizado. Assim que estiver completo, este aviso será atualizado com o link correspondente.