Back to flin
flin

Lo que la auditoría nos enseñó sobre construir un lenguaje

Las lecciones arquitectónicas y de proceso de auditar FLIN -- lo que funcionó, lo que no, y lo que haríamos diferente al construir un lenguaje desde cero.

Juste A. Gnimavo (Thales) & Claude | March 26, 2026 7 min flin
EN/ FR/ ES
flinrust

Auditar 186.252 líneas de código no es solo un ejercicio de búsqueda de defectos. Es un espejo sostenido frente a cada decisión arquitectónica, cada elección de proceso y cada compensación hecha a lo largo de 301 sesiones de desarrollo. Los defectos en sí mismos son síntomas. Las lecciones están en los patrones -- qué categorías de defectos recurren, qué subsistemas acumulan deuda técnica más rápido, y dónde el proceso de desarrollo crea puntos ciegos sistemáticos.

La auditoría de FLIN nos enseñó siete lecciones sobre la construcción de un lenguaje de programación. Algunas confirmaron lo que ya sospechábamos. Otras nos sorprendieron. Todas darán forma a cómo construimos software de ahora en adelante.

Lección 1: Las tablas de despacho duales son un defecto de diseño

El hallazgo más crítico de la auditoría -- el opcode CreateMap duplicado -- no fue un error individual. Fue una vulnerabilidad estructural. La VM de FLIN tiene dos funciones de ejecución (run() y execute_until_return()) que ambas necesitan manejar los mismos opcodes. Esta arquitectura de doble despacho es la causa raíz de toda una categoría de defectos.

Cuando la Sesión 273 auditó execute_until_return() para cobertura de opcodes, encontró que solo 59 de más de 170 opcodes estaban manejados. Eso es una tasa de cobertura del 35%. Cada opcode faltante era un fallo silencioso -- una sentencia continue que saltaba la operación como si nunca hubiera sido emitida por el compilador.

rust// The architectural problem: two loops that must stay synchronized
// but have no mechanism to enforce it

// Solution 1: shared dispatch function
fn dispatch_opcode(
    &mut self,
    opcode: OpCode,
    code: &[u8],
) -> Result<DispatchResult, VmError> {
    match opcode {
        OpCode::CreateMap => { /* single implementation */ }
        OpCode::Add => { /* single implementation */ }
        // ... all opcodes in one place
    }
}

// Solution 2: macro-generated match arms
macro_rules! opcode_dispatch {
    ($self:expr, $opcode:expr, $code:expr) => {
        match $opcode {
            OpCode::CreateMap => $self.handle_create_map($code)?,
            OpCode::Add => $self.handle_add($code)?,
            // ... generated from a single definition
        }
    };
}

La lección no es específica de FLIN. Cualquier sistema con tablas de despacho paralelas -- manejadores de eventos en dos ubicaciones, parsers de protocolo con múltiples puntos de entrada, procesadores de comandos con diferentes modos de ejecución -- es vulnerable a la misma divergencia. La corrección es siempre la misma: compartir una única implementación, ya sea a través de una función común, una macro, o generación de código.

Lección 2: Los fallos silenciosos son los errores más costosos

Tres de los cinco hallazgos más graves de la auditoría involucraron fallos silenciosos:

  • CreateMap descartando silenciosamente claves para entradas Value::Text
  • Entity.where() devolviendo silenciosamente todas las entidades en lugar de resultados filtrados
  • Validadores rechazando silenciosamente los guardados sin error ni advertencia

Cada uno de estos errores fue costoso no por el daño que causó, sino por el tiempo invertido en diagnosticarlos. Cuando las traducciones de un desarrollador FLIN no funcionan, no sospechan de la capa de opcodes. Cuando una consulta de entidad devuelve demasiados resultados, culpan a su sintaxis de filtro. Cuando un guardado parece tener éxito pero los datos desaparecen al refrescar, cuestionan la base de datos.

