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 ===
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
builde rodacmake -B build. Caso tenha feito apenas uma mudança em um arquivo existente basta rodarcmake --build buildpara recompilá-lo. Entretanto, caso adicione um novo arquivo.hou.cprecisará 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
Este conteúdo ainda não foi finalizado. Assim que estiver completo, este aviso será atualizado com o link correspondente.