Cada aplicacion lidia con el tiempo. Plazos. Fechas de vencimiento. Duraciones de cache. Periodos de suscripcion. Y cada aplicacion reinventa la rueda: importando una biblioteca de fechas, parseando cadenas de formato, manejando casos extremos de zonas horarias, convirtiendo entre unidades.
La aritmetica temporal de FLIN, implementada en la sesion 078, toma un enfoque diferente. Los literales de duracion son sintaxis de primera clase. Las operaciones temporales se verifican en tipos en tiempo de compilacion. Y gracias al plegado de constantes, los literales de duracion se compilan a enteros simples sin ninguna sobrecarga en tiempo de ejecucion.
El resultado es que deadline = now + 7.days no es una llamada a una biblioteca -- es una expresion del lenguaje que el compilador verifica y optimiza.
La sintaxis: duraciones como acceso a miembros
La sintaxis de duracion de FLIN aprovecha el acceso a miembros. Un numero seguido de una unidad de tiempo crea una duracion:
flintimeout = 30.seconds
cache_ttl = 5.minutes
sprint = 14.days
subscription = 1.months
annual = 1.yearsSe soportan siete unidades:
| Unidad | Milisegundos |
|---|---|
.seconds | 1,000 |
.minutes | 60,000 |
.hours | 3,600,000 |
.days | 86,400,000 |
.weeks | 604,800,000 |
.months | 2,592,000,000 (30 dias) |
.years | 31,536,000,000 (365 dias) |
Los valores de punto flotante tambien funcionan:
flinhalf_hour = 0.5.hours // 1,800,000 ms
ninety_mins = 1.5.hours // 5,400,000 msLa sintaxis fue elegida deliberadamente. Podriamos haber usado llamadas a funciones (days(7)) o sobrecarga de operadores (7 * DAY). Pero 7.days se lee como ingles, no requiere importaciones y se ajusta a la filosofia de FLIN de expresion natural. Tampoco requirio cambios en el lexer -- el parser lo reconoce como acceso a miembro en un literal numerico y lo convierte en una expresion de duracion.
Aritmetica temporal: operaciones con seguridad de tipos
Las duraciones se componen con valores de tiempo a traves de operadores aritmeticos. El verificador de tipos impone que solo se permitan combinaciones validas:
flin// Time + Duration = Time
deadline = now + 7.days
reminder = event_time - 1.hours
// Time - Time = Duration
time_remaining = deadline - now
// Duration + Duration = Duration
total = 2.hours + 30.minutes
// Duration - Duration = Duration
remaining = 1.hours - 15.minutes
// Duration * Number = Duration
double_timeout = 30.seconds * 2
// Duration / Number = Duration
half_period = 1.hours / 2Las reglas de tipo son explicitas y exhaustivas:
Time + Duration --> Time
Time - Duration --> Time
Time - Time --> Duration
Duration + Duration --> Duration
Duration - Duration --> Duration
Duration * Int/Float --> Duration
Duration / Int/Float --> DurationCualquier otra combinacion es un error de tiempo de compilacion:
flin// Compile error: cannot add Time + Time
bad = now + now
// Compile error: cannot add Duration + Int
also_bad = 7.days + 42
// Compile error: cannot multiply Time * Duration
very_bad = now * 7.daysEstos errores son detectados por el verificador de tipos antes de que se genere cualquier codigo. El desarrollador obtiene un mensaje de error claro en tiempo de compilacion, no una excepcion criptica en tiempo de ejecucion.
Implementacion: siete unidades, cero opcodes nuevos
La elegancia de la implementacion radica en lo que no construimos. La aritmetica de duraciones no requiere nuevos opcodes de VM. Las duraciones se representan como enteros simples (milisegundos i64) en tiempo de ejecucion, y la aritmetica usa los opcodes existentes Add, Sub, Mul y Div.
La magia ocurre en dos capas: el sistema de tipos (que impone combinaciones validas) y el generador de codigo (que convierte literales de duracion en constantes enteras).
Extension del AST
Se agrego un nuevo enum DurationUnit y una variante Expr::Duration al AST:
rustpub enum DurationUnit {
Seconds,
Minutes,
Hours,
Days,
Weeks,
Months,
Years,
}
// In the Expr enum
Expr::Duration {
value: Box<Expr>,
unit: DurationUnit,
span: Span,
}Integracion con el parser
El parser reconoce unidades de duracion dentro del acceso a miembros. Al parsear 30.seconds, primero parsea 30 como un literal numerico, luego encuentra .seconds. En lugar de tratar esto como acceso a campo en un entero (lo que seria un error de tipo), reconoce seconds como una unidad de duracion y construye un nodo Expr::Duration.
rust// In member access parsing
if let Some(unit) = match name.as_str() {
"seconds" => Some(DurationUnit::Seconds),
"minutes" => Some(DurationUnit::Minutes),
"hours" => Some(DurationUnit::Hours),
"days" => Some(DurationUnit::Days),
"weeks" => Some(DurationUnit::Weeks),
"months" => Some(DurationUnit::Months),
"years" => Some(DurationUnit::Years),
_ => None,
} {
return Ok(Expr::Duration { value: Box::new(expr), unit, span });
}Nueve lineas de codigo del parser. Sin cambios en el lexer. La sintaxis de duracion se maneja completamente dentro de la logica de parseo de acceso a miembros existente.
Sistema de tipos
Se agrego una nueva variante FlinType::Duration. El verificador de tipos valida que el valor dentro de una expresion de duracion sea numerico (Int o Float) y que las operaciones aritmeticas que involucren Duration sigan las reglas anteriores.
rust// Duration literal type checking
Expr::Duration { value, unit, .. } => {
let value_ty = self.check_expr(value)?;
match value_ty {
FlinType::Int | FlinType::Float => Ok(FlinType::Duration),
other => Err(TypeError::new(
format!("Duration value must be numeric, found {}", other),
span,
)),
}
}La verificacion de tipos de operaciones binarias se extendio con reglas conscientes de duracion:
rust// Time arithmetic rules in the type checker
(FlinType::Time, BinOp::Add, FlinType::Duration) => Ok(FlinType::Time),
(FlinType::Time, BinOp::Sub, FlinType::Duration) => Ok(FlinType::Time),
(FlinType::Time, BinOp::Sub, FlinType::Time) => Ok(FlinType::Duration),
(FlinType::Duration, BinOp::Add, FlinType::Duration) => Ok(FlinType::Duration),
(FlinType::Duration, BinOp::Sub, FlinType::Duration) => Ok(FlinType::Duration),
(FlinType::Duration, BinOp::Mul, FlinType::Int | FlinType::Float) => Ok(FlinType::Duration),
(FlinType::Duration, BinOp::Div, FlinType::Int | FlinType::Float) => Ok(FlinType::Duration),Generacion de codigo: plegado de constantes
El generador de codigo convierte literales de duracion en constantes enteras en tiempo de compilacion. 30.seconds no genera una instruccion de multiplicacion -- genera un solo PushConst(30000).
rustfn emit_duration(&mut self, value: &Expr, unit: &DurationUnit) -> EmitResult {
let multiplier: i64 = match unit {
DurationUnit::Seconds => 1_000,
DurationUnit::Minutes => 60_000,
DurationUnit::Hours => 3_600_000,
DurationUnit::Days => 86_400_000,
DurationUnit::Weeks => 604_800_000,
DurationUnit::Months => 2_592_000_000,
DurationUnit::Years => 31_536_000_000,
};
// Constant folding for literal values
if let Expr::IntLiteral(n, _) = value {
let ms = n * multiplier;
self.emit_push_const(Value::Int(ms));
return Ok(());
}
if let Expr::FloatLiteral(f, _) = value {
let ms = (f * multiplier as f64) as i64;
self.emit_push_const(Value::Int(ms));
return Ok(());
}
// Non-constant: emit value, then multiply
self.emit_expr(value)?;
self.emit_push_const(Value::Int(multiplier));
self.emit_opcode(OpCode::Mul);
Ok(())
}Para valores literales (el caso comun), la multiplicacion ocurre en tiempo de compilacion y el resultado se incrusta directamente en el bytecode. Para valores calculados (como n.days donde n es una variable), el emisor genera una multiplicacion en tiempo de ejecucion contra la constante en milisegundos de la unidad.
Esta es una abstraccion de costo cero en el sentido mas verdadero: 30.seconds y 30000 generan bytecode identico.
Por que milisegundos?
Elegimos milisegundos como la representacion interna por varias razones:
Precision. Los milisegundos son lo suficientemente precisos para temporizacion a nivel de aplicacion (plazos sub-segundo, duraciones de animacion) sin la complejidad de nanosegundos o microsegundos.
Compatibilidad. El Date.now() de JavaScript devuelve milisegundos. La mayoria de las APIs web usan milisegundos. Las aplicaciones FLIN que interactuan con APIs web pueden pasar marcas de tiempo directamente sin conversion.
Aritmetica de enteros. Al usar milisegundos i64, todas las operaciones temporales son aritmetica de enteros -- rapida, determinista y libre de problemas de precision de punto flotante. La duracion maxima representable es aproximadamente 292 millones de anos, lo que deberia ser suficiente para la mayoria de las aplicaciones.
Simplicidad. Una representacion, una unidad, sin tablas de conversion. Una duracion es un numero. La aritmetica temporal es adicion de enteros. La VM no necesita un "tipo de duracion" especial en tiempo de ejecucion.
Interaccion con palabras clave temporales
La aritmetica temporal se compone naturalmente con las palabras clave temporales de FLIN:
flin// Deadline is 7 days from now
deadline = now + 7.days
// Reminder is 1 hour before the deadline
reminder_time = deadline - 1.hours
// How much time is left?
time_remaining = deadline - now
// Entity query: users created in the last 90 days
recent_users = User.where(created_at > now - 90.days)
// Cache control
cache_until = now + 5.minutes
is_cached = now < cache_untilLas palabras clave temporales (now, today, yesterday, etc.) devuelven valores de tiempo. Los literales de duracion devuelven valores de duracion. El verificador de tipos asegura que solo se usen combinaciones validas. El resultado es un sistema completo de manipulacion temporal que se lee como ingles y se compila a operaciones de enteros.
Decisiones de diseno
Por que N.days en lugar de days(N)?
La sintaxis de llamada a funcion (days(7)) habria requerido registrar siete funciones integradas. La sintaxis de acceso a miembro (7.days) requirio nueve lineas de codigo del parser y cero funciones nuevas. La sintaxis tambien se lee mas naturalmente -- "siete dias" versus "dias de siete."
Por que meses y anos aproximados?
Los meses son treinta dias. Los anos son trescientos sesenta y cinco dias. Estas son aproximaciones -- los meses reales varian de veintiocho a treinta y un dias, y los anos bisiestos tienen trescientos sesenta y seis. Elegimos valores aproximados porque:
- La aritmetica de calendario exacta requiere conocer la fecha de inicio, el sistema de calendario y las reglas de zona horaria. Esta complejidad pertenece a una biblioteca de fechas, no a una primitiva del lenguaje.
- Para los casos de uso que FLIN apunta (duraciones de cache, periodos de suscripcion, politicas de retencion), las duraciones aproximadas son suficientes. "Retener datos por noventa dias" no necesita tener en cuenta la longitud de febrero.
- Los desarrolladores que necesitan aritmetica de calendario exacta pueden usar funciones explicitas de manipulacion de fechas (planificadas para futuras versiones de FLIN).
Por que seguridad de tipos en lugar de verificaciones en tiempo de ejecucion?
Podriamos haber representado las duraciones como enteros a nivel de tipo (igual que lo son en tiempo de ejecucion) y dejar que los desarrolladores sumen Time + Int libremente. La seguridad de tipos se perderia, pero la implementacion seria mas simple.
Elegimos seguridad de tipos porque la aritmetica temporal incorrecta es una categoria comun de errores. Sumar un entero en bruto a una marca de tiempo produce un resultado en la unidad equivocada (segundos versus milisegundos, o peor). El sistema de tipos detecta estos errores en tiempo de compilacion, antes de que causen errores de medianoche-desplegada-como-mediodia en produccion.
Patrones comunes habilitados por la aritmetica temporal
La aritmetica temporal desbloquea una categoria de logica de aplicacion que antes era dificil de expresar:
Gestion de suscripciones:
flinentity Subscription {
user: User
plan: text
started_at: time
expires_at: time
}
sub = Subscription {
user: current_user,
plan: "monthly",
started_at: now,
expires_at: now + 30.days
}
save sub
is_expired = now > sub.expires_at
days_left = (sub.expires_at - now) / 1.daysTiempos de espera de sesion:
flinsession_timeout = 30.minutes
last_activity = user.updated_at
is_timed_out = now > last_activity + session_timeoutTareas programadas:
flinentity Task {
name: text
due_at: time
remind_at: time
}
task = Task {
name: "Submit report",
due_at: now + 7.days,
remind_at: now + 6.days // Remind 1 day before
}
save taskCada patron se lee naturalmente. Sin importaciones de bibliotecas de fechas. Sin parseo de cadenas de formato. Sin utilidades de conversion de zonas horarias. Solo aritmetica sobre valores de tiempo.
Pruebas
El archivo de ejemplo time-arithmetic-test.flin ejercita cada combinacion:
flin// Duration literals
timeout = 30.seconds
cache_ttl = 5.minutes
sprint = 14.days
subscription = 1.months
// Floating-point durations
half_hour = 0.5.hours
ninety_mins = 1.5.hours
// Time arithmetic
deadline = now + 7.days
reminder_time = deadline - 1.hours
time_left = deadline - now
// Duration operations
total_time = 2.hours + 30.minutes
remaining = 1.hours - 15.minutes
double_timeout = 30.seconds * 2Todas las expresiones pasan la verificacion de tipos con exito. Todas las mil diez pruebas de biblioteca pasan. La implementacion agrega cero sobrecarga en tiempo de ejecucion para duraciones literales gracias al plegado de constantes.
Impacto en el progreso
La sesion 078 completo las doce tareas de TEMP-5 en una sola sesion, llevando la aritmetica temporal del cero por ciento al cien por ciento:
- Extension del AST (enum DurationUnit, Expr::Duration)
- Integracion con el parser (reconocimiento de acceso a miembros)
- Sistema de tipos (FlinType::Duration)
- Verificacion de tipos (validacion de literales de duracion, reglas de aritmetica temporal)
- Generacion de codigo (emit_duration con plegado de constantes)
- Sin nuevos opcodes de VM necesarios
- Pruebas y validacion
El progreso temporal general paso de setenta y uno a ochenta y tres de ciento sesenta tareas (cincuenta y uno punto nueve por ciento). El modelo temporal de FLIN cruzo la marca de la mitad.
Ciento cincuenta y tres lineas de codigo nuevo. Siete unidades de duracion. Aritmetica temporal con seguridad de tipos. Cero sobrecarga en tiempo de ejecucion. Y deadline = now + 7.days se convirtio en la expresion mas natural del lenguaje.
Esta es la Parte 8 de la serie "Como construimos FLIN" sobre el modelo temporal, documentando el sistema de aritmetica temporal que hace que las operaciones con fechas se sientan como expresiones nativas del lenguaje.
Navegacion de la serie: - [046] Every Entity Remembers Everything: The Temporal Model - [047] Version History and Time Travel Queries - [048] Temporal Integration: From Bugs to 100% Test Coverage - [049] Destroy and Restore: Soft Deletes Done Right - [050] Temporal Filtering and Ordering - [051] Temporal Comparison Helpers - [052] Version Metadata Access - [053] Time Arithmetic: Adding Days, Comparing Dates (usted esta aqui) - [054] Tracking Accuracy and Validation - [055] The Temporal Model Complete: What No Other Language Has