Aula 06: Ferramentas: Flex

Data de Publicação

25/03/2026

Data de Modificação

08/03/2026

Objetivos

Ao final desta aula, você será capaz de:

  • Utilizar o Flex para gerar scanners automaticamente a partir de especificações em arquivos .l.
  • Escrever definições (apelidos), regras com padrões e ações em C (ou o backend escolhido).
  • Integrar o scanner gerado com um parser (ex.: Bison) via tokens e yylval.
  • Explicar como o Flex resolve conflitos (ordem das regras, maximal munch) e como isso se relaciona com a teoria ER → AFD.

Motivação prática

Escrever scanners manualmente (com switch/if ou tabelas de transição) tende a:

  • Introduzir bugs em casos de borda (ERs complexas, lookahead).
  • Dificultar a manutenção quando a linguagem ganha novos operadores ou palavras-chave.
  • Não aproveitar algoritmos clássicos (Thompson, subconjuntos, minimização) de forma automática.

Ferramentas como Flex encapsulam a pipeline ER → AFN → AFD → código:

  • Você especifica o que é cada token (em ERs e ações).
  • A ferramenta gera como reconhecer (AFD simulado em C).

Pipeline do Flex

O diagrama abaixo resume o fluxo: do arquivo de especificação ao programa executável.

flowchart LR
  Arquivo[Arquivo .l] --> Flex[Flex]
  Flex --> Código[lex.yy.c]
  Código --> Comp[Compilador C]
  Comp --> Exec[Executável]
  Exec --> Entrada[Entrada]
  Entrada --> yylex[yylex]
  yylex --> Tokens[Tokens]

O Flex lê o .l, aplica construções equivalentes a Thompson e subconjuntos (e possivelmente minimização), e gera um arquivo C (por exemplo, lex.yy.c) que contém a função yylex() e as tabelas ou o código do AFD. Esse arquivo é compilado e linkado com o resto do projeto (por exemplo, o parser gerado pelo Bison).

Conteúdo

1. Estrutura do arquivo .l

O arquivo Flex é dividido em três seções separadas por %%.

Seção 1: Definições

  • Bloco %{ ... %}: código C (ou outro) copiado literalmente para o início do arquivo gerado (includes, variáveis globais, declarações).
  • Apelidos (nomes): definições da forma NOME expressão para reutilizar ERs. Ex.: DIGIT [0-9], ID [a-zA-Z][a-zA-Z0-9]*.

Seção 2: Regras

Cada linha tem a forma:

padrão   ação
  • Padrão: uma ER (podendo usar apelidos entre chaves, ex.: {DIGIT}+).
  • Ação: código C executado quando o padrão casa. Pode acessar yytext (lexema atual), yyleng (tamanho), e deve retornar o token (inteiro) para o parser quando aplicável. Ação vazia significa “ignorar” (ex.: espaços e comentários).

Seção 3: Código do usuário

Código C copiado ao final do arquivo gerado (ex.: main() que chama yylex() em loop, ou funções auxiliares). Em projetos com Bison, o main costuma estar em outro arquivo que chama o parser; o parser, por sua vez, chama yylex() para obter tokens.

2. Exemplo completo comentado

O exemplo abaixo ilustra um scanner mínimo para inteiros, identificadores, espaços e um operador. Em um compilador real, as ações retornariam constantes de token definidas em um header compartilhado com o parser (ex.: gerado pelo Bison).

%{
#include <stdio.h>
int line_num = 1;
%}

DIGIT   [0-9]
ID      [a-zA-Z][a-zA-Z0-9]*

%%
{DIGIT}+        { printf("NUMBER: %s\n", yytext); return NUMBER; }
{ID}            { printf("ID: %s\n", yytext); return ID; }
"+"             { return PLUS; }
[ \t]           { /* ignorar */ }
\n              { line_num++; }
.               { fprintf(stderr, "Erro linha %d: %s\n", line_num, yytext); }
%%

