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_BREAK)      /* break */ \
    X(TOK_KW_CONTINUE)   /* continue */ \
    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);

    if (peek(lexer) == '.' && is_digit(peek_next(lexer))) {
        advance(lexer);
        while (is_digit(peek(lexer))) advance(lexer);
    }

    // Um número não pode ser seguido imediatamente por letra ou '_' (ex: 123x é inválido)
    if (is_alpha(peek(lexer))) {
        while (is_alpha(peek(lexer)) || is_digit(peek(lexer))) advance(lexer);
        return error_token(lexer, "Literal numérico inválido.");
    }

    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':
            if (lexer->current - lexer->start > 1) {
                switch (lexer->start[1]) {
                    case 'o': return check_keyword(lexer, 2, 2, "ol", TOK_KW_BOOL);
                    case 'r': return check_keyword(lexer, 2, 3, "eak", TOK_KW_BREAK);
                }
            }
            break;
        case 'c':
            if (lexer->current - lexer->start > 1) {
                switch (lexer->start[1]) {
                    case 'h': return check_keyword(lexer, 2, 2, "ar", TOK_KW_CHAR);
                    case 'o': return check_keyword(lexer, 2, 6, "ntinue", TOK_KW_CONTINUE);
                }
            }
            break;
        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.");
    }
}

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);

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
}

Para a entrada a cima sua saída deve ser semelhante à seguinte:

=== Iniciando Análise Léxica ===
<TOK_IDENTIFIER, "x">
<TOK_COLON, ":">
<TOK_KW_INT, "int">
<TOK_ASSIGN, "=">
<TOK_NUMBER, "5">
<TOK_IDENTIFIER, "name">
<TOK_COLON, ":">
<TOK_KW_STRING, "string">
<TOK_ASSIGN, "=">
<TOK_STRING, ""Compiladores"">
<TOK_IDENTIFIER, "valid">
<TOK_COLON, ":">
<TOK_KW_BOOL, "bool">
<TOK_ASSIGN, "=">
<TOK_KW_TRUE, "true">
<TOK_KW_IF, "if">
<TOK_IDENTIFIER, "x">
<TOK_GT_EQ, ">=">
<TOK_NUMBER, "5">
<TOK_AND, "and">
<TOK_IDENTIFIER, "valid">
<TOK_LBRACE, "{">
<TOK_IDENTIFIER, "x">
<TOK_ASSIGN, "=">
<TOK_IDENTIFIER, "x">
<TOK_PLUS, "+">
<TOK_NUMBER, "1">
<TOK_RBRACE, "}">
<TOK_KW_IF, "if">
<TOK_IDENTIFIER, "x">
<TOK_EQ, "==">
<TOK_NUMBER, "6">
<TOK_OR, "||">
<TOK_NOT, "!">
<TOK_IDENTIFIER, "valid">
<TOK_LBRACE, "{">
<TOK_IDENTIFIER, "print">
<TOK_LPAREN, "(">
<TOK_STRING, ""Sucesso"">
<TOK_RPAREN, ")">
<TOK_RBRACE, "}">
<TOK_KW_FN, "fn">
<TOK_IDENTIFIER, "process">
<TOK_LPAREN, "(">
<TOK_IDENTIFIER, "v">
<TOK_COLON, ":">
<TOK_KW_VOID, "void">
<TOK_RPAREN, ")">
<TOK_ARROW, "->">
<TOK_KW_INT, "int">
<TOK_LBRACE, "{">
<TOK_KW_RETURN, "return">
<TOK_NUMBER, "0">
<TOK_RBRACE, "}">
<TOK_EOF, "">
=== Análise Concluída ===
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 e TOK_KW_VOID.

Para rodar, caso não tenha feita na etapa anterior ainda, crie uma pasta chamada build e roda cmake -B build. Caso tenha feito apenas uma mudança em um arquivo existente basta rodar cmake --build build para recompilá-lo. Entretanto, caso adicione um novo arquivo .h ou .c precisará rodar o primeiro e o segundo comando novamente.

Após compilar o projeto basta chamar o binário pelo terminal passando o nome do arquivo que quer "compiler", no nosso caso ./build/compiler teste.slang

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.

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.