Algunos errores bloquean funcionalidades enteras. Se sitúan en un cuello de botella en la arquitectura del sistema, y hasta que se resuelven, todo lo que depende de ellos permanece inutilizable. El error de manejo de None era uno de estos. Era pequeño -- diez líneas de código para corregirlo -- pero se interponía entre nosotros y todo el modelo temporal.
El 7 de enero de 2026, teníamos 27 pruebas de integración temporal. Las 27 fallaban. El operador de acceso temporal (@) estaba implementado. El almacenamiento de historial de versiones funcionaba. La base de datos mantenía correctamente las versiones de las entidades. Pero en el momento en que intentabas usar cualquiera de esto en código real, la VM lanzaba un TypeError y se detenía.
El problema
El modelo temporal de FLIN te permite acceder a versiones anteriores de una entidad usando el operador @:
flinuser = User { name: "Alice" }
save user
user.name = "Bob"
save user
previous = user @ -1 // Obtener la versión anteriorEl operador @ con un desplazamiento negativo recupera versiones anteriores. user @ -1 significa "la versión anterior a la actual". Cuando solo existe una versión, user @ -1 correctamente devuelve None -- no hay versión anterior.
El error se manifestaba cuando intentabas acceder a una propiedad de ese valor None:
flinprevious = user @ -1 // Devuelve None (solo existe 1 versión)
<div>{previous.name}</div> // TypeError: expected object, found noneEste es el problema de la referencia nula que ha plagado los lenguajes de programación desde el "error de mil millones de dólares" de Tony Hoare. En el caso de FLIN, el opcode GetField de la VM asumía que siempre recibiría un objeto. Cuando recibía None, lanzaba un error de tipo.
Por qué esto bloqueaba todo
El modelo temporal trata fundamentalmente de consultar versiones que pueden o no existir. Cada consulta temporal puede devolver None:
user @ -1devuelveNonesi no hay versión anterioruser @ yesterdaydevuelveNonesi la entidad no existía ayeruser @ -10devuelveNonesi hay menos de 10 versiones
Si el acceso a propiedades en None lanza un error, entonces cada consulta temporal requiere una verificación explícita de nulo antes de acceder a cualquier campo:
flin// Sin propagación de None: verboso y repetitivo
previous = user @ -1
{if previous}
<div>{previous.name}</div>
{/if}
// Para múltiples campos: exponencialmente peor
previous = user @ -1
{if previous}
<div>
Name: {previous.name}
Email: {previous.email}
Role: {previous.role}
</div>
{/if}Este patrón necesitaría envolver cada acceso temporal en cada plantilla. Para una funcionalidad diseñada para hacer elegantes las consultas de viaje en el tiempo, requerir verificaciones de nulo repetitivas en todas partes anularía completamente su propósito.
La corrección
La corrección fue notablemente simple. Modificamos dos opcodes en la VM: GetField y GetFieldDyn.
rust// OpCode::GetField (src/vm/vm.rs:1484-1494)
if let Value::Object(id) = obj {
// Ruta normal: acceder al campo del objeto
self.push(value)?;
} else if obj == Value::None {
// NUEVO: Propagar None en lugar de lanzar error
self.push(Value::None)?;
} else {
return Err(RuntimeError::TypeError {
expected: "object or none",
found: obj.type_name().to_string(),
});
}El mismo patrón fue aplicado a GetFieldDyn para el acceso dinámico a propiedades. Cuando la VM encuentra un acceso a campo en None, simplemente coloca None en la pila en lugar de lanzar un error.
Después de la corrección:
flinuser = User { name: "Alice" }
save user
previous = user @ -1 // Devuelve None
<div>{previous.name}</div> // Devuelve None (renderizado como cadena vacía)Sin error. Sin fallo. La plantilla se renderiza, y el valor None se muestra como nada -- que es exactamente el comportamiento correcto cuando no hay versión anterior.
La decisión de diseño
Consideramos dos enfoques para el manejo de None.
Opción A: Propagación implícita de None -- El enfoque que elegimos. None.property devuelve None, permitiendo que los valores fluyan a través de cadenas de acceso a propiedades sin interrupción. Esto refleja el encadenamiento opcional de JavaScript (null?.property), la navegación segura de TypeScript y el Option::and_then() de Rust.
Opción B: Verificaciones explícitas -- Requerir que los desarrolladores protejan cada acceso con un bloque if. Esto es más explícito pero dramáticamente más verboso, especialmente en plantillas donde las consultas temporales son más comunes.
flin// Opción A: Limpio e intuitivo
<div>
Current: {user.name}
Previous: {(user @ -1).name}
</div>
// Opción B: Verboso y repetitivo
<div>
Current: {user.name}
{if user @ -1}
Previous: {(user @ -1).name}
{/if}
</div>Elegimos la Opción A por varias razones. Primero, FLIN está diseñado para el desarrollo rápido, y requerir verificaciones de nulo para cada acceso temporal contradice ese objetivo. Segundo, las consultas temporales naturalmente producen None en muchos escenarios válidos -- no es un caso excepcional sino uno normal. Tercero, el comportamiento coincide con lo que los desarrolladores esperan de los lenguajes modernos.
La contrapartida es que la propagación de None puede enmascarar errores. Si un desarrollador accede a user.nmae (un error tipográfico) y obtiene None en lugar de un error, el error tipográfico podría pasar desapercibido. Aceptamos esta contrapartida porque el verificador de tipos de FLIN detecta errores de nombres de campos en tiempo de compilación. La propagación de None solo afecta a valores en tiempo de ejecución que son legítimamente None, no a errores en nombres de campos.
Probando la corrección
Verificamos la corrección con un archivo de prueba dedicado:
flinentity User { name: text }
user = User { name: "Alice" }
save user
previous = user @ -1 // Devuelve None
result = previous.name // Devuelve None (¡sin error!)
print(user.name) // Alice
print(previous) // none
print(result) // noneLas 1.005 pruebas de biblioteca continuaron pasando. Sin regresiones. La corrección fue puramente aditiva -- manejó un caso que anteriormente lanzaba un error, sin cambiar el comportamiento de ningún caso que anteriormente tenía éxito.
El patrón más amplio: propagación de None
La corrección que implementamos es el primer paso hacia un patrón más amplio de propagación de None en FLIN:
None.property -> None (implementado)
None + 5 -> None (mejora futura)
len(None) -> None (mejora futura)
None.method() -> None (mejora futura)Cada una de estas extensiones sigue el mismo principio: cuando None fluye hacia una operación, la operación produce None en lugar de un error. Esto crea un "canal de None" a través del cual los valores ausentes pueden propagarse a través de cadenas arbitrarias de computación sin interrumpir la ejecución.
El patrón es particularmente poderoso para consultas temporales que encadenan múltiples accesos:
flin// Cadena profunda de propiedades con acceso temporal
old_address = (user @ -3).profile.address.city
// Si user @ -3 es None, toda la cadena devuelve None
// Sin error en ningún pasoSin la propagación de None, esto requeriría verificaciones de nulo anidadas cuatro niveles de profundidad. Con ella, el desarrollador escribe exactamente lo que quiere decir y obtiene None si algún eslabón de la cadena está ausente.
Impacto en el modelo temporal
Antes de esta sesión, el modelo temporal estaba teóricamente completo pero prácticamente inutilizable. El almacenamiento de historial de versiones funcionaba. El operador @ funcionaba. La base de datos mantenía correctamente las versiones. Pero en el momento en que intentabas mostrar datos temporales en una plantilla -- el caso de uso principal -- la VM se bloqueaba.
Después de esta sesión, las consultas temporales básicas funcionaban de extremo a extremo:
flinentity User { name: text }
user = User { name: "Alice" }
save user // versión 1
user.name = "Alice Smith"
save user // versión 2
previous = user @ -1 // { name: "Alice" }
original = user @ -2 // None (no hay versión 0)
current = user @ 0 // { name: "Alice Smith" }
<div>
Current: {user.name}
Previous: {previous.name}
</div>La corrección de 10 líneas desbloqueó 27 pruebas de integración, de las cuales 4 comenzaron a pasar inmediatamente. Las 23 restantes estaban bloqueadas por un problema separado de renderizado de espacios en blanco HTML (abordado en el siguiente artículo), no por errores de lógica temporal.
El poder de las correcciones pequeñas y estratégicas
Esta sesión ejemplifica un principio que hemos observado repetidamente durante el desarrollo de FLIN: las correcciones más impactantes son a menudo las más pequeñas. El error de manejo de None bloqueaba un subsistema completo -- acceso temporal, historial de versiones, detección de cambios -- y la corrección fue agregar una única rama condicional a dos opcodes.
La clave fue identificar que esta pequeña corrección se situaba en un cuello de botella arquitectónico crítico. El opcode GetField es una de las instrucciones ejecutadas con más frecuencia en la VM. Cada acceso a propiedad, cada enlace de plantilla, cada lectura de campo pasa por él. Un pequeño cambio allí se irradia hacia el exterior para afectar cada parte del sistema.
Diez líneas de código. Una hora de trabajo. Todo un subsistema de funcionalidades desbloqueado. A veces lo más valioso que un desarrollador puede hacer no es construir algo nuevo, sino remover un único obstáculo que impedía que todo lo demás funcionara.
Esta es la Parte 158 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: - [157] El error de iteración del bucle for - [158] El error de manejo de None (estás aquí) - [159] El error de renderizado de espacios en blanco HTML