- A PostHog reescreveu um parser SQL em C++ baseado em ANTLR usando várias sessões do Claude Code e produziu 16 mil linhas de parser em Rust, 5 mil linhas de ferramentas e milhares de linhas de testes, obtendo cerca de 70 vezes mais desempenho em um notebook
- A nova implementação se concentra em um parser preditivo por descida recursiva com um núcleo de expressões Pratt, aplicando lookahead limitado e backtracking especulativo apenas onde necessário para eliminar o custo da travessia genérica de grafos do ANTLR
- Usando o parser C++ existente como implementação de referência, a equipe combinou testes baseados em propriedades, consultas reais anonimizada, testes de regressão, geração guiada por cobertura de código e redução com ShrinkRay para eliminar iterativamente divergências entre os dois parsers
- No modo shadow em produção, milhões de resultados de parsing nunca diferiram do parser antigo, permitindo migrar o tráfego em poucas horas, com ganho médio de 454 vezes em produção
- A abordagem em que o gerador de parser fornece a gramática e a implementação de referência, enquanto um LLM valida a equivalência com fuzzing, mostra o potencial de reduzir de meses para dias o desenvolvimento de parsers de alto desempenho, antes dependente de conhecimento especializado
Por que a PostHog precisa de um parser SQL
- A PostHog transforma o SQL de entrada em SQL bruto do ClickHouse para permitir que usuários acessem os dados diretamente com SQL
- Fornece uma visão lógica dos dados independente da disposição física no banco de dados
- Protege consultas existentes contra quebras mesmo quando a camada de banco de dados muda
- Adiciona otimizações de desempenho e controle de acesso durante a transformação
- A maioria das ferramentas da PostHog, como análise de produto, replay de sessão e rastreamento de erros, também envia consultas escritas em SQL pelo mesmo processo de transformação
- Para transformar SQL, primeiro é preciso convertê-lo em uma árvore sintática abstrata (AST), que depois é convertida novamente em SQL do ClickHouse
- O parser é o primeiro componente a lidar com entrada de consulta não confiável
- Todo o controle de acesso e as otimizações posteriores também dependem da árvore criada pelo parser
Estrutura e custo do parser ANTLR antigo
- Antes da codificação com IA, escrever e manter um parser manualmente era muito difícil, então a PostHog usava o gerador de parser open source ANTLR
- Ao fornecer a gramática em um arquivo declarativo .g4, o ANTLR gera a maior parte do código do parser
- Como o parser antigo já era gerado em C++, este ganho de desempenho não veio simplesmente da troca da linguagem de implementação para Rust
- O ANTLR é poderoso e flexível, mas exige mais trabalho para processar cada token
- Ele compila a gramática em algo próximo a uma rede de transição aumentada (ATN), isto é, um autômato finito não determinístico (NFA) com pilha
- Em tempo de execução, um interpretador genérico percorre esse grafo
- Em vez de funções escritas diretamente, como
parseExpression(), ele passa por camadas adicionais de abstração e chamadas indiretas
- Como também suporta lookahead dinâmico arbitrário, quando há várias alternativas ele simula simultaneamente todas as interpretações até restar apenas uma válida
- Mesmo um ANTLR muito otimizado dificilmente consegue ser mais rápido que um parser por descida recursiva implementado manualmente por causa dessa arquitetura de interpretador de travessia de grafos
Duas implementações de parser testadas em paralelo
- Embora a IA torne mais fácil escrever e manter um parser implementado manualmente, não bastava apenas pedir ao Claude um parser Rust sem erros
- O Claude cometeu muitos erros
- Repetidamente duvidou da viabilidade da reescrita
- Tinha a tendência de encerrar o trabalho ao fim de cada rodada de codificação
- Em várias sessões de longa duração do Claude Code, foram testadas em paralelo duas abordagens
- A abordagem centrada em desempenho usava parser por descida recursiva e loop de expressões Pratt, com lookahead e backtracking adicionados apenas onde necessário
- A abordagem centrada em chance de sucesso seguia o comportamento do ANTLR o máximo possível, mas implementava as transições em código explícito em vez de usar travessia genérica de grafos
- As duas abordagens acabaram funcionando em níveis parecidos, mas levou alguns dias para confirmar isso
- O objetivo era corresponder exatamente ao parser C++ existente em todas as consultas realistas e produzir resultados o mais próximos possível também em consultas artificiais
- Até SQLs que parecem anormais, mas são válidos, como
SELECT SELECT FROM FROM WHERE WHERE AND AND, entraram no teste
- Até SQLs que parecem anormais, mas são válidos, como
- O parser C++ existente foi tratado como implementação de referência (oráculo), e o desenvolvimento seguiu uma abordagem guiada por testes: encontrar SQL tratado de forma diferente, corrigir o novo parser e comparar de novo
Gerando divergências com testes baseados em propriedades
- Primeiro foram aprovados os testes de regressão acumulados durante o desenvolvimento do parser antigo, e depois novas divergências passaram a ser geradas de forma sistemática
- Em testes baseados em propriedades (PBT) com Hypothesis, a propriedade verificada foi definida como “o novo parser corresponde à implementação de referência”
- A entrada são consultas SQL
- O Hypothesis procura consultas em que os resultados dos dois parsers não coincidem
- Para gerar SQL interessante, foi criada com o Claude uma ferramenta que gera código de um gerador de SQL a partir do arquivo de gramática do ANTLR
- Também foi escrito um parser separado para ler o próprio arquivo
.g4 - Depois, transformações como troca de tokens ou adição de parênteses foram incluídas na etapa de geração
- Também foi escrito um parser separado para ler o próprio arquivo
Como os prompts foram montados para evitar correções frágeis
- O PBT produzia novos testes de forma estável, mas o Claude repetia correções frágeis, como consertar um caso específico com lookahead de um token e depois descobrir que eram necessários dois tokens
- Como a janela de contexto frequentemente enchia e era compactada, havia a possibilidade de corrigir o código tendo esquecido a gramática real ou o comportamento do parser de referência
- Para aliviar esse problema, foi dado o comando para carregar no contexto, imediatamente antes de escrever o código de correção de cada divergência, tanto os arquivos de gramática relevantes quanto o código-fonte em C++
CPU e Claude trabalhando juntos sem parar
- O PBT continuava rodando em segundo plano, registrando casos de falha em arquivos, e as ferramentas foram montadas para que o Claude buscasse esses casos quando não houvesse outra tarefa
- Os casos de falha eram coletados por vários caminhos
- Testes de regressão existentes
- SQL gerado por PBT
- Consultas anonimizadas trazidas dos logs de produção
- Casos criados ao pedir a agentes em segundo plano que “pensassem profundamente em edge cases”
- Os dois parsers desenvolvidos em paralelo compartilhavam a coleção de testes de regressão, para que uma falha descoberta em uma sessão fosse imediatamente refletida na outra
- O Hypothesis consegue reduzir casos gerados por ele próprio até uma reprodução mínima, mas isso não se aplica a SQL externo, então nesses casos foi usado o ShrinkRay
- Depois também foi adicionada geração de testes guiada por cobertura de código
- Ela detectava componentes de SQL ainda não exercitados e passava a enviesar a geração para produzir essas estruturas com mais frequência
- Não foi essencial para atingir 100% de precisão no conjunto de consultas de produção, mas ajudou a encontrar casos muito sutis
Ciclo automatizado para encontrar e corrigir falhas
- O loop final de desenvolvimento repetia autonomamente os seguintes passos
- Gerar novas falhas com PBT, conjunto de consultas reais, testes de regressão e exploração de edge cases
- Reduzir as falhas e adicioná-las à lista de testes de regressão, que seguia crescendo
- Priorizar soluções gerais e verificar como a gramática e a implementação C++ de referência tratam o caso
- Aplicar a correção e gerar um resumo de um parágrafo legível por humanos
- Rodar toda a suíte de regressão para confirmar que todos os casos passam
- Executar o mesmo processo novamente de forma autônoma
- Como o novo parser era muito mais rápido, ele podia rodar em produção junto com o parser C++ existente em modo shadow, reportando qualquer diferença de resultado
Validação em produção e migração
- Na comparação prévia com logs de consultas de produção, cerca de 50 mil consultas foram testadas
- No modo shadow, milhões de execuções de parsing foram feitas rapidamente, sem uma única divergência em relação ao parser antigo
- O plano original era manter o modo shadow por alguns dias, mas os resultados foram fortes o bastante para mudar o tráfego de produção para o novo parser em poucas horas
- Após a migração, foi mantido um shadow reverso de 0,1%, comparando novamente com o parser antigo
- O novo parser produz não apenas a AST, mas também as informações de posição no código-fonte exatamente como o parser C++ do ANTLR
70 vezes no notebook, 454 vezes em produção
- Em benchmark de notebook, o novo parser registrou desempenho cerca de 70 vezes superior ao parser antigo
- Em consultas de produção, ficou em média 454 vezes mais rápido
- Em produção, ele lida principalmente com SQLs mais longos que não acertam o cache do parser, por isso a diferença de desempenho foi maior do que no notebook
- O resultado final é composto por cerca de 16 mil linhas de código do parser, 5 mil linhas de ferramentas e milhares de linhas de testes
- Em consultas realistas, ele é equivalente ao parser antigo, e as diferenças ficam limitadas a um número muito pequeno de consultas artificialmente construídas
Estrutura do parser final e a mudança na forma de desenvolver
- O parser final tem a seguinte estrutura
- A maior parte foi implementada como parser preditivo por descida recursiva
- O processamento de expressões usa um núcleo de parser Pratt
- O cursor básico é LL(2), ampliando o alcance apenas em pontos específicos com exploração limitada de lookahead sem consumir a entrada
- Backtracking especulativo local, baseado em escolha ordenada, é aplicado apenas em um pequeno número de decisões realmente necessárias
- O parser inteiro foi escrito em Rust pelo Claude Opus 4.7 em maio de 2026, sem código escrito manualmente
- A configuração de PBT que gera entradas a partir da gramática e orienta a geração com cobertura de código está mais próxima do estado da arte em fuzzing de parser do que de um simples “vibe coding”
- Um trabalho que poderia levar meses mesmo para um desenvolvedor com conhecimento específico de parsers foi realizado em poucos dias
- No futuro, pode se tornar comum usar geradores de parser como o ANTLR para fornecer a implementação de referência, enquanto LLMs escrevem parsers implementados manualmente e mais rápidos, alinhando a equivalência com PBT e fuzzing
Ainda não há comentários.