Trilha 18 · Lógica e teoria da computação

Regex e autômatos

Expressões regulares parecem magia para iniciantes, mas têm uma base matemática sólida: toda regex compila para um NFA (autômato finito não-determinístico), que por sua vez compila para um DFA. Entender essa cadeia explica tanto o poder quanto as limitações das regex.

① Intuição

Regex = linguagem para descrever linguagens

Uma expressão regular descreve um conjunto de strings — a linguagem que ela reconhece. Por exemplo, (a|b)*abb descreve todas as strings sobre {a, b} que terminam com "abb": "abb", "aabb", "babb", "aaabb", ...

O teorema de Kleene (1956) prova que as linguagens descritas por expressões regulares são exatamente as linguagens reconhecidas por DFAs — as linguagens regulares. Quando você escreve re.compile(r"\\d+"), o motor compila isso para um DFA (via NFA intermediário) e o executa contra a entrada.

Regex não pode fazer tudo: o Lema do Bombeamento prova que certas linguagens não são regulares. O exemplo clássico é aⁿbⁿ — strings com o mesmo número de 'a' e 'b'. Nenhuma regex consegue reconhecê-la porque requer contar arbitrariamente — e DFAs têm memória finita (estado finito). Para isso, precisamos de autômatos com pilha (lição seguinte).
② Visualização interativa

DFA para o padrão (a|b)*abb

Digite uma string com letras 'a' e 'b' e avance passo a passo. O DFA tem 4 estados; q3 (duplo círculo) é o único aceitador — a string deve terminar aqui.

entrada (a/b):
q0q1q2q3baababa
a
a
b
b
Padrão: (a|b)*abb — qualquer string que termine com "abb".
0/5
③ Explicação técnica

Regex em Python

# Expressões regulares em Python — sintaxe e operações
import re

# Metacaracteres fundamentais:
# .    qualquer caractere (exceto \n por padrão)
# *    zero ou mais do anterior
# +    um ou mais do anterior
# ?    zero ou um do anterior
# |    alternância (ou)
# ()   grupo
# []   classe de caracteres
# ^$   início e fim da string

# Exemplos:
re.match(r"(a|b)*abb", "aabb")       # match — termina em "abb"
re.match(r"\d+",          "42px")       # match "42"
re.fullmatch(r"[A-Za-z_]\w*", "nome_1") # identificador
re.findall(r"\b\w+\b", "olá mundo")  # ['olá', 'mundo']

# Grupos de captura:
m = re.match(r"(\d{4})-(\d{2})-(\d{2})", "2026-06-14")
m.group(1)  # '2026'
m.group(2)  # '06'
m.group(3)  # '14'

Construção de Thompson: de regex para NFA

# Compilação de regex para NFA (construção de Thompson)
# Cada operação de regex produz um fragmento de NFA

# Símbolo 'a':
#   →[q_ini] --a--> [q_fim]

# Concatenação: r · s  (r seguido de s)
#   NFA(r).fim → ε → NFA(s).ini

# Alternância: r | s
#   novo_ini → ε → NFA(r).ini
#   novo_ini → ε → NFA(s).ini
#   NFA(r).fim → ε → novo_fim
#   NFA(s).fim → ε → novo_fim

# Fechamento de Kleene: r*
#   novo_ini → ε → NFA(r).ini
#   novo_ini → ε → novo_fim     (aceita string vazia)
#   NFA(r).fim → ε → NFA(r).ini (repetição)
#   NFA(r).fim → ε → novo_fim

# Após construir NFA → algoritmo de subconjuntos → DFA
# re.compile() faz exatamente isso (em C, muito rápido)
Backtracking vs DFA: muitos motores de regex (Python re, PCRE) usam backtracking com extensões (lookahead, backreferences) que vão além de linguagens regulares — mas podem ter complexidade exponencial no pior caso (ReDoS). Motores baseados em DFA puro (RE2, ripgrep) garantem O(n) no comprimento da entrada mas não suportam backreferences. Ripgrep — o grep mais rápido — usa um motor DFA híbrido escrito em Rust.
④ Projeto para programar

Construindo um motor de regex

Mini projeto: implemente um motor de regex simplificado que suporte apenas ., * e caracteres literais (sem grupos ou alternância). Use a abordagem de simulação direta de NFA: para cada caractere da entrada, compute o conjunto de estados alcançáveis por transições ε. Teste com padrões como "a*b", ".*c".

Projeto principal: estenda o motor para suportar |, () e +. Compile a regex para NFA usando a construção de Thompson (representação explícita como grafo). Execute o NFA com simulação de subconjunto. Verifique que o resultado é idêntico ao módulo re para 1000 strings aleatórias.

Desafio extra: implemente um detector de ReDoS: dado um padrão regex, analise se ele pode exibir backtracking exponencial (ambiguidade exponencial). Use a técnica de análise de "caminhos de estrela aninhadas" — se encontrar, reporte qual subcadeia pode causar o pior caso e sugira uma reescrita.

⑤ Exercícios rápidos

Teste sua intuição

Qual é a relação entre regex, NFA e DFA?
Por que uma expressão regular não pode reconhecer a linguagem aⁿbⁿ?
Qual é a diferença prática entre motores de regex baseados em DFA vs backtracking?
⑥ Aplicações no mundo real

Onde você encontra isso

🔍

ripgrep — busca ultrarrápida

ripgrep (rg) é a ferramenta de busca mais rápida em benchmarks reais. Internamente usa a crate regex do Rust, que compila padrões para DFAs hibridizados — lazy DFAs que constroem estados sob demanda e DFAs completos para padrões simples. Garante O(n) sem risco de ReDoS. É o motor de busca padrão do VS Code e do GitHub code search.

🛡️

WAF — Web Application Firewall

Firewalls de aplicação web usam regex para detectar payloads maliciosos (SQL injection, XSS). Um WAF mal escrito com regex backtracking é vulnerável ao próprio ReDoS: um atacante envia uma string maliciosa que faz o motor de regex ficar em loop exponencial, causando DoS no firewall. CloudFlare sofreu um incidente desse tipo em 2019, derrubando serviços por 27 minutos.

🧬

Bioinformática — busca em genomas

Ferramentas como BLAST e HMMER buscam padrões em sequências de DNA e proteínas. As sequências são strings sobre alfabetos {A, C, G, T} ou os 20 aminoácidos. Modelos de Markov ocultos (HMMs) são generalizações probabilísticas de autômatos — cada estado emite um símbolo com uma certa probabilidade. Um perfil HMM de uma família proteica é essencialmente um autômato treinado.

← Anterior: Máquinas de estado Próxima: Gramáticas e linguagens →