Back to flin
flin

Arithmétique temporelle : ajouter des jours, comparer des dates

Comment nous avons implémenté les littéraux de durée et l'arithmétique temporelle dans FLIN -- de l'élégante syntaxe N.days au constant folding à la compilation, offrant des abstractions à coût zéro pour les opérations de dates.

Thales & Claude | March 30, 2026 12 min flin
EN/ FR/ ES
flintimearithmeticdatesfunctions

Chaque application traite du temps. Échéances. Dates d'expiration. Durées de cache. Périodes d'abonnement. Et chaque application réinvente la roue : importer une bibliothèque de dates, analyser des chaînes de format, gérer les cas limites de fuseaux horaires, convertir entre unités.

L'arithmétique temporelle de FLIN, implémentée dans la session 078, adopte une approche différente. Les littéraux de durée sont une syntaxe de première classe. Les opérations temporelles sont vérifiées au moment de la compilation. Et grâce au constant folding, les littéraux de durée sont compilés en entiers simples sans surcoût à l'exécution.

Le résultat est que deadline = now + 7.days n'est pas un appel de bibliothèque -- c'est une expression du langage que le compilateur vérifie et optimise.

La syntaxe : les durées comme accès membre

La syntaxe de durée de FLIN se greffe sur l'accès membre. Un nombre suivi d'une unité de temps crée une durée :

flintimeout = 30.seconds
cache_ttl = 5.minutes
sprint = 14.days
subscription = 1.months
annual = 1.years

Sept unités sont supportées :

UnitéMillisecondes
.seconds1 000
.minutes60 000
.hours3 600 000
.days86 400 000
.weeks604 800 000
.months2 592 000 000 (30 jours)
.years31 536 000 000 (365 jours)

Les valeurs à virgule flottante fonctionnent aussi :

flinhalf_hour = 0.5.hours      // 1 800 000 ms
ninety_mins = 1.5.hours     // 5 400 000 ms

La syntaxe a été choisie délibérément. Nous aurions pu utiliser des appels de fonction (days(7)) ou la surcharge d'opérateurs (7 * DAY). Mais 7.days se lit comme du français, ne nécessite aucun import, et s'inscrit dans la philosophie d'expression naturelle de FLIN. Cela n'a pas non plus nécessité de modifications du lexer -- le parseur le reconnaît comme un accès membre sur un littéral numérique et le convertit en expression de durée.

Arithmétique temporelle : opérations type-safe

Les durées se composent avec les valeurs temporelles via les opérateurs arithmétiques. Le vérificateur de types s'assure que seules les combinaisons valides sont autorisées :

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 / 2

Les règles de types sont explicites et exhaustives :

Time + Duration   -->  Time
Time - Duration   -->  Time
Time - Time       -->  Duration
Duration + Duration  -->  Duration
Duration - Duration  -->  Duration
Duration * Int/Float -->  Duration
Duration / Int/Float -->  Duration

Toute autre combinaison est une erreur de compilation :

flin// Erreur de compilation : impossible d'additionner Time + Time
bad = now + now

// Erreur de compilation : impossible d'additionner Duration + Int
also_bad = 7.days + 42

// Erreur de compilation : impossible de multiplier Time * Duration
very_bad = now * 7.days

Ces erreurs sont détectées par le vérificateur de types avant la génération de code. Le développeur reçoit un message d'erreur clair à la compilation, pas une exception cryptique à l'exécution.

Implémentation : sept unités, zéro nouvel opcode

L'élégance de l'implémentation réside dans ce que nous n'avons pas construit. L'arithmétique de durée ne nécessite aucun nouvel opcode de VM. Les durées sont représentées comme de simples entiers (i64 en millisecondes) à l'exécution, et l'arithmétique utilise les opcodes existants Add, Sub, Mul et Div.

La magie opère à deux niveaux : le système de types (qui applique les combinaisons valides) et le générateur de code (qui convertit les littéraux de durée en constantes entières).

Extension de l'AST

Un nouvel enum DurationUnit et un variant Expr::Duration ont été ajoutés à l'AST :

rustpub enum DurationUnit {
    Seconds,
    Minutes,
    Hours,
    Days,
    Weeks,
    Months,
    Years,
}

// In the Expr enum
Expr::Duration {
    value: Box<Expr>,
    unit: DurationUnit,
    span: Span,
}

Intégration dans le parseur

