1 puntos por GN⁺ 5 시간 전 | Aún no hay comentarios. | Compartir por WhatsApp
  • PostHog reescribió un parser de SQL en C++ basado en ANTLR usando varias sesiones de Claude Code, creando un parser en Rust de 16K líneas junto con 5K líneas de herramientas y miles de líneas de pruebas, y logró una mejora de velocidad de unas 70 veces en una laptop
  • La nueva implementación se centra en un parser predictivo de descenso recursivo y un núcleo de expresiones Pratt, aplicando lookahead limitado y backtracking especulativo solo donde hace falta para eliminar el costo del recorrido genérico de grafos de ANTLR
  • Tomando el parser previo en C++ como implementación de referencia, combinaron pruebas basadas en propiedades, consultas reales anonimizadas, pruebas de regresión, generación guiada por cobertura de código y reducción con ShrinkRay para ir eliminando repetidamente las discrepancias entre ambos parsers
  • En modo shadow en producción, millones de resultados de parsing nunca difirieron del parser anterior, por lo que cambiaron el tráfico en cuestión de horas, registrando una mejora de 454 veces en promedio en producción
  • El enfoque en el que un generador de parsers aporta la gramática y una implementación de referencia, mientras un LLM valida la equivalencia con fuzzing, muestra el potencial de reducir de meses a días el desarrollo de parsers de alto rendimiento que antes requería conocimiento especializado

Por qué PostHog necesita un parser de SQL

  • PostHog transforma el SQL de entrada en SQL crudo de ClickHouse para permitir que los usuarios accedan directamente a los datos con SQL
    • Proporciona una vista lógica de los datos independiente de la disposición física de la base de datos
    • Protege las consultas existentes para que no se rompan aunque cambie la capa de base de datos
    • Añade optimizaciones de rendimiento y control de acceso durante la transformación
  • La mayoría de las herramientas de PostHog, como análisis de producto, session replay y seguimiento de errores, también pasan consultas escritas en SQL por ese mismo proceso de transformación
  • Para transformar SQL, primero hay que convertirlo en un árbol de sintaxis abstracta (AST), que luego se transforma otra vez en SQL de ClickHouse
  • El parser es el componente que primero procesa entradas de consultas no confiables
    • Todo el control de acceso y las optimizaciones posteriores dependen del árbol que genera el parser

La estructura y el costo del parser anterior con ANTLR

  • Antes de la codificación con IA, escribir y mantener un parser a mano era muy difícil, así que PostHog usó el generador de parsers open source ANTLR
  • Si se proporciona la gramática en un archivo declarativo .g4, ANTLR genera la mayor parte del código del parser
  • El parser anterior ya estaba generado en C++, así que esta mejora de rendimiento no fue simplemente el resultado de cambiar el lenguaje de implementación a Rust
  • ANTLR es potente y flexible, pero requiere más trabajo al procesar cada token
  • También admite lookahead dinámico arbitrario, por lo que si hay varias alternativas simula todas las interpretaciones al mismo tiempo hasta que solo una siga siendo válida
  • Incluso ANTLR bien optimizado difícilmente puede superar a un parser de descenso recursivo implementado directamente cuando su estructura se basa en un intérprete de recorrido de grafos

Probar en paralelo dos implementaciones del parser

  • Con IA se vuelve más fácil escribir y mantener un parser implementado directamente, pero no bastaba con pedirle a Claude un parser en Rust sin errores
    • Claude cometía muchos errores
    • Repetidamente dudaba de que la reescritura fuera posible
    • Al final de cada ronda de codificación tendía a intentar dar el trabajo por terminado
  • Probaron dos enfoques en paralelo usando varias sesiones de Claude Code de larga duración
    • El enfoque centrado en rendimiento usó un parser de descenso recursivo y un loop de expresiones Pratt, agregando lookahead y backtracking solo donde era necesario
    • El enfoque centrado en probabilidad de éxito siguió lo más posible el comportamiento de ANTLR, pero implementando las transiciones como código explícito en vez de recorrido genérico de grafos
  • Ambos enfoques terminaron funcionando a un nivel parecido, aunque confirmarlo tomó varios días
  • El objetivo era coincidir por completo con el parser previo en C++ en todas las consultas realistas y dar resultados lo más parecidos posible incluso en consultas artificiales
    • Incluso SQL válido pero extraño, como SELECT SELECT FROM FROM WHERE WHERE AND AND, se incluyó en las pruebas
  • Tomaron el parser previo en C++ como implementación de referencia (oráculo) y desarrollaron con un enfoque guiado por pruebas: encontraban SQL tratado de forma distinta, corregían el nuevo parser y luego volvían a comparar

Generar discrepancias con pruebas basadas en propiedades

  • Primero hicieron pasar las pruebas de regresión acumuladas durante el desarrollo del parser anterior, y después generaron sistemáticamente nuevas discrepancias
  • En las pruebas basadas en propiedades (PBT) con Hypothesis, definieron la propiedad a verificar como “el nuevo parser coincide con la implementación de referencia”
    • El valor de entrada es una consulta SQL
    • Hypothesis busca consultas en las que los resultados de ambos parsers no coinciden
  • Para generar SQL interesante, construyeron junto con Claude una herramienta que genera código para producir SQL a partir del archivo de gramática de ANTLR
    • También escribieron un parser aparte que lee el propio archivo .g4
    • Después incluyeron en la fase de generación mutaciones como intercambio de tokens o adición de paréntesis