int main(void) {
    while (yylex() != 0) { }
    return 0;
}
  • {DIGIT}+: um ou mais dígitos → token NUMBER.
  • {ID}: identificador → token ID.
  • "+": operador literal → token PLUS.
  • [ \t]: espaço ou tab → ignorado (sem return).
  • \n: nova linha → atualiza contador de linha.
  • .: qualquer outro caractere → erros léxicos (e opcionalmente return de token de erro).

3. Como o Flex resolve conflitos

Dois tipos de conflito aparecem quando várias regras podem casar com o mesmo trecho da entrada:

  1. Mesma entrada, várias regras
    Ex.: a string if casa com uma regra para a palavra-chave if e com a regra para identificador.
    Regra: o Flex escolhe a primeira regra listada que casar. Por isso palavras-chave devem vir antes da regra de identificador.

  2. Prefixo comum, tamanhos diferentes
    Ex.: <= pode ser reconhecido como < e depois =, ou como um único token <=.
    Regra (maximal munch): o Flex escolhe o maior lexema possível. Assim <= é reconhecido como um token se houver uma regra para "<=".

Essas regras tornam o comportamento determinístico e previsível, alinhado à simulação de um AFD (sempre uma única transição por caractere e uma única regra “vencedora” ao final do lexema).

4. Integração com Bison/Yacc

Quando o scanner é usado com um parser gerado pelo Bison:

  • Os valores de token (NUMBER, ID, PLUS, etc.) são definidos no Bison (ou em um header gerado por ele) e declarados no início do .l com %option header ou incluindo o header do parser.
  • O valor semântico do token (ex.: o número inteiro para NUMBER, o lexema para ID) é passado pela variável global yylval (ou por argumentos, dependendo da opção). O parser usa esse valor para construir a AST.
  • O Flex é invocado pelo parser: a cada necessidade de um novo token, o parser chama yylex(); o Flex retorna o tipo do token e preenche yylval.
flowchart LR
  Parser[Parser Bison] --> yylex
  yylex[yylex Flex] --> Tok[Token + yylval]
  Tok --> Parser

5. Variáveis e funções úteis

Nome Descrição
yytext Ponteiro para o lexema atual (string terminada em \0).
yyleng Comprimento do lexema em yytext.
yylval Valor semântico do token (compartilhado com Bison).
yylex() Função que retorna o próximo token; atualiza yytext, yyleng, yylval.
yyin Arquivo de entrada (por padrão stdin).

Conexões com o projeto Mini-Pascal

  • Na Etapa 2 você implementa um scanner manual; os conceitos (padrões, lexemas, tokens, lookahead, maximal munch) são os mesmos que o Flex automatiza.
  • Na Etapa 4, ao usar ANTLR4, a especificação léxica é feita na gramática (.g4), não em um arquivo .l; mesmo assim, a ideia de “regras por padrão + ação” e a resolução de conflitos são análogas.
  • Entender Flex ajuda a ler documentação e exemplos clássicos de compiladores em C e a comparar com geradores baseados em gramáticas (ANTLR).

Resumo

  • O Flex gera um scanner em C a partir de um arquivo .l com definições, regras (padrão + ação) e código do usuário.
  • A pipeline é: .l → Flex → lex.yy.c → compilação → executável; yylex() simula o AFD sobre a entrada.
  • Conflitos: primeira regra que casar vence; maximal munch para lexemas maiores.
  • Integração com Bison: tokens definidos no parser; valor semântico em yylval; parser chama yylex() para obter tokens.

Perguntas de reflexão

  • Por que colocar a regra de identificador antes da regra de palavras-chave produziria comportamento indesejado?
  • Como você passaria o valor numérico de um token NUMBER para o parser usando yylval?
  • Que vantagens e desvantagens você vê em usar Flex em vez de um scanner manual para o Mini-Pascal?

Referências

  • Aho et al. (2006). Compilers: Principles, Techniques, and Tools. Cap. 3 (Lex).
  • Documentação do Flex: flex.sourceforge.net.

Materiais da aula

Última atualização: 08/03/2026

De volta ao topo