Le parseur reconnaît les unités de durée dans l'accès membre. En parsant 30.seconds, il parse d'abord 30 comme un littéral numérique, puis rencontre .seconds. Au lieu de traiter cela comme un accès de champ sur un entier (ce qui serait une erreur de type), il reconnaît seconds comme une unité de durée et construit un noeud 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 });
}

Neuf lignes de code de parseur. Aucune modification du lexer. La syntaxe de durée est gérée entièrement dans la logique existante d'analyse d'accès membre.

Système de types

Un nouveau variant FlinType::Duration a été ajouté. Le vérificateur de types valide que la valeur à l'intérieur d'une expression de durée est numérique (Int ou Float) et que les opérations arithmétiques impliquant Duration suivent les règles ci-dessus.

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 vérification de types des opérations binaires a été étendue avec des règles intégrant les durées :

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),

Génération de code : constant folding

Le générateur de code convertit les littéraux de durée en constantes entières à la compilation. 30.seconds ne génère pas une instruction de multiplication -- il génère un simple 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(())
}

Pour les valeurs littérales (le cas courant), la multiplication se fait à la compilation et le résultat est intégré directement dans le bytecode. Pour les valeurs calculées (comme n.daysn est une variable), l'émetteur génère une multiplication à l'exécution contre la constante en millisecondes de l'unité.

C'est une abstraction à coût zéro dans le sens le plus strict : 30.seconds et 30000 génèrent un bytecode identique.

Pourquoi les millisecondes ?

Nous avons choisi les millisecondes comme représentation interne pour plusieurs raisons :

Précision. Les millisecondes sont suffisamment précises pour le timing au niveau applicatif (échéances infra-seconde, durées d'animation) sans la complexité des nanosecondes ou microsecondes.

Compatibilité. Le Date.now() de JavaScript retourne des millisecondes. La plupart des API web utilisent des millisecondes. Les applications FLIN qui interagissent avec des API web peuvent transmettre les horodatages directement sans conversion.

Arithmétique entière. En utilisant des millisecondes i64, toutes les opérations temporelles sont de l'arithmétique entière -- rapide, déterministe et exempte de problèmes de précision en virgule flottante. La durée maximale représentable est d'environ 292 millions d'années, ce qui devrait suffire pour la plupart des applications.

Simplicité. Une seule représentation, une seule unité, pas de tables de conversion. Une durée est un nombre. L'arithmétique temporelle est une addition d'entiers. La VM n'a pas besoin d'un « type durée » spécial à l'exécution.

Interaction avec les mots-clés temporels

L'arithmétique temporelle se compose naturellement avec les mots-clés temporels de FLIN :

flin// L'échéance est dans 7 jours
deadline = now + 7.days

// Le rappel est 1 heure avant l'échéance
reminder_time = deadline - 1.hours

// Combien de temps reste-t-il ?
time_remaining = deadline - now

// Requête sur les entités : utilisateurs créés dans les 90 derniers jours
recent_users = User.where(created_at > now - 90.days)

// Contrôle du cache
cache_until = now + 5.minutes
is_cached = now < cache_until

Les mots-clés temporels (now, today, yesterday, etc.) retournent des valeurs temporelles. Les littéraux de durée retournent des valeurs de durée. Le vérificateur de types s'assure que seules les combinaisons valides sont utilisées. Le résultat est un système complet de manipulation du temps qui se lit comme du langage naturel et se compile en opérations entières.

Décisions de conception

Pourquoi N.days au lieu de days(N) ?

La syntaxe d'appel de fonction (days(7)) aurait nécessité l'enregistrement de sept fonctions intégrées. La syntaxe d'accès membre (7.days) a nécessité neuf lignes de code de parseur et zéro nouvelle fonction. La syntaxe se lit aussi plus naturellement -- « sept jours » au lieu de « jours de sept ».

Pourquoi des mois et années approximatifs ?

Les mois font trente jours. Les années font trois cent soixante-cinq jours. Ce sont des approximations -- les vrais mois varient de vingt-huit à trente et un jours, et les années bissextiles ont trois cent soixante-six jours. Nous avons choisi des valeurs approximatives parce que :

  1. L'arithmétique calendaire exacte nécessite de connaître la date de départ, le système calendaire et les règles de fuseau horaire. Cette complexité appartient à une bibliothèque de dates, pas à une primitive du langage.
  2. Pour les cas d'usage visés par FLIN (durées de cache, périodes d'abonnement, politiques de rétention), les durées approximatives sont suffisantes. « Conserver les données pendant quatre-vingt-dix jours » n'a pas besoin de tenir compte de la longueur de février.
  3. Les développeurs qui ont besoin d'une arithmétique calendaire exacte peuvent utiliser des fonctions de manipulation de dates explicites (prévues pour les futures versions de FLIN).