rust// The correct pattern: fail loudly
OpCode::QueryWhere => {
    let predicate = self.pop()?;
    let entity_type = self.pop()?;

    match self.apply_predicate(&entity_type, &predicate) {
        Ok(filtered) => self.push(Value::List(filtered))?,
        Err(e) => {
            // NEVER silently fall back to returning all entities
            return Err(VmError::QueryError {
                entity: entity_type,
                predicate: format!("{:?}", predicate),
                cause: e.to_string(),
            });
        }
    }
}

El principio que adoptamos después de la auditoría: cada operación que puede fallar debe o tener éxito con el resultado correcto o fallar con un mensaje de error que apunte a la causa. No hay un término medio aceptable donde una operación "tenga éxito" con datos incorrectos.

Lección 3: La representación de valores debe ser transparente para las operaciones

FLIN tiene dos representaciones para cadenas: Value::Text(String) para cadenas cortas en línea y Value::Object(ObjectId) apuntando a ObjectData::String asignado en el heap. Esta optimización reduce la presión de asignación para cadenas pequeñas comunes. Pero creó un contrato que cada operación de cadenas debe honrar: ambas representaciones deben producir un comportamiento idéntico.

La lección más amplia: cuando un sistema tiene múltiples representaciones para el mismo concepto semántico, debe haber una única función que las normalice. Cada consumidor debería llamar al normalizador en lugar de hacer pattern-matching directamente sobre las representaciones.

rust// The normalizer pattern
fn as_string(&self, value: &Value) -> Result<Cow<str>, VmError> {
    match value {
        Value::Text(s) => Ok(Cow::Borrowed(s)),
        Value::Object(id) => {
            let s = self.get_string(*id)?;
            Ok(Cow::Borrowed(s))
        }
        _ => Err(VmError::TypeError {
            expected: "text",
            got: value.type_name(),
        })
    }
}

Lección 4: El desarrollo basado en sesiones crea brechas de accesibilidad

La auditoría de funciones reveló que el 95% de las funciones integradas estaban implementadas en bytecode pero solo el 12% eran accesibles desde plantillas. Esta brecha surgió porque cada sesión se enfocaba en hacer que su característica específica funcionara de extremo a extremo, lo que significaba implementar la función en bytecode y probarla en contexto de bytecode. La exposición en plantillas era un paso separado que se postergaba rutinariamente.

Lección 5: Las optimizaciones del compilador deben ser invisibles

El error de CreateMap solo se manifestó cuando el compilador eligió emitir una cadena como Value::Text en lugar de Value::Object. Esta elección era una optimización. Pero la optimización no era invisible para el resto del sistema.

El principio: las optimizaciones del compilador deben ser transparentes para el runtime. Si el compilador elige representar un valor de manera diferente por razones de rendimiento, cada parte del runtime que toque ese valor debe manejar todas las representaciones posibles.

Lección 6: Una auditoría es una transferencia de conocimiento

Antes de la auditoría, el código base de FLIN existía en un estado distribuido -- parcialmente en logs de sesión, parcialmente en los datos de entrenamiento de Claude, parcialmente en el propio código, pero no en la comprensión completa de ninguna entidad individual. La auditoría cambió eso.

Lección 7: Cero problemas de seguridad no es un accidente

La auditoría encontró cero vulnerabilidades de seguridad en 186.252 líneas. Esto no fue suerte. Fue una consecuencia de decisiones arquitectónicas tomadas en las primeras sesiones:

  • FLIN no usa SQL, por lo que la inyección SQL es estructuralmente imposible.
  • El motor de plantillas de FLIN escapa la salida por defecto, por lo que XSS requiere una aceptación explícita.
  • Las operaciones de archivo de FLIN validan rutas contra directorios permitidos.
  • Rust elimina desbordamientos de búfer y uso después de liberar.

La lección es que la seguridad no es principalmente una cuestión de codificación cuidadosa. Es una cuestión de elegir arquitecturas que eliminen categorías de vulnerabilidad por completo.