Cómo estructuraron los prompts para evitar arreglos frágiles

  • Las PBT producían nuevas pruebas de forma confiable, pero Claude repetía arreglos frágiles, como corregir un caso con lookahead de un token y descubrir después que en realidad hacía falta lookahead de dos tokens
  • La ventana de contexto se llenaba y se comprimía con frecuencia, así que era posible que corrigiera cosas habiendo olvidado la gramática real o el comportamiento del parser de referencia
  • Mitigaron ese problema indicándole que, justo antes de escribir el código para corregir cada discrepancia, volviera a cargar como contexto tanto el archivo de gramática relevante como el código fuente en C++

Mantener corriendo al mismo tiempo la CPU y Claude

  • Las PBT seguían ejecutándose en segundo plano, registrando los casos fallidos en archivos, y configuraron las herramientas para que Claude tomara esos casos cuando no tuviera otra tarea
  • Los casos fallidos se recopilaron desde varias rutas
    • Pruebas de regresión existentes
    • SQL generado por PBT
    • Consultas anonimizadas traídas de logs de producción
    • Casos creados al pedirle a un agente en segundo plano que “pensara muy a fondo en casos límite”
  • Los dos parsers desarrollados en paralelo compartían un conjunto de pruebas de regresión, así que cualquier fallo descubierto en una sesión se reflejaba de inmediato en la otra
  • Hypothesis puede reducir los casos que genera directamente hasta una reproducción mínima, pero eso no aplica al SQL externo, así que para esos casos usaron ShrinkRay
  • Más adelante también añadieron generación de pruebas guiada por cobertura de código
    • Detectaban componentes de SQL que todavía no se habían ejecutado y sesgaban la generación para producir esas estructuras con mayor frecuencia
    • No fue indispensable para llegar al 100% de precisión en el conjunto de consultas de producción, pero sí ayudó a encontrar casos muy sutiles

El ciclo automático para encontrar y corregir fallos

  • El loop final de desarrollo repetía de forma autónoma los siguientes pasos
    1. Generar nuevos fallos con PBT, conjunto de consultas reales, pruebas de regresión y exploración de casos límite
    2. Reducir esos fallos y agregarlos a una lista de pruebas de regresión en expansión constante
    3. Priorizar soluciones generales mientras se verifica cómo los tratan la gramática y la implementación de referencia en C++
    4. Aplicar la corrección y luego imprimir un resumen legible por humanos en un párrafo
    5. Ejecutar toda la batería de pruebas de regresión para confirmar que todos los casos pasan
    6. Volver a ejecutar el mismo proceso de forma autónoma
  • Como el nuevo parser era mucho más rápido, pudieron ejecutarlo en producción en modo shadow junto con el parser anterior en C++ e informar cualquier diferencia de resultados

Validación en producción y cambio de tráfico

  • En la comparación previa usando logs de consultas de producción, probaron unas 50 mil consultas
  • En modo shadow ejecutaron rápidamente millones de parseos y no apareció ni una sola discrepancia frente al parser anterior
  • Al principio planeaban mantener el modo shadow durante varios días, pero como los resultados fueron suficientemente sólidos, cambiaron el tráfico de producción al nuevo parser en apenas unas horas
  • Después del cambio mantuvieron un shadow inverso del 0.1% comparando otra vez con el parser anterior
  • El nuevo parser produce no solo el AST, sino también la información de posición en el código fuente, exactamente igual que el parser ANTLR en C++

70 veces en laptop, 454 veces en producción

  • En benchmarks de laptop registró un rendimiento de alrededor de 70 veces más rápido que el parser anterior
  • En consultas de producción fue en promedio 454 veces más rápido
    • En producción se procesan sobre todo SQL más largos que no aciertan en la caché del parser, por lo que la diferencia de rendimiento fue mayor que en la laptop
  • El resultado final consta de unas 16K líneas de código del parser, unas 5K líneas de herramientas y miles de líneas de pruebas
  • En consultas realistas es equivalente al parser anterior, y las diferencias se limitan a una cantidad muy pequeña de consultas artificialmente construidas

La estructura final del parser y el cambio en la forma de desarrollarlo

  • El parser final tiene la siguiente estructura
    • La mayor parte está implementada como un parser predictivo de descenso recursivo
    • Para procesar expresiones usa un núcleo de parser Pratt
    • El cursor base es LL(2), y solo en puntos específicos amplía el alcance con exploración limitada de lookahead sin consumir la entrada
    • Solo en unas pocas decisiones estrictamente necesarias aplica backtracking especulativo local basado en alternativas ordenadas
  • Todo el parser fue escrito en Rust por Claude Opus 4.7 en mayo de 2026, y no escribieron el código manualmente
  • La configuración de PBT que genera entradas desde la gramática y guía la generación con cobertura de código se parece mucho al estado del arte en fuzzing de parsers, y se diferencia de un simple “vibe coding”
  • Un trabajo que incluso para desarrolladores con conocimiento específico de parsers podría tomar meses se completó en pocos días
  • En adelante, podría volverse común que generadores de parsers como ANTLR proporcionen una implementación de referencia, mientras un LLM escribe un parser implementado directamente y más rápido, igualando la equivalencia con PBT y fuzzing

Aún no hay comentarios.

Aún no hay comentarios.