Pourquoi la sûreté de type plutôt que les vérifications à l'exécution ?

Nous aurions pu représenter les durées comme des entiers au niveau du système de types (comme elles le sont à l'exécution) et laisser les développeurs additionner Time + Int librement. La sûreté de type serait perdue, mais l'implémentation serait plus simple.

Nous avons choisi la sûreté de type parce que les erreurs d'arithmétique temporelle sont une catégorie de bugs courante. Ajouter un entier brut à un horodatage produit un résultat dans la mauvaise unité (secondes versus millisecondes, ou pire). Le système de types détecte ces erreurs à la compilation, avant qu'elles ne causent des bugs minuit-déployé-comme-midi en production.

Patterns courants rendus possibles par l'arithmétique temporelle

L'arithmétique temporelle débloque une catégorie de logique applicative auparavant difficile à exprimer :

Gestion d'abonnements :

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.days

Expirations de session :

flinsession_timeout = 30.minutes
last_activity = user.updated_at
is_timed_out = now > last_activity + session_timeout

Tâches planifiées :

flinentity Task {
    name: text
    due_at: time
    remind_at: time
}

task = Task {
    name: "Soumettre le rapport",
    due_at: now + 7.days,
    remind_at: now + 6.days    // Rappel 1 jour avant
}
save task

Chaque pattern se lit naturellement. Pas d'import de bibliothèque de dates. Pas d'analyse de chaînes de format. Pas d'utilitaires de conversion de fuseaux horaires. Juste de l'arithmétique sur des valeurs temporelles.

Tests

Le fichier d'exemple time-arithmetic-test.flin teste chaque combinaison :

flin// Littéraux de durée
timeout = 30.seconds
cache_ttl = 5.minutes
sprint = 14.days
subscription = 1.months

// Durées à virgule flottante
half_hour = 0.5.hours
ninety_mins = 1.5.hours

// Arithmétique temporelle
deadline = now + 7.days
reminder_time = deadline - 1.hours
time_left = deadline - now

// Opérations sur les durées
total_time = 2.hours + 30.minutes
remaining = 1.hours - 15.minutes
double_timeout = 30.seconds * 2

Toutes les expressions passent la vérification de types avec succès. Les mille dix tests de bibliothèque passent. L'implémentation n'ajoute aucun surcoût à l'exécution pour les durées littérales grâce au constant folding.

Impact sur la progression

La session 078 a complété les douze tâches TEMP-5 en une seule session, faisant passer l'arithmétique temporelle de zéro pour cent à cent pour cent :

  • Extension de l'AST (enum DurationUnit, Expr::Duration)
  • Intégration dans le parseur (reconnaissance de l'accès membre)
  • Système de types (FlinType::Duration)
  • Vérification de types (validation des littéraux de durée, règles d'arithmétique temporelle)
  • Génération de code (emit_duration avec constant folding)
  • Aucun nouvel opcode de VM nécessaire
  • Tests et validation

La progression temporelle globale est passée de soixante et onze à quatre-vingt-trois sur cent soixante tâches (cinquante et un virgule neuf pour cent). Le modèle temporel de FLIN a franchi la barre de la moitié.

Cent cinquante-trois lignes de nouveau code. Sept unités de durée. Arithmétique temporelle type-safe. Zéro surcoût à l'exécution. Et deadline = now + 7.days est devenu l'expression la plus naturelle du langage.


Ceci est la partie 8 de la série « Comment nous avons construit FLIN » sur le modèle temporel, documentant le système d'arithmétique temporelle qui rend les opérations de dates aussi naturelles que des expressions du langage.

Navigation dans la série : - [046] Chaque entité se souvient de tout : le modèle temporel - [047] Historique des versions et requêtes de voyage dans le temps - [048] Intégration temporelle : des bugs à 100 % de couverture de tests - [049] Destroy et Restore : la suppression douce bien faite - [050] Filtrage et tri temporels - [051] Fonctions de comparaison temporelle - [052] Accès aux métadonnées de version - [053] Arithmétique temporelle : ajouter des jours, comparer des dates (vous êtes ici) - [054] Précision du suivi et validation - [055] Le modèle temporel complet : ce qu'aucun autre langage n'offre

Share this article:

Responses

Write a response
0/2000
Loading responses...

Related Articles