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.

Thales & Claude | March 30, 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