Mirando hacia adelante

La auditoría no fue el final. Fue la transición de construir a endurecer. Las 301 sesiones que construyeron FLIN fueron un acto de creación -- desordenado, rápido, iterativo, ocasionalmente brillante, ocasionalmente defectuoso. La auditoría y sus sesiones de corrección fueron un acto de disciplina -- sistemático, exhaustivo, poco romántico, esencial.

FLIN emergió de la auditoría como un sistema más fuerte. No porque no tuviera errores -- aún tenía espacio para mejoras en pruebas de fuzzing, verificación de concurrencia y pruebas basadas en propiedades. Pero porque cada error que tenía ahora era conocido, documentado, y corregido o rastreado. Las incógnitas desconocidas habían sido convertidas en cantidades conocidas. Y las cantidades conocidas pueden ser gestionadas.


Esta es la Parte 153 de la serie "Cómo construimos FLIN", que documenta cómo un CEO en Abidjan y un CTO de IA diseñaron y construyeron un lenguaje de programación desde cero.

Navegación de la serie: - [152] 3.452 pruebas, cero fallos - [153] Lo que la auditoría nos enseñó sobre construir un lenguaje (estás aquí) - [154] Llamadas panic en producción: rastreo y eliminación

Share this article:

Responses

Write a response
0/2000
Loading responses...

Related Articles

Thales & Claude deblo

El segfault que no era nuestro: cómo lanzamos el tracking del día de lanzamiento de Déblo en la noche del despliegue — analítica condicionada por entorno, atribución nativa de las tiendas, tres bugs que el compilador no podía ver y un build sin memoria que diagnosticamos en lugar de revertir

El 1 de julio de 2026 — el día del lanzamiento — el riesgo nunca fue el texto. Era que las campañas de pago salieran a ciegas. Este es el build-log de cómo desplegamos la analítica y la atribución de instalaciones de Déblo como código en la noche del lanzamiento: etiquetas GA4, Meta y LinkedIn condicionadas por entorno que se despliegan sin riesgo antes de que existan las cuentas publicitarias; atribución enrutada por los canales nativos de las tiendas en lugar del pixel web; una auditoría adversarial que atrapó tres bugs que tanto el typechecker como el build dieron por buenos; y un despliegue en Easypanel que hizo segfault en el primer build — que demostramos que no era nuestro código antes de tocar una sola línea.

18 min Jul 1, 2026
deblolaunch-dayclaude-opus-4.8claude-code +26
Thales & Claude thales

Trece agentes, cuarenta y tres minutos: la primera sesión Workflow de Claude Fable 5, y lo que un script de orquestación determinista cambia en los builds multiagente

Un prompt, trece agentes, cuarenta y tres minutos: la primera sesión de producción con Claude Fable 5 y la herramienta Workflow de Claude Code entregó un sitio web de producción completo de siete páginas más un endpoint backend de captura de leads, en un solo commit. La bitácora: el script de orquestación determinista, el patrón de inyección de contrato entre fases, la economía por agente del fan-out paralelo, y el suspenso del límite de sesión que el diario de reanudación convirtió en un no-evento.

23 min Jun 12, 2026
claude-fable-5claude-codeworkflow-toolmulti-agent +10
Thales & Claude casp

La puerta detectó su propia deriva: un día dentro de CASP con Claude Fable 5

Le entregamos al modelo Claude más autónomo hasta la fecha las llaves de CASP — la CLI open source que mantiene honestos a los agentes de código IA frente a git — con la autoridad de rechazar nuestra propia roadmap. Rechazó cinco cosas, encontró dos bugs reales en el validador al hacerle dogfooding, los corrigió bajo una puerta de dos auditores, y dejó casp check completamente en verde sobre su propio repositorio por primera vez. CASP 0.3.0 es el resultado.

15 min Jun 10, 2026
caspzerosuiteworkflowai-cto +9