Flux & the FDK
La documentation complète de Flux — un langage dataflow généraliste et déterministe dont l'unique VM se tient derrière de nombreux types d'application : graphiques temps réel, interfaces interactives, scènes animées, jeux vérifiés côté serveur. À l'intérieur : le système de types, les quatre plans, le runtime et chaque pilier du FDK — un seul fichier autonome, chaque figure intégrée, rien depuis le réseau.
Traduction instantanée — la version anglaise fait foi et est la seule tenue à jour.
Flux — documentation du langage et du FDK
Flux est un langage applicatif total, causal et déterministe pour la plateforme web. Un programme Flux n'est pas une séquence d'instructions — c'est un graphe de dataflow typé : chaque valeur est un flux, chaque flux porte un kind (sa dimension, pas seulement sa forme), et le moteur — et non l'auteur — prend en charge l'évaluation, l'incrémentalité, la mémoire et l'ordonnancement. Quatre plans coopérants séparent ce qu'un programme calcule (ANALYSIS) de ce qu'il montre (CANVAS), de la façon dont il s'anime d'un état à l'autre (TRANSITION), et de la façon dont il interagit et persiste (APP) — avec un pare-feu à sens unique entre eux qui rend les garanties fondamentales structurelles plutôt que disciplinaires.
Le langage est livré avec le FDK (Flux Development Kit) : le prélude standard et les API des piliers — compute (dataframes, statistiques, maths métier), collections, color, text, i18n, units, réseau, display, host services, le plan server, et l'identité asset/currency — tous bornés, sous contrôle de capacités, et spécifiés au même niveau de déterminisme que le cœur.
Une seule VM de dataflow se tient derrière de nombreux types d'application — un graphique en direct, une interface interactive, une scène animée, un jeu vérifié côté serveur — parce que les quatre plans couvrent calculer → montrer → animer → interagir. Flux est généraliste ; ses spécialisations métier se posent par-dessus, et le fer de lance en est le tracé de graphiques financiers et l'analyse de marché, ce qui explique pourquoi beaucoup d'exemples tracent des prix — bien que rien dans le modèle ne soit spécifique aux marchés. Une seule ligne suffit pour saisir le caractère du langage :
fluxplot rsi(close, input(14)) // osc(0,100) → its own pane, 0–100 scale, 30/70 guides, params UI — all inferred
#Parcours de lecture
- Nouveau sur Flux — lisez le livre dans l'ordre : Qu'est-ce que Flux → Piliers de conception → Premiers pas → Les quatre plans.
- Écrire des programmes — le livre de recettes, puis la référence du FDK pour l'API dont vous avez besoin, avec la référence du langage à portée de main.
- Évaluer les garanties — Garanties, puis Compilateur et runtime et Kinds.
- Étendre l'hôte (représentations, outils, panneaux) — Intégration hôte.
#La carte
#Le livre
| Page | Contenu |
|---|---|
| 01 — Qu'est-ce que Flux | Le modèle mental, à quoi ressemblent les programmes, où ils s'exécutent, les niveaux de confiance, les non-objectifs assumés |
| 02 — Piliers de conception | Total · causal · byte-déterministe · dimensionnel · cloisonné par pare-feu · sécurisé par capacités · optimisable |
| 03 — Premiers pas | D'une ligne aux paramètres, à la composition, à l'état, au MTF, au canvas et à une première app |
| 04 — Les quatre plans | ANALYSIS / CANVAS / TRANSITION / APP en profondeur ; le pare-feu ; live() |
#Référence du langage
| Page | Contenu |
|---|---|
| Structure lexicale | Tokens, littéraux, interpolation, niveaux de mots-clés, règles de saut de ligne |
| Grammaire | La grammaire normative, les cinq lectures de la flèche unique, la précédence, les propriétés formelles |
| Kinds | Le système de types dimensionnel : sortes, treillis, coercition, tags, types nommés |
| Opérateurs | L'algèbre dimensionnelle de chaque opérateur ; UFCS ; with ; la famille ? |
| Inférence | Inférence de kind ; inférence de présentation (kind → panneau/échelle/repères) ; la politique d'erreurs |
| Temps et état | flux, délai, fenêtres, scan, horloges et @, causalité, live() |
| Canvas | Les signaux comme propriétés, espaces, événements → actions, primitives, le modèle de performance |
| Transitions | Morph, replay, focus, view — interpolation cosmétique entre états calculés |
| Plan APP | Le cœur d'architecture Elm, messages, souscriptions, commandes, slots, capacités |
#Référence du FDK
| Page | Contenu |
|---|---|
| Vue d'ensemble du FDK | Prélude, namespaces, doc-as-data, le modèle de capacités, matrice de statut |
| compute | math/stat/vec/decimal/time/ta, dataframes Table/Col/Mat, bibliothèques métier |
| collections | Vec/Deque/Map/Set/Tree — bornés, ordonnés, à sémantique de valeur |
| color | Le kind color, construction, interpolation OKLab, canaux de sortie |
| text | string, str/fmt, le codec Md, édition, coloration syntaxique, diff, recherche, validateurs |
| i18n | Locales, catalogues de messages, MessageFormat 2, collation, RTL |
| units | Quantités meas[u] : conversions, point/delta affine, murs |
| net | Verbes, transports, codecs, flux, backpressure, presets, cache/hors-ligne |
| display | Les scènes comme valeurs, les deux strates, modèles 2D/3D, viz.*, panneaux, liaisons d'outils |
| host services | Fichiers, presse-papiers, notifications, auth, paiements, médias, impression, polices, intégration |
| server | Apps headless, stockage partagé avec ACL hiérarchisés, prérendu, le troisième pied du déterminisme |
| asset & currency | Tags d'asset (B,Q[,@v]), fx et money, places de marché, toSource |
#Rouages internes
| Page | Contenu |
|---|---|
| Compilateur et runtime | Le pipeline, interpréteur ≡ WASM (I7), routines épinglées, budgets, le harnais de vérification |
| Modèle mémoire | Représentation des valeurs, colonnes SoA, le plan de vivacité, slotmaps, arènes, mémoire linéaire |
| Optimiseur | La loi de correction, la validation de traduction, les paliers T0–T3, le modèle de coût |
| Concurrence | Ordonnancement du DAG pur, barrières vs work-stealing, byte-égalité 1 ≡ N |
| Packages | fluxpack, adressage par contenu, sélection de version minimale, agrégation de capacités |
| Intégration hôte | Les cinq descripteurs, registres ouverts, représentations, jonctions d'extension |
#Guides
| Page | Contenu |
|---|---|
| Livre de recettes | Des recettes qui marchent pour chaque aspect du langage |
| Garanties | Ce qui est garanti, et comment chaque garantie est appliquée et vérifiée par machine |
| Éditeur | Complétion consciente des kinds, documentation au survol, aperçu en direct, débogage du dataflow |
| FAQ | Les questions courantes auxquelles le modèle répond ; les non-objectifs délibérés |
| Glossaire | Chaque terme, défini précisément |
#État d'implémentation
La documentation décrit Flux v1 tel que spécifié par les plans de conception scellés
(docs/DESIGN-flux-*.md). L'état en un coup d'œil :
| Domaine | État |
|---|---|
| Cœur du plan ANALYSIS — grammaire (Lezer), kinds & inférence, interpréteur DAG, optimiseur avec validation de traduction, corpus golden & tests différentiels | Implémenté (src/flux/lang) |
| Backend WASM (Binaryen), exécution multi-worker (mémoire partagée, compteurs ready atomiques), câblage du graphique | Implémenté (src/flux/lang, src/flux/display) |
| Format de distribution fluxpack — writer, loader, manifeste, vérification, gate de rebuild | Implémenté (src/flux/pack) |
| Plans CANVAS / TRANSITION | Conception scellée ; implémentation échelonnée |
| Plan APP (harnais TEA, capacités, slots) | Conception scellée ; implémentation échelonnée |
| Piliers du FDK (collections d'abord, puis selon l'ordre figé) | Conceptions scellées ; implémentation échelonnée |
Les pages individuelles ne répètent pas ce tableau ; les fonctionnalités que la conception elle-même diffère sont étiquetées Post-v1, Réservé, ou Décision ouverte en ligne (voir les conventions ci-dessous).
#Conventions utilisées partout
- Les kinds sont écrits en style code :
price,level,osc(0,100),signal,vec(κ, N),record{…}. - Les codes d'erreur sont verbatim et porteurs de sens :
[ErrDim],[ErrCausal],[ErrTotal],[ErrFirewall],[WarnTop]… - Les invariants sont numérotés I1–I7 (le contrat horloge/déterminisme) ; les règles d'amendement A1–A15 affinent le système de kinds.
- Les lignes d'exemple portent des annotations pédagogiques :
plot x // level → centered pane; les formes rejetées sont marquées// ✗ [ErrDim] …. - Post-v1. / Réservé. / Décision ouverte. étiquettent en ligne le matériel dont la conception est différée.
#Voir aussi
- Qu'est-ce que Flux — commencez ici si le langage est nouveau pour vous.
- Premiers pas — le premier programme, et ce qu'il infère pour vous.
- Kinds — le système de types dimensionnel sur lequel tout le reste s'appuie.
- Garanties — ce qui est promis, et comment chaque promesse est vérifiée par machine.
- Vue d'ensemble du FDK — les bibliothèques et le modèle de capacités.
- Glossaire — chaque terme, défini précisément.
Qu'est-ce que Flux
Flux est un langage applicatif total, causal et déterministe pour la plateforme web. C'est un langage généraliste doté de spécialisations métier — le tracé de graphiques financiers et l'analyse de marché sont la spécialisation phare, pas la définition. Vous pouvez écrire un indicateur en une ligne, une scène animée en cinq, ou une application interactive complète en cinquante ; tous sont le même genre d'objet : un graphe de dataflow pur sur des flux typés, exécuté par un hôte qui garantit — par construction, et vérifié par machine — que le programme se termine, ne réécrit jamais son propre passé, produit les mêmes octets sur chaque machine, et ne peut toucher à rien qui ne lui ait été explicitement accordé.
Ce chapitre vous donne la forme du langage tout entier : l'idée unique qui le sous-tend, les quatre plans dont un programme est fait, à quoi ressemblent les programmes, où ils s'exécutent, à qui ils s'adressent, et ce que Flux ne fait délibérément pas. Tout est détaillé dans les chapitres ultérieurs ; rien ici ne requiert de connaissance préalable de Flux ni du trading.
#Une idée : chaque valeur est un flux
Une valeur Flux n'est pas un nombre posé dans une variable. C'est un flux : une valeur
telle qu'elle évolue le long d'un axe ordonné d'unités de données — relevés de capteurs,
entrées de journal, tours de jeu, ou (dans le domaine phare) barres de marché. close n'est
pas « le dernier prix » ; c'est l'historique complet des prix de clôture, une valeur par unité,
jusqu'à maintenant.
Tout découle du fait de prendre cela au sérieux :
- Une constante est un flux dégénéré — la même valeur à chaque unité.
2etclosesont la même sorte de chose, donc ils se combinent librement. - L'arithmétique est élément par élément.
fast - slowsoustrait deux historiques entiers, unité par unité — pourtant le moteur l'évalue de façon incrémentale, une nouvelle valeur par nouvelle unité. - Il n'y a ni index ni boucle. Vous n'écrivez jamais
for i, ne gérez jamais de tampon, ne décidez jamais quand recalculer. Vous décrivez ce qu'une valeur est, pas quand la mettre à jour.
fluxfast = ema(close, 12) // a stream: the 12-unit exponential average, over all history
slow = ema(close, 26)
plot fast - slow // element-wise difference — itself a stream
Parce que les flux sont des valeurs, la composition fonctionnelle ordinaire constitue tout le
modèle de programmation : des fonctions de flux vers flux (def), des enregistrements de flux
(bb.upper), des flux d'enregistrements, des flux pilotant des propriétés visuelles. Il y a une
seule algèbre, appliquée partout.
#Décrivez une expression ; le moteur évalue
Le modèle mental en une phrase : vous décrivez une expression pure, le moteur l'évalue. Vous
n'écrivez jamais la boucle, le tampon, ou le await. Le compilateur inline vos définitions dans
un DAG incrémental typé (graphe orienté acyclique) d'opérations, le vérifie — kinds,
causalité, totalité, frontières de plans — choisit un noyau natif pour chaque nœud qu'il
reconnaît, planifie sa mémoire statiquement, puis l'exécute soit par unité (en direct, un
pas par nouvelle unité de données), soit en lot (rejeu sur tout l'historique). Les deux
exécutions sont la même fonction et produisent les mêmes octets.
C'est pourquoi un programme Flux n'a pas de code de cycle de vie. Il n'y a pas de callback « à chaque nouvelle barre », pas de gestion de souscriptions dans le code ANALYSIS, pas d'invalidation de cache : le DAG est le graphe de dépendances, et le moteur le fait avancer. C'est aussi pourquoi le moteur peut faire des promesses fortes — un graphe pur, typé, borné et acyclique est un objet que vous pouvez vérifier, ordonnancer, paralléliser et optimiser sans changer son sens.
#Quatre plans, un seul pare-feu à sens unique
Une application complète a des parties aux besoins très différents. Un calcul doit être exact et reproductible. Une animation doit lire l'horloge et peut recourir à l'aléatoire. Une transition d'écran doit pouvoir interpoler librement sans corrompre les données. L'état applicatif doit répondre aux événements utilisateur et déclencher des effets. Flux ne moyenne pas ces besoins en un seul compromis ; il les sépare en quatre plans coopérants — un langage, une algèbre d'expressions, quatre jeux de règles :
| Plan | Rôle | Horloge | Règles |
|---|---|---|---|
| ANALYSIS | calcul sur les unités de données : indicateurs, signaux, transformations de représentation | l'unité de données (la barre) | total, causal, déterministe, no-repaint, en bac à sable |
| CANVAS | présentation : dessin animé, décor, effets, interaction au pointeur | la frame | écran, temps réel et aléatoire autorisés — hors des garanties, par conception |
| TRANSITION | interpole le rendu entre deux états calculés | la frame | cosmétique par construction : elle ne peut jamais changer une valeur |
| APP | état applicatif et UI : Model · update · view · Sub / Cmd | événements | update pure, totale, déterministe ; journal de messages rejouable ; effets sous contrôle de capacités |
Les plans sont reliés par un pare-feu à sens unique : la présentation peut lire l'analyse ;
l'analyse ne lit jamais la présentation. Une scène CANVAS peut briller plus fort quand une
valeur ANALYSIS monte ; une expression ANALYSIS qui tente de lire la souris, l'horloge murale ou
un signal aléatoire sans graine est rejetée à la compilation avec [ErrFirewall]. Le plan APP
lit les séries via des souscriptions typées et ne peut jamais écrire dans ANALYSIS.
Pourquoi cette règle existe. Le pare-feu est ce qui permet aux garanties et à la liberté de coexister dans un même programme. Votre signal reste no-repaint de façon prouvée — une valeur, une fois produite pour un pas, ne change jamais — même tandis qu'un effet animé et aléatoire danse juste à côté, parce que le langage rend impossible que l'effet reboucle dans le signal. Toute valeur qui compte vit du côté discipliné du mur ; tout ce qui est décoratif vit du côté libre.
Le chapitre Les quatre plans couvre chaque plan en profondeur.
#À quoi ressemble un programme — trois avant-goûts
#Un analytique, en une ligne
fluxplot rsi(close, input(14)) // rsi : osc(0,100) → own pane, 0–100 scale, guides 30/70
Cette seule ligne est un programme complet et livrable. Tout le reste est inféré du kind de l'expression — le type dimensionnel qui dit ce que la valeur est physiquement, pas seulement sa largeur :
closea le kindprice;rsimappe toute quantité scalaire versosc(0,100)— un oscillateur borné et sans dimension.- Un
osc(0,100)ne partage aucune échelle avec le prix, donc il se matérialise dans son propre panneau plutôt que sur le graphique des prix — avec une échelle 0–100 fixe. - Les lignes de repère conventionnelles à 30 et 70 proviennent des métadonnées de l'opération et sont tracées automatiquement.
input(14)déclare un paramètre : l'éditeur en dérive un contrôle, la valeur réglée vit avec l'instance du graphique (la source conserve la valeur par défaut), et l'ajuster réexécute le graphe sans recompiler.- La série est enregistrée — nom, style de rendu, classe sémantique, jusqu'à sa description d'accessibilité — tout dérivé du kind.
Rien concernant le placement, l'échelle, les lignes de référence ou la tuyauterie des paramètres n'est écrit où que ce soit, et rien de tout cela n'a besoin de l'être. Quand vous voulez vraiment le contrôle, chacun de ces défauts est surchargeable sur place — voir Premiers pas.
#Une scène qui bouge
fluxcircle { at:(spring(close)); glow: 16; trail: 24 } // a comet easing toward the price
on every(1 bars) -> spawn ring { r: 6->24; life: 200 bars }
Ceci est le plan CANVAS. Son axiome : chaque propriété est un signal. Une constante, un flux
de données et un générateur d'animation comme spring(close) — un signal qui tend continûment
vers sa cible — sont la même sorte de chose et se combinent avec la même algèbre. Il n'y a pas
d'API d'animation séparée à apprendre : vous câblez des signaux dans des propriétés, et les
événements (on … ->) engendrent ou interpolent des primitives. La scène se compile une fois en
une structure retenue ; l'hôte route chaque signal vers le chemin d'exécution le moins coûteux
(en cache, par unité, ou piloté par le compositeur).
#Une application
fluxvariant Msg { Inc | Reset }
app counter {
init(p) = { n: 0 }
update(m, msg) = match msg {
Inc -> { model: m with { n: m.n + 1 }, cmds: [] }
Reset -> { model: m with { n: 0 }, cmds: [] }
}
view(m) = panel(slot: right.panel) { text("count {m.n}") ; button("+1", Inc) }
subs(m) = []
}
Ceci est le plan APP : The Elm Architecture (TEA), durcie. Un Model (état borné et typé),
une fonction update pure et totale qui replie les messages en un nouvel état plus une liste de
commandes — descriptions d'effets inertes que l'hôte peut exécuter — une view pure qui
renvoie un arbre de primitives d'UI validées, et des souscriptions déclaratives (subs) par
lesquelles le monde ambiant (temps, données, gestes de l'utilisateur) entre sous forme de
messages. Parce que update est un repli pur sur un journal de messages, l'état applicatif est
rejouable par construction : le journal est l'unique source de vérité. Cette app ne demande
aucune capacité, donc l'hôte ne lui laissera rien faire au-delà de dessiner sa vue et recevoir
ses propres messages.
#Une flèche, cinq lectures
Vous avez maintenant vu la flèche -> trois fois, signifiant trois choses différentes —
l'action d'un événement, une branche d'un match, et (au chapitre suivant) une lambda. C'est
délibéré, et c'est le seul élément de syntaxe qui vaille la peine d'être appris d'emblée, parce
que Flux a exactement un seul token de flèche et il porte cinq lectures, chacune sélectionnée
par son contexte :
| Lecture | Exemple | Ce qui la sélectionne |
|---|---|---|
| lambda | vec.map(v, (x) -> x * 1.1) |
la position attend une fonction |
| événement → action | on click -> burst(40) ring { } |
le mot-clé de tête on |
| paire de tween | tween r 6 -> 24 over 300ms |
aucune fonction n'est attendue — la flèche apparie deux valeurs |
| branche de match | match m.phase { ask -> … } |
le mot-clé de tête match |
| compréhension de vue | for lvl in levels -> dot { … } |
la tête for … in |
L'analyseur n'a jamais à deviner : une tête de mot-clé revendique la flèche, et partout ailleurs le kind de la position décide. La récompense, c'est que vous ne trimballez jamais une table de symboles fléchés dans votre tête — il n'y en a qu'un, et vous le lisez depuis son contexte.
#Où s'exécutent les programmes
Un programme Flux a deux véhicules d'exécution et un seul sens. Pendant que vous éditez, un interpréteur évalue le DAG directement — retour instantané, valeurs par nœud que le débogueur peut lire, aperçu en direct à chaque frappe. Quand un programme s'exécute pour de vrai — ou est livré à quelqu'un d'autre — il est compilé en un module WebAssembly et exécuté dans un bac à sable. Les deux ne sont pas « proches » : ils sont bit-identiques. L'invariant I7 exige que l'interpréteur et le module compilé produisent exactement les mêmes octets sur les mêmes entrées, et la chaîne d'outils le vérifie à chaque compilation en exécutant les deux et en affirmant l'égalité — une divergence empêche l'artefact d'être livré. La même discipline s'étend à travers les machines : l'évaluation en virgule flottante est scalaire et non réassociée, et chaque routine ayant une marge de variance selon la plateforme (maths transcendantes, arithmétique décimale, Unicode, calendriers, génération aléatoire, la représentation des valeurs manquantes) est épinglée à une seule implémentation partagée. Ce que vous avez vu dans l'éditeur est ce qui s'exécute, partout, jusqu'au dernier bit. La machinerie est spécifiée dans Compilateur et runtime.
#Le langage d'une plateforme : deux niveaux de confiance
Flux n'est pas une niche d'extension embarquée ; c'est le langage dans lequel la plateforme elle-même est écrite, et le langage dans lequel ses utilisateurs l'étendent. Les deux publics partagent un seul langage et un seul bac à sable, distingués uniquement par la confiance — les capacités accordées, jamais le code :
- Niveau A — first-party. L'interface de la plateforme elle-même, y compris ses outils les plus exigeants, est écrite en Flux. Sa source reste privée et est livrée en WASM compilé — la plus forte affirmation possible que le langage est suffisant pour de vraies applications.
- Niveau B — auteurs et utilisateurs. N'importe qui peut écrire des indicateurs, des représentations, des outils de dessin et des applications. Un artefact partagé ou acheté arrive en WASM uniquement, jamais en source : le travail de l'auteur est protégé, et le consommateur exécute un binaire non fiable en toute sécurité — parce que les scripts sont totaux (ils ne peuvent pas s'emballer), en bac à sable (le langage n'a aucune primitive d'E/S à détourner), et à refus par défaut : chaque effet requiert une capacité, et un artefact porte un manifeste inspectable, agrégé transitivement, de tout ce qu'il peut demander, visible avant l'installation.
Post-v1. Le déploiement du partage public et de la place de marché est différé ; le modèle de confiance qui le rend sûr est en v1 et n'est affaibli nulle part.
#Ce que Flux ne fait délibérément pas (v1)
Les limites assumées font partie de la conception. Chacune est un choix avec une justification, pas une lacune :
- Pas de boucles à exécution libre. Flux est total, pas Turing-complet — par choix. Les
fenêtres, les replis et les boucles existent, mais chaque borne est une constante à la
compilation sous un plafond. Un programme qui ne peut pas énoncer sa borne est rejeté avec
[ErrTotal]à la compilation plutôt que tué par un timeout à l'exécution. (Décision ouverte. Une échappatoireunsafeopt-in pour une boucle non bornée est discutée dans la conception et déconseillée par défaut ; rien dans le catalogue n'en a besoin.) Voir Piliers de conception. - Pas d'E/S directes depuis les scripts. Le langage n'a pas de fetch, pas de DOM, pas d'eval, pas de descripteurs de fichiers. Les effets sont arbitrés par l'hôte : un script émet des données de commande inertes sous des capacités déclarées, et l'hôte les exécute. C'est ce qui fait de l'exécution de code non fiable un acte de routine plutôt qu'une évaluation des risques.
- Grain temporel centré sur la barre. L'horloge ANALYSIS avance sur les unités de données clôturées. La granularité au niveau du tick et du flux d'ordres est une extension post-v1 nommée, pas une promesse implicite.
- Les données externes hors-prix restent en dehors d'ANALYSIS. Les données alimentées par le
réseau entrent dans le plan APP via des souscriptions typées ; elles ne peuvent pas devenir
silencieusement un « indicateur ». Réservé. Le kind
metricnomme la jonction par laquelle un flux de données externe causal pourrait entrer plus tard dans ANALYSIS, délibérément maintenue ouverte et inerte en v1. - Pas de scan de tout le marché, pas de gestion de portefeuille. Le travail inter-séries sur une poignée d'instruments nommés est de première classe ; scanner le marché entier, et gérer de nombreuses positions simultanées, ne sont pas des préoccupations de v1. (Un screener borné — une fonction totale mappée sur un univers à capacité fixe, avec un top-K à clé — est un pilier Post-v1. scellé ; c'est un map-reduce borné, jamais un scan non borné.)
- Pas de migration de code automatique. Flux ne livre pas de convertisseur depuis d'autres écosystèmes de scripts. Sa sémantique exclut délibérément des motifs que certains écosystèmes permettent (éditions rétroactives de l'historique, boucles non bornées, E/S ambiantes), de sorte qu'une traduction automatique fidèle est impossible pour toute une classe de programmes ; la documentation enseigne plutôt directement les motifs Flux équivalents.
- Post-v1. Les harnais de backtest de stratégies, la livraison d'alertes à distance, et la
synchronisation multi-appareils sont conçus mais différés ; les alertes locales (
alert) sont en v1.
#Comment lire cette documentation
Le livre (cette section) construit le modèle mental dans l'ordre :
- Qu'est-ce que Flux — ce chapitre.
- Piliers de conception — les sept garanties, pourquoi chacune existe, et ce que chacune vous apporte.
- Premiers pas — une première session guidée, d'une ligne à une petite application.
- Les quatre plans — ANALYSIS, CANVAS, TRANSITION et APP en profondeur, y compris le pare-feu et les valeurs en direct.
Autour du livre :
- Référence — le langage normatif : structure lexicale, grammaire, kinds, opérateurs, inférence, temps et état, canvas, transitions, le plan APP.
- FDK — le Flux Development Kit : le prélude de la stdlib et les API des piliers (compute, collections, color, text, i18n, units, net, display, host services, server, asset/currency) plus le catalogue de capacités.
- Rouages internes — comment les garanties sont implémentées : compilateur et runtime, modèle mémoire, optimiseur, concurrence, packages, intégration hôte.
- Guides — le livre de recettes, la page des garanties, l'éditeur, la FAQ et le glossaire.
Si vous ne lisez qu'une page de plus, lisez Piliers de conception : chaque décision de conception du langage remonte à l'un des sept.
#Voir aussi
- Piliers de conception — les sept garanties derrière tout ce qui précède.
- Premiers pas — écrivez votre premier programme dans les dix prochaines minutes.
- Les quatre plans — le modèle de plans et le pare-feu, en profondeur.
- Garanties — ce qui est promis et comment chaque promesse est vérifiée par machine.
- Index du FDK — la surface d'API destinée aux auteurs et le modèle de capacités.
- README — la carte de la documentation et l'état d'implémentation.
Piliers de conception
Chaque décision de conception dans Flux remonte à sept piliers. Chaque pilier est une propriété que le langage impose par construction — pas une convention, pas une règle de linter, pas une bonne pratique, mais quelque chose que le compilateur prouve sur chaque programme accepté et, là où la preuve a besoin d'aide, vérifie par machine à chaque compilation. Ce chapitre énonce chaque pilier, explique pourquoi il existe, détaille ce qu'il vous apporte concrètement, et le montre dans un micro-exemple.
Lisez ce chapitre pour comprendre pourquoi Flux a la forme qu'il a ; lisez Premiers pas pour le ressentir en pratique.
#1. Total, pas Turing-complet
L'énoncé. Chaque programme Flux se termine, et son coût par pas est connu à la compilation. Il n'y a pas de boucles non bornées, pas de récursion non bornée, pas de collections non bornées.
Pourquoi. Flux appartient à la famille du dataflow synchrone (dans la lignée de Lustre et SCADE) : des langages bâtis pour des systèmes où « le programme pourrait ne pas finir ce pas » n'est pas une issue acceptable. Un hôte de graphique doit faire avancer chaque script actif à chaque unité de données dans un budget de frame ; une plateforme qui exécute le code d'autrui doit borner ce que ce code peut consommer. La Turing-complétude retirerait exactement ces garanties — le problème de l'arrêt rend un script non borné impossible à budgéter — et n'apporte rien dont de vrais programmes analytiques aient besoin. La totalité est un choix, et Flux le fait ouvertement.
Ce que cela vous apporte.
- Terminaison garantie. Aucun script ne se bloque, jamais — ni le vôtre ni celui d'autrui.
- Un budget statique. La mémoire et le coût par unité sont calculés à la compilation ; un
programme qui dépasse le budget est rejeté avant de s'exécuter avec
[ErrTotal], avec la borne fautive nommée — jamais tué en cours d'exécution par un timeout. - Bac à sable par construction. Un programme total sans primitives d'E/S ne peut pas être transformé en arme pour une attaque par épuisement de ressources. C'est la fondation sur laquelle reposent les niveaux de confiance.
- Vérifiabilité partout ailleurs. Les vérifications statiques (kinds, causalité, exhaustivité) sont décidables parce que le langage est total et que ses bornes sont des constantes.
En pratique. L'itération existe — bornée, et franche à ce sujet :
fluxhi20 = highest(close, 20) // windowed reducer — the bound is a constant
w = window(close, 20) // the same 20 values as a vec, for map/fold
hi = scan(close, (prev) -> math.max(prev, close)) // running state with feedback (see pillar 2)
window avec map/fold couvre les boucles comptées, scan couvre l'état courant, et
loop(max, …) couvre « itérer jusqu'à terminaison » avec un plafond déclaré. Chaque borne est une
constante à la compilation sous un plafond global ; une longueur dépendante des données est une
erreur de kind, pas une surprise à l'exécution.
#2. Causal par construction
L'énoncé. Une valeur produite pour un pas ne peut plus jamais changer ensuite. L'historique est immuable — non comme une discipline que l'auteur maintient, mais comme un théorème du langage.
Pourquoi. Dans tout système qui réévalue sur un historique croissant, le défaut le plus mortel est la valeur qui change rétroactivement en douce : un analytique qui semblait prophétique sur l'historique parce que, à chaque pas passé, il avait silencieusement lu des données qui n'existaient pas encore. Le résultat est une divergence direct/rejeu qu'aucun test n'attrape, parce que les deux exécutions sont « correctes » — pour des définitions différentes du temps. Flux élimine le défaut à la racine en le rendant inexprimable : aucune construction du langage ne peut lire le futur.
Trois règles produisent le théorème :
- Les délais ne regardent que le passé.
x[n]atteintnpas en arrière, avecn ≥ 0une constante. Un délai négatif ne s'analyse en aucun sens ; il lève[ErrCausal]. - Le rééchantillonnage ne lit que les unités clôturées.
x @ "1d"lit la dernière unité journalière clôturée — jamais celle encore en formation, et jamais une clôture future. (L'unité en formation est accessible pour l'affichage seulement, vialive(), que le pare-feu tient à l'écart de toute analyse.) - La rétroaction doit passer par un délai unitaire. Tout cycle dans le graphe doit traverser
x[1](la règle quescanrespecte en interne), de sorte que la sortie d'aujourd'hui peut dépendre de la sortie d'hier mais jamais d'elle-même.
Par induction sur le graphe : output[t] = f(inputs[0..t]), mathématiquement. Une valeur passée
n'a plus rien dont dépendre, donc rien ne peut la déplacer. Cette propriété s'appelle
no-repaint — une valeur, une fois produite pour un pas, ne change jamais — et elle tient pour
chaque programme accepté, pas pour un sous-ensemble soigné.
Ce que cela vous apporte.
- Direct ≡ historique. Le flux que vous calculez en direct et le flux que vous rejouez sur l'historique sont la même fonction — l'épine dorsale honnête de toute évaluation.
- Des signaux dignes de confiance. Un croisement qui s'est déclenché reste déclenché. Les marques et les alertes reposent sur un sol qui ne peut pas bouger.
- Vérifiable par machine. La causalité est une propriété de graphe (pas de cycles à délai nul), décidée à la compilation — la même idée de calcul d'horloges prouvée dans les langages synchrones.
En pratique.
fluxprev = close[1] // yesterday's close — legal, and na on the first bar
gain = math.max(close - prev, 0)
peek = close[-1] // ✗ [ErrCausal] — a negative delay reads the future
Le rejet vient avec sa raison : une valeur venue du futur forcerait vos valeurs passées à changer une fois que la réalité rattrape son retard.
#3. Déterministe à l'octet près
L'énoncé. Le même programme sur les mêmes données produit les mêmes octets — entre l'interpréteur d'édition et le WASM compilé, entre deux exécutions, entre deux machines.
Pourquoi. « À peu près égal » n'est pas une propriété sur laquelle on peut bâtir. La byte-égalité, si : elle rend le rejeu exact, les tests golden significatifs, les résultats reproductibles d'un appareil à l'autre, et la ré-exécution indépendante (par exemple, un serveur revérifiant l'exécution d'un client) tout simplement possible. La virgule flottante est déterministe si et seulement si chaque source de variance est épinglée — donc Flux les épingle toutes, comme politique du langage plutôt que fardeau de l'auteur.
Ce qui est épinglé.
f64scalaire, pas de SIMD dans le domaine déterministe, pas de réassociation FP. Le réordonnancement vectoriel et les réductions réassociées changent les bits ; le cœur déterministe de Flux refuse les deux, et l'ordre de réduction est fixe.- Les transcendantes (
log,exp,sin,cos,tan,atan,atan2,pow) passent par une seule build libm épinglée — jamais leMathdu moteur hôte, qui diffère légitimement d'un ULP entre moteurs. - Décimal, Unicode, calendrier, PRNG. Les maths monétaires en virgule fixe, les opérations sur chaînes (unités scalaires Unicode, tables de casse épinglées), l'arithmétique calendaire (données de fuseaux horaires épinglées), et le générateur aléatoire à graine (un PRNG entier basé sur compteur) sont chacun une seule routine partagée, utilisée à l'identique par l'interpréteur, le module compilé et tout ré-exécuteur.
- Un
nacanonique. Les valeurs manquantes se comparent comme absentes partout, et à chaque frontière de stockage ou de hachagenaest forcé à un unique motif de bits — de sorte que deux moteurs ne sont jamais en désaccord sur les octets de « rien ».
L'équivalence n'est pas supposée ; elle est vérifiée à chaque compilation en exécutant l'interpréteur et le module compilé sur des données réelles et en affirmant la bit-égalité (invariant I7). Une divergence bloque l'artefact.
Ce que cela vous apporte.
- Débogage exact au rejeu. Reculez aussi fiablement que vous avancez ; chaque valeur est reproductible à la demande.
- Des tests qui veulent dire quelque chose. Un instantané golden correspond exactement, ou bien le programme a changé. Il n'y a aucune tolérance à régler et aucune instabilité à excuser.
- Accord entre machines. Deux appareils — ou un client et un serveur vérificateur — calculent des résultats identiques, ce qui rend honnêtes les artefacts partagés et la vérification indépendante.
En pratique.
fluxr = math.log(close / close[1]) // ratio in, dimensionless out — via the pinned libm, same bits everywhere
x = rand(42) // seeded: deterministic, replayable, admissible in ANALYSIS
L'aléatoire sans graine existe — du côté présentation du pare-feu (pilier 5), où le déterminisme n'est délibérément pas promis.
#4. Kinds dimensionnels
L'énoncé. Chaque flux porte un kind — un type dimensionnel qui enregistre ce que la valeur est physiquement, pas seulement qu'elle est un nombre. Le système de kinds calcule le kind de chaque résultat et rejette les opérations sans signification physique.
Pourquoi. Dans le code analytique, les pires bugs ne sont pas des erreurs de type qu'un
vérificateur conventionnel attraperait — tout est un flottant. Ce sont des erreurs de dimension :
ajouter un prix à un oscillateur, comparer un volume à un ratio, fournir un pourcentage là où un
level est attendu. Tout cela est un non-sens bien typé dans un monde qui ne connaît que les
flottants. Flux redonne aux données leur physique. L'axe des prix est modélisé comme un espace
affine : price est un point, level est un déplacement (vecteur), ratio est un
scalaire sans dimension ; les dimensions forment une algèbre sous les opérateurs. Les conséquences
agréables sont des théorèmes, pas des cas particuliers :
fluxrange = high - low // price − price → level (point − point = vector)
band = sma(close, 20) + 2 * stdev(close, 20) // price + level → price (point + vector = point)
rel = close / open // price ÷ price → ratio (dimensionless)
bad = close + rsi(close, 14) // ✗ [ErrDim] — point + dimensionless: no affine meaning
Ce que cela vous apporte.
- Le non-sens est rejeté à la compilation, avec une explication dimensionnelle et un correctif
suggéré — pas un
NaNmystérieux trois pas plus loin. - La présentation est inférée. Le kind est assez riche pour dériver l'affichage : un
pricese superpose à l'axe des prix ; unosc(0,100)obtient son propre panneau, une échelle fixe et ses lignes de repère ; unsignals'affiche comme des marques. Une ligne de mathématiques se matérialise en un graphique correctement meublé parce que le kind a dit tout ce qu'il fallait. - L'inférence est silencieuse jusqu'à ce que vous ayez tort. Vous n'annotez rien ; les kinds remontent depuis les sources à travers les opérateurs, chaque expression reçoit son kind le plus précis, et la seule fois où vous entendez parler du système, c'est quand il vous sauve.
Les kinds sont la clé de voûte sur laquelle s'appuient les autres piliers : le planificateur de mémoire dimensionne les tampons à partir des kinds, le modèle de coût de l'optimiseur les lit, et la complétion de l'éditeur filtre par eux. Le système complet — sortes, treillis, coercition, algèbre des opérateurs — est spécifié dans Kinds.
#5. Des plans avec un pare-feu à sens unique
L'énoncé. Le calcul et la présentation sont des plans séparés, et la dépendance ne les
traverse que dans une seule direction : la présentation peut lire l'analyse ; l'analyse ne peut
jamais lire la présentation. Les violations sont des erreurs à la compilation : [ErrFirewall].
Pourquoi. Un langage qui promet le déterminisme et veut une sortie plaisante, animée,
interactive a un problème : les écrans, les horloges murales, les pointeurs et l'aléatoire sont
exactement les choses que le déterminisme doit exclure. Les issues habituelles sont sinistres —
soit les garanties s'érodent en douce (« surtout déterministe »), soit la couche de sortie est
affamée jusqu'à l'inertie. Flux refuse le dilemme structurellement. Tout ce qui est non
déterministe — now(), coordonnées d'écran, état de survol, rand/noise sans graine —
existe, mais seulement du côté présentation (CANVAS, TRANSITION), où rien en aval ne dépend de
l'exactitude. Les plans ANALYSIS et APP restent à l'intérieur des garanties. Le mur entre eux est
directionnel et imposé par le compilateur — voir la figure dans Qu'est-ce que Flux.
Ce que cela vous apporte.
- Des garanties qui survivent à la décoration. Un effet animé et aléatoire peut se tenir à quelques pixels d'un signal sans aucune possibilité de le contaminer. Le « no-repaint » tient même dans des programmes pleins d'animation, parce que la dépendance ne peut pas être exprimée.
- La liberté là où c'est sûr. Les auteurs CANVAS obtiennent le temps réel, l'amortissement, les ressorts, le bruit et l'interactivité sans taxe de déterminisme — le plan est honnêtement hors des garanties, et le pare-feu est ce qui rend cette honnêteté abordable.
- Une surface d'audit propre. Le fait qu'un programme soit rejouable est un fait statique à propos du plan dans lequel vivent ses puits, et le compilateur vous le dit.
En pratique.
fluxdot { at:(bar.i, close); r: 4 } // CANVAS reading an ANALYSIS value — the legal direction
x = ema(now(), 20) // ✗ [ErrFirewall] — wall-clock time cannot enter ANALYSIS
La même règle donne à live(e) — la lecture, côté présentation, de l'unité encore en formation —
un foyer sûr : elle peut s'écouler vers des puits d'affichage, jamais dans une analyse confirmée.
Détails dans Les quatre plans.
#6. Sécurité par capacités
L'énoncé. Les scripts n'ont aucune autorité ambiante. Les effets sont des capacités-objet à refus par défaut : un script ne peut affecter le monde qu'à travers des capacités qu'il a déclarées, que l'utilisateur a accordées, et que l'hôte arbitre.
Pourquoi. Flux est le langage d'une plateforme où le code est partagé, vendu et exécuté par des gens qui ne l'ont pas écrit. Cela n'est tenable que si la sécurité est une propriété du langage et de l'hôte, pas d'un processus de revue. Flux y parvient par couches : le langage n'a aucune primitive qui fasse des E/S — pas de fetch, pas d'accès au DOM, pas d'eval, pas de descripteurs de fichiers — de sorte que le seul canal d'un script vers le monde, ce sont les données qu'il remet à l'hôte. Sur le plan APP, ce canal est explicite :
- Les commandes sont des données inertes. Un
Cmdporte des valeurs — un nom de son, une clé de stockage, un score — jamais un token, une URL ou un handle. L'hôte détient chaque ressource et n'exécute la commande que si la capacité correspondante a été déclarée et accordée. Émettre une commande que le manifeste n'accorde pas est rejeté à la compilation ([ErrCapDenied]) — cela ne devient jamais un événement à l'exécution qu'il faudrait attraper. - Les capacités sont des déclarations, pas des valeurs. Ce sont des entrées nommées
namespace:verbdans le descripteur de l'app —chart:read,storage:own,net:fetch— jamais des objets de première classe, donc elles ne peuvent être ni re-déléguées ni amplifiées depuis l'intérieur d'un script. - Les manifestes s'agrègent transitivement, avec zéro escalade. Le manifeste scellé d'un artefact est l'union des besoins en capacités sur toute sa fermeture de dépendances, intersectée avec l'octroi de l'utilisateur. Le besoin d'une dépendance fait surface dans le manifeste de haut niveau avant l'installation, et aucune dépendance ne peut jamais détenir une capacité que l'utilisateur n'a pas accordée à l'ensemble.
fluxapp quiz {
capabilities: [ chart:read, sfx, storage:own ] // everything this app may ever touch
// ... a Cmd like PlaySfx("ding") is data; the host decides whether it runs
}
Ce que cela vous apporte.
- Le code non fiable comme une routine. Installer un artefact est un consentement éclairé à une liste courte et exacte — pas un acte de foi envers un auteur.
- Pas de députés confus. L'autorité ne circule que le long des arêtes d'import, plafonnée par l'octroi ; une bibliothèque ne peut pas blanchir un accès à travers l'app qui l'embarque.
- Honnêteté first-party. Les propres applications de la plateforme s'exécutent sous le même régime avec les mêmes manifestes — la confiance est un niveau d'octroi, jamais un chemin de code.
#7. Optimisable par construction
L'énoncé. Un programme Flux est un DAG pur, typé, total et causal — la forme sur laquelle les optimisations classiques sont sûres par construction. Et l'optimiseur est vérifié à chaque compilation, jamais cru sur parole.
Pourquoi. Dans les langages impurs, les optimiseurs dépensent leur sophistication à prouver qu'une transformation ne peut pas observer un effet — et abandonnent prudemment quand ils n'y arrivent pas. Les programmes Flux n'ont aucun effet à observer : deux calculs quelconques du même sous-graphe pur sont interchangeables, donc le partage, l'élagage, le réordonnancement et la spécialisation n'exigent aucun exploit héroïque. La taille petite et bornée du programme ajoute un second avantage, inhabituel : les recherches sur le graphe entier, infaisables sur de grands programmes, sont abordables ici, donc l'optimiseur peut viser l'optimal plutôt que le « assez bon ».
Le modèle de confiance est la partie distinctive. L'optimiseur obéit à une seule loi : le programme optimisé doit être bit-identique à la sémantique de référence — l'évaluation canonique du DAG non optimisé (ou le noyau natif, pour les primitives intégrées). Cette loi est imposée par la validation de traduction : chaque compilation exécute les artefacts de référence et optimisé sur des données réelles et affirme la byte-égalité. L'optimiseur peut donc être agressif précisément parce que personne n'a à y croire — une compilation erronée ne peut pas être livrée, elle ne peut qu'échouer bruyamment au gate.
Ce que cela vous apporte.
- Partage gratuit. L'élimination des sous-expressions communes est globale : écrivez
ema(close, 26)à quatre endroits dans un script et c'est calculé une seule fois. Post-v1. Le partager entre quatre scripts co-actifs — un seul graphe fusionné pour tout le graphique — est un palier d'optimiseur ultérieur. - Un coût que vous pouvez voir. Le code mort est éliminé, les constantes pliées, les chaînes élément par élément fusionnées ; la gouttière de coût de l'éditeur montre le graphe optimisé, donc ce que vous lisez est ce que vous payez.
- Un défaut sûr et une voie rapide étiquetée. Le défaut est bit-exact, toujours. Post-v1. La virgule flottante relâchée (réassociation, fused multiply-add) est conçue comme un opt-in explicite et étiqueté — jamais un défaut silencieux, et jamais bit-exact.
En pratique.
fluxdef ema0(s, n) = let a = 2/(n+1) in scan(s, (p) -> a*s + (1-a)*p)
def macd0(s) = let l = ema0(s, 12) - ema0(s, 26) in { macd: l, signal: ema0(l, 9), hist: l - ema0(l, 9) }
plot macd0(close).macd, macd0(close).signal // two calls, one shared subgraph — CSE, verified bit-exact
Les paliers, le modèle de coût et le harnais de validation sont spécifiés dans Optimiseur.
#Comment les piliers se composent
Les piliers ne sont pas sept fonctionnalités indépendantes ; chacun s'appuie sur les autres, et l'ensemble des garanties tient parce que la boucle se referme :
- La totalité rend la vérification possible. Chaque analyse statique du langage — inférence de kind, vérification de causalité, exhaustivité, l'oracle de validation de l'optimiseur — se termine parce que les programmes sont totaux et que leurs bornes sont des constantes. Un langage avec des programmes non bornés ne pourrait garantir l'achèvement d'aucune de ses vérifications.
- La causalité fait du graphe un DAG, ce que l'inférence de kind parcourt en une passe, ce que l'ordonnanceur parallélise sans verrous, ce que le débogueur met en point de contrôle et rejoue, et ce que l'optimiseur réordonne sans risque. Un seul théorème d'acyclicité, consommé de quatre façons.
- Le déterminisme a besoin de la totalité et de la causalité — le rejeu n'est exact que si les programmes se terminent et que l'historique est immuable — et a besoin du pare-feu, qui garde le temps réel et l'aléatoire hors du domaine déterministe au lieu de demander aux auteurs d'être prudents.
- Les kinds alimentent tout. L'inférence de présentation (pilier 4), le plan mémoire qui rend les budgets statiques (pilier 1), la classification des puits par le pare-feu (pilier 5) et le modèle de coût de l'optimiseur (pilier 7) lisent tous les mêmes faits dimensionnels.
- La sécurité par capacités repose sur la totalité et la pureté. Le refus par défaut serait du théâtre si un script pouvait boucler indéfiniment, atteindre des E/S ambiantes, ou cacher des effets dans l'ordre d'évaluation. Parce qu'il ne peut faire aucune de ces choses, le manifeste de capacités est réellement l'histoire complète de ce qu'un artefact peut faire.
- La liberté de l'optimiseur est le dividende du déterminisme. « Bit-identique à la référence » n'est une loi utilisable que parce qu'une réponse de référence existe — déterministe, stable à l'octet, sur chaque machine. En échange, l'optimiseur rembourse le budget que la totalité avait promis : le coût que le compilateur a déclaré est le coût que vous observez.
Retirez un seul pilier et les autres s'affaiblissent ; ensemble, ils se referment : décrivez une expression, et le moteur peut l'évaluer — se terminant de façon prouvée, ne réécrivant jamais l'historique, identique partout, divulguant tout ce qu'elle touche, à un coût connu à l'avance.
#Voir aussi
- Qu'est-ce que Flux — le modèle mental et les quatre plans en un coup d'œil.
- Premiers pas — les piliers tels que vous les vivez en écrivant.
- Les quatre plans — le pare-feu en profondeur.
- Kinds — le système dimensionnel derrière le pilier 4.
- Temps et état — délais, horloges,
scanet causalité derrière le pilier 2. - Garanties — chaque promesse et la vérification machine qui l'impose.
Premiers pas
Ce chapitre est une première session guidée. Elle commence par le plus court programme qui fait quelque chose de réel, et le fait grandir — paramètres, style, composition, état, horloges multiples, une scène en mouvement, une petite application — en expliquant ce que le langage a fait pour vous à chaque étape et, tout aussi important, ce qu'il a refusé de faire.
Tout ici est un programme complet. Collez n'importe lequel dans l'éditeur et il s'exécute.
#Une ligne
fluxplot rsi(close, input(14))
C'est un analytique complet et publiable. Rien n'a été configuré, et pourtant :
closeest unprice.rsiest une famille d'oscillateurs bornés, donc l'expression a le kindosc(0,100).- Parce que le kind est un oscillateur borné, il ne peut pas partager l'axe des prix — il
obtient son propre panneau, avec une échelle 0–100 fixe et une ligne médiane. Parce que
l'opération est spécifiquement
rsi, ses repères conventionnels 30/70 sont tracés aussi. - Parce que vous avez écrit
input(14), un contrôle de paramètre apparaît, typé et doté d'une plage. - Parce que le programme est causal et total par construction, il est aussi rejouable, borné, et byte-identique sur chaque moteur — ce que le panneau des garanties vous dira sans qu'on le lui demande.
Supprimez input(…) et écrivez plot rsi(close, 14) : même analytique, pas de contrôle.
L'enveloppe input est la façon dont une valeur devient un bouton de réglage.
Ce qui vient de se passer. Vous n'avez choisi ni panneau, ni échelle, ni couleur, ni ligne de référence. Le kind de l'expression a tout porté. C'est la plus grande conséquence ergonomique d'un système de types dimensionnel, et elle vaut pour tout ce que vous écrirez ensuite.
#Paramètres et style
input accepte une valeur par défaut, une plage optionnelle, et des métadonnées optionnelles :
fluxlen = input(14, 2..200, title: "Length")
src = input(close, title: "Source")
show = input(true, title: "Show band")
plot rsi(src, len)
Le kind de la valeur par défaut décide du widget : un nombre donne un champ numérique (une plage
en fait un curseur), close donne un sélecteur de source, true donne une case à cocher, une
liste de chaînes donne une énumération.
Les défauts de présentation sont inférés, mais l'intention gagne toujours :
fluxm = macd(close)
plot m.hist { style: histogram, color: if m.hist > 0 then up else down }
plot m.macd, m.signal
plot ema(close, 200) { overlay } // it is already a price — this is explicit
plot rsi(close, 14) { guides: [20, 80] } // your own reference lines, kind-checked
Les valeurs de style forment un ensemble fermé (histogram, columns, stepline, area,
circles, cross) ; une ligne est le défaut qu'un level ou un price infère. Forcer un
level sur le graphique avec { overlay } lui donne son propre axe secondaire — le
compilateur sait qu'une échelle de prix partagée l'aplatirait à néant.
#Composition
def définit une fonction pure de flux vers flux. Elle est inlinée dans le graphe, il n'y a donc
aucun coût d'appel à considérer :
fluxdef zscore(x, n = 20) = (x - sma(x, n)) / stdev(x, n)
plot zscore(close) // (price − price) ÷ level = level ÷ level → ratio
plot zscore(hlc3, 50) // the same def on another price source — the kinds follow the argument
Toute fonction peut s'écrire comme une chaîne façon méthode — le receveur devient le premier argument — ce qui est la manière dont la plupart des gens finissent par écrire des analytiques :
fluxsmoothRsi = close.ema(20).rsi(14) // ≡ rsi(ema(close, 20), 14)
Le gain n'est pas la brièveté. Après avoir tapé close., l'éditeur ne propose que les fonctions
dont le premier paramètre accepte un price — le système de types devient un mécanisme de
découverte.
Plusieurs noyaux renvoient un enregistrement, et vous projetez le champ voulu :
fluxbb = bollinger(close, 20, 2)
plot bb.upper, bb.middle, bb.lower
fill bb.upper..bb.lower // a band: both operands are `price`, so the fill is well-formed
fill bb.upper..rsi(close,14) serait ✗ [ErrDim] — vous ne pouvez pas ombrer la région entre un
prix et un oscillateur sans dimension, et le langage le dit plutôt que de dessiner un non-sens.
#Signaux, marques et alertes
Une comparaison produit un signal, et un signal est présenté comme des marques — jamais comme
une ligne :
fluxcross = close cross_up ema(close, 50)
mark cross "crossed at {fmt.price(close)}"
alert cross "EMA-50 crossed up"
Les chaînes s'interpolent, de sorte qu'une étiquette ou un message d'alerte peut porter des
valeurs en direct. cross_up est un opérateur infixe : il est vrai sur la barre où le côté gauche
franchit le côté droit vers le haut, et sa définition — comme tout le reste — ne lit que des
données clôturées.
#État
Vous n'écrivez jamais de boucle sur les barres. Quand une valeur dépend de son propre passé, vous
écrivez un scan : une graine, et un pas qui reçoit l'état précédent.
flux// a running maximum since the start
def runMax(x) = scan(x, (prev) -> math.max(prev, x))
// a trailing stop that only ever ratchets upward while long
def trail(mult) =
let stop = close - mult * atr(14) in
scan(stop, (prev) -> math.max(prev, stop))
plot trail(3) // price − lit × level = price → it overlays
La fonction de pas voit prev — l'état à la barre précédente — et les valeurs de la barre
courante. C'est tout le modèle : l'état avance d'un pas par barre, à travers un délai unitaire, ce
qui est précisément pourquoi une valeur une fois émise ne peut jamais être réécrite.
Un état composite est un enregistrement, et une machine à états est un variant plus un match :
fluxvariant Trend { Up | Down }
def step(p, n) = match p.dir {
Up -> if close < p.ref - atr(n) then { dir: Trend.Down, ref: close } else p
Down -> if close > p.ref + atr(n) then { dir: Trend.Up, ref: close } else p
}
def flip(n) = scan({ dir: Trend.Up, ref: close }, (p) -> step(p, n))
match est l'éliminateur d'un variant, et il doit couvrir chaque constructeur — un cas non
couvert est [ErrTotalMatch], une erreur de compilation, pas une surprise à l'exécution.
#Plus d'une horloge
L'axe de pas d'une série est une valeur, de kind clock. @ lit une expression sur une autre
horloge, et il n'en lit que les unités clôturées :
fluxplot ema(close, 20) @ tf("1h") // hourly EMA, shown on whatever chart you are on
color bars: if close > ema(close, 50) @ tf("1d") then up else down
Cette seconde ligne est l'idiome de confluence : un graphique rapide, coloré par une tendance lente. Ce n'est pas une fonctionnalité spéciale — c'est le rééchantillonnage ordinaire, et il est causal, donc ce qu'une barre montrait hier, elle le montre encore aujourd'hui.
Les axes de pas pilotés par le prix sont aussi des horloges (renko(box), pnf(box, rev),
range(r)), ce qui explique pourquoi les représentations de graphique alternatives ne sont pas un
sous-système séparé : ce sont une horloge différente.
#Une scène qui bouge
La présentation vit sur son propre plan, où chaque propriété est un signal — une constante, une valeur de données et une animation sont la même sorte de chose, il n'y a donc pas d'API d'animation à apprendre :
fluxcircle {
at: (bar.i, spring(close)), // the position eases toward the data
r: 6,
glow: 16,
trail: 24
}
on close cross_up highest(close, 250)[1] -> burst(40) ring { r: 6 -> 24, life: 2s }
La scène peut lire des valeurs d'analyse. L'analyse ne peut pas lire la scène — cette règle à
sens unique est ce qui empêche les parties mobiles de jamais toucher les nombres. Essayez-le et
vous obtenez [ErrFirewall], avec une explication.
#Une petite application
Le quatrième plan ajoute un état qui persiste entre les événements et décide ce qui est affiché — un modèle, une update pure, une view pure, et des souscriptions déclaratives. Les effets sont des données inertes que l'hôte exécute ; les capacités sont à refus par défaut.
fluxvariant Msg { Tick | Reset }
app counter {
capabilities: [ ]
init(p) = { n: 0 }
update(m, msg) = match msg {
Tick -> { model: m with { n: m.n + 1 }, cmds: [] }
Reset -> { model: m with { n: 0 }, cmds: [] }
}
view(m) = row {
text("count: {m.n}")
button("reset", Reset)
}
subs(m) = [ OnTick(1000, Tick) ]
}
OnTick(1000, Tick) se lit « chaque seconde, applique le constructeur Tick ». Une souscription
porte le constructeur dans lequel l'hôte enveloppera sa charge utile — c'est ainsi qu'un
événement sait à quelle branche d'update il appartient, sans qu'aucune valeur de fonction
n'entre jamais dans le langage.
update est pure et totale : elle ne peut pas lire l'horloge, ne peut pas atteindre le réseau, et
doit gérer chaque message. Tout ce qui est ambiant — temps, aléatoire, données, entrée — arrive
sous forme de message, ce qui est exactement pourquoi une application peut être rejouée message
par message et pourquoi ses tests sont des goldens sur des fonctions pures.
#Ce que fait l'éditeur pendant que vous tapez
Complétion filtrée par kind après ., une carte de survol avec la signature et une sparkline en
direct de l'expression sur les données courantes, des diagnostics avec correctifs rapides, un
aperçu qui réévalue la partie typable de votre programme à chaque frappe (un nom à moitié tapé
laisse une seule valeur en blanc, jamais l'écran), et une vue dataflow qui répond « pourquoi ce
signal est-il vrai ici ? ». Voir l'éditeur.
#Voir aussi
- Les quatre plans — le pare-feu en profondeur, et à quoi sert
live(). - Kinds — le système de types qui a produit chaque défaut ci-dessus.
- Temps et état — délais, fenêtres,
scan, horloges, préchauffe. - Inférence — la table de présentation, les surcharges, le catalogue d'erreurs.
- Livre de recettes — des recettes qui marchent à travers chaque plan.
- Vue d'ensemble du FDK — les bibliothèques : compute, collections, text, net, display.
Les quatre plans
Un programme Flux n'est pas une seule sorte de chose. Calculer un indicateur, dessiner une comète qui suit le prix, faire morpher un graphique quand l'asset change, et faire tourner une application à état dans un panneau sont quatre activités différentes avec quatre horloges différentes et quatre jeux de règles différents. La plupart des langages vous donnent un seul plan et vous demandent d'être prudent. Flux vous en donne quatre, et fait de la frontière entre eux une propriété du système de types.
Le gain est une phrase qui vaut la peine d'être lue deux fois : une valeur peut être animée, aléatoire et dépendante de la frame, tout en restant, de façon prouvée, incapable de changer un nombre calculé. Pas par convention. Par construction.
Une note sur les exemples. Flux n'a pas d'expressions-instructions, donc une expression nue n'est pas un programme. Les lignes marquées
✗sur cette page sont donc des fragments d'expression : ils existent pour montrer ce que les règles de kind refusent, pas ce que l'analyseur accepte. Chaque ligne non marquée est une instruction légale.
#ANALYSIS — le plan qui calcule
| Horloge | la barre — elle n'avance que sur des données clôturées |
| Garanties | total, causal, déterministe, en bac à sable, no-repaint |
| Ce qui vit ici | indicateurs, signaux, transformations de représentation, les nombres sur lesquels repose une décision |
Le plan d'analyse est le plus contraint, et donc le plus digne de confiance. Tout ce qui s'y trouve est une fonction pure du passé : les délais ne regardent qu'en arrière, le rééchantillonnage ne lit que les unités clôturées, la rétroaction doit traverser un délai unitaire. Ce qui suit n'est pas une promesse mais un théorème — une valeur, une fois produite pour une barre, ne peut jamais changer.
fluxplot rsi(close, 14)
plot ema(close, 20) @ tf("1h") // a coarser clock — still causal
mark close cross_up ema(close, 50)
L'analyse ne lit rien des plans au-dessus d'elle. Il n'y a aucun symbole pour la souris, pour l'horloge murale, pour la frame courante, ou pour savoir si le mode 3D est activé. Pas « c'est une mauvaise pratique » — ces noms n'existent pas dans l'espace de noms de l'analyse.
#CANVAS — le plan qui montre
| Horloge | la frame |
| Autorisé | espace écran, temps de l'horloge murale, aléatoire — explicitement hors des garanties |
| Ce qui vit ici | scènes, dessins animés, décoration, effets |
Le plan canvas peut lire l'analyse. Il ne peut pas l'écrire.
fluxcircle {
at: (bar.i, spring(close)), // reads analysis; the easing is cosmetic
glow: 16 + 8 * throb(0.4), // per-frame, time-only — the compositor owns it
trail: 24
}
on close cross_up highest(close, 250)[1] -> burst(40) ring { life: 2s }
Chaque propriété est un signal — une constante, une valeur de données et une animation sont ici la même sorte de chose, il n'y a donc pas d'API d'animation à apprendre. Et parce que le compilateur sait quels signaux sont pilotés par le temps, il les route vers le compositeur de l'hôte : zéro JavaScript par frame pour les parties qui bougent le plus.
#TRANSITION — le plan qui interpole
| Horloge | la frame |
| Règle | il interpole le rendu entre deux états déjà calculés |
| Conséquence | il est cosmétique par définition — il ne peut pas changer une valeur, donc il ne peut pas repeindre |
fluxon switch(asset) -> morph chart over 500ms { ease: inOutCubic ; stagger: 0.3 }
on click -> focus(view, at: (bar.i, close), zoom: 2.0, over: 600ms)
La valeur de stabilisation d'une transition — l'endroit où elle atterrit — est une donnée
d'analyse et vit dans l'oracle. Sa trajectoire — la façon dont elle y arrive — est cosmétique
et n'y vit pas. C'est pourquoi prefers-reduced-motion peut sauter directement à l'état final et
ne rien changer du tout dont dépende un quelconque verdict.
#APP — le plan qui se souvient
| Horloge | événements |
| Forme | un Model borné, une update pure, une view pure, des subs déclaratives |
| Effets | des données de commande inertes que l'hôte exécute, sous des capacités à refus par défaut |
Post-v1. Le plan APP est scellé dans la conception et additif au cœur ; son déploiement suit le langage v1.
Les trois autres plans ne peuvent pas détenir un état qui persiste entre les événements et décide ce qui est affiché. Le plan APP ajoute exactement cela, et le paie avec une recette stricte : tout ce qui est ambiant — temps, entrée, aléatoire, réseau, valeurs d'analyse — arrive sous forme de message, et le journal de messages est l'unique source de vérité. C'est ce qui rend une application rejouable, testable sans mock, et ré-exécutable par un serveur bit pour bit.
Voir Plan APP pour le contrat complet.
#Le pare-feu
Une seule règle tient toute la conception ensemble :
Les flèches de dépendance ne pointent jamais vers une garantie plus faible.
La présentation peut lire l'analyse. L'analyse ne peut jamais lire la présentation. Le plan APP peut lire l'analyse (en lecture seule, via une souscription typée) et peut orchestrer la présentation (via des commandes) — mais il ne peut jamais écrire l'analyse non plus.
APP (mutable state + effects) ← the most permissive plane
│ reads ANALYSIS (Sub OnSeries) ✔ read-only
│ orchestrates CANVAS / TRANSITION (Cmd) ✔
▼
CANVAS / TRANSITION (cosmetic, per frame) ← reads ANALYSIS ✔
▼
ANALYSIS (pure, causal, no-repaint) ← reads nothing above it ✘
Ce que le pare-feu interdit réellement est précis. Voici les valeurs qui ne peuvent jamais s'écouler dans l'analyse :
| Interdit dans l'analyse | Pourquoi |
|---|---|
screen.*, hover, le pointeur |
l'espace écran n'est pas une donnée ; il varie selon l'appareil |
now(), l'horloge murale |
elle n'est pas rejouable, et elle ferait dépendre une valeur passée du moment où vous avez regardé |
rand, noise sans graine |
non déterministe ⇒ deux moteurs sont en désaccord |
live(e) |
elle lit la barre en formation — la seule chose qui peut encore changer |
Tous les quatre lèvent [ErrFirewall], à la compilation, avec une explication plutôt qu'une
réprimande.
Pourquoi cela mérite une séparation de plans plutôt qu'un lint. Une discipline dont vous devez vous souvenir est une discipline que vous oublierez à 2 heures du matin sous la pression d'une échéance. Un pare-feu imposé par le système de kinds en est un que vous ne pouvez pas oublier : le nom n'est pas dans la portée, et le compilateur ne laissera pas la valeur traverser. C'est ce qui rend sûr d'exécuter la scène animée, aléatoire et interactive d'un inconnu juste à côté du nombre sur lequel repose votre décision.
#live() — la seule exception, et pourquoi elle est sûre
Les traders veulent voir un indicateur se mettre à jour à l'intérieur de la barre en formation. Cette lecture est réellement utile et réellement non causale, donc Flux lui donne un nom, un plan, et un mur :
fluxplot live(ema(close, 20)) // ✓ display — the forming bar included, per frame
alert live(ema(close, 20)) > 100 // ✗ [ErrFirewall] — a decision may not read a forming value
rsi(live(close), 14) > 70 // ✗ [ErrFirewall] — analysis may not consume one either
live(e) réévalue le sous-graphe d'analyse de e en incluant la barre en formation, à chaque
frame. Son résultat ne peut s'écouler que vers des puits d'affichage (plot, mark, fill,
color bars, une scène). Tout puits confirmé — une alerte, une assertion, une valeur qu'un calcul
consomme — est [ErrFirewall].
Notez où live se place, car les deux emplacements ne sont pas des variations sur un même
thème. Il enveloppe l'expression dont le sous-graphe doit être réévalué : live(ema(close, 20))
demande la moyenne incluant la barre en formation, et atterrit dans un puits d'affichage. Poussé
vers l'intérieur, sur l'argument d'un noyau — ema(live(close), 20) — il cesse d'être une requête
d'affichage et devient une valeur en formation remise à un calcul, ce qui est la brèche elle-même.
Le pare-feu se moque qu'un plot attende à l'autre bout : le noyau d'analyse l'a déjà consommée.
Trois conséquences, qui comptent toutes :
- La série confirmée de
ereste byte-identique.live()ajoute une vue provisoire ; il ne modifie pas ce qui a été calculé. live()est exclu de l'oracle de byte-identité, exactement comme l'horloge murale — de sorte que la garantie que les deux moteurs concordent est intacte.- Un script qui l'utilise est marqué non rejouable dans le panneau des garanties. Vous voyez le compromis que vous avez fait.
C'est la forme générale de chaque échappatoire dans Flux : la nommer, la borner, la murer, et montrer à l'utilisateur ce qu'elle a coûté.
#Sur quel plan suis-je ?
Vous n'en déclarez jamais un. Le plan est inféré de ce que vous écrivez — c'est tout l'intérêt de « écrivez les maths, la machinerie suit » :
| Vous écrivez | Le plan |
|---|---|
plot, mark, fill, alert, assert, une expression d'indicateur |
ANALYSIS |
une primitive avec props, on … -> …, scene{…}, group, repeat |
CANVAS |
morph, focus, replay |
TRANSITION |
un bloc app |
APP |
Et si vous essayez de les mélanger d'une façon que le pare-feu interdit, le compilateur vous dit quelle valeur a franchi quelle ligne, et quoi faire à la place.
#Voir aussi
- Plan APP — le contrat applicatif complet.
- Canvas — signaux, espaces, événements, primitives, le modèle de performance.
- Transitions — morph, replay, focus, et le descripteur de transition.
- Temps et état — causalité, horloges,
live()en profondeur. - Garanties — ce que chaque plan promet, et comment c'est vérifié.
- display — les deux strates, et où s'arrête le déterminisme de présentation.
Structure lexicale
Cette page définit comment un texte source Flux devient un flux de tokens : le modèle de source, les commentaires, le saut de ligne significatif, le catalogue complet des tokens (identifiants, la famille numérique et ses suffixes collés, chaînes et interpolation, références de capacités, opérateurs et ponctuation), et le modèle de mots-clés — quels mots sont réservés, où, et pourquoi la plupart ne sont réservés que contextuellement. Tout ce qui vient ensuite — la grammaire, l'inférence de kinds, l'éditeur — consomme exactement le flux de tokens spécifié ici.
Deux propriétés cadrent tout le chapitre. D'abord, le lexer est total : tout texte en entrée produit un flux de tokens (une entrée mal formée se manifeste par des diagnostics précis, jamais par un crash). Ensuite, le lexer est linéaire et incrémental : chaque dispositif dépendant du contexte ci-dessous (le saut de ligne significatif, l'interpolation, les références de capacités, la consommation numérique) est borné par des compteurs que le lexer maintient déjà, de sorte que retokeniser après une édition ne touche que le voisinage édité — la propriété qui maintient l'aperçu en direct dans son budget de frame.
#Modèle de source
Un programme Flux est un texte Unicode. L'alphabet fixe du langage lui-même — identifiants, mots-clés, opérateurs, ponctuation — est en ASCII ; le texte arbitraire (tout Unicode) vit à l'intérieur des littéraux de chaîne et des commentaires. Entre les tokens, espaces et tabulations sont insignifiants et peuvent apparaître en nombre quelconque ; les sauts de ligne sont significatifs (voir TERM ci-dessous).
Une règle façonne de nombreux diagnostics de cette page : Flux n'a pas de juxtaposition. Deux expressions primaires adjacentes sans opérateur entre elles ne forment jamais un terme :
fluxx = 1.50 d // ✗ syntax error — NUMBER then IDENT: two adjacent primaries
y = 1.50d // decimal(2) — the glued suffix makes this ONE token
d = 2 // `d` alone is an ordinary identifier — a licit binding
z = 1.50 * d // valid — the detached `d` is just a name here
Parce que l'adjacence n'a jamais de sens, le lexer peut se permettre des tokens de suffixe collés
(1.50d, 4px) sans ambiguïté : soit le suffixe touche le nombre et la paire est un seul token,
soit ce n'est pas le cas et le programme est mal formé à moins qu'un opérateur n'intervienne.
Un script peut s'étendre sur plusieurs fichiers source .flux ; le modificateur de visibilité
package (voir grammaire — modules) est cantonné exactement
à cet ensemble de fichiers.
#Commentaires
| Forme | Token | Étendue | Rôle |
|---|---|---|---|
// … |
LINE | jusqu'en fin de ligne | commentaire ordinaire, ignoré |
/// … |
DOC | jusqu'en fin de ligne | doc-commentaire, s'attache au def suivant |
flux/// z-score of a series over n bars
def zscore(x, n=20) = (x - sma(x, n)) / stdev(x, n)
plot zscore(close, 20) as z // an ordinary comment
Un doc-commentaire est lexicalement un commentaire (le parser l'ignore) mais il n'est pas jeté : le
pipeline de documentation collecte le bloc /// qui précède un def et le publie comme la
documentation au survol de la définition et son entrée doc-as-data (voir
l'éditeur). Il n'y a pas de commentaires de bloc ; un commentaire se termine
toujours au saut de ligne. Les commentaires sont transparents aux règles de saut de ligne
significatif ci-dessous — une ligne qui se termine par un commentaire // … en fin de ligne est
classée par le dernier token avant le commentaire, et une ligne uniquement de commentaire ne
termine ni ne prolonge une instruction.
#Le saut de ligne significatif (TERM)
Flux n'a pas de terminateur d'instruction obligatoire. Une instruction se termine à la fin de sa
ligne — le lexer émet un token TERM au saut de ligne — sauf si la ligne est visiblement
inachevée. Les points-virgules sont des séparateurs optionnels (jamais requis) ; écrire plusieurs
instructions sur une ligne avec ; est licite, mais le Flux idiomatique est d'une instruction par
ligne.
Un saut de ligne n'émet pas de TERM (l'instruction continue) quand l'une des trois clauses
est vraie :
-
(a) Parenthèse ou crochet ouvrant. La profondeur d'imbrication
(/[est supérieure à zéro. À l'intérieur des parenthèses et des crochets, les sauts de ligne sont du pur blanc — c'est ainsi qu'un long appel se répartit sur plusieurs lignes. Les accolades ne réinitialisent pas cette profondeur : un corps entre accolades écrit à l'intérieur d'un appel est toujours à une profondeur > 0, donc ses éléments ont besoin d'un séparateur explicite,ou;. À la profondeur zéro, un saut de ligne sépare les éléments, ce qui explique qu'un record ou unmatchmulti-ligne au niveau supérieur n'a besoin d'aucune virgule :flux
def f(r) = r.a + r.b m = { a: 1 // depth 0 — the newline separates b: 2 } n = f({ a: 1 ; b: 2 }) // inside a call — the `;` is REQUIRED -
(b) La ligne est en attente. Le dernier token significatif de la ligne est un opérateur, un délimiteur ouvrant, un séparateur, ou un mot-clé qui attend grammaticalement un opérande —
if,then,else,let,in,not,and,or,as,from,with,over,def,match,variant,record,app,import,type,representation,tool,on,every,when,tween,spawn,burst,emit,rate,set,morph,replay,focus,cross_up,cross_down,pub,private,package,scene. Un mot-clé contextuel ne compte ici que dans son rôle de mot-clé : dansx = p.on, leonfinal est un nom de champ et l'instruction se termine. (Une subtilité lexicale : un%final collé à un chiffre est un littéral pourcentage, pas un modulo en attente —50%termine la ligne.) -
(c) Le token suivant est un continuateur. Le premier token de la ligne suivante ne peut qu'étendre une expression, jamais commencer un nouvel élément.
La clause (c) n'est pas une liste ad hoc. Définition normative : un token est un continuateur
ssi il n'appartient à aucun ensemble FIRST d'un quelconque élément de liste — formellement, ssi
il est hors de ⋃ FIRST(item) pris sur chaque production de liste répétée de la grammaire
(instructions, enfants de vue, constructeurs de variant, hooks de representation et de tool, bras
de match, champs de record, références de capacités, membres d'app). L'ensemble est calculé à
partir de la grammaire, pas maintenu à la main, ce qui rend la règle vérifiable par machine et la
maintient pour toujours en phase avec la grammaire.
À titre d'illustration, les continuateurs sont les tokens purement infixes ou postfixes — .
(accès membre), .., @, +, *, /, %, <, >, <=, >=, ==, !=, cross_up,
cross_down, and, or, ->, with, ?, ??, ?., :, ,, ;, les fermeurs ) ] },
le séparateur de variant | — et les mots-clés purement médians then, else, in, as,
from.
Trois tokens ne sont délibérément pas des continuateurs bien qu'ils puissent étendre une
expression, parce qu'ils peuvent aussi commencer un nouvel élément : - (moins unaire), [ (un
littéral de liste) et ( (une forme parenthésée). Un . en tête n'est un continuateur que s'il
n'est pas suivi d'un chiffre — .upper continue, mais .5 est un nombre et commence un élément.
Pour continuer d'une ligne à l'autre sur l'un de ces tokens ambigus, terminez plutôt la ligne
précédente en attente (clause b) :
fluxu = bollinger(close, 20)
.upper // `.` continues the postfix chain — clause (c)
total = close
+ open - // `+` continues (c); trailing `-` leaves the line pending (b)
low
cond = if close > open
then 1
else 0 // medial keywords are continuators — clause (c)
variant Wide { A | B
| C } // `|` is a continuator inside the declaration
multi = sma(
close,
20) // clause (a): inside ( ) newlines are whitespace
m = { a: 1 }
updated = m
with { a: 2 } // `with` is a continuator — postfix record update
L'idiome de la clause (b) pour les têtes ambiguës : a -⏎ b (la ligne se termine sur l'opérateur),
v[⏎ i] (la ligne se termine sur l'ouvreur). De même, over n'est pas un continuateur — en tant
que mot-clé contextuel, il peut commencer un élément (par exemple comme clé de propriété) — donc un
morph … over d multi-ligne se coupe après over, qui est un mot en attente au titre de la
clause (b).
Pourquoi cette règle existe. Une syntaxe sans terminateur se lit comme la notation que les auteurs esquissent réellement, mais elle ne doit jamais devenir une devinette sur les blancs. Les trois clauses sont décidables avec au plus un token de lookahead et les compteurs que le lexer porte déjà, de sorte que la classification des sauts de ligne est en O(1) par ligne, linéaire sur le fichier, et incrémentale sous les éditions. Et parce que l'ensemble des continuateurs est défini comme le complément de l'ensemble calculé des têtes d'éléments, il n'y a aucune liste curée à la main qui se désynchronise quand la grammaire grandit : ajouter une tête d'instruction la retire automatiquement des continuateurs.
#Catalogue des tokens
L'inventaire complet des tokens. Chaque classe est détaillée dans les sections qui suivent.
| Classe | Tokens | Notes |
|---|---|---|
| Identifiant | IDENT |
[A-Za-z_][A-Za-z0-9_]*, non réservé à sa position |
| Nombres | NUMBER |
formes entière, décimale, point en tête, exposant |
| Littéraux suffixés | DUR PCT PX DEC SPAN RATE |
suffixes collés ; SPAN autorise un espace |
| Booléens / absence | true false na |
littéraux typés (signal ; na habite tout kind) |
| Chaînes | STRING |
"…" ou '…', une seule ligne |
| Interpolation | STR_HEAD STR_MID STR_TAIL |
tokens fragments de "… {e} …" |
| Réf de capacité | CAPREF |
namespace:verb, uniquement dans les listes de capacités |
| Range / flèche | .. -> |
un token chacun |
| Comparaison | < > <= >= == != cross_up cross_down |
niveau non associatif |
| Additif / multiplicatif | + - · * / % |
- aussi unaire |
La famille ? |
?? ?. ? |
maximal munch, dans cet ordre |
| Ponctuation | @ . , : ; = ( ) [ ] { } ~ | |
| sépare uniquement les constructeurs de variant |
| Commentaires | LINE DOC |
//, /// |
| Disposition | TERM |
saut de ligne significatif |
#Identifiants
IDENT = [A-Za-z_] [A-Za-z0-9_]*
Les identifiants sont en ASCII : une lettre ou un underscore, puis des lettres, chiffres et
underscores. Un identifiant est un nom pour une liaison, un paramètre, un champ, un kind, une
étiquette d'argument, un module ou une déclaration. L'underscore seul _ est lexicalement un
identifiant ordinaire avec deux rôles bénis que lui confèrent la grammaire et l'élaborateur : le
motif joker dans les bras de match, et le placeholder implicite à paramètre unique en position
d'expression (vec.map(_ * 1.1), voir operators).
Le fait qu'un identifiant donné soit disponible dépend du modèle de mots-clés ci-dessous — la
plupart des mots-clés de Flux sont contextuels, donc des mots comme render, view ou color
restent utilisables comme noms de champs, paramètres et noms de kinds.
#Nombres
NUMBER = ( digits "." digits | "." digits | digits ) [ ("e"|"E") ["+"|"-"] digits ]
42, 2.5, .5 et 1.5e3 sont tous des tokens NUMBER. Un NUMBER nu a le kind lit — le
littéral replié en constante (const-folded) qui est polymorphe en dimension (close + 10 est un
price ; voir kinds).
Maximal munch, borné par la règle du point. Le scanner de nombres ne consomme jamais un .
suivi d'un autre . ou d'un non-chiffre. Cette seule règle fait que les ranges et l'accès membre
se composent avec les nombres sans séparateurs :
fluxlen = input(14, 2..200) // NUMBER RANGE NUMBER — the dot is never eaten before `..`
half = .5 // a leading-dot NUMBER
band = (2.5..3.5) // NUMBER RANGE NUMBER — a range lives in its own slots
u = bollinger(close, 20).upper // `.upper` is member access, not a malformed number
sci = 1. // exponent form
neg = -0.5 // unary minus applied to NUMBER (the sign is not part of the token)
#Littéraux suffixés
Six classes de littéraux portent leur unité comme un suffixe collé au nombre (sans espace) ;
SPAN seul accepte aussi exactement un espace. Chaque token est typé à la position d'expression par
la règle [LitTyped] du système de kinds :
| Token | Forme | Exemples | Kind |
|---|---|---|---|
DUR |
NUMBER s | ms |
2s, 300ms, 1.5s |
duration |
PCT |
NUMBER % |
50%, 0.5% |
ratio |
PX |
NUMBER px |
4px, 12px |
num taggé px (espace écran) |
DEC |
NUMBER d |
1.50d, 1.5e3d |
decimal(scale) |
SPAN |
NUMBER [ ] bar | bars |
200 bars, 1 bars, 3bar |
barspan |
RATE |
NUMBER /s | /min |
40/s, 3/min |
num·T⁻¹ (taux par unité de temps) |
fluxb = 300s + 20ms // duration arithmetic
c = 50% // ratio
d = 4px // screen-space size (CANVAS styling)
r = 40/s // emission rate
sp = life(200 bars) // barspan — unifies every(n bars) and life: n bars
a = 1.50d // decimal, scale 2 — exact money arithmetic
La règle du d collé (représentative de toute la famille). Le suffixe d doit toucher le
nombre, et le caractère après le suffixe ne doit pas prolonger un identifiant :
fluxa = 1.50d // DEC(1.50, scale 2) — one token
b = 1. // DEC — the suffix composes with scientific notation
c = 1.50 d // ✗ syntax error — NUMBER IDENT, two adjacent primaries (no juxtaposition)
d = 2 // a detached `d` is an ordinary identifier — a licit binding
d2 = 1.50 * d // valid — 1.50 times the bound `d`
Le même contrôle de frontière protège chaque suffixe : 2se n'est pas un DUR (le e final
prolongerait un identifiant, donc le lexer produit NUMBER(2) puis IDENT(se), ce que le parser
rejette comme juxtaposition), et 40/sec n'est pas un RATE (c'est 40 / sec, une division par
l'identifiant sec). L'échelle (scale) du DEC est le nombre de chiffres décimaux écrits :
1.50d est decimal(2), 2d est decimal(0).
Pourquoi des suffixes collés. Les unités sur les littéraux auraient pu être des tokens séparés (
1.50 d) ou des appels de constructeur (dur(300)), mais les deux écritures placent une analyse entre le nombre et son unité, et le mot détaché entrerait en collision avec des identifiants ordinaires —d,setbarssont tous des noms de liaison raisonnables. Coller rend l'unité partie du token, de sorte que la décision est prise par le lexer sans aucun impact sur la grammaire :1.50dne peut jamais être mal lu,dseul n'est jamais volé à l'auteur, et chaque littéral arrive à l'inférence en portant déjà son kind. Le seul relâchement —200 barsavec un seul espace — est accepté parce quebarsy est une tête réservée et que la forme span se lit comme de la prose dans les positionsevery(…)etlife:.
Décision ouverte. Le plan conserve SPAN, PX, RATE et l'opérateur binaire % en v1 et les
signale pour ratification finale ; ils sont documentés ici comme conservés.
#Chaînes
STRING = '"' frag '"' | "'" frag "'"
Les deux délimiteurs sont équivalents — Flux n'a pas de type caractère, donc l'apostrophe est libre
d'être un délimiteur de chaîne. Un littéral de chaîne a le kind string et doit se fermer sur la
ligne où il s'est ouvert (un saut de ligne brut dans une chaîne est une erreur de syntaxe). À
l'intérieur d'une chaîne, un antislash rend le caractère suivant littéral : \" dans une chaîne
"…", \{ et \} pour des accolades littérales (voir interpolation ci-dessous).
fluxplain = "no interpolation"
single = 'quote style'
alert close cross_up open "crossed" // strings feed the text channels (labels, messages)
Les chaînes sont des valeurs du kind catégoriel string — du texte borné et immuable pour les
étiquettes, les invites et les messages. Elles ne sont jamais tracées comme une série, et + sur
deux chaînes concatène (la seule surcharge catégorielle de + ; voir operators).
#Interpolation de chaînes
Un { dans une chaîne ouvre un trou d'interpolation contenant une expression Flux complète.
Lexicalement, le littéral est découpé en tokens fragments :
STR_HEAD = '"' frag '{' (opening fragment — also with ' delimiter)
STR_MID = '}' frag '{' (middle fragment)
STR_TAIL = '}' frag '"' (closing fragment)
interpStr = STR_HEAD expr { STR_MID expr } STR_TAIL
Une chaîne ne contenant aucun { non échappé reste un token STRING atomique. \{ et \}
dénotent des accolades littérales à l'intérieur d'un fragment. Le tokeniseur d'interpolation apparie
son délimiteur fermant à l'ouvrant ("…" ou '…'), et le { d'un fragment ouvre un mode de lexer
borné par le compteur d'accolades que le lexer tient déjà — de sorte que les trous s'imbriquent à
n'importe quelle profondeur où le programme lui-même peut s'imbriquer, et la tokenisation reste
linéaire et incrémentale.
fluxm = { a: 0 }
x = close
mark close > open "close {close} above {str(open)}"
label = "nested {(m with { a: 1 }).a} brace" // a full expression, inner braces counted
single = 'quote {x} style' // interpolation works in '…' too
lit = "a literal \{brace\}" // escaped — no hole opened
Un littéral interpolé a le kind string. Son AST est une concaténation des fragments, l'expression
de chaque trou étant formatée par le formateur canonique de son kind (fmt.* — épinglé, identique
sur chaque cible d'exécution), de sorte qu'une étiquette de mark ou d'alert peut être dynamique sans
aucun code de formatage répétitif. Voir text pour les règles de formatage.
#Références de capacités (CAPREF)
CAPREF = IDENT ":" IDENT (only inside a capability list)
Un descripteur du plan APP déclare ce qu'il peut toucher sous forme d'une liste de références de
capacités — des paires namespace:verb comme chart:read, storage:own, levels:write, ou un
simple IDENT pour les capacités à token unique (sfx) :
fluxapp structureGame {
capabilities: [chart:read, storage:own, levels:write, sfx, app:launch]
// …
}
CAPREF n'est délibérément pas une règle de lexer gloutonne. Il n'est reconnu que dans un seul
état grammatical — élément d'une liste de capacités — de sorte que son : n'entre jamais en
concurrence avec les autres deux-points du langage (champs de record f: v, propriétés
at: (x, y), enfants when …:, le : du ternaire). Partout ailleurs, a:b est un identifiant, un
deux-points et un identifiant avec leurs sens habituels.
À l'intérieur de ce seul état, le lexer lit le premier segment littéralement, même lorsqu'il
épelle un mot réservé : app:launch est une référence de capacité licite bien qu'app soit un
mot-clé dur partout ailleurs. Le relâchement est cantonné exactement à la position de namespace
d'une liste de capacités ; il ne s'étend à aucune autre position du langage.
#Opérateurs et ponctuation
| Tokens | Rôle |
|---|---|
.. |
range — 2..200, fill a..b, propriétés lo..hi ; jamais un opérateur arithmétique |
-> |
LA flèche — un token, une production grammaticale, cinq lectures contextuelles (grammar) |
< > <= >= == != cross_up cross_down |
comparaisons (un niveau non associatif) |
+ - · * / % |
additif / multiplicatif ; - est aussi le moins unaire |
?? ?. ? |
coalescence null · navigation sûre · tête de ternaire |
@ |
suffixe clock — close@"1d", sma(close, 9)@tf("4h") |
. |
accès membre / appel UFCS (et le frère plus court de ..) |
= : , ; |
liaison, deux-points clé/valeur et d'étiquette, séparateurs |
( ) [ ] { } |
groupement et appel · index et liste · blocs, records, corps |
~ |
marqueur de cadence approximative dans every(~ d) (CANVAS ; voir canvas) |
| |
séparateur des constructeurs de variant — son seul rôle |
Trois faits lexicaux sont ici porteurs :
- Maximal munch dans la famille
?: le lexer essaie??, puis?., puis?. Ainsia??best une coalescence,a?.bune navigation sûre, eta ? b : cun ternaire — aucune astuce d'espacement nécessaire, exactement comme..l'emporte sur.. ..et->sont des tokens uniques. Ni l'un ni l'autre n'est jamais deux caractères collés au moment de l'analyse, de sorte quebb.upper..bb.lowerne peut être mal lu (..n'apparaît jamais dans la chaîne postfixe d'une expression) et aucune seconde écriture de flèche n'existe nulle part dans le langage.|n'a aucun rôle d'opérateur. Il n'apparaît qu'entre les constructeurs d'une déclarationvariant(variant Phase { ask | suspense | revealed }). La disjonction booléenne est le motor; il n'y a pas d'opérateur ou-bit-à-bit (les opérations bit-à-bit sont les fonctions nommées debits.*). Cela garde|sans collision avec chaque niveau d'expression.
Pourquoi un seul token flèche. Une syntaxe en flèche apparaît à cinq endroits (lambdas, câblage d'événements, paires de tween, bras de match, compréhensions), et les langages qui font croître des flèches écrites différemment pour ces rôles forcent les lecteurs à mémoriser quelle flèche va où. Flux décrète un seul symbole :
->est un token et une production grammaticale, et les cinq lectures sont sélectionnées par le contexte — la tête gardienne (on,match,for … in) ou, pour les usages non gardés, par l'inférence de kinds. La contribution du lexer à ce décret est minimale et stricte : il existe exactement une écriture de flèche. Le récit complet de la désambiguïsation est sur la page grammaire.
#Le modèle de mots-clés
Flux garde l'ensemble des mots-clés durs aussi petit que la grammaire le permet, en trois paliers plus un non-palier délibéré.
#Palier 0 — durs, réservés partout
Les lieurs, connecteurs, opérateurs et littéraux fondamentaux sont réservés à toute position :
def let in if then else for match with variant record app
and or not cross_up cross_down true false na
plus les deux connecteurs purement médians as et from. Ces mots ne peuvent jamais être un
identifiant — ce sont des lieurs et des séparateurs dont la libération contextuelle n'apporterait
rien et coûterait du lookahead. La seule exception cantonnée, décrite ci-dessus, est la position de
namespace d'une référence de capacité, où app:launch lit app littéralement.
#Palier 1 — têtes contextuelles
Tout autre mot-clé du langage est contextuel : c'est un mot-clé à sa position de tête (le
premier token de sa production, ou un connecteur médian comme over dans morph … over d) et un
IDENT ordinaire dans les cinq emplacements de liaison :
| # | Emplacement de liaison | Exemple avec un mot de Palier 1 |
|---|---|---|
| a | nom de champ (déclaration ou littéral) | record Stop { color: color } |
| b | nom de paramètre | def zone(rect) = rect.w * rect.h |
| c | accès membre (.name) |
gpu.dot, x = p.on |
| d | nom de kind | c: color, state: variant { … } |
| e | étiquette d'argument | focus(view, over: 600ms, pad: 5%) |
Les mots de Palier 1, par famille :
- têtes d'instruction (ANALYSIS) :
plot mark fill alert assert bars input - têtes CANVAS et verbes d'événement :
on group repeat spawn burst emit rate tween flash bounce pulse shake set when every hover click drag enter exit move wheel - primitives CANVAS :
dot circle ring rect square triangle poly line path text image svg sparkline backdrop column - têtes et connecteurs TRANSITION :
morph over focus replay scene - têtes et hooks de representation :
representation transform render reduce liveReduce updateLastUnit persistKey - têtes et hooks d'outil de dessin :
tool barExtent priceExtent - têtes de membres du plan APP :
capabilities init update subs contributes view - outillage et modules :
test import type - connecteurs de clause réservés contextuellement à leur clause :
at(dansassert … at),color(danscolor bars:)
Les modificateurs de visibilité pub, private et package forment une sous-classe contextuelle
distincte : ce ne sont pas des têtes de production mais des préfixes de déclaration, décidés par
un token de lookahead (suivis d'une tête de déclaration ou d'une liaison, ce sont des
modificateurs ; suivis de =, :, . ou (, ce sont de simples identifiants).
Un mot de Palier 1 n'est un mot-clé que là où le parser peut le décaler (shift) comme tel — à sa
position de tête ou médiane. Partout ailleurs, le lexer restitue un identifiant ordinaire, donc
def f(render) = render * 2 est licite, et la liaison render = 3 l'est aussi : le membre gauche
d'une liaison décale un identifiant, pas une tête. Ce que vous ne pouvez pas faire, c'est utiliser
le mot là où sa production l'attend en lui donnant un autre sens.
fluxrecord Stop { color: color } // (a) field name + (d) kind name — both `color`
tool fib(a, b) {
render: line { x1: a.bar; y1: a.price; x2: b.bar; y2: b.price }
} // `render` is a keyword here — hook-head position
focus(view, over: 600ms) // (e) `over` as an argument label
Pourquoi la réservation contextuelle. Réserver purement et simplement chaque tête forcerait les auteurs à des noms distordus —
focusRef,bounds,vdot— précisément là oùfocus,rectetdotsont le vocabulaire voulu du domaine. La réservation contextuelle conserve la lisibilité qu'apportent les instructions à tête de mot-clé (chaque instruction est engagée par son premier token) tout en rendant les mots aux auteurs dans les cinq emplacements où aucune tête ne peut jamais apparaître. Les deux côtés sont prouvablement disjoints : les emplacements de liaison siègent dans des états du parser où seul unIDENTest attendu, donc libérer un mot à cet endroit n'introduit d'ambiguïté nulle part — une affirmation que la construction de la grammaire revérifie mécaniquement à chaque changement.
#Réservé par anticipation
Flux réserve un mot avant de livrer sa production dès lors que le mot est destiné à la syntaxe de
surface, afin qu'aucun programme existant ne puisse le masquer entre-temps (voir
additivité). Les mots du plan APP (app, match,
capabilities, init, update, subs, contributes, view, emit, variant) et les mots de
module (import, pub, private, package) ont été réservés ainsi dès la première version et ont
depuis reçu leurs productions.
Réservé. test en est l'instance actuelle : il est réservé en v1 sans production. Le bloc
test "name" { … } est une commodité d'outillage dont la production sera livrée plus tard, de façon
additive, avec ses tests de référence (golden tests).
#Délibérément NON réservés — les intégrés
Les noms de valeurs intégrées sont des identifiants ordinaires, pas des mots-clés :
close open high low volume time hl2 hlc3 ohlc4
bar clock screen pane ratio depth z up down self range
Un programme peut les lier — et un lint de style signale immédiatement le masquage :
fluxclose = 42 // legal — the shadowing lint flags: `close` hides the built-in series
plot close // now plots 42 at every bar
Pourquoi ne pas les réserver. Ces noms sont du vocabulaire, pas de la structure. Réserver
open,rangeouzempoisonnerait d'immenses pans du nommage ordinaire (tout rectangle a unrange, tout record unopenquelconque), pour zéro bénéfice d'analyse — aucune production grammaticale n'y est ancrée. Un lint donne à l'auteur l'avertissement qui compte (le masquage accidentel d'une source de données) tout en gardant l'ensemble des mots-clés minimal, ce qui à son tour garde le récit de l'additivité propre : moins le langage possède de mots, moins il a de collisions futures à gérer.
#Erreurs lexicales
Parce que le lexer et le parser sont tous deux totaux, une entrée mal formée produit des diagnostics, jamais des crashs. Les rejets caractéristiques au niveau lexical :
fluxs = "unterminated // ✗ syntax error — a string must close on its own line
x = 1.50 d // ✗ syntax error — juxtaposition (NUMBER then IDENT)
t = // ✗ syntax error — `se` does not complete a duration suffix
Tout le reste qui semble lexical — a < b < c, un -> égaré, un { a, b } en position
d'expression — est rejeté une étape plus tard, par la grammaire ; ces diagnostics sont catalogués
sur la page grammaire.
#Voir aussi
- Grammaire — la grammaire normative que ces tokens alimentent ; la flèche unique ; la précédence.
- Kinds — les kinds dimensionnels que portent les littéraux typés (
DUR,PCT,SPAN,DEC…). - Opérateurs — l'algèbre dimensionnelle derrière
+ - * / %, la famille?, UFCS. - Temps et état — ce que signifie le suffixe clock
@et pourquoi il est restreint. - Text — le kind
string, le formatage (fmt.*) et le pipeline d'interpolation. - Prise en main — les mêmes tokens rencontrés dans l'ordre du programme.
Grammaire
Cette page est la syntaxe normative de Flux : chaque forme d'instruction, la grammaire complète des
expressions, la flèche unique -> et ses cinq lectures contextuelles, l'échelle de précédence, les
décisions de désambiguïsation qui gardent l'analyse déterministe, et les six propriétés formelles
vérifiées par machine contre lesquelles la grammaire est figée. Les tokens (IDENT, NUMBER,
STRING, TERM, …) sont définis dans structure lexicale ; ce que les
programmes signifient est l'affaire des kinds, de l'inférence et du
temps et état.
Il y a exactement une grammaire, et elle est énoncée une seule fois : l'EBNF normatif ci-dessous est exprimé comme une grammaire LR Lezer — le même artefact pilote le compilateur, l'éditeur et l'outillage de documentation, de sorte qu'il n'existe aucune seconde description de la syntaxe qui dériverait. Le fait que la construction de la grammaire soit acceptée avec zéro conflit non résolu est le contrôle machine que le langage est non ambigu (voir propriétés formelles).
Notation. { x } se répète zéro ou plusieurs fois, [ x ] est optionnel, | sépare les
alternatives, "x" est un mot-clé littéral ou un token de ponctuation, et les noms en MAJUSCULES
sont des tokens lexicaux issus du catalogue des tokens.
#Une grammaire, tous les plans
Les programmes Flux vivent sur quatre plans — ANALYSIS, CANVAS, TRANSITION et APP — mais la syntaxe
est une grammaire unique. Le plan est inféré à partir des constructions utilisées, jamais
annoté : il n'y a pas de pragma de plan, pas de commutateur de mode au niveau du fichier. plot
et alert sont des sinks (puits) ANALYSIS ; on, les primitives de forme et spawn sont CANVAS ;
morph, focus et replay sont TRANSITION ; un descripteur app est APP. Un seul fichier les
mélange librement — un indicateur et sa présentation sont un seul programme — et le pare-feu entre
plans (la présentation peut lire l'analyse, jamais l'inverse) est appliqué par l'analyse de
dépendances sur l'arbre analysé, pas par la grammaire. Voir les quatre
plans.
fluxplot close // ANALYSIS — the smallest program
on click -> burst(40) dot { vel: 3 } // CANVAS — same file, same grammar
morph chart over 600ms // TRANSITION
#Programmes et séparation des instructions
program = { TERM } [ stmt { TERMSEP stmt } { TERM } ]
TERMSEP = ( TERM | ";" ) { TERM | ";" }
sep = ( TERM | "," | ";" ) { TERM | "," | ";" }
Un programme est une séquence d'instructions séparées par des sauts de ligne significatifs (TERM)
et/ou des points-virgules optionnels ; les lignes vides sont libres. À l'intérieur des corps entre
accolades, les éléments de liste sont séparés par sep — un saut de ligne, une virgule ou un
point-virgule, indifféremment — ce qui explique qu'un record multi-ligne au niveau supérieur n'a pas
besoin de virgules finales et qu'un bloc d'une ligne peut utiliser ;. Le saut de ligne ne compte
comme séparateur qu'à la profondeur de parenthèses zéro : un corps entre accolades écrit à
l'intérieur d'un appel est toujours dans les parenthèses de l'appel, donc ses éléments ont besoin
d'un , ou ; explicite. La politique complète des sauts de ligne (quand un saut de ligne termine
et quand il continue) est spécifiée dans structure
lexicale.
Flux n'a pas d'instructions-expressions : une expression nue au niveau instruction est une erreur de syntaxe, ce qui est précisément ce qui rend les instructions terminées par saut de ligne non ambiguës.
fluxrsi(close, 14) // ✗ syntax error — an expression is not a statement; write `plot rsi(close, 14)`
#Formes d'instruction
stmt = declStmt | plotStmt | markStmt | fillStmt | colorBarsStmt | alertStmt | assertStmt
| onStmt | groupStmt | repeatStmt | forStmt | uiElement | primStmt | spawnStmt
| tweenStmt | effectStmt | setStmt | morphStmt | focusStmt | replayStmt
| appStmt | importStmt
Presque toute instruction est engagée par son premier token — l'idiome de tête-mot-clé : au niveau
instruction, seul assertStmt peut commencer par assert, seul variantDecl par variant, et
ainsi de suite. Le parser ne devine jamais ; chaque tête possède un état LR distinct. Les deux
instructions qui commencent par un IDENT (une liaison, un conteneur de vue) et celle qui commence
par { (une liaison déstructurante) sont résolues par un token de lookahead, catalogué
ci-dessous.
#Déclarations
declStmt = [ visMod ] ( bindStmt | defStmt | typeDecl | variantDecl | recordDecl
| reprStmt | toolStmt )
visMod = "pub" | "private" | "package"
bindStmt = letPat "=" expr
letPat = recordPat | IDENT
defStmt = [ DOC ] "def" IDENT "(" [ params ] ")" "=" expr
params = param { "," param }
param = IDENT [ "=" literal ]
Une liaison nomme une valeur pour le reste du programme ; son membre gauche est un identifiant
unique ou un motif de record irréfutable qui déstructure sur place. Les liaisons sont immuables —
il n'y a pas d'instruction d'affectation, et let n'existe qu'à l'intérieur des expressions.
fluxn = input(14, 2..200)
{upper, lower} = bollinger(close, 20) // destructuring bind — no `:` after `{ IDENT`
let n = 20 // ✗ syntax error — `let` is expression-only; top-level binds are bare
Une définition de fonction lie un nom à une expression paramétrée. Les paramètres peuvent porter
des valeurs par défaut littérales ; un doc-commentaire /// s'attache à la définition. La récursion
est rejetée ([ErrTotalRec]) — la totalité par construction.
flux/// z-score of a series over n bars
def zscore(x, n=20) = (x - sma(x, n)) / stdev(x, n)
plot zscore(close) as z
Le modificateur de visibilité optionnel cantonne une déclaration pour le système de modules :
pub traverse un import, package couvre les fichiers source d'un script, private (le défaut)
reste dans son fichier. Le modificateur est contextuel — décidé par le token qui le suit — donc
pub, private et package restent utilisables comme noms ordinaires dans les emplacements de
liaison.
fluxpub def helper(x) = x * 2
private record Internal { a: num }
pub {upper, lower} = bollinger(close, 20) // a modifier also prefixes a (destructuring) bind
#Types nommés — variant, record, alias
variantDecl = "variant" IDENT "{" { TERM } ctorDecl { "|" ctorDecl } { sep } "}"
ctorDecl = IDENT [ "(" [ field { "," field } ] ")" ]
field = [ IDENT ":" ] kindExpr
recordDecl = "record" IDENT "{" { TERM } fieldDecl { sep fieldDecl } { sep } "}"
fieldDecl = IDENT ":" kindExpr [ "=" ( literal | "na" ) ]
typeDecl = "type" IDENT [ "(" IDENT { "," IDENT } ")" ] "=" kindExpr
kindExpr = "vec" "(" kindExpr "," constLen ")"
| "variant" "{" { TERM } ctorDecl { "|" ctorDecl } { sep } "}"
| "record" "{" { TERM } fieldDecl { sep fieldDecl } { sep } "}"
| IDENT [ "(" [ argList ] ")" ]
constLen = NUMBER | IDENT
variant déclare un type somme nommé (constructeurs séparés par |, charges utiles positionnelles
avec des noms de champs documentaires optionnels) ; record déclare un type produit nommé (chaque
champ nommé et doté d'un kind, avec des valeurs par défaut constantes optionnelles) ; type déclare
un alias transparent, possiblement paramétré — substitution, pas un nouveau type. Un kindExpr
nomme un kind du treillis (price, osc(0,100)), un type déclaré, un vec(element, constLength),
ou un variant{…} / record{…} structurel en ligne. Les chevrons ne sont jamais des délimiteurs de
type — < et > sont exclusivement des opérateurs de comparaison — donc chaque forme paramétrée
utilise des parenthèses.
fluxvariant Phase { ask | suspense | revealed }
variant Tool { Select | Event(kind: num) | At(price) } // named or anonymous payloads
record Level { price: price; kind: num = 3; label: string }
record Inline {
state: variant { Connecting | Ready } // inline structural kinds
slots: vec(Level, 100)
}
type long = decimal(18, 0)
type Series(T) = vec(T, 500) // transparent, parameterized alias
Le graphe de références entre types nommés doit être acyclique — record Node { next: Node } est
rejeté avec [ErrTotalType] à la résolution de noms, ce qui maintient chaque type fini.
#Sinks et paramètres ANALYSIS
plotStmt = "plot" exprList [ "as" IDENT ] [ block ]
markStmt = "mark" condExpr [ strLit ] [ block ]
fillStmt = "fill" addExpr ".." addExpr [ block ]
colorBarsStmt = "color" "bars" ":" condExpr
alertStmt = "alert" condExpr [ strLit ]
assertStmt = "assert" condExpr [ strLit ] [ "at" addExpr ]
strLit = STRING | interpStr
exprList = expr { "," expr }
Les sinks (puits) publient les valeurs d'analyse vers l'hôte : plot trace des séries
(optionnellement nommées avec as, optionnellement stylées avec un bloc final), mark dépose des
marqueurs étiquetés sur une condition, fill ombre entre deux séries, color bars: colore les
barres elles-mêmes, alert lève un message sur une condition, et assert énonce un invariant qui
doit tenir à chaque barre — ou, avec at, sur une barre donnée. Une assertion dont la condition est
na (échauffement) passe ; elle ne se déclenche que sur un signal assurément faux.
fluxbb = bollinger(close, 20)
equity = cum(close - open)
fill bb.upper..bb.lower
color bars: if close > ema(close, 200)@"1d" then up else down
alert close cross_up open "crossed"
assert rsi(close, 14) <= 100 "rsi bounded" // na on bars 0–13 — vacuously satisfied
assert equity > 0 "positive" at 500
mark rsi(close, 14) > 70 "overbought" { size: 8 }
Un paramètre de script est la forme d'expression input(…) (un primary, utilisable partout où
une expression l'est) :
inputExpr = "input" "(" inputDefault { "," inputArg } ")"
inputDefault = literal | IDENT | listLit
inputArg = metaArg | inputExtra
metaArg = IDENT ":" strLit
inputExtra = addExpr [ ".." addExpr ]
La valeur par défaut doit être constante — un littéral, un identifiant de source, ou une liste
d'étiquettes de chaîne (la forme énumérée) ; son kind fixe le kind du paramètre. Après la valeur par
défaut viennent un range (2..200) ou une borne optionnels et des arguments méta-UI nommés
(title:, group:, tooltip:, inline:), distingués d'un range par le lookahead IDENT : :
fluxlen = input(14, 2..200)
src = input(close)
name = input("fast", title: "Label", group: "Style")
mode = input(["ema", "sma"], tooltip: "kind") // enumerated — a variant of the labels
dec = input(1.50d) // decimal parameter
#Instructions CANVAS
onStmt = "on" eventExpr ARROW action
eventExpr = "hover" | "click" | "drag" | "enter" | "exit" | "move" | "wheel"
| everyExpr | condExpr
everyExpr = "every" "(" [ "~" ] ( DUR | SPAN | addExpr ) ")"
action = stmt | block
groupStmt = "group" [ block ]
repeatStmt = "repeat" addExpr "as" IDENT [ block ]
forStmt = "for" IDENT "in" condExpr ARROW uiChild
primStmt = primitive [ "when" condExpr ]
primitive = shapePrim [ block ] | contentPrim [ condExpr ] [ block ]
shapePrim = "dot" | "circle" | "ring" | "rect" | "square" | "triangle" | "poly"
| "line" | "path" | "backdrop" | "column"
contentPrim = "text" | "image" | "svg" | "sparkline"
spawnStmt = ( "spawn" | "burst" "(" addExpr ")" | "emit" "rate" "(" addExpr ")" ) primitive
tweenStmt = "tween" propPath arrowPair [ overClause ]
setStmt = "set" propPath "=" expr
effectStmt = ( "flash" | "bounce" | "pulse" | "shake" ) [ postfix ] [ block ]
propPath = IDENT { "." IDENT }
on câble un événement — un verbe d'interaction, une cadence every(…), ou n'importe quel flux
booléen — à une action. Une primitive dessine un élément, avec un bloc de propriétés et un garde
when final optionnel. spawn / burst(n) / emit rate(r) créent des éléments éphémères issus
d'un pool ; tween, set et les mots d'effet animent des propriétés. La sémantique de tout cela vit
sur la page CANVAS ; grammaticalement, ce sont des instructions à tête de mot-clé
partageant l'unique forme block.
fluxon click -> group { dot { at:(bar.i, close); r: 4 } }
on every(1 bars) -> spawn ring { r: 6->24; life: 200 bars }
on every(~ 500ms) -> pulse // `~` — approximate cadence
on close > highest(close, 250)[1] -> burst(40) dot { vel: 3 }
emit rate(norm(volume) * 40) dot { size: 2 }
tween p.r 6->24 over 300ms
circle { at:(spring(close)); trail: 24 } when close > open
Conteneurs de vue et compréhension. Un conteneur de vue est une tête d'identifiant
(optionnellement appelée) suivie d'un corps entre accolades obligatoire d'enfants ; la compréhension
for … in … -> est une forme instruction/enfant uniquement — for ne commence jamais une
expression (le mappage pur de données est vec.map) :
uiElement = IDENT [ callTail ] uiBlock
uiBlock = "{" { TERM } [ uiChild { sep uiChild } { sep } ] "}"
uiChild = uiElement | forStmt | whenChild | primStmt | expr
whenChild = "when" condExpr ":" uiChild
fluxslots = window(close, 5)
row { text "a"; text "b" }
panel(slot: side) { col { for s in slots -> renderSlot(s) } }
grid(cols: 2) { when close > open: text "up"; col { text "deep" } }
Un conteneur sans enfant (button(reset), panel(slot: x)) n'est pas un uiElement — c'est une
expression d'appel ordinaire de kind ui ; c'est le corps entre accolades qui fait la forme
conteneur.
#Instructions TRANSITION
morphStmt = "morph" ( "chart" | IDENT ) [ overClause ] [ block ]
overClause = "over" condExpr
focusStmt = "focus" "(" "view" { "," arg } ")"
replayStmt = "replay" "from" condExpr overClause
fluxanchors = window(close, 10)
morph chart over 600ms
morph pnf { keep: anchors }
focus(view, over: 600ms, pad: 5%) // `over` here is an argument label, not the clause keyword
replay from close > 100 over 2s
#Representations et outils de dessin
reprStmt = "representation" IDENT "(" [ params ] ")" reprBlock
reprBlock = "{" { TERM } [ reprHook { sep reprHook } { sep } ] "}"
reprHook = reprKey ":" reprVal
reprKey = "transform" | "render" | "reduce" | "liveReduce" | "updateLastUnit" | "persistKey"
reprVal = primStmt | block | expr
toolStmt = "tool" IDENT "(" [ params ] ")" toolBlock
toolBlock = "{" { TERM } [ toolHook { sep toolHook } { sep } ] "}"
toolHook = toolKey ":" toolVal
toolKey = "barExtent" | "priceExtent" | "render"
toolVal = primStmt | block | expr
Les deux sont des formes de déclaration à tête de mot-clé dont les corps sont une énumération fermée
de hooks — le script écrit les fonctions pures (géométrie, rendu, réduction), l'hôte fournit tout le
reste (placement, hit-testing, persistance ; d'où hitTest et lod ne sont pas des mots-clés et
restent des noms libres). Voir intégration hôte.
fluxrepresentation pnf(box, rev) {
transform: rebin(close, box, rev)
render: column { w: 1 }
reduce: decimate(cols, k)
liveReduce: mutateHead(cols)
updateLastUnit: patch(cols)
persistKey: "pnf-v1"
}
tool fib(a, b) {
barExtent: (a.bar, b.bar)
priceExtent: (a.price, b.price)
render: line { x1: a.bar; y1: a.price; x2: b.bar; y2: b.price }
}
#Le descripteur APP
appStmt = "app" IDENT appBody
appBody = "{" { TERM } [ appMember { sep appMember } { sep } ] "}"
appMember = capEntry | memberDef
capEntry = "capabilities" ":" capList
capList = "[" { TERM } [ capRef { "," capRef } { sep } ] "]"
capRef = CAPREF | IDENT
memberDef = ( "init" | "update" | "view" | "subs" | "contributes" )
"(" [ params ] ")" "=" memberBody
memberBody = uiElement | expr
Une app est un descripteur nommé avec une entrée capabilities: et les cinq membres fixes du
harnais TEA (TEA — The Elm Architecture). Les têtes de membres sont des mots-clés, pas des
identifiants — la forme est délibérément sans def parce que ces cinq rôles sont fixes.
memberBody est le seul endroit de la grammaire où un conteneur de vue au niveau instruction est le
membre droit de = (un view renvoie une vue) ; partout ailleurs, le membre droit de = est une
expression.
fluxapp structureGame {
capabilities: [chart:read, storage:own, levels:write, sfx]
init(p) = {score: 0, phase: ask}
update(m, msg) = match msg {
Tick(dt) -> m with {score: m.score + dt}
_ -> m
}
view(m) = panel(slot: side) {
row { text "score {m.score}"; when m.done: button(reset) }
for t in TOOLS -> button(t)
}
subs(m) = [OnTick(Tick)]
}
La sémantique du plan APP — Model, Msg, commandes en tant que données inertes, souscriptions — est spécifiée sur la page du plan APP.
#Imports et visibilité
importStmt = "import" pkgRef [ "as" IDENT ]
pkgRef = IDENT "/" IDENT
import author/package lie un paquet sous son nom ou un alias as ; seules les déclarations pub
du paquet sont atteignables, en tant que noms qualifiés mod.f. Le / de la coordonnée n'est
jamais confondu avec une division — il n'est atteignable qu'après import IDENT, un état où aucune
expression n'existe.
fluximport acme/wyckoff as wk
pub def helper(x) = wk.zone(x) * 2
Post-v1. Le mécanisme d'import, la résolution adressée par contenu et la sémantique du lockfile sont entièrement spécifiés (paquets) ; le déploiement du registre public est différé.
#Expressions
La grammaire des expressions est une stratification stricte : chaque niveau ne se réfère qu'au niveau immédiatement plus serré, de sorte que la précédence est intégrée dans la forme même de la grammaire (aucune déclaration de précédence n'est nécessaire pour la chaîne arithmétique, et l'analyse est LR(1) par construction).
expr = arrowExpr
arrowExpr = condExpr [ ARROW expr ] (right-assoc; lambda or pair — see below)
condExpr = ifExpr | letExpr | ternExpr
ifExpr = "if" expr "then" expr "else" expr (else mandatory)
letExpr = "let" letPat "=" expr "in" expr
ternExpr = coalExpr [ "?" expr ":" expr ] (≡ if/then/else)
coalExpr = orExpr [ "??" coalExpr ] (right-assoc; ≡ nz)
orExpr = andExpr { "or" andExpr }
andExpr = notExpr { "and" notExpr }
notExpr = "not" notExpr | cmpExpr
cmpExpr = addExpr [ cmpOp addExpr ] (NON-associative)
addExpr = mulExpr { addOp mulExpr }
mulExpr = unary { mulOp unary }
unary = "-" unary | postfix
postfix = primary { callTail | indexTail | clockTail | memberTail | safeNavTail | withTail }
primary = NUMBER | DEC | DUR | PCT | PX | SPAN | RATE | STRING | interpStr | BOOL | "na"
| IDENT | inputExpr | tweenSig | matchExpr | listLit | recordLit | blockExpr
| sceneExpr | parenForm
tweenSig = "tween" "(" arrowPair [ "," argList ] ")"
arrowPair = condExpr ARROW condExpr
#Conditionnelles : if, let, ternaire, coalescence
if porte toujours un else — il n'y a pas de problème de dangling-else car la forme incomplète
n'existe pas. let … in cantonne une liaison (ou une déstructuration irréfutable) sur une expression
de corps. Le ternaire est le même arbre que if/then/else, et ?? est du sucre pour nz
(remplacer na par une valeur par défaut) :
fluxplot if close > open then 1 else 0
r = let x = close - open in x * x
d = let { upper, lower } = bollinger(close, 20) in upper - lower // destructuring let
t = close > open ? 1 : 0 // ≡ if close > open then 1 else 0
z = close[1] ?? 0 // ≡ nz(close[1], 0)
x = if a then 1 // ✗ syntax error — else is mandatory
#match
matchExpr = "match" condExpr "{" { TERM } matchArm { sep matchArm } { sep } "}"
matchArm = pattern ARROW expr
pattern = "_" | "na" | ctorPat | recordPat | IDENT
ctorPat = IDENT [ "(" [ IDENT { "," IDENT } ] ")" ]
recordPat = "{" [ IDENT { "," IDENT } ] "}"
match est l'éliminateur des valeurs variant (et de na), utilisable dans toute position
d'expression. Le sujet du match (scrutinee) est sans flèche (condExpr), de sorte que chaque ->
à l'intérieur des accolades appartient à un bras. Les motifs sont plats en v1 : joker, na, un
constructeur avec des noms de charge utile liés, une déstructuration de record, ou un identifiant de
liaison — pas d'imbrication profonde. L'exhaustivité est vérifiée statiquement ; un match non
exhaustif est rejeté avec [ErrTotalMatch].
fluxvariant Phase { ask | suspense | revealed }
variant Tool { Select | Event(kind: num) | At(price) }
m = { phase: ask }
t = Event(3)
next = match m.phase {
ask -> suspense
suspense -> revealed
_ -> ask
}
which = match t { Event(k) -> k; At(p) -> 0; _ -> na }
q = SaveState.Saved // a qualified constructor disambiguates cross-variant homonyms
Une étiquette nullaire et un identifiant de liaison sont le même arbre d'analyse (un motif
IDENT) ; savoir si ask nomme un constructeur connu ou lie une variable fraîche est résolu
sémantiquement, exactement comme la résolution de fonction — jamais une bifurcation d'analyse.
#Littéraux de record, de liste et de bloc ; scene
recordLit = "{" { TERM } fieldAssign { sep fieldAssign } { sep } "}" (≥ 1 field)
fieldAssign = IDENT ":" expr
listLit = "[" { TERM } [ expr { "," expr } { sep } ] "]"
blockExpr = "{" { TERM } { blockBind sep } expr { TERM } "}"
blockBind = IDENT "=" expr
sceneExpr = "scene" uiBlock
parenForm = "(" [ parenItem { "," parenItem } ] ")"
parenItem = expr [ ".." expr ]
Un littéral de record construit le premier record ({a: 1, b: 2} ; un {} vide n'est pas un
record). Un littéral de liste construit un vec borné ([1, 2, 3], [] — la colonne vertébrale de
toute liste de commandes APP). Une expression de bloc séquence des liaisons immuables avant une
expression finale et se désucre en let … in imbriqués — la forme de corps multi-ligne :
fluxm = {a: 1, b: 2}
xs = [1, 2, 3]
empty = []
v = { x = 1; y = x * 2; y + x } // blockExpr — desugars to let x = 1 in let y = … in y + x
w = { close } // a one-expression block
bad = { a, b } // ✗ syntax error — neither a record (no `:`) nor a block (no `=`)
scene { … } empaquette une scène CANVAS multi-éléments comme une valeur de kind ui — la seule
forme d'expression d'une scène, qui vit sinon au niveau instruction. C'est ainsi qu'un def renvoie
un overlay :
fluxdef overlayOf(d) = scene {
line { a: d.a; b: d.b }
for it in items(d) -> dot { at:(it.bar, it.price) }
}
La forme parenthésée unifie le groupement, les coordonnées et les têtes de lambda : (x)
groupement, (x, y) une paire coordonnée/argument (at:(bar.i, close)), (2..200) un opérande de
range, (p) -> … une tête de lambda.
#La chaîne postfixe
callTail = "(" [ argList ] ")"
indexTail = "[" expr "]"
memberTail = "." IDENT
safeNavTail = "?." IDENT
clockTail = "@" clockOperand
clockOperand = STRING | IDENT [ callTail ] | "(" expr ")"
withTail = "with" recordUpdateBody
recordUpdateBody = "{" { TERM } [ fieldAssign { sep fieldAssign } { sep } ] "}"
argList = arg { "," arg }
arg = [ IDENT ":" ] expr
arrowPair = condExpr ARROW condExpr
Les six suffixes lient au même niveau (le plus serré) et s'associent à gauche, dans l'ordre lexical :
fluxslots = window(close, 20)
bb = bollinger(close, 20)
m = { a: 1 }
i = input(0, 0..19)
chain = bollinger(close, 20).upper[1]@"1d"
// ((( bollinger(close,20) ).upper )[1] )@"1d"
prev = close[1] // [Delay] — scalar stream, constant index
s = slots[i] // [Index] — vec element, runtime index, out-of-bounds → na
htf = ema(close, 50)@"1d" // clock suffix — see time-and-state
safe = bb?.upper // na-propagating navigation
y = m with {a: 3} // functional record update — shape-preserving
L'opérande de @ est délibérément restreint (une chaîne, un identifiant ou un appel, ou une
expression parenthésée) de sorte que x@"1d" + 1 s'analyse comme (x@"1d") + 1 sans subtilités de
précédence. with { … } est un mot-clé postfixe, pas une expression menée par une accolade — le
corps de mise à jour n'est atteignable qu'après le mot with, ce qui le maintient distinct de toute
autre accolade.
Les arguments peuvent être étiquetés (focus(view, over: 600ms)) ; l'étiquette est décidée par le
lookahead IDENT :. L'accès membre fait aussi office d'UFCS — close.ema(20).rsi(14) est
rsi(ema(close, 20), 14) ; le . se résout en champ, appel de fonction ou nom de module qualifié à
la compilation sans impact sur la grammaire (voir Opérateurs).
#Lambdas
Un lambda est une flèche dont le côté gauche est une forme parenthésée d'identifiants nus, dans une
position qui attend une fonction (les arguments d'ordre supérieur de fold, map, scan,
loop, …) :
fluxdef ema0(s, n) = let a = 2/(n+1) in scan(s, (p) -> a*s + (1-a)*p)
r = window(close / close[1], 20) // a vector of ratios
sq = vec.map(r, (x) -> x * x)
inc = vec.map(r, _ + 1) // placeholder sugar — exactly one `_`, mono-argument
Les parenthèses font partie de la forme — un x -> … nu en position de valeur est une paire de
tween, pas un lambda (voir la section suivante) ; le raccourci à argument unique est le placeholder
_. Un corps multi-instructions est une expression de bloc : (x) -> { d = x - open; d * d }.
#Blocs et propriétés
block = "{" { TERM } [ item { sep item } { sep } ] "}"
item = propEntry | stmt
propEntry = IDENT [ ":" propValue ]
propValue = arrowPair | addExpr ".." addExpr | condExpr
literal = NUMBER | DEC | DUR | PCT | PX | SPAN | STRING | BOOL
Il y a une seule forme de bloc. Ses éléments sont des propriétés (key: value, ou un simple
drapeau comme stagger) et/ou des instructions ; laquelle des deux une tête donnée permet (un bloc
dot prend des propriétés, un bloc group prend des instructions) est une restriction sémantique,
pas une grammaire séparée. Une valeur de propriété peut être une valeur simple, un range lo..hi, ou
une paire de tween a->b :
fluxdot { at:(bar.i, close); r: 6->24; glow: 16; span: 2..8; stagger }
#La flèche unique
-> est un token et une production grammaticale — Arrow { lhs, rhs } — avec cinq lectures
sélectionnées entièrement par le contexte. Il n'y a pas de second symbole flèche nulle part dans le
langage.
| # | Lecture | Exemple | Ce qui la sélectionne |
|---|---|---|---|
| 1 | lambda | scan(s, (p) -> a*s + (1-a)*p) |
le côté gauche est une forme parenthésée d'identifiants nus, dans une position attendant une fonction |
| 2 | événement → action | on click -> pulse |
la tête on ; l'opérande d'événement est sans flèche |
| 3 | paire de tween | r: 6->24, tween(0->1, ease: out) |
une position de valeur (propriété, tween) ; les deux côtés sont des valeurs |
| 4 | bras de match | Event(k) -> k |
la tête match possède les accolades ; chaque flèche de bras lui est revendiquée |
| 5 | compréhension de vue | for t in TOOLS -> button(t) |
la tête for … in ; la collection est sans flèche |
Déterministe par construction, en deux étapes :
- Les flèches gardées n'atteignent jamais la grammaire des expressions. Dans
on EVENT -> ACTION,match e { pattern -> expr }etfor x in coll -> child, l'opérande avant la flèche est uncondExpr— une strate qui exclutarrowExpr— de sorte que l'événement, le sujet et la collection ne peuvent jamais absorber le->. La flèche est revendiquée par la production propre de la tête, sans aucun choix pour le parser. - Les flèches non gardées construisent un seul arbre ; le rôle est décidé à l'inférence.
a -> bdans une expression s'analyse comme l'unique nœudArrow, qu'elle agisse comme un lambda ou comme une paire de tween. L'inférence de kinds la lit comme un lambda exactement quand le côté gauche est une forme parenthésée d'identifiants nus et que la position attend une fonction ; sinon c'est une paire de valeurs (6->24). Le même arbre sert aussi la production de paire dédiéearrowPairutilisée partweenet les valeurs de propriété.
fluxplot -> 3 // ✗ syntax error — an arrow needs a left side; no production begins with ->
Pourquoi une seule flèche. Cinq flèches écrites différemment exigeraient cinq tokens, cinq entrées de précédence et une table mentale du lecteur associant écriture et rôle. Un seul token avec des lectures gardées par la tête ne coûte rien à la grammaire (chaque garde est un état LR que la tête possède déjà), garde chaque programme visuellement cohérent, et déplace la seule véritable ambiguïté — lambda versus paire — vers le vérificateur de kinds, qui doit de toute façon inspecter cette position. Le parser ne bifurque jamais.
#Précédence et associativité
Du plus lâche au plus serré ; chaque niveau est une strate de la grammaire ci-dessus :
| Niveau | Opérateurs | Associativité |
|---|---|---|
| 0 | -> (lambda / paire) |
droite |
| 1 | ? : ternaire · if/then/else · let/in |
droite ; else obligatoire |
| 2 | ?? |
droite |
| 3 | or |
gauche |
| 4 | and |
gauche |
| 5 | not |
préfixe |
| 6 | < > <= >= == != cross_up cross_down |
non associatif |
| 7 | + - |
gauche |
| 8 | * / % |
gauche |
| 9 | - unaire |
préfixe — plus serré que *// |
| 10 | postfixe f(…) [i] @c .m ?.m with {…} |
gauche, même niveau, ordre lexical |
| 11 | primary | — |
Conséquences qui méritent d'être explicitées :
fluxm = -close[1] * 2 // (-(close[1])) * 2 — unary minus tighter than `*`, postfix tighter still
p = close > open // signal
q = volume > sma(volume, 20) // signal
r = not p and q or p // ((not p) and q) or p
s = m ?? 0.0 // m ?? 0.0 — null-coalescing
t = close > open ? high : low // (close > open) ? high : low
e = macd(close).hist[1]@"1d" // (((macd(close)).hist)[1])@"1d" — postfix chain, lexical order
bad = a < b < c // ✗ syntax error — comparisons do not chain (non-associative)
Hors de l'échelle. Trois familles de tokens ne prennent aucun niveau de précédence. Le range
.. est non associatif et n'apparaît que dans ses emplacements dédiés (fills, spans, ranges
d'input, éléments de parenthèse, valeurs de propriété) — jamais à l'intérieur de la cascade
d'opérateurs, de sorte que bb.upper..bb.lower n'a besoin d'aucune parenthèse. Le suffixe clock @
restreint son opérande droit à un clockOperand, de sorte que x@"1d" + 1 est (x@"1d") + 1 par
construction. Et : et , sont de purs séparateurs.
Pourquoi la stratification plutôt que des annotations de précédence. Chaque strate ne se réfère qu'à la suivante, plus serrée — il n'y a de récursion croisée nulle part dans la grammaire des expressions — de sorte que l'automate LR est la table de précédence. Rien n'a besoin d'être déclaré, donc rien ne peut être déclaré de façon incohérente ; le niveau de comparaison non associatif est une production qui ne se répète pas. L'interdiction de chaîner les comparaisons existe parce que
a < b < cn'a aucune lecture booléenne dans un langage dimensionnel (la première comparaison produit unsignal, qui n'est pas ordonné par rapport àc— la grammaire rejette la forme avant que le vérificateur de kinds ne le fasse).
Sauts de ligne. Le séparateur d'instruction TERM et ses règles de continuation interagissent
avec cette échelle (un opérateur infixe en début de ligne continue la ligne précédente). La politique
est lexicale et spécifiée dans structure lexicale.
#Catalogue de désambiguïsation
Chaque endroit où deux constructions pourraient se disputer le même token est fermé par l'un de cinq dispositifs : une règle de tokeniseur, la stratification, un mot-clé de tête gardien, des états LR distincts, ou un token de lookahead borné. Le plan de conception énumère chaque conflit potentiel et sa résolution ; la construction de la grammaire re-prouve mécaniquement l'absence de conflits à chaque changement. Les cas qu'un lecteur rencontre réellement :
L'accolade. Toutes les lectures de { sont séparées par l'endroit où l'accolade est atteinte :
| Vous voyez | C'est | Décidé par |
|---|---|---|
{f: v, …} en position d'expression |
littéral de record | le lookahead après { IDENT est : |
{x = 1; …; e} en position d'expression |
expression de bloc | le lookahead après { IDENT est = / ; / } |
{a, b} = e au niveau instruction |
liaison déstructurante | pas de : après { IDENT ; les instructions ne commencent jamais par une expression menée par une accolade |
match e { … } |
bras de match | la tête match possède cette accolade |
e with { … } |
corps de mise à jour de record | atteignable uniquement après le token with |
row { … }, panel(x) { … } |
conteneur de vue | accolade après une tête IDENT/appel en position d'instruction ou d'enfant |
plot … { … }, dot { … }, group { … } |
l'unique block (style/props/instructions) |
bloc final gardé par sa tête d'instruction |
variant T { … }, record T { … }, app N { … } |
corps de déclaration | tête de mot-clé |
Deux formes menées par une accolade — et seulement deux — peuvent commencer une expression
(littéral de record, expression de bloc), et un token après { IDENT les sépare ; un { a, b } nu
en position d'expression n'est ni l'un ni l'autre, et est rejeté. Toute autre accolade est atteinte
après un token gardien, dans un état LR où aucune expression ne peut commencer — de sorte que le
parser ne bifurque jamais sur {.
Le crochet. [ au début d'une expression ouvre un littéral de liste ; [ collé après un
postfixe est un index. Les deux siègent dans des états d'automate différents (attente-d'une-valeur
versus détention-d'une-valeur), de sorte qu'aucune entrée n'atteint les deux. L'unique forme d'index
porte alors deux rôles de typage, choisis par le kind du récepteur, pas par la grammaire : sur un
flux scalaire avec un index constant, c'est le delay causal close[1] ; sur un vec, c'est l'accès
à un élément slots[i] (index d'exécution, hors bornes produit na).
Éléments après un identifiant. À l'intérieur d'un bloc, le token après un IDENT en tête
aiguille : : → propriété, = → liaison, { → conteneur de vue, ( → appel (puis un { qui suit
en fait un conteneur ; sinon il reste un appel — le même arbre Call dans les deux cas, la question
du conteneur est sémantique), séparateur/} → simple drapeau ou enfant expression.
when, deux fois. when c: child (un enfant de vue conditionnel, avec :) versus
dot { … } when c (un garde final sur une primitive, sans :) — le deux-points décide.
at contextuel. Dans assert cond "msg" at 500, at n'est un mot-clé que dans cette position
de clause (après la condition et le message optionnel). Il n'entre jamais en collision avec la clé
de propriété at: d'un bloc canvas, qui est un IDENT : au niveau élément.
Le deux-points de capacité. chart:read est un token CAPREF unique reconnu uniquement à
l'intérieur d'une liste de capacités ; le [ de capabilities: [ … ] n'est de même atteint
qu'après capabilities :, jamais en position d'expression. Aucun autre deux-points du langage ne
peut apparaître dans cet état (détails).
color contextuel. color n'est un mot-clé qu'en tête de la séquence à deux tokens color bars ; suivi de n'importe quoi d'autre, c'est un identifiant ordinaire — ce qui permet à color de
nommer à la fois un champ et le kind color (record Stop { color: color }).
La famille ?. Le maximal munch du lexer ordonne ?? > ?. > ? ; les productions siègent
ensuite à trois strates différentes (coalescence, postfixe, ternaire). Le : du ternaire n'est
atteignable qu'après ? expr, un état LR disjoint de tout autre deux-points.
Préfixes de visibilité. pub / private / package sont des modificateurs exactement quand ils
sont suivis d'une tête de déclaration ou d'une liaison ; suivis de =, :, . ou (, ce sont de
simples identifiants. Comme deux identifiants adjacents ne sont jamais une analyse valide, pub def … s'engage de façon déterministe.
#Propriétés formelles
La grammaire est figée contre six critères, chacun avec une vérification par machine — concevoir en éliminant les mauvais états, puis vérifier par machine, plutôt que d'être prudent :
| Propriété | Signification | Garanti par | Vérifié par |
|---|---|---|---|
| Complète | tout programme voulu s'analyse ; chaque construction a une forme de surface | la grammaire est pilotée par corpus : chaque construction cataloguée a contribué une forme | le corpus d'exemples complet s'analyse en ASTs valides |
| Correcte | exactement le langage voulu ; les arbres reflètent la structure (a-b-c = (a-b)-c) |
une grammaire normative ; précédence et associativité totales | suite de conformité : positifs avec l'arbre attendu, négatifs avec le diagnostic attendu ; l'aller-retour analyse → format canonique → ré-analyse produit l'AST identique |
| Cohérente | aucune règle contradictoire ou morte | un artefact de grammaire unique ; chaque non-terminal atteignable | le linter du générateur de grammaire ne rapporte aucun avertissement |
| Non ambiguë | toute entrée valide a exactement un arbre | une classe de grammaire LR dont la construction échoue sur tout conflit ; chaque conflit potentiel résolu par un dispositif nommé | la construction s'achève avec zéro conflit non résolu ; le fuzzing ne trouve aucune entrée à deux arbres |
| Analyse décidable | le parser se termine toujours ; linéaire et incrémental | LR(1) par stratification ; aucun lookahead non borné nulle part | profil de complexité ; le parser incrémental sert l'aperçu en direct dans son budget de frame |
| Sémantiquement cohérente | tout programme analysé reçoit un sens défini ou une erreur précise — pas de trous | des analyses décidables sur l'arbre : l'inférence de kinds sur un treillis de hauteur finie, la causalité du clock-calculus, le pare-feu entre plans, la totalité par construction | le corpus typé assère les kinds attendus ; les négatifs assèrent le diagnostic exact ([ErrDim], tentatives de repaint, …) |
Trois de ces vérifications méritent une phrase chacune :
- La construction est la preuve de non-ambiguïté. La grammaire normative est exprimée une seule fois, comme une grammaire LR Lezer ; un générateur LR rapporte chaque conflit au moment de la construction, de sorte que « la construction est verte » est un contrôle machine de non-ambiguïté — pas une affirmation de revue. (Un formalisme à choix ordonné a été rejeté pour exactement cette raison : il ne détecte pas l'ambiguïté, il la cache silencieusement.) Le même artefact pilote le compilateur, les services de syntaxe de l'éditeur et l'outillage de documentation, de sorte qu'il n'y a rien qui puisse dériver.
- Le corpus fait l'aller-retour. Chaque exemple du corpus est analysé, imprimé par le formateur canonique, et ré-analysé ; les deux arbres doivent être identiques. Un bug de grammaire et un bug de formateur cassent la même barrière. (Le corpus est aussi la suite golden, de sorte qu'un exemple ne peut pas pourrir sans qu'un test passe au rouge.)
- Le parser est total. Le fuzzing assère que toute séquence d'octets soit s'analyse en un arbre unique, soit est rejetée avec des diagnostics propres — jamais un crash, jamais un blocage. L'entrée mal formée fait partie du domaine du langage.
La cohérence sémantique s'étend au-delà de l'analyse : pour chaque kind, l'admissibilité de chaque
opérateur, comparaison, fill et plot est énumérée sur l'ensemble des kinds fini par famille, de
sorte qu'aucune paire kind/construction n'est laissée sans un sens ou une erreur nommée. Le harnais
de vérification est décrit dans compilateur et runtime et les
garanties elles-mêmes dans garanties.
#Additivité et versionnage
La grammaire évolue sous une politique d'additivité stricte :
- Un script valide reste valide indéfiniment. La sémantique d'une construction existante n'est jamais altérée ; la surface ne fait que croître. Chaque extension doit elle-même prouver zéro conflit à la construction de la grammaire avant d'atterrir — l'additivité est une barrière de construction, pas une promesse.
- Les versions de langage sont déclarées, optionnellement. Chaque fichier source peut porter un
marqueur de version de langage ; un fichier sans marqueur signifie « courant ». (Le schéma de
données d'une app — son variant
Msget son modèle — est versionné séparément par l'app elle-même ; voir le plan APP.) - Les mots-clés sont réservés par anticipation. Les mots destinés à une future syntaxe de surface
sont réservés avant que leurs productions ne soient livrées, de sorte qu'aucun programme écrit
aujourd'hui ne peut masquer la syntaxe de demain —
testest l'exemple permanent (modèle de mots-clés). Combiné à la réservation contextuelle, cela rend la croissance future sans collision par construction. - La promesse du codemod. Si une dépréciation devenait un jour inévitable, un codemod automatique réécrirait les sources affectées au chargement — mécaniquement, pas en demandant aux auteurs de migrer à la main. Rien en v1 n'est déprécié.
Pourquoi cette règle existe. Un langage total et déterministe est une promesse sur le futur d'un programme, pas seulement sur son présent : un script qui rejoue de façon byte-identique aujourd'hui doit encore s'analyser — et signifier la même chose — sous chaque compilateur ultérieur. L'additivité stricte est la moitié syntaxique de cette promesse ; les invariants de routine épinglée et de byte-identité en sont la moitié sémantique (compilateur et runtime).
#Voir aussi
- Structure lexicale — les tokens, le saut de ligne significatif, les paliers de mots-clés.
- Kinds — le treillis dimensionnel contre lequel l'arbre analysé est vérifié.
- Opérateurs — l'algèbre dimensionnelle par opérateur, UFCS,
with, la famille?. - Inférence — comment les kinds (et la flèche lambda-versus-paire) sont décidés sur l'arbre.
- Le plan CANVAS · Le plan APP — sémantique des familles d'instructions ci-dessus.
- Garanties — la page de confiance : chaque propriété vérifiée par machine en un seul endroit.
Kinds — le système de types dimensionnels
Chaque valeur en Flux porte un kind : un énoncé de ce que la valeur signifie, pas simplement
de la façon dont elle est stockée. close n'est pas « un float » — c'est un price, un point sur
l'axe des prix. close − open n'est pas « un autre float » — c'est un level, un déplacement le
long de cet axe. rsi(close, 14) est un oscillateur borné, osc(0,100). Parce que le sens est
suivi, le compilateur rejette l'arithmétique dénuée de sens à la compilation, réconcilie sainement
les branches et les séries co-tracées, et infère la présentation — pane, échelle, guides — à partir
du seul kind. Les kinds sont la clé de voûte du langage : la causalité, la totalité, l'inférence de
présentation et les garanties de l'optimiseur reposent toutes sur eux.
Le mécanisme est général. La même machinerie donne un kind à l'arithmétique calendaire, aux montants
décimaux exacts, aux unités physiques (meas[u]), aux records et variants déclarés par
l'utilisateur, et aux messages et vues du plan APP ; les kinds de données de marché sont
l'instanciation phare et servent d'illustrations fil rouge ici. Ce chapitre est la référence
normative pour le système de kinds lui-même — ses deux systèmes coopérants, ses sortes et son ordre
partiel, la borne supérieure et la borne inférieure, ses tags orthogonaux, et les déclarations nommées. Les
règles par opérateur vivent dans Opérateurs ; l'algorithme d'inférence, la
politique d'erreurs complète, et la table complète kind → présentation vivent dans
Inférence.
#Deux systèmes, un seul canal d'erreur
« Borne supérieure et borne inférieure des opérations binaires » est une erreur de catégorie, et le système de
kinds est bâti sur son refus. price − price = level, pourtant price ⊔ price = price — la
soustraction et l'unification répondent à des questions différentes. Flux sépare donc proprement
deux systèmes :
- (A) Le treillis de coercition
(K, ≤, ⊔, ⊓, ⊤, ⊥).a ≤ bsignifie «ase coerce enbsans mentir sur lui-même ». La borne supérieure⊔unifie — les branches d'unif, les deux arguments denz, les séries co-tracées dans un même pane. La borne inférieure⊓contraint — le kind qu'un paramètre exige, l'intersection des bornes d'oscillateur. Le treillis répond à « existe-t-il un kind qui couvre les deux ? » — rien de plus. - (B) L'algèbre dimensionnelle des opérateurs pour
+ − × ÷ < > ==— une table de règles qui calcule le kind résultat de chaque opération à partir des kinds de ses opérandes, dans le style de l'analyse dimensionnelle classique. Ce n'est pas la borne supérieure : elle peut se déplacer à travers le treillis (price − price → level) ou refuser catégoriquement (price + osc → ⊤). - Un seul canal d'erreur :
⊤. Chaque règle non définie dans (B) et chaque incompatibilité dans (A) produit le même élément sommet⊤. Un⊤est une erreur dure dans une position exigeante — un opérande, une cible deplot, un argument — et un opérande est lui-même une position exigeante, c'est pourquoiclose + rsi(…)échoue au+, que quoi que ce soit en aval le lise ou non. Un⊤qui n'atteint aucune position exigeante est un avertissement ([WarnTop]), pas un échec.
Lisez les deux règles ci-dessus ensemble et il en découle une chose facile à manquer, et qui mérite
d'être dite franchement, car c'est la différence entre un système de types strict et un système
simplement bruyant : un mélange de dimensions n'atteint jamais ⊤. Ajoutez un price à un
oscillateur et l'algèbre n'a aucune règle, vous obtenez donc ⊤ et un [ErrDim] dur. Mais
réconciliez un price et un oscillateur — prenez l'une ou l'autre branche d'un if — et la
borne supérieure n'échoue pas. Elle efface la dimension et atterrit sur quantity : un kind réel, un
nombre qui se souvient qu'il est un nombre et a oublié de quoi. C'est un [WarnBranchDim], et la
valeur s'écoule.
Le canal dur est pour le non-sens qui est certain ; le canal doux pour un mélange qui n'est que
suspect. Ce qui les sépare n'est pas la consommation — c'est l'endroit où le ⊤ est né. ⊤
apparaît quand même dans une réconciliation, et y est quand même dur : un if dont les branches
traversent des sortes (un price et une couleur) n'a aucun kind commun du tout, et aucun
effacement ne peut le sauver.
price − price → level passe par B, jamais par A.
Pourquoi cette règle existe. Si
−était modélisée comme une borne supérieure,close − openaurait le kindprice ⊔ price = price— un déplacement mal étiqueté comme un point, et chaque règle en aval (price + level → price, placement d'overlay, choix d'axe) partirait silencieusement de travers. Si les borne supérieures étaient modélisées comme de l'algèbre,if c then ema12 else ema26aurait besoin d'une « règle d'opérateur » pour une question qui est en réalité « ces deux branches partagent-elles un kind ? ». Garder les deux systèmes séparés — et les laisser échouer dans un unique⊤partagé — réconcilie un treillis total (chaque paire de kinds a une borne supérieure) avec un système qui rejette encore le non-sens (cette borne supérieure est⊤exactement là où aucun kind commun honnête n'existe).
fluxfast = volume > sma(volume, 20)
spread = close - open // price − price → level (algebra, system B)
line = if fast then ema(close, 12) else ema(close, 26)
// price ⊔ price → price (join, system A)
plot spread, line
// ✗ plot close + rsi(close, 14) — [ErrDim]: point + dimensionless has no affine meaning
Trois échecs, et ce ne sont pas le même échec :
fluxfast = volume > sma(volume, 20)
mix = if fast then close else rsi(close, 14) // ⚠ [WarnBranchDim] — no common dimension, so
plot mix // the join ERASES it: mix : quantity. It flows.
// ✗ plot close + rsi(close, 14) — [ErrDim]: the ALGEBRA has no rule for point + dimensionless.
// Hard, at the `+`, read or not: an operand is a demanding
// position, and there is nothing suspicious about this — a
// point plus a bare number is not a thing.
// ✗ if fast then close else up — [ErrDim]: price ⊔ color = ⊤. Crossing SORTS is not a mixture
// the lattice can erase — there is no common kind to land on.
[WarnTop] est le lint qui se pose au sommet du premier cas : une liaison dont la dimension a été
effacée et que personne ne lit est presque toujours une erreur en devenir, donc laissez mix non
tracé et vous obtenez un second avertissement disant exactement cela. Consommez-la et le [WarnTop]
disparaît — le [WarnBranchDim] reste, car l'effacement est toujours là.
Un ⊤ se propage aussi comme un poison de kind sans re-diagnostiquer : une seule faute racine
produit un seul message, et chaque nœud qu'il contamine reste silencieux (la règle anti-cascade ;
voir Inférence pour le cône typable qui maintient l'aperçu en direct fonctionnel
autour de lui).
#Le substrat affine
L'asymétrie de l'algèbre n'est pas une convention — c'est de la géométrie. L'axe des prix est un espace affine à une dimension :
priceest un point sur cet axe,levelest un vecteur — un déplacement entre points,ratioest un scalaire sans dimension.
La géométrie affine livre alors gratuitement les règles fondamentales : point − point = vecteur
(price − price → level), point ± vecteur = point (close + atr(14) → price), point ÷ point =
scalaire (price ÷ price → ratio), et « point + point » n'existe pas du tout — c'est exactement
pourquoi close + rsi(close, 14) est refusé plutôt que calculé.
Au-dessus des paires affines, une dimension est un élément d'un groupe abélien d'exposants
entiers sur les cinq générateurs {P, V, T, I, rad} — prix, volume, temps, indice ordinal de
barre, angle. La multiplication additionne les vecteurs d'exposants, la division les soustrait, de
sorte que × et ÷ sont totales sur les dimensions (price × volume → pv est P·V ;
pv ÷ volume → price est P¹V¹−V¹). Seules +, − et les comparaisons d'ordre exigent une
dimension égale, car ajouter des mètres à des kilogrammes — ou des points à des déplacements d'un
axe différent — n'a aucun sens.
fluxgap = close - close[1] // level pt(P) − pt(P) = vec(P)
band = close + 2 * atr(14) // price pt(P) + lit·vec(P) = pt(P)
rel = close / open // ratio P⁰ — dimensionless, centered on 1
flow = close * volume // pv P·V by the group law
speed = change(close, 5) / (5 bars) // slope vec(P) ÷ vec(I) = P·I⁻¹ — price per bar
Trois paires affines vivent dans l'antichaîne, une par axe : price/level sur P,
time/duration sur T, et barindex/barspan sur l'axe ordinal I (règle A1 — l'axe des x
est affine aussi, donc barindex − barindex → barspan, et une pente de régression est
level ÷ barspan → slope = P·I⁻¹, prix par barre, délibérément distincte de P·T⁻¹).
#Les sortes
Le treillis est stratifié en sortes : structure fine à l'intérieur de chaque sorte, et entre
sortes la borne supérieure est ⊤ et la borne inférieure est ⊥. Les extrema sont partagés par toutes les
sortes : ⊤ (n'importe lequel — le canal d'erreur, dur quand exigé) et ⊥ (jamais — le kind de
na, qui habite tout kind : na : ∀κ.κ entre dans le treillis à ⊥ et se subsume vers le haut
vers ce qui est attendu).
#La sorte scalaire
Tout ce qui est dans la sorte scalaire siège sous quantity, qui siège sous ⊤.
La colonne vertébrale sans dimension — lit < {ratio, osc(lo,hi), signal, dir, depth} < num :
lit— un littéral replié en constante (const-folded), polymorphe en dimension :lit ≤safetout scalaire, dimensionné ou non. C'est ce qui rendclose + 10,close > 30000etnz(x, 0)licites sans cérémonie : le littéral adopte le kind que son contexte lui donne.ratio— sans dimension, centré sur 1, multiplicatif (price ÷ price, largeur de bande).osc(lo,hi)— sans dimension et borné, une famille paramétrée par intervalle (rsi → osc(0,100),cmo → osc(-100,100),cmf → osc(-1,1)). Voir le sous-treillis d'intervalles ci-dessous.signal— un événement booléen,{0,1}(comparaisons,cross_up,rising,in_session).dir— une direction,{-1, 0, +1}(règle A6) : catégoriel-discret, le frère plat designal(dir < num). Il type le champ de direction des structures de tendance (superTrend(…).dir), est présenté comme coloration de barres ou marks, et est discriminé par comparaison (st.dir == 1), jamais parmatchet jamais par<. (Il n'y a pas de+unaire dans la grammaire : la valeur+1s'écrit1.)depth— une z-intention sans dimension taggée@z, produite par l'analyse et consommée par le canal z de présentation (projetée en 3-D, aplatie en 2-D).num— sans dimension de rôle inconnu : la borne supérieure de la colonne vertébrale.kdj(…).jet les accesseurs calendaires atterrissent ici.
L'antichaîne dimensionnée — chaque élément ≤ quantity, chaque ≥ lit, et tous incomparables
deux à deux : price = pt(P) · level = vec(P) · volume = V (valeurs signées autorisées :
obv) · pv = P·V (flux monétaire) · time = pt(T) · duration = vec(T) · barindex = pt(I)
· barspan = vec(I) · slope = P·I⁻¹ · angle = rad — plus chaque dimension composée que la
loi de groupe peut produire (P², P²·V⁻¹, …), qui sont des kinds pleinement traçables présentés
dans un pane auto étiqueté par leurs exposants, sans avertissement (règle A3).
quantity — la dimension effacée, borne supérieure de toute la sorte scalaire. C'est ce
qu'une borne supérieure dimensionnellement incompatible produit (voir L2 ci-dessous) : toujours traçable,
mais suspecte, et toujours signalée. quantity est réservé aux dimensions réellement effacées ou
mélangées ; une dimension composée précise comme P² ne s'y dégrade jamais.
#Les sortes catégorielles — color, clock, string
Les kinds catégoriels sont plats (chacun est un seul élément, ne se joignant qu'à lui-même) et vivent hors de l'arithmétique ; chacun est consommé par un éliminateur dédié plutôt que calculé :
color— une valeur de style RGBA, consommée par les fills, la coloration de barres et les canaux de style (Color).clock— un calendrier de rééchantillonnage : un indice ordinal avec les constructeurstf("1d"),renko(box),pnf(box, rev),range(r). Unclockest une valeur de première classe (on peut en sélectionner un avec unif, en prendre un commeinput) mais il n'est éliminé que par@(Temps et état) ; il n'est jamais tracé et jamais comparé.string— texte UTF-8 borné et immuable (règle A12) : étiquettes, invites, messages d'alerte.stringest plat commecoloret hors de l'arithmétique numérique, avec exactement une surcharge d'opérateur :string + string → stringest la concaténation — l'unique ligne catégorielle de la table de règles de+. Il n'y a pas destring < string(le tri dépendant de la locale est exclu par le déterminisme), l'égalité est l'égalité bit-à-bit, et unstringest consommé par les canaux attendant du texte — il n'est jamais tracé comme une série (Text).
#Les sortes structurelles — vec et record
Les kinds structurels se composent composante par composante ; les formes non concordantes sont du non-sens et se résolvent aux extrema.
vec<κ>[n]— un vecteur deκavec une capacité repliée en constante (const-folded)n(n ≤ N_max = 10 000). Capacité, pas compte exact : voir ci-dessous. Introduit parwindow(e, n), les littéraux de liste,vec.fill(N, x)etvec.range(N); covariant enκ.Notation.
vec<κ>[n]est le métalangage de cette documentation. Dans le source, le même kind s'écritvec(κ, N)— avec des parenthèses, car<et>sont exclusivement les opérateurs de comparaison et jamais des délimiteurs de type.record{f₁:κ₁, …}— un produit nommé. Cette sorte est porteuse pour le catalogue : une large part des kernels intégrés renvoient des records —bollinger → record{upper, middle, lower: price},macd → record{macd, signal, hist: level},superTrend → record{st: price, dir: dir},adx → record{adx, plusDi, minusDi: osc(0,100)}. La projection este.f; la mise à jour fonctionnelle este with { f: v }(préservant la forme — les champs non listés sont reportés, les champs inconnus sont[ErrField]).
#Les sortes du plan APP — variant et ui
Le plan APP ajoute deux sortes par la même construction, directement sous ⊤, sans rouvrir le
treillis fondamental scellé :
variant{Tag₁:κ₁ | …}— la somme étiquetée, dual catégoriel derecord(le produit). Son introduction est l'injection de constructeurTag(e), son éliminateur estmatch(symétrique à la projection). L'ordre, la borne supérieure, la borne inférieure et la propagation de⊤sont dérivés identiquement àrecord: les ensembles d'étiquettes sont invariants (mêmes étiquettes → raffiner par charge utile, de façon covariante ; ensembles d'étiquettes différents → incomparables, borne supérieure⊤, borne inférieure⊥). Les messages, commandes et souscriptions d'une app sont desvariants (Le plan APP).ui— la sorte des primitives de vue : un ensemble fermé et vérifié de primitives, plat commecolor, consommé par le renderer de l'hôte, et jamais coercible en un scalaire (ui ⊔ κ = ⊤,ui ⊓ κ = ⊥entre sortes — une primitive en position scalaire est attrapée immédiatement).
#Pas de sorte fonction
Il n'y a délibérément aucun kind flèche dans le treillis. Les lambdas sont de seconde classe :
un (p⃗) -> body n'est admis que là où un kernel d'ordre supérieur attend une fonction
(window/fold/map/scan/loop, vec.where, sortBy, …) et est inliné à cet endroit. Un
lambda ne peut donc jamais être un champ de record, un élément de vec, une charge utile de
variant ou une valeur liée par let — c'est ce qui garde chaque valeur représentable comme de
simples données, chaque buffer borné, et les artefacts compilés exempts de closures et de pointeurs
de fonction. La récursion passe par def (dont le graphe d'appels doit être acyclique —
[ErrTotalRec]) ou par un scan/loop borné, jamais par un lambda auto-référençant.
#Le catalogue des kinds
Une ligne par kind. La table de présentation complète (pane/overlay/échelle/guides/classe css par kind) est normative dans Inférence ; la dernière colonne ici est le résumé en une ligne.
| kind | signification | dimension | producteurs typiques | se présente comme |
|---|---|---|---|---|
lit |
littéral const-folded, polymorphe en dimension | adopte le contexte | littéraux numériques, params const | adopte son consommateur |
ratio |
sans dimension, centré sur 1, multiplicatif | P⁰ | close/open, bbw, vortex |
pane autour de 1, guide 1 |
osc(lo,hi) |
oscillateur borné (famille d'intervalles) | sans dimension | rsi, stochastic, mfi (0,100) · cmf (−1,1) |
pane, [lo,hi] fixe, ligne médiane + guides raffinés |
osc(-∞,∞) |
oscillateur non borné centré sur 0 (A4) | sans dimension | roc, cci, trix, coppock, fisher, kst |
pane, échelle auto, guide 0 |
signal |
événement booléen {0,1} |
sans dimension | comparaisons, cross_up, in_session |
marks / fills / couleur de barre — jamais une ligne |
dir |
direction {-1,0,+1} (A6) |
sans dimension | superTrend(…).dir, côté SAR |
coloration de barres / marks — jamais une ligne |
depth |
z-intention normalisée (@z) |
sans dimension | (produit par l'analyse ; consommé par z) | axe z en 3-D, aplati en 2-D |
num |
sans dimension, rôle inconnu (LUB de la colonne) | sans dimension | kdj(…).j, accesseurs calendaires |
pane par défaut, échelle auto |
price |
point sur l'axe des prix | pt(P) | close, ema, vwap, champs de bollinger |
overlay sur l'axe des prix partagé |
level |
déplacement de prix | vec(P) | close−open, atr, stdev, champs de macd |
pane centré sur 0 |
volume |
compte de l'actif de base (signé autorisé) | V | volume, obv, ad |
pane, échelle auto signée, guide 0 |
pv |
flux monétaire | P·V | eldersForceIndex |
pane, auto, guide 0 |
time |
instant sur la ligne de temps | pt(T) | time |
axe des x / annotation — jamais une série |
duration |
temps écoulé exact (machine) |
vec(T) | time − time[n] |
axe des x / annotation |
barindex |
position ordinale de barre | pt(I) | rollingExtremaIndex |
axe des x / ancrage — jamais une série |
barspan |
compte de barres | vec(I) | barssince |
compteur de pane [0,max] |
slope |
prix par barre | P·I⁻¹ | lrSlope (= level ÷ barspan) |
pane centré sur 0 |
angle |
angle avec unité @deg|@rad |
rad | chopZone (angle@deg) |
canal de style / pane [-π,π] |
| dims composées | tout autre vecteur d'exposants (A3) | vecteur-ℤ sur {P,V,T,I,rad} | price×price (P²), eom (P²·V⁻¹) |
pane auto étiqueté par les exposants |
quantity |
dimension effacée / mélangée (LUB scalaire) | effacée | borne supérieures avec perte, sommes affines non normalisées | pane par défaut — l'effacement reste visible |
color |
valeur de style RGBA | — | constructeurs de couleur, dégradés | consommé par fill / couleur de barre |
clock |
calendrier de rééchantillonnage | — | tf, renko, pnf, range |
jamais tracé — éliminé par @ |
string |
texte UTF-8 borné immuable | — | littéraux, interpolation, fmt.* |
consommé par étiquettes/alertes — jamais une série |
vec<κ>[n] |
vecteur à capacité fixe | élément κ | window, littéraux de liste, vec.fill |
réduit / indexé, ou une representation déclarée |
record{…} |
produit nommé | par champ | bollinger, macd, adx, superTrend |
éclaté par champ |
variant{…} |
somme étiquetée (plan APP) | par charge utile | Msg/Cmd/Sub, inputs étiquetés |
consommé par match — jamais tracé |
ui |
primitive de vue (plan APP) | — | button, panel, scene{…} |
consommé par le renderer de l'hôte |
⊤ |
n'importe lequel — le canal d'erreur | — | borne supérieures échouées, règles d'algèbre manquantes | non présentable — [ErrPlot] |
⊥ |
jamais — le kind de na |
— | na |
non présentable |
decimal(scale), period, le tag d'actif (B,Q[,@v]), meas[u] et metric[id] sont des tags
orthogonaux sur ces kinds, pas des kinds à part entière — voir Tags
orthogonaux ci-dessous.
#L'ordre partiel
num tandis que
l'antichaîne dimensionnée ne monte que jusqu'à quantity, et lit siège en sûreté sous chaque
scalaire.
La décision la plus lourde de conséquences dans l'ordre : les dimensions forment une antichaîne,
et num est incomparable à chaque dimension — price ⊀ num. Les kinds dimensionnés montent
jusqu'à quantity, jamais jusqu'à num.
Pourquoi cette règle existe. Si
price ≤ numtenait, alorssignal ⊔ price = num— un événement booléen et un price se réconcilieraient silencieusement en un nombre traçable ordinaire, et la détection d'erreurs qui motive tout le système s'évaporerait exactement au moment où elle compte (un co-tracé ou unifmélangeant des choses incompatibles). En routant plutôt tous les kinds dimensionnés versquantity, un mélange n'est jamais silencieux : en arithmétique il est refusé ([ErrDim]), et en réconciliation de branches il compile versquantityet est signalé ([WarnBranchDim]) — visible, traçable, suspect.
Descendre l'ordre gagne de l'information, et la présentation lit toujours le kind le plus bas connu — c'est pourquoi l'inférence synthétise le kind minimal (principal) et ne coerce qu'aux sites de consommation (voir Inférence).
#Deux paliers d'arêtes de coercition
Chaque arête de l'ordre est strictement montante, et chaque arête appartient à exactement l'un de deux paliers disjoints :
≤safe— silencieux.⊥ ≤ κ ≤ ⊤pour tout κ ; l'élargissement des bornesosc(osc(0,100) ≤ osc(-100,100)) ; l'élargissement de la capacitévec(vec<κ>[k] ≤safe vec<κ>[N]pourk ≤ N) ; les arêtes de la colonne{ratio, osc, signal, dir, depth} ≤ numetnum ≤ quantity; etlit ≤chaque scalaire.≤lossy— compile, avertit, propose un correctif rapide. Effacer une dimension (D ≤ quantitypour toutDdimensionné), et accepter une combinaison affine non normalisée. La perte d'information est licite mais jamais silencieuse.
L'antisymétrie survit parce que les deux paliers montent strictement et ne se chevauchent jamais.
#Polymorphisme littéral
lit est le plancher ergonomique de la sorte scalaire. Un littéral const-folded se coerce en sûreté
vers tout scalaire, de sorte que les choses naturelles sont licites sans cérémonie :
fluxhot = close > 30000 // price vs lit → the lit reads as price → signal
lift = close + 10 // lit adopts price → price
safe = nz(change(close, 1), 0) // level ⊔ lit = level
shift = rsi(close, 14) - 50 // osc(0,100) − lit → osc(-50,50): interval propagated
Le compilateur linte tout de même les littéraux impossibles contre les bornes revendiquées
(rsi(close,14) > 150 est [WarnLit]), car un lit adoptant un kind ne suspend pas le bon sens
arithmétique.
#Capacité, pas longueur : vec<κ>[n]
Le n dans vec<κ>[n] est une capacité const-folded — une borne supérieure, pas un compte
exact. Un vecteur plus court habite un vecteur plus long via l'élargissement ≤safe
vec<κ>[k] ≤safe vec<κ>[N] (k ≤ N), sa queue [k, N) se lisant na. La mémoire reste bornée par
le plafond déclaré, donc la totalité tient ; l'itération bornée (map/fold) est consciente de na
sur la capacité, de sorte qu'une queue élargie ne contribue rien. [ErrLen] ne se déclenche que
lorsque deux capacités déclarées sont réellement incompatibles — sinon l'opérande le plus court
s'élargit vers le plus long. C'est aussi pourquoi les littéraux de liste de longueurs différentes se
joignent proprement : vec<κ>[k] ⊔ vec<κ>[m] = vec<κ>[max(k,m)].
fluxw = window(close, 20) // vec<price>[20] — a specific produced length
wide = nz(w, window(close, 50)) // vec<price>[50] — 20 widens into 50, tail na
Les longueurs non constantes ou au-delà du plafond sont [ErrTotal] (n ≤ N_max = 10 000) — le
prix d'une « mémoire totale et bornée » est qu'une capacité est toujours un fait connu à la
compilation.
#Borne supérieure et borne inférieure
#Le catalogue de la borne supérieure
κ ⊔ κ = κ ⊥ ⊔ κ = κ ⊤ ⊔ κ = ⊤ lit ⊔ κ = κ
osc(L,H) ⊔ osc(L',H') = osc(min L L', max H H') — the envelope
spine siblings → num (osc ⊔ ratio = num · dir ⊔ signal = num)
D ⊔ D' (D ≠ D') = quantity D ⊔ num = quantity signal ⊔ price = quantity
vec<S>[k] ⊔ vec<T>[m] = vec<S ⊔ T>[max(k,m)]
record{f:A} ⊔ record{f:B} = record{f: A ⊔ B} — differing field sets → ⊤
variant: same label set → per-payload join — differing label sets → ⊤
across sorts → ⊤
Deux placements méritent leurs raisons :
signal ⊔ price = quantity, pas⊤(correction L2).quantityest une borne supérieure commune des deux (signal ≤ num ≤ quantity,price ≤ quantity), donc⊤ne serait pas la plus petite — et la politique d'erreurs veut exactement ce résultat : la réconciliation de branches de dimensions mélangées est un avertissement ([WarnBranchDim]sur unquantitytraçable), tandis qu'une dimension mélangée dans une position arithmétique reste un[ErrDim]dur. La frontière entre dur et doux court entre l'algèbre et la réconciliation, pas à travers le treillis.dir ⊔ signal = num, pasquantity. Les deux sont des feuilles de la colonne sans dimension sousnum, doncnumest leur borne supérieure — exactement commeosc ⊔ ratio = num. Promouvoir versquantityplacerait un élément plus grand au-dessus d'une borne supérieure existante et détruirait l'unicité de la LUB.
Les bornes supérieures atterrissent sur ⊤ exactement là où réside le vrai non-sens : entre sortes
(price ⊔ color, num ⊔ record{…}), sur des formes structurelles non concordantes (ensembles
de champs ou d'étiquettes qui diffèrent), et — la seule exception énumérée au sein d'une sorte — sur
une dimension égale avec des tags de représentation différents ([ErrRepr], ci-dessous). Les
simples mélanges de dimensions n'atteignent jamais ⊤ ; ils montent vers quantity et restent
visibles.
fluxc = volume > sma(volume, 20)
a = if c then rsi(close, 14) else cmo(close, 14)
// osc(0,100) ⊔ osc(-100,100) = osc(-100,100) — envelope
b = if c then close else rsi(close, 14)
// price ⊔ osc = quantity — compiles, [WarnBranchDim], suggest two panes
#Le catalogue de la borne inférieure et la correction lit
⊤ ⊓ κ = κ ⊥ ⊓ κ = ⊥
quantity ⊓ D = D num ⊓ ratio = ratio — a constraint refines
osc ⊓ osc = interval intersection (disjoint → lit, by L1)
incomparable scalars ⊓ = lit — correction L1
across sorts → ⊥ (lit does not cross sorts)
variant/record: same shape → per-component meet — differing shapes → ⊥
La correction L1 est porteuse : la borne inférieure de deux scalaires incomparables — disons
price ⊓ volume, ou dir ⊓ signal — est lit, pas ⊥. lit ≤safe les deux opérandes, donc
lit est une borne inférieure commune, et la plus grande borne inférieure doit se situer au niveau
ou au-dessus de chaque borne inférieure commune ; forcer ⊥ contredirait
lit ≤ price ∧ lit ≤ volume ⇒ lit ≤ price ⊓ volume et casserait la loi d'absorption
(price ⊓ (price ⊔ level) = price ⊓ quantity = price tient précisément parce que les bornes inférieures
raffinent plutôt qu'elles n'annihilent). Au sein de la famille des oscillateurs, la même correction
résout l'intersection vide : deux kinds osc d'intervalles disjoints sont des scalaires
incomparables, donc leur borne inférieure est lit — l'intervalle vide ∅ n'est le bas que du
sous-treillis d'intervalles vu isolément, jamais la GLB globale.
#Le treillis d'intervalles osc
osc(lo,hi) est une famille ordonnée par inclusion d'intervalles :
osc(L,H) ≤ osc(L',H') ⟺ [L,H] ⊆ [L',H']. La borne supérieure est l'enveloppe, la borne inférieure est
l'intersection, et les bornes peuvent être des constantes infinies : osc(-∞,∞) (règle
A4) est le kind de toute la famille du pourcentage de variation (roc, cci, trix,
coppock, fisher, kst, chaikinVolatility, volumeOscillator) — sans dimension, centrée sur
0, additive, ce qui la distingue proprement de ratio (centré sur 1, multiplicatif) et de num
(rôle inconnu).
Les bornes sont des revendications de présentation, pas des invariants d'exécution. Le kind
osc(0,100) assère « ceci est présenté sur une échelle fixe 0–100 avec une ligne médiane » ; seul
un clamp explicite rend une borne réelle à l'exécution, et l'arithmétique propage honnêtement
les intervalles revendiqués (rsi − 50 → osc(-50,50)). La règle A5 scinde le catalogue en
conséquence : une borne réellement imposée par le calcul (rsi, stochastic, mfi, cmf) produit
une échelle [lo,hi] fixe ; une borne simplement conventionnelle (adx, correl,
balanceOfPower) est une revendication — échelle auto plus guides indicatifs. Une borne qui
n'aurait pas réussi à se replier en constante retomberait sur num ; aucun kernel du catalogue n'en
produit.
#Propagation profonde de ⊤
vec<⊤>[n], record{f: ⊤, …} et variant{T: ⊤ | …} se réduisent tous à ⊤. Sans cela, une
non-concordance enfouie — vec<price> ⊔ vec<color> produisant vec<⊤> — se glisserait sous chaque
garde κ ≠ ⊤ et ferait surface comme un mystère d'exécution au lieu d'un diagnostic à la
compilation. Une erreur à n'importe quelle profondeur est une erreur de la valeur entière.
#Clôture, vérifiée par énumération
Avec L1 et L2 en place, chaque paire de kinds a une LUB unique et une GLB unique. L'ensemble des
kinds est fini par famille structurelle : le cœur dimensionnel est fini, et chaque axe paramétré
— bornes osc, capacité vec, échelle et précision decimal, les tags de représentation, les
composantes du tag d'actif et l'annotation de paire fx — est un tag const-folded dont les clés
sont énumérables au point de vérification. Les lois du treillis (antisymétrie, absorption, unicité de
LUB/GLB) sont donc vérifiées par machine par énumération exhaustive, famille par famille — pas
simplement argumentées (Garanties). Les déclarations nommées
record/variant préservent cela : leur graphe de références doit être acyclique ([ErrTotalType]),
de sorte que chaque kind nommé s'aplatit en une structure de hauteur finie.
#L'algèbre des opérateurs en bref
Les tables complètes et normatives par opérateur sont dans Opérateurs. La forme du
système B, en un écran — une règle manquante signifie ⊤, d'où [ErrDim] à la consommation :
±(dimension égale ; affine) :pt(d) − pt(d) → vec(d)·pt(d) ± vec(d) → pt(d)·vec ± vec → vec·ratio ± ratio → ratio·osc ± osc →intervalle propagé ·lit ± x → x· dimensions différentes →⊤. Une surcharge catégorielle :string + string → string(concaténation), et rien d'autre ne touchestring.- Combinaisons affines (
(high + low) / 2) : l'expression est normalisée enΣ λᵢ·ptᵢavec des coefficients const-folded ;Σλ = 1 → price(un point pondéré),Σλ = 0 → level(un déplacement pur), tout le reste →quantity+[WarnAffine]. Multiplier par unlitpréserve le rôle, doncsma(close,20) + 2 * stdev(close,20)se type commeprice + level → price. La règle A7 applique ceci après inlining, de sorte que les composites récursifs se typent correctement :dema = 2·ema − ema(ema)aΣλ = 1 → price. ×et÷(la loi de groupe, règle A2) : les vecteurs d'exposants s'additionnent et se soustraient, de sorte que les deux opérateurs sont totaux sur les dimensions —D × ratio → D,price × volume → pv,price × price → P²(traçable, A3),pv ÷ volume → price,pv ÷ price → volume,level ÷ barspan → slope.- Comparaisons d'ordre (
< > <= >= cross_*) : admises uniquement sur la sorte scalaire ordonnée avec des dimensions compatibles (κₐ ⊔ κ_b ∉ {quantity, ⊤}) →signal.price < oscest[ErrDim];color,clock,string,record,vec,variant,uine sont jamais ordonnés ;direst catégoriel-discret et hors de l'ordre. - Égalité (
== !=) : égalité décidable →signal— égalité bit-à-bit pour les scalaires,string,dir,color; égalité profonde, consciente denapourrecord/vec/variant(formes non concordantes →[ErrDim]) ;clocketuisont consommés, jamais comparés ([ErrArg]). - Logique (
and or not) :signal × signal → signal— il n'y a pas d'arithmétique sursignal.
fluxtrend = ema(close, 50) // price ([CallPoly]: α = price)
hits = count(close cross_up trend, 20) // osc(0,20) — const-folded n
since = barssince(close cross_up trend) // barspan — a bar count, not a num
bad = close < rsi(close, 14) // ✗ [ErrDim] — price ⊔ osc = quantity
#Tags orthogonaux
Au-delà de sa dimension, un kind peut porter jusqu'à trois tags orthogonaux, plus un axe qui ne
vit que sur ratio :
- un tag de représentation numérique —
f64(défaut) oudecimal(scale)(règle A11) ; - un tag de représentation du temps sur la dimension
T—machine(défaut ; temps écoulé exact —duration) oucalendar(period, règle A10) ; - un tag d'actif — un n-uplet à arité fixe à clés-chaînes
(B, Q [, @v])sur les kinds de dimension prix (règle A9) ;
plus (4) l'annotation de paire de devises de fx — un axe porté uniquement par ratio,
dont le sommet est le ratio nu lui-même. Comme ratio ne porte jamais de tag d'actif, aucun kind
ne porte jamais quatre tags : un decimal price[BTC,USD] en détient trois (représentation + actif +
dimension), et c'est le plafond.
[ErrRepr]), les composantes d'actif s'élargissent tandis que ± exige
l'identité ([ErrDim]).
Les deux familles d'axes obéissent à deux régimes différents — et la différence est le point essentiel :
- Les tags de représentation (1, 2) ne coercent jamais silencieusement. Sur deux kinds de
dimension égale mais de tags de représentation différents, la borne supérieure et la borne inférieure produisent
⊤— faisant surface comme[ErrRepr]dans une position exigeante. Le seul pont est une conversion explicite (toDecimal/toFloat; les constructeursperiodpour le temps calendaire). Ceci reflète le refus arithmétique : on n'ajoute pas unf64 priceà undecimal price, ni undurationmachine à unperiodcalendaire, sans le dire. - Le tag d'actif (3) s'élargit par composante. Chaque composante a son propre sommet
(
⊤base,⊤quote,⊤venue) ; une borne supérieure élargit la composante qui diffère et préserve celle qui concorde —price[B1,Q] ⊔ price[B2,Q] = price[⊤base, Q]— et ne lève jamais[ErrRepr]. La sûreté vient non pas de la borne supérieure mais de l'algèbre±taggée :+,−, les comparaisons et la règle affine exigent toutes des tags identiques composante par composante, et refusent avec[ErrDim]sinon. L'élargissement garde la composition possible (lesprice[BTC,USD]de deux places peuvent former un indice) ; la barrière d'identité attrape encore chaque bug d'unité.
Pour les deux régimes, des tags identiques retombent sur les règles de dimension simples avec le
tag préservé à travers ± × ÷ : decimal price − decimal price = decimal level,
period + period = period, price[BTC,USD] − price[BTC,USD] = level[BTC,USD].
#Représentation numérique — f64 et decimal(scale)
decimal(scale) est un virgule fixe exact, orthogonal à la dimension : un decimal a toujours
une dimension, et la loi de groupe est inchangée (money ÷ qty → price avec money ≡ pv,
qty ≡ volume). L'échelle (scale) chevauche le kind et le système la calcule : ± prend le max des
deux échelles, × les additionne, et ÷ est la seule opération non close — elle requiert une
échelle cible et un mode d'arrondi explicites. La précision déclarée choisit le support le plus
étroit (i64/i128/i256) ; dépasser la précision déclarée produit un na déterministe plus un
diagnostic, jamais un débordement silencieux. Les littéraux sont suffixés : 1.50d est decimal(2),
1.5 est f64. Le domaine du decimal est l'argent réglé — montants d'ordres, fills, soldes ; les
kernels d'analyse restent f64.
fluxexact = toDecimal(close, 2) // decimal(2) price — explicit entry, rounds
bad = close + exact // ✗ [ErrRepr] — f64 price + decimal price
worse = if c then close else exact // ✗ [ErrRepr] — the join refuses the same mixture
fine = toDecimal(close * volume, 2) + toDecimal(close * volume, 2) // decimal(2) pv — a money amount adds; same representation, scales combine
#Représentation du temps — machine et calendaire
time = pt(T) est un instant (une époque int64 sous le capot) ; duration = vec(T) est le
temps machine écoulé exact. period est le même vec(T) taggé calendar — une quantité
en mois-et-jours consciente des fuseaux et des transitions d'heure d'été. t + duration et
t + period sont tous deux dimensionnellement pt(T) + vec(T) → pt(T) ; le tag est ce qui sépare
« exactement 24 heures plus tard » de « la même heure d'horloge demain », et les deux ne se mélangent
jamais silencieusement :
fluxage = time - time[20] // duration — machine-exact
renew = time + time.months(1) // time — calendar arithmetic, DST-aware
// ✗ (time - time[20]) + time.months(1) — [ErrRepr]: a duration and a period do not add
Les valeurs period ne sont produites que par les constructeurs time.years/months/weeks/days(n)
(composables : time.months(1) + time.days(10)) ; les accesseurs calendaires (year, dayOfWeek,
…) projettent un time dans une zone déclarée en tant que num, et l'arithmétique calendaire hors
plage produit un na déterministe, jamais un débordement. La base de données de fuseaux horaires
épinglée et la routine de conversion partagée qui rendent ceci reproductible sont spécifiées avec le
pilier compute (Compute).
#Le tag d'actif — (B, Q [, @v])
Un price n'est jamais un nombre nu : c'est un taux, quote-par-base. Le tag d'actif rend cette identité dimensionnelle, par kind :
| kind | tag | lecture |
|---|---|---|
price[B,Q], level[B,Q] |
base + quote | un taux et son déplacement — les deux composantes chevauchent |
volume[B] |
base seule | un compte de l'actif de base — pas de devise |
pv[Q] |
quote seule | un montant monétaire — la base s'annule dans price × volume et est délibérément abandonnée |
La quote chevauche chaque dimension contenant le facteur P (y compris P² composé, slope) ; les
kinds sans dimension ne portent aucun tag d'actif. Le défaut est mono-actif
price[primary, baseccy], byte-identique à un script mono-série qui ne mentionne jamais l'axe ; les
tags ne deviennent concrets que là où les clés de séries sont des littéraux statiques, et
s'élargissent vers les sommets de composantes sinon. La composante de place @v est sur activation
(désactivée par défaut ; l'absence est le niveau composite concret, pas ⊤venue).
fluxc = volume > sma(volume, 20)
btcUsd = series("BTC-USD").close // price[BTC,USD]
btcEur = series("BTC-EUR").close // price[BTC,EUR]
ethUsd = series("ETH-USD").close // price[ETH,USD]
diff = btcUsd - btcUsd[1] // level[BTC,USD]
mixA = btcUsd + ethUsd // ✗ [ErrDim] — mixing assets
mixQ = btcUsd + btcEur // ✗ [ErrDim] — mixing currencies
cmpQ = btcUsd < btcEur // ✗ [ErrDim] — comparisons gate on identity too
rel = btcUsd / ethUsd // ratio — cross-base relative strength, tag dropped
either = if c then btcUsd else btcEur // price[BTC,⊤quote] — the join widens, never errors
#fx et money — des kinds existants, taggés
La conversion de devises n'a besoin d'aucune nouvelle sorte. fx[Q1/Q2] est le kind ratio
portant une annotation de paire de devises optionnelle et ordonnée — le quatrième axe de tag, ne
vivant que sur ratio, sommet = le ratio nu. money[Q] est un alias pour decimal pv[Q].
Zéro nouvelle sorte, zéro nouvelle hauteur de treillis. Un fx naît d'une division de prix de même
base et de quote différente, ou d'un flux déclaré comme série fx ; la conversion est une annulation
d'unités vérifiée par les types :
fluxc = volume > sma(volume, 20)
btcUsd = series("BTC-USD").close
btcEur = series("BTC-EUR").close
eurGbp = series("EUR-GBP").close // an fx series, declared as one
usdPerEur = btcUsd / btcEur // fx[USD/EUR] — same base, quotes differ
inUsd = btcEur * usdPerEur // price[BTC,USD] — the shared quote cancels
flipped = 1 / usdPerEur // fx[EUR/USD] — the reciprocal edge
avg = if c then usdPerEur else eurGbp // ratio — differing pairs join to bare ratio
Une non-concordance de paire dans ×/÷ s'élargit vers ⊤quote plutôt que d'échouer (le
régime × n'échoue jamais durement) ; l'annotation de paire est exclue de la barrière d'identité
de comparaison, de sorte que deux taux fx se comparent toujours comme les ratios qu'ils sont. Le
catalogue complet des arêtes — dérivation, réciproque, chaînage triangulaire, conversion dans les
deux ordres d'opérandes — est dans Actifs et devises.
#Annotations d'unité et de métrique
Deux axes de tags supplémentaires étendent la même construction au-delà de la finance, tous deux
portés par num seul, tous deux avec le num nu comme sommet :
meas[u](règle A14) — quantités généralistes : une annotation d'unité dontuest un produit de symboles de catalogue à exposants entiers (meas[m],meas[m·s⁻¹],meas[kg·m·s⁻²]), augmentée d'un bitpoint|deltaaffine pour les échelles à zéro arbitraire (un pointunit.tempC(20)versus un déplacementunit.tempCDelta(5)— la même discipline point/vecteur queprice/level, appliquée à la température).±et les comparaisons filtrent sur des unités identiques ;×/÷composent les cartes d'exposants avec des facteurs de conversion épinglés exacts et s'annulent complètement enratio; les produits mixtes avec les kinds dimensionnés financiers sont un mur explicite ([ErrDim]). L'algèbre complète, le verbe de conversion et les règles d'entrée vivent dans Unités.
fluxwarm = unit.tempC(20) + 5 // a point ± lit → point — the lit reads as a delta
span = unit.tempC(25) - unit.tempC(20) // point − point → a 5-degree delta
- Réservé.
metric[id](règle A15) — annotations d'identité pour les séries non-prix (indices macro, métriques on-chain, analytics). Le±de même id préserve, des ids différents refusent (classe[ErrDim]),metric[A] ÷ metric[B] → ratio, et les familles préservant le kind portent l'annotation à travers. L'axe est délibérément inerte en v1 — aucune série non-prix n'entre dans le plan ANALYSIS tant que l'amendement n'est pas armé ; sa conception est spécifiée avec Actifs et devises.
#Records et variants nommés
Les kinds structurels peuvent être déclarés et nommés au niveau du programme :
fluxrecord Band { upper: price, middle: price, lower: price }
variant Tool { Select | Draw | Erase }
variant Save { Saved | Failed(code: num) }
t = Tool.Select // qualified constructor — resolves homonyms
s = Save.Saved // across variants that share a label
Un constructeur nullaire est lu comme une valeur de son variant ; un constructeur portant une charge
utile est appliqué comme une fonction, sa charge utile vérifiée contre les kinds de champs déclarés.
La forme qualifiée T.C sélectionne le constructeur du variant nommé T — résolue à la
résolution de noms, avant que la projection de champ ne s'applique jamais, de sorte que les
collisions d'étiquettes entre variants sont un non-problème.
Deux disciplines maintiennent les déclarations nommées à l'intérieur des garanties du treillis scellé :
- Les records sont monomorphes en v1. Un
defqui projette un champ requiert un kind de record concret connu à l'inférence. Post-v1. une extension à lignes ouvertes (polymorphisme de lignes) est une extension additive nommée — jamais un prérequis v1. - Le graphe de références doit être acyclique. Un
record/variantdéclaré peut référencer d'autres déclarations nommées, mais tout cycle — direct ou transitif — est rejeté à la résolution de noms avec[ErrTotalType], le jumeau au niveau kind du graphe d'appelsdefacyclique ([ErrTotalRec]). Chaque kind nommé s'aplatit donc en une structure de hauteur finie, et la propriété de hauteur finie sur laquelle reposent la clôture et la vérification par machine s'étend inchangée aux déclarations utilisateur.
fluxrecord Node { next: Node } // ✗ [ErrTotalType] — cyclic reference, unbounded height
#Un exemple détaillé : le treillis refuse la mauvaise physique
Le graphique Point & Figure est un test de stress que le système de kinds passe sans un seul nouveau
kind — et il montre la lentille dimensionnelle à l'œuvre pour de vrai. Chaque pièce reçoit son kind
naturellement : pnf(box, rev) est un clock ; l'état de colonne est un scan sur un simple
record ; et la taille de boîte doit être un level, parce que la géométrie l'exige :
fluxbox = atr(14) // level — a displacement, by construction
anchor = close // price — a point
edge = anchor + 3 * box // price ✓ — pt(P) + lit·vec(P) = pt(P)
// the column state the frontier belongs to — a plain bounded scan over a record
state = scan({ dir: 1, extreme: close, count: 0 },
(p) -> if close > p.extreme + box
then { dir: 1, extreme: close, count: p.count + 1 }
else p)
Supposez que vous ayez déclaré la boîte comme un price. Alors anchor + box serait price + price
— point + point, aucun sens affine — et le compilateur répond [ErrDim] sur-le-champ. Le treillis
ne fait pas que permettre le modèle correct ; il refuse le modèle incorrect. La même lentille type
le reste de l'état : count est un compte de boîtes sans dimension (num), donc count * box est
num · vec(P) → level et extreme + count * box retombe sur price ; et count n'est délibérément
pas un barspan — les boîtes ne sont pas des barres, et les kinds empêchent les deux comptages
d'être jamais confondus.
#Erreurs et na au niveau kind
La politique d'erreurs complète — y compris les diagnostics de causalité, de totalité et de pare-feu — est spécifiée dans Inférence ; le squelette au niveau kind est :
- Dure (
[ErrDim],[ErrRepr],[ErrLen],[ErrField],[ErrArg],[ErrPlot],[ErrTotalType],[ErrTotalMatch]) : une garantie porteuse est violée ou le non-sens est certain — un⊤/⊥dans une position exigeante, une condition annexe violée, une non-concordance de forme. - Avertissement (
[WarnTop],[WarnAffine],[WarnBranchDim],[WarnBoundsØ],[WarnLit]) : suspect mais possiblement intentionnel, ou une perte d'information récupérable — exactement le palier≤lossyet le cas du⊤mort.
na mérite son énoncé au niveau kind : na synthétise ⊥ et se subsume dans tout kind attendu, de
sorte que l'absence est une valeur de tout kind, jamais une forme séparée. Le na d'exécution se
propage à travers l'arithmétique avec le kind préservé, et toute comparaison touchant na est
elle-même na — jamais true ni false. On teste l'absence explicitement :
fluxm = sma(close, 200) // price — na during warm-up
odd = m == na // legal, but always na — never true
hit = is_na(m) // signal — the way to ask
has = is_some(m) // signal — its dual
Un match sur un sujet possiblement na doit le couvrir (un bras na ou _), et déstructurer un
record na produit na dans chaque champ — l'absence se compose structurellement, sans cas
particuliers à mémoriser.
#Voir aussi
- Opérateurs — l'algèbre dimensionnelle complète par opérateur, UFCS,
with. - Inférence — inférence de kinds bidirectionnelle, inférence de présentation, la
politique d'erreurs,
na. - Temps et état — flux, delay, clocks et
@, causalité. - Unités — l'algèbre
meas[u]complète : conversions, échelles affines. - Actifs et devises — le tag d'actif,
fxetmoney, les places. - Piliers de conception — comment le dimensionnel rejoint les autres piliers.
Opérateurs et sémantique des expressions
Cette page est la référence pour la couche opérateurs de Flux : ce que signifie chaque opérateur,
quels kinds il accepte, quel kind il produit, et pourquoi chaque règle est ce qu'elle est. Elle
couvre l'algèbre dimensionnelle de + - * /, les deux familles de comparaison, la logique sur
signal, le delay x[n], le rééchantillonnage e @ clock, les ranges, l'indexation et la
projection, la syntaxe de méthode UFCS, la mise à jour de record avec with, la déstructuration, la
famille de sucre ?, le placeholder _, et la table de précédence complète. La façon dont les
kinds sont assignés à des programmes entiers — synthèse, vérification, principalité — est le sujet
de l'Inférence ; le treillis sur lequel les opérateurs calculent est défini dans
Kinds.
Une note sur les échantillons. Flux n'a pas d'instructions-expressions, donc une expression nue n'est pas un programme. Les lignes marquées
✗sur cette page sont donc des fragments d'expression : elles existent pour montrer ce que les règles de kinds refusent, pas ce que le parser accepte. Chaque ligne non marquée est une instruction licite.
#Une algèbre, deux systèmes
Deux systèmes distincts coopèrent sous chaque expression, et les garder séparés est ce qui rend les règles ci-dessous prévisibles :
- Le treillis de coercition (A).
a ≤ bsignifie «ase coerce enbsans mentir ». Sa borne supérieure⊔unifie les kinds aux sites de réconciliation — les branches d'unif, les deux opérandes denz, les séries co-tracées. Sa borne inférieure⊓intersecte les contraintes — le kind qu'un paramètre exige, les bornesoscqui se recouvrent. - L'algèbre des opérateurs (B).
+ - * / < > ==sont régis par une table de règles — l'analyse dimensionnelle dans la tradition des vérificateurs d'unités de mesure — qui calcule le kind résultat de chaque application. Ce n'est pas la borne supérieure :price - price = level, tandis queprice ⊔ price = price.
Les deux systèmes signalent l'échec via un seul canal d'erreur : toute règle non définie de (B)
et toute incompatibilité de (A) produisent ⊤. Un ⊤ est une erreur dure uniquement dans une
position exigeante (un opérande, une cible de plot, un argument) ; un ⊤ lié à un nom
intermédiaire que rien ne consomme est un avertissement ([WarnTop]). La politique est détaillée
dans Inférence.
Le substrat affine. L'axe des prix est un espace affine à une dimension : price est un
point dessus, level est un vecteur (un déplacement), ratio est un scalaire sans
dimension centré sur 1. La dimension elle-même est un groupe abélien d'exposants sur les
générateurs {P prix, V volume, T temps, I indice-de-barre, rad angle} : * additionne les
exposants, / les soustrait. Deux conséquences en découlent gratuitement :
*et/sont totales sur les dimensions — il y a toujours un vecteur d'exposants résultat ;- seules
+et-exigent un accord dimensionnel, car ajouter un point à un point n'a aucun sens géométrique.
Pourquoi cette règle existe. La lecture affine n'est pas une décoration ; c'est ce qui permet au vérificateur de dire non à
close + rsi(close, 14)tout en acceptantclose + atr(14)— les deux sont « un prix plus un nombre » pour un œil non typé, mais un seul des deux nomme un déplacement sur l'axe des prix. Chaque règle ci-dessous est une instance de ce substrat, pas un cas particulier.
#Addition et soustraction : + et -
+ et - requièrent des opérandes de la même dimension et suivent ensuite les règles affines :
| règle | lecture | exemple | résultat |
|---|---|---|---|
pt(d) - pt(d) → vec(d) |
point − point = vecteur | close - open |
level |
pt(d) ± vec(d) → pt(d) |
point ± vecteur = point | close + atr(14) |
price |
vec ± vec → vec |
les vecteurs s'additionnent | atr(14) - atr(28) |
level |
ratio ± ratio → ratio |
les scalaires s'additionnent | vortex(14).plus - vortex(14).minus |
ratio |
osc ± osc → intervalle propagé |
arithmétique d'intervalles sur la revendication | rsi(close,14) - 50 |
osc(-50,50) |
lit ± x → x |
un littéral adopte la dimension | close + 10 |
price |
dims ≠ → ⊤ |
aucun sens affine | close + rsi(close,14) |
✗ [ErrDim] |
fluxspread = close - open // price − price : level
band = close + 2 * atr(14) // price + level : price
centered = rsi(close, 14) - 50 // osc(0,100) − lit : osc(−50,50)
close + rsi(close, 14) // ✗ [ErrDim] — point + dimensionless: no affine meaning
obv() + close // ✗ [ErrDim] — volume and price do not add
La ligne osc mérite une note : les bornes d'un osc(lo,hi) sont une revendication de
présentation, pas un invariant d'exécution (seul clamp rend une borne réelle), et ± propage la
revendication par arithmétique d'intervalles. Soustraire la ligne médiane d'un oscillateur recentre
donc son pane : rsi - 50 est dessiné dans un pane fixe [-50,50] avec une ligne médiane à 0.
Il n'y a pas d'arithmétique sur signal — combinez les signaux avec and/or/not
(voir Logique), comptez-les avec count(sig, n).
#La seule surcharge catégorielle : la concaténation de chaînes
L'unique ligne d'opérateur hors de la table dimensionnelle est la concaténation :
string + string → string. - * / et chaque paire mixte impliquant string restent hors de
l'arithmétique et produisent [ErrDim].
fluxlabel = "px " + fmt.price(close) + " @ " + fmt.time(time) // string
"holdings: " + volume // ✗ [ErrDim] — format it: fmt.num(volume)
L'interpolation ("px {fmt.price(close)}") se désucre vers le même pipeline de concaténation
(fmt.cat), de sorte que les deux écritures sont équivalentes ; les chaînes de + fusionnent en une
seule écriture bornée à la compilation. Voir text pour les garanties du kind
string.
#Combinaisons affines : la règle Σλ
Une somme pondérée de points a un sens exactement quand ses coefficients le disent. Sur une
expression normalisée en Σ λᵢ · ptᵢ (les coefficients sont des littéraux const-folded, donc
x * 0.5 et x / 2 sont le même λ, et (2*high + low + close) / 4 se normalise bien), le
vérificateur applique [Affine] :
Σλ = 1→ le kind point — un barycentre de prix est un price :(high + low) / 2 : price;Σλ = 0→ le kind vecteur — une combinaison à somme nulle est un déplacement :high - low : level;- tout le reste →
quantity+[WarnAffine]— dimension effacée, probablement un diviseur manquant.high + lowseul (Σλ = 2) attire l'avertissement avec un correctif rapide : diviser par 2 ou utiliser la source pré-typéehl2.
Deux raffinements font que cette règle couvre l'algèbre réelle des indicateurs :
- Un coefficient littéral préserve le rôle :
k * stdev(close, 20)est toujours unlevel, c'est pourquoisma(close, 20) + 2 * stdev(close, 20)se type commeprice + level → price— une bande de volatilité atterrit sur le graphique, pas dans un pane. (Le déroulé complet ascendant de cette expression exacte, se terminant par sa décision de présentation, est tracé dans Inférence.) - La règle s'applique aux composites, récursivement, après inlining — pas seulement aux moyennes
OHLC syntaxiques.
dema = 2*ema(close,20) - ema(ema(close,20),20)aΣλ = 1et se type commeprice;temaaussi. Sans application récursive, chaque composite multi-emase typerait à tort commelevel.
Les barycentres OHLC courants sont des sources pré-typées (hl2, hlc3, ohlc4 : price), de sorte
que le cas à 90 % n'exerce même jamais la règle.
Les tags d'actif filtrent la combinaison. Tous les points d'une même combinaison affine doivent
porter le même tag d'actif, composante par composante, exactement comme ± l'exige (voir
ci-dessous) :
fluxbtcUsd = series("BTC-USD").close // price[BTC,USD]
btcEur = series("BTC-EUR").close // price[BTC,EUR]
mid = (btcUsd + btcEur) / 2 // ✗ [ErrDim] — quotes differ; convert one leg first
Pourquoi cette règle existe. Sans la clause de tag,
(btcUsd + btcEur) / 2se normaliserait enΣλ = 1et passerait la vérification de types comme unpricevalide, moyennant silencieusement deux devises. La règle affine ne doit pas être une porte dérobée contournant l'interdiction de mélange.
#Multiplication et division : * et /
* et / sont totales sur les dimensions : le vecteur d'exposants du résultat est la somme
(respectivement la différence) de ceux des opérandes. Les kinds nommés sont les vecteurs d'exposants
que vous rencontrez le plus souvent ; toute autre combinaison est toujours une dimension composée
valide et traçable étiquetée par ses exposants (eom : P²·V⁻¹ reçoit un pane auto — aucun
avertissement, car une dimension composée précise n'est pas une dimension effacée).
| règle | exemple | résultat |
|---|---|---|
D × ratio → D |
close * (volume / sma(volume, 20)) |
price |
lit × κ → κ |
2 * stdev(close, 20) |
level |
D × osc(0,1) → D |
close * bbPctB(close, 20) |
price |
price × volume → pv |
close * volume |
pv (flux monétaire) |
price × price → P² |
close * close |
P², pane étiqueté |
price ÷ price → ratio |
close / close[1] |
ratio |
level ÷ level → ratio |
atr(14) / atr(28) |
ratio |
level ÷ price → ratio |
atr(14) / close |
ratio |
D ÷ ratio → D |
close / historicalVolatility(close, 20) |
price |
pv ÷ volume → price |
cum(close * volume) / cum(volume) |
price (vwap à la main) |
pv ÷ price → volume |
loi de groupe | volume |
level ÷ barspan → slope |
change(close, 20) / barssince(sig) |
slope = P·I⁻¹ |
fluxrel = close / close[1] // ratio — centred on 1, own pane
flow = close * volume // pv — money flow, own pane
vwap0 = cum(close * volume) / cum(volume) // price — back on the chart
Notes sur les cas limites :
slopeest prix-par-barre, pas prix-par-seconde : l'axe des x est ordinal, donclevel ÷ barspan → slope = P·I⁻¹. Diviser unlevelpar unedurationproduiraitP·T⁻¹, une dimension différente (et rarement voulue).lrSloperenvoieslopepour cette raison — dérivé de la loi de groupe, pas décrété.sqrtdivise les exposants par deux etpow(x, n)les met à l'échelle, ce qui est la raison pour laquellestdev = sqrt(variance)se type correctement ; les transcendantales (log,exp, trigonométrie) exigent une entrée sans dimension —log(close / close[1])est correct ; le pilier compute fait delog(close)un[ErrDim], avec un correctif rapide vers le ratio.- Décision ouverte. La grammaire garde
%au palier multiplicatif ; aucun kernel du catalogue ne l'utilise et sa conservation est un point ouvert nommé du plan de grammaire. Sa sémantique entière sur les alias entierslong(≡ decimal(18,0)) etlong128(≡ decimal(38,0)) est épinglée (troncature vers zéro ; le reste prend le signe du dividende ; la division par zéro etMIN / −1produisentna, jamais un trap). Voir compute.
#Tags d'actif et de devise sous les opérateurs
Les kinds de dimension prix portent un tag d'actif (B, Q[, @v]) — base, quote, place
optionnelle ; volume porte la base seule, pv la quote seule. Les opérateurs traitent le tag avec
deux régimes différents, et la séparation est délibérée :
±et les comparaisons filtrent sur l'identité — mélanger lève[ErrDim];*,/et la borne supérieure élargissent — une composante différente monte vers son sommet par composante (⊤base,⊤quote), jamais une erreur.
fluxbtcUsd = series("BTC-USD").close // price[BTC,USD] — a quote-tagged close
d = btcUsd - btcUsd[1] // level[BTC,USD] — tag preserved through ±
btcUsd + ethUsd // ✗ [ErrDim] — bases differ: adding two assets
btcUsd + btcEur // ✗ [ErrDim] — quotes differ: adding two currencies
pnlUsd + pnlEur // ✗ [ErrDim] — pv[USD] + pv[EUR]: convert first
La division dérive les taux de change, et la multiplication les consomme — la règle du rôle fx :
fluxbtcUsd = series("BTC-USD").close // price[BTC,USD]
btcEur = series("BTC-EUR").close // price[BTC,EUR]
ethUsd = series("ETH-USD").close // price[ETH,USD]
fxUE = btcUsd / btcEur // fx[USD/EUR] — same base, quotes differ, concrete keys
cross = btcUsd / ethUsd // ratio — cross-base: relative strength, tag dropped
usd = btcEur * fxUE // price[BTC,USD] — shared quote cancels: (EUR/BTC)·(USD/EUR)
usd2 = btcEur / (1 / fxUE) // ÷ fx[EUR/USD] converts the same way
// triangular chaining — both operands are DERIVED; no primitive conjures a rate
btcGbp = series("BTC-GBP").close // price[BTC,GBP]
gbpEur = btcGbp / btcEur // fx[GBP/EUR]
usdGbp = fxUE / gbpEur // fx[USD/EUR] ÷ fx[GBP/EUR] → fx[USD/GBP]
La liste complète des arêtes, en un seul endroit — l'aiguillage price ÷ price est un 2×2 sur
(base égale ?, quote égale ?) :
price[B,Q] ÷ price[B,Q] → ratio(les deux égales) ·price[B1,Q] ÷ price[B2,Q] → ratio(la base diffère) ·price[B1,Q1] ÷ price[B2,Q2] → ratio(les deux diffèrent — aucune conversion propre n'existe, donc le tag est abandonné et vous obtenez un nombre sans dimension nu) ;price[B,Q1] ÷ price[B,Q2] → fx[Q1/Q2]— uniquement quand les deux clés de quote sont des constantes concrètes ; si l'une ou l'autre est⊤quote, le résultat retombe sur un simpleratio;price[B,Q1] * fx[Q2/Q1] → price[B,Q2](commutative) etprice[B,Q1] / fx[Q1/Q2] → price[B,Q2]— conversion ; la quote partagée doit correspondre chaîne pour chaîne, sinon le résultat s'élargit versprice[B,⊤quote](jamais[ErrDim]: la sûreté est réservée à±) ;leveletpvse convertissent par les deux mêmes arêtes ;1 / fx[Q1/Q2] → fx[Q2/Q1]— la réciproque ; le numérateur doit être le littéral1, apparié avant toute coercition. Une mauvaise direction est un tag distinct (fx[USD/EUR] ≠ fx[EUR/USD]), jamais une coercition ;fx[A/Q] ÷ fx[B/Q] → fx[A/B]— chaînage triangulaire ;fx * fx → num(l'annotation de paire n'a pas d'emplacement surnum) ;fx ± fx → ratio— une somme de taux est une magnitude, pas un taux, donc l'annotation de paire disparaît (les kernels sont différents :ema(fxUE)gardefx[USD/EUR]) ;price[B,Q] * volume[B] → pv[Q]— la base s'apparie et est abandonnée : un flux monétaire est un montant en devise, agnostique à la base, donc lespv[USD]de deux actifs différents peuvent être sommés — c'est de l'arithmétique de portefeuille, pas le bug de mélange d'actifs ;- le
*inter-actifs s'élargit :price[B1,Q1] * price[B2,Q2] → P²[⊤base,⊤quote], jamais une erreur.
Il n'y a pas de primitive fxRate(a, b) : une valeur fx est dérivée par ÷ ou arrive comme une
série dont le producteur se déclare flux fx. Le modèle complet — métadonnées de place, toSource,
séries multi-devises — est spécifié dans asset-currency.
#Comparaisons : deux familles, deux portées
Un unique test « la borne supérieure est définie » serait la mauvaise barrière : κ ⊔ κ = κ rend
toute borne supérieure d'un kind avec lui-même admissible, y compris les kinds sans ordre et sans
égalité utile. Flux cantonne plutôt chaque famille de comparaison aux sortes où son sens est
réel. Les deux familles renvoient signal.
#Ordre : < > <= >= cross_up cross_down
Les comparaisons d'ordre sont restreintes à la sorte scalaire avec un vrai ordre total, et les
deux côtés doivent être dimensionnellement compatibles (κₐ ⊔ κᵦ ∉ {quantity, ⊤}) :
- ordonnés : la colonne vertébrale numérique (
ratio,osc(·),signal,num) et l'antichaîne dimensionnée (price,level,volume,pv,time,duration,barindex,barspan,slope,angle) ; - exclus :
color,clock,string,record,vec,variant,ui—[ErrDim]/[ErrArg].string < stringest rejeté délibérément : le cœur n'a pas de tri dépendant de la locale (une exclusion de déterminisme — voir text) ; direst exclu de l'ordre :{-1, 0, +1}est catégoriel-discret, pas une magnitude. Comparez-le uniquement avec==.
fluxbreakout = close > 30000 // price ⊔ lit = price → signal
overbought = rsi(close, 14) > 70 // osc vs lit → signal
golden = ema(close, 50) cross_up ema(close, 200) // signal — true on the crossing bar
close > rsi(close, 14) // ✗ [ErrDim] — price ⊔ osc = quantity: incomparable magnitudes
tf("1d") < tf("4h") // ✗ [ErrDim] — a clock has no order to compare on
superTrend(10, 3).dir > 0 // ✗ [ErrDim] — dir is categorical: write dir == 1
cross_up / cross_down sont des opérateurs de comparaison infixes (jamais des appels
préfixes) : a cross_up b est vrai exactement à la barre où a passe d'en dessous-ou-égal à
au-dessus de b. Ils siègent au palier de comparaison et sont non associatifs comme le reste.
Les tags d'actif filtrent aussi l'ordre : les opérandes doivent porter des tags identiques
composante par composante — price[BTC,USD] < price[BTC,EUR] est [ErrDim] même si la borne
supérieure des deux côtés s'élargirait vers un price valide. L'annotation de paire fx est
exemptée de la barrière (elle vit sur ratio, hors de l'axe d'actif) : fxUE < fxGJ → signal
compare deux taux comme de simples ratios.
#Égalité : == et !=
L'égalité va plus loin que l'ordre — sur chaque sorte dont l'égalité est décidable et terminante :
- égalité bit-à-bit pour
string,dir,color— et l'égalité scalaire ordinaire sur la colonne vertébrale (close == open) ; - égalité profonde, décidable, consciente de
napourrecord,vec,variant— composante par composante ; deux formes qui diffèrent sont[ErrDim]; - exclus :
clocketuisont des kinds consommés, jamais comparés —[ErrArg]; - opérandes scalaires de dimensions ou de tags d'actif différents :
[ErrDim], exactement comme pour l'ordre ; l'annotation de pairefxest de nouveau exemptée.
fluxst = superTrend(10, 3)
mark st.dir == 1 // dir compares by equality → signal
bb = bollinger(close, 20)
same = window(close, 4) == window(close, 4) // deep, na-aware equality on a vec → signal
tf("1d") == tf("1d") // ✗ [ErrArg] — clock is consumed, not compared
#Comparaisons et na
Toute comparaison touchant na produit na — jamais true, jamais false : na == x,
na < x, même na == na sont tous na. L'absence est testée explicitement :
fluxhave = is_some(rsi(close, 14)) // signal — presence (not is_na(x))
gap = is_na(close[1]) // signal — absence (first bar)
Pourquoi cette règle existe. Pendant l'échauffement, un indicateur est
napour ses premières barres. Sina > 70s'évaluait silencieusement àfalse, chaque règle de seuil se déclencherait — ou refuserait de se déclencher — sur des données fantômes, et le bug serait invisible par construction. Forcernaà travers les comparaisons fait propager l'absence jusqu'ausignal, oùis_na/is_somela gèrent délibérément. La même discipline traverse tout le langage :matchdoit couvrirna, et les réducteurs de fenêtre le propagent.
#Logique : and, or, not
Les opérateurs logiques sont définis sur signal et seulement sur signal
(signal × signal → signal) ; not est un opérateur préfixe d'un palier plus serré qu'and.
fluxsetup = close > ema(close, 50) and rsi(close, 14) < 30
flat = not in_session("09:30-16:00 America/New_York")
close and volume // ✗ [ErrArg] — `and` demands signals; neither operand is one
Il n'y a pas d'effet de court-circuit à proprement parler — les expressions sont pures — donc
and/or sont de la simple algèbre booléenne sur le support {0,1}, conscients de na comme tout
le reste.
#Delay : x[n]
x[n] lit la valeur du flux x d'il y a n barres. L'index doit être un littéral naturel
const-folded :
fluxmom = close - close[10] // level — momentum as a displacement
prevH = macd(close).hist[1] // projection, then delay — postfix chain
close[-1] // ✗ [ErrCausal] — the future is not addressable
close[input(5)] // ✗ [ErrTotal] — delay must be a compile-time constant
n ≥ 0;x[0]estxlui-même. Un index négatif est[ErrCausal]: Flux est no-repaint — une valeur, une fois produite pour un pas, ne change jamais — et une lecture dans le futur n'est pas une violation de style mais un programme inexprimable.- Un index non constant est
[ErrTotal]: la profondeur de delay dimensionne un ring buffer, et la totalité exige que toute la mémoire soit bornée à la compilation. - Sur les
npremières barres,x[n]estna(échauffement), ce que les règles de comparaison ci-dessus portent ensuite en sûreté.
La même syntaxe de crochets sur un récepteur vec n'est pas un delay mais une lecture
d'élément ([Index]) : slots[h.slot] prend un index ordinal d'exécution, et une lecture hors
bornes produit na plutôt qu'une erreur — la capacité déclarée borne déjà le coût. Les deux rôles
sont distingués par le kind du récepteur, jamais par de la devinette ; voir Indexation et
projection.
#Rééchantillonnage : e @ clock
@ est l'éliminateur du kind clock : e @ c re-cadence le flux e sur la clock c et
préserve le kind de e ([At]). Les clocks sont des valeurs de première classe — composables,
assignables, acceptables comme input — et @ est la façon dont elles sont consommées :
fluxcalm = atr(14) < atr(50) // signal — a quiet-volatility regime
daily = ema(close @ "1d", 50) // MTF: a daily EMA under any chart timeframe
bricks = close @ renko(atrBox(14)) // representation change = clock change
c = if calm then tf("1d") else tf("4h")
trend = ema(close @ c, 20) // the clock itself was computed
- Le rééchantillonnage est causal : à toute barre, il voit l'unité en cours ou la dernière unité
close de la clock plus grossière, jamais une clôture future.
[ErrCausal]garde le reste. - Constructeurs :
tf("1d"),renko(box),pnf(box, rev),range(r)— oùboxest unlevel(le treillis le force : une frontière de brique estanchor + k·box, et seulprice + levelest un price). - v1 autorise une clock par série : composer deux clocks (
renko(b) @ "1d") est rejeté. - Note de grammaire : le membre droit de
@est un opérande clock restreint, doncx @ "1d" + 1s'analyse comme(x @ "1d") + 1.
Les clocks généralisent la « timeframe » — le traitement complet (échauffement, alignement,
live()) est dans temps et état.
#Ranges : a..b
.. construit un range et n'existe que dans les positions de range — ce n'est pas un opérateur
d'expression, ne peut pas être chaîné (non associatif), et n'entre jamais en collision avec la
projection . ou les littéraux de nombre (2..200 se lexe comme 2 .. 200).
fluxbb = bollinger(close, 20)
fill bb.upper..bb.lower // band between two price streams
len = input(14, 2..200) // bounded parameter range
Les deux extrémités d'un fill doivent vivre sur le même axe scalaire ordonné —
fill close..rsi(close, 14) est [ErrDim] (voir
Inférence pour les règles d'admissibilité).
#Indexation et projection
La projection e.f lit un champ d'un record ([Proj]). Un champ manquant ou un récepteur
non-record est [ErrField] — avec un correctif rapide vers le nom le plus proche :
fluxbb = bollinger(close, 20) // record{upper, middle, lower : price}
plot bb.upper // price
bb.uper // ✗ [ErrField] — no such field; did you mean upper?
L'indexation v[i] sur un récepteur vec<κ>[n] lit un élément ([Index]). L'index peut être un
ordinal d'exécution ; la capacité déclarée n garde l'opération totale, et les lectures hors bornes
produisent na — jamais [ErrTotal], jamais un trap. C'est le substrat de l'idiome slotmap
(collections bornées à handles stables et tombstones na).
Un nom pointé peut aussi être un nom qualifié plutôt qu'une projection — T.Ctor sélectionne un
constructeur du variant déclaré T, et mod.f nomme une entrée d'un module importé. Les deux se
résolvent pendant la résolution de noms, avant que [Proj] ne soit jamais considéré. Ce qui nous
amène au troisième sens du point :
#UFCS : close.ema(20).rsi(14)
Uniform Function Call Syntax : recv.f(args) est exactement f(recv, args) — le récepteur
devient le premier argument. C'est un désucrage sémantique sur l'arbre déjà analysé, pas une forme
grammaticale, et c'est ce qui fait que les pipelines de gauche à droite se lisent dans le sens où
coulent les données :
fluxsmooth = close.ema(20).rsi(14) // ≡ rsi(ema(close, 20), 14)
top5 = vec.topK(window(close, 100), (x) -> x, 5) // a namespace call — the dot means something else here
Le point a trois sens, décidés à la compilation avec un token de lookahead :
recv.fsans(— un champ de record (bb.upper), règle[Proj];recv.f(oùfse résout en une fonction — un appel UFCS, désucré enf(recv, …);mod.f/T.Ctoroù le côté gauche nomme un module ou un variant déclaré — un nom qualifié, résolu avant l'un ou l'autre des précédents.
Départage : si recv a un champ f et que f nomme une fonction, recv.f(…) est l'appel UFCS —
un champ n'est jamais appelable, donc l'autre lecture ne pourrait être qu'une erreur.
Le piège est de lire (2) là où seul (3) s'applique. UFCS atteint les fonctions dans la portée —
le prélude — et topK n'en fait pas partie : elle vit dans vec.*. Donc w.topK(…) n'est pas un
appel UFCS mais une projection de champ sur un vecteur, ce qui est [ErrField]. Écrivez
vec.topK(w, …). La règle empirique est celle que le menu de complétion impose déjà : si l'éditeur
ne le propose pas après le point, ce n'est pas là.
Pourquoi cette règle existe. UFCS n'ajoute aucune grammaire et aucune sémantique — le vérificateur vérifie
recvcontre le premier paramètre de la fonction exactement comme si vous aviez écrit l'appel direct — mais il achète la découvrabilité : aprèsclose., l'éditeur peut proposer précisément les fonctions dont le premier paramètre accepte unprice, filtrées par kind. Un seul mécanisme, des pipelines lisibles, une complétion consciente des kinds.
Les arguments nommés se composent avec lui : recv.f(x, mode: fast) lie mode par nom de paramètre
et le vérifie comme un positionnel (voir
Inférence).
#Mise à jour de record : e with { … }
with produit un record identique à e sauf pour les champs listés. Il est préservant de la
forme ([With]) :
- chaque champ listé doit déjà exister —
[ErrField]sinon (pas de création silencieuse de champ) ; - chaque valeur doit se coercer vers le kind déclaré du champ —
[ErrArg]sinon ; - les champs non listés sont reportés inchangés, et le résultat a exactement le kind de
e.
fluxvariant Phase { ask | suspense | revealed }
m = { score: 0, phase: ask }
m2 = m with { score: m.score + 1, phase: revealed }
m with { scrore: 0 } // ✗ [ErrField] — typo caught, nothing silently added
with est un suffixe postfixe (même palier que l'appel et la projection), donc il se chaîne :
m with { a: 1 } with { b: 2 }.
Pourquoi cette règle existe. La mise à jour fonctionnelle sans
withsignifie reconstruire le record entier à la main — et un champ oublié est une perte d'état silencieuse. La préservation de la forme transforme cette classe de bug en[ErrField]à la compilation.
#Déstructuration : let {a, b} = e
Un motif de record à gauche d'une liaison extrait les champs par nom. Il est irréfutable — il ne peut jamais échouer à l'exécution, donc il est permis exactement là où aucun branchement n'est possible :
fluxw = let { upper, lower } = bollinger(close, 20) in upper - lower // level — `let … in` is an EXPRESSION
{ macd, hist } = macd(close) // a program-level bind destructures too
- un champ nommé qui n'existe pas, ou un sujet non-record, est
[ErrField]; - si le sujet est
na, chaque champ lié estna— l'absence s'écoule vers l'intérieur, de façon cohérente avec les règles de comparaison ; - la déstructuration de variant est réfutable et vit donc dans
match, dont le vérificateur impose l'exhaustivité ([ErrTotalMatch]) — unletne branche jamais.
#La famille ? : ??, ?., ternaire
Trois morceaux de pur sucre, tous désucrés vers des constructions scellées — pas de nouveaux kinds, pas de nouvelle sémantique :
| sucre | se désucre en | notes |
|---|---|---|
x ?? d |
nz(x, d) |
kind = x ⊔ d ; remplit na avec une valeur par défaut |
e?.f |
if is_na(e) then na else e.f |
navigation sûre ; chaînable, court-circuite au premier na |
c ? a : b |
if c then a else b |
même arbre ; c doit être un signal |
fluxm = macd(close) // record{macd, signal, hist}
len = close[1] ?? close // price — first bar handled
hist = m?.hist ?? 0 // safe-nav then default
side = close > open ? 1 : -1 // ternary, right-associative
Parce que ?? est nz, il suit les règles de borne supérieure : des branches de dimensions
différentes s'élargissent vers quantity (avec un avertissement), et mélanger des tags de
représentation (f64 vs decimal) est [ErrRepr] — le sucre ne peut pas faire ce que la forme
désucrée ne ferait pas.
#Le placeholder _
Au site d'argument d'un kernel d'ordre supérieur, un unique _ libre dénote l'unique paramètre d'un
lambda implicite :
fluxscaled = window(close, 20).map(_ * 1.1) // ≡ .map((x) -> x * 1.1)
present = vec.where(window(close, 20), (x) -> is_some(x)) // a namespaced HOF — write the lambda
window(close, 20).fold(0, _ + _) // ✗ — two-parameter position: write (acc, x) -> acc + x
Le placeholder est confiné à un kernel du prélude atteint par UFCS, où le récepteur fixe l'unique
paramètre ; un appel d'ordre supérieur avec namespace comme vec.where(…) prend un lambda explicite,
car _ n'a aucun lieur auquel s'attacher là.
La règle est strictement mono-argument : exactement un _, dans une position attendant une
fonction à un paramètre. Deux _ ou plus, ou un site à deux paramètres comme fold, sont rejetés
avec une demande de lambda explicite — le sucre ne devine jamais quel _ est lequel. Le motif _
des bras de match est un symbole différent, distinct par sa position ; les deux n'entrent jamais en
collision.
#Précédence et associativité
Du plus lâche au plus serré. La grammaire stratifiée normative — y compris pourquoi cette table n'a besoin d'aucune annotation de précédence — est dans grammar.md.
| palier | opérateurs | associativité | notes |
|---|---|---|---|
| 1 | -> |
droite | un seul token flèche ; son rôle (lambda, paire de tween, on, bras de match, for) est décidé par sa position de tête — voir grammar.md |
| 2 | c ? a : b · if…then…else · let…in |
droite | else est obligatoire — pas de dangling-if |
| 3 | ?? |
droite | coalescence null |
| 4 | or |
gauche | |
| 5 | and |
gauche | |
| 6 | not |
préfixe | |
| 7 | < > <= >= == != cross_up cross_down |
non associatif | a < b < c est une erreur d'analyse — écrivez a < b and b < c |
| 8 | + - |
gauche | |
| 9 | * / % |
gauche | |
| 10 | - unaire |
préfixe | plus serré que * : -a * b = (-a) * b |
| 11 | postfixe : f(…) · [n] · @c · .m · ?.m · with {…} |
gauche | un seul palier, appliqué dans l'ordre lexical : f(x)[1]@"1d".m = (((f(x))[1]) @ "1d").m |
| 12 | primary | — | littéraux, noms, (…), liste […], record/bloc {…}, match, scene |
Hors de la cascade :
..— non associatif, licite uniquement dans les positions de range (fill a..b,input(n, lo..hi)) ;@— son opérande droit est un opérande clock restreint, doncx @ "1d" + 1est(x @ "1d") + 1;:et,— séparateurs, jamais opérateurs.
Chaque palier ne se réfère qu'au suivant, plus serré — pas de récursion croisée — ce qui rend toute la cascade non ambiguë par construction plutôt que par condition annexe.
#Voir aussi
- kinds.md — le treillis sur lequel ces opérateurs calculent : sortes,
⊤/⊥, arêtes de coercition, tags. - inference.md — comment les kinds sont assignés, l'inférence de présentation, et le catalogue d'erreurs complet.
- grammar.md — la grammaire normative, les cinq lectures de la flèche unique, la désambiguïsation.
- time-and-state.md — flux, clocks,
scan/fold/loop, causalité et échauffement en profondeur. - asset-currency — le modèle complet de tag d'actif : places,
toSource, money. - compute —
math.*/stat.*et les règles dimensionnelles de la bibliothèque numérique.
Inférence — kinds, présentation, et la politique d'erreurs
Kinds donne la relation : quels kinds existent et quels jugements tiennent. Cette page donne l'algorithme : comment un kind est effectivement assigné à chaque nœud d'un programme, comment la présentation d'un graphique — pane, échelle, guides, couleur, UI de paramètres — est dérivée de ces kinds plutôt que configurée, et ce qui se passe quand quelque chose ne type pas.
Trois propriétés rendent l'algorithme digne d'être spécifié précisément, plutôt que laissé comme un détail d'implémentation. Il est principal (le kind qu'il synthétise est le plus petit que le programme admet, il n'y a donc jamais de choix à faire), il est déterministe (le même source produit toujours les mêmes kinds, ce qui permet à deux moteurs d'émettre du code byte-identique), et il est total (chaque état d'édition, y compris une ligne à moitié saisie, reçoit un kind ou une erreur précise — jamais un échec silencieux).
#Deux modes
L'inférence est bidirectionnelle : elle lit les mêmes règles de typage dans deux directions.
La synthèse — Γ ⊢ e ⇒ κ — est le mode par défaut, ascendant. Elle produit le plus petit
kind que l'expression admet. Les feuilles synthétisent leur kind exact (close ⇒ price,
14 ⇒ lit, "hi" ⇒ string) ; les introductions synthétisent leur structure (un littéral de record,
une scène, un constructeur) ; les éliminations synthétisent en calculant — un appel, une
projection, un match, un delay, un rééchantillonnage, et chaque nœud arithmétique, qui demande son
kind résultat à l'algèbre dimensionnelle.
La vérification — Γ ⊢ e ⇐ κ — est le mode descendant, et elle ne s'exécute qu'aux sites de
consommation, où un kind attendu existe déjà :
| Site de consommation | Ce qui est vérifié |
|---|---|
| un argument d'appel | eᵢ ⇐ πᵢ — le kind déclaré du paramètre |
un champ de record, une mise à jour with |
vⱼ ⇐ κ_field |
| une instruction de sortie | plot e ⇐ presentable · mark s ⇐ signal|dir · fill a..b ⇐ ordered-scalar · color bars: ⇐ signal|dir|color |
une valeur init ou un bras update du plan APP |
champ ⇐ le kind du Model |
| un lambda | (p⃗) -> body ⇐ (π⃗)→ρ — un lambda ne synthétise jamais ; il est vérifié contre le kind de fonction que le kernel d'ordre supérieur exige |
Cette dernière ligne est aussi ce qui désambiguïse la flèche : (x) -> x * 1.1 est un lambda
exactement quand sa position attend une fonction, et une paire de tween sinon. La
désambiguïsation est une conséquence du mode, pas une règle séparée (voir Grammaire).
#La subsomption est confinée
La règle de coercition — « un a peut être utilisé là où un b est attendu si a ≤ b » — se
déclenche uniquement à la frontière entre les deux modes. Pour vérifier e ⇐ κ : synthétiser
e ⇒ κ', puis exiger κ' ≤ κ. Silencieux si l'arête est ≤safe ; un avertissement avec un
correctif rapide si elle est ≤lossy ; [ErrDim], [ErrArg] ou [ErrPlot] — selon la position —
si κ' ⊀ κ.
La coercition ne se déclenche jamais pendant la synthèse. Un nœud n'est jamais élargi spontanément ; son kind synthétisé reste le plus bas connu.
Pourquoi le confinement est porteur. Supposez que la subsomption soit permise dans la synthèse. Alors
x = close - closepourrait synthétiserlevelou, par l'arête d'effacement avec perte,quantity. Les deux sont dérivables. Maisplot xlit le registre de présentation au kind :leveldonne un pane centré sur zéro,quantitydonne un pane auto par défaut. Un seul programme, deux sorties valides, deux artefacts compilés différents — et la garantie que l'aperçu de l'éditeur correspond au module livré (I7) serait perdue. Le confinement est ce qui fait du « kind d'une expression » une fonction plutôt qu'un choix.
#Principalité
Avec la synthèse définie comme ci-dessus, chaque expression a un kind principal : l'unique plus
petit kind qu'elle admet. La preuve est une induction d'une ligne — chaque feuille synthétise
exactement, chaque nœud combine les kinds minimaux de ses enfants avec soit ⊔ soit l'algèbre
dimensionnelle (les deux renvoyant un résultat unique, vérifié par énumération), et la seule règle
d'élargissement est exclue du mode. Donc le kind synthétisé est le plus petit, et vérifier ≤ à
chaque consommateur est alors complet.
Le cas multi-site. Un champ initialisé à na synthétise ⊥ à ce site — mais son kind n'y est
pas coincé : le kind principal d'un champ de record est la borne supérieure sur tous ses sites de
construction et d'affectation. Dans le plan APP, un champ de Model écrit picked: na dans init
et picked: key (un string) dans un bras d'update a le kind string, na restant une valeur
d'exécution licite de ce champ (⊥ ≤ string). Le point fixe converge en une passe car les kinds
assignés ne dépendent jamais du record lui-même. Déclarer record Model { … } d'emblée est la même
chose écrite explicitement.
#Terminaison et déterminisme
L'algorithme est une unique passe ascendante sur le graphe trié topologiquement. Il se termine
parce que le treillis est fini par famille et de hauteur finie, le graphe est acyclique (la causalité
le garantit), et ⊔, ⊓ et l'algèbre sont tous en O(1) — il n'y a aucune itération de point fixe et
aucun stockage de variables d'unification nulle part.
Il est aussi confluent : le kind synthétisé d'un nœud ne dépend que des kinds de ses entrées, et
⊔ est commutatif et associatif, de sorte que le résultat est indépendant de l'ordre topologique
choisi. L'inférence est donc une fonction déterministe du graphe — le même programme produit
toujours les mêmes kinds, d'où le même module émis. C'est l'un des fondements de l'égalité
byte-à-byte interpréteur ≡ WASM (voir Compilateur et
runtime).
#Polymorphisme borné, résolu puis oublié
Le catalogue est plein de familles préservant le kind : ema, sma, sum, highest, change,
stat.stdev — chacune écrite (src: α ≤ quantity, len: lit) → ρ(α). C'est le seul polymorphisme
en v1, et il est délibérément peu profond.
À chaque site d'appel, α est résolu en un kind fermé et monomorphe : α := la borne supérieure
des kinds synthétisés pour les arguments en positions α ; puis α ≤ quantity est vérifié
([ErrArg] sinon) ; puis le kind de retour est la forme de la famille instanciée à ce α. Pour les
familles de différence, la forme est le δ dérivé de l'algèbre ± figée : δ(price) = level.
fluxa = ema(close, 20) // α := price ⇒ price
b = ema(rsi(close, 14), 9) // α := osc(0,100) ⇒ osc(0,100) — smoothing preserves the kind
c = change(close, 5) // δ(price) ⇒ level
d = change(rsi(close, 14), 5) // δ(osc) ⇒ osc, centred on 0
α est résolu puis oublié. Ce n'est pas une variable d'unification qui persiste à travers le
programme, de sorte que deux sites ne peuvent jamais se contredire, et la propriété « pas
d'unification générale » qui garde l'inférence en une seule passe survit intacte.
#Typer un programme inachevé
Un éditeur type un programme qui est en train d'être écrit, pas un programme fini — donc l'algorithme assigne un kind à chaque état d'édition.
Un nom non lié (vous êtes à mi-chemin de le saisir) et un trou de syntaxe (un nœud d'erreur
d'analyse) synthétisent tous deux un trou de kind : un ⊤ contenu qui émet exactement un
diagnostic ([ErrUnbound]) et n'empoisonne pas ses frères.
De là découle le cône typable : le plus grand sous-graphe dans lequel chaque nœud, et chaque
entrée transitive de chaque nœud, est exempt de ⊤ et exempt de trous. L'aperçu en direct évalue
exactement ce cône et rend le reste comme --. Une erreur locale ne vide donc jamais tout l'aperçu —
un programme correct a un cône égal au graphe entier, et un programme avec un nom non lié
prévisualise encore tout ce qui n'en dépend pas.
Le cône est un artefact strictement au moment de l'édition : un ⊤ dans une position consommée
reste un échec dur pour émettre du code. La garantie de byte-identité est intacte.
#Re-typage incrémental
Lors d'une édition, seuls le nœud modifié et son cône aval de consommateurs de kind sont re-synthétisés ; le kind de chaque autre nœud est mémoïsé sous l'identité de nœud épinglée que le compilateur maintient déjà pour le hachage et l'élimination de sous-expressions communes. Parce que le graphe est acyclique et le treillis est monotone et fini, ceci converge en une passe descendante.
Le re-typage incrémental est observationnellement égal à une ré-inférence complète — c'est la même fonction, mémoïsée — de sorte qu'il ne peut jamais être en désaccord avec le module livré. C'est ce qui maintient le budget d'édition sous les 16 ms, avec l'analyse incrémentale.
#La présentation est inférée, pas configurée
Voici le fruit d'un système de kinds qui suit le sens. Un kind dit déjà ce qu'est une valeur ; il dit donc aussi comment elle devrait être montrée. C'est pourquoi le premier programme que quiconque écrit fait une ligne et n'a besoin d'aucune option :
stdev est une dispersion (un vecteur), un multiple littéral garde son rôle, et point + vecteur = point fait atterrir toute l'expression sur l'axe des prix — donc elle se met en overlay.
Le compilateur dérive une entrée de registre à partir du kind, et l'affine avec des métadonnées de l'opération elle-même :
registry := merge( reg(kind), opMeta(expression) )
reg(kind) fournit les défauts ; opMeta les affine (un rsi ajoute ses guides 30/70). La table
de défauts complète :
| Kind | Mode | Échelle | Lignes de référence | Classe CSS |
|---|---|---|---|---|
price |
overlay, sur l'axe des prix | échelle de prix partagée | — | flux-price |
level |
pane propre | symétrique autour de 0 | 0 |
flux-level |
osc(lo,hi) |
pane propre | [lo,hi] fixe |
point médian, plus les guides de l'opération (rsi → 30/70) | flux-osc |
ratio |
pane propre (log optionnel) | autour de 1 | 1 |
flux-ratio |
volume |
pane propre | conscient du signe ([0,max] quand non négatif) |
0 |
flux-volume |
pv |
pane propre | auto | 0 |
flux-pv |
signal |
marks / fills / coloration de barres — jamais une ligne | — | — | flux-signal |
dir |
coloration de barres / marks — jamais une ligne | — | — | flux-dir |
slope |
pane propre | symétrique autour de 0 | 0 |
flux-slope |
barspan |
pane propre (un compte de barres) | [0,max] |
0 |
flux-barspan |
barindex |
position sur l'axe des x / ancre — jamais une série | — | — | flux-barindex |
dimension composée (P², P²·V⁻¹) |
pane propre, auto-étiqueté par ses exposants | auto | — | flux-num |
angle |
canal de style, ou un pane sur [-π,π] |
[-π,π] |
0 |
flux-angle |
depth |
projeté sur l'axe z en 3-D ; aplati en 2-D | espace z de l'hôte | — | flux-depth |
time / duration / period |
axe des x / annotation | — | — | — |
decimal(scale) |
suit sa dimension ; valeurs formatées à scale décimales |
sa dimension | sa dimension | sa dimension |
record{…} |
éclaté champ par champ | par champ | par champ | par champ |
vec(κ, N) |
réduit ou indexé au kind d'élément κ ; ou rendu comme une representation (un volume profile est un histogramme) |
hérite de κ |
hérite de κ |
hérite de κ |
color, clock, string, ui |
consommés par leur canal — jamais tracés | — | — | — |
num / quantity |
pane par défaut — vous voyez l'effacement | auto | — | flux-num |
⊤ / ⊥ |
non présentable — [ErrPlot] |
— | — | — |
La classe CSS fait partie de la dérivation, pas d'un détail du thème : l'hôte l'appose sur la série rendue, de sorte qu'une feuille de style peut restyler chaque oscillateur du graphique sans qu'aucun script ne nomme une couleur. C'est le seul endroit où « la présentation est inférée » atteint jusqu'à la page.
reg(κ) : chaque kind porte son propre pane, échelle, guides et classe CSS, et le registre est fusionné avec les métadonnées propres de l'opérateur. Un string, un clock et un color sont consommés, jamais tracés comme une série ; ⊤ et ⊥ sont [ErrPlot]. Rien de tout cela n'a été configuré.
Quatre règles complètent le tableau :
- Overlay si et seulement si la valeur partage l'axe des prix. Sinon elle reçoit un pane.
- Le co-tracé joint les échelles. Deux séries dans un même pane prennent la
⊔de leurs bornes ; si cette borne supérieure atterrit surquantity, vous obtenez[WarnBranchDim]et une suggestion de scinder le pane. (Un overlaypriceà côté d'un paneration'est pas un mélange — ce sont deux panes, et cela n'avertit de rien.) - Le kind final décide.
level + price → price, donc l'expression se met en overlay. - Les lignes de référence issues de la convention vivent dans
opMeta, pas dans le kind.osc(0,100)donne une ligne médiane ; le fait quersimarque conventionnellement 30 et 70 est une propriété dersi.
Un record éclate en ses champs, chacun présenté à son propre kind — c'est pourquoi
plot bollinger(close, 20, 2) produit trois lignes de prix plus une bande, et plot macd(close)
produit un pane centré avec un histogramme et une ligne de signal, sans aucun code pour le dire.
#Surcharges — l'intention l'emporte sur le défaut
L'inférence donne le défaut ; l'auteur le surcharge dans le bloc plot, et parce que le système
connaît le kind, la surcharge est intelligente plutôt qu'aveugle :
fluxm = macd(close)
plot m.macd { overlay } // a level FORCED onto the chart → it gets its OWN secondary axis
plot ema(close, 20) { pane } // a price FORCED into a pane → auto-scaled, fine
plot m.hist { style: histogram, color: if m.hist > 0 then up else down }
plot rsi(close, 14) { guides: [20, 80] } // authored reference lines, kind-checked as level|osc
ich = ichimoku() // sourceless: it reads high/low/close itself
plot ich.chikou { offset: -26 } // a DISPLAY shift: it moves the x position, never the value
| Surcharge | Effet |
|---|---|
{ overlay } / { pane } |
force le mode. Forcer un level ou un osc en overlay lui donne un axe secondaire — le système sait qu'une échelle de prix partagée le rendrait invisible. |
{ scale: own | shared } |
choisit l'échelle. { scale: shared } sur un level lève [WarnScale]. |
{ style: … } |
le glyphe de rendu — un ensemble fermé et autorisé par l'hôte : histogram, columns, stepline, area, circles, cross (une ligne est le défaut inféré). |
{ color: … } |
une expression de couleur par barre. |
{ guides: [ … ] } |
des lignes de référence rédigées par l'auteur, vérifiées par kind. |
{ title }, { precision }, { width } |
métadonnées de présentation. |
{ offset: ±lit } |
un décalage d'affichage le long de x, borné. Il déplace où une valeur est dessinée, jamais quelle donnée elle a lue — la causalité vit à l'index de données, donc dessiner dans le futur est un choix de rendu, pas un look-ahead. |
L'UI de paramètres est dérivée de la même façon : un input(…) synthétise son kind, et le kind donne
le widget (un champ numérique avec un range, un sélecteur de source, un booléen, une énumération),
avec title:/group:/tooltip: comme métadonnées optionnelles. Un descripteur d'accessibilité est
dérivé du kind également, de sorte qu'une série tracée est annoncée de façon significative sans que
l'auteur n'écrive d'étiquette.
#Ce qui peut être présenté, tout court
L'admissibilité de présentation est un jugement comme un autre, décidé par kind, et énuméré exhaustivement :
| Jugement | Admet | Rejette |
|---|---|---|
[Plot] |
price, level, osc(·), ratio, volume, pv, signal, slope, barspan, num, quantity, angle, depth, les dimensions composées, un decimal d'une dimension traçable, un record (éclaté), un vec d'un kind traçable |
string, time, duration, period, barindex, dir, color, clock, variant, ui, un vec brut irréductible, ⊤, ⊥ → [ErrPlot] |
[Mark] |
signal, dir |
tout le reste → [ErrArg] |
[Fill] |
deux opérandes de la même dimension, tirés de l'ensemble tracé de type prix — price, level, volume, pv, ratio, osc, slope, num (strictement plus étroit que l'ensemble ordonné) |
fill price..osc → [ErrDim] ; time, duration, barindex, barspan, angle — ordonnables mais non remplissables ; tout kind catégoriel ou structurel → [ErrArg] |
[ColorBars] |
signal, dir, color |
tout le reste → [ErrArg] |
[CmpOrd] |
scalaires ordonnés de dimension compatible et tags d'actif identiques | dir (catégoriel), string, color, clock, record, vec, variant, ui → [ErrDim]/[ErrArg] |
[CmpEq] |
scalaires, string, dir, color (égalité bit-à-bit) ; record, vec, variant (profonde, consciente de na) |
clock, ui — ils sont consommés, jamais comparés → [ErrArg] |
Notez l'asymétrie délibérée : dir n'est pas traçable comme une ligne et pas ordonnable,
mais il est compatible avec les marks et constitue un canal de coloration de barres licite. Ses
seuls canaux de présentation sont les deux qui ont du sens pour une direction à trois valeurs.
Pourquoi énumérer plutôt qu'argumenter. Parce que le treillis est fini par famille, ces tables sont vérifiées par machine — chaque kind, et chaque paire de kinds, est passé par chaque jugement. « Aucun kind n'est laissé sans sémantique » n'est pas une affirmation dans un document ; c'est un test qui échoue quand cela cesse d'être vrai.
#La politique d'erreurs
Une erreur est dure si et seulement si une garantie porteuse est violée — causalité, totalité, le pare-feu — ou qu'une impossibilité dimensionnelle apparaît dans une position exigeante. Tout le reste qui n'est que suspect est un avertissement avec un correctif rapide.
#Erreurs dures
| Code | Se déclenche quand | Exemple |
|---|---|---|
[ErrDim] |
l'algèbre dimensionnelle n'a aucune règle, ou les tags divergent | close + rsi(close,14) · btcUsd + btcEur |
[ErrRepr] |
même dimension, tags de représentation différents, pas de conversion explicite | if c then f64Price else decPrice · duration ⊔ period |
[ErrCausal] |
un cycle sans delay unitaire, un rééchantillonnage non causal, un lag non borné | une boucle de rétroaction écrite sans scan |
[ErrTotal] |
une fenêtre, capacité ou borne d'itération qui n'est pas une constante, ou dépasse N_max |
window(close, n) où n n'est pas const |
[ErrTotalRec] |
un cycle dans le graphe d'appels def |
def a() = b() · def b() = a() |
[ErrTotalType] |
un cycle dans le graphe de références de types | record Node { next: Node } |
[ErrTotalMatch] |
un match qui ne couvre pas chaque étiquette (ou _) |
un bras manquant, ou un na non couvert |
[ErrFirewall] |
l'analyse lit une valeur de présentation non déterministe | rsi(live(close), 14) · now() dans l'analyse · rand sans graine |
[ErrLen] |
deux longueurs de vecteur déclarées sont incompatibles | zipper des capacités déclarées qui ne peuvent pas s'élargir |
[ErrField] |
un champ manquant ou inconnu | bb.upprer · m with { typo: 1 } |
[ErrArg] |
le kind d'un argument n'est pas admissible à ce paramètre | passer un color là où un scalaire est exigé |
[ErrPlot] |
la valeur n'est pas présentable | plot time · plot someClock |
[ErrUnbound] |
un identifiant non lié ou un trou de syntaxe | en cours d'édition — un diagnostic contenu |
Le plan APP ajoute [ErrState] pour un champ de Model non borné, sur exactement ce motif ; voir
Plan APP.
#Avertissements
| Code | Se déclenche quand |
|---|---|
[WarnTop] |
une liaison intermédiaire atterrit sur ⊤/quantity et n'est jamais consommée |
[WarnAffine] |
une combinaison affine non normalisée (high + low sans /2) |
[WarnBranchDim] |
les branches d'un if, ou deux séries co-tracées, ont des dimensions différentes → quantity |
[WarnBoundsØ] |
deux bornes osc s'intersectent en rien |
[WarnLit] |
un littéral hors d'une borne connue (rsi(close,14) > 150) |
[WarnScale] |
{ scale: shared } sur un kind qu'une échelle de prix partagée aplatirait |
[WarnNaNChain] |
une chaîne susceptible de propager na là où nz était probablement voulu |
#Ce qu'est un message d'erreur
Un diagnostic est humain, dimensionnel, actionnable, et lié — jamais une trace de pile :
price + osc — you are adding a price and a 0–100 oscillator.
close + rsi(close, 14)
^^^^^^^^^^^^^^ osc(0,100), a dimensionless bounded value
A point on the price axis and a dimensionless number have no common meaning.
Did you mean close + atr(14) (a price + a displacement),
or close * norm(rsi(close, 14)) (scale the price by a fraction)?
→ kinds: the affine substrate
Un repaint refusé est expliqué, pas simplement interdit (« cela ferait changer une valeur passée une fois la barre close »), car le refus est la fonctionnalité.
Anti-cascade. Un nœud empoisonné se propage sans re-diagnostiquer : une seule faute racine produit exactement un message, et les nœuds en aval restent silencieux. Une seule faute de frappe ne produit pas quarante erreurs.
#na à l'exécution
Les kinds sont statiques ; na est l'histoire à l'exécution, et les deux sont conçus pour
s'accorder.
- Toute comparaison touchant
naproduitna— jamaistrue, jamaisfalse. L'absence est testée avecis_na(x)et la présence avecis_some(x), tous deuxsignal. nz(x, d)(et son sucrex ?? d) substitue une valeur par défaut ;max/minabsorbentnapoint par point, tandis que les réducteurs de fenêtre le propagent (une fenêtre contenant un trou produitna, correspondant à l'oracle natif dont ils doivent rester byte-identiques).- Un
matchsur un sujet possiblementnadoit le couvrir — un brasnaou_. - Déstructurer un record
nadonnenaà chaque champ. - Un NaN produit par l'arithmétique est
na— le kind est préservé, et il est affiché comme un vide plutôt que comme un échec. La division par zéro n'est pas une erreur ; c'est une valeur absente. - En interne,
naest forcé à un unique motif de bits canonique à chaque frontière de stockage, de hachage et de sérialisation. Rien de cela n'est observable dans un programme — c'est ce qui fait que deux moteurs s'accordent sur les octets d'une valeur qui n'est pas là (Modèle mémoire).
#Voir aussi
- Kinds — le treillis, les sortes, les tags, les déclarations nommées.
- Opérateurs — l'algèbre dimensionnelle règle par règle.
- Grammaire — comment les modes d'inférence résolvent la flèche et les têtes de conteneur.
- Temps et état — causalité, échauffement,
live()et le pare-feu en pratique. - L'éditeur — complétion filtrée par kind, cartes au survol, le cône typable dans l'aperçu en direct.
- Compilateur et runtime — pourquoi l'inférence déterministe est un prérequis à la byte-identité.
Temps et état
Cette page spécifie le modèle temporel du plan ANALYSIS : ce qu'est un flux, comment on accède au passé, pourquoi on ne peut pas accéder au futur, comment s'écrivent l'état borné et l'itération, et comment l'axe des pas d'une série — son horloge — est lui-même une valeur de première classe. Tout le reste de Flux s'appuie sur les garanties définies ici : causalité (une valeur ne dépend que du passé), no-repaint (une valeur, une fois produite pour un pas, ne change jamais), totalité (chaque construction se termine dans une borne connue à la compilation) et byte-identité (le même programme produit les mêmes octets sur chaque moteur, warm-up compris).
Les kinds (price, level, signal, …) sont spécifiés dans kinds.md ; cette page les
emploie librement et en annote ses exemples. Les horloges côté présentation — frames et événements —
relèvent de canvas.md et app-plane.md ; ici, l'horloge est
l'horloge des données, celle sur laquelle avancent les valeurs d'analyse.
#Tout est un flux
Une valeur du plan ANALYSIS est un flux : une valeur au fil des pas. close n'est pas un
nombre ; c'est l'historique entier des valeurs de clôture, une par pas de l'horloge du graphique. Une
constante est un flux dégénéré — la même valeur à chaque pas. Dans la spécialisation graphique, un
pas est une barre ; rien dans le modèle ne dépend de cette lecture.
L'arithmétique est élément par élément : une expression relie des flux entiers, et le résultat est de nouveau un flux.
fluxfast = ema(close, 12) // price — a stream: one value per step
slow = ema(close, 26) // price
spread = fast - slow // level — element-wise: spread at step t = fast[t] - slow[t]
Il n'y a aucun indice à gérer et aucune boucle à écrire : vous énoncez la relation une fois, et elle tient à chaque pas. Sous le capot, le runtime est incrémental — quand un nouveau pas arrive, chaque nœud avance d'un cran, en réutilisant son propre état borné ; rien n'est recalculé depuis le début. La lecture élément par élément (historiques entiers) et la lecture incrémentale (un pas à la fois) décrivent le même programme ; le compilateur vous doit leur équivalence.
Pourquoi cette règle existe. Les flux sans indices sont ce qui rend possible le reste de la page. Parce qu'un programme ne nomme jamais de positions, il ne peut nommer une position future ; parce que chaque opérateur avance pas à pas avec un état borné, la totalité et les bornes mémoire sont des propriétés du langage, non de la discipline de l'auteur.
#L'opérateur de délai x[n]
La seule façon d'atteindre le passé est le délai postfixe x[n] : la valeur qu'avait le flux x
il y a n pas.
fluxprev = close[1] // price — the previous step's close; na on the first step
diff = close - close[1] // price − price → level : the one-step change
up4 = close > close[4] // signal — na on the first four steps (comparison propagates na)
Règles :
nest une constante const-foldée,n ≥ 0— un littéral, ou un paramètre dont la plage bornée permet au compilateur de réserver le pire cas. Le délai, c'est de la mémoire ; la mémoire est bornée à la compilation.- Avant le pas
n,x[n]vautna— le passé qui n'existe pas encore (voir Warm-up etna). - Sur un
vec, le même postfixe[i]est une lecture d'élément bornée, non un délai ; le kind de l'opérande lève l'ambiguïté.
Un délai dépendant des données est rejeté : le retard serait non borné.
fluxk = barssince(close cross_up open)
close[k] // ✗ [ErrTotal] — the lag must be a compile-time constant
#Il n'y a pas d'indice négatif
x[-1] — « la valeur suivante » — n'existe pas. Ni comme forme déconseillée, ni comme lint qu'un
auteur déterminé pourrait faire taire : cette forme n'existe pas. L'indice de délai est non
négatif par la définition même du langage, et toute tentative d'en écrire un négatif est rejetée.
fluxclose[-1] // ✗ [ErrCausal] — the future is not addressable
Pourquoi cette règle existe. La causalité est une propriété de compilation, non une règle de style. Dans un langage où
x[-1]s'analyse et se contente d'un avertissement, chaque garantie en aval — rejouabilité, fiabilité des alertes, ré-exécution byte-identique — ne tient que pour les scripts bien élevés. Flux inverse le fardeau : le script mal élevé est inexprimable, si bien que les garanties tiennent pour tout programme qui compile.
#La causalité est un théorème
Parce que le passé n'est atteignable qu'à travers un délai borné et non négatif, et que l'unité en
formation de toute horloge est illisible (voir @ — l'éliminateur d'horloge),
tout programme ANALYSIS satisfait, par construction :
output[t] = f(inputs[0..t])
La valeur au pas t est une fonction des entrées jusqu'au pas t inclus — jamais de quoi que ce soit
de plus tardif. Deux conséquences en découlent.
Tout cycle de rétroaction franchit un délai d'une unité. Une définition peut dépendre de sa propre valeur précédente (c'est cela, un état courant), mais jamais de sa propre valeur courante — un cycle sans délai n'a aucune lecture causale et est rejeté :
fluxema20 = 0.1 * close + 0.9 * ema20 // ✗ [ErrCausal] — cycle with no unit delay
ema20 = scan(close, (prev) -> 0.1 * close + 0.9 * prev) // ✓ the delay is built into scan
scan (ci-dessous) est la manière sanctionnée de refermer une boucle de rétroaction : le combinateur
vous remet la sortie du pas précédent, si bien que le délai d'une unité fait partie de son sens plutôt
que d'être quelque chose que vous devez penser à insérer.
No-repaint. Puisque output[t] ne dépend que de inputs[0..t], et que les entrées sont en ajout
seul (append-only), une valeur une fois produite ne peut jamais être contredite par des données
ultérieures. C'est la garantie no-repaint : une valeur, une fois produite pour un pas, ne change
jamais. L'historique est immuable — le graphique auquel vous revenez en défilant est exactement celui
qui a été calculé en direct, l'alerte qui s'est déclenchée est exactement celle qu'une ré-exécution
déclenche. Le repaint n'est pas « détecté » ni « signalé » ; il est absent du vocabulaire. Toute la
famille des rejets partage un seul diagnostic : [ErrCausal] — délai négatif, rééchantillonnage non
causal, cycle de rétroaction sans délai d'unité, retard non borné.
#Warm-up et na
La plupart des noyaux ont besoin d'historique avant de pouvoir répondre : un RSI à 14 pas n'a rien
d'honnête à dire au pas 3. Jusqu'à ce qu'assez de données soient arrivées, la sortie d'un noyau est
na — la valeur absente. C'est son warm-up naturel, et chaque noyau hérite exactement du warm-up
de sa définition ; le langage n'impose par-dessus aucune politique globale de « na jusqu'à N ». La
byte-identité tient dès le tout premier pas : un noyau Flux et l'implémentation native du même noyau
chez l'hôte s'accordent sur chaque octet, warm-up compris (invariant I6).
na habite chaque kind (na : ∀κ.κ) et se propage à travers l'arithmétique avec le kind préservé. Sa
sémantique de comparaison est stricte :
- Toute comparaison impliquant
na—na == x,na < x, mêmena == na— vautna, jamaistruenifalse. On ne peut pas tester l'absence avec==. - L'absence se teste avec
is_na(x) → signal; la présence avec son dualis_some(x) = not is_na(x) → signal. - Un
matchsur une valeur possiblementnadoit la couvrir (un brasnaou_), sans quoi le match est rejeté comme non exhaustif. - Déstructurer un record absent (
let {upper, lower} = bbquandbbvautna) donnenadans chaque champ.
fluxr = rsi(close, 14) // osc(0,100) — na on steps 0..13
warm = is_na(r) // signal — 1 during warm-up
bad = r == na // na, always — never true; use is_na
assert r <= 100 // passes from step 0: na <= 100 is na, and an na verdict is PASS
Le verdict d'assert sur na est délibérément pass : une assertion ne se déclenche que sur un
signal définitivement faux, si bien que le warm-up ne peut produire de faux échecs. Là où l'absence
elle-même doit échouer, écrivez assert is_some(r) and r <= 100.
La substitution d'une valeur par défaut utilise nz(x, d), ou sa forme opérateur x ?? d (c'est la
même construction) :
fluxo = nz(obv(), 0) // volume — 0 is a literal and adopts the slot's kind
f = close ?? sma(close, 5) // price — x ?? d ≡ nz(x, d); right-associative
Les deux opérandes doivent s'accorder dimensionnellement (le kind du résultat est leur borne supérieure) ; même
dimension avec des tags de représentation différents — f64 contre decimal, temps machine contre
temps calendaire — est [ErrRepr], réclamant une conversion explicite.
Absorption vs propagation. L'arithmétique propage na. Trois opérateurs point à point
l'absorbent au contraire — math.max, math.min, nz — si bien que math.max(x, na) = x : une
opérande manquante n'empoisonne pas un extrême courant. Les réducteurs de fenêtre n'héritent pas de
cette absorption : highest(x, 20) sur une fenêtre contenant un trou donne na, exactement comme
sma, sum et stdev. L'asymétrie est fixée par la byte-identité aux noyaux de l'hôte : les
réducteurs de fenêtre suivent l'oracle natif, et un fold écrit à la main avec max (qui saute na par
absorption) est un programme différent — légitime — et non une orthographe plus rapide de highest.
Une note de représentation : na est un unique motif de bits épinglé à chaque frontière de stockage et
de hachage, pour que la byte-identité et la vérification du rejeu tiennent à travers les moteurs ; la
sémantique ci-dessus n'en est pas affectée. Les détails vivent dans
internals/compiler-and-runtime.md.
#Fenêtres et itération bornée
window(x, n) matérialise les n dernières valeurs d'un flux sous forme de vecteur :
fluxw = window(close, 20) // vec(price, 20) — the last 20 closes, at every step
La capacité n est const-foldée — un littéral, ou un input de plage bornée (le compilateur réserve
le pire cas). Une capacité non constante ou surdimensionnée est rejetée :
fluxlen = barssince(close cross_up open)
window(close, len) // ✗ [ErrTotal] — capacity must be a compile-time constant
fold et map sur une fenêtre sont la boucle for totale : ils visitent exactement la capacité de la
fenêtre, pas davantage, et se terminent par construction.
fluxw = window(close, 20)
hi = w.fold(na, (acc, x) -> math.max(acc, x)) // price — max absorbs na during warm-up
devs = w.map((x) -> x - sma(close, 20)) // vec(level, 20) — element-wise, kind-tracked
x[1] et la fenêtre window(x, 4) sur une même série : le passé qui n'existe pas encore se lit comme na.
#Pourquoi il n'y a pas de filter (ni de flatMap)
fluxw.filter((x) -> x > 0) // ✗ — there is no filter: the result length would depend on data
La longueur de sortie d'un filtre dépend des données ; un flatMap multiplie les longueurs par un
facteur dépendant des données. Ni l'un ni l'autre n'a de capacité à la compilation, et c'est la
capacité qui porte les garanties de totalité et de mémoire — chaque collection dans Flux est un
vec<κ>[n] dont le n est une borne que le compilateur peut imputer à son budget. La sélection ne
rétrécit donc jamais ; elle masque :
vec.where(v, pred)— même longueur,nalà où l'élément échoue au prédicat ;vec.mask(v, live)— même longueur,nalà où le vecteursignalparallèle vaut 0.
fluxw = window(close, 50) // vec<price>[50]
above = vec.where(w, (x) -> x > sma(close, 50)) // vec<price>[50] — na holes, no shrinking
Les vecteurs masqués se composent avec l'itération sensible aux na : un élément na ne produit
rien. Les folds et les réducteurs voient le trou et appliquent leur propre politique na ; la
compréhension de vue/canvas le saute purement et simplement — un élément masqué ne dessine aucun enfant :
fluxgroup { for lvl in window(close, 5) -> dot { at:(bar.i, lvl) } } // na elements: no dot
Pourquoi cette règle existe. « Mémoire bornée » n'est un théorème que si aucune opération ne peut faire croître une collection au-delà de sa capacité déclarée ni rendre sa taille une surprise d'exécution. Les masques gardent la forme statique et déplacent la question « combien ont survécu ? » dans les valeurs (trous
na, uncountlà où il en faut un) — ce qui est précisément l'information qu'un programme total peut transporter.
#État courant : scan
scan(seed, (prev) -> e) est l'accumulateur courant — la construction de rétroaction du langage. Au
premier pas, prev est la graine (évaluée à ce pas) ; à chaque pas ultérieur, prev est la valeur que
le scan lui-même a produite au pas précédent. La valeur émise est l'état. Le délai d'une unité
qu'exige la causalité est à l'intérieur du combinateur : prev a toujours un pas de retard, si bien
qu'un scan ne peut jamais lire sa propre sortie courante.
scan déroulé : l'arête de rétroaction franchit toujours un délai d'une unité, et chaque valeur émise est définitive.
L'exemple canonique — une moyenne mobile exponentielle construite à partir des principes premiers :
fluxdef ema0(s, n) =
let a = 2 / (n + 1) in
scan(s, (prev) -> a * s + (1 - a) * prev) // α → α : Σλ=1 affine step, kind-preserving
Un extrême courant tient en une ligne :
fluxpeak = scan(high, (prev) -> math.max(prev, high)) // price — running maximum since the first step
#État composite : une graine record
L'état reste rarement scalaire. Amorcez un scan avec un record et c'est le record entier qui est
l'accumulateur ; le système de kinds suit chaque champ.
fluxdd = scan({ peak: close, draw: 0 }, (prev) ->
let p = math.max(prev.peak, close) in
{ peak: p, draw: (p - close) / p }) // record{peak: price, draw: ratio}
plot dd.draw // (p − close) : level ; level ÷ price → ratio
Un trailing-stop avec bascule — la famille SuperTrend — est un record d'un price et d'un dir
({-1, 0, +1}, comparé avec ==, jamais matché) :
fluxdef flip(mult) =
let band = mult * atr(14) in // num × level → level
scan({ stop: close - band, side: 1 }, (prev) ->
if prev.side == 1 then
if close < prev.stop then { stop: close + band, side: -1 }
else { stop: math.max(prev.stop, close - band), side: 1 } // ratchet: never loosens
else
if close > prev.stop then { stop: close - band, side: 1 }
else { stop: math.min(prev.stop, close + band), side: -1 })
#Machines à états : une graine variant et match
Quand l'état est un mode, amorcez le scan avec un variant et faites-le avancer avec match —
l'éliminateur force le traitement de chaque mode ([ErrTotalMatch] sinon), si bien qu'une machine à
états ne peut pas oublier silencieusement un cas :
fluxvariant Phase { Flat | Long(entry: price) }
def step(prev) = match prev {
Flat -> if close cross_up ema(close, 50) then Phase.Long(entry: close) else Phase.Flat
Long(e) -> if (e - close) / e > 0.05 then Phase.Flat else prev
}
pos = scan(Phase.Flat, (prev) -> step(prev))
Regardez la règle de sortie, car c'est le système de kinds qui l'a écrite. Le premier jet évident est
close < e * 0.95 — et il est rejeté : e est un price, un point affine, et mettre un point à
l'échelle par un scalaire n'a aucun sens (5 % du « 42ᵉ parallèle » n'est pas un lieu). L'algèbre efface
la dimension, et comparer le résultat à un price est [ErrDim]. Ce que vous vouliez réellement dire
est un déplacement mesuré par rapport à l'entrée — (e - close) / e, un price − price sur un
price, ce qui est un ratio, et un ratio se compare volontiers à 0.05. Le compilateur n'a pas
simplement refusé le premier jet : il a nommé le second.
#Esquisse détaillée : une colonne point-and-figure
Les représentations pilotées par le prix maintiennent un état de colonne : dans quel sens la colonne
court, son extrême courant, combien de boîtes elle a remplies. En tant qu'état de scan :
flux// state : record{ dir: dir, extreme: price, count: num }
def pnfCol(box, rev) = // box : level (const-folded), rev : num
scan({ dir: 1, extreme: close, count: 0 }, (prev) ->
if prev.dir == 1 and close >= prev.extreme + box then
prev with { extreme: prev.extreme + box, count: prev.count + 1 }
else if prev.dir == 1 and close <= prev.extreme - rev * box then
{ dir: -1, extreme: prev.extreme - rev * box, count: rev }
else prev) // sketch: up-side only, one box per step
Les kinds sont tout l'intérêt de l'esquisse. count est un pur nombre de boîtes — sans dimension —
tandis que box : level porte la dimension de prix. Donc :
count * box : level— un nombre de boîtes fois une hauteur de boîte est un déplacement ;extreme + count * box : price— ancrer ce déplacement à l'extrême de la colonne redonne un point sur l'axe des prix.
Stockez count : num et l'algèbre reconstruit chaque quantité géométrique avec le bon kind ; stockez
un prix par boîte et l'arithmétique se typerait comme un non-sens (price + price n'a aucun sens
affine). La version de production de cette esquisse n'est pas du tout un scan mais un changement
d'horloge — pnf(box, rev) dans Horloges — et pourtant son état
interne se type par exactement ce raisonnement.
#stateful — l'échappatoire de bas niveau
stateful(seed, (st, bar) -> e) expose directement la primitive récursive du moteur : vous recevez
l'état précédent et les données brutes du pas courant, et vous renvoyez l'état suivant. C'est la même
construction que scan, le sucre syntaxique en moins, pour le rare noyau dont le pas ne peut se
formuler comme une expression sur des flux nommés. Préférez scan ; recourez à stateful quand
vous avez besoin du record de barre entier d'un coup.
fluxacc = stateful({ n: 0, ups: 0 }, (st, bar) -> // seed is a RECORD; bar is the whole step
{ n: st.n + 1, ups: st.ups + (if bar.close > bar.open then 1 else 0) })
#loop — itération bornée au sein d'un pas
loop(max, init, step) itère au sein d'un seul pas : max tours (const-foldé), en partant de init
et en appliquant step à la valeur courante. C'est le remplacement total d'un while : la borne fait
partie du programme, si bien que la terminaison est un fait, non un espoir. Les chercheurs de racines,
les solveurs de valeur implicite et les passes de lissage en sont les usagers naturels.
fluxdef nroot(x) = // num → num : Newton, a fixed 24 rounds
loop(24, x / 2, (g) -> (g + x / g) / 2)
plot nroot(rsi(close, 14)) // dimensionless in, dimensionless out
La totalité vient de max, et de max seul. C'est un plafond statique : le compilateur budgète le
pire cas, si bien que le coût d'un pas est connu avant que le pas ne s'exécute, quoi que fassent les
données. Sous cette forme livrée, le plafond est aussi le compte exact — loop exécute max tours et
prend la dernière valeur — ce qui, en pratique, ne coûte rien ici, car Newton double ses chiffres
corrects à chaque tour : une racine f64 a convergé bien avant le tour 24, et les tours restants sont
des points fixes.
Post-v1. Le design scellé porte un quatrième argument, un prédicat de sortie anticipée :
loop(max, init, step, until) s'arrête dès que until tient. Cela n'affaiblit pas la totalité —
le budget reste max, et le compilateur le réserve toujours — cela laisse seulement une itération
convergée cesser de payer pour des tours dont elle n'a pas besoin. La signature livrée ne le prend pas
encore, si bien que la forme ci-dessus est celle qui s'exécute aujourd'hui.
Aucune boucle non bornée n'existe dans la surface v1 : il n'y a aucun jeton pour cela, et chaque noyau
itératif pratique énonce son budget. (Open decision. Le design discute d'une échappatoire unsafe
optionnelle pour une boucle non bornée et la déconseille par défaut ; rien dans le catalogue n'en a
besoin.)
#Flux cumulatifs et ancrés
Deux combinateurs couvrent « depuis toujours » et « depuis un événement » :
cum(x) → α— la somme expansive depuis le premier pas (unscandéguisé) ;cumSince(reset, x) → α— la même accumulation, réinitialisée à chaque front montant dureset : signal.
fluxtotal = cum(volume) // volume — since the first step
sess = in_session("09:30-16:00 America/New_York") // signal — 1 while the session is open
sVol = cumSince(sess, volume) // volume — since the session opened
Le VWAP ancré est cet idiome appliqué à un ratio d'accumulations — et il se type parce que l'algèbre dimensionnelle divise les dimensions l'une par l'autre :
fluxsess = in_session("09:30-16:00 America/New_York") // signal — the anchor
avwap = cumSince(sess, close * volume) / cumSince(sess, volume) // pv ÷ volume → price
avwap = vwap(anchor: sess) // the packaged overload
N'importe quel signal peut ancrer : une ouverture de session, un croisement, une rupture
structurelle. Un input de kind price ou time peut être placé par clic sur le graphique —
l'hôte réécrit la valeur choisie comme entrée épinglée, si bien que l'analyse lit toujours une entrée,
jamais le pointeur, et un « VWAP depuis le pas où j'ai cliqué » reste pleinement causal et rejouable.
#Horloges : l'axe des pas est une valeur
Chaque série avance sur une horloge, et clock est un kind de première classe : un indice de pas
ordinal plus une correspondance entre indices de pas et temps. Une horloge répond à deux questions —
quel est l'horodatage du pas i ? et quel pas contient le temps t ? — et rien d'autre ; la
position sur l'axe est toujours l'indice ordinal, jamais le temps mural.
Quatre constructeurs bâtissent des horloges :
| constructeur | les pas avancent sur | exemple |
|---|---|---|
tf("1h") |
des intervalles de temps clôturés | heures, jours, semaines |
renko(box) |
le prix se déplaçant d'un box : level |
briques |
pnf(box, rev) |
le prix remplissant des boîtes, se retournant après rev |
colonnes point-and-figure |
range(r) |
le prix traversant une plage r |
barres de range |
Les horloges à grain temporel grossier et les horloges pilotées par le prix sont le même concept : une règle indiquant quand une unité se clôture. C'est pourquoi les représentations graphiques alternatives ne sont pas des moteurs de rendu greffés sur des données de barres — une série Renko ou point-and-figure est la machinerie de série ordinaire tournant sur une horloge différente — et pourquoi l'analyse multi-résolution n'est pas une fonctionnalité spéciale : une expression à une autre résolution est une expression sur une autre horloge, rééchantillonnée en retour (section suivante).
Les horloges sont des valeurs ordinaires. Elles circulent à travers if, à travers les paramètres de
def, à travers input :
fluxregime = adx(14).adx > 25 // signal — a trending regime
c = if regime then tf("1d") else tf("4h") // clock — chosen like any other value
Les paramètres de constructeur se const-foldent : renko(50) (le littéral adopte le kind level), ou
un input borné. Une boîte dépendante des données est rejetée — une horloge est un axe fixe, non une
quantité qui dérive avec les données qu'elle est censée indexer.
Les fonctions peuvent consommer des horloges comme n'importe quelle valeur : barsPerYear(clk) → num
dérive le nombre de périodes par an d'une horloge de temps (pour l'annualisation) ; sur une horloge
d'événements (renko, pnf), la question n'a pas de réponse et elle renvoie na avec un diagnostic.
#@ — l'éliminateur d'horloge
e @ c évalue e par unité de l'horloge c et relit le résultat sur les pas de la série courante.
C'est l'éliminateur du kind clock — les horloges sont hors de l'arithmétique, et @ est la seule
opération qui les consomme. Le rééchantillonnage préserve le kind (resample : (α, clock) → α) et est
causal : à tout pas, e @ c lit la valeur de e à la dernière unité clôturée de c — jamais
celle en formation.
fluxc = tf("4h") // clock — a first-class value
d = ema(close, 20) @ "1d" // price — the daily EMA, on the chart's steps
r = rsi(close, 14) @ tf("1h") // osc(0,100) — kind-preserving
x = close @ c // any clock value works as the operand
L'opérande peut être un littéral de chaîne (raccourci pour l'horloge à grain temporel qu'il nomme), un
identifiant ou un appel (tf("1h"), renko(box)), ou une expression parenthésée. @ est un
opérateur postfixe et lie plus fortement que la comparaison, ce qui rend l'idiome de confluence
naturel à lire :
fluxplot close > ema(close, 20) @ "1d" // signal — close vs the last CLOSED daily EMA
s'analyse comme close > (ema(close, 20) @ "1d") : la clôture du pas courant face à la moyenne
quotidienne à la date du jour complet le plus récent.
e @ tf("1h") : chaque pas fin lit la dernière unité grossière clôturée ; l'unité en formation est invisible pour l'analyse, et les pas antérieurs à la première unité clôturée lisent na.
#Le localisateur retient le plancher contenant
Faire correspondre le temps t d'un pas à une unité de l'horloge grossière suit la règle du
plancher contenant : l'unité dont l'intervalle contient t — la plus récente dont le début est
Tₖ ≤ t, jamais une correspondance au plus proche. La lire suit ensuite la règle de l'unité clôturée
ci-dessus : un pas à l'intérieur d'une unité encore en formation prend la dernière unité déjà
clôturée.
Pourquoi cette règle existe. L'arrondi au plus proche fait correspondre un pas de la première moitié d'une unité en formation à cette unité en formation — une valeur qui va encore changer — ce qui est une anticipation (look-ahead) : l'analyse lirait des données qui n'existaient pas au pas en cours de calcul, et l'historique subirait un repaint à la clôture de l'unité. Le plancher contenant est le seul alignement sous lequel
output[t] = f(inputs[0..t])survit au rééchantillonnage.
Avant la première unité clôturée de l'horloge grossière, e @ c vaut na — le rééchantillonnage
hérite du warm-up comme tout le reste, et la byte-identité tient au travers.
#Le contrat d'horloge
La machinerie de rééchantillonnage est fixée par sept invariants ; ils sont la sémantique de référence
de @ et des horloges en général :
| invariant | énoncé |
|---|---|
| I1 | La position est ordinale — une série est adressée par indice de pas ; le temps mural n'entre jamais dans l'axe. |
| I2 | Le localisateur de rééchantillonnage retient le plancher contenant — jamais l'arrondi au plus proche. |
| I3 | Seules les unités clôturées sont lisibles ; l'unité en formation est invisible pour ANALYSIS. |
| I4 | La grille de temps est la grille réelle de l'horloge, non une grille uniforme idéalisée. |
| I5 | Une horloge par série (v1) — la composition d'horloges est rejetée. |
| I6 | Byte-identité, warm-up compris : un noyau rééchantillonné coïncide avec le noyau natif dès le premier pas. |
| I7 | L'interpréteur et le moteur compilé produisent des octets identiques, vérifié à chaque compilation. |
I5 est la limite honnête de v1 : une série a exactement une horloge, si bien qu'une horloge ne peut pas être rééchantillonnée sur une autre horloge —
fluxclose @ renko(50) @ "1d" // ✗ — one clock per series in v1: no clock composition
est rejeté. Le rebucketing empilé (briques de prix, puis agrégation quotidienne des briques) est un concept cohérent et délibérément hors du périmètre scellé de v1.
#Séries étrangères et la règle as-of
Le rééchantillonnage est une instance d'un problème général : aligner une série qui se clôture selon
son propre calendrier sur l'axe ordinal du graphique. Un autre actif, une autre place de marché, un
autre calendrier de session — leurs pas ne coïncident pas avec ceux du graphique. La règle d'alignement
est la jointure as-of, et c'est la même règle de plancher contenant que @ :
Au pas
tdu graphique, une série étrangère lit son pas le plus récent d'horodatage ≤ l'instant det.
Jamais l'arrondi au plus proche — cela lorgnerait un pas étranger qui ne s'est pas encore produit.
Pendant un vide étranger (son marché fermé, celui du graphique ouvert), la valeur se maintient : la
dernière valeur étrangère connue est toujours la plus récente. Avant le premier pas étranger, la valeur
est na — le warm-up ordinaire.
fluxeth = series("ETH-USD").close // price[ETH,USD] — aligned as-of, holds over gaps
rel = close / eth // ratio — cross-rate (same quote), plottable
Parce que la jointure as-of n'admet aucune ligne future, la propriété no-repaint du composite est
héritée d'emblée : un indicateur inter-séries est exactement aussi rejouable qu'un indicateur
mono-série. Le système de kinds garde séparément ce que vous pouvez combiner : les tags d'actif et de
devise sur price font de price[BTC,USD] + price[ETH,USD] un [ErrDim] — voir
fdk/asset-currency.md.
#Points, durées, périodes
Trois kinds portent le temps lui-même, et la discipline affine de l'axe des prix s'applique mot pour mot à l'axe du temps :
| kind | rôle | représentation |
|---|---|---|
time |
un point sur la ligne du temps | instant machine (epoch 64 bits) |
duration |
un vecteur de temps machine écoulé | exact — tag machine |
period |
un vecteur de temps calendaire | conscient des zones/DST — tag calendar |
duration et period sont le même objet dimensionnel — un déplacement sur l'axe du temps —
distingués par un tag de représentation, exactement comme f64 et decimal taguent la même
dimension numérique. Les deux additions sont point + vecteur → point :
fluxage = time - time[1] // duration — pt(T) − pt(T) → vec(T), exact
expiry = time + time.months(3) // time — calendar arithmetic, DST-aware
later = time + time.months(1) + time.days(10) // period constructors compose
Les valeurs period se construisent uniquement par les constructeurs time.years(n),
time.months(n), time.weeks(n), time.days(n) (arguments constants). Les deux représentations ne se
mélangent jamais implicitement :
fluxtime.days(1) + (time - time[1]) // ✗ [ErrRepr] — calendar + machine: convert explicitly
Pourquoi cette règle existe. « Un jour » et « 24 heures » sont des affirmations différentes : au travers d'un passage à l'heure d'été, ajouter
time.days(1)retombe sur la même heure murale le jour civil suivant, tandis qu'ajouter unedurationde 24 heures atterrit à une heure près. Les deux sont utiles ; les confondre silencieusement, c'est ainsi que la logique de session casse deux fois par an. Le tag garde chaque addition honnête et fait de la conversion une décision visible.
Les accesseurs calendaires — year, month, day, hour, minute, second, dayOfWeek,
dayOfYear — projettent un time dans une zone déclarée et renvoient num. La zone par défaut est une
entrée de rejeu épinglée : puisqu'un accesseur s'exécute dans le plan ANALYSIS, sa zone ne peut pas
être un réglage ambiant par spectateur, sinon deux machines calculeraient des valeurs différentes pour
le même script ; là où aucun défaut épinglé ne s'applique, la zone est écrite explicitement, comme
in_session le fait déjà. L'arithmétique qui sort de la plage représentable ne boucle pas : elle donne
na avec un diagnostic, préservant l'ordre causal des horodatages.
#live(e) — l'unité en formation, affichage uniquement
Tout ce qui précède concerne les valeurs confirmées : des flux qui avancent quand une unité se
clôture. Les affichages veulent légitimement une chose de plus — l'unité encore en formation.
live(e) la fournit, sous forme de signal de présentation :
live(e)réévalue le sous-graphe d'analyse deeà chaque frame, y compris l'unité en formation ;κ(live(e)) = κ(e)— il préserve le kind.e @ live(tf("1h"))limite la lecture au rééchantillonnage : seule l'unité en formation de l'horloge grossière est lue en direct ; tout le reste reste confirmé.- Une valeur
live(…)ne peut circuler que vers des puits d'affichage :plot,mark,fill,color bars, une scène. Elle est provisoire par nature — la valeur de chaque frame remplace la précédente, et aucune n'est actée ni journalisée ; quand l'unité se clôture, la valeur confirmée prend le relais.
Tout puits confirmé est barré par le pare-feu — la frontière à sens unique entre présentation et analyse (voir book/04-the-four-planes.md) :
fluxplot live(ema(close, 20)) // ✓ display — the forming step included, per frame
h1 = high @ live(tf("1h")) // ✓ display — the forming hour's running high
rsi(live(close), 14) // ✗ [ErrFirewall] — feeding an analysis calculation
alert live(close) > sma(close, 20) // ✗ [ErrFirewall] — alerts are confirmed sinks
assert live(close) > 0 // ✗ [ErrFirewall] — assertions run on confirmed data
Le flux confirmé de e reste intact — byte-identique avec ou sans live où que ce soit dans le
script — et les valeurs live() sont exclues de l'oracle de byte-identité exactement comme le sont les
signaux d'horloge murale. Un script qui utilise live() est signalé non rejouable : son affichage
ne peut être reproduit depuis le journal, parce que les données en formation qu'il a peintes n'ont
jamais été actées. Son analyse demeure rejouable ; le drapeau porte sur les pixels.
Pourquoi cette règle existe. L'unité en formation est la seule valeur du système qui va changer. La laisser entrer dans un calcul fabriquerait précisément le repaint que le langage existe pour exclure — un indicateur qui paraît prémonitoire en direct et se réécrit à la clôture. Le router uniquement vers des puits d'affichage donne l'usage légitime (observer l'unité courante se développer) sans aucun effet sur quelque valeur confirmée, alerte ou rejeu que ce soit.
#Aides pour sessions, événements et pivots
Le petit vocabulaire qui relie les flux aux calendriers et aux événements :
| aide | kind | signification |
|---|---|---|
in_session(spec) |
signal |
1 tant que la session nommée est ouverte ; la spec nomme les heures, la zone et le calendrier de l'actif |
barssince(s) |
barspan |
pas écoulés depuis le dernier déclenchement de s |
valuewhen(s, x) |
kind de x |
la valeur qu'avait x au dernier déclenchement de s |
count(s, n) |
osc(0,n) |
combien des n derniers pas ont déclenché s |
rising(x, n) / falling(x, n) |
signal |
monotone sur les n derniers pas |
a cross_up b / a cross_down b |
signal |
croisement, sous forme de comparaison infixe |
barssince renvoie barspan, non un simple nombre : les comptes de pas portent la dimension ordinale
(l'axe des x est affine lui aussi), si bien qu'un compte de barres ne peut pas être silencieusement
ajouté à un prix — tandis que barindex − barindex → barspan et les quantités de type pente (price
par barspan) tombent de l'algèbre avec les bons kinds.
fluxsess = in_session("09:30-16:00 America/New_York") // signal
sinceX = barssince(close cross_up ema(close, 50)) // barspan
atX = valuewhen(close cross_up ema(close, 50), close) // price — held until the next firing
#Confirmation de pivot : la latence dans la signature
Un pivot — un extremum local — n'est connaissable qu'a posteriori : un sommet n'est un sommet-pivot que
lorsque assez de pas ultérieurs n'ont pas réussi à le dépasser. Flux rend cet a posteriori explicite.
Les détecteurs de pivot renvoient signal @lag right : le signal se déclenche au pas où la confirmation
s'achève, right pas après l'extremum, et l'annotation @lag fait partie de la signature figée — la
latence est documentée, bornée et visible pour le lecteur, non une surprise d'implémentation. La valeur
du pivot se récupère avec valuewhen :
fluxph = pivot_high(high, 3, 3) // signal @lag 3 — confirmed 3 steps after the top
lvl = valuewhen(ph, high) // price — the last confirmed pivot level
L'alternative familière — un zigzag dont le dernier segment mute jusqu'à ce que le pivot suivant se
confirme — est inexprimable, et délibérément : une dernière valeur mutante signifie qu'une valeur
produite a changé (repaint, exclu par le théorème de causalité), et sa latence de confirmation est non
bornée (exclue par la totalité). La décomposition causale est : les pivots confirmés comme valeurs
d'analyse (ci-dessus), et — là où l'on veut un segment en formation à l'écran — un marqueur de
présentation provisoire alimenté par live(), qui ne devient jamais une valeur d'analyse. Ce qui ne
peut être écrit est précisément la partie qui aurait menti.
#Voir aussi
- kinds.md — le système de kinds dimensionnels que portent ces flux : sortes, tags,
nacomme⊥. - operators.md — l'algèbre dimensionnelle par opérateur, la famille
?,with. - inference.md — comment les kinds (et le
nade warm-up) se propagent dans un programme. - canvas.md — l'horloge de frame, les signaux de présentation, et où atterrissent les valeurs
live(). - fdk/compute.md — les dataframes,
asofJoinet les statistiques à fenêtre bâties sur ces primitives. - guides/cookbook.md — des recettes détaillées : confluence multi-résolution, VWAP ancré, machines à états.
Le plan CANVAS
Le plan canvas est là où un programme montre des choses. Il tourne sur la frame, il a le droit d'utiliser l'espace écran, le temps de l'horloge murale et l'aléa, et il peut lire tout ce que le plan d'analyse a calculé — tout en étant structurellement incapable d'y réécrire.
Il a un seul axiome, et l'axiome est toute la conception :
Chaque propriété est un signal. Une constante, une valeur de données et une animation sont la même sorte de chose. Il n'y a donc aucune API d'animation — animer une propriété, c'est lui donner un signal qui varie, exactement comme tracer une valeur, c'est lui en donner un qui le fait.
fluxdot { at: (bar.i, close), r: 4 } // a constant radius
dot { at: (bar.i, close), r: 2 + norm(volume) * 8 } // a data-driven radius
dot { at: (bar.i, close), r: tween(2 -> 10, 400ms) }// an animated radius
Trois programmes, un seul modèle de propriétés, aucun framework.
Une note sur les exemples. Flux n'a pas d'instructions-expressions : une expression seule n'est donc pas un programme. Les lignes marquées
✗sur cette page sont par conséquent des fragments d'expression : elles montrent ce que les règles de kind refusent, non ce que l'analyseur accepte. Toute ligne non marquée est une instruction légale.
#Les quatre axes
Un programme canvas est organisé selon quatre axes orthogonaux : les espaces (où vit une coordonnée), les signaux (ce qu'est une propriété), les événements → actions (ce que fait l'interaction), et la composition (comment les éléments sont groupés et répétés).
#1. Espaces — la coordonnée dérive son axe de son kind
| Ce que vous écrivez | L'axe sur lequel elle atterrit |
|---|---|
price |
l'axe des prix |
time, bar.i (barindex) |
l'axe des x temps / ordinal |
screen.cx, screen.w, … |
les pixels du viewport |
pane, ratio |
une fraction de sous-pane |
z (depth) |
l'axe de profondeur — projeté en 3D, aplati en 2D |
Mélanger deux espaces dans une même coordonnée est [ErrDim] à la compilation. Une scène
géométriquement incohérente n'est pas exprimable.
fluxdot { at: (bar.i, close), r: 4 } // the time axis × the price axis
line { at: (bar.i, ema(close, 200)), w: screen.w, stroke: token.grid }
dot { at: (bar.i, close + volume) } // ✗ [ErrDim] — price + volume: two axes, one coordinate
L'exception dont tout le monde a besoin — épingler une étiquette huit pixels au-dessus d'une bougie —
est l'ancre composite. Écrite high + 8px dans une coordonnée, c'est un constructeur de
coordonnée à deux composantes — une ancre de données plus un décalage en pixels — et non une somme
arithmétique entre sortes. Les deux composantes gardent leurs propres axes et ne sont jamais ajoutées
l'une à l'autre ; le + est la notation du constructeur, non l'opérateur.
La partie en pixels doit être une const. Un décalage dérivé de l'écran (screen.h * 0.05) rendrait
toute la position dépendante de l'écran, et la position est l'une des choses qui doivent rester
déterministes. display énonce la même règle du côté du moteur de rendu.
#2. Signaux — les générateurs et les combinateurs
| Famille | Membres |
|---|---|
| Générateurs | tween(a -> b, d, ease) · sweep(a -> b, d) · wave(sin|tri|saw, amp, T) · spring(target) · noise(seed, v) · ramp · throb |
| Combinateurs | mix · lerp · clamp · norm · stagger · since · hold · pick · rand |
| Faits de l'hôte | now() / clock:wall · screen.* · bar.isLast · chart.lastBar |
| Le lecteur de la barre en formation | live(e) — puits d'affichage uniquement |
Post-v1. Le générateur spring est livré sous sa forme à un seul argument spring(target) ; une
raideur optionnelle spring(target, k) est conçue et différée.
Chacun des « faits de l'hôte » est réservé au canvas : le lire depuis l'analyse lève
[ErrFirewall]. Cela inclut l'aléa — avec une dérogation délibérée :
fluxjitter = rand(1337) // ✓ legal in analysis: a pinned integer generator, replayable, byte-identical
rand() // ✗ [ErrFirewall] in analysis — unseeded randomness is not replayable
L'aléa avec graine est déterministe et donc admissible partout. L'aléa sans graine est un signal de présentation, et il reste du côté présentation du mur.
#3. Événements → actions
fluxema50 = ema(close, 50)
on click -> burst(40) ring { life: 2s }
on every(1 bar) -> spawn ring { r: 6 -> 24, opacity: 100% -> 0% }
on close cross_up ema50 -> flash
on switch(asset) -> morph chart over 500ms
L'opérande d'événement peut être un flux booléen issu de l'analyse (close cross_up ema50), un
timer (every(1 bar), every(300ms), every(~2s) pour une période avec gigue), ou un événement
de pointeur (hover, click, drag, enter, exit, move, wheel).
L'action peut être un spawn (spawn, burst(n), emit rate(r) — tous puisés dans un pool
plafonné avec un life:), un tween d'une propriété, un effet borné (flash, bounce, pulse,
shake), ou un set.
L'interaction sur ce plan reste cosmétique : elle peut spawn, tween, flash, et régler des propriétés de présentation. Elle ne peut pas changer une valeur calculée, et elle ne peut rien persister. Quand vous avez besoin d'un état qui survit à un événement et décide ce qui est affiché, vous êtes passé dans le plan APP — et le langage vous oblige à le dire.
#4. Composition
fluxgroup { dot { at: (bar.i, close), r: 3 } } // transform / blend / clip a subtree
repeat 8 as i { dot { at: (bar.i, close), r: 2 + i } } // instancing — `i` parameterizes the shape
for lvl in window(close, 5) -> dot { at: (bar.i, lvl) } // a comprehension over a BOUNDED collection
repeat et for sont les deux façons de fabriquer beaucoup de choses. Les deux sont bornées par
construction — un compte const, ou une collection dont la capacité est déclarée — ce qui rend le
budget d'instances calculable à la compilation plutôt que découvert à 3 heures du matin en production.
Pour itérer sur les indices 0 … N−1 plutôt que sur des données, prenez-les comme une collection :
vec.range(N) est le vecteur de ces indices, et la compréhension le consomme comme n'importe quel autre.
fluxstep = atr(14)
for i in vec.range(5) -> dot { at: (bar.i, close + i * step), r: 3 }
La règle à retenir n'est pas que les indices sont indisponibles — c'est que le compte doit être
const. vec.range prend un littéral, exactement comme window et repeat. Un compte dépendant des
données rendrait le budget d'instances dépendant des données, et le budget est précisément ce qui doit
être connu avant que la première frame ne soit dessinée.
#Les primitives
L'ensemble est clos et contrôlé — un script ne peut pas inventer une primitive, et ne peut donc pas faire passer en fraude du balisage, une URL ou un buffer d'octets bruts dans le moteur de rendu :
dot · circle · ring · rect · square · triangle · poly · line · path
text · image · svg · sparkline · backdrop
Elles partagent un seul modèle de propriétés : at, size / r / w / h, rotate, fill,
stroke, width, opacity, glow, blend, z, life, color, trail, paintOrder. Le modèle
est tout le vocabulaire — il n'y a aucun dialecte par primitive par-dessus. Une line se positionne
avec at et se dimensionne avec w / h, exactement comme un dot ; elle n'a aucune propriété
d'extrémité qui lui soit propre.
Une scène peut aussi être une valeur :
fluxdef overlayOf(m) = scene {
line { at: (bar.i, m.anchor), w: screen.w, stroke: token.grid, width: 2 }
for lvl in m.levels -> dot { at: (bar.i, lvl), r: 3, opacity: 60% }
}
scene{…} a le kind ui, si bien qu'elle peut être renvoyée d'une fonction et remise à une fenêtre —
c'est ainsi qu'un overlay de dessin atteint un pane de graphique sans que le plan canvas et le plan app
aient à se connaître l'un l'autre.
#Le modèle de performance
Vous n'écrivez jamais de boucle de rendu, et vous n'en optimisez jamais aucune. La scène se compile une fois, et le compilateur classe chaque signal et l'achemine :
| Classe | Exemple | Route | Coût par frame |
|---|---|---|---|
| statique | stroke: token.grid |
en cache — jamais recalculé | 0 |
| par barre | at: (bar.i, ema(close, 20)) |
buffers pré-alloués ; formes identiques instanciées | O(Δ barres) |
| par frame, temps seulement | glow: throb(0.4) |
le compositeur de l'hôte | 0 JavaScript par frame |
C'est la troisième ligne qui compte. Les propriétés qui bougent le plus — un glow, une pulse, un parallax — sont exactement celles qui ne coûtent rien, parce qu'elles ne touchent jamais le runtime du langage du tout.
Les émetteurs (spawn, burst, emit rate) puisent dans un pool plafonné avec un life:
obligatoire, si bien qu'une tempête de particules a un plafond à la compilation. Dépasser un budget de
scène — opérations de draw-list, instances, ou travail GPU dans le pire cas — est [ErrSceneBudget]
à la compilation. Il n'y a aucun dépassement mémoire à l'exécution, et aucune réinitialisation du
périphérique.
#Ce que canvas peut et ne peut pas faire
| Peut | Ne peut pas |
|---|---|
| lire n'importe quelle valeur d'analyse | écrire n'importe quelle valeur d'analyse |
utiliser now(), screen.*, rand sans graine |
laisser l'un d'eux atteindre une décision |
lire la barre en formation via live() |
alimenter une alerte, une assertion ou un calcul avec live() |
| spawn, tween, flash, set | persister, activer un autre script, reconfigurer l'app |
| basculer la visibilité de sa propre sortie | toucher à celle de quoi que ce soit d'autre |
Tout ce qui est dans la colonne de droite lève une erreur de compilation assortie d'une explication —
habituellement [ErrFirewall], et habituellement avec la suggestion du plan que vous vouliez réellement.
#Voir aussi
- Les quatre plans — le pare-feu, et où s'y situe canvas.
- Transitions — le plan qui interpole entre deux états calculés.
- display — la scène retenue, les deux strates,
viz.*, les panes et les fenêtres. - Temps et état —
live(), les horloges, et ce que signifie « la barre en formation ». - Plan application — quand l'interaction a besoin de se souvenir de quelque chose.
- color — les tokens, les couleurs explicites, et l'interpolation perceptuelle.
Le plan TRANSITION
Une transition interpole le rendu entre deux états qui ont déjà été calculés. Cette phrase est tout l'argument de sûreté : si les deux extrémités sont des valeurs produites par le plan d'analyse, alors interpoler entre elles ne peut pas produire une nouvelle valeur — donc une transition est cosmétique par définition, et aucune animation, si élaborée soit-elle, ne peut repeindre un graphique.
Le plan existe parce que l'alternative est pire. Une animation greffée sur une couche de rendu finit par lire un état qu'elle ne devrait pas lire, et par ré-entrer dans des calculs où elle ne devrait pas ré-entrer. Lui donner son propre plan, avec sa propre horloge et sa propre règle, garde la garantie là où elle doit être.
#Ce qu'un script peut faire ici
Cinq pouvoirs, et pas plus :
flux// ① parameterize a built-in morph
on switch(asset) -> morph chart over 500ms { ease: inOutCubic ; stagger: 0.3 ; surplus: collapse }
// ② carry the transition of a custom representation (the `morph:` hook of its descriptor — post-v1)
// ③ trigger a bounded effect on a signal
on close cross_up ema(close, 50) -> flash
// ④ animate the view
on click -> focus(view, at: (bar.i, close), zoom: 2.0, over: 600ms, ease: outBack(1.2))
// ⑤ replay history from the bar where a signal fired
replay from close cross_up ema(close, 200) over 8s
switch(asset) est un événement de l'hôte, non un symbole d'analyse : l'actif courant est un fait
que l'hôte détient, et le pare-feu interdit à l'analyse de le lire. Il est délivré comme n'importe quel
autre front de présentation (hover, click, enter), ce qui explique qu'une transition puisse y
répondre là où un indicateur ne le peut pas.
Post-v1. Le pouvoir ② — le hook morph: par représentation — est conçu, mais ce n'est pas encore
une clé d'auteur : le contrôleur de morph est mono-type (bougie), et le descripteur auquel le hook se
rattacherait est réifié dans le même déploiement. D'ici là, une représentation personnalisée s'anime
via le morph intégré (①), qui est le chemin que prend déjà toute représentation intégrée.
replay from prend un signal, non un indice de barre. Le rejeu commence à la barre où la condition
se déclenche et se déroule vers l'avant sur la durée déclarée, ce qui vous permet d'adresser un rejeu
par ce qui s'est passé plutôt que par à quelle distance dans le passé c'était. Le point de départ
est donc une valeur d'analyse que l'oracle contient déjà — le plan de transition la découvre, il ne la
calcule jamais.
#La stabilisation est dans l'oracle ; la trajectoire, non
C'est la distinction qui fait fonctionner le plan, et il vaut la peine d'être exact à son sujet :
| Dans l'oracle de byte-identité ? | Pourquoi | |
|---|---|---|
| la valeur de stabilisation — là où la transition atterrit | oui | c'est de la donnée d'analyse (ou une valeur de Model) ; c'est ce que la scène est une fois l'animation terminée |
la trajectoire — comment elle y arrive, t ∈ (0,1) |
non | elle est par frame, dépendante du périphérique, et observable par personne d'autre que l'œil |
Deux conséquences en découlent directement.
prefers-reduced-motion ne change rien d'important. L'hôte l'applique au compositeur : il saute
directement à l'état de stabilisation. Puisque la stabilisation est dans l'oracle et que la trajectoire
ne l'est pas, le verdict — les nombres, le golden, le rejeu — est identique dans les deux cas.
Une transition ne laisse jamais fuiter sa progression dans une décision. Le plan n'expose que le
front terminal (Done), jamais la progression continue ; et ce front est planifié à un rang de
journal déterministe dérivé de la durée déclarée, non au moment d'horloge murale où l'animation s'est
trouvée finir. Deux clients, l'un animant et l'autre avec le mouvement réduit, journalisent le même
front au même rang. C'est l'invariant [TransSettle] ; voir
display.
Pourquoi la progression est retenue. Si un script pouvait lire
t = 0.63en pleine animation et le stocker dans un modèle, alors le modèle — et tout verdict calculé à partir de lui — dépendrait de la fréquence d'images, du périphérique et de l'humeur du GPU. Retenir la trajectoire n'est pas une limitation de l'animation ; c'est ce qui rend l'animation gratuite.
#Le descripteur de transition
Les intégrés et les scripts convergent sur un seul descripteur, si bien qu'une représentation personnalisée s'anime exactement comme le fait une bougie :
| Champ | Signification |
|---|---|
durationMs |
combien de temps |
easing |
la courbe — une valeur de la sorte ease, opaque et contrôlée par l'hôte |
wave, staggerSpread |
la forme du décalage (stagger) à travers les éléments |
wickLead |
l'amorce des parties fines, avant les corps |
surplusPolicy |
collapse | spawn | hold — ce qui arrive aux éléments qui n'ont pas de contrepartie de l'autre côté |
chromeFadeFrac, holdDeadlineMs, flipTiming |
les temporisations cosmétiques autour du morph |
Des surcharges par appel (over D, stagger, surplus:) l'affinent au point d'utilisation.
ease est une sorte, non un variant — vous pouvez faire circuler inOutCubic ou
cubicBezier(a,b,c,d), mais vous ne pouvez pas faire un match sur une courbe pour la décomposer.
Cette opacité est délibérée : une courbe décomposable laisserait un script re-paramétrer le temps d'une
manière dépendante des données, et le caractère borné de l'animation s'en irait avec elle.
#La frontière avec le moteur natif
Le lourd morph par bougie reste natif — c'est un chemin chaud, et ce n'est pas au langage de le réimplémenter. Flux orchestre : il remplit le descripteur une fois, et le contrôleur natif l'exécute.
Cette séparation est la raison pour laquelle il n'y a aucune falaise de performance quand vous animez : le script ne s'exécute pas par frame, et ce qui le fait est le code qui était déjà là.
#Ce qu'une transition ne peut pas faire
| Peut | Ne peut pas |
|---|---|
| interpoler le rendu entre deux états calculés | calculer un état |
| lire des valeurs d'analyse (pour savoir où atterrir) | écrire une valeur d'analyse |
répondre à un front de l'hôte (switch(asset), un clic) |
lire l'horloge murale dans un Model |
| exposer son front terminal | exposer sa progression |
#Voir aussi
- Les quatre plans — où se situe TRANSITION, et pourquoi il est séparé.
- Canvas — le plan dont les transitions empruntent l'algèbre de signaux.
- display — les deux strates, les invariants de transition,
prefers-reduced-motion. - Host integration — le descripteur de transition comme cible de compilation.
- Temps et état — pourquoi une interpolation ne peut jamais être un repaint.
Le plan APP — applications
Post-v1. Le plan APP est entièrement conçu et strictement additif au cœur figé ; son déploiement suit le langage v1. Tout ce qui suit est normatif quant à ce qu'une application est.
Les trois premiers plans calculent, présentent et interpolent. Aucun d'eux ne peut détenir un état qui persiste entre les événements et décide de ce qui est affiché — un score, un document, une sélection, un blotter de positions ouvertes. Cette unique primitive manquante est ce qu'ajoute le plan APP : un modèle réductible, sous une recette qui préserve chaque garantie sur laquelle s'appuient les autres plans.
Une application est un modèle, un update pur, une view pure, et un ensemble d'abonnements déclaratifs. Les effets sont des données inertes que l'hôte exécute ; les capacités sont en refus par défaut ; le journal de messages est l'unique source de vérité, si bien qu'une application peut être rejouée message par message, testée sans un seul mock, et ré-exécutée par un serveur bit à bit.
Une note sur les exemples. Plusieurs exemples ci-dessous portent un membre d'un bloc
app— unupdate, uneview, unsubs— hors de son bloc. Un membre n'est légal qu'à l'intérieur d'un tel bloc, alors lisez ces exemples comme si le membre siégeait dansapp name { … }; les déclarations de type et dedefà côté de lui sont des instructions de premier niveau ordinaires. Tout autre exemple de cette page est un programme complet.
#Undo, redo et voyage dans le temps sont gratuits
Dans la plupart des bases de code, undo/redo est une fonctionnalité que vous reconstruisez dans chaque application : une pile d'opérations inverses, maintenue à la main, et subtilement fausse sur les bords — l'undo qui oublie un champ, le redo qui ramène une sélection dont vous étiez déjà passé à autre chose. Dans Flux, vous ne la construisez pas du tout. C'est une propriété de l'architecture, parce que l'état d'une application ne change jamais autrement qu'à travers un unique réducteur déterministe et journalisé.
L'état est un fold : le modèle est fold(init, [the journal of every message so far]). Undo
rembobine le journal d'un pas et refold ; redo le ré-étend. Parce que le fold est une fonction pure
du journal, l'état sur lequel vous atterrissez est exactement l'état que vous avez quitté — reproduit,
non reconstruit.
fluxvariant Msg { Bump | Undo | Redo }
app tally {
capabilities: [ journal ]
init(p) = { doc: { n: 0 }, ui: { pending: na } }
update(m, msg) = match msg {
Bump -> { model: m with { doc: m.doc with { n: m.doc.n + 1 } }, cmds: [] }
Undo -> { model: m, cmds: [ Journal(UndoToMark) ] }
Redo -> { model: m, cmds: [ Journal(RedoToMark) ] }
}
view(m) = row {
button("undo", Undo)
text("{m.doc.n}")
button("+1", Bump)
button("redo", Redo)
}
subs(m) = []
}
Les deux bras qui donnent à cette application tout son historique d'undo sont Journal(UndoToMark) et
Journal(RedoToMark). Ils renvoient le modèle inchangé et remettent à l'hôte une commande inerte ;
l'hôte — là où vit le journal — le tronque jusqu'à la cible et re-fold (init, [msg]') en le nouveau
modèle. Rien dans le code de l'application ne sait annuler une édition. Il ne sait que progresser.
Un seul mécanisme, deux visages. Pour l'utilisateur, c'est l'undo/redo dans l'app — un undo qui
ne peut pas oublier silencieusement un champ ni ressusciter une sélection périmée, dans un éditeur de
niveaux, un outil de trading, un jeu. Pour le développeur, le même journal est un débogueur à voyage
dans le temps : le curseur d'axe-barre du débogueur d'analyse devient un curseur d'événements.
Faites défiler jusqu'à n'importe quel message passé, avancez à reculons, posez un point d'arrêt sur
donnée — arrêter au premier message où score franchit 100 — et placez l'exécution à côté d'une
exécution de référence pour voir où, si tant est, elles divergent.
Cela en découle de la même façon pour des applications très différentes :
- Un éditeur de niveaux — chaque trait est un message, si bien que l'undo ramène le dessin exactement : le même résultat, parce que le même fold.
- Un jeu — rembobinez jusqu'à n'importe quel coup et rejouez à partir de là ; le match entier est la liste des coups, et n'importe quel préfixe de celle-ci est un état valide.
- Un blotter — faites défiler l'historique des positions comme une vidéo, parce que l'historique est le journal.
Pourquoi l'undo est correct, et non presque-correct. Voici la subtilité qu'un undo bricolé à la
main rate presque toujours. Le modèle d'une application est scindé en deux : un doc — l'état
métier que l'historique possède — et un ui — la sélection, le curseur, la forme à demi dessinée,
la requête en vol — que l'historique ne possède pas. L'undo rembobine doc et laisse ui intact.
Ainsi il ne ranime jamais une sélection que vous avez faite il y a trois éditions, ni une requête qui a
depuis abouti. La frontière fait partie de la conception ; vous ne pouvez pas oublier de la tracer,
parce que les deux vivent dans des champs différents du modèle.
Pourquoi le redo est exact, et non approximatif. Flux est déterministe à l'octet — l'interpréteur
et le module compilé s'accordent sur chaque bit, et na a un unique motif canonique — si bien que
re-folder le journal reproduit l'état octet par octet. Cela inclut les réponses revenues du réseau : un
résultat asynchrone est entré dans le modèle sous forme de message, il est donc déjà dans le
journal, rejoué tel quel plutôt que re-récupéré. Rembobiner ne déclenche jamais la requête une seconde
fois ; il lit la réponse qui a déjà eu lieu.
Deux notes honnêtes. D'abord, ceci est du design, non du code livré — le plan APP est séquencé après le langage v1 (le label en haut de cette page), et rien de tout cela ne s'exécute encore. Ensuite, c'est exempt de travail sur mesure, non exempt de coût : un undo reconstruit le modèle en re-foldant depuis le checkpoint mémoïsé le plus proche, si bien qu'il coûte un travail proportionnel à la distance en arrière jusqu'à ce checkpoint, et la machinerie — un journal de messages, un fold, une chronologie d'événements — est réelle, modeste et nouvelle. Elle est réutilisée par chaque application plutôt que reconstruite dans chacune, ce qui est tout l'intérêt. Le contrat formel, les bornes de coût et les deux exceptions nommées sont dans Totalité, déterminisme, rejeu ci-dessous. Et notez l'affirmation exacte : ceci est un undo/redo sur l'exécution d'une application — les événements qu'elle a traités — jamais sur son code source.
#La forme d'une application
fluxapp counter {
capabilities: [ clock, sfx ] // `clock` backs OnTick; `sfx` backs PlaySfx — both are needed
init(p) = { n: 0 }
update(m, msg) = match msg {
Tick -> { model: m with { n: m.n + 1 }, cmds: [] }
Reset -> { model: m with { n: 0 }, cmds: [ PlaySfx("reset") ] }
}
view(m) = row {
text("count: {m.n}")
button("reset", Reset)
}
subs(m) = [ OnTick(1000, Tick) ]
}
Les deux entrées de cette liste de capacités sont porteuses, et la seconde est facile à oublier. clock
est ce qui soutient OnTick ; sfx est ce qui soutient PlaySfx. Retirez sfx et le programme cesse
de compiler — non au moment où le son aurait été joué, mais au bras Reset, parce qu'émettre une
commande que le manifeste n'accorde pas est [ErrCapDenied]. La liste n'est pas une documentation
d'intention ; c'est l'octroi contre lequel le compilateur vérifie chaque cmds:.
Le bloc app compile en un descripteur — un frère des descripteurs de représentation et d'outil, sans
classe de base :
| Membre | Kind | Rôle |
|---|---|---|
capabilities: |
une liste de références de capacités | ce que l'application demande. Refus par défaut : tout ce qui n'est pas listé n'est pas simplement indisponible, c'est une erreur de compilation que de l'émettre. |
init(params) |
→ Model |
l'état initial pur |
update(model, msg) |
→ record{ model: Model, cmds: vec(Cmd, N) } |
le réducteur pur, total, déterministe |
view(model) |
→ UiTree |
un arbre pur de primitives contrôlées — jamais du balisage brut |
subs(model) |
→ [Sub] |
des entrées déclaratives, recalculées à partir de chaque modèle |
contributes? |
— | contributions d'interface optionnelles (panes, panels, commandes, outils) |
Les cinq noms de membres sont des mots-clés fixes, non des identifiants libres : les rôles du harnais font partie du langage, si bien que le compilateur peut les vérifier.
#Le Model — un état borné
Chaque champ d'un Model doit avoir un kind borné. Les scalaires conviennent ; string aussi (UTF-8
immuable avec un plafond déclaré) et decimal(scale) (un type valeur de largeur fixe) ; les records
aussi et vec(κ, N) avec un N const-foldé. Un champ non borné — ou un ⊤ — est [ErrState],
l'analogue exact de [ErrPlot] pour le Model.
fluxvariant Verdict { Right | Wrong }
record Model { n: num ; label: string ; history: vec(Verdict, 64) } // bounded — ok
// a growing list with no declared cap is not a Model field: ✗ [ErrState]
Le plafond est une longueur const-foldée — un littéral, comme ci-dessus, ou un identifiant qui se
const-folde en un (vec(Level, MAX_LEVELS)). Dans tous les cas, le compilateur connaît le nombre avant
l'arrivée du premier message.
Pourquoi borner le Model. Parce que l'empreinte mémoire d'une application devient alors calculable à la compilation — le plan hérite du même argument de totalité que le plan d'analyse, et une application en cours d'exécution ne peut ni fuir, ni s'emballer, ni être tuée en pleine frame pour avoir dépassé un budget qu'elle n'a jamais déclaré. Le caractère borné n'est pas une restriction imposée à l'auteur ; c'est ce qui permet au compilateur de promettre le budget en premier lieu.
#La partition doc / ui
Toute application dotée d'un undo en a besoin, et ce dès la première ligne : scinder le Model en un
sous-record doc (l'état métier, versionné par l'historique) et un sous-record ui (sélection,
curseur, l'époque d'une requête en vol, un brouillon de glisser) qui n'entre pas dans l'historique.
Sans la frontière, annuler un changement ressusciterait une sélection périmée ou un brouillon mort. Avec elle, l'undo est exactement « tronquer le journal jusqu'à la borne précédente et re-folder ».
#Mise à jour fonctionnelle de record
m with { … } réécrit les champs listés et reporte le reste. Il préserve la forme : un champ qui
n'existe pas est [ErrField], et un champ que vous oubliez est conservé, non silencieusement perdu.
fluxupdate(m, msg) = match msg {
Score(pts) -> { model: m with { doc: m.doc with { score: m.doc.score + pts } }, cmds: [] }
Select(h) -> { model: m with { ui: m.ui with { sel: h } }, cmds: [] }
}
#Le kind du Model est un point fixe
Un champ initialisé à na dans init ne reste pas à ⊥ : son kind est la borne supérieure, champ
par champ, sur init et chaque bras d'update. picked: na dans init, puis picked: key (un
string) dans un bras, donne Model.picked : string, na demeurant une valeur d'exécution légale
(l'absence). Déclarer record Model { … } d'emblée épingle explicitement les mêmes kinds — les deux
voies coïncident.
#Le slotmap — une collection bornée à suppression stable
Les éditeurs conservent des dessins ; un blotter de trading conserve des positions ; un outil conserve
des ancres. Les trois ont besoin d'une collection à laquelle vous pouvez ajouter, retirer, et
l'adresser de façon stable — et le Model ne peut pas croître, ne peut pas se ré-indexer, et n'a pas
de filter.
Le pattern officiel est un slotmap sur un vecteur borné :
fluxrecord Level { id: num ; price: price ; kind: Tool ; label: string ; gen: num }
record Model {
slots: vec(Level, MAX_LEVELS) // a tombstone (`na`) marks a free slot
live: vec(signal, MAX_LEVELS) // 1 where a slot is occupied
count: num
nextId: num // the monotone domain-id counter (persisted)
}
def emptyDoc() = { slots: emptySlots(MAX_LEVELS), live: vec.fill(MAX_LEVELS, 0), count: 0, nextId: 1 }
// removal — functional, length-preserving, nothing shifts
def remove(m, h) =
m with { slots: vec.setAt(m.slots, h.slot, na),
live: vec.setAt(m.live, h.slot, 0),
count: m.count - 1 }
// iteration — `na`-aware: a tombstone produces no child at all
view(m) = col { for lvl in vec.mask(m.slots, m.live) -> levelRow(lvl) }
Quatre règles le font fonctionner :
- La suppression écrit une pierre tombale, ne compacte jamais.
vec.setAtest total et préserve la longueur ; un hors-bornes laisse le vecteur inchangé. Il n'y a pas d'affectation indexée dans Flux — la pureté n'est pas négociable. - L'itération est sensible aux
na.vec.mask(slots, live)garde la longueur et vide les slots morts ; la compréhension les saute. (vec.where(slots, is_some)est la même idée avec un prédicat au lieu d'un signal parallèle.) - L'identité est double. L'identifiant de domaine — un compteur monotone dans le Model, ou un UUID v7 pour le stockage partagé — est l'identité côté script : c'est ce que portent les messages, l'undo et la sérialisation. Le handle slot/génération est un indice d'exécution interne, reconstruit au chargement. Un indice de slot sérialisé serait mort à l'arrivée ; un identifiant de domaine survit.
- Le débordement est un état de l'application. Remplir
Nn'est pas un crash : le bras d'ajout renvoie un message, et l'application décide quoi dire.
Pourquoi un compteur de génération. Réutiliser un slot après une suppression laisserait un handle périmé lire un nouvel élément — le classique bug ABA. Incrémenter
genà la création fait qu'un handle périmé litnaà la place. Et au chargement, lenextIdpersisté est borné (clamp) vers le haut (max(persisted, max(id)+1)), jamais rebasé vers le bas — sinon, supprimer les identifiants les plus hauts puis recharger re-frapperait un identifiant que l'historique a déjà utilisé.
#update — pur, total, déterministe
update ne peut pas lire l'horloge, ne peut pas lire l'aléa, ne peut pas toucher au DOM, ne peut pas
lire une série en direct en ligne. Tout ce qui est ambiant arrive sous forme de message. Il doit
traiter chaque message — l'exhaustivité du match est [ErrTotalMatch], vérifiée, non espérée.
Il renvoie un record nommé, jamais une paire anonyme : { model: …, cmds: [ … ] }. Il n'y a pas de
sorte tuple dans le treillis, et en ajouter une pour cela aurait été un vrai coût sans bénéfice.
cmds est une liste ordonnée de descripteurs inertes. Une commande est une donnée : le nom d'un
son, une clé, un score. Elle ne porte jamais une socket, un token, une URL, ni un handle DOM — l'hôte
détient la ressource, le script ne détient qu'une requête.
#Les résultats asynchrones reviennent sous forme de messages
Une commande avec résultat porte le constructeur dans lequel son résultat doit être enveloppé. L'hôte applique ce constructeur au résultat et délivre un message :
fluxvariant Msg { Save | Saved(epoch: num, ok: signal) | Cancel }
update(m, msg) = match msg {
Save -> { model: m with { ui: m.ui with { epoch: m.ui.epoch + 1 } },
cmds: [ SaveLevel(m.doc, m.ui.epoch + 1, Saved) ] } // ← carries `Saved`
Saved(e, ok) -> if e == m.ui.epoch // a stale result is ignored
then { model: m with { ui: m.ui with { saving: 0 } }, cmds: [] }
else { model: m, cmds: [] }
}
Pourquoi pas une abstraction de tâche. Un type
Task/effet composable enchaîneraitA ⤳ B(A) ⤳ Cet cacherait les résultats intermédiaires — ils n'atteindraient jamais le journal. Le re-folding, le voyage dans le temps et le rejeu côté serveur perdraient tous la capacité de reconstruire le modèle bit à bit. Ainsi, chaque résultat asynchrone reste un message. La verbosité d'une longue chaîne est le prix reconnu du rejeu exact, et un désucrage optionnel d'une séquence fixedo … then …vers la même machine à messages garde la trace identique dans les deux cas.
Le jeton d'époque ci-dessus est la réponse générale à un résultat périmé : la commande porte un
scalaire fourni par l'app ; l'hôte le renvoie tel quel ; le bras le compare à l'époque courante et
abandonne ce qui ne compte plus. Il vit dans ui, jamais dans doc — il ne doit pas voyager dans un
journal partagé.
#view — un arbre pur de primitives contrôlées
view renvoie un UiTree : des conteneurs (col, row, grid, stack, tabs, scroll, panel,
et les rails de l'application), du contenu (text, label, badge, chip, icon, progress,
sparkline, image), des contrôles (button, toggle, slider, select, radioGroup, textInput,
metaForm), et des fenêtres sur les autres plans — chartView, paneView, sceneView.
Les trois fenêtres sont la seule façon pour un UiTree d'atteindre un autre plan, et chacune prend un
type de cible différent : chartView monte un moteur de graphique et accepte une scène CANVAS comme son
overlay: ; paneView projette une série ; sceneView(target, tree, space) peint une scène libre dans
une cible de rendu nommée, en coordonnées Data, Screen ou World3D. (sparkline apparaît dans la
ligne de contenu ci-dessus : il rend une série en ligne, et n'est pas une fenêtre sur une cible.) Voir
display.
Il n'y a aucun balisage brut, aucune chaîne HTML, aucun gestionnaire d'événement. Un clic est un message :
fluxview(m) = panel(slot: right.panel) {
row { text("levels: {m.doc.count}") ; button("add", AddLevel) }
chartView(chartId: "main",
onClick: ClickAt, // a constructor reference, not a closure
overlay: overlayOf(m.doc, m.ui.draft, m.ui.sel))
when m.ui.saving: progress("saving…")
}
Deux détails de cet extrait sont porteurs.
Les slots de callback prennent une référence de constructeur, jamais une fonction. onClick: ClickAt
nomme le constructeur que l'hôte appliquera au véritable (bar, price) au moment du clic. Le
compilateur vérifie les kinds de charge utile du constructeur contre les kinds d'arguments déclarés du
slot. Aucune valeur de fonction n'entre jamais dans le treillis — il n'y a pas de sorte flèche, et cela
le maintient ainsi.
Le graphique n'est pas re-rendu par le diff de vue. chartView monte le vrai moteur de graphique ;
le réconciliateur ne touche jamais qu'à l'habillage autour. La scène passée comme overlay: est une
valeur CANVAS (scene{…}) — le canal sanctionné du plan de présentation vers un pane.
L'hôte fait le diff du petit arbre, l'assainit (le texte entre comme texte ; un nœud inconnu est rejeté), et peint. Une vue ne peut donc jamais injecter de balisage, et une application hostile ne peut pas dessiner une fausse boîte de dialogue système : l'ensemble des primitives est clos.
#subs — la porte d'entrée déclarative
Les abonnements sont recalculés à partir de chaque modèle, et le catalogue est clos. Tout ce qui est ambiant — le temps, l'entrée, l'aléa, les données, le réseau, le wallet, les autres utilisateurs — entre ici, sous forme de message.
| Abonnement | Délivre | Soutenu par |
|---|---|---|
OnTick(everyMs, C) |
un tick périodique | clock |
OnFrame(C) |
un message par frame d'animation | clock |
OnSeries(key, C) |
des valeurs d'analyse, en lecture seule | chart:read |
OnChartClick(C) / OnHover(C) |
(bar, price) — le survol (hover) est throttlé aux frontières de barre |
chart:read |
OnDrawingChange(C) |
des dessins ont changé | chart:read |
OnRand(seed, C) |
de l'aléa avec graine | rand:seeded |
OnFeed(C) |
une charge utile réseau typée par schéma | net:fetch / net:stream |
OnRoute(path, C) |
un deep link, analysé par une grammaire hôte fixe | ui:navigate |
OnConnectivity(C) |
des fronts de connectivité | net:offline |
OnTransfer(reqKey, C) |
la progression d'un transfert pour une requête | l'octroi net:* propre à la requête |
OnReveal(C), OnRevealProgress(C) |
un résultat calculé par l'hôte, et la progression d'un reveal — le résultat est le vecteur anti-triche ouvert | chart:read |
OnKey(C), OnPointer(C), OnWheel(C), OnGamepad(i, C) |
des fronts d'entrée — jamais un échantillon maintenu | input:* |
OnFocus(C) |
focus gagné ou perdu — journalisé, si bien qu'un événement clavier n'est délivré que lorsque le journal atteste le focus | — (médié par la possession du slot) |
OnVisible(itemKey, threshold, C) |
une transition de visibilité par clé — le front derrière le chargement paresseux et les accusés de lecture | — |
OnPeerMsg(C) |
une entrée de journal distante, dans une session collaborative | net:rtc |
OnLocale(C) |
la locale active a changé | i18n:catalogue |
OnSession(C) |
le cycle de vie de la session — login, refresh, logout | auth:session / auth:passkey |
OnEntitlement(C) |
un droit ou un abonnement vérifié par le serveur | pay:checkout |
OnGeo(minInterval, C) |
une position surveillée, à un intervalle minimum déclaré | geo:read |
OnWallet(C), OnTx(C) |
des événements de cycle de vie de wallet et de transaction | wallet:* / chain:* |
OnPresence(C), OnContactUpdate(C), OnInvite(C) |
des événements sociaux | social:* / present:* |
OnSharedChange(scope, C) |
un changement dans une collection partagée hébergée | storage:shared |
OnWebhook(path, C) |
un appel HTTP entrant, décodé contre le schéma déclaré | le plan serveur |
OnJob(spec, C) |
une exécution planifiée — le jumeau serveur de schedule:wake |
le plan serveur |
OnQueue(name, C) |
un élément de travail | le plan serveur |
NoSub |
« rien cette frame » — le constructeur nullaire qui fait typer l'abonnement conditionnel | — |
Les quatre derniers sont les abonnements du plan serveur : ils sont délivrés à une application
headless, et ils sont ce à quoi ressemble un subs quand il n'y a aucun viewport du tout. Ils
appartiennent à ce catalogue plutôt qu'à un second, parce qu'une application headless n'est pas un genre
de programme différent — c'est le même init/update/view/subs avec un ensemble d'entrées
différent qui l'atteint. Voir server et
host services.
Chaque abonnement avec charge utile porte le constructeur que l'hôte lui appliquera, exactement
comme le font les commandes — OnTick(100, Tick), OnSeries("rsi", Got), OnChartClick(ClickAt). Sans
cela, un événement ne saurait pas à quel bras d'update il appartient.
Les abonnements peuvent être modelés en débit sans quitter le modèle :
OnSeries(k, Got).throttle(100) délivre au plus dix messages par seconde, et le front délivré reste
journalisé, si bien que le rejeu demeure exact.
fluxsubs(m) = [ if m.ui.live then OnSeries("close", Got).throttle(100) else NoSub,
OnChartClick(ClickAt) ]
#Capacités — le modèle de sécurité
Le script ne détient jamais un objet capacité. Il émet une requête — emit Cap(args) — et l'hôte,
seul détenteur, l'interprète via un gestionnaire que seul l'hôte possède. Une requête pour une capacité
que le manifeste n'accorde pas est rejetée à la compilation ([ErrCapDenied]), non à l'exécution.
Il n'y a aucune autorité ambiante : aucun objet global n'est dans la portée, aucun.
Une tranche représentative du catalogue (chaque entrée est en refus par défaut, atténuée par l'hôte, et graduée par le niveau de confiance) :
| Capacité | Accorde | Atténuation par l'hôte |
|---|---|---|
storage:own |
Persist / LoadPersist |
un espace de noms partitionné par application, avec un quota |
journal |
Journal(UndoToMark | RedoToMark | JumpToMark) |
troncature + re-fold + réinstallation médiées par l'hôte |
chart:read |
OnSeries, OnChartClick, OnHover, et les requêtes de pixels bornées |
lecture seule, séries publiques, barres causales |
chart:ctl |
SetChart, RevealForward |
délégué au moteur de graphique, à débit limité |
net:fetch(domain) |
Fetch, Sub OnFeed |
consentement utilisateur par domaine ; l'hôte détient la socket ; la charge utile est décodée contre le schéma déclaré par l'app et vérifiée en kind à la frontière |
net:stream(domain) |
une connexion bidirectionnelle persistante | consentement par destination ; l'hôte détient le descripteur de fichier ; des frames typées |
clock |
OnTick, OnFrame, After(ms, msg) |
des timers bornés ; le message différé ré-entre dans le journal |
rand:seeded |
OnRand(seed) |
une graine dérivée du serveur à travers un générateur d'entiers épinglé |
wallet:* / chain:* |
une intention de signature | le wallet signe, l'utilisateur confirme dans le wallet ; une simulation décodée obligatoire précède toute signature ; portée par argument |
ui:contribute:<kind> |
des contributions d'interface | contrôlé au montage |
storage:shared |
des collections hébergées avec des ACL par paliers | les paliers compilent en sécurité au niveau des lignes — jamais un langage de règles libre |
Certaines choses sont inexprimables pour tout palier, de confiance ou non : eval et la génération
de code, le DOM brut, une socket brute, un client de base de données brut, un token ou un cookie, et tout
store global. Elles ne sont pas « interdites par politique » — elles n'ont aucun nom dans le langage.
#Deux paliers de confiance — la sécurité est l'octroi, non le code
L'interface first-party et l'application d'un inconnu exécutent le même langage dans le même sandbox. Elles ne diffèrent que par ce qui leur a été accordé.
- Palier A — l'interface first-party (y compris l'éditeur de niveaux). Sa source Flux reste privée et n'est jamais livrée ; elle est livrée en WASM. Elle est de confiance par périmètre, et ses octrois privilégiés se tiennent derrière une triple barrière : un octroi authentifié et gardé par un admin ; un médiateur hôte qui seul détient le token ; et la sécurité au niveau des lignes dans la base de données.
- Palier B — les applications d'utilisateurs et de la marketplace. Non fiables du point de vue de
qui les charge. Refus par défaut strict, vue assainie, realm isolé. L'acheteur reçoit le WASM seul,
jamais la source — si bien que le consentement ne peut reposer sur la lecture du code. Il repose sur
le manifeste scellé, qui voyage avec le binaire, est dérivé par le compilateur à partir des sites
emit Cap(leur seule origine), et est inspectable avant l'installation.
La confiance est décidée par l'hôte à partir d'un enregistrement de provenance côté serveur indexé par le hash de contenu du binaire. Un module ne peut pas se déclarer de confiance, et aucune métadonnée embarquée n'est crue.
Aucun palier ne relâche jamais un invariant du langage. Un palier de confiance accorde des effets ; il ne relâche ni la causalité, ni le no-repaint, ni la totalité, ni le pare-feu. Le repaint est inexprimable pour tout le monde.
#Manifestes transitifs, zéro escalade
Une application bâtie sur des packages agrège leurs requêtes :
manifest(A) = ( ⋃ emit Cap over the transitive closure of A ) ⊓ the user's grant
Trois conséquences, toutes normatives. Le net:fetch d'une dépendance remonte dans le manifeste de
l'acheteur avant l'installation — aucune capacité cachée. Aucune dépendance ne peut dépasser ce que
l'utilisateur a accordé à l'application — l'autorité ne circule que le long des arêtes d'import,
plafonnée par l'octroi. Et aucune dépendance ne détient un objet capacité, si bien qu'elle ne peut ni
la re-déléguer ni l'amplifier.
#La révocation est un événement, non une vérification
Un octroi peut être retiré en cours de session — par un geste de l'utilisateur, ou par un superviseur.
L'hôte écrit une borne CapRevoked dans le journal à cet instant, exactement comme il écrit une
borne de pause, afin qu'un re-fold reproduise la révocation de façon déterministe. (Sans la borne, un
re-fold reconstruirait la membrane au manifeste non révoqué et une commande post-révocation réussirait
— une divergence silencieuse, que le plan ne permet pas.)
Les commandes échouent alors en position fermée : celle qui porte un constructeur d'achèvement reçoit
[ErrCapRevoked] à travers ce même constructeur ; une commande fire-and-forget est abandonnée et
auditée. Un effet déjà en vol est annulé, et le jeton d'époque absorbe tout résultat déjà périmé.
#Totalité, déterminisme, rejeu
| Propriété | Mécanisme |
|---|---|
| Totalité | update/view/subs sont totaux ; match est exhaustif ; pas de boucles libres ; le Model est borné |
| Déterminisme | update est pur ; tout ce qui est ambiant arrive comme message ; l'aléa est OnRand(seed) à travers un générateur d'entiers épinglé partagé bit à bit par l'interpréteur, le WASM et le serveur |
| Rejeu exact | re-folder la trace de messages reconstruit le Model bit à bit — ce qui prouve qu'un journal est cohérent, non infalsifiable (voir ci-dessous) |
| Coût borné | le coût par message est borné ; le diff de vue touche un petit arbre ; les effets lourds sont des commandes, hors d'update |
Le journal n'est pas une liste plate de messages. Deux raffinements sont imposés par les vrais éditeurs :
- Bornes d'undo. Un message peut poser ou non une borne. L'undo tronque jusqu'à la borne précédente, jamais jusqu'au message précédent — sinon un glisser se désagrégerait en une centaine de micro-pas. Fusionner un geste, c'est placer la borne au relâchement du pointeur.
- Checkpoints. Re-folder depuis
inità chaque undo est linéaire en l'historique, si bien que les modèles sont mémoïsés à intervalles et le re-fold démarre du plus proche.
#Ce que le rejeu prouve — et ce qu'il ne prouve pas
Parce qu'un verdict est une fonction pure de (init, [msg]), un serveur peut le recalculer sur les mêmes
octets, et un score revendiqué mais non mérité est un journal qui ne se fold pas vers le résultat
revendiqué. Cette propriété n'a pas été conçue ; elle est héritée du déterminisme. C'est aussi le point
où cette page doit au lecteur une limite précise plutôt qu'un slogan.
Le rejeu prouve la COHÉRENCE d'un journal, non sa NON-FALSIFIABILITÉ.
« Diverge ⇒ falsifié » est complet pour un score dérivé de la graine et du temps écoulé, parce
que le serveur détient les deux : la graine est dérivée côté serveur à partir de (runId, level, qIndex)
et jamais acceptée du client, et le temps écoulé d'un run classé est estampillé par l'hôte et substitué
au re-fold, si bien qu'un compte de Tick falsifié n'achète rien. Il n'est pas complet pour une
troisième classe, et la brèche est ouverte et nommée.
Un score alimenté par un résultat poussé par l'hôte se re-fold sans divergence. OnReveal délivre un
résultat que l'hôte a calculé — un résultat de noyau, révélé sous forme de message. Ce résultat entre
dans le journal en tant que donnée, et un re-fold rejoue les données telles quelles : la graine
re-dérive les messages venus d'OnRand, et celui-ci n'en venait pas. Ainsi, un résultat falsifié se
re-fold vers le résultat revendiqué sans le moindre murmure de divergence. La vérification passe ; la
revendication reste un mensonge. La ligne OnReveal du catalogue d'abonnements ci-dessus est exactement
ce vecteur, et il vaut la peine de savoir laquelle c'est.
La même classe couvre un pixel. Une lecture de pixel bornée est dérivée du viewport du client — son pan et son zoom — et un serveur sans viewport ne peut pas la re-dériver. D'où la règle permanente, qui est une discipline au niveau du langage et non du serveur : une valeur de pixel n'alimente jamais un verdict classé. C'est une lecture, ou c'est cosmétique.
Un run alimenté par un résultat a donc exactement deux destinations honnêtes, et pas de troisième : l'hôte re-dérive le résultat côté serveur (en ré-exécutant le noyau lui-même, ce que le plan serveur est justement ce qui rend possible), ou le run est à score local seulement, exclu du classement partagé. Il n'est jamais accepté sur la seule force du journal du client. server porte l'argument de clôture.
#Les deux exceptions nommées
update est le seul producteur d'un Model en fonctionnement normal. Il existe exactement deux
exceptions médiées par l'hôte, et toutes deux sont déterministes :
- Voyage dans le temps.
Journal(UndoToMark)fait tronquer le journal par l'hôte, re-folder(init, [msg]')et réinstaller le résultat. Le journal reste la source de vérité — il est rembobiné et re-dérivé, jamais fabriqué. - Migration et rechargement à chaud. Lors d'un changement de hash de build, l'hôte soit re-fold le
journal retenu avec le nouvel
update(état re-dérivé des mêmes entrées), soit — quand le journal est long — décode le snapshot persisté et le fait passer par unmigrate(old) -> Modeltotal que l'application déclare.
Un (re)lancement initié par l'hôte — un tap de notification, un réveil planifié, un deep link — n'est pas une troisième exception : sa charge utile est délivrée comme les premiers messages journalisés de la nouvelle session, ordonnés avant toute autre livraison d'abonnement.
#Évolution de schéma
Tant que le variant Msg ne fait que croître (nouveaux constructeurs, jamais retypés ni retirés),
un ancien journal reste re-foldable sous un nouvel update — reprendre l'état est gratuit. Un changement
cassant franchit la jonction monomorphe à travers un upcast total explicite (migrateMsg), ou fait
surface comme SchemaMismatch. Jamais un décodage silencieux d'anciens octets vers une nouvelle
forme. Le snapshot et le journal migrent comme une seule unité : un snapshot v2 sous un journal v1 serait
un split brain. Les checkpoints d'un build précédent sont invalidés, non réinterprétés.
#Tester une application
Parce qu'update et view sont purs, totaux et déterministes, et que le fold entier est à l'intérieur
de l'oracle de byte-identité, un test d'application est un golden sur des fonctions pures — à quatre
grains, sans aucun mock nulle part.
Le grain de trace porte le poids : le harnais de rejeu est le harnais de test. Un test de trace
fold une liste de messages littérale — fold(init(p), [ Tick, Tick, Reset ]) — et prend en golden
le Model obtenu. Rien ne pilote un abonnement en direct, et rien n'en a besoin : le journal réifie déjà
chaque événement en message, si bien que la liste de messages est le mock — total, typé, et la chose
même qu'un serveur ré-exécute. Un golden de trace d'un score classé épingle le temps écoulé et la graine,
faisant autorité côté hôte, comme entrées.
Les trois autres grains sont des assertions ordinaires sur les mêmes fonctions pures (m0, m1 et
expectedTree représentent les propres valeurs de modèle et la vue de l'application, comme dans la note
du haut) :
flux// step — a deep, na-aware equality on two lattice values
assert update(m0, Tick) == { model: m0 with { n: 1 }, cmds: [] }
// view — a snapshot of the canonical view buffer
assert view(m1) == expectedTree // chartView nodes are goldened as nodes, never as pixels
// property — an invariant asserted at every intermediate model, over a seeded message list
assert m.count == popcount(m.live)
Le champ cmds est une donnée inerte, si bien qu'affirmer quels effets ont été émis ne nécessite ni
fausse horloge ni fausse socket : le descripteur est l'assertion. Et un abonnement non déterministe
n'est jamais mocké — il est pris en golden sur sa description (subs(m)), tandis que sa livraison est
fournie comme une liste littérale de messages. Là où un invariant est déjà prouvé par le kind, aucun
test n'est dû du tout.
#Étendre l'interface
Une application déclare ce qu'elle ajoute — panes, panels, controls, menus, tools,
statusItems, commands — et ne le fabrique jamais. Chaque contribution nomme un slot (une région),
un prédicat when:, et un render produisant un UiTree. Contribuer requiert ui:contribute:<kind>.
Le gestionnaire de disposition possède le où ; l'application possède le quoi. Il arbitre
l'ordre, l'ancrage, les onglets et les divisions, il persiste l'agencement, il réconcilie et assainit
l'arbre, et il confine une contribution défaillante à son propre pane. Chaque slot expose un port —
rect(), onResize, onVisible, requestFocus(), dispose() — dont les notifications arrivent sous
forme de messages, et dont le cycle de vie possède l'instance WASM du pane : l'ouverture instancie le
module, la fermeture libère sa mémoire linéaire, abandonne ses abonnements et révoque ses contributions.
Une application ne possède jamais non plus son état d'affichage. Elle émet une requête
(RequestForeground, RequestDetach, …) et le gestionnaire de disposition décide ; le verdict revient
plus tard sous forme de message. L'ordre d'autorité est total et infalsifiable : utilisateur >
superviseur > application. Forcer sa propre fenêtre au premier plan n'est pas à débit limité — c'est
inexprimable.
#Les trois patterns officiels
| Pattern | Forme | Où réside l'essentiel |
|---|---|---|
| Slotmap | vec(Item, N) + pierres tombales + live + count + identifiants de domaine |
dans le Model, borné |
| Document | un sous-record doc avec des plafonds déclarés ; un pool de nœuds Tree(Node, N) ; un historique avec bornes et checkpoints. Au-delà d'une fenêtre éditable, le document persiste par morceaux et le Model détient la fenêtre active |
le stockage hôte ; le Model détient la fenêtre d'édition |
| Feed | une window: vec(Item, W) bornée + curseur + total ; approcher du bord émet LoadPage(cursor, C) ; le rendu est une virtualList pilotée par OnVisible |
le cache hôte ou le stockage partagé |
Un feed n'est donc jamais une collection non bornée dans le Model : c'est une fenêtre déterministe sur un stock détenu par l'hôte. Un grand document est la même idée appliquée à du contenu éditable.
#Composition et le pare-feu
APP (mutable state + effects) ← the most permissive plane
│ reads ANALYSIS (Sub OnSeries) ✔ read-only
│ orchestrates CANVAS / TRANSITION (Cmd) ✔
▼
CANVAS / TRANSITION (cosmetic, per frame) ← reads ANALYSIS ✔
▼
ANALYSIS (pure, causal, no-repaint) ← reads nothing above it ✘
Quatre règles dures, vérifiées statiquement :
- L'analyse ne voit jamais le plan APP. Un signal reste prouvablement exempt de repaint même à côté d'un jeu — un jeu qui change d'actif ne repeint pas un indicateur ; il le recalcule, à travers une capacité.
- Le plan APP lit l'analyse en lecture seule, à travers
Sub OnSeries. - CANVAS atteint le plan APP par des événements ; le plan APP atteint CANVAS par des commandes — il orchestre la présentation, il ne la réécrit pas.
- Le plan APP n'écrit jamais l'analyse.
Lire une série en direct depuis update obéit à la même règle de plancher contenant que tout
rééchantillonnage : la barre clôturée la plus récente, jamais une correspondance au plus proche.
Noter un quiz ne peut pas repeindre l'indicateur à côté de lui.
#Voir aussi
- Les quatre plans — le pare-feu, plan par plan.
- Kinds —
variant,record,ui, et les kinds bornés qu'un Model peut détenir. - display — les primitives de vue, la valeur de scène, les panes et les fenêtres.
- net —
net:fetch/net:stream, les charges utiles typées, les files hors ligne. - server — les applications headless, le stockage partagé, et le rejeu côté serveur.
- Garanties — ce que le rejeu, le déterminisme et le sandbox promettent réellement.
Le FDK — le Flux Development Kit
Le FDK, c'est tout ce contre quoi vous programmez : le prélude standard, les routines épinglées, les API des piliers et le catalogue de capacités. C'est la différence entre « un langage » et « une plateforme sur laquelle bâtir un produit ».
Deux propriétés le traversent de part en part, et ce sont elles qui expliquent la forme qu'a le FDK :
- Complet pour le domaine, pas généraliste. Il n'y a pas de
filterqui rétrécit une liste, pas de moteur d'expressions régulières, pas de file non bornée, pas d'E/S ambiante. Chacune de ces choses a été écartée parce qu'elle briserait la totalité, le déterminisme ou le bac à sable — et pour chacune il existe un remplaçant borné qui fait le travail dont le domaine a réellement besoin. - Tout est épinglé. Là où une implémentation pourrait différer entre deux moteurs — une transcendante, une division décimale, un pliage de casse Unicode, une addition calendaire, un tirage aléatoire, un tri avec des valeurs absentes — le FDK porte une seule routine, partagée par l'interpréteur, le module compilé et le serveur. Pas « le même algorithme ». Le même code.
#Le prélude
En v1, le prélude est plat — chaque fonction est dans la portée, et le chaînage à la méthode fait le reste :
fluxplot close.ema(20).rsi(14) // ≡ plot rsi(ema(close, 20), 14)
Après avoir tapé close., l'éditeur ne propose que les fonctions dont le premier paramètre accepte
un price. Le système de types est le mécanisme de découverte, et c'est ce qui rend un espace de
noms plat de quelques centaines de fonctions navigable plutôt qu'écrasant.
Les modules et les noms qualifiés (mod.f) existent, et ils s'étendent aux packages — voir
packages.
#Les espaces de noms
| Espace de noms | Ce qu'il couvre |
|---|---|
math.* |
Arithmétique dimensionnelle : abs sign min max clamp floor ceil round préservent la dimension ; sqrt divise les exposants par deux ; pow(x, n:lit) les met à l'échelle ; log exp sin cos tan atan atan2 exigent une entrée sans dimension. Toute transcendante passe par la bibliothèque épinglée — jamais celle de la plateforme. |
stat.* |
mean stdev variance skew kurtosis median percentile rank correl covar zscore linreg + la famille à pondération exponentielle. Réducteurs à fenêtre bornée ; statistiques d'ordre via l'ordonnancement na épinglé. |
vec.* |
map fold scan zip sum avg product min max reverse take drop window · fill · range · setAt · where / mask (préservent la longueur) · sortBy / topK · count any all. Pas de filter, pas de flatMap — une longueur dépendante des données briserait la totalité. |
decimal.* |
Virgule fixe exacte : div round (la division nomme son échelle cible, à égalité vers le pair ; round quantifie vers une échelle), avec toDecimal / toFloat nus comme pont vers f64. long ≡ decimal(18,0), long128 ≡ decimal(38,0). |
time.* |
Le calendrier : years months weeks days (les seuls producteurs d'un period), les accesseurs, les conversions epoch, in_session, barsPerYear. now() est un symbole de présentation — le lire depuis l'analyse est [ErrFirewall]. |
str.* / fmt.* |
Texte borné : len slice startsWith endsWith contains indexOf split trim pad rep upper lower ; interpolation de chaînes ; fmt.num/price/pct/time via un unique formateur canonique. Pas d'expressions régulières — voir text pour ce qui les remplace. |
ta.* |
Le catalogue d'indicateurs — plus de quatre-vingts kernels, chacun avec une signature de kind. |
anim.* / sig.* |
Les générateurs et combinateurs de signaux du CANVAS (tween, spring, wave, noise, stagger, hold…). Présentation uniquement, par le pare-feu. |
enc.* / crypto.* |
Codecs base64 base32 hex ; hachages épinglés (sha256, blake3, …), un MAC à clé, et vérification de signature — un bac à sable vérifie, il ne signe jamais. |
id.* |
Identifiants déterministes : uuidV4(seed), uuidV7(t, seed), nanoid, slug. Amorcés, jamais d'entropie ambiante. |
bits.* |
Le travail bit à bit sous forme de fonctions nommées (band bor bxor bnot shl shr sar · popcount clz ctz rotl rotr) sur le mot machine, plus un tampon d'octets borné — le substrat des codecs binaires. Nommées, jamais infixes : and/or/not restent la logique signal scellée, et aucun nouveau jeton n'entre dans la grammaire. |
geom.* |
Géométrie 2-D pour les outils de dessin et la mise en page personnalisée — dans l'espace écran par conception. |
coll.* |
Combinateurs d'ordonnancement et de collation ; voir collections et i18n. |
viz.* |
Données → marques : échelles, axes, légendes, facettes, transformations statistiques. Voir display. |
#Les piliers
Chaque pilier est une API complète, avec sa propre page :
| Pilier | En une ligne |
|---|---|
| compute | L'algèbre de dataframe en colonnes, la couche numérique et les bibliothèques de domaine. |
| collections | Vec / Deque / Map / Set / Tree — bornés, ordonnés, à sémantique de valeur. |
| color | Le kind color, ses constructeurs, l'interpolation perceptuelle et les canaux de sortie. |
| text | Texte structuré, le protocole d'édition, la segmentation, le diff, la recherche, les validateurs. |
| i18n | Les locales comme valeurs, les catalogues de messages, la sélection du pluriel et du genre, la collation, le RTL. |
| units | meas[u] — quantités générales, échelles affines, conversions exactes. |
| net | Le réseau comme un flux : cinq verbes, charges utiles typées, contre-pression déclarée. |
| display | Les scènes comme valeurs, les deux strates, les panneaux et les fenêtres, viz.*. |
| host services | Fichiers, presse-papiers, notifications, authentification, paiements, médias, impression, polices, intégration. |
| server | Applications headless, stockage partagé à accès par paliers, prérendu. |
| asset & currency | Le tag d'instrument (B, Q [, @v]), fx, money, les venues. |
#Le modèle de capacités
Rien dans le FDK n'atteint le monde extérieur de lui-même. Un effet est une donnée inerte que le script émet ; l'hôte — seul détenteur de la ressource — l'exécute, et seulement si la capacité a été déclarée dans le manifeste et accordée par l'utilisateur.
fluxapp reader {
capabilities: [ net:fetch, storage:own, notify:send ]
init(p) = { unread: 0 }
update(m, msg) = match msg {
Got(item) -> { model: m with { unread: m.unread + 1 },
cmds: [ Persist("inbox", item), Notify("new-item", item, Open) ] }
Open(hit) -> { model: m with { unread: 0 }, cmds: [] }
}
view(m) = col { text("unread: {m.unread}") }
subs(m) = [ OnFeed(Got) ]
}
Chaque effet dans ce corps est une requête, et chacune répond d'une ligne du manifeste : OnFeed
pour net:fetch, Persist pour storage:own, Notify pour notify:send. Supprimez une ligne de
capabilities: et l'entrée cmds correspondante cesse de compiler.
Trois propriétés font de cela plus qu'une liste de permissions :
- Une requête pour une capacité non accordée est une erreur de compilation (
[ErrCapDenied]), pas une exception d'exécution à rattraper et réessayer. - Une capacité n'est jamais une valeur. Le script ne peut ni en détenir une, ni en stocker une, ni en passer une, ni en re-déléguer une. Il détient une requête.
- Un manifeste s'agrège transitivement, sans aucune escalade. Si un package dont vous dépendez veut le réseau, cela remonte dans votre manifeste, visible pour quiconque installe votre application, avant qu'il ne l'installe — et cela reste plafonné par ce que l'utilisateur a accordé.
Voir App plane pour le catalogue et les paliers de confiance.
#La doc-comme-donnée
Chaque fonction, kind, mot-clé et opérateur porte un enregistrement de documentation structuré — sa signature, ses kinds, ses paramètres, son résumé, ses exemples. Une source, plusieurs rendus :
- la carte de survol de l'éditeur et sa liste de complétion,
- ces pages,
- les messages d'erreur,
- les snippets.
Ils ne peuvent pas diverger, parce que ce sont les mêmes données. Et un lint de complétude impose la règle qui verrouille le tout : chaque construction du langage possède un enregistrement de documentation et au moins un exemple exécutable — et chaque exemple est un golden. Une fonction documentée dont l'exemple cesse de fonctionner fait passer un test au rouge.
#État d'implémentation
Le cœur du langage et sa surface du plan d'analyse sont implémentés ; les piliers sont des designs scellés, en cours de construction dans un ordre figé, les collections d'abord. Certaines pages portent Post-v1. là où le déploiement d'une fonctionnalité suit v1, Reserved. là où une jonction est délibérément maintenue ouverte et inerte, et Open decision. là où le design lui-même laisse un choix ouvert. Le tableau de synthèse est dans le README.
#Voir aussi
- Prise en main — le FDK tel qu'on le rencontre pour la première fois.
- Kinds — le système de types sur lequel repose chaque espace de noms.
- App plane — capacités, commandes, souscriptions.
- Cookbook — le FDK en recettes concrètes.
- Packages — modules, imports et bibliothèques tierces.
- L'éditeur — là où la doc-comme-donnée se vit réellement.
compute — nombres, dataframes et bibliothèques de domaine
Post-v1. La couche dataframe, la couche numérique et les bibliothèques de domaine sont scellées
en conception ; les espaces de noms scalaires (math, stat, vec, decimal, time, ta) sont
la surface v1 que le plan d'analyse utilise déjà.
Le pilier compute est ce qui fait de Flux un langage d'analyse général plutôt qu'un langage de script pour graphiques. C'est une algèbre de dataframe en colonnes, bornée, typée par kind — pure, totale, fusionnée, dimensionnelle — surmontée d'une couche numérique (matrices, algèbre linéaire, statistiques) et de bibliothèques de domaine. Une série de marché n'est pas ici le fondement ; c'est un cas particulier d'une table.
Tout y est bâti à partir du cœur figé : une table est un enregistrement de colonnes, une colonne est un vecteur borné, une requête est un graphe pur. Aucune nouvelle sorte, aucune nouvelle grammaire, aucun nouveau substrat.
#Six propriétés, chacune un théorème plutôt qu'un vœu
- Borné et total. Chaque collection est un
vec(κ, N)dont la capacité est repliée à la compilation. La capacité de sortie de chaque opérateur relationnel est dérivable statiquement de ses entrées, et aucune opération ne rétrécit jamais. La mémoire dont une requête a besoin est calculable à la compilation ; une requête hors budget est rejetée, jamais tuée en cours d'exécution. - En colonnes. Une table est un enregistrement dont les champs sont des colonnes — struct-of-arrays, la même disposition que le moteur utilise déjà pour les barres.
- Typé par kind et dimensionnel. Chaque colonne porte sa dimension, sa représentation numérique
et son tag d'actif. Toute une classe de bugs — ajouter un prix à un volume, un prix BTC à un prix
ETH, un
f64à undecimal, joindre sur des clés d'instruments différents — devient une erreur de compilation. - Pure. Une table ne touche ni au réseau ni à l'état applicatif, elle vit donc sur n'importe quel plan sans poser de question de pare-feu.
- Paresseuse et fusionnée. Une requête est un graphe inerte, fusionné en une seule passe en colonnes. Le pushdown de projection est de l'élimination de code mort ; le pushdown de prédicat consiste à fusionner le masque dans le scan producteur ; rien d'intermédiaire n'est jamais matérialisé.
- Byte-identique.
f64scalaire, pas de SIMD, pas de réassociation en virgule flottante, chaque ordre de réduction épinglé, chaque statistique d'ordre via la routine d'ordonnancementnaépinglée, chaque décimale via la routine entière épinglée.
#Les espaces de noms scalaires
C'est la surface du quotidien — disponible sur chaque plan, et dimensionnelle de bout en bout.
| Espace de noms | Contenu |
|---|---|
math.* |
abs sign min max clamp floor ceil round (préservent la dimension) · sqrt (divise les exposants par deux, si bien que stdev = sqrt(variance) est bien typé) · pow(x, n:lit) · log exp sin cos tan atan atan2 (exigent l'absence de dimension — log(price) est [ErrDim], avec une correction rapide) · lerp norm |
stat.* |
mean stdev variance skew kurtosis median percentile rank correl covar zscore linreg · la famille à pondération exponentielle ewmVar ewmStd ewmCov ewmCorr — tous des réducteurs à fenêtre bornée |
vec.* |
map fold scan zip sum avg product min max reverse take drop window · fill(N, x) · range(N) · setAt(v, i, x) · where / mask (préservent la longueur) · sortBy / topK · count any all |
decimal.* |
div round — la division nomme son échelle cible (à égalité vers le pair), round quantifie vers une échelle ; avec toDecimal / toFloat nus comme pont vers f64 — monnaie en virgule fixe exacte |
time.* |
le calendrier : years months weeks days (les seuls producteurs d'un period), les accesseurs, les conversions epoch*, in_session, barsPerYear |
ta.* |
le catalogue d'indicateurs |
enc.* crypto.* id.* bits.* |
encodage, hachages épinglés et vérification de signature, ids déterministes, travail sur bits et tampons d'octets |
Trois règles les traversent toutes et méritent d'être intériorisées :
Les maths sont correctes en unités. sqrt divise les exposants par deux, pow les met à
l'échelle, les transcendantes exigent une entrée sans dimension. Ce n'est pas de la pédanterie —
c'est ce qui fait que sqrt(variance) → level passe le typage alors que log(price) ne le passe
pas.
Les statistiques d'ordre sont épinglées. median, percentile et rank trient en interne, et
utilisent l'unique ordre total épinglé sur na (valeurs absentes en dernier, stable par index)
— jamais le tri de la plateforme. percentile nomme une seule méthode d'interpolation ; median
sur un effectif pair est le point milieu ; rank nomme une seule politique d'égalités. Une fenêtre
contenant un trou produit na, et il existe un golden pour exactement ce cas.
Les moments supérieurs et les statistiques EW sont stables par construction. L'asymétrie et le
kurtosis étendent la récurrence de Welford épinglée ; la famille à pondération exponentielle est un
unique scan borné avec une récurrence en λ épinglée — jamais la forme instable numériquement en
une passe.
#Les tables
Une table est un enregistrement de colonnes, plus un masque de présence et un décompte des lignes vivantes :
Table<{ c₁:κ₁, …, cₘ:κₘ }>[N] ≡ record{
c₁: vec(κ₁, N), …, cₘ: vec(κₘ, N), // the columns — struct-of-arrays, one shared row cap
live: vec(signal, N), // 1 = a real row, 0 = a tombstone
count: num // live rows ≤ N
}
Parce qu'elle est un enregistrement de vecteurs, elle hérite des lois du treillis, de l'égalité
profonde consciente de na, et de la projection (t.close) gratuitement — et elle se compile
directement sur le moteur en colonnes existant. Col(κ, N) est un alias nommé d'un vecteur borné ;
Mat(κ, R, C) ajoute un second axe const avec une garantie de rectangularité.
Une Series — les données indexées par le temps du plan d'analyse — est le cas particulier, et
le pont est une identité plutôt qu'une conversion.
#La totalité, verbe par verbe
Le problème le plus difficile que ce pilier résout s'énonce simplement : les dataframes classiques tirent leur puissance d'opérations dont la cardinalité de sortie dépend des données — un filter rétrécit, un group-by renvoie une ligne par clé distincte, une jointure peut exploser. Flux l'interdit. Les cinq règles qui le remplacent :
| Règle | |
|---|---|
| T1 | Le plafond de sortie est une fonction statique des plafonds d'entrée. N pour select/filter/sort/window · min(N,k) pour un slice · un G déclaré pour un group-by · Ng pour une jointure as-of · Ng × F pour une équi-jointure avec un fan-out déclaré · Na + Nb pour une concaténation. Jamais dépendant des données — et un plafond dérivé qui dépasserait N_max est [ErrTotal] à la compilation. |
| T2 | La sélection est un MASQUE, jamais un rétrécissement. Un « filter » écrit na là où le prédicat est faux et efface le bit de vie. La longueur ne bouge pas ; count, si. |
| T3 | L'itération est consciente de na, si bien que rien n'a besoin d'être compacté. Les agrégations réduisent les lignes vivantes : les tombstones sont exclues avant la réduction, pas absorbées après. |
| T4 | Le dépassement est une politique nommée, jamais une croissance. Au-delà du plafond déclaré : Reject (jette l'élément excédentaire, avec un diagnostic), Truncate (garde les survivants canoniques), ou Bucket (un panier « autre »). Un plafond absent est une erreur de compilation. |
| T5 | Tout est fonctionnel, et s'abaisse vers les primitives vec.* figées — ce qui explique pourquoi la fusion en une seule passe en colonnes est gratuite. |
Les verbes eux-mêmes sont ordinaires, et ils se chaînent :
fluxdef liquidBars(n) =
let bars = series("BTC-USD").toTable(n).derive(range, high - low) in // ADD a column — a fresh record
let hot = bars.where(bars.volume > sma(bars.volume, 20) and bars.range > atr(14)) in
hot.select(time, close, volume, range) // projection
def dailyVwap(n) =
let bars = series("BTC-USD").toTable(n) in
bars.groupBy((r) -> { y: year(r.time), d: dayOfYear(r.time) }, maxGroups: 366)
.agg((g) -> { vwap: (g.close * g.volume).sum() / g.volume.sum(), // pv ÷ volume → price
hi: g.high.highest() })
Trois détails dans cet extrait portent tout le design.
where prend un masque de colonne, pas une lambda de ligne — il ne supprime jamais une ligne,
il en vide une. Le masque est une expression de colonne ordinaire sur le receveur, et c'est pourquoi
le receveur a un nom : il n'y a pas de variable de ligne implicite dans une requête Flux, si
bien qu'une table intermédiaire est liée avec let … in et ses colonnes en sont projetées
(bars.volume). Une chaîne qui ne nomme jamais son intermédiaire ne peut pas parler de ses
colonnes.
groupBy déclare son plafond (maxGroups: 366) : la mémoire dont un group-by a besoin est donc
connue avant son exécution, ce qui est exactement ce qu'un hash-aggregate non borné ne peut pas
promettre.
derive ajoute une colonne (il construit un enregistrement neuf) ; with { … } redéfinit
une colonne existante. La distinction n'est pas stylistique — with préserve la forme par la loi,
si bien qu'ajouter un champ par ce biais serait [ErrField].
#Les jointures
La jointure as-of est la primitive, pas une arrière-pensée — parce que les données de séries temporelles sont l'objet de la plupart des jointures dans ce domaine, et parce que c'est la jointure dont le plafond de sortie est trivialement statique (une ligne par ligne de gauche) :
fluxdef joins(quotes, refs) =
let bars = series("BTC-USD").toTable(256) in
{ aligned: bars.asofJoin(quotes, key: time, take: [bid, ask]), // the most recent quote at or before each row
paired: bars.join(refs, key: symbol, kind: Inner, maxFanout: 4) } // equi-join with a DECLARED maximum fan-out
Une équi-jointure sans maxFanout déclaré ne compile pas. C'est le prix à payer pour qu'une requête
n'explose jamais en production, et c'est un prix qui vaut la peine.
#Le modèle d'exécution
Une requête est un DAG de nœuds purs, et c'est le même DAG que le moteur optimise déjà. Ainsi :
- Le pushdown de projection est de l'élimination de code mort — une colonne que personne ne lit n'est jamais matérialisée.
- Le pushdown de prédicat consiste à fusionner le masque dans le scan producteur.
- L'élimination de sous-expressions communes est globale, à l'échelle d'une requête entière, et entre scripts.
- Rien d'intermédiaire n'est matérialisé. Une chaîne de dix verbes est une seule passe sur les colonnes.
L'exécution suit le modèle morsel → sink : les colonnes sont découpées en morceaux, les morceaux circulent dans le pipeline fusionné, et les résultats atterrissent dans le sink. Le parallélisme s'applique aux réductions indépendantes (groupes distincts, cellules distinctes) — jamais à l'intérieur d'une réduction unique, car cela réassocierait la virgule flottante et briserait l'identité byte-à-byte.
Pourquoi l'ordre de réduction est épinglé même quand cela coûte en vitesse. Une somme parallèle est plus rapide et donne un dernier bit différent. Deux moteurs seraient alors en désaccord, le rejeu dériverait, et un serveur ne pourrait pas vérifier le travail d'un client. L'ordre de réduction est donc fixé, et le parallélisme est trouvé là où il ne change pas un seul bit.
#La couche numérique
Mat(κ, R, C) avec deux axes const, et au-dessus :
- Algèbre linéaire — décompositions, résolutions, valeurs propres pour les matrices symétriques — chacune avec un ordre de réduction figé, si bien que le dernier bit est le même sur toutes les machines.
- Statistiques — la famille des moments, les statistiques d'ordre, la corrélation, le chemin de régression OLS/QR.
- Traitement du signal — la FFT, bornée et déterministe.
- Aléatoire — le générateur d'entiers épinglé fondé sur un compteur, si bien qu'une simulation se rejoue à l'identique.
- Optimisation — des solveurs bornés, avec un plafond d'itérations qui fait partie du type.
#Bibliothèques de domaine
| Fonction | Ce qu'elle calcule |
|---|---|
sharpe(returns, rf, barsPerYear(clock)) |
le ratio annualisé, avec la base rendue explicite |
rollOls(y, x, 60) |
une régression glissante — fenêtre bornée |
drawdown(equity) |
le pic-à-creux courant |
impliedVol(price, strike, t, r) |
un point de surface d'options |
ytm(cashflows), duration(bond) |
revenu fixe — un rendement, et une sensibilité temporelle |
laspeyres(p0, p1, q0), paasche(p0, p1, q1) |
nombres-indices |
Chacune d'elles est une composition bornée des primitives ci-dessus — ce qui veut dire que chacune
est aussi lisible, et que chacune passe le typage dimensionnellement. sharpe ne peut pas
annualiser silencieusement avec la mauvaise base, parce que la base est un argument dont le kind dit
ce qu'il est.
geom.* est la famille de géométrie 2-D — distances, point-dans-forme, intersections — utilisée par
les outils de dessin et la mise en page personnalisée. Elle est dans l'espace écran par
conception : la projection dans l'espace des données appartient à l'hôte, ce qui maintient le
pare-feu intact.
#Le tableur
Post-v1. Un dataframe borné et réactif avec une grammaire de cellules (SUM, AVERAGE,
VLOOKUP, SUMIF, COUNTIF, …), évalué par le même moteur de graphe avec coupure anticipée — une
feuille est une requête qui ne recalcule que ce qui a changé. Il figure ici parce qu'il découle de
l'algèbre plutôt que d'y être greffé : une feuille est une table bornée plus un graphe de
dépendances, et Flux possède déjà les deux.
#Jonctions réservées
Reserved. metric[id] — une série hors-prix (un indicateur économique, une métrique on-chain,
un flux analytique) entrant dans le plan d'analyse comme un flux causal de première classe, tagué
avec son identité de sorte qu'ajouter une série d'IPC à une série de hashrate est [ErrDim]. La
jonction est conçue et inerte en v1 ; rien de hors-prix n'entre dans l'analyse tant qu'elle n'est
pas armée.
#Voir aussi
- Kinds — le système dimensionnel que porte chaque colonne.
- collections — les structures bornées sous les tables.
- display —
viz.*, qui transforme uneTableen scène. - units —
meas[u], pour les quantités hors du domaine de marché. - Modèle mémoire — la disposition en colonnes et le plan de vivacité.
- Optimiseur — la fusion et les pushdowns, et pourquoi ils sont sûrs.
collections — bornés, ordonnés, à sémantique de valeur
Post-v1. Vec est le kind de séquence de v1 et fait déjà partie du cœur du langage. Map,
Set, Deque et Tree sont des designs scellés dont le déploiement suit v1 — collections est le
premier package implémenté après le cœur du langage, et l'ordre des packages qui le suivent est
figé.
Un langage accumule d'ordinaire une ménagerie de collections — liste, dictionnaire, ensemble, tuple, dictionnaire ordonné, compteur, deque, tas — parce qu'il poursuit trois axes orthogonaux à la fois : mutable contre immuable, extensible contre fixe, ordonné contre non ordonné. Flux fixe les trois par construction, et la ménagerie s'effondre. Ce qu'il reste, c'est un seul substrat — une arène bornée — plus une discipline d'accès : cinq kinds de conteneurs, un vocabulaire, un ordre.
Cette page spécifie ce cadre : le substrat, les cinq kinds, l'API uniforme, d'où vient la capacité, pourquoi chaque conteneur est ordonné et rien n'est haché, pourquoi une API à sémantique de valeur ne coûte rien à l'exécution, et l'ordre figé dans lequel les pièces sont construites.
#Pourquoi ce cadre est plus petit ici qu'ailleurs
Chacun des trois axes qui produisent la ménagerie est déjà décidé, à l'échelle du langage, avant qu'un seul conteneur n'existe :
| Axe | Ce que Flux a déjà décidé | Ce que l'axe coûte ici |
|---|---|---|
| mutable / immuable | sémantique de valeur — il n'y a pas de mutation à exposer | pas de scission mutable/immuable |
| extensible / fixe | borné (A13) — chaque tampon a une capacité repliée à la compilation | pas de scission extensible/fixe |
| ordonné / non ordonné | déterministe (I6/I7) — chaque ordre d'itération est épinglé | pas de scission ordonné/non ordonné |
Trois décisions, trois scissions qui n'ont jamais lieu. Une seule abstraction subsiste, et elle montre trois visages : séquence (atteindre un élément par sa position), associatif (l'atteindre par clé), et hiérarchique (l'atteindre par lien). Cinq kinds couvrent les trois visages ; tout le reste se compose à partir d'eux sans nouveau kind.
Et l'API à sémantique de valeur n'est pas un impôt. Parce que Flux est pur, analysable et borné, cette API est exécutée en place (§ Efficacité) : l'ergonomie d'une collection persistante avec la performance d'un tableau mutable — grâce aux contraintes, non pas malgré elles.
#Le substrat — une arène bornée, et des kinds abstraits
Chaque conteneur est une arène bornée — un vec(Slot, N) — enveloppée dans un kind
structurellement abstrait : sa représentation est cachée. Un script ne filtre jamais les
entrailles d'un conteneur ; il n'a que l'API bénie, et c'est cette API qui maintient l'invariant (le
tri d'un Map, les curseurs d'anneau d'un Deque, l'acyclicité d'un Tree). Trois conséquences en
découlent, et chacune est une raison pour laquelle le design vaut l'abstraction :
- Les invariants sont protégés. Il n'y a aucun moyen, depuis un script, de briser l'ordre de tri
d'un
Mapou de corrompre les curseurs d'unDeque. Les états qui violeraient l'invariant ne sont pas simplement rejetés — ils sont inexprimables. - L'implémentation est libre. L'hôte choisit le support — un tableau trié, un B-tree borné, un hachage épinglé — et peut le changer sans changer une ligne de code utilisateur, parce qu'aucun code utilisateur ne l'a jamais vu.
- Chaque opération est une routine épinglée. Les opérations de conteneur rejoignent l'ensemble
épinglé (
decimal, les codecs,fmt.*, l'ordre total surna) : une seule routine, partagée par l'interpréteur, le module compilé et le serveur. L'invariant I7 — interpréteur ≡ module compilé, byte pour byte — tient donc à travers unMapexactement comme il tient à travers unema, et le rejeu reste exact.
Paramétré, monomorphe par site. Map(K, V, N) prend deux kinds et un cardinal const N,
exactement comme vec(κ, N) prend un kind d'élément et une longueur. C'est la paramétrisation de
kind — instanciée à des K/V/N concrets à chaque site d'utilisation — et ce n'est ni du
polymorphisme de rangée ni des génériques de première classe. La hauteur du kind résultant est finie
dès que ses paramètres le sont :
height(Map(K, V, N)) = max(height K, height V) + 1
si bien que le treillis reste fini par famille structurelle, ce qui rend la borne supérieure, la
borne inférieure et les lois de clôture énumérables — le même argument qui porte déjà vec et
record.
#Les cinq kinds
| Kind | Visage | Support (caché) | Ordre canonique | À quoi il sert |
|---|---|---|---|---|
Vec(κ, N) |
séquence, par index | tableau | index 0 … len−1 |
la primitive ; le pivot vers lequel projette tout autre kind |
Deque(κ, N) |
séquence, deux bouts | tampon en anneau | avant → arrière | O(1) aux deux bouts : file, pile et tampon glissant en un seul kind |
Map(K, V, N) |
associatif | tableau trié / B-tree borné | clé croissante | état sous une clé dynamique |
Set(K, N) |
associatif, sans valeur | tableau trié | clé croissante | appartenance et algèbre d'ensembles |
Tree(κ, N) |
hiérarchique | pool de nœuds + index de handles | pré-ordre DFS | hiérarchies, avec zéro type récursif |
#Les écrire
Deux orthographes coexistent, et il vaut la peine de les séparer une bonne fois :
- En position de kind, la séquence garde sa forme de surface figée — en minuscules, entre
parenthèses :
vec(κ, N). Les quatre autres utilisent la forme ordinaire de kind paramétré :Deque(κ, N),Map(K, V, N),Set(K, N),Tree(κ, N). Aucune nouvelle grammaire n'est en jeu : ce sont les expressions de kindIDENT(args)existantes, de la même forme queosc(0, 100)oudecimal(18, 2). - En position de valeur, les espaces de noms de construction sont en majuscules :
Vec.of,Map.of,Set.of,Deque.empty,Tree.node. L'espace de noms de fonctionsvec.*(map,fold,sortBy,topK, …) est inchangé et toujours en minuscules.
fluxrecord Scanner {
ema20: Map(string, price, 64)
seen: Set(string, 64)
recent: Deque(num, 32)
}
#Vec — la séquence
Le visage positionnel, et celui que vous avez déjà : at(i), set(i, x), first / last,
slice, window(n), et push / pop en queue — la pile naturelle, O(1) en place. Il porte
toute l'algèbre : map / fold / scan / zip / where / mask / sortBy / topK / fill /
range / setAt.
fluxavgVol = sma(volume, 20)
hot = vec.where(window(volume, 64), (v) -> v > avgVol) // vec(volume, 64) — na in the holes
top3 = vec.topK(window(high, 64), (h) -> h, 3) // vec(price, 3)
La lambda vient avant le compte dans topK : la fonction de clé est ce dont l'opération parle,
et le compte est une borne sur son résultat. Les deux sont les appels vec.* ordinaires — le même
espace de noms, de la même forme, que vec.map et vec.fold.
Vec est le pivot : tout autre conteneur s'y projette via toVec (§ L'API uniforme), ce qui
explique pourquoi l'API par conteneur reste petite — chaque kind porte seulement ses opérations
structurelles, et le reste de l'algèbre s'atteint par le pont.
#Deque — file, pile et tampon en anneau, en un seul kind
Une seule structure pour chaque discipline d'accès. La représentation est un anneau — ⟨buf: vec(κ, N), head, len⟩ — si bien que at(i) est O(1), tout comme les deux bouts :
| Discipline | Push | Pop |
|---|---|---|
| file (FIFO) | pushBack |
popFront |
| pile (LIFO) | pushBack |
popBack |
| deque | l'un ou l'autre bout | l'un ou l'autre bout |
pushFront / pushBack / popFront / popBack produisent le deque sans l'élément ; l'élément
lui-même se lit avec peekFront / peekBack, qui valent na sur un deque vide. Séparer la lecture
du retrait est ce qui fait que chaque opération renvoie exactement une valeur.
fluxdef rolling(q, x) = q.popFront().pushBack(x) // fixed-size ring: one out, then one in
Pourquoi
popFrontvient en premier. Pousser sur un deque plein est un rejet avec un diagnostic, pas une éviction silencieuse — aucun conteneur ne laisse jamais tomber vos données dans votre dos, et aucun ne grandit jamais. Un anneau qui écrase son plus ancien élément s'écrit donc comme un pop explicite suivi d'un push. Là où l'éviction est la politique que vous voulez, elle est déclarée comme telle : la politique de contre-pressionLatest(n)de net est une file bornée dont la règle d'éviction fait partie de son kind.
Il n'y a délibérément aucun kind Queue, Stack ou Heap séparé. Si l'intention a besoin d'un
nom, Queue et Stack sont des alias à API restreinte sur Deque, pas des kinds à part entière.
Et beaucoup de besoins de « file » sont déjà couverts ailleurs : la politique Latest(n) est une
file bornée, la boîte aux lettres Sub / Cmd est la file de messages, window(n) est le
tampon glissant. Le Deque en espace utilisateur sert le reste — une frontière de parcours en
largeur, une liste de travail, une file d'animation.
Post-v1. Une file de priorité (Heap) est différée : topK couvre déjà le besoin de top-k,
et le tas arrive avec le lot d'algorithmes de graphe qui le requiert réellement (§ L'ordre
d'implémentation).
#Map — le kind associatif ordonné
Le vrai manque que le cadre comble : l'état sous une clé dynamique. L'état par symbole dans un
scanner multi-actifs, « ai-je déjà vu cet id », les comptes par panier — chacun d'eux, sans Map,
est un balayage linéaire sur un slotmap ou un vec trié à la main.
Le support est un tableau trié de clés et de valeurs (ou un B-tree borné, au choix de l'hôte) :
| Opération | Signification |
|---|---|
get(k) |
V | na — recherche binaire, O(log N), na quand la clé est absente |
getOr(k, d) |
la valeur, ou d quand absente |
has(k) |
signal |
insert(k, v) / remove(k) |
un nouveau map — exécuté en place quand l'ancien est mort |
update(k, (v) -> …) |
réécrit une entrée via une lambda |
keys / values / entries |
projections vers vec, dans l'ordre des clés |
range(lo, hi, k: lit) |
un balayage de plage borné sur le support trié — O(log N + k) |
fluxsyms = Vec.of(["BTC-USD", "ETH-USD", "SOL-USD"]) // vec(string, 3)
def bump(m, k) = m.insert(k, m.getOr(k, 0) + 1)
counts = vec.fold(syms, Map.empty(64), (m, k) -> bump(m, k)) // Map(string, num, 64)
nBtc = counts.get("BTC-USD") // num | na
range est l'opération qui retire discrètement tout un kind du design. Une plage est aussi un
préfixe sur un support trié, si bien qu'une saisie semi-automatique — la raison classique de
recourir à un trie — est un balayage de plage borné :
fluxdef suggest(idx, lo, hi) = idx.range(lo, hi, k: 10) // vec(record{ key: string ; val: num }, 10)
O(log N + k), un plafond déclaré de k résultats, aucun kind trie nulle part.
Le slotmap du plan APP est le même idiome : il est un Map(Handle, V, N) dont l'hôte assigne
les clés. Le cadre généralise le slotmap plutôt que de le dupliquer.
#Set — l'ensemble ordonné
Un Map dont la colonne de valeurs est retirée : ⟨keys: vec(K, N), len⟩. Il porte has, add,
remove, et l'algèbre qui n'a pas sa place sur un map — union, intersect, diff, subset.
fluxlevels = Set.of(window(high, 64)) // Set(price, 64) — deduplicated, sorted, na skipped
known = levels.has(close) // signal — O(log N)
near = levels.toVec().take(5) // back into vec-land, and the whole vec algebra
#Tree — une hiérarchie comme pool de nœuds
Les arbres existent surtout pour l'affichage — une watchlist repliable par secteur, les conditions imbriquées d'une stratégie, une hiérarchie de scène ou de menu — et pour toute donnée dont la structure interne est une hiérarchie. Un arbre n'est jamais un type récursif :
fluxrecord Node { value: num ; children: Node } // ✗ [ErrTotalType] — a type may not reference itself
Un type récursif n'a pas de hauteur finie, et un langage qui borne sa mémoire à la compilation ne
peut pas en admettre un. Un Tree est donc un pool de nœuds plat — ⟨nodes: vec(Node, N), root⟩ où un nœud contient sa valeur plus deux index de handles (firstChild, nextSibling ; na
signifie absent). Il est borné par son nombre de nœuds N, d'où découle la borne sur la
profondeur. C'est exactement ce que font déjà les nœuds BSP du plan d'affichage : les enfants sont
des handles, pas des nœuds.
Le parcours est un catamorphisme, et l'hôte pilote la récursion :
fluxrecord Sector { name: string ; weight: ratio }
def totalWeight(t) = tree.fold(t, (node, kids) -> node.weight + kids.sum()) // ratio
fold passe à votre lambda la valeur d'un nœud et les enfants déjà pliés (sous forme de vec),
en parcourant le tableau plat en post-ordre. Il termine par construction, parce que N borne le
pool.
Pourquoi le script n'écrit jamais la récursion. Un parcours récursif écrit dans le script serait un
defrécursif, et le graphe d'appels doit rester acyclique ([ErrTotalRec]) pour que la preuve de totalité tienne. Confier la récursion à un kernel hôte borné préserve les deux propriétés : l'arbre est parcouru, et le langage reste total. La lambda est de seconde classe — éliminée au site d'appel — si bien qu'aucune flèche n'entre jamais dans le treillis.
Le reste de la surface : map, flatten(t) -> vec(κ, N) (pré-ordre DFS), depth,
children(node), insertChild(parent, x), prune(node). La construction est Tree.node(x, kids),
ou Tree.unfold(seed, step, N) — l'anamorphisme dual, borné par N nœuds.
fluxleaf = Tree.node({ name: "Energy", weight: 0.18 }, [])
rows = leaf.flatten() // vec(Sector, N) — DFS pre-order
#L'API uniforme — Foldable, et le pont toVec
Vous l'apprenez une seule fois. Chaque conteneur est Foldable, et toVec est la lentille
universelle : dès l'instant où vous tenez un vec, toute l'algèbre vec (sortBy, topK, sum,
where, fold) s'applique. C'est ce qui garde minuscule l'API par conteneur — chaque kind n'expose
que ce qui est structurel en lui, et tout le reste passe par le pont.
Le vocabulaire partagé par les cinq :
count(c) · isEmpty · isFull |
occupation |
cap(c) |
la capacité N — un const |
fold(c, seed, step) · map(c, f) · forEach |
parcours dans l'ordre canonique |
where / mask |
sélection, préserve la longueur, na dans les trous |
toVec(c) |
le pont vers l'algèbre vec |
Par visage :
| Visage | Opérations |
|---|---|
| séquence | at(i) · first / last · push / pop (Vec) · pushFront / pushBack / popFront / popBack / peek* (Deque) |
| associatif | get(k) · has · insert / remove / update · keys / values / entries; Set ajoute add / remove et union / intersect / diff |
| hiérarchique | root · children · insertChild / prune · fold · flatten · depth |
La construction est uniforme elle aussi : C.of(foldable) construit un conteneur à partir de
n'importe quel foldable — un littéral, un vec, un autre conteneur — et C.empty(N) est le
conteneur vide à la capacité N. Vec.of([…]), Set.of([…]), Map.of([(k, v) …]),
Deque.empty(N), Tree.node(x, kids).
Ce sont des routines hôtes par kind, dispatchées sur le kind du receveur au site d'appel — le
même dispatch structurel que les opérations en colonnes Table / Col / Mat utilisent déjà.
Ainsi tree.fold(t, f) sur un Tree et vec.fold(v, seed, step) sur un vec sont des routines
différentes derrière un seul nom, choisies selon ce sur quoi vous l'avez appelé, à la compilation.
#Quatre règles, propagées partout
L'enveloppe totalité-et-déterminisme apparaît à l'identique dans les cinq kinds :
- Plein ⇒ rejet et diagnostic. Jamais de croissance silencieuse.
isFullvous laisse décider d'abord. - Absent ⇒
na. Une clé manquante, un peek vide, un index hors plage —na, jamais une exception.naest conscient denaà travers le reste de l'algèbre, si bien qu'il se propage au lieu de piéger. - Aucune opération de raccourcissement, jamais. Il n'y a pas de
filter: une longueur dépendante des données briserait la borne.whereetmaskpréservent la longueur et laissentnadans les trous, et l'itération est consciente dena, si bien que rien n'a besoin d'être compacté. - Un ordre canonique par conteneur (§ Déterminisme).
fluxavgVol = sma(volume, 20)
hot = vec.where(window(volume, 64), (v) -> v > avgVol) // ✓ same length, na where the predicate is false
fluxhot = vec.filter(window(volume, 64), (v) -> v > avgVol) // ✗ no such operation — the length would depend on the data
#La capacité vient du contexte
La ligne dure de la règle de mémoire bornée, énoncée une bonne fois :
-
La capacité est un const de compilation : un littéral (
Map(price, num, 64)), un const nommé (MAX_BARS), ou un const fourni par le contexte hôte — le nombre maximal de barres d'un graphique, une taille de watchlist déclarée. C'est le motif que le kind série utilise déjà :flux
type Series(T) = vec(T, MAX_BARS) // the context fixes the ceiling -
L'occupation est une valeur d'exécution, toujours ≤ capacité.
countbouge ;N, non. -
La capacité se propage à travers les conversions.
vec(κ, M).toSet()est unSet(κ, M): la déduplication ne peut que réduire la population, doncMest la borne sûre — inférée, jamais annotée. -
Un plafond d'exécution est interdit. Une capacité qui varie avec les données rendrait le compte mémoire de compilation improuvable.
fluxn = count(volume > sma(volume, 20), 64) // a RUN-TIME value: it depends on the data
seen = Set.empty(n) // ✗ [ErrTotal] — a capacity must const-fold
Pourquoi « cela dépend du contexte » n'est pas la même chose que « cela dépend des données ». Le contexte fixe le plafond const — combien de barres ce graphique contient, combien de niveaux cet outil autorise. Les données fixent l'occupation sous ce plafond. Gardez les deux séparés et la mémoire dont un script a besoin est calculable avant qu'il ne s'exécute ; confondez-les et elle ne l'est pas.
#Déterminisme — ordonné par défaut, zéro hachage
Chaque conteneur a un ordre canonique, utilisé par toVec, par fold, et par l'itération,
parce que I6/I7 exigent que deux moteurs produisent les mêmes octets.
| Kind | Ordre canonique |
|---|---|
Vec |
ordre d'index |
Deque |
avant → arrière |
Map / Set |
clé croissante |
Tree |
pré-ordre DFS (enfants dans l'ordre d'insertion) |
Map et Set sont triés par clé — les kinds associatifs sont des structures ordonnées, pas des
tables de hachage. Trois raisons, dans l'ordre où elles ont compté :
- Les clés Flux sont presque toujours déjà des kinds ordonnés (
string,num,decimal), donc rien n'est perdu. - L'itération triée est déterministe par construction. Il n'y a pas de graine à épingler, pas d'historique d'insertion à reproduire, pas de divergence à traquer.
- C'est généralement l'ordre que vous vouliez de toute façon — classements, affichage par clé.
Pourquoi un ordre de hachage n'est pas une option. Un ordre de hachage dépend d'une graine et de l'historique d'insertion, si bien que deux moteurs peuvent itérer le même ensemble dans deux ordres différents tout en étant tous deux « corrects ». Sous I7 — interpréteur ≡ module compilé, byte pour byte — et sous un rejeu qu'un serveur re-dérive pour vérifier le travail d'un client, « tous deux corrects » est une divergence. Trier les clés coûte O(log N) à la recherche et rachète toute la propriété, sans rien à épingler.
Les clés doivent être comparables. Un kind de clé doit admettre un ordre total et une égalité
([CmpOrd] / [CmpEq]) : string, num, dir, decimal, et les enregistrements de ceux-ci. Les
kinds sans égalité — clock, ui, une lambda — ne peuvent pas être des clés :
fluxrecord Bad { picked: Set(ui, 8) } // ✗ [ErrArg] — `ui` has no equality, so it cannot be a key
Post-v1. Un support en ordre d'insertion ou à hachage épinglé (sans graine) reste disponible comme choix d'implémentation de l'hôte derrière la même API, si jamais un profil à clés non ordonnées ou à insertions intensives l'exige. L'ordre exposé reste déterministe dans les deux cas ; la décision est fondée sur des preuves, et le tri-par-défaut est la réponse livrée.
#Efficacité — une API de valeur, exécutée en place
C'est ici que les contraintes se rentabilisent. Une collection immuable bornée a l'air lente. Elle ne l'est pas, et la raison est mécanique.
Fonctionnelle mais en place. L'API renvoie un nouveau conteneur — m2 = m.insert(k, v) — mais
le compilateur détient déjà le DAG des usages. Quand m est mort après l'appel, l'hôte mute
l'arène en place : O(1) ou O(log N), pas une copie O(N). Aucune machinerie nouvelle n'est en jeu
; c'est l'analyse de vivacité existante (tampons persistants-tenus contre transitoires-recyclés)
appliquée aux mises à jour de conteneurs. Rien n'est annoté, rien n'est emprunté — c'est inféré.
fluxdef track(book, sym) = book.insert(sym, ema(close, 20)) // returns a new Map…
// …and updates the arena in place when the old one is dead
C'est observablement pur dans les deux cas : en place et copie produisent la même valeur, si bien que I6/I7 tiennent et les octets sont identiques. L'optimisation est invisible sauf dans le profil.
Pas de trie persistant. Les collections immuables non bornées ont besoin d'un trie mappé sur tableau de hachage pour être efficaces — c'est ce qui paie le partage structurel. Les arènes bornées plus l'exécution en place obtiennent le même effet plus directement : un tableau contigu, muté en place, pas de ramasse-miettes, pas de course aux pointeurs, et des balayages linéaires qui sont favorables au cache.
Zéro allocation en régime permanent. Le plafond N pré-dimensionne l'arène, si bien que l'arène
est allouée une seule fois. Même le pire cas — une copie réelle — est un memcpy de taille fixe.
toVec est souvent gratuit. Là où le support est déjà le vecteur, toVec est une vue, pas
une copie. Seule une réorganisation (une vue triée d'un support en ordre d'insertion) matérialise
quoi que ce soit.
#Conversions et compositions
Le pont of / toVec rend la conversion mécanique, et la capacité vient avec :
| De → vers | Comment | Sémantique |
|---|---|---|
| vec → set | v.toSet() / Set.of(v) |
déduplique, trie, saute na ; cap = le cap source |
| set → vec | s.toVec() |
trié |
| vec → map | v.toMap((x) -> key(x)) / Map.of(pairs) |
clé dérivée ; en cas de collision le dernier gagne |
| map → vec | m.entries() / m.keys() / m.values() |
ordre des clés |
| vec → deque | Deque.of(v) |
avant → arrière = ordre source |
| tree → vec | t.flatten() |
pré-ordre DFS |
Tout le reste se compose, et n'ajoute aucun kind :
fluxtype Graph = Map(num, Set(num, 16), 64) // bounded adjacency, by handle
type Counts = Map(string, num, 64) // a counter / bag
type Index = Map(string, Set(num, 32), 256) // an inverted index: token → document ids
Un parcours de graphe borné est une loop bornée sur exactement deux de ces kinds — une frontière
Deque et un Set de nœuds visités :
fluxrecord Walk { frontier: Deque(num, 64) ; seen: Set(num, 64) ; order: vec(num, 64) }
Le parcours en largeur, le parcours en profondeur, l'ordre topologique, les composantes connexes et
la détection de cycles sont tous des defs sur cet état. Un multimap est Map(K, vec(V, M), N). Un
slotmap est Map(Handle, V, N). Aucun d'eux n'est un nouveau kind.
Le pilier compute reste une famille séparée. Table / Col / Mat sont en colonnes et
relationnels — groupBy, asofJoin, stat, régression — et ils ne sont pas forcés dans le
vocabulaire des conteneurs, pas plus qu'un dataframe ne devrait être forcé dans un dictionnaire. Le
pont entre les deux mondes est Col ↔ Vec : une colonne est une séquence. Voir
compute.
#Les plans, le pare-feu et le rejeu
Les conteneurs sont des valeurs pures, ils ne franchissent donc aucune frontière de plan et ne soulèvent aucune question de pare-feu :
- Dans le plan ANALYSIS, ils sont bornés et déterministes — un
Mapde moyennes mobiles par actif, unSetde niveaux détectés, unTreede structure de marché. - Dans le plan APP, ce sont des champs du Model. Le slotmap du Model en est déjà un, et la map de stable-id à côté est l'autre :
fluxrecord Level { id: num ; price: price ; label: string }
record Model { levels: Map(num, Level, 64) ; count: num }
Aucune opération de conteneur ne lit la présentation, et aucune ne lit une valeur device-variable,
si bien qu'aucune ne peut briser le pare-feu ([ErrFirewall] n'est pas atteignable depuis cette
API). Chaque opération est une routine épinglée avec un ordre canonique, si bien que le rejeu
byte-identique — et l'anti-triche qui repose dessus — tient à travers n'importe quel conteneur.
Les lambdas passées à fold / map / where sont de seconde classe, éliminées au site d'appel, si
bien qu'aucune sorte flèche n'entre dans le treillis.
#Limites délibérées
Ce sont des décisions de conception, pas des manques :
- Aucune collection non bornée. La borne est toujours un const. La totalité n'a pas d'exceptions.
- Aucun type récursif (
[ErrTotalType]) — les arbres et les graphes passent par un pool de nœuds et des index de handles, jamais un type qui se pointe lui-même. - Pas de
filter— une longueur dépendante des données brise le compte mémoire.where/maskpréservent la longueur. - Les clés doivent être ordonnables et comparables par égalité (
[CmpOrd]/[CmpEq]) — pas de cléclock,uiou lambda. - Aucun plafond variable à l'exécution.
- Post-v1. Différés, chacun avec une raison et un déclencheur : un
Heap/ file de priorité (topKcouvre le besoin ; le tas est livré avec les algorithmes qui le requièrent), un support à hachage épinglé (le tri-par-défaut suffit ; la décision est fondée sur des preuves), unDequeen finger-tree avec split et concat en O(log N) (les bouts O(1) de l'anneau suffisent), et le polymorphisme de rangée sur les kinds de valeurs (v1 est monomorphe par site).
#L'ordre d'implémentation
L'ordre est figé, et il est propre en dépendances — chaque lot n'a besoin que de ceux qui le précèdent, et chacun est tiré par un consommateur nommé plutôt que par symétrie :
| Lot | Débloque | |
|---|---|---|
| 1 | Vec — fait ; réadapté comme le visage de séquence du cadre |
tout |
| 2 | Map / Set — ordonnés, sans hachage, en place |
l'index inversé et le balayage de plage par préfixe (saisie semi-automatique, sans kind trie), le slotmap à clé, les compteurs et les sacs |
| 3 | Tree — pool de nœuds et catamorphisme |
l'AST Markdown du pilier text et le motif document ; Tree est livré avec ou avant le codec Md |
| 4 | Deque — chez son premier vrai consommateur |
une frontière de parcours en largeur, une liste de travail, une file d'animation |
| 5 | Heap / file de priorité — avec le lot d'algorithmes de graphe |
Dijkstra et A*, qui sont les seuls appelants qui en ont réellement besoin ; BFS, DFS, l'ordre topologique, les composantes et les cycles n'ont besoin que des lots 2 et 4 |
| 6 | Map à hachage épinglé, Deque en finger-tree — fondés sur des preuves, derrière la même API |
rien, jusqu'à ce qu'un profil dise le contraire |
Le bilan : cinq kinds, un vocabulaire (Foldable plus toVec), à sémantique de valeur et
exécutés en place, ordonnés et déterministes, bornés — et chacune de ces propriétés est la
conséquence d'une décision que le langage avait déjà prise.
#Voir aussi
- compute — la famille en colonnes
Table/Col/Mat, et le pontCol ↔ Vec. - Kinds — la paramétrisation de kind, la hauteur finie, et les classes
[CmpOrd]/[CmpEq]. - text — l'AST Markdown, le consommateur nommé de
Tree. - App plane — le slotmap, qui est un
Map(Handle, V, N). - Modèle mémoire — l'arène bornée, la vivacité, et l'exécution en place.
- net —
Latest(n), la file bornée dont la politique d'éviction fait partie de son kind.
units — quantités générales
Post-v1. Le pilier units est un amendement scellé et gouverné au système de kinds.
Un mètre n'est pas une seconde. Un kilogramme n'est pas un kilo-octet. Vingt degrés Celsius plus vingt degrés Celsius, ce n'est pas quarante degrés Celsius — et un langage qui vous laisse l'écrire est un langage qui finira par produire un nombre que vous ne pourrez pas défendre.
Le pilier units porte les quantités physiques et générales dans le système de types, avec la même
machinerie que les kinds de marché utilisent déjà : un tag sur num, des conversions exactes,
et des règles qui font que les cas dénués de sens échouent à compiler.
Une note sur les exemples. Flux n'a pas d'expressions-instructions, si bien qu'une expression nue n'est pas un programme. Les lignes marquées
✗sur cette page sont donc des fragments d'expression : ils existent pour montrer ce que les règles de kind refusent, pas ce que le parseur accepte. Toute ligne non marquée est une instruction légale.
#Trois régimes, une règle chacun
Les unités réelles tombent dans exactement trois classes, et chacune reçoit son propre traitement. C'est le consensus établi à travers les systèmes qui ont essayé, et il vaut la peine de l'énoncer d'emblée parce que la plupart des demi-mesures viennent de leur confusion :
| Régime | Exemples | Traitement |
|---|---|---|
| Linéaire | longueur, masse, volume, taille de données, vitesses et débits | le tag, plus une conversion multiplicative exacte |
| Affine | température (°C, °F, K) | le même tag, plus un bit point | delta qui sélectionne la bonne formule et proscrit l'arithmétique dénuée de sens |
| Non linéaire | décibels, pH, magnitudes | pas des unités. Fonctions pures explicites — une conversion d'unité ne quitte jamais sa classe de convexité |
Rien n'est retiré par cette troisième ligne : les décibels fonctionnent toujours. Ils fonctionnent comme une fonction, parce que c'est ce qu'ils sont.
#Le tag
meas[m] meas[m·s⁻¹] meas[kg·m·s⁻²] meas[B] meas[px]
Une unité est un produit de symboles à exposants entiers, tirés d'un catalogue fermé et
versionné. Chaque symbole déclare sa famille (longueur, masse, données, température…), son
facteur exact vers l'unité canonique de cette famille — un rationnel épinglé, si bien que
km = 1000 m et KiB = 1024 B et mi = 1609344/1000 m sont exacts, pas approximatifs — et, pour
la température seulement, sa pente et son décalage affines.
Structurellement, c'est le même geste que l'annotation de paire de devises a déjà fait : un axe
d'annotation porté par num, dont le sommet est le num nu. Zéro nouvelle sorte, zéro nouvelle
hauteur de treillis.
fluxd = unit.km(5) // meas[km]
elapsed = 7200s // duration — two hours. NOT a unit: see below
v = d / elapsed // meas[km·s⁻¹] — a distance ÷ a duration: the time bridge
Deux contraintes sur le catalogue méritent d'être connues, parce que ce sont celles qui surprennent les gens :
- Les symboles de temps n'existent que comme composants de débit.
meas[m·s⁻¹]est légal ; unmeas[s]autonome ne l'est pas, et il n'existe pas de constructeur pour un tel objet. Une durée a exactement un seul foyer en Flux — le kindduration— et l'algèbre ne peut pas en isoler un second. Vous en obtenez une comme littéral de durée (7200s,500ms) ou comme différence de temps (time - time[n]) ; vous atteignez ensuite le monde des unités par le pont temporel ci-dessous, jamais par un constructeur de temps fabriqué. - Les symboles dérivés déclarent leur famille comme une puissance d'une autre. Un litre est
longueur³ avec un facteur exact vers le mètre cube ; un hectare est longueur² avec un facteur
exact vers le mètre carré. Ainsi
meas[L]etmeas[m³]sont deux tags d'une seule famille convertible, et×/÷les unifient exactement commekmetms'unifient.
#L'algèbre
± exige l'unité identique. Unités différentes, pas de conversion, pas de somme :
fluxside = unit.m(5) + unit.m(3) // meas[m] ✓
unit.m(5) + unit.ft(3) // ✗ [ErrDim] — convert first: unit.m(5) + toUnit(unit.ft(3), m)
× et ÷ composent les exposants et unifient les symboles au sein d'une famille, en repliant
le facteur exact :
fluxr = unit.km(1) / unit.m(1) // ratio — the same family cancels, ×1000 folded exactly
v = unit.m(6) / 2s // meas[m·s⁻¹] — the TIME BRIDGE: a measure ÷ a duration
L'annulation complète vous donne un simple ratio, comme il se doit.
Le pont temporel est la façon dont s entre dans un tag. Il n'y a pas de unit.s(…) par quoi
diviser — la seconde n'atteint l'algèbre que comme le kind qui la possède, duration. Le pont
fonctionne dans les deux sens : meas[u] ÷ duration → meas[u·s⁻¹] (distance ÷ temps = vitesse) et
meas[u·s⁻¹] × duration → meas[u] (vitesse × temps = distance). Les composants de temps se
canonicalisent vers s, si bien qu'un h ou un min dans un tag se normalise avant de se
compenser, et une expression dont le tag se compense en temps pur renvoie de nouveau une duration
— meas[m] ÷ meas[m·s⁻¹] est une ETA. Cette boucle fermée est ce qui rend un meas[s] isolé
inatteignable plutôt que simplement découragé.
Le mur mixte. Une dimension de marché et une unité physique ne se multiplient pas :
fluxclose * unit.kg(2) // ✗ [ErrDim] — a price is not a mass, and their product means nothing
La finance garde ses propres axes — la devise via le tag d'actif, le temps calendaire via period,
les angles via leur propre unité — et le mur entre eux est une arête énumérée, pas un accident
des règles.
#Échelles affines : le bit point / delta
C'est la partie que toute bibliothèque d'unités se trompe au moins une fois. Une température peut être un point sur une échelle (il fait 20 °C dehors) ou une différence sur cette échelle (la température a monté de 5 °C). Elles se convertissent différemment, et une seule d'entre elles peut être additionnée :
fluxt = unit.tempC(20) // meas[°C · point] — a POINT: 20 degrees Celsius
dt = unit.tempCDelta(5) // meas[°C · delta] — a DIFFERENCE of 5 degrees
warm = t + dt // meas[°C·point] ✓ point + delta = point (25 °C)
rise = t - t // meas[°C·delta] ✓ point − point = delta
t + t // ✗ [ErrDim] — "20 °C plus 20 °C" is not 40 °C. It is nothing.
hot = toUnit(t, F) // 68 °F — the affine formula: slope AND offset
dHot = toUnit(dt, F) // 9 °F — the linear formula: slope ONLY
Si cette distinction vous semble familière, c'est normal : c'est exactement la distinction
point/vecteur que l'axe des prix fait déjà (price − price = level). Le pilier units n'invente pas
un mécanisme — il réutilise celui sur lequel le langage a été bâti.
Le bit discipline aussi les fonctions qui lisent le zéro arbitraire d'une échelle :
| Opération | Sur un point | Sur un delta ou une unité linéaire |
|---|---|---|
abs, sign |
✗ [ErrDim] — ils lisent le zéro, qui est arbitraire |
✓ |
sum sur une collection |
✗ [ErrDim] |
✓ |
| un taux de variation | ✗ [ErrDim] |
✓ |
floor, ceil, round |
✓ (quantification au sein de l'échelle) | ✓ |
Pourquoi
abs(20 °C)est refusé. La valeur absolue d'une température n'est pas une température — c'est une affirmation sur la distance à un zéro que quelqu'un a choisi en 1742. Sur l'échelle Kelvin, la même expression donnerait une réponse différente, et les deux seraient « correctes ». Le compilateur refuse d'en choisir une, et vous propose la conversion qui rend votre intention explicite.
#Faire entrer et sortir les valeurs
Entrée : une méta-tête déclarée sur une entrée, vérifiée contre le catalogue.
Sortie : meas.value(x) retire le tag quand vous voulez vraiment le nombre nu, toUnit(x, u)
convertit, et meas.valueIn(x, u) fait les deux en un seul appel. Il n'y a jamais de coercition
implicite — meas[u] ≤ num est une arête avec perte, elle avertit donc et propose une
correction rapide plutôt que de jeter silencieusement l'unité qui était tout l'intérêt.
Un point affine est totalement exclu de ce palier avec perte. Un site bare-num est ambigu en
échelle pour un point — 20 °C et 68 °F sont la même température et des nombres différents — si bien
que meas[°C·point] ≤ num est un [ErrDim] dur, pas un avertissement, et un meas.value nu sur un
point est [ErrArg]. La correction rapide est valueIn, dont la signature vous force à nommer
l'échelle à la sortie : meas.valueIn(t, F) vaut 68, et le dit dans l'appel. La moitié avec perte
de la règle est la commodité ; cette moitié-ci est celle qui attrape le bug — c'est le même
argument du zéro arbitraire qui refuse abs(20 °C), appliqué à la frontière.
Le formatage est conscient de la locale et passe par les tables épinglées, si bien qu'une mesure s'affiche correctement sans que le nombre ne devienne dépendant de la locale — voir i18n.
#Ce que cela coûte et ce que cela rapporte
Cela coûte un tag sur num, un bit pour les échelles affines, et un catalogue fermé. Cela rapporte :
fluxdistance = unit.km(5) // meas[km]
bytes = unit.B(2048) // meas[B]
elapsed = 7200s // duration — two hours
speed = distance / elapsed // meas[km·s⁻¹] — a distance ÷ a duration, and the compiler knows it
budget = bytes / elapsed // meas[B·s⁻¹] — a bandwidth, correctly
wrong = distance + elapsed // ✗ [ErrDim] — caught here, not in production
Et cela se compose avec tout le reste : une colonne de mesures dans une table, une mesure dans un champ du Model, une mesure rendue par un graphique — toutes portent l'unité, et toutes refusent le même non-sens.
#Voir aussi
- Kinds — le mécanisme de tag, et le substrat affine que ceci réutilise.
- Opérateurs — l'algèbre dimensionnelle dans laquelle
meas[u]se branche. - asset & currency — l'axe de tag frère, pour les instruments et la monnaie.
- compute — les mesures en colonnes, et l'annotation frère
metric[id]. - i18n — formater une quantité pour une locale sans rendre le nombre dépendant de la locale.
asset & currency — le tag d'instrument
Un prix est un taux : tant d'unités d'une devise de cotation par unité d'un instrument de base. Une fois que vous le dites à voix haute, toute une classe de bugs devient une erreur de type — parce que « BTC coté en dollars » et « BTC coté en euros » sont alors des choses visiblement différentes, et les additionner est visiblement un non-sens.
C'est tout le pilier. Un tag d'actif structuré chevauche les kinds de dimension prix, les
opérateurs le contrôlent, et fx et money en découlent comme des versions taguées de kinds qui
existaient déjà. Zéro nouvelle sorte.
Une note sur les exemples. Flux n'a pas d'expressions-instructions, si bien qu'une expression nue n'est pas un programme. Les lignes marquées
✗sur cette page sont donc des fragments d'expression : ils existent pour montrer ce que les règles de kind refusent, pas ce que le parseur accepte. Toute ligne non marquée est une instruction légale. La notation à crochets (price[BTC,USD],fx[USD/EUR]) est la façon dont cette page écrit un kind en prose et dans les commentaires ; ce n'est pas de la syntaxe source.
#Le tag
price[B, Q] level[B, Q] pv[Q] volume[B]
| Composant | Ce que c'est | Porté par |
|---|---|---|
base B |
l'instrument | chaque kind de dimension prix — sauf pv, qui l'abandonne (délibérément) |
quote Q |
la devise dans laquelle le prix est libellé — l'unité dans laquelle la valeur est mesurée | chaque dimension contenant un prix |
venue @v |
troisième composant optionnel, sur adhésion, désactivé par défaut | les mêmes kinds, quand activé |
Les kinds sans dimension — ratio, osc, num, signal, dir — ne portent aucun tag
d'actif. Une force relative est un nombre ; elle n'appartient pas à un instrument.
Chaque composant a son propre sommet (⊤base, ⊤quote, ⊤venue), et la borne supérieure
élargit le composant qui diffère tout en préservant celui qui coïncide :
price[BTC,USD] ⊔ price[ETH,USD] = price[⊤base, USD] // the quote survives
tag ⊔ ⊤component = ⊤component // never an error
#La sûreté vient de l'algèbre, pas de la borne supérieure
C'est la décision de conception qui vaut la peine d'être comprise, parce qu'elle est contre-intuitive au premier abord : la borne supérieure est permissive (elle élargit), et la sûreté vit dans les opérateurs.
fluxbtcUsd = series("BTC-USD").close // price[BTC,USD]
ethUsd = series("ETH-USD").close // price[ETH,USD]
btcEur = series("BTC-EUR").close // price[BTC,EUR]
btcUsd + ethUsd // ✗ [ErrDim] — different bases
btcUsd + btcEur // ✗ [ErrDim] — different quotes: a dollar is not a euro
move = btcUsd - btcUsd // level[BTC,USD] ✓
± exige des tags identiques, composant par composant. Cette seule règle attrape les deux
erreurs qui comptent — mélanger les instruments, et mélanger les devises — et elle les attrape à la
compilation, là où un nombre faux et silencieux aurait autrement été produit.
L'ordre et l'égalité contrôlent de la même façon. price[BTC,USD] < price[BTC,EUR] ne compile pas.
#Division : un 2×2, et l'une de ses cases est un taux de change
fluxbtcUsd = series("BTC-USD").close
ethUsd = series("ETH-USD").close
btcEur = series("BTC-EUR").close
ethEur = series("ETH-EUR").close
same = btcUsd / btcUsd // ratio — same base, same quote
rel = btcUsd / ethUsd // ratio — base differs, quote shared: relative strength; the tag is dropped
rate = btcUsd / btcEur // fx[USD/EUR] — SAME base, quote differs: this IS an exchange rate
btcUsd / ethEur // ✗ [ErrDim] — both differ: no shared axis to cancel, so the ratio is undefined
La troisième case est l'intéressante. Divisez le même instrument coté en deux devises et
l'instrument s'annule — ce qui reste est le taux entre les devises. Flux le nomme : fx[USD/EUR].
La quatrième case n'a rien à annuler : une base différente et une quote différente ne partagent
aucun axe, si bien que la division est [ErrDim] plutôt qu'un nombre silencieux. Un ratio a besoin
d'un dénominateur commun — une quote partagée pour une force inter-bases, une base partagée pour un
taux inter-quotes — et quand ni l'un ni l'autre n'est présent, il n'y a pas de valeur défendable à
renvoyer.
Il n'y a pas de primitive fxRate(a, b). Une valeur fx est dérivée — par cette division, ou
par un flux qui se déclare source de taux de change. C'est délibéré : un taux que vous avez conjuré
à partir d'un nom de symbole est un taux que personne n'a vérifié.
#Multiplication : la conversion est une annulation d'unités
fluxbtcUsd = series("BTC-USD").close
btcEur = series("BTC-EUR").close
usdPerEur = btcUsd / btcEur // fx[USD/EUR] — derived by the division above, never conjured
eurPerUsd = btcEur / btcUsd // fx[EUR/USD] — its reciprocal
back = btcEur * usdPerEur // price[BTC,USD] ✓ — the shared quote cancels: (EUR/BTC)·(USD/EUR)
also = btcEur / eurPerUsd // price[BTC,USD] ✓ — the reciprocal converts the same way
La conversion de devise n'est pas une règle spéciale rapportée. C'est l'algèbre ordinaire des exposants, appliquée à un tag qui se trouve nommer une devise — ce qui est exactement ce qui rend difficile de se tromper.
La multiplication inter-actifs élargit plutôt que d'échouer (price[BTC,USD] × price[ETH,USD]
est un kind P² à tags élargis) : un produit de deux instruments est inhabituel mais pas dénué de
sens, et la règle qui attrape les vraies erreurs est ±, pas ×.
#Le flux monétaire : pv abandonne la base
Multipliez un prix par un volume et vous avez un flux monétaire — un montant notionnel de devise.
La base s'apparie, puis s'annule, délibérément : un flux d'argent est agnostique à la base, un
montant dans une devise plutôt qu'une quantité d'un instrument. Donc pv[Q] porte la quote
seule.
fluxbtc = series("BTC-USD")
eth = series("ETH-USD")
btcE = series("BTC-EUR")
flowBtc = btc.close * btc.volume // pv[USD] — the base pairs, then drops
flowEth = eth.close * eth.volume // pv[USD]
flowEur = btcE.close * btcE.volume // pv[EUR]
book = flowBtc + flowEth // pv[USD] ✓ — notionals in one currency compose
flowBtc + flowEur // ✗ [ErrDim] — a dollar flow is not a euro flow
Que deux flux monétaires en USD s'additionnent est le but, pas un oubli : un dollar de notionnel
BTC et un dollar de notionnel ETH sont la même dimension, et les sommer est exactement le total de
portefeuille qu'un book veut — un dollar est un dollar. L'erreur que ± attrape encore sur un flux
monétaire est le mélange de devises, jamais le mélange de bases. La discrimination par actif,
quand vous la voulez, vient de la détention du pv par actif dans un vec ou une Table indexée
sur la base — jamais d'un re-tag du flux.
Rediviser un flux est l'algèbre ordinaire des groupes, sans règle spéciale : pv[Q] ÷ volume[B] → price[B,Q] récupère le prix, et pv[Q] ÷ price[B,Q] → volume[B] récupère la taille — les mêmes
exposants P·V qui ont construit le flux, exécutés à l'envers.
#fx et money n'inventent rien
| Notation | Est en réalité |
|---|---|
fx[Q1/Q2] |
le kind ratio existant, portant une annotation de paire de devises |
money[Q] |
decimal pv[Q] — un flux monétaire en virgule fixe exacte |
Zéro nouvelle sorte, zéro nouvelle hauteur de treillis. L'annotation de paire est un quatrième axe
de tag qui vit sur ratio seul — et puisque ratio ne porte aucun tag d'actif, le plafond de
trois tags par kind est préservé.
#Venue et source
La venue est l'endroit d'où un prix provient. C'est des métadonnées par défaut — portées sur le producteur, pas dans le kind — parce que taguer chaque prix avec une bourse fragmenterait le type de chaque expression qui en touche deux, pour une sûreté que personne n'a demandée.
Open decision. La venue peut être activée comme troisième composant sur adhésion du tag, pour le cas d'arbitrage où deux prix du même instrument sur deux bourses ne doivent pas être interchangeables. Elle est conçue, et désactivée par défaut.
Reserved. pinVenue — épingler une série à une venue au niveau du type — est spécifié et inerte
en v1.
Un producteur déclare ce qu'il est :
| Kind de producteur | Signification |
|---|---|
Index |
un indice calculé, pas un instrument négociable |
Venue |
un flux de bourse |
Fx |
un flux de taux de change — la seconde façon dont une valeur fx peut naître |
Metric |
Reserved. une série hors-prix (la jonction metric[id]) |
toSource(key) est la jonction qui appose le tag d'un flux sur la série qu'il produit, et la remet
à l'hôte pour une ingestion causale en ajout seul — c'est ainsi qu'un flux externe devient une
série ordinaire, sans repaint (net).
#Travail inter-séries
fluxbtc = series("BTC-USD")
eth = series("ETH-USD")
spread = btc.close / eth.close // ratio — plottable
corr = stat.correl(returns(btc.close), returns(eth.close), 30) // osc(-1,1)
rel = series("ALT-USD").close / btc.close // relative strength
La série étrangère est alignée sur l'axe ordinal du graphique par une jointure as-of — la barre
étrangère la plus récente au moment de la barre courante ou avant. Jamais une correspondance au plus
proche, qui lirait le futur. Les trous conservent la dernière valeur connue ; avant la première
barre étrangère, la valeur est na. Le no-repaint est hérité plutôt que ré-argumenté.
#Voir aussi
- Kinds — les axes de tag et leurs deux régimes.
- Opérateurs — les règles
±,×,÷et la règle de rôle FX en entier. - units — l'axe de tag frère, pour les quantités physiques.
- compute — les colonnes taguées par actif, et les jointures qui ne peuvent pas mélanger les instruments.
- Host integration — la compilation inter-séries et le contrat as-of.
text — chaînes, texte structuré et édition
Post-v1. Le kind string et la surface str.* / fmt.* appartiennent au cœur scellé. Le codec
de texte structuré, le rendu assaini, le protocole d'édition, la coloration syntaxique, la
segmentation, le diff, la recherche et le catalogue de validateurs qui les surplombent forment une
conception additive scellée dont le déploiement suit v1.
Le texte est le substrat qu'un langage d'application généraliste ne peut éviter. Un forum a des messages, un cours a des leçons, un éditeur a un document, un formulaire a un champ dans lequel quelqu'un a tapé, et un graphique a un libellé sous un repère. Flux porte tout cela — sous un seul mur et un seul déverrouillage. Le mur est la règle A12 : pas d'expressions régulières, pas d'analyse syntaxique arbitraire. Le déverrouillage, c'est le geste qui portait déjà les codecs de feed et de formulaire, les tables de calendrier et les tables Unicode — une grammaire fixe compilée vers un kind déclaré, bornée et épinglée. Les tables Unicode sont le précédent qui compte le plus ici, car ce pilier s'appuie sur elles pour les quatre cents lignes qui suivent : la casse, la normalisation, et la segmentation qui indique à un caret où il a le droit de se tenir. Rien sur cette page n'assouplit A12 ; ce pilier est ce qui la peuple, et les deux dernières sections montrent pourquoi l'interdiction est satisfaisable plutôt que simplement restrictive.
Lire les exemples. Une ligne marquée ✗ est souvent un simple fragment d'expression — Flux n'a pas d'instructions-expressions, elle illustre donc une règle de kind plutôt qu'un programme. Les exemples positifs sont toujours des instructions légales.
#Le kind string
string est du texte UTF-8 borné et immuable : libellés, invites, messages, clés, la source
d'un document. C'est une sorte catégorielle — plate, comme color — et quatre conséquences en
découlent.
Il est hors de l'arithmétique numérique, avec exactement une surcharge. + sur deux chaînes est
une concaténation : l'unique ligne catégorielle de la table de règles de +. Rien d'autre dans
l'algèbre numérique ne touche un string.
Il n'a pas d'ordre. Il n'existe pas de string < string, car une réponse défendable exigerait
une locale, et une locale tirée de nulle part est une valeur qui diffère entre deux lecteurs.
L'égalité, elle, est admise — égalité bit à bit sur les octets UTF-8, que tout moteur calcule à
l'identique. Un ordre sensible à la locale existe, mais seulement comme combinateur nommé,
explicite et épinglé : voir i18n.
Il n'est jamais une série. Un string est consommé par les canaux qui attendent du texte — le
libellé d'un repère, le message d'une alerte, le contenu d'un nœud ui — et il n'est pas traçable.
fluxsym = "BTC-USD"
label = sym + " — " + fmt.price(close) // string + string → string
alert close cross_up ema(close, 50) "{sym} crossed its 50"
same = "a" == "b" // ✓ signal — bit-equality
"a" < "b" // ✗ [ErrDim] — `string` is never an ordered kind
plot label // ✗ [ErrPlot] — a string feeds text channels; it is not traced
Il est borné. Chaque string a un plafond de longueur — déclaré à la frontière qui le produit
(le maxLen d'un champ de saisie, le maxTextLen d'un codec) ou hérité du plafond global. Le
débordement est une troncature déterministe, et la troncature coupe à une frontière de scalaire,
jamais au milieu d'un scalaire (ce qui laisserait de l'UTF-8 invalide). C'est la discipline
vec(κ, N) appliquée au texte : la mémoire qu'une instance peut occuper est calculable avant son
exécution.
#L'unité est le scalaire Unicode
len, slice, indexOf et split comptent et indexent en scalaires Unicode (points de code).
Jamais un octet. Jamais une unité de code UTF-16.
Pourquoi cette règle existe. L'interpréteur s'exécute sur la plateforme, et le module compilé s'exécute sur sa propre mémoire UTF-8. Si l'interpréteur déléguait
lenà la longueur de chaîne de la plateforme, il compterait des unités de code UTF-16, et le module compilé compterait des scalaires. Les deux s'accordent sur l'ASCII et divergent sur le premier caractère au-delà : un libellé portant un symbole monétaire, un nom accentué, un titre CJK ou un emoji aurait une longueur ici et une autre là. L'identité octet à octet (invariant I7) est la propriété qui permet à un script d'être réexécuté et digne de confiance ; elle ne peut survivre à une APIstringqui signifie deux choses différentes. Doncstr.*ne délègue jamais aux méthodes de chaîne de la plateforme — elle exécute la même routine épinglée des deux côtés.
Trois unités existent dans ce pilier, elles ne sont pas interchangeables, et la différence entre elles n'est pas académique :
| Couche | Unité | Où vous la rencontrez |
|---|---|---|
| Stockage | octet UTF-8 | invisible pour le programme ; le seuil de petite chaîne est un détail de stockage |
Le kind string |
scalaire Unicode | len, slice, indexOf, split, rep ; troncature au plafond |
| Édition et affichage | grappe de graphèmes | Caret.off, Sel, truncate, Tok.start/len, longueurs de diff, portées de surlignage |
| Texte | Octets | Scalaires | Graphèmes |
|---|---|---|---|
abc |
3 | 3 | 3 |
é en un seul point de code |
2 | 1 | 1 |
é en e plus un accent aigu combinant |
3 | 2 | 1 |
| un emoji de famille à liant sans chasse (ZWJ) | 25 | 7 | 1 |
Le scalaire est l'unité canonique du kind — celle sur laquelle deux moteurs doivent s'accorder. Le graphème est l'unité de l'utilisateur — ce qu'une personne appelle « un caractère », et donc ce dont un caret doit avancer d'un pas : avancer par scalaires découperait cet emoji de famille en quatre personnes et trois liants, et laisserait un point d'insertion atterrir entre une lettre et son accent. Les deux sont épinglés ; ni l'un ni l'autre n'est celui de la plateforme.
#str.* et fmt.*
| Fonction | Notes |
|---|---|
len, slice, indexOf, contains, startsWith, endsWith |
indexées par scalaire ; slice est bornée |
split(s, sep, maxParts) |
le nombre de parts est déclaré, donc la longueur du résultat est repliée en constante |
trim, pad, padStart, padEnd, rep(s, n) |
le compte de rep est un littéral — la longueur de sortie est connue à la compilation |
replace(s, from, to) |
remplacement littéral, borné — pas un motif |
upper, lower |
invariantes à la locale, via la table de casse Unicode épinglée |
normalize(s, form) |
NFC / NFD, sur la même table scellée |
truncate(s, n) |
sûr au graphème — ne coupe jamais une grappe en deux |
graphemes(s), graphemeAt, graphemeSlice |
la surface de segmentation (ci-dessous) |
fmt.num, fmt.price, fmt.pct, fmt.time |
le formateur canonique épinglé |
fmt.cat |
la concaténation vers laquelle se désucre un littéral interpolé |
#Le formateur épinglé
fmt.num/price/pct/time sont une seule routine canonique, partagée octet pour octet par
l'interpréteur, le module compilé et le serveur. Elles ne sont jamais la conversion nombre-vers-chaîne
de la plateforme.
Pourquoi cette règle existe. Deux moteurs sont en désaccord sur les nombres-en-texte de façons que personne ne remarque jusqu'à ce qu'un golden échoue : combien de décimales un
f64rend par défaut, dans quel sens le dernier chiffre s'arrondit, et à quelle magnitude la sortie bascule en notation scientifique. Un seul chiffre divergent dans un libellé est une sortie divergente, et l'oracle d'identité octet à octet échouerait alors sur chaque script qui imprime un nombre — ce qui est presque tous. Le formatage de texte est tenu au même standard que les fonctions transcendantes : une routine épinglée, un golden, aucune exception.
upper et lower sont invariantes à la locale pour la même raison. C'est une limite délibérée, pas
un oubli : la casse sensible à la locale appartient à i18n, où la locale est un argument
explicite et les tables sont versionnées.
#Interpolation
Un { à l'intérieur d'un littéral de chaîne ouvre un trou contenant une expression Flux
complète. Le littéral se lexe en jetons-fragments et l'analyseur y intercale les expressions ;
l'AST est un fmt.cat des fragments, chaque trou étant désucré via fmt.* selon son kind. Un
libellé est donc dynamique sans aucune API de construction de chaîne :
fluxsym = "BTC-USD"
stamp = fmt.time(time, "HH:mm")
mark close cross_up ema(close, 50) "{sym} crossed at {fmt.price(close)}"
Les accolades littérales s'échappent en \{ et \}, et les deux délimiteurs — "…" et '…' — se
comportent à l'identique ; les détails au niveau du jeton sont dans la
structure lexicale.
#Mémoire : petites chaînes, l'arène et la promotion
Presque toute chaîne d'une application est courte — un libellé, un prix formaté, une clé, une invite. Celles-là vivent en ligne dans la valeur elle-même (l'optimisation des petites chaînes) et coûtent zéro allocation. Les plus longues vont dans une arène bump réinitialisée une fois par tick d'évaluation — par barre, par frame. Il n'y a pas de ramasse-miettes : Flux est pur et ses durées de vie sont bornées, si bien que la réinitialisation de l'arène est la désallocation.
La concaténation fusionne. La ligne ci-dessous ne construit pas trois intermédiaires : elle compile vers un seul calcul de longueur et une seule écriture d'arène — le patron string-builder, rendu invisible par la pureté et l'élimination des sous-expressions communes.
fluxdef tag(c) = "px " + fmt.price(c) + " @ " + fmt.time(time, "HH:mm")
record Model { last: string ; n: num } // `last` outlives the tick → promoted out of the arena
Promotion. Une chaîne qui survit à son tick est matérialisée hors de l'arène : copiée en
mémoire à durée de vie de nœud ou de Model, jamais laissée sous forme de vue. Trois choses la
déclenchent — la capture par un scan ou par un nœud à état, un champ de Model, et un checkpoint.
Pourquoi la promotion n'est pas un détail d'optimisation. Un checkpoint qui stockerait une vue-tranche dans une arène par tick, une fois cette arène réécrite un millier de fois, restaurerait ce qui se trouve par hasard à ces offsets. Rembobiner en arrière dans une session produirait un texte différent à chaque tentative, et le rejeu ne serait pas exact au bit près. La copie à la promotion est ce qui réconcilie « sans déchet » avec « rejouable » — deux propriétés que ce langage refuse d'échanger l'une contre l'autre.
#Texte structuré — le codec Md
Les documents de classe Markdown entrent par un codec, non par un analyseur. Ce que la
grammaire admet (close et versionnée comme md-v1, un sous-ensemble strict de CommonMark) : titres
ATX 1–6 · paragraphes · emphase et gras · code inline · blocs de code clôturés et indentés · citations
à imbrication bornée · listes ordonnées et non ordonnées à imbrication bornée · ruptures thématiques ·
liens · images · tables à colonnes bornées · sauts durs.
Exclu de façon permanente : le passe-plat de balisage brut. Il n'existe aucune production pour lui, et l'assainisseur ci-dessous ne l'accepterait pas s'il y en avait une.
Post-v1. Les notes de bas de page et les listes de définitions ne sont pas des exclusions
permanentes — elles sont nommées pour une version de grammaire ultérieure, additive (md-v2),
arrivant comme une nouvelle version épinglée avec son propre golden, exactement comme le fait un bump
de table.
La sortie est un arbre en pool de nœuds borné, pas un kind récursif :
fluxvariant MdNode {
Doc | Heading(level: num) | Para | Em | Strong | Code | CodeBlock(lang: string)
| Quote | List(ordered: signal) | Item | Link(href: string) | Image(ref: string)
| Table | Row | Cell | Text(s: string) | Break | Rule
}
Le document est un Tree(MdNode, N) — le pool de nœuds de collections. Les
plafonds sont déclarés au site de décodage, et le débordement est une troncature bornée avec un
diagnostic, la même discipline que vec(κ, N) :
fluxMD_CAPS = { maxNodes: 2000, maxDepth: 8, maxTextLen: 4000 }
def article(body) = md.parse(body, MD_CAPS) // → Tree(MdNode, N)
Deux points d'entrée, une seule routine épinglée. Md dans le catalogue de codecs de
net décode un corps récupéré directement vers l'arbre à la frontière ; md.parse(s, caps)
fait de même dans le script, ce dont un éditeur a besoin pour prévisualiser un brouillon vivant dans
le Model. Ce sont la même routine — une source de vérité unique, interpréteur ≡ module compilé, avec
un golden par version de grammaire.
Pourquoi c'est un codec et non un analyseur. Un analyseur est un programme qui s'exécute sur des données, et son coût est fonction des données. Un codec est une projection dans un kind déclaré : la grammaire est fixée avant que le programme ne s'exécute, la profondeur et le nombre de nœuds sont plafonnés au site d'appel, et le travail est donc borné par des nombres que le compilateur peut lire. La distinction est exactement ce qui fait survivre la totalité au contact du texte. C'est aussi pourquoi décoder une charge utile n'apparaît jamais dans votre code comme de l'analyse — voir la dernière section.
Un lien est une donnée. Link(href) porte un string, et un string n'est pas une autorité. Un
href devient navigation, et un ref devient un chargement, seulement à la frontière de rendu,
sous la politique de l'hôte. Un arbre arrivé du réseau ne peut atteindre quoi que ce soit par
lui-même.
#Le rendu assaini
richText(ast) -> ui est une primitive rendue par l'hôte dans le catalogue clos ui : l'hôte
parcourt l'arbre et rend uniquement des constructions vérifiées.
| Nœud | Ce que fait l'hôte |
|---|---|
| suites de texte | insérées comme contenu texte, jamais comme balisage ; typographie issue des tokens |
Link(href) |
une ancre vérifiée par l'hôte : les routes internes se résolvent via la liste blanche de navigation ; un href externe reçoit l'affordance de lien externe de l'hôte et s'ouvre à travers l'hôte |
Image(ref) |
résolue uniquement via asset:load (liste blanche plus quota), ou abandonnée avec un substitut et un diagnostic |
CodeBlock(lang) |
colorée (ci-dessous) |
| un nœud malformé | rejeté |
Pourquoi les images passent par la politique d'assets. Un document récupéré qui pourrait pointer un pixel en hotlink serait une balise de pistage, et le lecteur n'aurait aucun moyen de le savoir. Faire passer chaque image par la liste blanche signifie qu'un document ne charge que ce que la politique d'assets de l'application admet déjà — le réseau ne peut introduire une nouvelle origine en l'écrivant dans un lien.
Il n'y a pas de « classe de nœud inconnue » en transit : MdNode est un variant clos produit par
une routine épinglée, si bien que l'assainisseur ne devine jamais une balise étrangère — il ne juge
que le malformé, ce qui est un travail bien plus petit et bien plus décidable. Un conteneur prose
enveloppe alors la sortie long-format avec une mesure de largeur de lecture et des tokens de rythme
vertical ; les pages de cours, les corps de documentation et les messages de forum en sont les
consommateurs.
Décision ouverte. Les href relatifs à l'intérieur d'un document récupéré : les résoudre contre l'origine du feed, ou les interdire purement. Le plan laisse la question ouverte.
#Le protocole d'édition
Un éditeur de texte riche est un widget du catalogue ui, mais la partie intéressante n'est pas le
widget — c'est le protocole en dessous, conçu une fois, ici, pour que l'édition soit exacte au
rejeu quoi que fasse la pile d'entrées de l'hôte. Les positions sont des données, et elles sont
sûres au graphème :
fluxrecord Caret { node: num ; off: num } // `off` counts GRAPHEME CLUSTERS
record Sel { anchor: Caret ; focus: Caret }
Les éditions sont des messages. Le widget de l'hôte les livre via les constructeurs que
l'application a déclarés — la même dérogation OnX(args, C) qu'utilise chaque événement d'hôte :
fluxrecord Caret { node: num ; off: num }
record Sel { anchor: Caret ; focus: Caret }
variant EditOp { Insert(at: Caret, s: string) | Delete(r: Sel) | Replace(r: Sel, s: string)
| SetSel(r: Sel) | SetMark(r: Sel, m: MarkKind) }
variant MarkKind { Em | Strong | Code | Link(href: string) }
Un seul réducteur les applique. text.apply(doc, op) -> doc est une unique fonction épinglée et
totale — et la totalité tient au plafond, pas en dessous : aucune entrée, et aucune séquence
d'entrées, ne l'empêche de retourner un document.
| Situation | Comportement |
|---|---|
| une position hors de la plage valide | bornée (clamp) à la plage |
| une opération entièrement hors du document | no-op plus un diagnostic (le précédent vec.setAt) |
| une édition franchissant des frontières de nœuds | le pool de nœuds se scinde et fusionne de façon déterministe |
Replace(r, s) |
exactement Delete(r) puis Insert(…, s) — il hérite donc des deux disciplines |
SetMark sur une partie d'une suite de texte |
la suite se scinde aux bords de la sélection |
une édition qui déborderait le plafond N du pool |
no-op plus un diagnostic — jamais un dépassement |
La composition IME n'entre jamais dans le journal. Tant qu'une composition est en cours, ses
états intermédiaires sont locaux à la présentation — entrée de classe continue, la même classe
que la position en vol d'un glisser ou qu'un offset de défilement. Seul le texte validé atterrit,
sous forme de message Insert ou Replace.
Pourquoi le journal ne voit que les validations. Les moteurs de saisie (IME) diffèrent — les mêmes frappes produisent des chaînes candidates intermédiaires différentes selon les plateformes et les versions. Si ces intermédiaires étaient journalisés, une session enregistrée sur une machine ne se re-replierait pas sur une autre, et l'annulation parcourrait des états candidats qu'aucun utilisateur n'a jamais choisis. Journaliser la validation rend le rejeu exact à l'octet à travers les moteurs IME, et donne à l'annulation le sens qu'un rédacteur en attend.
L'annulation est le journal de l'application — ses bornes et sa coalescence, rien d'autre. Il n'y
a pas de seconde pile d'annulation à l'intérieur de l'éditeur, et c'est pourquoi l'annulation ne peut
ressusciter une sélection périmée : le sous-record doc du Model est versionné par l'historique, et
son sous-record ui ne l'est pas.
fluxvariant Msg { Edit(op: EditOp) | Move(r: Sel) | Undo }
app notes {
capabilities: [ storage:own, journal ]
init(p) = { doc: md.parse(p.seed, MD_CAPS), ui: { sel: p.sel } } // MD_CAPS: above
update(m, msg) = match msg {
Edit(op) -> { model: m with { doc: text.apply(m.doc, op) }, cmds: [] }
Move(r) -> { model: m with { ui: m.ui with { sel: r } }, cmds: [] }
Undo -> { model: m, cmds: [ Journal(UndoToMark) ] }
}
view(m) = prose { richText(m.doc) }
subs(m) = []
}
Move n'écrit que dans ui, si bien qu'un déplacement de caret n'est pas une étape annulable ;
Edit écrit dans doc, donc ç'en est une. Le partitionnement est la sémantique d'annulation. Un
simple textarea multiligne est le même protocole moins SetMark.
#Coloration syntaxique
fluxrecord Tok { start: num ; len: num ; class: TokClass } // start/len in GRAPHEME clusters
variant TokClass { Kw | Ident | Num | Str | Comment | Op | Punct | Plain }
TXT_CAPS = { maxTextLen: 4000 }
def toksOf(src) = hl.tokens("flux", src, TXT_CAPS) // vec(Tok, N)
Les grammaires sont un catalogue clos, exactement comme les codecs : tokeniseurs à passe unique
bornés, épinglés et versionnés par langage. L'ensemble initial est flux, json, js,
html-escaped, md. codeBlock(lang, text) -> ui rend les classes via les tokens du thème.
Un lang inconnu se rend en texte brut avec un diagnostic — jamais une supposition. La détection
par heuristique est un non-but : elle ferait dépendre le rendu d'un document d'un classifieur, et un
classifieur est exactement le genre de chose qui change d'avis entre deux versions.
Décision ouverte. Quels langages le catalogue adopte après l'ensemble validé ; le plan liste des candidats sans trancher.
#Segmentation Unicode
Les tables sont épinglées et versionnées, et elles sont le fondement sur lequel repose le modèle d'édition :
| Table | Vous donne |
|---|---|
| Grappes de graphèmes (UAX #29) | str.graphemes, graphemeAt, graphemeSlice ; arithmétique de caret ; truncate |
| Rupture de mot et de ligne (UAX #29 / #14) | saut de mot pour le caret ; indices de retour à la ligne pour le moteur de rendu |
| Casse et normalisation | upper, lower, normalize (NFC / NFD) — une table scellée, deux usages |
La routine est partagée entre l'interpréteur et le module compilé, et elle n'est jamais le segmenteur de la plateforme. C'est de nouveau l'argument du formateur : une table de plateforme est une cible mouvante qui expédie au calendrier de la plateforme, et deux moteurs sur deux versions seraient alors en désaccord sur l'endroit où un caret a le droit de se tenir. Une table épinglée avec un numéro de version est une table que vous pouvez mettre dans un golden.
La troncature sûre au graphème supplante la troncature au scalaire pour tout ce qu'une personne lit ; la troncature au scalaire reste la règle au plafond du kind, car cette frontière concerne la validité de stockage — ne jamais scinder un scalaire, ne jamais émettre d'UTF-8 invalide — plutôt que ce qu'un lecteur voit.
#Diff et patch
fluxvariant Edit { Keep(len: num) | Ins(s: string) | Del(len: num) } // lengths in GRAPHEME clusters
edits = txt.diff(prev, next, 400) // vec(Edit, K) — bounded by the declared maxD
back = txt.patch(prev, edits) // total; `back == next` when the diff was exact
L'algorithme est celui de Myers, borné par un maxD déclaré. Au-delà de cette borne il n'échoue
pas et ne s'exécute pas plus longtemps : il retombe sur un résultat grossier dans le même
variant — un Del de l'ancien texte, un Ins du nouveau — avec un diagnostic. txt.diff est donc
total par construction, et chaque consommateur ne gère qu'une seule forme. txt.patch est total.
txt.diffLines(a, b, maxD) partage la routine à la granularité de la ligne.
Le départage à l'intérieur de la recherche de plus longue sous-séquence commune est un choix canonique unique, épinglé, avec un golden — car deux diffs également bons sont deux sorties d'octets différentes, et l'identité octet à octet n'accepte pas « également bon ». Les consommateurs sont les ordinaires : historique de révisions, la gouttière « modifications », et la réconciliation d'UI optimiste quand la réponse du serveur arrive.
#La pile de recherche
La recherche se compose sur collections ; ce pilier fournit les pièces côté texte, toutes bornées et toutes épinglées :
| Pièce | Signature | Notes |
|---|---|---|
| Tokeniseur | search.tokens(s, caps) -> vec(string, N) |
frontières de mot UAX #29 plus tables de mots vides épinglées |
| Correspondance floue | search.fuzzy(q, s, maxDist: lit) |
Levenshtein borné → record{ hit: signal ; dist: num } |
| Sous-séquence | search.subseq(q, s) |
la correspondance de palette de commandes → record{ hit: signal ; score: num } |
| Recherche par préfixe | m.range(lo, hi, k: lit) |
un balayage de plage sur le Map ordonné — aucun kind trie n'existe, et aucun n'est nécessaire |
| Classement | search.bm25(postings, stats, q, k: lit) |
score déterministe, départage stable par identifiant de document |
| Portées de surlignage | search.spans(q, s, caps) |
portées indexées au graphème, alimentant le rendu |
Les index sont des collections bornées ordinaires, et un index est un record ordinaire qui les contient — trois champs, un par question que vous pouvez lui poser :
| Champ | Kind | Répond |
|---|---|---|
terms |
Map(Token, Set(DocId, D), T) |
l'appartenance, et — parce que le Map est ordonné — le préfixe |
postings |
Map(Token, vec(record{ doc: DocId ; tf: num }, N), T) |
quels documents, et à quelle fréquence |
stats |
record{ n: num ; avgdl: num ; docLen: Map(DocId, num, D) } |
les normes du corpus par lesquelles le classeur divise |
Les deux appels bornés prennent leur plafond de résultat comme argument nommé k, exactement
comme le fait le range du Map lui-même dans collections — le compte est un
plafond déclaré sur la réponse, non un nombre positionnel de plus à mal compter :
fluxdef query(idx, q) = {
hits: search.bm25(idx.postings, idx.stats, q, k: 20), // vec(record{ doc, score }, 20)
head: idx.terms.range("flu", "flv", k: 10) // the typeahead window, O(log N + k)
}
Les tables de mots vides sont par locale, avec en comme base — le même traitement uniforme que
i18n accorde à chaque table. Un stemmer existe comme variant explicite et épinglé,
désactivé par défaut : la racinisation change ce que signifie une requête, et cela devrait être
une décision plutôt qu'un défaut.
Décision ouverte. Les langages de stemmer au-delà des deux premiers, et si les tables de mots vides appartiennent à ce pilier ou à i18n.
#Validateurs — le catalogue qui remplace les motifs
Chaque validateur est un prédicat sur une grammaire fixe, et leur ensemble est un catalogue clos — jamais un motif que l'appelant fournit.
| Validateur | Grammaire |
|---|---|
valid.isEmail(s) |
la grammaire d'e-mail WHATWG |
valid.isUrl(s) |
la grammaire d'URL (celle-là même qu'utilise le codec URL) |
valid.isPhone(s) |
la forme E.164 |
valid.luhn(s) |
la somme de contrôle |
valid.isSlug(s) |
la forme de slug |
valid.inRange(x, lo, hi) |
une borne numérique |
valid.matches(s, fmt) |
un format nommé, tiré d'un variant clos |
fluxvariant DateFmt { Iso8601 | Rfc3339 | Ymd | Dmy | Mdy }
variant NamedFormat { Date(f: DateFmt) | Hex | Base64 | Uuid | Iban }
variant Rule { Format(NamedFormat) | Email | Url | Phone | Luhn | Slug | InRange(lo: num, hi: num) | Required }
DateFmt est lui-même une énumération close de formes de date épinglées — jamais une chaîne de
motif fournie par l'utilisateur, ce qui serait un langage de motifs introduit en fraude par un
paramètre. Le catalogue croît par addition : une nouvelle entrée est une nouvelle routine
épinglée avec un golden. Il ne fait jamais croître une grammaire d'exécution.
fluxvalid.matches(s, "^[a-z]+$") // ✗ [ErrArg] — `matches` takes a NamedFormat, not a pattern
re.match("(a+)+b", s) // ✗ [ErrUnbound] — no such name: there is no regex engine
Les formulaires sont validés champ par champ. valid.form(form, rules) prend un record dont les
champs parallèlent ceux du formulaire, chacun portant une Rule, et retourne un record qui les
parallèle à nouveau, chaque champ étant un Ok ou un Err. Required est une vérification de
présence — le champ est non-na et non vide — et il est évalué avant le prédicat de valeur, si
bien qu'un champ manquant signale « manquant » plutôt que « malformé ». Chaque autre branche nomme
l'un des prédicats ci-dessus. Une Rule est une donnée, pas une valeur-fonction : le descripteur
est clos, ce qui garde intacte la discipline sans-flèche et permet à l'éditeur d'afficher l'ensemble
de règles sous forme de table.
fluxvariant Check { Ok | Err(reason: string) }
def errText(v) = match v { Ok -> "" ; Err(r) -> r } // renders one field's verdict
def validate(form) =
let rules = { email: Email, age: InRange(13, 120) } in // one Rule per field
valid.form(form, rules) // { email: Ok, age: Err(reason) }
note = errText(Err("too young")) // the message a field renders under itself
Le record par champ est exactement ce qu'un champ de saisie ou un widget de formulaire consomme pour rendre sa propre erreur — c'est pourquoi la validation n'a jamais besoin d'un canal latéral.
#Ce qui est exclu, et pourquoi l'interdiction est satisfaisable
Il n'y a pas d'expressions régulières. Pas « déconseillées » — aucun nom du langage n'en évalue une.
Pourquoi elles sont exclues. Une expression régulière est un calcul non borné décrit par des données. Son coût n'est pas fonction du plafond déclaré de l'entrée mais d'un motif qui arrive à l'exécution, et le pire cas d'un moteur à retour arrière est catastrophique sur des entrées d'apparence ordinaire. Un langage dont la promesse centrale est que tout programme termine dans un budget que le compilateur peut énoncer ne peut admettre une construction dont le budget est écrit par quiconque fournit le motif. C'est la même raison pour laquelle il n'y a pas de
filterqui rétrécit un vecteur ni de file non bornée : les exclusions ne sont qu'une seule exclusion, appliquée avec cohérence.
Une interdiction n'est honnête que si le travail qu'elle proscrit peut encore être fait. Celle-ci est remplacée des deux côtés à la fois :
- La validation est le catalogue nommé, borné et déterministe ci-dessus. Vous n'écrivez pas un motif pour une adresse e-mail, une URL, un UUID ou un IBAN — vous nommez le format, et la grammaire derrière le nom est fixe, épinglée et testée au golden. Si un format manque, la réponse est une entrée de catalogue, pas un langage de motifs.
- La structure n'a jamais besoin d'analyse, car elle arrive déjà typée. Une charge utile est
décodée contre le schéma que l'application a déclaré (
Json(Trade),Md,Csv, un formulaire URL), et un champ requis cassé fait surface commeDecodeError(field, reason)— jamais unnasilencieux qui empoisonne un calcul trois sauts plus loin. Voir net.
Entre elles, les cas qui atteignent d'habitude un motif — « est-ce une adresse valide », « extraire les champs de ce corps », « vérifier la forme de cet identifiant » — sont couverts par des constructions dont le coût est un nombre écrit dans la source.
La casse et le collationnement dépendants de la locale sont également exclus du cœur. upper et
lower sont invariantes à la locale, et il n'y a pas de < sur les chaînes. Ce travail existe — avec
une locale explicite et des tables épinglées — dans i18n, pour qu'une valeur
calculée ne dépende jamais de qui la lit.
Décision ouverte. Une porte est laissée entrouverte, et le plan la décrit sans la franchir : un
motif qui est un littéral à la compilation pourrait être compilé au build en un automate épinglé
(temps linéaire, pas de retour arrière, taille plafonnée) — auquel point ce n'est pas une expression
régulière au sens d'A12 mais du sucre par-dessus la discipline de grammaire fixe, car le motif
constant est une grammaire fixe et l'automate est la routine épinglée. Si un consommateur nommé le
justifiait un jour, il arriverait sous la forme re.match(litPattern, s) et re.find,
littéral-seulement à jamais. Un motif qui arrive à l'exécution, ou par des données, reste exclu de
façon permanente. C'est le mur réel.
#Voir aussi
- Kinds — la sorte
string, l'égalité bit à bit, et pourquoi il n'y a pas d'ordre. - Structure lexicale — les littéraux de chaîne et les jetons d'interpolation.
- collections —
Tree,MapetSet: le pool de nœuds et les index sur lesquels ce pilier se compose. - i18n — le rendu sensible à la locale, la sélection de pluriel, et le seul ordre de chaînes qui existe.
- net — le catalogue de codecs, et le décodage typé par schéma à la frontière.
i18n — locales, messages, collationnement
Post-v1. Le pilier i18n est une conception additive scellée. Il ouvre la jonction du catalogue de messages que le plan APP tient en réserve, et n'ajoute au langage aucune sorte, aucun symbole de grammaire et aucune nouvelle flèche.
Une application qui parle une seule langue est un prototype. La faire parler plusieurs langues est d'ordinaire l'endroit où une base de code acquiert sa classe de bugs la plus durable : un nombre qui se lit différemment selon qui le regarde, un pluriel correct dans la langue à laquelle le développeur pensait, un ordre de tri qui change quand une traduction est livrée, un nom de droite à gauche qui réordonne la ponctuation autour de lui.
Flux prend tout cela au sérieux et refuse exactement une chose : il ne laissera rien de tout cela atteindre une valeur calculée. Une locale décide comment un nombre est rendu et comment deux noms sont ordonnés. Elle ne décide jamais ce qu'un nombre est. Tout sur cette page découle de cette phrase.
Lire les exemples. Deux conventions sont en usage ci-dessous, et toutes deux sont sanctionnées. Une ligne marquée ✗ est souvent un simple fragment d'expression — Flux n'a pas d'instructions-expressions, elle illustre donc une règle de kind plutôt qu'un programme. Et un membre d'un bloc
app(update,view,subs) est parfois montré seul, puisqu'un membre n'est légal qu'à l'intérieur de son bloc. Tout le reste est une instruction complète qui s'analyse telle quelle.
#Une locale est une valeur
locale est une clé de chaîne opaque — "fr", "en-GB", "ar" — livrée par l'hôte comme entrée
explicite : une entrée de plan APP, ou une entrée épinglée dans le contexte de rejeu. Ce n'est
jamais un défaut ambiant par visiteur qu'un calcul pourrait aller chercher.
fluxloc = input("en", title: "Locale") // explicit, and pinned into the replay input set
def caption() = fmt.duration(4800000, loc) // a RENDERING — "1 hr 20 min" · "1 h 20 min"
plot ema(close, 20) // the SERIES cannot depend on a locale at all
Le nombre de cette seconde ligne est le même nombre dans chaque locale. Seule la chaîne change.
Pourquoi la locale n'est jamais ambiante. C'est la règle du fuseau horaire, mot pour mot. Un accesseur de calendrier vit dans le plan ANALYSE, si bien que si le fuseau du graphique était un réglage ambiant par visiteur, l'auteur, le module compilé et le serveur qui réexécute le script calculeraient chacun un
dayOfWeekdifférent pour la même barre — et la sortie d'un script dépendrait de qui l'a ouvert. Épingler les tables ferme la dérive ; cela ne ferme pas la question de quel fuseau le défaut résout. Donc le fuseau est soit une entrée explicitement épinglée, soit un argument explicite. Une locale est le même genre de danger avec un rayon d'impact plus large, et elle reçoit la même réponse : une valeur explicite, dans l'ensemble d'entrées de rejeu, ou rien.
La négociation de locale — les préférences d'un visiteur face aux locales qu'une app déclare — est de l'habillage hôte. L'application n'exécute jamais cet algorithme ; elle reçoit la valeur résolue. Et pour tout ce qui est scoré, rejoué ou vérifié, la locale résolue est figée dans les entrées de rejeu aux côtés de la graine et des versions de tables.
#Les tables sont épinglées
Chaque comportement dépendant de la locale lit un sous-ensemble CLDR épinglé et versionné. Pas la bibliothèque d'internationalisation de la plateforme — une table avec un numéro de version qui est livrée à l'intérieur du build.
| Table | Décide | Lue par |
|---|---|---|
| Règles de pluriel | quelle variante d'un message un compte sélectionne | t, fmt.relTime, fmt.duration |
| Symboles numériques | séparateur décimal, séparateur de groupes, mise en forme des chiffres | fmt.num, fmt.pct, fmt.price |
| Patrons de date/heure | ordre des champs, noms de mois et de jours | fmt.time |
| Patrons de listes | « a, b and c » contre « a, b et c » | fmt.list |
| Adaptations de collation | l'ordre de deux chaînes dans une locale | coll.sort, coll.topK, coll.fold |
| Métadonnées de script et RTL | la direction de base d'une locale | textDir, la mise en page |
| Bidi (UAX #9) | comment les suites de direction mixte sont réordonnées | le moteur de rendu de l'hôte |
Un bump de table est une nouvelle version épinglée, qui est un nouveau hash de build. Ce n'est jamais une dérive silencieuse sous une application figée — le mode de défaillance où une mise à jour de bibliothèque de routine change discrètement ce qu'une app rend, ou comment elle trie, n'a aucun moyen de se produire ici.
La livraison est scindée, une seule fois. Le sous-ensemble en est embarqué dans le runtime ;
le sous-ensemble de toute autre locale est un asset de bundle paresseux et versionné, chargé par
locale déclarée. C'est délibérément différent de la base de fuseaux horaires, qui est embarquée en
entier : seul en a ce statut de plateforme, et la section suivante dit exactement pourquoi, et
exactement jusqu'où va ce privilège.
#Le catalogue de messages
La limite de v1 — les chaînes d'application vivent dans la source — est levée par une jonction qui garde les chaînes entièrement hors du script.
- L'application déclare des clés. L'hôte détient le catalogue : une table
(appId, locale) → { key → message }, rédigée aux côtés de l'app et livrée via le mécanisme de bundle d'assets. Les catalogues se versionnent donc avec l'app, se mettent en cache hors ligne, et n'entrent jamais dans le script sous forme d'octets. - La capabilité
i18n:catalogueaccorde deux choses :t(key: string, args: record) -> string, résolue côté hôte (sélection et formatage), etSub OnLocale(C).
fluxapp reader {
capabilities: [ i18n:catalogue ]
init(p) = { locale: p.locale, unread: 3 }
update(m, msg) = match msg {
Locale(l) -> { model: m with { locale: l }, cmds: [] }
}
view(m) = col { text(t("inbox.unread", { count: m.unread })) }
subs(m) = [ OnLocale(Locale) ]
}
Le script nomme une clé et transmet des arguments. Il ne voit jamais un message, n'en concatène jamais un, et n'en analyse jamais un.
Pourquoi le catalogue est détenu par l'hôte. Un script qui porterait ses propres chaînes devrait en faire quelque chose — sélectionner une forme de pluriel, interpoler un argument, choisir une variante genrée — et cela veut dire analyser un format de message à l'exécution, ce qu'A12 interdit pour la même raison qu'elle interdit toute autre grammaire d'exécution. Remettre le catalogue à l'hôte déplace l'analyse au temps de chargement, via une grammaire fixe et épinglée, une seule fois. Ce que le script détient est une clé : une valeur sans structure à interpréter.
Les deux modes de défaillance sont totaux. Ni l'un ni l'autre ne lève d'exception, et ni l'un ni l'autre ne retourne une chaîne vide :
| Défaillance | Comportement |
|---|---|
| Clé manquante | la chaîne de repli déclarée (fr-CA → fr → en), puis la clé elle-même, verbatim, plus un diagnostic |
Placeholder sans champ args correspondant, ou une valeur de mauvais kind |
le jeton de placeholder littéral est rendu, plus un diagnostic |
Champs args en trop |
ignorés |
Là où le catalogue est disponible au build — une app first-party, ses propres chaînes — l'ensemble
de placeholders est vérifié à la compilation contre chaque site d'appel t(key, args), et un
placeholder inconnu est [ErrInput] avant même que l'app ne s'exécute. Les bundles tiers qui
chargent paresseusement retombent sur la règle d'exécution ci-dessus. La coquille d'un traducteur
dégrade un libellé ; elle ne fait pas tomber une vue.
#Un message est un arbre de sélection, pas une concaténation
Les messages utilisent MessageFormat 2 : déclarations, sélecteurs .match pour la sélection de
pluriel, d'ordinal et générale — le genre vit ici — et placeholders avec fonctions de formatage. Un
message est écrit par un traducteur et ressemble à ceci, dans le catalogue, jamais dans votre
source :
.input {$count :number}
.match $count
one {{You have {$count} new message.}}
* {{You have {$count} new messages.}}
Pourquoi la sélection ne peut se faire en collant des chaînes ensemble. « You have {n} new messages » n'est pas un message avec un trou dedans. En anglais, ce sont deux formes. Dans d'autres langues, ce sont trois, quatre ou six, et laquelle s'applique est fonction du nombre qu'aucune application ne devrait encoder. Le genre est pire : ce n'est pas un préfixe que l'on peut concaténer, car dans de nombreuses langues il change l'accord sur toute la phrase. La concaténation force chaque traducteur dans la grammaire de la langue dans laquelle le code a été écrit, et produit un texte correct nulle part ailleurs. Mettre la sélection à l'intérieur du message — dans le catalogue, là où le traducteur travaille — permet de restructurer un message pour sa langue sans qu'une seule ligne de code change.
Trois propriétés gardent ceci déterministe :
- La sélection lit les règles de pluriel épinglées pour la locale du message. Les catégories sont
zero one two few many other; lesquelles une locale utilise réellement est l'affaire de la table, et la table a une version. - Les fonctions de formatage délèguent aux routines
fmt.*épinglées. Il y a une seule source de vérité de formatage dans tout le système ; MessageFormat ne fait jamais croître un second formateur de nombre ou de date qui pourrait arrondir différemment du premier. - La grammaire de message est fixe — conforme à A12, comme chaque codec — et elle est analysée côté hôte au chargement du catalogue. Un message malformé est un diagnostic au chargement avec un repli clé-verbatim, non une surprise à l'exécution.
#Formatage sensible à la locale
fmt.* gagne une locale explicite. Les formes invariantes à la locale demeurent, et elles
restent le défaut dans le plan ANALYSE.
| Quoi | Forme invariante à la locale | Forme sensible à la locale |
|---|---|---|
| Nombre | fmt.num(x) |
fmt.num(x, locale) |
| Pourcentage | fmt.pct(x) |
fmt.pct(x, locale) |
| Prix | fmt.price(x) |
fmt.price(x, locale) |
| Date et heure | fmt.time(t, pattern, zone) |
fmt.time(t, pattern, zone, locale) |
| Temps relatif | — | fmt.relTime(t, ref, locale) |
| Durée | — | fmt.duration(d, locale) |
| Liste | — | fmt.list(v, listType, locale) |
Les trois lignes du bas sont une surface entièrement nouvelle, et elles n'ont pas de forme invariante à la locale pour une bonne raison : il n'y a pas de réponse invariante à la locale à « il y a trois heures ». Ce sont la paire d'humanisation plus le joncteur de listes, et elles chevauchent les règles de pluriel comme tout le reste — le choix d'unité (secondes → minutes → heures → jours → semaines → mois → années) est borné par fenêtre, non ouvert.
fluxdef posted(ts, ref, loc) = fmt.relTime(ts, ref, loc) // "3 hours ago" · "il y a 3 heures"
def spanOf(d, loc) = fmt.duration(d, loc) // "1 hr 20 min" · "1 h 20 min"
def stamp(ts, loc) = fmt.time(ts, "d MMM y", "UTC", loc)
Le rendu de devise compose la table de symboles épinglée avec le tag de cotation qu'un montant
porte déjà, si bien qu'un price[BTC, EUR] se rend avec le bon symbole à la bonne position pour la
locale sans que quiconque passe la devise deux fois. Voir asset & currency.
Chacune d'elles est une routine épinglée — interpréteur ≡ module compilé ≡ serveur — avec un
golden par routine et par famille de locale. Parce que la locale est un paramètre plutôt qu'un
mode, deux locales ne peuvent jamais se courir après à l'intérieur d'une exécution de l'oracle : le
golden pour ("fr") et le golden pour ("ja") sont deux faits indépendants et reproductibles.
Pourquoi le nombre ne devient jamais dépendant de la locale.
fmt.num(x, loc)retourne unstring. Il ne change pasx, et il n'y a pas de « locale courante » que l'arithmétique pourrait consulter. C'est toute l'astuce, et il vaut la peine d'être franc sur ce que cela exclut : une application ne peut pas brancher sur un nombre formaté, ne peut pas calculer avec un tel nombre, et ne peut pas le réinjecter dans l'analyse, parce que c'est du texte — et le texte n'est pas traçable, pas ordonné, et pas numérique. La locale atteint le rendu et s'y arrête.
#Collationnement — un ordre sans opérateur
La machinerie d'ordonnancement figée n'admet pas les clés string. La fonction de clé de
sortBy doit retourner un scalaire ordonné, et string est exclu de l'ordonnancement (il n'y a pas
de < sur les chaînes, par A12).
fluxbyName = vec.sortBy(rows, (r) -> r.name) // ✗ [ErrArg] — a `string` is not an ordered key kind
"a" < "b" // ✗ [ErrDim] — no ordering on `string`, ever
Donc le collationnement est livré comme son propre combinateur épinglé plutôt que comme une astuce de fonction de clé :
fluxrows = Vec.of([{ name: "Ötzi" }, { name: "Adam" }, { name: "Zoë" }]) // Vec(record{ name: string }, 3)
loc = "de"
needle = "STRASSE"
ranked = coll.sort(rows, (r) -> r.name, loc) // the pinned CLDR order for an explicit locale
top10 = coll.topK(rows, 10, (r) -> r.name, loc) // the same order, bounded result
folded = coll.fold(needle, loc) // case-insensitive matching → a `string` value
Le string extrait est une donnée. L'ordre qui lui est appliqué est l'ordre de collation
CLDR épinglé pour la locale explicite, avec des adaptations versionnées par locale, et avec la
même politique absent-en-dernier, stable-par-index qu'utilisent déjà sortBy et topK. La routine
rejoint l'ensemble épinglé et porte son golden.
Pourquoi ce n'est pas une faille. L'ordonnancement général de chaînes reste inexprimable :
<sur les chaînes etsortBysur une clé chaîne restent des erreurs après la livraison de ce pilier. Ce qui existe est un ordre nommé, à locale explicite, épinglé — précisément la forme qu'a déjà le calendrier, où l'arithmétique arbitraire de fuseau horaire n'existe pas mais où les tables IANA nommées et épinglées existent. L'exclusion par A12 du collationnement dépendant de la locale reposait sur deux fondements, et tous deux sont traités plutôt qu'écartés : le fondement du déterminisme est dissous par l'épinglage (la locale est explicite, les tables sont versionnées), et le fondement de la totalité tenait déjà, car unstringest borné — entrée bornée, clé bornée, ordre total.
La troisième d'entre elles est le repli de casse, et elle répond à une question différente des
deux autres : non pas quel ordre, mais ces éléments correspondent-ils. coll.fold(s, locale) lit
les mêmes tables et retourne un string. Le repli produit une donnée, et l'égalité sur les
chaînes est déjà admise comme égalité bit à bit — si bien qu'une comparaison repliée n'a pas besoin
d'un nouvel opérateur non plus, et faire correspondre « STRASSE » à « straße » cesse d'être un cas
spécial qu'il faut se rappeler.
Décision ouverte. Le plafond de dérivation de clé pour les chaînes très longues — la politique entrée-bornée, clé-bornée — est laissé ouvert par le plan.
#De droite à gauche et bidi
textDir(locale) -> dir retourne le kind dir existant : 1 pour gauche-à-droite, -1 pour
droite-à-gauche. Il est consommé comme donnée — l'hôte inverse les côtés du rail et du panneau à
partir des tokens, et une application ne lit la valeur que pour des décisions de contenu.
fluxdef isRtl(loc) = textDir(loc) == -1 // `dir` is discriminated by comparison, never by `match`
Ce n'est délibérément pas une présentation ANALYSE de dir — dont les seuls canaux de graphique
restent les repères et la coloration de barres — de sorte que le pilier n'ajoute aucun kind
nouveau.
Trois choses s'ensuivent alors, et toutes trois sont côté hôte :
- Les suites de texte se rendent avec l'isolation bidi UAX #9. La table est détenue et épinglée par ce pilier, verrouillée en version aux côtés des tables de segmentation de text.
- Les valeurs interpolées sont encapsulées en isolat par défaut. Une chaîne droite-à-gauche fournie par l'utilisateur déposée dans une phrase gauche-à-droite réordonne autrement la ponctuation autour d'elle — le point final saute en tête de ligne, les parenthèses s'échangent. C'est l'un des bugs les plus fiablement livrés de l'industrie, et défaillir vers l'isolation signifie qu'une application ne peut pas le livrer.
- Les conteneurs
uiportent un contrat de direction logique — début et fin, plutôt que gauche et droite — résolu par l'hôte. Il n'y a pas de code de miroir par app, et donc pas de bug de miroir par app.
#en est la locale de base ; rien d'autre n'est privilégié
en est embarqué dans le runtime comme terminal de repli — la seule locale avec un statut de
plateforme, et elle a ce statut pour exactement une raison : une chaîne de repli a besoin d'un endroit
où s'arrêter.
Toute autre locale est un citoyen uniforme. fr, es, it, de, ja, ar — toutes sont
livrées de la même manière : assets de bundle versionnés, paresseux par locale déclarée, sans aucun
traitement spécial nulle part dans la machinerie. Une application déclare sa liste de locales dans
le manifeste, où elle est inspectable avant l'installation, exactement comme une capabilité.
Cette uniformité est un engagement de conception, non une coïncidence du bundle actuel. Le produit first-party livre le français en premier parce que son public est français — via le mécanisme identique qu'utilise toute locale, sans raccourci disponible pour lui qu'une app tierce ne pourrait prendre. Une plateforme qui ferait croître une seconde locale privilégiée ferait croître un second chemin de code avec elle, et le second chemin de code est là où vit la divergence.
Décision ouverte. La coupe exacte des tables que la base en embarquée doit porter, face à ce que
même en peut charger paresseusement. La racine de collation est le candidat lourd ; le plan ne
tranche pas.
#Réagir à un changement de locale
fluxsubs(m) = [ OnLocale(Locale) ]
Sub OnLocale(C) livre un changement de locale comme message journalisé, portant le constructeur
qui le route vers update. L'application se re-rend. Rien d'autre ne change.
OnLocale est une ouverture additive du catalogue clos d'abonnements — le même mécanisme
d'extension qu'utilisent les abonnements réseau — et non un cas spécial boulonné sur son flanc.
Pourquoi un message plutôt qu'une relecture ambiante. Le journal est la source de vérité unique : le comportement entier d'une application est reconstructible en repliant ses messages. Un changement de locale qui muterait un global ambiant serait une entrée qui n'est jamais entrée dans le journal, et la même session se re-replierait alors vers une vue différente. En tant que message, il est enregistré, rejouable et testable comme tout autre bord — et le fait que la vue change tandis que les nombres du modèle ne changent pas est visible là, dans le repli.
#Déterminisme, en trois règles
Tout ce qui précède se résume en trois phrases, et elles sont la raison pour laquelle le pilier a cette allure plutôt que celle qu'a d'habitude une bibliothèque d'internationalisation.
1. Une locale affecte le rendu et l'ordonnancement. Jamais une valeur calculée.
| Une locale PEUT décider | Une locale ne peut JAMAIS décider |
|---|---|
comment un nombre est rendu (fmt.num(x, loc) → un string) |
la valeur de x |
l'ordre dans lequel deux noms apparaissent (coll.sort(…, loc)) |
le résultat d'une comparaison numérique quelconque |
| quelle forme de pluriel un message prend | quelle branche un if prend en ANALYSE |
| la direction de base de la mise en page | une série tracée, ou un signal |
2. Chaque table est épinglée et versionnée, si bien que deux moteurs s'accordent — et si bien qu'une mise à jour de table est un changement de build que vous pouvez voir, plutôt qu'un changement de comportement que vous ne pouvez pas voir.
3. La locale est une valeur explicite, si bien qu'elle vit dans l'ensemble d'entrées de rejeu. Une réexécution reproduit le même rendu, et un serveur qui vérifie un résultat regarde le même texte que l'utilisateur a vu.
#Ce que cela coûte, et ce que cela ne coûte pas
Cela coûte une ligne de capabilité, une famille de tables épinglées, un combinateur épinglé pour
l'ordonnancement, et un abonnement. Cela n'ajoute aucune sorte au treillis, aucun symbole à la
grammaire, et aucune seconde flèche : locale est une valeur string, t / coll.* / fmt.*
sont des routines épinglées derrière des définitions du prélude, et textDir réutilise le kind dir
qui existe déjà.
Le pare-feu est intact. La sortie dépendante de la locale est du travail de présentation et de plan
APP ; ANALYSE garde les défauts fmt.* invariants à la locale, et une locale ne l'atteint que comme
entrée explicitement épinglée.
Post-v1. L'outillage de rédaction de catalogue — l'intégration à l'éditeur et le format d'export de traduction — suit le runtime, comme le fait l'outillage.
Décision ouverte. Si un slug() sensible à la locale tire les tables de translittération dans le
cœur, ou les en tient dehors.
#Voir aussi
- text — le kind
string, la segmentation, et le formateur épinglé que ce pilier étend. - Plan APP — abonnements, capabilités, et le journal que
OnLocalechevauche. - collections — le
Mapordonné, et l'ordonnancement stable absent-en-dernier quecoll.sortréutilise. - display — le catalogue
ui, la direction logique, et la membrane de sortie. - units — rendre une quantité pour une locale sans que la quantité devienne dépendante de la locale.
- Kinds — pourquoi
stringn'a pas d'ordre, et ce qu'estdir.
color — la couleur comme valeur
La couleur repose sur une ligne de faille. Elle est présentation — elle appartient au thème, au visiteur, aux yeux — et pourtant une couleur dérivée de données est une décision que l'analyse a prise, et une décision que l'analyse a prise doit être reproductible à l'octet, sinon le rejeu dérive et un serveur ne peut plus re-dériver ce qu'un client prétend avoir calculé.
Flux résout cela par une séparation nette plutôt que par un compromis. La décision de couleur par barre est de l'analyse : déterministe, rejouable, à l'intérieur de l'oracle. Le mappage vers les pixels est de la présentation : sensible au thème, par visiteur. Le déterminisme vit là où vivent les données ; le thème vit là où sont les yeux. Tout sur cette page découle de cette seule ligne.
Cette page couvre le kind color de bout en bout : comment une couleur est représentée, comment on
en construit une, comment deux couleurs interpolent, les canaux par lesquels une couleur atteint le
graphique, et les frontières que la conception maintient délibérément closes. Certains exemples
ci-dessous sont négatifs — un fragment suivi de ✗ et d'un code d'erreur est l'illustration d'une
règle, non un programme.
#La valeur — un RGBA u32 porté comme entier f64 exact
Une color est du RGBA8, empaqueté 0xRRGGBBAA (rouge dans l'octet de poids fort, alpha dans l'octet
de poids faible, alpha droit), et portée à travers le dataflow comme entier f64 non négatif.
Ce n'est pas un compromis ; c'est exact. Un u32 est inférieur à 2³², et tout entier inférieur à 2⁵³ est représenté exactement dans un f64 et y fait l'aller-retour. Une couleur est donc un f64 dont la valeur se trouve être un entier, et les conséquences sont entièrement bonnes :
| Conséquence | Pourquoi elle en découle |
|---|---|
| Aucun nouveau canal de calcul | Tout le moteur f64 — const, select, na, la porte I7 sur les colonnes f64 — est réutilisé tel quel. |
if c then a else b sur les couleurs |
Déjà le select f64 existant. Rien n'a été ajouté pour cela. |
color == color |
Un == f64 exact — égalité bit à bit, → signal. |
couleur na |
Le NaN canonique — distinct de tout entier de couleur fini. Chez l'hôte, il signifie pas de surcharge par barre (transparent). Il se propage à travers select, si bien que la totalité tient. |
| Sérialisation | Le seul endroit où la représentation se montre : une colonne de couleur est expédiée comme Uint32Array, jamais comme un tableau f32. na se sérialise en 0x00000000. |
Pourquoi le puits est un
Uint32Arrayet jamais du f32. Un f32 a 24 bits de mantisse, si bien qu'il cesse de représenter des entiers consécutifs au-delà de 2²⁴ — une valeur RGBA au-dessus serait silencieusement arrondie vers une couleur voisine. La couleur aurait toujours l'air plausible, ce qui est le pire type de bug. La colonne est expédiée en u32, et cette classe de bug n'existe pas.
L'ordre d'empaquetage 0xRRGGBBAA correspond à la notation CSS #rrggbbaa, ce qui est commode à la
frontière mais reste par ailleurs un détail interne, épinglé : l'hôte extrait les canaux
explicitement. Ce dont I7 se soucie n'est pas la disposition — c'est que l'interpréteur et le module
compilé calculent les mêmes entiers f64, ce qu'ils font.
#La couleur absente
na est une couleur comme il est un nombre : la valeur qui n'est pas là. Sur une colonne de
couleur, elle signifie pas de surcharge par barre, si bien que la barre conserve ce que le
graphique aurait dessiné. Cela fait de « ne colorer que les barres qui m'intéressent » une expression
ordinaire plutôt qu'un mode spécial :
fluxfresh = barssince(close cross_up ema(close, 50)) < 5 // signal
color bars: if fresh then up else na // colour the fresh bars; leave the rest alone
La même règle couvre la chauffe sans coût : sur les barres où un indicateur n'a pas encore rempli sa
fenêtre, sa valeur est na, donc une couleur qui en dérive est na, donc ces barres sont laissées
non surchargées. Rien ne lève d'exception, rien n'est indéfini, et aucune branche n'a eu à être
écrite pour cela.
#La séparation analyse / présentation
Deux canaux quittent le plan d'analyse, et choisir entre eux est la première décision que vous prenez.
#dir — le canal sémantique, et l'idiomatique
dir est le kind {-1, 0, +1}. L'analyse répond à la question sémantique — cette barre est-elle
en hausse, plate ou en baisse ? — et l'hôte mappe cette réponse vers les couleurs
hausse / neutre / baisse du thème.
fluxst = superTrend(10, 3) // sourceless (it reads high/low/close) → record{ st: price, dir: dir }
color bars: st.dir // the host maps {-1, 0, +1} to the theme
Cela achète deux choses à la fois. La sensibilité au thème sans casser le déterminisme : la
valeur d'analyse est un dir, non un RGBA, si bien que ce que l'oracle compare à l'octet est la
décision, non son apparence. Et l'accessibilité gratuitement : parce que l'hôte détient le
mappage, il peut mapper dir vers une palette sûre pour le déficit de vision des couleurs — environ
un homme sur douze ne peut séparer le rouge du vert, et un script qui aurait codé en dur le rouge et
le vert aurait rendu cela impossible à corriger.
dir est la manière idiomatique de colorer les barres. Prenez une color explicite quand vous avez
besoin d'une couleur que le thème ne peut nommer.
#color — le canal explicite, pour couleurs personnalisées et dégradés
Une couleur explicite dans l'analyse est légale, et elle est déterministe en raison d'où ses valeurs
peuvent provenir : elles sont des constantes épinglées (up, down, neutral) ou calculées
par des maths épinglées (mix, rgb de canaux calculés). Les deux sont à l'intérieur de l'oracle
et couvertes par I7.
fluxcolor bars: if close > ema(close, 200) then up else down // pinned constants; the host remaps to theme
Ce qui n'est pas légal est une couleur qui varie avec le spectateur. Un token de thème se résout contre le thème courant du visiteur, ce qui en fait une entrée ambiante, par visiteur — la même classe que lire l'horloge murale ou l'écran :
fluxcolor bars: if close > open then token.bull else token.bear // ✗ [ErrFirewall] — a token resolves per visitor
color bars: if minute(now()) > 30 then up else down // ✗ [ErrFirewall] — now() is a presentation symbol
Le thème est un remappage par l'hôte de valeurs épinglées ou sémantiques au moment du rendu, jamais une entrée ambiante dans l'analyse. Les tokens sont pour le plan où sont les yeux :
fluxtriangle { at: (bar.i, low), r: 6, fill: token.bull } // CANVAS — theme-aware, and correctly so
Pourquoi le pare-feu est tracé ici et non un pas plus tard. Si une couleur de thème pouvait entrer dans l'analyse, alors la valeur qu'un script produit dépendrait de qui la regarde. Deux visiteurs calculeraient des octets différents à partir des mêmes données ; un golden dépendrait d'un thème ; un serveur re-dérivant l'exécution d'un client devrait connaître le schéma de couleurs du client pour être d'accord avec lui. La ligne est tracée là où sont les données, pour que les données signifient la même chose partout.
#Choisir un canal
| Ce que vous voulez | Ce que vous écrivez |
|---|---|
| des barres colorées par un état sémantique — direction de tendance, le côté d'un stop | un dir ; l'hôte le mappe vers le thème, et vers une palette sûre pour le déficit de vision des couleurs |
| une couleur de marque, une palette scientifique, un dégradé | une color explicite — constantes épinglées ou maths épinglées |
| une couleur de thème dans l'UI ou sur le canvas | un token.* |
| une couleur de thème dans l'analyse | rien : c'est [ErrFirewall]. Émettez un dir et laissez l'hôte le mapper — c'est la même image, calculée du bon côté de la ligne |
#Construction — la surface color.*
Chaque constructeur est un appel ordinaire d'un ensemble clos de fonctions vérifiées par l'hôte. Il n'y a aucun changement de grammaire nulle part dans ce pilier : aucun littéral de couleur, aucun nouveau token, aucune re-vérification de la grammaire figée.
Constantes sémantiques épinglées, versionnées comme les maths épinglées, alignées sur le thème du
graphique : up, down, neutral.
| Constructeur | Arguments | Notes |
|---|---|---|
rgb(r, g, b) |
r, g, b ∈ [0, 255] |
bornées (clamp) de façon déterministe |
rgba(r, g, b, a) |
plus a : ratio ∈ [0, 1] |
alpha droit |
hsl(h, s, l) / hsla(h, s, l, a) |
h ∈ [0, 360), s, l ∈ [0, 1] |
HSL → RGB est linéaire par morceaux — zéro transcendante, donc le déterminisme est trivial |
hex(s) |
"#rgb", "#rrggbb", "#rrggbbaa" |
un littéral de chaîne, analysé à la compilation ; un littéral malformé est na plus un diagnostic |
withAlpha(c, a) / fade(c, a) |
un remplacement de canal — peu coûteux | |
lighten(c, amt) / darken(c, amt) |
perceptuels : ils déplacent la luminosité OKLab | |
mix(a, b, t) |
t : ratio |
le mélange perceptuel — voir ci-dessous |
fluxbrand = rgb(34, 211, 163) // a const-folded colour node
mint = hex("#22d3a3") // parsed at compile — the same node
ghost = up.withAlpha(0.25) // straight alpha, not premultiplied
def ramp(t) = mix(down, up, t) // a gradient — blended in OKLab
Un constructeur dont les arguments sont tous constants se replie en constante vers un nœud
couleur const. Un constructeur à arguments dynamiques s'abaisse vers une opération d'exécution
déterministe — et est identique à l'octet entre l'interpréteur et le module compilé, comme tout autre
noyau.
#Tokens de thème
token.bull, token.bear, token.grid sont les couleurs du thème. Ils sont sensibles au thème
— ils se résolvent contre le thème courant du visiteur — et ils sont le bon défaut pour tout ce qui
est sémantique dans l'UI et le canvas. Cette même propriété est ce qui les tient hors de l'analyse
(section précédente). Une color explicite est, par contraste, aveugle au thème par choix de
l'auteur : c'est ce que vous voulez pour une couleur de marque ou une palette scientifique, et c'est
ce que vous ne voulez pas pour « la haussière ».
#Les échelles de palette épinglées
Les palettes scientifiques et catégorielles proviennent d'une table de palette épinglée par l'hôte et versionnée — au même rang que les tables de symboles monétaires et Unicode — échantillonnée par un ensemble clos d'appels :
| Appel | Ce qu'il donne |
|---|---|
color.seq(scheme, t) |
un schéma séquentiel perceptuel échantillonné à t : ratio |
color.div(scheme, t) |
un schéma divergent |
color.cat(scheme, i) |
une entrée catégorielle, perceptuellement équilibrée à l'index i |
color.quantize(scheme, t, n: lit) |
le même, quantifié en n paliers discrets |
Le schéma nomme une ligne de cette table close (la famille séquentielle — viridis, magma — et les
familles divergente et catégorielle à côté). Comme hex, il est vérifié à la compilation : une
recherche de palette est une sélection à la compilation, jamais une chaîne interprétée à l'exécution.
Ce sont les échelles de couleur first-party qu'utilisent les canaux d'encodage viz.* — voir
display.
Post-v1. La génération de schémas fidèles à la teinte en OKLCH — et la famille withHue /
withChroma / withLightness — attend le lot de complétion des maths épinglées qui livre atan2 et
la paire sinus-cosinus. OKLab cartésien (ci-dessous) est livré en premier et n'a besoin d'aucun des
deux.
Cette porte est étroite, et deux familles voisines se tiennent en dehors. Toutes deux sont de la
pure arithmétique de canal, et toutes deux n'ont besoin que de pow — qui est épinglée, et
disponible :
- les aides WCAG —
color.relLuminance(c), qui est la linéarisation que le chemin OKLab effectue déjà, exposée ; etcolor.contrast(a, b) -> ratio, pour qu'un auteur puisse valider le contraste d'une palette à la compilation plutôt que de le découvrir dans un audit d'accessibilité ; - l'algèbre de composition —
color.over(a, b), source-over sur alpha droit, plusmultiply,screenetoverlay, comme fonctions pures épinglées. La propriétéblendde la scène nomme un mode hôte ; ceci est l'algèbre au niveau des valeurs, pour un auteur qui calcule une couleur plutôt que d'en demander une.
Ni l'une ni l'autre n'attend le lot des transcendantes, et il vaut la peine d'être explicite là-dessus, car l'hypothèse naturelle — « ce sont des maths de couleur, donc elles sont derrière la porte des maths de couleur » — est fausse d'une manière qui différerait deux familles utiles sans raison.
#Interpolation — OKLab, perceptuellement uniforme, déterministe
mix(a, b, t) mélange deux couleurs en OKLab. Le pipeline, par canal :
sRGB8 → [÷255] → linear (gamma decode, pow 2.4) → LMS (3×3 matrix)
→ L′M′S′ (cbrt) → OKLab (matrix) → lerp L, a, b by t
→ the exact inverse (cube is x·x·x) → linear → sRGB (pow 1/2.4) → [×255, round]
Pourquoi pas un lerp direct en RGB. Interpoler du rouge au vert en sRGB brut passe par un brun boueux, car sRGB n'est pas perceptuellement uniforme : des pas égaux dans ses canaux ne sont pas des pas égaux pour l'œil, et le point médian de deux couleurs saturées atterrit quelque part de sombre et de gris. En OKLab, des pas égaux sont perceptuellement égaux, si bien qu'une rampe rouge-vers- vert reste propre tout du long. Un dégradé est un dispositif de communication ; un dégradé au milieu boueux en est un cassé.
Déterminisme. Chaque pow de ce pipeline passe par la bibliothèque de maths épinglée — celle-
là même qu'utilise le logarithme d'un ema — si bien qu'il produit des bits identiques dans
l'interpréteur et dans le module compilé. I7 tient à travers un mélange de couleurs exactement comme
il tient à travers une moyenne mobile.
Trois détails du pipeline sont porteurs, et chacun est une décision :
- OKLab cartésien, non OKLCH. La forme cartésienne n'a besoin que de
pow, qui est épinglée et disponible. La forme cylindrique a besoin d'atan2et de la paire sinus-cosinus, qui ne le sont pas — donc choisir OKLab en premier est ce qui permet au mélange perceptuel d'être livré maintenant plutôt que derrière une dépendance. cbrtest réalisée comme une fonction impaire —cbrt(x) = sign(x) · pow(|x|, ⅓)— si bien qu'une valeur LMS négative légèrement hors gamut reste totale au lieu de produire un NaN. La totalité n'est pas une propriété que l'on a le droit de suspendre à l'intérieur d'une routine de couleur.- La sortie est bornée au gamut (clamp) à
[0, 255]par canal, avec l'arrondi épinglé (arrondi au pair, exactement comme arrondissent les puits).
Un dégradé est donc une expression ordinaire sur des données, et il est dans l'oracle comme toute autre :
fluxt = norm(volume) // ratio in [0, 1] — normalized against its own range
heat = mix(neutral, up, t) // an OKLab ramp, one colour per bar
plot close { color: heat } // the series, coloured by relative volume
Trois lignes, aucun objet palette, aucune comptabilité d'espace colorimétrique, et un résultat identique à l'octet sur chaque machine qui l'exécute.
#Les canaux de sortie
Il y a trois façons dont une couleur atteint le graphique, et seulement trois :
| Canal | Accepte | Ce qu'il produit |
|---|---|---|
color bars: … |
signal | dir | color |
une colonne de couleur par barre sur les bougies |
{ color: … } à l'intérieur d'un bloc plot |
une color par barre |
une colonne de couleur parallèle attachée au puits de ce plot |
une colonne dir |
— | l'hôte mappe {-1, 0, +1} vers les couleurs hausse / neutre / baisse du thème |
Le canal { color: } est ce qui fait tomber un histogramme coloré de l'algèbre ordinaire plutôt que
d'un cas spécial :
fluxm = macd(close, 12, 26, 9) // record{ macd, signal, hist : level }
plot m.hist { color: if m.hist > 0 then up else down }
En v1, le canal de bloc { color: } est réservé au plot. Sur un bloc fill ou mark, c'est une
erreur plutôt qu'une propriété silencieusement ignorée :
fluxbb = bollinger(close, 20, 2)
fill bb.upper..bb.lower { color: up } // ✗ [ErrArg] — the { color: } channel is plot-only in v1
Pourquoi une erreur et jamais un abandon silencieux. Une propriété qui est acceptée, ignorée et jamais dessinée est un bug que l'on trouve en fixant un graphique en se posant des questions. Le compilateur sait que le canal n'existe pas sur ce bloc ; il le dit.
#Comment une couleur atteint le moteur de graphique
Le graphique porte une entrée de couleur par barre, et elle a trois bras qui reflètent les trois canaux ci-dessus :
- une colonne
dirousignalrecolore les bougies via le mécanisme par barre existant — sensible au thème et au déficit de vision des couleurs, car le mappage est celui de l'hôte ; - une colonne
colorexplicite (u32) pilote un chemin RGBA par barre — le corps et la mèche de la bougie, ou une série ; - un
plotavec un canal{ color: }colore chaque barre ou segment de cette série.
La couleur par barre sur une série à échelle superposée (overlay) — une moyenne mobile, une ligne de prix — est expédiée comme segments colorés.
Post-v1. Colorer un glyphe mark (un point) est différé, tout comme colorer une bande fill.
Sous un verrou MTF — où la géométrie appartient à une série sur une autre unité de temps — une couleur
par barre retombe sur la couleur de base ; la couleur par barre par l'unité de temps verrouillée est
un suivi.
#La porte I7 — une couleur est une donnée, donc elle est vérifiée comme une donnée
La porte de vérification compare chaque colonne de puits au niveau du bit f64, et elle est délibérément agnostique à ce qu'une colonne signifie. Une colonne de couleur est u32-dans-f64, donc elle est comparée exactement comme une colonne de prix : la porte n'a eu besoin d'aucun changement pour accepter la couleur, et elle n'acceptera pas une couleur qui diffère d'un bit entre les deux moteurs.
Ce que cela coûte, concrètement : chaque opération de couleur doit émettre des bits identiques des
deux côtés (const et select sont gratuits ; pack et le chemin OKLab passent par les maths
épinglées), et le corpus qui exerce la porte est délibérément hostile — canaux à 0 et 255, alpha aux
deux extrémités, na, et entrées hors gamut pour mix.
C'est toute la raison pour laquelle une couleur n'est pas un « style ». Un style serait hors de l'oracle, et un programme de couleur serait invérifiable. Une couleur est une donnée, et elle est vérifiée à l'octet comme toute autre valeur.
#Totalité et treillis
color est une sorte catégorielle plate. Elle n'est pas sur l'épine dorsale numérique, et le
treillis l'impose :
fluxshade = up + 1 // ✗ [ErrDim] — a colour is not a number: there is no meaning to add to it
Le seul opérateur défini entre deux couleurs est == (égalité bit à bit, → signal). La couleur ne
porte aucun ordre, si bien que < et ses proches ne type-checkent pas non plus sur elle. Et une
couleur n'est pas traçable en ligne :
fluxplot up // ✗ [ErrPlot] — a colour is not a series; it is consumed by `color bars:` or the { color: } channel
Chaque opération de couleur est totale : les canaux bornent (clamp), na se propage, et il n'y a
aucun comportement indéfini à rencontrer. C'est ce qui laisse une couleur circuler à travers select
et na sans un seul cas spécial nulle part dans le moteur.
#Frontières délibérées
| Frontière | Statut |
|---|---|
Une chaîne de couleur CSS libre — url(…), expression(…), HTML embarqué |
Jamais. Aucun constructeur n'en produit une et le lexeur ne porte aucun littéral de couleur, si bien qu'elle est structurellement inexprimable — non filtrée, non assainie, non atteignable. |
Un littéral de grammaire #rrggbb |
Post-v1. Un nouveau token forcerait une re-vérification sans conflit de la grammaire figée ; hex("#…") couvre le besoin aujourd'hui. |
| OKLCH et interpolation de teinte | Post-v1. Attend le lot de complétion des maths épinglées (atan2, sinus-cosinus, cbrt) ; OKLab cartésien est livré en premier. |
Colorer une bande fill ou un glyphe mark |
Post-v1. En v1, les consommateurs de couleur à l'exécution sont color bars: et le canal { color: } du plot. |
| Définition de la palette de thème et de déficit de vision des couleurs | Une préoccupation de l'hôte par conception — l'analyse émet dir ou des couleurs épinglées, et l'hôte les mappe. |
| Gamut large, HDR, alpha prémultiplié | Hors périmètre. Le sRGB 8 bits à alpha droit est livré. |
La première ligne est celle sur laquelle il vaut la peine de s'attarder, car c'est la seule véritable surface d'injection qu'un système de couleur possède. La raison pour laquelle une chaîne CSS libre ne peut atteindre le moteur de rendu n'est pas qu'un assainisseur la rejette. C'est qu'il n'y a aucun moyen de la dire : les constructeurs sont un ensemble clos, aucun d'eux ne prend une chaîne de style arbitraire, et la grammaire n'a aucun littéral de couleur où l'une pourrait se cacher. Une frontière que vous imposez par un filtre est une frontière que vous finirez par mal faire. Une frontière que vous imposez par la grammaire en est une que vous ne pouvez pas.
Pour des arrière-plans plus riches, la sorte structurelle close paint — un variant sur Solid,
Linear, Radial et Texture — est la surface sanctionnée, et son bras texture porte une clé
d'asset que l'hôte résout sous une liste blanche, jamais des octets issus du script. Voir
display.
#Voir aussi
- display — la scène, les canaux d'encodage
viz.*, etpaint. - Kinds — où se situe la sorte
color, et pourquoi elle n'a pas d'arithmétique. - compute — les maths épinglées par lesquelles passe le chemin OKLab.
- Garanties — I7, l'oracle, et ce que vaut « identique à l'octet ».
- Les quatre plans — le pare-feu contre lequel ce pilier est tracé.
display — scènes, panneaux et cibles de rendu
Post-v1. Le pilier display est entièrement conçu et additif au cœur gelé.
Un seul mot recouvre un graphique, un tableau de bord, une visualisation de données, une vue 3D et un jeu : une scène. Une scène est une fonction pure du modèle, et c'est une valeur — un arbre de primitives validées que vous pouvez lier, renvoyer depuis une fonction et passer à une fenêtre. L'hôte transforme cette valeur en pixels ; le langage ne touche jamais un pixel.
Ce n'est pas un choix stylistique. C'est ce qui permet à un jeu dans un panneau d'être rejoué à la frame exacte, à un overlay de graphique d'être goldené sans capture d'écran, et à une animation d'être belle sans jamais toucher une valeur dont dépend un verdict.
#Le théorème de présentation
La géométrie d'une scène est une fonction pure du Modèle. Elle est donc déterministe, identique octet pour octet, bornée, et à l'intérieur de l'oracle de rejeu. La peinture de cette géométrie — le GPU, le compositeur, les signaux par frame — est non déterministe vis-à-vis du monde, et est donc réservée à l'hôte, hors de l'oracle, et isolée du Modèle par le pare-feu.
Tout le reste de cette page découle de cette phrase. Un panneau peut lire un flux d'exchange en direct, un carnet d'ordres, l'état d'un jeu ; un indicateur ne peut jamais être relu par aucun d'entre eux.
#Les deux strates
Chaque scène se scinde, structurellement, en deux couches :
| (a) géométrie retenue | (b) cosmétiques par frame | |
|---|---|---|
| Quoi | position, forme, taille, couleur de token, ordre — en fonction du Modèle | glow, pulse, parallaxe, shimmer, particules de morph |
| Piloté par | valeurs du Modèle, valeurs de stabilisation de tween/spring, aléa seedé |
horloge murale, espace écran, aléa non seedé |
| S'exécute où | dans l'oracle — les mêmes octets sur chaque moteur | sur le compositeur hôte — zéro JavaScript par frame |
| Rejouable | oui | non, et il n'en a jamais besoin |
Le routage est décidé par le kind du signal, que le compilateur connaît déjà. Un signal
dérivé du Modèle relève de la strate (a) ; un signal qui touche now(), screen.* ou de l'aléa
non seedé relève de la strate (b) — et lire l'un d'eux depuis l'analyse est [ErrFirewall], tout comme
faire passer clandestinement l'un d'eux dans un champ du Modèle que lit un verdict.
#La valeur scène
fluxdef overlayOf(m) = scene {
when volume > sma(volume, 20) * 2 :
dot { at: (bar.i, high), r: 4, fill: token.spike, glow: throb(0.4) }
when ema(close, 9) cross_up ema(close, 21) :
triangle { at: (bar.i, low), r: 6, fill: token.bull }
}
// …and, inside an `app` block, the view mounts it:
// view(m) = chartView(chartId: "main", overlay: overlayOf(m))
Une scène est une valeur de kind ui. Notez ce qui n'est pas dans cet exemple : aucune boucle sur les barres (les
signaux par barre sont évalués par barre, implicitement), aucune API d'animation (glow est un signal comme un autre),
et aucun moyen de réécrire dans l'analyse qu'elle lit.
Le glow relève de la strate (b) — il va au compositeur. La position relève de la strate (a) — elle est
dans l'oracle. La frontière passe à travers la même primitive, et le compilateur sait de quel côté
se trouve chaque propriété.
#Primitives, composition, layout
L'ensemble des primitives est clos et validé : dot, circle, ring, rect, square,
triangle, poly, line, path, text, image, svg, sparkline, backdrop. Elles partagent
un seul modèle de propriétés — at, size/r/w/h, rotate, fill, stroke, width, opacity,
glow, blend, z, life, color, trail, paintOrder.
Trois combinateurs de composition, tous bornés :
| Signification | Borne | |
|---|---|---|
group { … } |
transformer / mélanger / rogner un sous-arbre — le nœud interne universel | — |
repeat n as i { … } |
instanciation : n formes paramétrées par l'index |
n est const-folded |
for x in coll -> child |
une compréhension sur une collection bornée | la capacité déclarée de la collection |
Il n'y a pas d'itérateur entier : for i in range(n) n'existe pas, parce que n n'est pas une
collection. La borne vient toujours d'une capacité déclarée — ce qui rend le budget d'instances
calculable à la compilation.
Le layout réutilise les conteneurs ui gelés (col, row, grid, stack, tabs, scroll,
panel, les rails de l'application) et ajoute wrap/flow, plus deux widgets de données : virtualList
(le flux fenêtré — l'hôte ne matérialise que les lignes visibles) et tableView (une grille de données
qui rend les colonnes d'une Table directement, sans jamais matérialiser de lignes).
#Style
Chaque propriété visuelle est un signal, il n'y a donc pas d'API d'animation séparée. Les couleurs viennent de deux endroits :
- Les tokens —
token.bull,token.bear,token.grid. Sensibles au thème, et le défaut pour tout ce qui est sémantique. - Les couleurs explicites —
rgb(r,g,b),rgba(r,g,b,a),hex("#RRGGBB"). Aveugles au thème, par choix de l'auteur ; pour une palette scientifique, utilisez les constructeurs d'échelle épinglés (color.seq,color.div,color.cat).
Les remplissages riches forment un variant clos :
fluxrecord Stop { color: color ; at: ratio }
variant paint {
Solid(c: color) | Linear(start: Stop, to: Stop, angle: angle)
| Radial(center: Stop, stops: vec(Stop, 8)) | Texture(assetRef: string, fit: fit)
}
Texture prend une clé d'asset, jamais des octets — l'hôte la résout sous asset:load, avec une
allowlist et un quota.
L'unique surface d'injection, close structurellement. Une chaîne CSS libre (
url(…), une expression, du markup) n'est pas filtrée — elle est inexprimable. Aucun constructeur n'en produit, et le lexer ne porte aucun littéral de couleur. Unhexmalformé produitnaet un diagnostic ; un canal hors plage est clampé par le sanitizer.
#Coordonnées
Une coordonnée dérive son axe de son kind : price → l'axe des prix, barindex → l'axe des x
ordinal, time → l'axe du temps, screen.* → les pixels du viewport, world3D → la scène 3D.
Mélanger des espaces dans une même coordonnée est [ErrDim] à la compilation. Une scène
géométriquement incohérente, ça n'existe pas.
Deux subtilités méritent d'être énoncées précisément :
- L'ancre de données plus un décalage en pixels (
dataSignal + 8px) est un constructeur de coordonnée composite, pas une addition arithmétique entre sortes. La partie pixel doit être une const, pour que la position reste dans l'oracle. Un décalage dérivé de l'écran (screen.h * 0.05) ferait basculer la position entière dans la strate (b) — interdit pour une ancre que lit un verdict. paintOrdern'est pasz.paintOrder : numest l'ordre de peinture 2D (qui est devant qui dans le plan).z : depthest la coordonnée de profondeur, auto-normalisée et projetée. Le kind décide lequel est lequel, donc ils ne peuvent pas être confondus.
#Le diff retenu
Une scène est un arbre de vnodes à clé. Elle compile une seule fois ; à chaque frame logique, le
réconciliateur diffe f(Model) par rapport à l'arbre précédent et ne réémet que ce qui a changé. Les enfants
à clé conservent leur identité à travers les réordonnancements ; les événements sont délégués à la racine.
L'oracle est défini sur la draw-list absolue f(Model), jamais sur la séquence de diffs.
Ainsi un moteur qui re-rend plus, ou moins, ou dans un ordre différent, ne peut pas changer ce qui est
rejoué.
#Encoder les données : viz.*
L'auto-présentation pilotée par le kind est déjà une grammaire d'encodage — pour une série. viz.* en est le
pendant pour des données tabulaires arbitraires : une bibliothèque de fonctions pures qui consomment une Table du
pilier compute et renvoient une valeur ui.
fluxplot viz.chart(t, { x: t.time, y: t.close, color: t.sector }, mark: Line)
plot viz.histogram(t.returns, bins: 40)
plot viz.facet(t, by: t.sector, shared: yScale, child: tile) // small multiples on ONE shared scale
plot viz.legend(scale) // derived from the encoded channels
L'inférence de domaine est un fold borné (extent(col)), la même auto-échelle que l'hôte
effectue déjà sur la fenêtre visible — causale, sans look-ahead. Tout s'abaisse en scene{},
for, primitives et arithmétique : aucune nouvelle grammaire, aucune nouvelle sorte.
Le brushing est borné et committé sur l'edge : viz.brush journalise une plage au relâchement du pointeur,
les vues sœurs lisent cette plage depuis le Modèle, et le filtrage croisé lié fonctionne sans câblage manuel — sans
aucun flux continu d'échantillons de pointeur à proximité du Modèle.
#Outils de dessin : les trois liaisons hôte
Un outil de dessin personnalisé est une contribution, et le geste est piloté par l'hôte à travers exactement trois liaisons déclaratives :
| Liaison | Ce qu'elle fait |
|---|---|
drawPreview: |
un template de forme (line, rect, circle, poly, path) paramétré par les ancres placées jusque-là et le pointeur en direct. L'hôte l'interpole comme présentation pure — zéro message pendant le geste — et émet un seul message journalisé à la fin (relâchement du pointeur, le geste terminal, ou le plafond d'ancres déclaré, qui committe plutôt que de déborder). |
magnet: |
des candidats de snap déclarés (Ohlc, Anchors) plus un rayon en pixels. L'hôte snappe avant la livraison : le (bar, price) que votre script reçoit est déjà snappé, correct aussi bien sur les axes linéaires que logarithmiques. La projection reste côté hôte ; le script reste dans l'espace de données — au coût nommé ci-dessous. |
cursor: |
un enum de curseur allowlisté par l'hôte, appliqué au survol via le système de picking. Cosmétique, zéro message, hors de l'oracle. |
Esc ou une capture perdue jette le template avec aucun message du tout — rien n'est entré dans
le journal, donc il n'y a rien à annuler.
Ce que coûte magnet:, nommé. Quel candidat l'emporte dépend du rayon en pixels et du
viewport, donc la sélection est une décision côté hôte, dépendante du device — et le (bar, price) sur lequel
il snappe est journalisé, ce qui le place carrément à l'intérieur de l'oracle. Il hérite donc exactement
du contrat de forgeage de résultat que porte le picking : un verdict reposant sur une ancre snappée exige que
le serveur la redérive, sinon le run reste hors du leaderboard partagé. Ce qui borne
l'exposition, c'est que le snap ne peut jamais atterrir que sur une valeur redérivable depuis le Modèle — un OHLC de
la barre sous le curseur, ou une ancre qui existe déjà. Il ne peut pas faire surgir un prix qui n'a jamais
été là.
#Panneaux, cibles et fenêtres
Une cible de rendu est une ressource hôte sous une capabilité, adressée par une clé chaîne
allowlistée — jamais un handle que le script détient. Trois fenêtres projettent une valeur ui dans une cible :
flux// a `ui` value: the three windows in a container — exactly what a `view` returns
col {
chartView(chartId: "main", asset: "BTC-USD", overlay: overlayOf(m), onClick: ClickAt)
paneView("rsi")
sceneView(target: "pane.game", tree: worldOf(m), space: World3D, onPick: Tapped)
}
La même valeur ui est routée par un seul renderer hôte vers le substrat que l'état du panneau
requiert — et le script ne sait jamais lequel :
UiTree ─► reconcile ─► SOLID → DOM / SVG (crisp, accessible, the default)
LIQUID → Canvas2D → texture (deterministic capture)
FLOATING → texture + chrome (shadow, refraction)
SPATIAL → a quad in the 3-D scene
Rétrograder un substrat sous charge GPU n'émet aucun message et ne change pas la draw-list absolue. Le spatial et le liquide peignent la même géométrie logique.
#Le modèle 3D
La 3D est une projection, pas un second langage. Le même scene{…} porte des coordonnées
world3D lorsque sa fenêtre déclare cet espace ; les primitives (mesh, camera, light,
material, billboard) sont déclaratives et validées, et les shaders sont des recettes détenues par l'hôte ou
des clés de catalogue allowlistées — jamais du texte WGSL, jamais une lambda.
Deux sortes additives servent ce pilier, toutes deux plates sous ⊤ et opaques à match — le statut
exact de clock :
ease— une courbe d'interpolation (linear,inOutCubic,outBack(s),cubicBezier(…),springCurve(k,d)), consommée par les transitions, jamais de l'arithmétique. C'est une sorte, pas un variant, précisément pour qu'un script ne puisse pas décomposer et reparamétrer une courbe.shader<υ>— un shader hôte paramétré par le kind de son record d'uniformes. Le schéma est le kind : le compilateur vérifie les uniformes que vous passez contreυavec une règle d'appel ordinaire.
Le mode 3D propre au graphique en est un client, pas un cas particulier — et à un angle de caméra de zéro il est identique au pixel près au 2D simple, par construction.
#Le modèle d'exécution
La scène compile une seule fois, en une draw-list bornée et déterministe sérialisée en mémoire linéaire. L'hôte la décode, l'assainit, et peint — vectoriel pour du 2D net, GPU pour la 3D. Le module ne touche jamais une Web API.
Les signaux sont classifiés et routés :
| Classe | Exemple | Route | Coût par frame |
|---|---|---|---|
| statique | stroke: token.grid |
mise en cache — jamais recalculée | 0 |
| par barre | at: (bar.i, ema(close,20)) |
buffers pré-alloués ; formes identiques instanciées | O(Δ barres) |
| par frame, temps seul | glow: throb(0.4) |
le compositeur hôte | 0 JavaScript par frame |
Trois budgets, tous const-folded et vérifiés à la compilation : le nombre d'ops de la draw-list,
le nombre d'instances (un repeat n émet une op et n instances ; un émetteur puise dans un
pool plafonné fixé par l'hôte), et le travail GPU au pire cas d'une recette de shader. Dépasser l'un
d'eux est [ErrSceneBudget] à la compilation — jamais un dépassement de mémoire ou un reset de device à
l'exécution.
#[ObsDeterminism] — le déterminisme dans l'observation
Une sortie déterministe n'en est que la moitié. L'autre moitié est celle que tout le monde oublie : partout où un canal lit l'état de présentation et le route en retour comme message, une valeur non déterministe pourrait fuiter dans le Modèle — et de là dans un verdict.
[ObsDeterminism]. Toute subscription ou tout canal portant un signal de présentation dans le plan APP doit livrer un payload qui soit (i) déterministe et rejouable — un index ordinal, un edge discret, une clé stable, une const, ou une mesure hôte épinglée — soit (ii) tagué présentation, et donc[ErrFirewall]s'il alimente un champ du Modèle que lit un verdict. Aucun canal ne livre jamais un temps d'horloge murale continu, la progression d'une transition, une position de spawn transitoire, une intersection de pick continue, un échantillon tenu de pointeur/molette/analogique, ou une mesure variable selon le device dans un Modèle.
Ses instances, chacune imposée là où elle vit :
| Invariant | Ce qu'il garantit |
|---|---|
[TransSettle] |
une transition n'expose que son edge terminal, jamais sa progression. L'edge est planifié à un rang de journal déterministe dérivé de la durée déclarée — pas au moment réel où l'animation se termine. |
[TickOrdinal] |
OnTick journalise un index entier monotone, jamais une heure murale ; dt est la constante déclarée de la subscription. Une heure de frame brute est taguée présentation. |
[SpawnGeom] |
les particules d'émetteur et de morph sont cosmétiques par construction — une position transitoire n'est jamais lisible par quoi que ce soit qui alimente le Modèle. |
[TextMetric] |
toute mesure de texte entrant dans la géométrie passe par une routine de métrique hôte épinglée, identique octet pour octet entre devices. |
[FocusMsg] |
chaque transition de focus est un message journalisé ; un événement clavier n'atteint le Modèle que si le journal atteste le focus à ce rang. |
[HeldFromEdges] |
les entrées tenues (touches, pointeur, molette, axes de gamepad) sont toujours dérivées d'edges journalisés, jamais échantillonnées librement par frame. |
[PickKey] |
un pick ne livre que la clé — la coordonnée d'intersection continue est de la présentation et n'atteint jamais le Modèle. Le gameplay décide sur une clé, par construction. |
[SlotGeom] |
la géométrie de slot (un rectangle, un redimensionnement) est de la présentation : elle dimensionne une vue, elle ne décide jamais un verdict. |
[HoverEdge] |
le survol est réduit à des edges discrets de franchissement de barre, ou bien il est tagué présentation. Son payload est acceptable ; sa cadence n'est pas rejouable. |
Avec [DiffAbsolute] (l'oracle est la draw-list absolue, pas la séquence de diffs),
cela rend la draw-list logique rejouable bit pour bit : vous rejouez
(init, messages) → Model → geometry, jamais le framebuffer.
La limite honnête. Le rejeu prouve qu'un journal est cohérent ; il ne prouve pas qu'il est véridique. Un payload poussé par l'hôte et journalisé comme donnée — une clé de pick, un résultat pré-calculé — est re-foldé tel quel, parce que le rejeu ne relance pas le ray-cast ni le kernel pour l'attester. Ainsi un score qui dépend d'un tel résultat exige que le serveur le redérive, sinon le run doit être exclu d'un leaderboard partagé. Il en va de même pour le temps : le temps écoulé dans un run classé est horodaté par l'hôte et substitué au re-fold ; les ticks journalisés du client sont indicatifs.
#Transitions
Une transition interpole le rendu entre deux états déjà calculés. Elle est cosmétique par définition : elle ne peut pas changer une valeur, donc elle ne peut pas repeindre.
fluxon switch(asset) -> morph chart over 500ms { ease: inOutCubic ; stagger: 0.3 ; surplus: collapse }
on click -> focus(view, at: (bar.i, close), zoom: 2.0, over: 600ms, ease: outBack(1.2))
Une transition pilotée par le temps relève de la strate (b) de bout en bout — non rejouable, et exclue de l'oracle. Une transition pilotée par un changement d'état a sa valeur de stabilisation dans l'oracle (la géométrie sur laquelle elle atterrit) tandis que sa trajectoire reste cosmétique.
prefers-reduced-motion est un fait hôte appliqué au compositeur : il saute à l'état de stabilisation.
Parce que la stabilisation est dans l'oracle et que la trajectoire ne l'est pas, le verdict est inchangé — et
l'edge terminal atterrit toujours au même rang de journal, si bien que deux clients honnêtes, l'un avec le mouvement
réduit et l'autre sans, produisent la même trace.
#Entrée
Chaque signal venu du monde entre par un unique point d'ingestion et devient un message.
fluxapp snakePane {
capabilities: [ clock, input:keyboard ]
update(m, msg) = match msg {
Turned(c) -> { model: m with { dir: turn(m.dir, c) }, cmds: [] }
Tick(n) -> { model: advance(m), cmds: [] }
FocusChanged(f) -> { model: m with { focused: f }, cmds: [] }
}
subs(m) = [ OnKey(Turned), OnTick(120, Tick), OnFocus(FocusChanged) ]
}
La doctrine des edges, qui unifie la moitié des invariants ci-dessus : un edge discret journalisé
peut entrer dans le Modèle ; un signal de présentation continu ne le peut jamais. Marquer un élément comme lu, compter
les impressions, suivre la progression de lecture et charger paresseusement une liste sont tous exprimables — en tant qu'
edges (OnVisible(itemKey, threshold, C)), pas en tant que géométrie. Une position de scroll, en tant que
valeur lisible, n'existe pas.
#La membrane de sortie
Tout ce que l'hôte peint passe par une membrane : un sanitizer (le texte comme texte, un nœud inconnu rejeté, les valeurs hors plage clampées), des métriques de texte épinglées, une couche d'accessibilité qui est annotée et inférée, le picking qui renvoie des clés, des assets résolus depuis des clés allowlistées, et la localisation via un catalogue détenu par l'hôte.
Il n'existe aucune route d'un script vers du markup brut, des octets bruts, ou une URL brute. Non parce qu'ils sont filtrés — parce qu'ils ne peuvent pas être nommés.
#Accessibilité : vous annotez, et le kind infère par-dessus
Une scène de pixels est, par défaut, invisible pour un lecteur d'écran. Deux mécanismes y répondent, et ils se composent — le second est un ajout au premier, jamais un remplacement.
L'annotation est dans le langage. Chaque primitive ui porte un
a11y: record{ role, label, desc } optionnel — localisé par token, vérifié par le sanitizer. Un scene{} CANVAS
porte un describe:, son alternative textuelle. Les deux sont des props bornées ordinaires (chaînes et
tokens) : aucune nouvelle sorte, aucune nouvelle grammaire.
fluxdef overlayOf(m) = scene {
when volume > sma(volume, 20) * 2 :
dot { at: (bar.i, high), r: 4, fill: token.spike,
a11y: { role: token.roleMark, label: token.spikeLabel, desc: token.spikeDesc } }
}
Et le kind infère, gratuitement. Au-delà du a11y:/describe: manuel, le kind dimensionnel
d'un plot ou d'un mark auto-dérive un nom accessible, une plage et une sonification — une sixième
sortie inférée, aux côtés de l'overlay, du panneau, de l'échelle, des lignes de référence et de la
couleur que l'inférence de présentation dérive déjà du kind. plot rsi(close, 14) a le kind
osc(0,100), et cela seul suffit à l'hôte pour annoncer « oscillateur RSI, 0 à 100,
actuellement 72, au-dessus du guide 70 », et pour proposer une sonification (la hauteur est la valeur, le pan est le temps).
Aucun effort de l'auteur — exactement comme le panneau et l'échelle n'en coûtent aucun.
Notez ce que lit l'inférence : le kind, pas la géométrie. Le descripteur vit côté hôte, hors de l'oracle, et est sûr vis-à-vis du pare-feu — il lit la scène et ne réécrit jamais dedans.
Quatre extensions complètent le contrat, chacune une prop hôte plutôt qu'un mécanisme du langage :
les régions live (live: Polite | Assertive ; un toast est Polite par défaut), les états de widget
(a11y s'élargit de {role, label, desc} à expanded / checked / selected / disabled /
pressed, dérivés du Modèle comme n'importe quelle autre prop), les relations (controls:,
describedby:, activedescendant:, adressées par clé de nœud — les vnodes à clé sont l'identité),
et le focus intra-panneau (un tabindex baladeur à l'intérieur d'un widget composite, plus le
piège à focus contractuel d'une surface modale). L'ordre de tabulation reste centralisé dans l'unique composant qui touche le DOM,
qui est aussi le seul endroit où prefers-reduced-motion est appliqué.
#Voir aussi
- Plan APP — vues, contributions, slots, et le cycle de vie PORT.
- Canvas — signaux, espaces, événements et l'ensemble gelé de primitives.
- Transitions — le descripteur de transition et la frontière de morph.
- compute — la
Tablequeviz.*consomme. - color — le kind
color, l'interpolation OKLab et les palettes épinglées. - Intégration hôte — descripteurs, registres, et les deux brèches.
net — le réseau comme un flux
Post-v1. Le pilier réseau est scellé en conception ; sa face capabilité (net:fetch,
net:stream) fait partie du contrat du plan APP.
Une connexion réseau est un flux sur l'axe d'arrivée. Ce n'est pas une métaphore — c'est la même affirmation que le langage fait déjà à propos de l'axe des x d'un graphique, appliquée à une horloge différente. Et c'est ce qui permet à un seul mot, flux, de recouvrir requête/réponse, le push serveur, le polling, un flux-de-flux, et la pagination : ils ne diffèrent que par leur régime d'arrivée.
Le script n'ouvre jamais un socket. Il décrit une connexion ; l'hôte — le seul détenteur du descripteur de fichier, de la session TLS et du token — l'ouvre, et livre ce qui arrive sous forme de messages journalisés.
#Le théorème de placement
Où un flux réseau peut-il vivre ? La réponse est forcée, pas choisie :
L'arrivée est non déterministe vis-à-vis du monde — le réseau arrive quand il arrive. Il vit donc entièrement dans le plan APP, où le non-déterminisme entre comme un message journalisé et ne contamine rien. Lire le réseau depuis l'analyse est
[ErrFirewall].La promotion dans l'analyse se fait par exactement une jonction : un
scancausal qui folde les arrivées en barres closes, suivi detoSource, que l'hôte ingère en append-only — sans jamais réviser une barre close. C'est seulement alors que l'opérateur de resample ordinaire s'applique.
Un panneau peut donc lire un flux d'exchange, une passerelle de chat, une rivière RSS. Un indicateur
ne le peut pas — il ne voit jamais que la série causale que toSource a produite. Le pare-feu n'est pas
affaibli par le réseau ; le réseau est routé autour de lui.
#Les cinq verbes
Une connexion est décrite par un spec et consommée à travers les deux portes gelées du plan APP —
les commandes en sortie, les subscriptions en entrée. Aucun verbe ne porte jamais un descripteur de fichier, un token, ou une
URL forgée : l'url est une clé chaîne allowlistée que l'hôte résout, sur un domaine consenti.
| Verbe | Kind | Ce que c'est |
|---|---|---|
request(spec, On) |
Cmd |
requête/réponse ponctuelle ; le résultat re-rentre comme un message via le constructeur qu'elle porte |
subscribe(spec, On) |
Sub |
un flux push entrant (server-sent events, une subscription de socket, un flux polling) |
connect(spec, On) |
Sub |
un canal bidirectionnel persistant ; le compagnon sink(connKey).send(v) émet une commande sortante |
datagrams(spec, On) |
Sub |
des datagrammes non ordonnés, non fiables |
paginate(spec, On, next, maxPages) |
Sub |
déroule une boucle bornée, produisant un flux de pages |
Un spec est un record NetSpec. Vous en construisez rarement un champ par champ : un preset en renvoie un, et
with surcharge les champs qui vous importent.
fluxvariant Msg { Got(f: Recv) | Ping }
app feed {
capabilities: [ net:stream ]
init(p) = { last: na, missed: 0 }
update(m, msg) = match msg {
Got(f) -> match f {
Data(t) -> { model: m with { last: t.price }, cmds: [] }
Dropped(n) -> { model: m with { missed: m.missed + n }, cmds: [] }
_ -> { model: m, cmds: [] }
}
Ping -> { model: m, cmds: [ sink("exchange.ws").send(Ping) ] }
}
view(m) = text("last {fmt.price(m.last)} · missed {m.missed}")
subs(m) = [ connect(ws("exchange.ws") with { codec: Json, schema: Trade }, Got) ]
}
Notez ce que fait le bras _ : l'enveloppe a sept bras, et match est exhaustif, si bien que le
compilateur ne vous laissera pas oublier qu'un socket peut lâcher, prendre du retard, ou être révoqué. Notez aussi
Dropped(n) — le modèle compte ce qu'il n'a jamais vu.
#L'enveloppe d'arrivée
Tout ce qui arrive est enveloppé dans un seul variant, déclaré une fois, et éliminé par match.
Il est montré ici en notation-spec — le <κ> est le métalangage pour « le schéma que vous avez
déclaré », pas de la syntaxe de surface : un record v1 est monomorphe, et l'hôte fournit Recv déjà
spécialisé à votre schema.
variant Recv<κ> {
Data(κ) // a decoded unit — κ is the schema you declared
| DecodeError(field: string, reason: string) // a broken required field — never a silent `na`
| Dropped(count: num) // backpressure evicted this many
| Lagged // the consumer fell behind
| NetErr(class: NetErrClass, reason: string) // a TYPED failure — branch on the class
| SchemaMismatch(expected: string, got: string)// the provider's version left the accepted window
| Revoked(capRef: string) // the capability was revoked mid-session
}
variant NetErrClass {
RateLimited(retryAfter: duration | na) // pace — never hammer
| Unauthorized // re-authenticate; do not blind-retry
| Http(status: num) | Timeout | Dns | Tls | Cors | Refused | Closed(code: num | na)
} // the WS close code drives resume vs re-identify
Pourquoi la classe d'erreur est un variant et non une chaîne. Une politique de reconnexion qui se branche sur le libellé d'un message d'erreur de l'hôte n'est pas portable, pas testable, et — parce que le libellé n'est pas stable octet pour octet entre moteurs — pas rejouable. La classe fait autorité côté hôte et est identique octet pour octet, si bien que
RateLimited(retryAfter)→ cadencer,Unauthorized→ se ré-authentifier,Http(404)→ abandonner,Timeout→ réessayer, est une fonction totale que vous pouvez écrire une fois et à laquelle vous fier.
Une request ponctuelle ne livre jamais Dropped ni Lagged (il n'y a pas de backpressure sur une seule
réponse). Un flux peut livrer les sept bras — et match vous force à les gérer.
#Le cycle de vie est déclaratif
Une subscription présente signifie « cette connexion est voulue » ; la retirer signifie « la fermer ».
Le runtime diffe les subscriptions par leur connKey, exactement comme il diffe tout le reste.
Chaque champ d'un spec porte une classe :
| Classe | Champs | Effet d'un changement |
|---|---|---|
| Hot | backpressure, heartbeat, timeout, filtres de subscription | appliqué sans rouvrir la connexion |
| Reopen | url, protocol, auth, codec, framing | reconnecte |
Changer le connKey lui-même est une reconnexion explicite. Un send sur une clé dont la subscription
a disparu est un no-op qui remonte comme un message — jamais un crash, jamais une écriture sur un socket fermé.
#Codecs
Un codec est une projection d'un kind — pas un parser. Vous déclarez la forme que vous attendez, et l'hôte décode dedans et vérifie le kind à la frontière :
fluxrecord Trade { price: price(BTC, USD) ; qty: volume ; ts: time }
trades = subscribe(ws("exchange.ws") with { codec: Json, schema: Trade }, Got)
codec et schema sont deux champs, pas un. Le codec dit comment les octets sont façonnés ; le
schema dit en quel kind ils décodent. Omettez schema et il est inféré depuis le codec —
mais le nommer est ce qui permet à la vérification de frontière de tenir.
C'est ce qui rend l'interdiction des expressions régulières et du parsing ad-hoc satisfaisable plutôt que
simplement restrictive : vous ne parsez jamais un payload dans le script, parce que le payload arrive déjà
typé. Un champ requis qui est cassé vous donne DecodeError(field, reason) — jamais un
na silencieux qui empoisonne un calcul trois sauts plus loin.
Le décodeur est incrémental et adossé à une arène : il remplit un buffer borné à mesure que les octets arrivent,
zero-copy là où le layout le permet, sans allocation en régime permanent. Le framing (comment un flux d'octets
est découpé en messages) est une couche séparée et composable, si bien qu'un codec peut être réutilisé à travers les
transports. Le catalogue est clos — extensible seulement en tant que liste validée, jamais en tant que grammaire
d'exécution. En v1 c'est Json, XmlFeed (ce avec quoi le preset RSS/Atom décode), Utf8
et Raw, plus les binaires compacts Cbor et MsgPack, les tabulaires Csv et Tsv, et
Url. Post-v1. Md suit le pilier text, avec les protocoles qui le portent.
Chaque codec porte une fenêtre de versions acceptées. Un fournisseur qui dérive hors d'elle produit
SchemaMismatch(expected, got) à la connexion, avant qu'une seule donnée soit livrée — un
« mise à jour requise », plutôt qu'un modèle corrompu.
#La backpressure est déclarée, jamais implicite
Un producteur rapide et un consommateur lent n'est pas un cas limite ; c'est monnaie courante. La politique fait donc partie du spec, et chaque option est totale :
BackPressure est un variant clos de cinq bras, et c'est le champ back: du spec :
| Politique | Comportement |
|---|---|
Latest(n) |
un ring borné des n arrivées les plus récentes — le défaut pour un flux de prix |
DropOldest(n) |
une file bornée de n ; lorsqu'elle est pleine, évincer la plus ancienne |
DropNewest(n) |
une file bornée de n ; lorsqu'elle est pleine, refuser le nouveau venu |
Sample(clk) |
garder-le-dernier à chaque tick d'une horloge de barre — coalescent, pour un flux que vous ne faites qu'échantillonner |
Block |
vraie backpressure, vers l'hôte : l'hôte cesse de lire le socket |
Un flux hot qui ne déclare pas de back prend Latest. Il n'y a pas de file non bornée, donc il n'y a aucun
moyen d'écrire la fuite de mémoire classique où une app bufferise silencieusement un firehose jusqu'à en mourir. Et
parce que l'éviction est comptée — Dropped(n) est un bras de l'enveloppe — le modèle sait toujours
ce qu'il n'a pas vu.
Celle qu'il est facile de rater :
Block. Un fold des ticks vers les barres OHLCV doit voir chaque tick, sinon la barre qu'il construit n'est pas la barre qui a eu lieu.Sampleperdrait silencieusement des ticks entre les edges d'horloge, etLatest(n)les perdrait sous une rafale. Ainsi le foldbars(tf)qui alimente la jonction vers l'analyse déclareback: Block— et le coût de ce choix est explicite et local : l'hôte cesse de lire le socket, plutôt que le script agrège silencieusement un mensonge.
Le rate-shaping est un axe différent et vit ailleurs : throttle / debounce / sample /
dedup / merge sont des combinateurs de l'algèbre de flux, appliqués à un flux que vous avez déjà ;
et pace sur le spec applique un token-bucket aux propres requêtes sortantes du pipeline. La backpressure est ce qui
se passe quand les arrivées dépassent le consommateur — pas la vitesse à laquelle vous choisissez de demander.
La pagination suit la même discipline : une boucle bornée avec un nombre maximum de pages déclaré, ou un pull incrémental piloté par le modèle — jamais une boucle libre sur un nombre inconnu de pages.
#L'asynchronie sans callbacks
Il n'y a pas d'await, pas de promise, pas de callback dans le langage. Un pipeline est décrit,
l'hôte le pilote, et chaque résultat re-rentre comme un message via le constructeur que la commande
portait. C'est toute l'histoire — et c'est la raison pour laquelle le comportement entier d'une application est
reconstructible depuis son journal.
Le coût est honnête et mérite d'être énoncé : une longue chaîne asynchrone devient plusieurs constructeurs de
messages et plusieurs bras d'update, plutôt que trois lignes d'await. Ce que vous y gagnez, c'est que
chaque étape intermédiaire est dans le journal — de sorte que le voyage dans le temps, le rejeu et la vérification côté
serveur fonctionnent tous, ce qu'ils ne peuvent pas si les intermédiaires sont cachés à l'intérieur d'une tâche.
#Protocoles
Le catalogue de protocoles se décompose en cinq axes orthogonaux (transport, framing, encodage,
session, livraison), ce qui explique pourquoi un seul type de spec recouvre tout plutôt qu'une API par
protocole. Un preset est une simple fonction renvoyant un NetSpec avec les défauts raisonnables pour son
protocole déjà posés — un backoff de reconnexion, un heartbeat, un ring de backpressure — et with
surcharge ce dont vous avez besoin. Cela réduit le cas courant à une ligne :
fluxquotes = request(rest("api.example.com") with { codec: Json, schema: Quote }, GotQuote)
events = subscribe(sse("events.example.com") with { codec: Json, schema: Event }, GotEvent)
ticks = connect(ws("stream.example.com") with { codec: Cbor, schema: Tick, back: Latest(256) }, GotTick)
posts = subscribe(rss("blog.example.com/feed", 300s), GotPost)
Les presets sont rest · ws · sse · rss · wt. rss prend son intervalle de polling comme argument,
parce qu'un poll sans cadence déclarée n'est pas un poll ; les autres lisent le leur depuis le transport.
Possible dans le navigateur aujourd'hui : HTTP, server-sent events, WebSocket, WebTransport, et les flux polling. Reporté à un relais : les protocoles qui nécessitent un socket TCP brut — le navigateur ne peut pas en ouvrir un, et prétendre le contraire serait un mensonge dans la documentation plutôt qu'une fonctionnalité.
#Capabilités, auth et gouvernance
Tout le pilier se tient derrière la famille net:*, deny par défaut :
net:fetch(domain)etnet:stream(domain)sont accordées par domaine, avec le consentement de l'utilisateur, et imposées par la content-security policy de l'hôte. Le script ne détient jamais le socket.- L'authentification est détenue par l'hôte. Le token n'entre jamais dans le script — l'app reçoit un handle opaque, et l'hôte attache le credential. Un script fuité ne peut pas faire fuiter un credential qu'il n'a jamais eu.
- L'egress est gouvernable. Un superviseur peut restreindre (jamais élargir) ce qu'une application peut atteindre ; une révocation est journalisée comme une borne, si bien qu'un re-fold la reproduit de manière déterministe et qu'une commande émise après elle échoue en position fermée.
- L'offline est un état de première classe. Une politique de cache par grant, une file de commandes offline bornée
rejouée sous une clé d'idempotence à la reconnexion, et des edges
OnConnectivity— jamais un merge multi-writer silencieux, qui est un non-but nommé.
#La jonction vers l'analyse
flux// APP plane: every tick (`back: Block`), folded into closed bars, handed to the host
feed = connect(ws("exchange.ws") with { codec: Cbor, schema: Trade, back: Block }, Got)
src = feed.bars(tf("1m")).toSource("BTC-ext") // the stream is the receiver — UFCS, no pipe operator
// ANALYSIS plane: it is now an ordinary, causal series
plot ema(series("BTC-ext").close, 20) @ tf("1h")
L'hôte ingère en append-only : une barre close n'est jamais révisée. Ainsi une série alimentée par un socket d'exchange porte exactement la même garantie de no-repaint qu'une série alimentée par le pipeline first-party — non parce que le réseau est de confiance, mais parce que la jonction refuse de le laisser réécrire l'histoire.
#Voir aussi
- Plan APP — commandes, subscriptions, le journal, capabilités.
- Host services — la doctrine du resource-handle que suit ce pilier.
- server — l'autre côté du fil, quand il est nôtre.
- text — pourquoi il n'y a pas d'expression régulière, et ce qui la remplace.
- Intégration hôte —
data:sourceet le contrat d'ingestion causale. - Les quatre plans — le pare-feu que ce pilier contourne.
Host services — la doctrine du resource-handle
Post-v1. Chaque famille de cette page est scellée en conception ; le déploiement suit le langage v1. Le statut est énoncé une fois, ici, et non répété section par section.
Une application qui n'a pas le droit d'ouvrir un fichier, de lire le presse-papiers, de notifier un utilisateur, de le connecter, de prendre un paiement, de redimensionner une image ou de charger une police est une démo, pas un produit. Cette page est la couche à effets qui comble ce manque : fichiers utilisateur, presse-papiers, notifications et réveil programmé, offline, authentification contre votre propre backend, paiements, opérations média et capteurs, impression et PDF, polices personnalisées, et l'exécution d'une autre application à l'intérieur de la vôtre.
C'est une longue liste de choses qu'un script en bac à sable se voit autorisé à faire, et exactement un seul argument de sécurité court sous tout cela. Cet argument est la doctrine du resource-handle, et il vient en premier parce que chaque famille qui le suit est un corollaire plutôt qu'une nouvelle histoire à auditer.
Une note sur les exemples. Une ligne marquée ✗ est un fragment. Flux n'a pas d'instructions-expressions, si bien qu' une telle ligne illustre une règle ; ce n'est pas un programme. Chaque exemple positif est un programme qui parse.
#La doctrine
Normatif, transversal. Un script ne détient jamais une ressource porteuse d'octets. Il détient une clé de handle opaque, à portée de capabilité, à portée de session — un
stringsans aucune structure que le script puisse exploiter — et l'hôte, le seul détenteur des octets, du socket, du token et du buffer de pixels, la résout. Chaque transformation est une opération hôte nommée sur un handle, tirée d'un catalogue clos par famille :Cmd Op(handleIn, params, C)renvoie unmsgportant un nouveau handle ou des métadonnées — jamais du contenu.
#Ce que le script peut détenir
| Ce que le script détient | Ce que l'hôte détient |
|---|---|
une clé de handle (string, opaque, à portée de session) |
les octets d'un fichier, d'une archive, d'une photo |
des métadonnées : name, mime, size, w, h |
le socket, sa session TLS, son descripteur de fichier |
un verdict : Paid, Declined, Ok(rev), Denied |
le token d'auth, le cookie, la boucle de refresh |
une requête — données inertes dans cmds |
le buffer de pixels, l'image décodée, le raster de glyphes |
La frontière avec le code pur est tracée à exactement un seul endroit, et elle est tracée généreusement : un
buffer borné déclaré — buf(N), un vec d'octets sur le substrat bits.* — est
in-script et pur, vous pouvez donc écrire un codec binaire, un checksum sur des données que vous avez calculées, un
encodage bit-packé. Ce qui n'est pas in-script, c'est un fichier utilisateur ou un asset de plateforme : ce sont
des handles plus des ops hôte. La distinction n'est pas « les octets sont dangereux » ; c'est « les octets que vous n'avez pas
créés ne vous appartiennent pas ».
#Pourquoi une seule doctrine plutôt qu'une par famille
Chaque famille aurait pu avoir sa propre histoire de sécurité : une API de fichiers qui assainit les chemins, une API d'image qui valide les dimensions, une API d'auth qui limite la portée des tokens, une API de police qui valide une fonte téléchargée. C'est un audit par famille, une occasion de se tromper par famille — et un endroit de plus, chaque fois que le catalogue grandit, pour qu'une fonctionnalité élargisse un trou que personne ne surveille.
Pourquoi cette règle existe. Sous la doctrine, l'argument est fait une seule fois. Un script ne peut pas exfiltrer ce qu'il n'a jamais détenu ; il ne peut pas forger un handle, parce qu'un handle est résolu contre une table côté hôte indexée par le grant ; il ne peut pas en re-déléguer un, parce qu'il n'existe aucun canal qui porte de l'autorité. Un relecteur lisant une nouvelle famille n'a qu'une chose à vérifier — remet-elle au script la ressource, ou une clé vers elle ? — et si la réponse est « une clé », la famille hérite de tout l'argument de sécurité gratuitement. C'est la propriété qui permet au catalogue de grandir sans que la surface d'attaque grandisse avec lui.
Trois corollaires suivent immédiatement, et ce sont eux que vous ressentez en programmant :
- Les handles sont opaques. Il n'y a aucun verbe qui en résout un vers du contenu. Pas filtré — absent du catalogue. Le bug d'exfiltration classique (lire le fichier, le poster sur mon serveur) nécessite une étape qui n'a pas de nom.
- Les handles sont à portée de session. Ils ne survivent pas à un rechargement, c'est pourquoi persister un
document signifie persister votre propre état sérialisé, jamais un handle du système d'exploitation
retenu. Un handle écrit dans la partition
docpersistée compile, et il est mort au lancement suivant — l'erreur que le scoping est conçu pour rendre peu coûteuse à trouver. - Chaque résultat est un message. Une op hôte revient via le constructeur de complétion qu'elle portait, si bien que toute la chaîne est dans le journal et se rejoue à l'identique.
fluxbytes(f.handle) // ✗ no such verb — nothing resolves a handle to content
f.handle.pixels // ✗ no such field — pixels are host-side, always
Décision ouverte. La durée de vie d'un handle à travers suspend/resume est laissée ouverte par le plan ; à portée de session avec un re-pick explicite au resume est la recommandation, pas encore un arbitrage.
#Le moule
Chaque ligne du catalogue a la même forme, ce qui explique pourquoi elle s'apprend en une seule séance plutôt que famille par famille :
- L'intention sort comme un
Cmdinerte, sous une capabiliténamespace:verbdeny par défaut. La commande est de la donnée — une clé de handle, un nom, un id de template, une quantité. Elle ne porte aucune ressource. - Le résultat revient comme un
msgjournalisé, via le constructeur de complétion que la commande portait. Il n'y a pas de callback, pas de promise, pas d'await. - Un token d'epoch absorbe l'obsolescence. Une commande porte un scalaire fourni par l'app ; l'hôte le renvoie en écho verbatim ; un résultat dont l'epoch ne correspond plus est écarté par le bras qui le reçoit.
- La révocation emprunte la membrane. Un grant retiré en cours de session écrit une borne
CapRevokeddans le journal ; une commande avec un constructeur de complétion se voit répondre[ErrCapRevoked]à travers lui, et une commande fire-and-forget est écartée et auditée.
Une application complète, de bout en bout — choisir une image, demander à l'hôte ce que c'est, sauvegarder une copie :
fluxapp thumbnailer {
capabilities: [ file:pick, file:save, image:ops ]
init(p) = { src: na, out: na, w: 0, h: 0 }
update(m, msg) = match msg {
Choose -> { model: m, cmds: [ FilePick(["image/png"], 4000000, Picked) ] }
Picked(f) -> { model: m with { src: f.handle },
cmds: [ ImageOp(f.handle, Meta, Info) ] }
Info(d) -> { model: m with { w: d.w, h: d.h }, cmds: [] }
Save -> { model: m, cmds: [ FileSave(m.src, "copy.png", Saved) ] }
Saved(ok) -> { model: m with { out: ok }, cmds: [] }
Cancelled -> { model: m, cmds: [] }
}
view(m) = row { button("choose…", Choose) ; text("{m.w}×{m.h}") ; button("save", Save) }
subs(m) = []
}
m.src est un string. C'est la représentation entière d'une image de deux mégaoctets à l'intérieur de ce
programme. Remarquez ce qui est absent : pas de buffer, pas de décodage, pas de try, pas de nettoyage — et aucun moyen pour une
dépendance compromise de lire l'image de l'utilisateur, parce que rien dans la portée ne le peut.
fluxcmds: [ Notify("hello", args, Tapped) ] // ✗ [ErrCapDenied] — notify:send is not in capabilities:
#Le catalogue de capabilités
Chaque ligne est deny par défaut, atténuée par l'hôte, et graduée par la confiance. Cette table est la carte ; les sections qui suivent donnent à chaque famille son grant, son atténuation, et sa limite honnête.
| Capabilité | Accorde | Atténuation hôte |
|---|---|---|
file:pick / file:save / file:drop |
picker natif, save-as et téléchargement, drops du système d'exploitation | allowlist de mime, plafonds de taille, quota ; handles à portée de session, jamais d'octets |
clip:read / clip:write |
le presse-papiers (texte en v1) | gaté par geste ; une lecture est soumise à une invite ; le contenu collé est de la donnée, assainie au rendu |
notify:send |
Notify, SetBadge, ClearBadge |
contenu templatisé uniquement, rate-limité, consentement selon le modèle de permissions de la plateforme |
schedule:wake |
ScheduleWake |
déclenché par l'hôte ; une app fermée est relancée et son payload est le premier message journalisé |
net:offline |
politique de cache par grant, une file de commandes offline, OnConnectivity |
rejouée sous une clé d'idempotence ; file bornée ; pas de merge multi-writer |
auth:passkey / auth:session |
la cérémonie WebAuthn ; des sessions sur votre propre backend | les cérémonies et les formulaires sont vérifiés par l'hôte ; le token est détenu par l'hôte ; l'app voit un handle |
pay:checkout |
Pay, Sub OnEntitlement |
le checkout du fournisseur s'exécute côté hôte ; l'app ne voit jamais l'instrument ; les vendeurs sont vérifiés par le prestataire |
image:ops / capture:photo / capture:qr |
ops nommées sur des handles ; capture caméra ; décodage QR | catalogue clos, pixels jamais in-script ; consentement par capture |
geo:read / motion:edges |
position ponctuelle et surveillée ; edges de mouvement | grossière par défaut ; edges discrets uniquement ([HeldFromEdges]) |
share:generic |
Share(record{ text?, urlRef?, fileHandle? }) |
la feuille de partage de la plateforme : visible et gatée par geste avant l'envoi |
doc:print |
Print, ExportPdf, ExportImage |
rendu paginé par l'hôte d'un arbre déjà assaini ; la sortie est un handle de fichier |
asset:font |
des assets de police validés, par app | métriques épinglées ([TextMetric]) ; un fallback déclaré ; jamais de reflow silencieux |
ui:embed |
appView(appId, params?) |
realm enfant, journal et grants — isolés, jamais hérités |
display:awake |
wake-lock d'écran | visible-seulement, révocable |
#Fichiers et données utilisateur — file:*
Les éditeurs ouvrent et sauvegardent des documents ; les forums attachent des fichiers ; les tableaux de bord importent et exportent. Trois verbes couvrent cela.
| Verbe | Kind | Livre |
|---|---|---|
FilePick(accept, maxBytes, C) |
Cmd |
le picker natif → record{ handle, name, mime, size }, ou Cancelled |
FileSave(handle, suggestedName, C) |
Cmd |
save-as / téléchargement ; le contenu est un handle, ou du texte sérialisé par l'app sous quota |
| un drop sur le panneau | msg livré par Sub |
le même record{ handle, name, mime, size } |
accept est une allowlist close de types mime ou d'extensions, plafonnée par le grant — pas un
motif que le script compose. La zone de drop est la surface propre du panneau et rien au-delà
([SurfaceConfine]) : une application produit des pixels, et accepte des drops, seulement là où elle détient le
bail.
Les ops hôte sur un handle de fichier forment un catalogue clos — un hash SHA-256, le pack et l'unpack d'archive (à taille plafonnée), et les ops d'image de la famille média. Les fichiers texte décodent uniquement à travers des codecs déclarés (CSV, Markdown, JSON) : le payload arrive déjà typé, ce qui rend l' absence d'un moteur d'expressions régulières une règle vivable plutôt qu'une épreuve. Voir text.
Le transport d'un fichier — un upload reprenable, un téléchargement par plage, la progression — n'est pas ici. C'est
net, à travers Sub OnTransfer(reqKey, C). Cette capabilité fournit le handle ; le
pilier réseau fournit le tuyau. La séparation est délibérée : elle garde une seule histoire sur la
backpressure, les retries et l'idempotence au lieu de deux.
La limite honnête. Un handle ne survit pas à la session. Une application qui veut « rouvrir le dernier document » persiste son propre état et le redérive, ou demande à l'utilisateur de choisir à nouveau. Il n'y a aucun handle du système d'exploitation retenu, parce qu'un handle retenu est de l'autorité ambiante avec un joli nom.
#Le presse-papiers — clip:*
Cmd ClipWrite(text)— texte en v1 ; images par handle plus tard.Cmd ClipRead(C)→msg(string).
Les deux sont gatés par geste : ils s'exécutent à l'intérieur d'une action utilisateur, et une lecture est soumise à une invite. Rien n'est ambiant — il n'y a pas d'événement de presse-papiers sans la capabilité, et il n'y a pas de sniffing de format dans le script.
Pourquoi un collage n'est pas un danger. Le contenu collé arrive comme de la donnée, et la donnée est assainie là où elle est rendue, comme n'importe quelle autre chaîne : la vue est un arbre de primitives validées, pas du markup. Un collage dans un éditeur de texte riche passe par le protocole d'édition de text, qui est une opération sur un modèle de document, pas une injection d'octets dans un DOM. Le presse-papiers est donc une source de messages ordinaire, et la discipline de messages habituelle constitue toute sa défense.
#Notifications, badges et réveil programmé — notify:*, schedule:*
C'est la boucle de rétention de tout ce qui est social — quelqu'un a répondu, une mention, un rappel — et le canal de livraison qu'une alerte purement locale n'a pas.
| Verbe | Kind | Ce qu'il fait |
|---|---|---|
Notify(template, args, clickMsg) |
Cmd |
rend une notification de plateforme à partir d'un template ; un tap livre clickMsg |
SetBadge(n) / ClearBadge |
Cmd |
le badge de l'icône d'application |
ScheduleWake(at, payload, C) |
Cmd |
l'hôte se déclenche à at ; si l'app est fermée elle est relancée |
toast(…) |
ui |
chrome in-app, avec un contrat aria-live — une primitive de vue, pas cette capabilité |
Le contenu est templatisé, jamais une chaîne libre remise à la couche plateforme. L'id de template et ses arguments sont vérifiés contre un catalogue déclaré par l'application ; l'hôte le rend.
Pourquoi des templates et non des chaînes. Une chaîne libre passant dans une surface du système d'exploitation est le seul canal qu'une application pourrait utiliser pour dire quelque chose que la plateforme attribuera à nous — une fausse invite système, un faux avertissement de sécurité, une ligne de phishing rendue dans le chrome propre de la plateforme. Les templates rendent cela inexprimable tout en laissant le cas légitime (un argument substitué dans une phrase que vous avez écrite et que nous avons validée) entièrement ouvert. La même discipline gouverne
share:generic, avec un assouplissement, et pour une raison énoncée : la feuille de partage est visible et gatée par geste, si bien que l'utilisateur lit et peut éditer le payload avant qu'il ne parte.
#La clause du message de lancement
Un réveil se déclenche pendant que l'app est ouverte : un message ordinaire. Un réveil se déclenche pendant que l'app est fermée : l'hôte relance l'app — et ensuite ?
Règle de livraison normative. Un (re)lancement initié par l'hôte — un tap de notification, un réveil programmé, un deep link — livre son payload comme le(s) premier(s) message(s) journalisé(s) de la nouvelle session, via le constructeur déclaré, ordonné avant toute autre livraison de subscription.
Cela mérite d'être énoncé précisément parce que ça ressemble à une possible troisième exception à « update
est le seul producteur d'un Modèle », aux côtés du voyage dans le temps et de la migration. Ce n'en est pas une.
- Le payload ne devient pas un Modèle. Il devient un message, en tête d'un journal tout neuf.
inits'exécute toujours en premier ;updatefolde toujours ; le journal reste l'unique source de vérité.- Un démarrage à froid est donc rejouable comme n'importe quel autre run : re-folder le journal reproduit le lancement exactement, parce que le lancement est dans le journal.
Ce que la clause corrige réellement, c'est l'ordonnancement. Sans elle, un payload de lancement pourrait s'entrelacer avec le premier tick ou le premier edge de connectivité différemment sur deux machines, et le re-fold divergerait. Épingler le payload au rang zéro rend le démarrage à froid déterministe.
fluxapp reminders {
capabilities: [ notify:send, schedule:wake ]
init(p) = { queued: 0, resumed: na }
update(m, msg) = match msg {
Arm(t) -> { model: m with { queued: m.queued + 1 },
cmds: [ ScheduleWake(t, "daily-review", Woke) ] }
Woke(payload) -> { model: m with { resumed: payload },
cmds: [ Notify("review-due", payload, Tapped) ] }
Tapped(hit) -> { model: m with { resumed: hit }, cmds: [] }
}
view(m) = col { text("queued: {m.queued}") }
subs(m) = []
}
La limite honnête. Il n'y a pas d'exécution continue en arrière-plan. Un réveil est un relancement plus un
message, pas un processus qui tournait pendant que vous ne regardiez pas. Rester en vie caché est
une capabilité différente, plus étroite (display:keepalive). Tout le reste serait une promesse que le
navigateur ne nous laisse pas tenir.
Post-v1. Le push d'origine serveur — l'app fermée, à travers les devices — est la moitié fan-out de cette
famille et vit dans server (push:send). Cette section est la moitié locale.
#Offline, cache et connectivité — net:offline
Le mécanisme est spécifié dans net ; sa face capabilité appartient à cette page, parce que c'est ce qu' une application demande réellement.
| Élément | Comportement |
|---|---|
Sub OnConnectivity(C) |
online / offline / limité, comme des edges discrets journalisés — jamais un signal continu |
| une politique de cache par grant | les réponses servies depuis le cache sont de la donnée comme les autres ; la fraîcheur est remontée |
| une file de commandes offline bornée | une commande émise hors ligne est mise en file côté hôte et rejouée à la reconnexion sous une clé d'idempotence |
La file a un plafond déclaré. Le débordement est un message à l'application, pas un gonflement silencieux — la même discipline que toute autre structure bornée. Les messages de complétion arrivent en retard, et le token d'epoch absorbe ceux qui n'importent plus.
Un non-but nommé. Le merge multi-writer offline (types de données répliqués sans conflit) n'est pas dans le périmètre. La file est mono-utilisateur, rejouée dans l'ordre. Deux personnes éditant le même document hors ligne et se réconciliant à la reconnexion est un produit différent avec un cœur différent, et prétendre qu'une file le résout serait un mensonge raconté dans une API.
#Identité pour votre propre backend — auth:*
Toutes les applications ne se connectent pas via un fournisseur connu. Beaucoup ont des comptes sur un backend que leur auteur exploite. C'est à cela que sert cette famille.
| Verbe | Kind | Ce qui se passe |
|---|---|---|
AuthPasskey(action, rpRef, C) |
Cmd |
l'hôte réalise la cérémonie WebAuthn ; le script reçoit un handle de session opaque et un verdict |
AuthLogin(formRef, C) |
Cmd |
les credentials sont collectés dans une surface de formulaire vérifiée par l'hôte et échangés contre votre endpoint déclaré |
Sub OnSession(C) |
Sub |
le cycle de vie de la session, sous forme de messages |
Cmd Logout |
Cmd |
y met fin |
fluxvariant SessionEvent { Established(h: string) | Refreshed(h: string) | Expired | LoggedOut }
fluxapp forum {
capabilities: [ auth:passkey, auth:session ]
init(p) = { session: na }
update(m, msg) = match msg {
SignIn -> { model: m, cmds: [ AuthPasskey(Login, "forum.example", Signed) ] }
Signed(h) -> { model: m with { session: h }, cmds: [] }
SignOut -> { model: m with { session: na }, cmds: [ Logout ] }
Session(e) -> match e {
Established(h) -> { model: m with { session: h }, cmds: [] }
Refreshed(h) -> { model: m with { session: h }, cmds: [] }
Expired -> { model: m with { session: na }, cmds: [] }
LoggedOut -> { model: m with { session: na }, cmds: [] }
}
}
view(m) = col { when is_na(m.session): button("sign in", SignIn) }
subs(m) = [ OnSession(Session) ]
}
Trois choses sont absentes de ce programme, et leur absence est le design.
Le token. Il est détenu par l'hôte. m.session est un handle opaque ; l'hôte attache le credential
aux requêtes sortantes sous le grant. Le refresh est automatique, côté hôte, et ne remonte que comme
Refreshed(h). Un script fuité ne peut pas faire fuiter un credential qu'il n'a jamais eu — l'argument exact
que net fait à propos du socket, généralisé.
Le mot de passe. L'entrée sensible est collectée dans une surface de formulaire vérifiée par l'hôte et ne transite jamais par la mémoire du script. C'est le même précédent qu'un sélecteur de wallet : la surface qui prend le secret n'est pas une surface que l'application a dessinée.
Le matériel de clé. Pour un passkey il n'y a pas de clé côté script, ni côté hôte non plus — elle vit dans l'authentificateur de la plateforme. La cérémonie est celle de l'hôte ; le script obtient un verdict.
fluxm with { token: e.token } // ✗ no such field — a session event carries a handle, not a credential
L'identité exposée à l'application reste opaque par paire : une application apprend un identifiant stable pour elle-même, pas un qui corrèle un utilisateur à travers les applications. L'exception est définitionnelle et inévitable — si le compte est sur le backend propre de l'application, l'application est le fournisseur d'identité.
#Paiements — pay:*
| Verbe | Kind | Livre |
|---|---|---|
Pay(sku, qty, C) |
Cmd |
checkout médié par l'hôte dans la surface propre du fournisseur → un verdict |
Sub OnEntitlement(C) |
Sub |
des entitlements et abonnements vérifiés par le serveur |
fluxvariant PayVerdict { Paid(receiptRef: string) | Declined | Cancelled | Pending }
def afterPay(m, v) = match v {
Paid(r) -> m with { ui: m.ui with { pending: 0 }, doc: m.doc with { receipt: r } }
Declined -> m with { ui: m.ui with { pending: 0 } }
Cancelled -> m with { ui: m.ui with { pending: 0 } }
Pending -> m
}
Le script ne voit jamais l'instrument. Pas un numéro de carte, pas un token, pas une redirection qu'il pourrait altérer : le checkout s'exécute dans la surface côté hôte du fournisseur, et l'application reçoit un verdict et une référence de reçu — un autre handle opaque, que le serveur valide (voir server).
Les montants sont decimal money[Q] — virgule fixe exacte, portant la devise de cotation comme un tag inféré
plutôt que comme syntaxe de surface. La raison n'est pas la minutie : la virgule flottante binaire ne peut pas
représenter 0.10, et un prix erroné d'un ulp est un bug que vous découvrez dans un rapprochement
comptable des mois plus tard. Voir asset & currency et le namespace decimal.*
dans compute.
Pending est un bras de première classe, pas une erreur. Certaines méthodes de paiement se règlent de manière asynchrone ; le
verdict le dit, et l'entitlement arrive plus tard via OnEntitlement. Une application qui
accorde l'accès sur Paid seul et n'écoute jamais l'entitlement a un bug que le système de types
ne peut pas attraper — mais match vous force au moins à regarder Pending.
Décision ouverte. L'ensemble de fournisseurs pour la v1 est ouvert. La capabilité est agnostique vis-à-vis du fournisseur par conception ; quels fournisseurs sortent en premier n'est pas arrêté.
#Média, capture et capteurs — image:*, capture:*, geo:*, motion:*
#Les opérations d'image sont un catalogue clos
fluxvariant ImgOp { Resize(w: num, h: num, fit: fit) | Crop(area: rect) | Rotate(quarter: num)
| Filter(preset: string) | Meta }
Cmd ImageOp(handle, op, C) prend un handle et une opération nommée, et renvoie un nouveau handle
ou des métadonnées (dimensions, mime). C'est toute la surface.
Pourquoi le catalogue est clos. L'alternative, ce sont des pixels dans le script — un tableau que le programme lit et écrit, et donc une convolution, une FFT, un noyau de filtre que vous avez écrit vous-même. C'est une bonne chose pour un langage d'avoir cela et une mauvaise chose pour celui-ci : cela met une boucle non bornée et dépendante des données au milieu d'un langage total, et cela remet à un script en bac à sable le contenu de la photographie d'un utilisateur. Un catalogue clos d'ops nommées est la forme qui est à la fois compatible avec le bac à sable et honnête — et le traitement du signal 2D général in-script est un non-but nommé, pas une omission.
#Capture
Cmd CapturePhoto(C) produit un handle d'asset. Cmd ScanQr(C) produit msg(string) — un payload
décodé, livré comme de la donnée. Les deux sont gatés par consentement à chaque capture. Ni l'un ni l'autre n'est le chemin d'appel
audio/vidéo, qui est un profil réseau et vit dans net.
Décision ouverte. Le fait que capture:* sur un navigateur de bureau sans caméra dégrade vers un fallback
de file-pick déclaré est laissé ouvert par le plan.
#Les capteurs livrent des edges, jamais des échantillons
fluxapp tracker {
capabilities: [ geo:read ]
init(p) = { fixes: 0, last: na }
update(m, msg) = match msg {
Moved(pos) -> { model: m with { fixes: m.fixes + 1, last: pos }, cmds: [] }
}
view(m) = col { text("fixes: {m.fixes}") }
subs(m) = [ OnGeo(60000, Moved) ] // at most one fix a minute
}
Cmd GeoOnce(accuracy, C) lit une position une fois ; Sub OnGeo(minInterval, C) surveille. Le consentement est
grossier par défaut — une position accordée est un voisinage sauf si l'utilisateur en décide autrement.
L'orientation et le mouvement n'exposent que des subscriptions à edges discrets : un franchissement de seuil, un geste de secousse. Il n'y a pas de cap continu, pas d'échantillon d'accéléromètre par frame.
Pourquoi des edges et non des échantillons. C'est
[HeldFromEdges], et c'est une règle de déterminisme avant d'être une règle de vie privée. Un Modèle est un fold sur des messages journalisés ; le fold doit se reproduire bit pour bit au rejeu, sur un autre device, à une autre fréquence de frame. Un échantillon de capteur continu n'est rien de tout cela — sa cadence dépend du matériel et de la charge, si bien que deux rejeux de la même session verraient des nombres de messages différents et divergeraient. Un edge discret (« le seuil a été franchi », « le device a été secoué ») est déterministe en rang : c'est un message comme un autre. Un échantillon tenu qui atteint un champ du Modèle que lit un verdict est[ErrFirewall], vérifié à la compilation — la même règle qui garde la position du pointeur et le survol hors du score d'un jeu.
#Partage et wake-lock
Cmd Share(record{ text?, urlRef?, fileHandle? }) ouvre la feuille de partage de la plateforme — visible,
gatée par geste, éditable par l'utilisateur avant l'envoi, ce qui est exactement pourquoi un champ text libre est
admis ici et refusé à Notify. display:awake garde l'écran allumé tant que le panneau est
visible, et est révocable.
#Impression et export de document — doc:print
| Verbe | Sortie |
|---|---|
Cmd Print(viewRef) |
l'hôte rend l'arbre retenu du panneau vers un média paginé |
Cmd ExportPdf(viewRef, C) |
le même rendu, livré comme un handle de fichier → sauvegarder ou partager |
Cmd ExportImage(sceneRef, format, C) |
une scène vers PNG (raster) ou SVG (émis par le sanitizer), comme un handle de fichier |
L'hôte possède la pagination : la taille de page, les en-têtes et pieds de page sont des tokens de template ; les indications de saut de page sont des propriétés de conteneur cosmétiques sur la strate de présentation, si bien qu'elles ne peuvent jamais changer ce que le document est, seulement où il se coupe. Il n'y a ici aucune nouvelle surface de script — l'arbre en cours d' impression a déjà été assaini pour l'écran, et l'impression est un second renderer sur la même valeur. L'export atterrit comme un handle de fichier, et la doctrine prend le relais à partir de là : le script ne touche jamais non plus les pixels produits.
#Typographie — asset:font
Une police est un asset chargé et validé par l'hôte, keyé et sous quota comme n'importe quel autre, qui étend l'
allowlist de tokens de typographie pour cette application uniquement. Il n'y a pas de @font-face, et pas d'URL de
police — cela reste inexprimable.
Pourquoi une police est un problème de déterminisme. Les métriques de texte alimentent le layout. Si deux devices résolvent une police différemment — un fallback ici, une fonte légèrement différente là — le même programme produit un arbre de boîtes différent, et la géométrie que l'oracle vérifie octet pour octet cesse de correspondre. Ainsi chaque police, à chaque version, atterrit avec des métriques épinglées rejoignant l'ensemble
[TextMetric]: la routine de mesure est partagée par l'interpréteur, le module compilé et le serveur, et un layout ne peut pas subir de reflow différent sur deux machines. Une police manquante n'est pas une substitution silencieuse : le fallback déclaré s'applique et un diagnostic est levé.
C'est tout l'assouplissement. La typographie personnalisée était exclue du pilier display pour exactement cette raison, et cette capabilité est sa réintroduction sanctionnée — asset validé, métriques épinglées, fallback déclaré.
#Composition d'apps à l'exécution — ui:embed
appView(appId, params?) -> ui instancie une autre application dans un slot enfant : son propre realm
WASM, son propre journal, ses propres grants.
fluxapp dashboard {
capabilities: [ ui:embed ]
init(p) = { child: "clock-widget" }
update(m, msg) = match msg { Swap(id) -> { model: m with { child: id }, cmds: [] } }
view(m) = col { appView(m.child, { theme: "dark" }) }
subs(m) = []
}
Pourquoi l'embarquement ne peut pas amplifier l'autorité. Rien n'est hérité — dans aucune direction. L' enfant ne reçoit pas les grants du parent, si bien qu'une mini-app hébergée ne peut pas emprunter l'accès réseau du tableau de bord ; et le parent ne reçoit pas ceux de l'enfant, si bien qu'un embarqueur ne peut pas récolter ce qu' un utilisateur a accordé à l'app qu'il héberge. Les deux communiquent à travers exactement deux canaux déclarés — les
paramspassés au montage, et le bus de contexte gaté par capabilité — et ni l'un ni l'autre ne porte de Modèle, de journal, ou de capabilité. La composition est donc plate vis-à-vis de l'autorité : le manifeste d'une application est ce qu'il est, quel que soit celui qui l'héberge.
Le cycle de vie de l'enfant emprunte le port du slot — mount, suspend, dispose — le cycle chartView
généralisé. Un enfant crashé montre la carte d'erreur du gestionnaire de layout dans son propre slot, et l'
embarqueur continue de tourner : l'isolation des fautes est par realm.
Les consommateurs sont les plus évidents : un tableau de bord hébergeant des mini-applications, une page de cours embarquant
un exercice en direct, une marketplace composant une app dans une app. Cela complète la composition au niveau
des packages — les packages composent du code, appView compose des programmes en cours d'exécution.
Décision ouverte. Le plafond de profondeur d'embarquement est ouvert ; une profondeur de un est la recommandation pour la v1.
#Ce qui reste inexprimable
Rien sur cette page n'assouplit un invariant. Chaque famille ajoute une ligne à la table de capabilités et
rien d'autre — aucune sorte de treillis, aucun symbole de grammaire, aucune flèche, et aucun changement au pare-feu ni à la
totalité. Et pour chaque palier, de confiance ou non, ceux-ci n'ont toujours pas de nom dans le langage : eval et la
génération de code, le DOM brut, un socket brut, un client de base de données brut, un token, un cookie, un store
global, et les octets derrière un handle.
Un palier de confiance accorde des effets. Il ne desserre jamais la causalité, le no-repaint (une valeur, une fois produite pour un pas, ne change jamais), la totalité ni le pare-feu — c'est pourquoi une application qui peut imprimer, payer et prendre une photographie est toujours une application dont le comportement entier est un fold sur son journal.
#Voir aussi
- Plan APP — le modèle de capabilités, les commandes, les subscriptions, le journal.
- net — le socket que l'hôte détient, les transferts et la progression, la file offline.
- server — le fan-out de push, la validation de reçu, et le stockage partagé.
- display — les primitives de vue,
toast, les slots et le sanitizer à travers lequel ces capabilités font leur rendu. - text — les codecs déclarés et le protocole d'édition par lequel passe un collage.
- Garanties — ce que la sécurité par capabilités promet réellement, et comment c'est vérifié.
server — l'application, tournant sans écran
Post-v1. Le plan serveur est scellé en conception ; son déploiement suit le langage v1 et est conditionné à son premier consommateur. Le statut est énoncé une fois, ici, et non répété section par section.
Tout ce qui a été décrit jusqu'ici s'exécute sur le client. C'est une position réelle, tenue honnêtement — mais elle laisse une liste de besoins qui ne cessent d'arriver de directions différentes et se révèlent, à l'inspection, être un seul design manquant : un leaderboard capable de prouver qu'un score a été mérité, un forum qui doit stocker des posts quelque part, un webhook qui doit atterrir quelque part, une page publique qu'un crawler peut lire, une notification push vers un device dont l'app est fermée.
Cette page est ce design unique. Son affirmation centrale est plus modeste qu'elle n'en a l'air et plus grande en
conséquence : une application serveur est la même application, headless. Pas un second langage,
pas un second runtime, pas un second modèle mental — le même bloc app, sans view.
#La thèse — un seul plan, les consommateurs qu'il unifie
| Consommateur | Ce dont il a besoin de ce plan |
|---|---|
| Ré-exécution grader / anti-triche | rejouer le journal d'un client, côté serveur, et rendre un verdict |
| État durable partagé pour applications tierces | un endroit où un forum, un board ou une room peut vivre |
| Webhooks entrants | une URL qu'un fournisseur de paiement ou un vendeur de données peut appeler |
| Livraison d'alerte et de push à distance | atteindre un device dont l'application est fermée |
| Validation de reçu et d'entitlement | vérifier un achat quelque part où l'acheteur ne peut pas éditer |
| Facturation de flux au compteur | compter ce qui a été consommé, là où le compteur peut être de confiance |
| Miroir de gouvernance | un audit append-only, chaîné par hash, d'une flotte |
| Prérendu SEO | servir une page publique lisible sans rien exécuter |
| Exécution côté serveur sous licence | exécuter un module sur notre infrastructure sous sa licence |
| Séquenceur de collaboration always-on | une autorité unique qui ordonne les éditions concurrentes |
Dix besoins ; un seul plan. La raison pour laquelle ils s'effondrent en un seul, c'est que les dix sont des folds — un init,
un flux de messages, un update total — et que celui qui doit aussi faire un rendu recourt à un
view pur et rien de plus. Le client possède déjà cette machine, et il possède déjà un oracle
qui prouve que deux moteurs l'exécutent identiquement. Le plan serveur est cette machine, hébergée.
Post-v1. Un relais média un-vers-plusieurs est l'exception : c'est de l'infrastructure média plutôt qu'un fold, et il est reporté en dernier.
#Le substrat — un backend hébergé, pas de second prestataire
Le plan est bâti sur le backend que la plateforme fait déjà tourner, et n'en introduit pas de second :
| Élément | Rôle |
|---|---|
| Postgres managé avec row-level security | stockage durable, avec la discipline de propriété imposée dans la base de données |
| un runtime d'edge function qui exécute WebAssembly nativement | l'hôte d'exécution pour une application headless |
| des buckets d'objets | sources publiées et modules compilés, exactement comme le pipeline de publication client les utilise |
| un feed de changements temps réel | le transport sous Sub OnSharedChange |
L'unité de déploiement n'est pas « un build ». C'est la closure de build épinglée :
build-hash = source · lockfile (the transitive module-hash closure) · compiler version
· pinned routines · the canonical memory plan
Le rebuild gate — la vérification qui se dresse déjà entre un changement de source et un module livré — devient le deploy gate. Un serveur n'exécute jamais des octets qui diffèrent des octets que le client a vérifiés. Cette seule phrase est ce sur quoi repose le reste de cette page, et c'est la raison pour laquelle l' histoire du déterminisme survit au passage hors du device.
#Zéro changement de langage — un app sans view
Un worker serveur est un bloc app avec init, update et subs, un ensemble de capabilités serveur, et un
journal détenu par le serveur. Aucun nouveau modèle d'écriture. Aucun code serveur en forme libre. L'Elm Architecture
(TEA) que décrit le plan APP en est la totalité, moins le
membre qui peint.
fluxapp grader {
capabilities: [ storage:shared ]
init(p) = { graded: 0, rejected: 0 }
update(m, msg) = match msg {
Scored(r) -> { model: m with { graded: m.graded + 1 },
cmds: [ SharedPut(r.runId, r.verdict, Written) ] }
Written(v) -> match v {
Ok(rev) -> { model: m, cmds: [] }
Conflict(rev) -> { model: m with { rejected: m.rejected + 1 }, cmds: [] }
Denied -> { model: m with { rejected: m.rejected + 1 }, cmds: [] }
QuotaExceeded -> { model: m with { rejected: m.rejected + 1 }, cmds: [] }
}
}
subs(m) = [ OnQueue("runs", Scored) ]
}
C'est un serveur complet. Il n'a pas de view, et le compilateur ne le regrette pas : les cinq membres
sont optionnels, et un programme qui ne fait jamais de rendu n'en déclare jamais un.
Pourquoi ce n'est pas une simple commodité. Parce qu'
updateest pur, total et déterministe, deux problèmes opérationnels qui nécessitent normalement de l'ingénierie sur mesure cessent d'être des problèmes. Le scaling horizontal : n'importe quelle instance peut servir n'importe quel message, parce qu'il n'y a pas d'état caché à migrer — le Modèle est un fold. La récupération après crash : une instance qui meurt est remplacée par une qui re-folde le journal depuis le dernier checkpoint. Ce sont les mécanismes propres du client — checkpoint et journal — tournant sur une machine avec une adresse différente.
#La seule chose que le serveur doit décider et que le client n'a jamais eu à décider
Un fold pur est déterministe étant donné un ordre. Deux instances edge recevant des livraisons concurrentes ne s'accordent pas sur un ordre par le simple fait d'être pures — la pureté n'est pas un algorithme de consensus, et la traiter comme tel est l'erreur classique.
Ainsi l'autorité d'ordonnancement est le substrat de stockage partagé, et c'est le seul. Une
révision (rev) est assignée par le backend à l'écriture, jamais élue par une instance. Les livraisons de
subscription concurrentes se sérialisent à travers cette assignation, et le journal que chaque instance re-folde
est celui ordonné de manière autoritaire.
#Le catalogue de subscriptions serveur
| Subscription | Livre |
|---|---|
OnWebhook(path, C) |
un appel HTTP entrant, décodé à la frontière contre le schéma que l'app a déclaré |
OnSharedChange(scope, C) |
un changement dans une collection partagée, sous forme de messages journalisés |
OnJob(spec, C) |
un run programmé — le jumeau serveur de schedule:wake |
OnQueue(name, C) |
un élément de travail |
L'ingress obéit à la discipline que le pilier réseau a déjà établie : un payload de webhook est
décodé contre un schéma déclaré, et un champ requis cassé est un message typé, jamais un
na silencieux qui corrompt un Modèle trois sauts plus loin. Voir net.
#Les lignes de commande serveur
| Capabilité | Accorde |
|---|---|
storage:shared |
les verbes de collection ci-dessous |
mail:send |
email transactionnel templatisé — templatisé, pour la raison que donne host services |
push:send |
fan-out Web-Push vers les devices enregistrés — la moitié livraison du notify:* du client |
net:fetch |
egress, sous le même modèle de grant par domaine que le client, avec un budget d'egress serveur |
pay:* |
validation de reçu : le Paid(receiptRef) du client remet une référence, et le serveur est là où elle est vérifiée et enregistrée |
Déclaré, pas déployé ad hoc. Une application serveur est livrée à travers le même pipeline de publication que n'importe quelle autre : source → compilation → manifeste scellé → ligne de provenance. Il n'y a pas de porte dérobée par laquelle un script arrive sur le serveur par une autre route, ce qui garde l' histoire de l'audit-à-la-publication vraie pour le code serveur comme pour le code client.
#storage:shared — des collections hébergées avec accès par palier
Où vit un forum ? Le stockage par utilisateur (storage:own) est de la mauvaise forme — un post n'est pas
privé. Les collections first-party sont de la mauvaise forme — elles sont first-party par construction. La
capabilité manquante est une collection partagée et durable qu'une application peut déclarer, avec un contrôle
d'accès qu'un auteur peut énoncer et qu'un relecteur peut lire.
#Les quatre paliers
| Palier | Qui peut lire | Qui peut écrire | La forme qu'il sert |
|---|---|---|---|
Own |
le propriétaire | le propriétaire | documents par utilisateur — la sémantique storage:own, hébergée |
Room(roomKey) |
les membres de la room | les membres de la room | une session, un board, un document collaboratif |
PublicRead |
tout le monde | le propriétaire | un article publié, un profil, un leaderboard |
PublicAppend |
tout le monde | tout le monde, sous quota et modération | la forme forum : n'importe qui peut poster, personne ne peut réécrire |
Une application déclare ses collections, son palier par collection, et ses index à la publication. Le compilateur transforme chaque palier en politique de row-level security dans la base de données.
Pourquoi un ensemble clos de paliers, et non un langage de règles. Un langage de règles général paraît plus expressif, et il l'est — y compris de manières que vous n'aviez pas prévues. C'est un second programme, écrit dans un second langage, tournant dans le chemin de sécurité, sans système de types, sans argument de totalité, sans harnais de test et sans relecteur qui le maîtrise. Son mode de défaillance est silencieux et total : une règle qui lit une clause de manière trop permissive expose une table, et rien de cela n'est visible dans la source de l'application.
Quatre paliers nommés ont un mode de défaillance différent, et c'est tout l'intérêt. Le palier est dans le manifeste — inspectable avant l'installation, aux côtés de la liste de capabilités. Il compile en une politique que la base de données impose en dessous de l'application, si bien qu'un bug dans l'application ne peut pas la contourner. Et parce que l'ensemble est clos, le mapping palier → politique est écrit une fois et vérifié une fois, plutôt que redérivé par app par qui que ce soit qui a écrit les règles ce jour-là.
Ce n'est pas une fonctionnalité plus petite déguisée en propriété de sûreté. C'est la propriété de sûreté : la raison pour laquelle nous pouvons dire « une application ne peut pas lire la ligne d'un autre utilisateur », c'est qu'aucune application ne peut exprimer la règle qui le lui permettrait.
Et, dans le même esprit : jamais de SQL brut. Pas de SQL restreint, pas de SQL assaini — aucune surface SQL du tout. Il n'y a aucune chaîne qui devient une requête, et donc aucune injection.
#L'ensemble clos de verbes
| Verbe | Kind | Livre à travers C |
|---|---|---|
SharedGet(key, C) |
Cmd |
la valeur, ou NotFound |
SharedQuery(index, range, limit, C) |
Cmd |
un ensemble de lignes borné sur un index déclaré à la publication |
SharedPut(key, value, C) |
Cmd |
un verdict d'écriture |
SharedDel(key, C) |
Cmd |
un verdict d'écriture |
Sub OnSharedChange(scope, C) |
Sub |
les changements dans le scope, sous forme de messages journalisés |
Chacun d'eux porte un constructeur de complétion — y compris les écritures :
fluxvariant WriteVerdict { Ok(rev: num) | QuotaExceeded | Denied | Conflict(rev: num) }
Pourquoi une écriture durable n'est jamais fire-and-forget. Une commande qui change un état durable et partagé et ne renvoie rien donne à une application exactement deux options, toutes deux mauvaises : supposer que ça a marché, ou poller. Le verdict clôt cela :
Ok(rev)vous donne la nouvelle révision,Deniedvous dit que le palier vous a refusé,QuotaExceededvous dit la vérité sur le quota, etConflict(rev)vous dit que quelqu'un d'autre est arrivé avant.
Conflict(rev) est le bras de concurrence optimiste. Une écriture porte la révision sur laquelle elle était basée ;
si la révision stockée a bougé, l'écriture n'atterrit pas et l'application se voit remettre la révision
qui a gagné. Elle relit, redérive, et décide. Ce qui n'arrive jamais, c'est un écrasement silencieux —
le mode de défaillance où deux utilisateurs ont tous deux « réussi » et où l'un d'eux a perdu son travail sans que personne
ne soit prévenu.
#Les valeurs sont typées à la frontière
Une valeur stockée est décodée contre le schéma que l'application a déclaré, avec la même machinerie que le
plan APP utilise déjà pour la persistance : un lecteur tolérant, SchemaMismatch quand un fournisseur dérive
hors de la fenêtre de versions acceptées, et un migrate total quand la forme bouge. Les montants peuvent être
decimal — virgule fixe exacte, ce qu'est l'argent. Les quotas s'appliquent par application et par
utilisateur.
Les collections PublicAppend portent en plus des rate limits côté hôte, des hooks de signalement, et la suppression par le
propriétaire ou l'administrateur. C'est du chrome de plateforme, pas de la surface de script : une application ne peut pas
s'octroyer des pouvoirs de modération en les déclarant, et une application ne peut pas échapper à la modération en
ne les déclarant pas.
Décision ouverte. La surface de modération précise pour PublicAppend — quels hooks de signalement et de suppression
sont au niveau plateforme et lesquels sont au niveau application — est laissée ouverte par le plan.
#La requête partagée bornée
fluxPAGE = 50
app board {
capabilities: [ storage:shared ]
init(p) = { rows: vec.fill(PAGE, na) }
update(m, msg) = match msg {
Load -> { model: m, cmds: [ SharedQuery("byTime", (0..PAGE), PAGE, Rows) ] }
Rows(rs) -> { model: m with { rows: rs }, cmds: [] }
Changed(e) -> { model: m, cmds: [ SharedQuery("byTime", (0..PAGE), PAGE, Rows) ] }
}
view(m) = col { for p in m.rows -> text(p.body) }
subs(m) = [ OnSharedChange("board", Changed) ]
}
Le buffer de lignes du Modèle est un vec fixe de capacité PAGE, et une page courte laisse na dans la
queue. Il n'y a pas d'étape de compaction dans ce view, et aucune n'est possible : le langage n'a pas de
filter, et where / mask préservent la longueur — ils vident une entrée, ils n'en retirent
jamais une (collections). Ce qui rend le view correct malgré tout, c'est que l'itération est
consciente des na : une compréhension n'émet aucun enfant pour un trou. Ainsi une page de onze posts dans un buffer
de cinquante rend onze lignes, et les trente-neuf trous ne produisent rien — sans filtre, et
sans une longueur qui dépend des données.
Trois contraintes sont visibles dans cette unique commande, et chacune est porteuse.
L'index est nommé, et il a été déclaré à la publication. Vous ne requêtez pas par un prédicat
arbitraire ; vous requêtez un index qui existe. C'est le jumeau, côté stockage, de la règle selon laquelle il n'y a pas de
filter dans le langage : un scan dépendant des données a un coût dépendant des données, et un plan qui
promet un coût borné ne peut pas en héberger un.
La plage et la limite sont bornées, et la limite est const-folded. Une requête ne peut pas renvoyer « tous
les posts ». Elle renvoie une page, dans un vec dont le Modèle a déclaré la capacité — c'est pourquoi le
Modèle reste borné ([ErrState] sinon) et pourquoi le plan mémoire reste plat quelle que soit la
taille que la collection atteint sur le serveur.
Un changement est un message. OnSharedChange livre à travers le feed temps réel, journalisé comme
toute autre subscription — si bien qu'une vue collaborative re-requête parce qu'elle a reçu un message, non
parce qu'elle a pollé. La re-requête est explicite dans l'exemple ci-dessus, et c'est délibéré : le plan
ne rafraîchit pas secrètement votre Modèle dans votre dos.
#Le premier consommateur — le grader
Le grader de leaderboard est la plus petite preuve de bout en bout de tout le plan, et c'est là que le déploiement commence.
Un client joue ; chaque entrée qu'il a reçue est dans son journal. Il soumet le run. Le serveur
instancie le même module que le client a exécuté — même build-hash, même closure épinglée — et
re-folde (init, [msg]). Puis il compare.
Deux entrées rendent ce re-fold digne de confiance, et les deux existent déjà pour d'autres raisons :
- Le seed est dérivé du serveur. L'aléa entre dans une application via
OnRand(seed), et le seed est frappé par le serveur, pas par le client. Un joueur ne peut pas chercher un seed favorable, parce qu'il ne l'a pas choisi. - Le temps écoulé fait autorité côté hôte. L'horloge contre laquelle le run a été scoré n'est pas un nombre que le client a rapporté.
Ce que cela change pour le rejeu. Jusqu'à présent, le rejeu était une fonctionnalité de développeur : le voyage dans le temps dans l'éditeur, un harnais de test sans mocks, un rapport de bug qui se reproduit exactement. Le plan serveur en fait un mécanisme de sécurité, sans rien ajouter au langage. Si un verdict est une fonction pure de
(init, [msg]), et que le serveur peut recalculer cette fonction sur les mêmes octets, alors un score revendiqué est vérifiable — et un score forgé est un journal qui ne folde pas vers le résultat revendiqué. La propriété anti-triche n'a pas été conçue ; elle a été héritée du déterminisme. C'est l'argument le plus fort que ce design avance en faveur de la pureté, et c'est la raison pour laquelle le grader sort en premier : il prouve la closure épinglée et l'application headless, et il n'a besoin de rien d'autre.
#La limite de cet argument
« Un run forgé ne folde pas vers le résultat revendiqué » est vrai pour un score dérivé du seed et du temps écoulé, parce que le serveur possède les deux. Ce n'est pas vrai pour tout score, et l' écart mérite d'être énoncé précisément plutôt que de laisser un lecteur le découvrir.
Un score alimenté par un résultat poussé par l'hôte se re-folde sans divergence. Considérez un jeu dont le résultat
dépend de quelque chose que l'hôte a décidé et remis à l'application — un résultat de kernel révélé comme
un message, OnReveal -> Revealed(outcome). Ce résultat entre dans le journal comme de la donnée. Il n'est pas
redérivé du seed au retour — le seed redérive les messages qui venaient de l'
aléa, et celui-ci n'en venait pas. Ainsi le re-fold du serveur rejoue le résultat exactement tel qu'il est
écrit, y compris un résultat forgé, et atteint le résultat revendiqué sans le moindre soupçon de divergence.
La vérification passe. La revendication est quand même un mensonge.
La même classe couvre un pixel. Une lecture prise depuis le viewport du client — une distance, un angle, tout ce qui est dérivé du pan et du zoom — ne peut pas être redérivée par un serveur qui n'a pas de viewport. Un scalaire de pixel forgé se re-folde proprement pour la même raison. D'où la règle permanente : une valeur de pixel n'alimente jamais un verdict classé. C'est une lecture, ou c'est cosmétique. C'est une discipline au niveau du langage, pas au niveau du serveur, et elle tient que ce plan existe ou non.
Le plan serveur est ce qui ferme le premier. Parce qu'un résultat vient d'un kernel hôte, et que ce runtime exécute WebAssembly nativement, le serveur peut relancer ce kernel lui-même et redériver le résultat plutôt que de faire confiance à la copie qu'en a le journal. Le re-fold a alors un résultat dérivé du serveur, un seed dérivé du serveur et une horloge faisant autorité côté hôte, et l'argument de divergence se referme sur tout le verdict.
En attendant, un run alimenté par un résultat a exactement deux destinations honnêtes, et pas de troisième : l'hôte redérive le résultat, ou bien le run est en score local uniquement et exclu du leaderboard partagé. Il n'est jamais accepté sur la seule foi du journal du client.
Le verdict est écrit dans la ligne de score avec son reçu. Rien du jeu n'a eu besoin d'être écrit deux fois, et aucun « moteur de règles côté serveur » n'existe pour se désynchroniser du client — les deux sont le même artefact, par construction.
Ce qui atterrit ensuite. Le stockage partagé derrière la première fonctionnalité communautaire ; puis le séquenceur de
collaboration et le miroir de gouvernance sur le même runtime ; puis le fan-out de push et de mail. Le projecteur de
prérendu peut atterrir indépendamment de tous, parce qu'il ne dépend de rien dans
storage:shared.
Décision ouverte. Le fait qu'un séquenceur always-on puisse tolérer la latence de démarrage à froid en edge, ou qu'il nécessite un canal persistant, est décidé chez son consommateur, pas ici.
#Prérendu SEO — une projection view pure
Une application qui fait son rendu à travers WebAssembly présente à un crawler une page vide. Pour un cours public, un site de documentation ou un forum, ce n'est pas un compromis — c'est une disqualification.
La jonction qui corrige cela ne nécessite aucun changement de langage, parce qu'elle est déjà là :
view( fold( init(p), prerenderMsgs(route) ) ) — pure · total · inside the I7 oracle
fold et view sont des fonctions pures de données. Le serveur peut donc exécuter le même
module WebAssembly au moment de la publication et projeter le UiTree résultant en HTML
sémantique et assaini.
#Le projecteur est le jumeau serveur du sanitizer
nœud UiTree |
Se projette en |
|---|---|
conteneurs (col, row, grid, panel) |
balises sémantiques |
text, label |
nœuds de texte |
image(assetRef) |
un <img> résolu |
| texte riche | prose sémantique |
| un lien | une véritable ancre — crawlable |
chartView |
une image poster statique |
C'est le même catalogue clos que le sanitizer client peint, ciblant un backend différent. Une application ne peut pas injecter de markup dans une page prérendue pour la même raison qu'elle ne peut pas injecter de markup dans une page live : aucune primitive ne produit de markup, et l'ensemble de nœuds est clos.
#Fixtures, gating, et fraîcheur
Fixtures par route. Une application déclare prerenderMsgs par route — les payloads OnRoute
qui mettent le Modèle dans l'état que cette route doit montrer. Chaque route publique produit une page statique.
Le contenu gaté rend son teaser, exactement comme le modèle d'accès le spécifie déjà : le crawler
voit ce que voit un visiteur déconnecté, ce qui est la seule chose honnête à lui servir.
Contenu partagé dynamique. Un fold pur ne peut pas lire storage:shared — la pureté n'est pas négociable
même ici. Ainsi l'exécution du projecteur embarque un snapshot SharedQuery récupéré par l'hôte dans les
messages de fixture de la route, et le re-prérendu d'une route affectée est déclenché par les write
hooks de la collection, batché et debouncé.
La conséquence est énoncée clairement plutôt qu'édulcorée : la crawlabilité du contenu partagé est à terme, pas live. Un post est visible pour un crawler peu après avoir été écrit, pas à l' instant où il est écrit.
Jamais par requête. Il n'y a pas de calcul serveur par hit. Une page est produite à la publication, et régénérée de manière incrémentale quand une écriture l'invalide. C'est un plafond délibéré sur la surface opérationnelle de tout le plan : le chemin de requête reste un fichier statique servi depuis un CDN, et aucun pic de trafic ne peut devenir une facture de calcul ou une latence de démarrage à froid.
L'hydratation, c'est le boot. La page émise porte le boot d'application standard ; l'application démarre et repeint à partir de zéro. Il n'y a pas d'étape de réconciliation d'état du DOM, parce qu'un repaint déterministe en rend une inutile — la machinerie dont d'autres stacks ont besoin pour réconcilier un arbre rendu par le serveur avec un arbre rendu par le client n'a rien à faire ici.
Décision ouverte. Les variantes de locale — une page prérendue par locale, ou une seule page avec un
ensemble hreflang — sont ouvertes, et interagissent avec le modèle de catalogue dans i18n.
Décision ouverte. La politique de fraîcheur pour le contenu partagé dynamique — le lag acceptable, la fenêtre de batching, et le fait qu'une collection à fort taux de rotation se retire entièrement du prérendu — est ouverte.
#Confiance, paliers et quotas
Post-v1. Les applications serveur sont vérifiées par le prestataire uniquement au début. Le code serveur généré par l'utilisateur est reporté derrière le même gate que toute autre capabilité qui peut dépenser nos ressources pour le compte de quelqu'un d'autre. C'est une décision de déploiement, pas une frontière de langage : le code qui tournera à terme là est le code qui y tourne maintenant.
Tout le reste se reporte verbatim depuis le client :
- Capabilités deny par défaut, agrégées transitivement et plafonnées par le grant :
manifest = ( ⋃ emit Cap over the transitive closure ) ⊓ the grant. Une ligne serveur est une ligne comme n'importe quelle autre. - Le manifeste est inspectable avant que quoi que ce soit soit déployé, et il est dérivé par le compilateur depuis les sites d'emit — sa seule origine.
- Quotas et metering par application : temps CPU, lignes, egress. Les hooks de comptabilité que le pilier réseau a définis pour les flux facturés au compteur trouvent leur consommateur de facturation ici — un plafond atteint est un disjoncteur, pas une facture surprise.
- Audit. Le log d'audit hôte immuable gagne un miroir serveur chaîné par hash, si bien que la gouvernance à travers une flotte lit une seule discipline de journal plutôt que deux.
Décision ouverte. Le schéma de reçu et d'entitlement pour la validation pay:* n'est pas arrêté ; il est
co-conçu avec la famille paiements dans host services.
#Le troisième pied du déterminisme
L'identité octet a été une affirmation à deux parties : l'interpréteur et le module compilé s'accordent, et le gate I7 le prouve à chaque compilation. Le plan serveur en fait une affirmation à trois parties.
| Pied | L'affirmation | Imposé par |
|---|---|---|
| interpréteur ≡ WASM | les deux moteurs client produisent les mêmes octets | le gate I7, à chaque compilation |
| WASM (client) ≡ WASM (serveur) | le serveur exécute le même artefact | la closure de build épinglée = le deploy gate |
| interpréteur ≡ WASM ≡ serveur | un verdict calculé sur un device et recalculé sur le serveur est les mêmes octets | les deux ci-dessus, ensemble |
Le troisième pied n'est pas gratuit. Il tient si et seulement si deux conditions tiennent, et les deux sont mécaniques :
Le serveur exécute les mêmes routines épinglées. Chaque endroit où deux implémentations pourraient
légitimement différer — l'arithmétique décimale, les transcendantes, le générateur d'entiers seedé,
la collation, l'ordonnancement des na, les métriques de texte — est servi par une seule routine, partagée. Pas le même
algorithme : le même code. Un serveur qui recourrait à la bibliothèque décimale de sa propre plateforme, ou à son propre
Math.log, serait une troisième implémentation, et une troisième implémentation est un troisième jeu de bugs.
Le serveur relie la closure de dépendances exacte. Le build-hash épingle le lockfile, qui épingle la closure transitive de module-hash. Un serveur qui résoudrait une dépendance vers une version compatible plus récente exécuterait un programme différent qui se trouve type-checker — et il serait en désaccord avec le client sur une certaine entrée, tôt ou tard, d'une manière que personne ne pourrait reproduire.
Pourquoi cela vaut la discipline. Sans les deux conditions, « identique octet pour octet » est une promesse entre deux parties qui se font confiance — un client et un compilateur sur la même machine. Avec elles, c'est un fait qu'une tierce partie peut vérifier : quiconque détient le journal et le build-hash peut recalculer le résultat et comparer. C'est la différence entre une histoire de déterminisme que vous utilisez pour déboguer et une derrière laquelle vous pouvez mettre un leaderboard, un reçu ou un audit.
Le résidu honnête. La réserve sur la virgule flottante qui qualifie le déterminisme sur le client — la
clause de même-runtime pour les séries f64 calculées par les chemins de cœur du moteur — ne se dissout pas
en passant à un serveur ; elle s'applique là où elle s'appliquait avant. Ce que le troisième pied couvre, c'est le
domaine qui importe ici : le fold et la view, et les montants decimal à l'intérieur d'eux,
qui sont de l'arithmétique entière exacte à travers une routine partagée et épinglée. La virgule flottante de WebAssembly
est spécifiée bit pour bit entre machines, et les deux côtés exécutent la même closure — de sorte que la garantie
est le déterminisme octet de l'artefact, jamais une affirmation sur le moteur hôte environnant.
Décision ouverte. Le déploiement multi-région est sûr par le même argument de closure — les mêmes octets tournent partout — mais la discipline opérationnelle qui garde les régions synchronisées n'est pas encore écrite.
#Voir aussi
- Plan APP — le cœur TEA, le journal, les capabilités, et les deux exceptions nommées de production de Modèle.
- host services — la moitié client : paiements, notifications, et la doctrine du resource-handle.
- net — l'ingress typé, les grants d'egress, et la file offline de l'autre côté du fil.
- Garanties — ce que le déterminisme promet, et les vérifications machine derrière chaque promesse.
- Compilateur & runtime — le gate I7, les routines épinglées, et les builds reproductibles.
- Packages — le lockfile et la closure de dépendances que le deploy gate épingle.
Compilateur et runtime
Flux tourne sur deux moteurs : un interpréteur de graphe, qui sert l'éditeur, l'aperçu live et le débogueur, et un module WebAssembly compilé, qui sert le run. Ce ne sont pas des approximations l'un de l'autre. Ils produisent les mêmes octets, et cette égalité est vérifiée à chaque compilation, sur des données hostiles, avant que quoi que ce soit ne soit livré.
Cet unique invariant est la colonne vertébrale de tout le reste de cette page. C'est ce qui permet d'échanger le moteur sous un chart en cours d'exécution sans aucune saccade visuelle, ce qui permet à un optimiseur d'être agressif sans être digne de confiance, et ce qui permet à un serveur de ré-exécuter le travail d'un client et de détecter un mensonge.
Ensemble, les deux moteurs forment une seule machine abstraite — la FVM, la Flux Virtual Machine : une machine dataflow totale, sandboxée, déterministe, dont le jeu d'instructions est l'ensemble des kernels et des opérations de graphe, dont la mémoire est la disposition linéaire statique du modèle mémoire, et dont l'arithmétique est fixée à l'octet. Ni l'interpréteur ni le module WebAssembly n'est la FVM ; chacun est une façon de l'exécuter, et I7 (ci-dessous) est la garantie que les deux façons ne peuvent pas diverger. C'est une machine virtuelle au sens exact où la JVM en est une — une sémantique unique avec plusieurs implémentations conformes — pas une boucle de bytecode : les « instructions » sont un graphe typé de kernels, et les deux implémentations l'abaissent différemment mais doivent atterrir sur les mêmes bits.
#Le pipeline
source
│ parse Lezer — incremental, total: an arbitrary input yields one tree or a clean error
▼
│ resolve + inline names and `def` bodies (the call graph is acyclic, so inlining terminates)
▼
typed DAG kinds inferred bottom-up; presentation derived from the kinds
│ causality check every feedback cycle must cross a unit delay
▼
│ optimize common-subexpression elimination · dead code · constant folding · fusion · kernel selection
▼
│ memory plan liveness intervals → slots → an exact footprint
▼
emit ──┬─→ interpreter closures (edit · preview · debug · THE ORACLE)
└─→ WebAssembly (Binaryen) (run · distribute)
L'interpréteur paie une fois, quand le graphe est construit. Ensuite, la boucle chaude, ce sont des kernels natifs sur des colonnes pré-allouées.
L'unité de compilation est (graphe, paramètres résolus, capacité en barres). Les paramètres sont
résolus à la compilation — changer un réglage recompile et re-passe au gate. C'est un compromis
délibéré : il achète une disposition d'état 100 % statique, une mémoire min = max exacte, et un
gate qui valide les octets mêmes et l'instance même qui vont servir. Il n'y a aucun écart entre
ce qui a été vérifié et ce qui s'exécute.
#Deux moteurs, un seul contrat
| Interpréteur | Module WebAssembly | |
|---|---|---|
| Sert | l'édition, l'aperçu live, le débogueur dataflow | le run, et la distribution |
| Retour | instantané — aucune étape de compilation dans la boucle | compilé et passé au gate, puis échangé |
| Rôle dans le contrat | l'oracle | le candidat |
#I6 — une feuille est byte-identique à son kernel
Un nœud qui correspond à un kernel natif produit exactement les octets que ce kernel produit,
warm-up compris. Flux n'impose pas sa propre convention na-jusqu'à-N : un indicateur Flux sur une
horloge est le même citoyen qu'un natif, dès la première barre. C'est ce qui fait de « réécrire le
catalogue en Flux » une proposition sûre plutôt qu'une réécriture de chaque golden.
#I7 — l'interpréteur et WASM concordent, octet par octet
Le gate tourne à chaque compilation et il bloque :
- L'oracle est l'interpréteur, qui évalue le graphe directement.
- Le candidat est le module WebAssembly instancié.
- Ils sont comparés octet par octet sur chaque colonne sink, sur un corpus hostile, déterministe, adaptatif — des séries plus longues que le lookback maximum résolu du programme, ensemencées de trous, ±infini, zéro négatif, motifs de NaN bruts non canoniques, demi-entiers exacts, séquences plates, séquences monotones, magnitudes à 1e±9 — d'abord en batch, puis live (barre par barre, à travers le pas incrémental), puis sur les vraies données s'il y en a.
- Toute divergence bloque la compilation. Le module n'est pas livré ; l'interpréteur continue à servir.
Pourquoi le chemin live est dans le gate aussi. L'égalité en batch est la moitié facile. Le chemin live avance une barre à la fois à travers un chemin de code différent dans le module, et c'est exactement là que se cache un bug d'état subtil. Dans le module émis, un seul corps partagé sert les deux — la plage batch et l'avance d'une seule barre sont le même code, appelé avec des bornes différentes — donc live ≡ batch par construction ; le gate vérifie alors la seule jonction qui reste (les bases de colonnes vs le scratch live) plutôt que de lui faire confiance.
#Ce que ça achète, concrètement
- Les permutations de moteur sont invisibles. Servir des résultats de l'interpréteur maintenant et des résultats WASM un instant plus tard produit les mêmes octets. Aucun état de migration, aucune réconciliation, aucun repaint, aucun churn de golden.
- L'optimiseur n'a besoin d'aucune confiance. Tout changement de valeur qu'il introduit est attrapé par le gate bloquant, à la compilation qui l'a produit.
- Un module distribué est vérifiable. Quiconque détient le graphe peut ré-exécuter la validation localement : l'identité est vérifiable, pas promise.
#Le chemin navigateur, tel qu'il est construit
Le thread principal n'importe jamais le compilateur. Il orchestre :
- Un script enregistré sert immédiatement, sur l'interpréteur — le chemin prouvé, sans aucune étape de compilation entre la frappe et l'affichage.
- Le service compile dans un worker, où la compilation et le gate I7 sont atomiques : des octets divergents ne sont jamais rendus, parce qu'ils ne sont jamais rendus du tout.
- Quand le module passé au gate existe, le service fait une montée de version silencieuse. Par I7 les octets sont identiques, donc la permutation ne requiert aucun re-rendu et ne produit aucun événement visible.
Le bundle du compilateur est récupéré paresseusement, sur intention d'éditeur — jamais sur le chemin du chart, et exclu du precache hors-ligne. Les modules compilés sont mis en cache par paliers (instances attachées, par script et configuration ; modules, par une clé canonique, dans un cache borné), parce que la partie coûteuse, c'est la compilation, pas l'instanciation.
#Déterminisme : la discipline des routines épinglées
La byte-identité ne survit pas au premier contact avec une bibliothèque standard. Deux moteurs peuvent diverger sur le dernier bit d'un logarithme, le signe d'un zéro, ou l'arrondi d'un demi-entier, et chacune de ces divergences suffit à casser le rejeu. Alors Flux épingle les routines — le même code, des deux côtés, sans aucune délégation à la plateforme.
| Routine | Épinglée sur | Pourquoi le choix évident est faux |
|---|---|---|
log exp sin cos tan atan atan2 pow |
une libm WebAssembly épinglée, utilisée par les deux moteurs | deux moteurs JavaScript diffèrent d'au moins 1 unité à la dernière place |
% |
le reste de la bibliothèque épinglée, lié directement de module à module | le reste lui-même est spécifié exactement — ce qui est épinglé, c'est la liaison : le router par l'hôte reformerait un NaN produit, et la canonicalisation doit se faire en un seul endroit |
round |
f64.nearest — départage vers le pair |
l'arrondi au supérieur diverge sur chaque demi-entier |
min / max |
la chaîne de sélection exacte absorbant na |
les instructions natives propagent NaN et renvoient −0 pour min(−0, +0) — les deux divergent de la règle du langage |
| constantes NaN | émises comme un motif de bits entier, puis réinterprétées | un NaN transporté à travers une API hôte n'a aucun bit garanti |
na au repos |
le NaN silencieux canonique 0x7FF8000000000000 |
WebAssembly laisse indéterminés le signe et le payload d'un NaN produit |
decimal |
une seule routine d'entiers multi-limbes partagée | un moteur en virgule flottante n'a pas d'entier 128 bits natif ; toute émulation divergerait |
string, fmt.* |
des tables Unicode épinglées et un seul formateur canonique | la longueur de chaîne de la plateforme est en unités UTF-16 ; le formatage de nombre de la plateforme diffère au dernier chiffre |
| calendrier | une routine époque ↔ civil épinglée, des données de fuseau horaire épinglées | deux implémentations correctes divergent encore sur les trous de DST et le bornage de fin de mois |
rand(seed) |
un seul générateur d'entiers épinglé basé sur un compteur | l'arithmétique entière est bit-identique gratuitement ; le mélange en flottant ne l'est pas |
ordre de na dans les tris |
un seul ordre total épinglé (valeurs absentes en dernier, stable par index) | un comparateur partiel laisse l'ordre au tri de la plateforme |
| le plan mémoire | une fonction déterministe du graphe | l'oracle de valeur est aveugle à la disposition, donc rien d'autre n'attraperait une divergence |
Pourquoi l'interpréteur n'appelle pas la plateforme. Il est écrit en TypeScript et tourne sur un moteur JavaScript, donc
Math,Number,String.prototype,DateetIntlsont juste là. Les utiliser ferait concorder l'interpréteur avec lui-même et diverger du module — et la divergence serait invisible pour un programme (rien n'observe un payload de NaN ; rien n'observe le dernier bit d'un logarithme) et visible seulement pour un oracle au niveau de l'octet. La discipline est : l'interpréteur exécute la même routine épinglée que le module.
#La toolchain est épinglée aussi
L'abaissement vers WebAssembly passe par Binaryen, et ce doit être une fonction déterministe :
- le pipeline d'optimisation est fixe (un pipeline
O3, sans biais de réduction, fast-math off, un ensemble de fonctionnalités fixe) et re-validé par le gate à chaque compilation ; - la version de Binaryen est une entrée du hash de build, pas seulement une note dans un changelog ;
- l'itération est indépendante de l'ordre des pointeurs ou des adresses ; l'ordre des fonctions et des locals est canonique ; les sections name et producer sont omises ou épinglées ; aucun timestamp, chemin ou build id n'entre dans l'artefact.
L'optimisation est obligatoire, pas optionnelle — un module non optimisé n'est pas un module « plus sûr », c'est un module différent, et tout l'enjeu est qu'il n'y en ait qu'un seul.
Les octets émis sont une fonction pure de (programme, toolchain), donc la toolchain a une seule identité — la version du compilateur, l'épinglage de Binaryen, l'identité des routines mathématiques épinglées, et l'ensemble de fonctionnalités activé — et chaque clé de cache d'un artefact compilé la porte tout entière. Sans cela, modifier une règle d'émission continuerait à servir des octets périmés qu'une recompilation ne produit plus : un cache dont la clé porte moins que la toolchain est une divergence silencieuse à mèche longue.
La minification et l'obfuscation, là où elles sont utilisées, sont déterministes et appliquées après le gate, sur l'artefact de distribution. Le hash de provenance et le gate de recompilation sont définis sur l'artefact qui est effectivement livré.
#La surface runtime
Le module compilé expose la même forme que le moteur natif utilise déjà :
| Surface | Signification |
|---|---|
make |
instancier et réinitialiser |
advance(bar) |
le pas live |
run(n) |
la plage batch |
snapshot / restore |
une copie de la région d'état — un checkpoint est un memcpy, et la reprise est exacte à l'octet |
Un seul corps sert run et advance, donc les chemins live et batch ne peuvent pas dériver l'un de
l'autre, et un checkpoint est une copie contiguë plutôt qu'un sérialiseur sur mesure.
#Pourquoi WebAssembly
Pas pour la vitesse. La mesure honnête : fusionner des kernels avec de la glue ne gagne rien en batch
(la frontière est amortie sur un long historique), SIMD est exclu par la byte-identité (une
réduction horizontale réassocie le flottant et change les bits), et le calcul f64 scalaire dans un
moteur JavaScript moderne est déjà à un petit facteur près. La vraie vitesse vient du travail
algorithmique et des kernels natifs, pas du langage d'exécution.
WebAssembly est la cible pour quatre raisons structurelles :
- Un seul artefact d'exécution dès le premier jour. L'interpréteur et le module doivent concorder bit pour bit ; figer cette égalité au départ est bien moins coûteux que de la rétro-adapter.
- Déterminisme inter-machines. La sémantique en virgule flottante de WebAssembly est strictement spécifiée — ce qui rend la ré-exécution côté serveur significative plutôt que « même runtime, probablement ».
- Volets applicatifs. Un seul format d'exécution et de distribution pour les indicateurs, les représentations, les dessins, les scènes, les transitions et la logique applicative.
- Un artefact de distribution opaque et sandboxé. Un script partagé ou acheté est livré en WASM, jamais en source : la propriété intellectuelle est protégée, et la frontière de confiance du consommateur est un binaire plus un manifeste scellé.
La frontière honnête. WebAssembly calcule. Il ne peint jamais : il produit de la géométrie, une draw-list, et un arbre de vue en mémoire linéaire, et l'hôte — JavaScript, ou le GPU — fait la peinture. Il n'y a aucune Web API atteignable depuis le module. Le rendu 2D reste vectoriel (net à n'importe quel zoom) ; le rendu 3D va au GPU. Un rasteriseur logiciel à l'intérieur de WebAssembly a été envisagé et rejeté : il perd la netteté vectorielle et il est plus lent que le GPU.
eval et la génération de code dynamique restent interdits. WebAssembly n'est pas « eval avec des
étapes en plus » — c'est un bac à sable avec de la mémoire linéaire, sans DOM, et un accès à l'hôte
uniquement à travers des imports validés, admis par son propre jeton de politique dédié.
#Budgets — comptés à la compilation, jamais chronométrés à l'exécution
Un budget imposé par un chronomètre n'est pas un budget : le même script serait accepté sur une machine et tué sur une autre, et le rejeu en mourrait. Chaque plafond dans Flux est donc un compteur, évalué sur le graphe, avant que quoi que ce soit ne s'exécute.
| Plafond | Valeur | Ce qu'il borne |
|---|---|---|
N_max |
10 000 dans le navigateur ; 100 000 sur le serveur et en backtest | la longueur const d'une fenêtre ou d'un vec. Au-delà : [ErrTotal] |
maxNodes |
3 072 par script | la taille du graphe |
maxBricksPerBar |
1 000 | combien d'unités re-binnées une barre peut produire (Renko, P&F). Au-delà du plafond, l'hôte agrège plutôt que de faire exploser le budget |
N_active |
16 scripts co-actifs par chart | combien de scripts fusionnent en un seul DAG partagé ; le plafond agrégé est N_active × maxNodes |
| mémoire par instance | la borne structurelle maxNodes × N_max |
le pire cas légal. L'empreinte déclarée est le plan de vivacité exact, qui est bien plus petit |
Les nombres sont la partie la moins intéressante de ce tableau. Ce qui compte, c'est comment ils sont imposés.
Le graphe est l'autorité, pas le texte. maxNodes est jugé sur le DAG après inlining,
élimination de sous-expressions communes et élimination de code mort — le graphe qui va réellement
s'exécuter. Les gardes du front-end (taille de l'AST, expansion de l'inlining) siègent bien au-dessus
de lui et n'existent que pour empêcher une entrée hostile d'épuiser le compilateur ; elles ne
prononcent jamais un verdict de budget. De même, nMax ne touche jamais un octet calculé :
navigateur et serveur diffèrent dans ce qu'ils acceptent, jamais dans ce qu'ils produisent.
Le plafond mémoire n'est pas une vérification — c'est l'allocation. Le module déclare sa mémoire
linéaire avec min = max, dimensionnée à partir du plan de vivacité (§ modèle mémoire).
La croissance n'est pas interdite à l'exécution ; elle est impossible. La partie statique est
vérifiée à la compilation et les longueurs restantes à l'instanciation — jamais pendant qu'une barre
est en train d'avancer.
Il n'y a aucun timeout par barre. Le coût à l'exécution est statiquement borné par
maxNodes × N_max × maxBricksPerBar : la totalité donne la terminaison, et les plafonds donnent la
borne pratique. Un graphe hors-budget est rejeté à la compilation, pas tué en plein frame — la
garde runtime ci-dessous est de la défense en profondeur, pas le chemin d'application. Un build porte
bien un timeout à l'horloge murale, mais c'est une annulation interactive dans l'éditeur, jamais
un verdict : accepter et rejeter restent une fonction pure de la source.
Quand le budget de frame agrégé est néanmoins dépassé — beaucoup de scripts co-actifs sur un seul chart — l'hôte applique une politique de dégradation déterministe, limitant ou suspendant les scripts basse priorité dans un ordre déclaré explicite. L'ordre n'est jamais dépendant des données, donc la dégradation se rejoue comme tout le reste.
Où le travail s'exécute est aussi une décision comptée. Le service estime le coût d'un graphe en unités de coût et ne dépêche vers une flotte de workers qu'au-dessus d'un seuil calibré ; en dessous, une passe monothread fusionnée finit avant qu'une flotte n'ait démarré. L'estimation est une fonction pure du graphe, donc le routage est reproductible — et par 1 ≡ N le choix ne peut changer un octet dans un sens comme dans l'autre.
#Isolation des fautes
Un script qui échoue à l'exécution — un graphe hors-budget, une véritable erreur sur un graphe valide — est mis en quarantaine : son nœud est marqué et retiré du graphe actif, sans tuer le worker et sans déranger ses voisins, qui partagent la même carte d'instances. Redémarrer le worker est le dernier recours, et les voisins reprennent depuis leur dernier checkpoint.
Un NaN n'est pas une faute. C'est na, et il est affiché comme un trou.
#Le harnais de vérification
Le harnais est un livrable de première classe, pas un dossier de tests. Chaque sous-suite déclare son oracle, son corpus, et si elle bloque la livraison :
| Suite | Ce qu'elle affirme | Bloquant |
|---|---|---|
| Goldens | chaque exemple est un golden déterministe ; un golden inchangé reste byte-identique | oui |
| Propriétés | principalité, confluence (le kind est invariant sous tout ordre topologique), re-typage incrémental ≡ inférence complète, totalité, causalité, le plan mémoire est une fonction déterministe du graphe avec pic ≤ somme | oui |
| Fuzz + un générateur bien typé | le parseur et le vérificateur de kinds sont totaux (toute entrée donne un arbre ou un rejet propre) ; le générateur échantillonne la grammaire gelée et le treillis pour émettre des graphes type-corrects et causaux qui alimentent l'oracle | oui, une fois qu'il alimente l'oracle |
| Oracle différentiel (trois voies) | interpréteur ↔ WASM ↔ kernel natif — couvrant I6, optimisé ≡ référence, et I7 | oui |
| Métamorphique | les relations préservant la sémantique énumérées : optimisé ≡ référence · interpréteur ≡ WASM ≡ serveur · 1 ≡ N workers · plan-pic ≡ plan-somme · confluence sous tout ordre topologique · recompile ≡ recompile, byte-identique · la draw-list absolue est invariante à la cible et à la séquence | oui |
| Stress 1 ≡ N | le même graphe sous un worker et sous plusieurs, avec affectation hostile aléatoire : octets de sortie identiques, et aucune écriture de slot concurrente | oui |
| Énumération du treillis | les lois et chaque jugement d'admissibilité, énumérés par famille | oui |
| Moniteur de capacités | « aucune commande hors du manifeste n'est jamais exécutée » — le seul composant qui mérite un model checking | oui |
| Bench / budget | calibre le modèle de coût avec des mesures ; garde le budget de compilation au repos ; vérifie que le pic à l'exécution égale le pic planifié | consultatif |
Une subtilité mérite d'être énoncée : l'oracle à trois voies appelle la même routine épinglée sur les trois côtés, donc il est aveugle à un bug à l'intérieur d'une routine épinglée. C'est pourquoi chaque routine épinglée porte aussi une seconde implémentation de référence indépendante, comparée bit pour bit sur des entrées fuzzées. L'oracle attrape le désaccord ; seule une seconde implémentation attrape une erreur partagée.
#Builds reproductibles
Le hash de build est une fonction pure de : la source, le lockfile (la clôture transitive des hashs de dépendances), la version du compilateur, les routines épinglées, et le plan mémoire canonique.
Épingler les entrées est nécessaire mais pas suffisant, alors le gate de recompilation comble l'écart : il recompile la même source et le même lock deux fois, sur des machines différentes et avec des nombres de threads différents, et affirme que le module émis est byte-identique au hash stocké. Le rejeu côté serveur en dépend : un build non reproductible casserait le rejeu silencieusement, parce qu'un oracle au niveau de la valeur ne peut pas voir les octets émis à travers deux compilations.
#Performance — mesurée, et honnête
Pourquoi WebAssembly a soutenu que la cible n'avait pas été choisie pour la
vitesse. Le benchmark de certification règle ce que la vitesse est réellement — mesuré sur un seul
Apple M4, sur cent mille barres, avec les branches TypeScript, interpréteur et WASM prouvées
byte-identiques avant qu'un seul chronométrage ne soit pris. Donc chaque ligne ci-dessous compare le
même algorithme : la branche TypeScript, ce sont les kernels de l'interpréteur eux-mêmes, appelés
directement sur un Float64Array ordinaire, pas une réécriture naïve. Et face à cela, en batch — le
chemin que chaque chart emprunte — le module compilé est plus rapide partout, d'une marge qui croît
avec la quantité de données intermédiaires que l'indicateur déplace :
| Charge de travail — le même algorithme des deux côtés | Combien plus rapide que TypeScript |
|---|---|
Kernels O(1) triviaux — rsi, change |
≈ parité — V8 compile une boucle f64 serrée presque aussi bien |
Scans pondérés et déques — wma, alma, highest |
×1.3–2 |
| Charts réalistes — un chart classique à onze tracés tourne à ≈ 21.5 ns/bar | ×1.6–2.5 |
Scans et réductions d'ordre p — percentrank, kama |
×2–3 |
Composites multi-étages — fisher, connorsRsi |
×3.7–5 |
Composites lourds multi-sorties — stdErrorBands |
×9–14 |
Pourquoi — le datapath, pas le moteur. Décompose l'écart et le moteur d'exécution en est la
petite part. Wasm est du code machine avec des opérations f64 en instruction unique et sans
vérifications de bornes dynamiques sur une mémoire linéaire prouvée dans les bornes à la compilation
— mais TurboFan de V8 compile une boucle de tableau typé chaude et monomorphe presque aussi bien,
donc la différence de moteur à elle seule n'est que de ×1.1–2, ce qui est exactement pourquoi les
kernels triviaux atterrissent à parité. Le levier, c'est le datapath. Un indicateur multi-étages
écrit à la main en TypeScript matérialise un Float64Array complet par étage, écrivant puis
relisant ses valeurs à chaque fois ; le compilateur flux fusionne chaque étage en une seule
passe, gardant les intermédiaires dans des locals et des rings. La branche TypeScript de
stdErrorBands alloue huit tableaux ; le module n'en alloue aucun — et chaque intermédiaire
matérialisé, ce sont deux passes mémoire complètes de pur surcoût. C'est de là, et non du jeu
d'instructions, que vient le ×2-à-×14. (La compilation elle-même prend des dizaines de millisecondes,
presque toutes passées par Binaryen à abaisser le graphe en octets — un coût au moment de l'édition,
payé une fois, jamais par barre.)
Et en dessous, les algorithmes préservent les octets. Un maximum glissant lit une déque monotone
au lieu de re-balayer sa fenêtre ; une réinitialisation ne touche que la géométrie vivante, d'environ
28.5 µs à 0.8 µs ; un réglage de longueur borné réduit l'état d'un script d'environ vingt fois ; un
sma et un sum qui coïncident partagent un seul ring. Chacun est un changement O(n) avec les
mêmes octets de sortie — un gain que l'optimiseur et les kernels natifs livrent
dans n'importe quel langage, et une raison de plus pour laquelle la figure ci-dessus n'est pas une
histoire à propos du moteur d'exécution.
Le seul cas où TypeScript gagne — et pourquoi ce n'est pas un gros titre. Fais couler un seul
kernel O(1) qui a un stepper f32 natif — un ema ou un rsi avancé barre par barre — et la
bibliothèque native gagne, ×5–8 : flux paie un appel à travers la frontière WASM à chaque barre là où
la bibliothèque fait juste un pas. Mais cette branche tourne en f32, pas f64 — un résultat de
précision différente, enregistré comme une divergence documentée et jamais cité comme argument de
vitesse. Et elle n'existe que là où un stepper natif existe : un wma fenêtré ou un highest à
déque n'en a pas, donc ce chemin recalcule en batch, où le module reprend tout l'avantage.
Pourquoi SIMD est écarté — par choix. Le seul levier qui bougerait le calcul scalaire, c'est
SIMD, et la byte-identité l'exclut : f32x4 empaquette quatre voies en une, et une réduction
horizontale réassocie le flottant et perd de la précision, donc les bits changent. Les
mêmes-bits-partout, c'est ce sur quoi reposent tous ensemble le no-repaint, le rejeu, les goldens et
la ré-exécution du serveur ; les échanger contre une fraction de facteur dépenserait la propriété qui
donne un sens à ces chiffres. C'est une décision, enregistrée et imposée, pas une fonctionnalité
encore à venir — la même ligne que l'optimiseur trace un niveau plus bas quand il
interdit la réassociation.
Alors à quoi sert WebAssembly. La vitesse est réelle, mais relis la décomposition : elle vient du dataflow fusionné que le modèle de compilation te donne — pas de WebAssembly comme moteur d'exécution, qui n'est que le ×1.1–2. Les raisons de cibler WebAssembly spécifiquement sont les quatre raisons structurelles de Pourquoi WebAssembly : un seul artefact d'exécution, pour que l'interpréteur et le module puissent être tenus égaux dès le départ ; du flottant spécifié à travers les machines, pour qu'un serveur puisse ré-exécuter le travail d'un client et attraper un mensonge ; un seul format qui porte aussi bien un indicateur, une scène et un volet applicatif entier ; et un binaire sandboxé et opaque à distribuer, source retenue. Flux est rapide parce qu'il compile un graphe dataflow fusionné — et il compile ce graphe en WebAssembly pour la correction, le déterminisme et la distribution. Un benchmark que tu peux reproduire est un meilleur argument qu'un superlatif.
#Voir aussi
- Modèle mémoire — la disposition que le plan produit, et pourquoi le plan lui-même est épinglé.
- Optimiseur — la loi de correction, les paliers, et la validation de traduction.
- Concurrence — l'ordonnanceur, et la preuve 1 ≡ N que la suite de stress exerce.
- Packages — l'adressage par contenu, le lockfile, et ce que le gate de recompilation scelle.
- Garanties — les mêmes propriétés, énoncées pour le lecteur qui doit leur faire confiance.
- Inférence — pourquoi l'inférence déterministe est un prérequis pour tout cela.
Modèle mémoire
Flux n'a pas de ramasse-miettes, pas d'allocateur à l'exécution, et aucun moyen de manquer de mémoire pendant l'exécution. Ce n'est pas une optimisation — c'est une conséquence du langage. Chaque buffer a une taille const-foldée, chaque durée de vie est statiquement exacte, et le compilateur ne vérifie donc pas un plafond mémoire : il calcule l'allocation. Un programme qui dépasserait son budget est rejeté avant de s'exécuter, jamais tué en cours d'exécution.
Cette page décrit comment les valeurs sont représentées, comment le datapath est disposé, comment le plan de vivacité transforme un graphe en carte mémoire, et quelles bornes sont imposées où. Elle distingue ce qui est implémenté aujourd'hui dans le back-end du plan d'analyse de ce que le design scellé spécifie pour les plans encore en construction.
#Représentation des valeurs
À l'exécution, chaque scalaire est un double IEEE-754. Les kinds sont une discipline à la
compilation : une fois la dimension vérifiée, un price et un level sont tous deux un f64. Rien
du kind ne survit dans le datapath — ce qui est précisément pourquoi un système de types
dimensionnel ne coûte rien à l'exécution.
#na et son motif de bits canonique
na est un NaN. C'est commode — l'arithmétique le propage gratuitement — et c'est un piège, parce
que la spécification WebAssembly laisse le bit de signe et le payload d'un NaN produit
indéterminés (0/0, sqrt(-1), ∞ − ∞). Deux moteurs pourraient donc stocker des octets
différents pour la même valeur absente, et la byte-identité se casserait silencieusement : aucun
programme ne peut observer un payload de NaN (is_na est un test x ≠ x, insensible au payload),
donc seul un oracle au niveau de l'octet le verrait jamais.
La règle, implémentée des deux côtés :
- Les valeurs par barre vivent dans des registres machine (locals WASM, la pile de l'interpréteur). Là, un NaN peut porter n'importe quel payload — aucune opération du jeu d'instructions n'en observe un.
- À chaque frontière observable — une écriture de colonne sink, un snapshot, un hash, une
sérialisation — la valeur est forcée à l'unique NaN silencieux canonique
0x7FF8000000000000. Dans le back-end WASM c'est uni64.storedu jumeau bit-exact du store canonicalisant de l'interpréteur.
Ainsi na se comporte comme une valeur absente ordinaire dans le langage, et comme un unique motif
de bits exact partout où il peut être comparé.
#Decimals
decimal(scale) est un entier mis à l'échelle, pas un flottant. La largeur de stockage suit la
précision déclarée — i64 pour jusqu'à ~18 chiffres (un type WebAssembly natif, huit octets, le
chemin rapide), i128 par défaut, i256 pour les plus grandes magnitudes crypto.
La largeur de stockage n'est pas la largeur de calcul. Un produit promeut son intermédiaire
(i64 × i64 → i128) puis re-quantifie à la précision déclarée de la destination, de sorte qu'un
débordement est impossible à cacher. Dépasser la borne déclarée donne na plus un diagnostic —
jamais un wraparound, jamais de comportement indéfini.
Pourquoi le seuil suit la déclaration. Les chiffres numériques d'un résultat ne dépendent pas de la largeur de stockage ; seul le point à partir duquel « ceci est hors domaine » se déclenche en dépend. Déclarer
decimal(18, s)signifie « au-delà de ceci, c'est une erreur de domaine, pas un nombre plus grand ». Parce que ce seuil décide quand unnaapparaît, la précision déclarée fait partie du hash du script : deux parties rejouant le même programme doivent s'accorder sur le moment où l'absence commence.
#Strings
Une string est immuable, UTF-8, et bornée par un plafond déclaré. Son unité est le scalaire
Unicode — jamais un octet, jamais une unité de code UTF-16 — de sorte que l'indexation et le slicing
concordent entre moteurs, et que la troncature coupe toujours sur une frontière de scalaire.
La plupart des strings en pratique sont courtes (un label, un prix formaté), donc elles vivent inline dans la valeur — une optimisation small-string, zéro allocation. Les plus longues vont dans une arène bump réinitialisée une fois par tick d'évaluation (par barre, ou par frame) : pas de GC, parce que la pureté plus les durées de vie bornées rendent la réinitialisation toujours sûre.
Une règle garde cela sûr sous rejeu : une string qui survit à son tick — capturée par un scan,
stockée dans un champ de Model, écrite dans un checkpoint — est matérialisée hors de l'arène
(copiée), jamais laissée comme une vue dans une mémoire sur le point d'être écrasée. Sans cela,
scrubber en arrière dans le débogueur lirait une arène réécrite mille fois depuis.
#Agrégats
| Kind | Représentation |
|---|---|
vec(κ, N) |
un span contigu de N éléments de κ. N est une capacité, donc un vecteur plus court habite un plus long avec une queue de na. |
record{…} |
une struct plate — dans le datapath, un groupe de colonnes parallèles, une par champ |
variant{…} |
un tag plus son payload |
Map, Set, Deque, Tree |
des structures d'arène bornées, ordonnées, sans hachage — voir collections |
#Le datapath est columnar
Le moteur évalue un graphe sur des barres, et il le fait colonne par colonne, pas ligne par
ligne. Chaque nœud produit un Float64Array de longueur égale à la capacité en barres ; les sinks
portent leurs propres colonnes. Les records sont struct-of-arrays : un nœud bollinger est trois
colonnes, pas un tableau d'objets à trois champs.
Deux conséquences valent d'être nommées :
- La boucle chaude n'alloue rien. Chaque buffer est alloué une fois, depuis le graphe, avant la première barre. Il n'y a aucune allocation, aucune libération, et aucune fragmentation pendant le pas.
- La plupart des positions d'anneau sont dérivées ; deux familles gardent un curseur. Un kernel
fenêtré tient son historique dans un anneau de sa période
p. Pour les kernels sans curseur — les sommes glissantes (sma,sum,stdev,bollinger), les scans d'anneau purs (wma,cci,change,stderr,percentrank) et les fenêtres pondérées (winw,alma) — le slot pour la barrekestk mod p, lu directement sur le compteur de barres, et il n'y a aucun index d'écriture à persister parce qu'il n'y en a aucun à garder. Deux familles sont véritablement différentes, et la différence est exactement l'état qu'un checkpoint doit porter :rsimet en anneau les variations, pas les barres. Il n'en pousse une que sur une barre dont le prédécesseur est fini, donc son curseur d'écriture compte les pushes, pas les barres — un trou dans les données avanceket laisse le curseur là où il était. Il n'est donc pas une fonction dek, et il est persisté, aux côtés des moyennes courantes.- Les déques monotones —
highest,lowest,aroonup,aroondown— persistent une tête et une queue. Les positions d'anneau sont dérivées de ces deux compteurs ; les compteurs eux-mêmes sont de l'état, réécrit à chaque barre.
Le plan d'état compte chacun d'eux : une position dérivée ne coûte rien, un curseur coûte une
cellule. Et puisqu'un checkpoint est un memcpy de toute la région d'état, ces cellules voyagent avec
lui — l'anneau d'un rsi et la tête et la queue d'un highest sont dans le snapshot, pas
reconstruits depuis le compteur de barres de l'autre côté.
#La carte de mémoire linéaire
Le module compilé possède une seule mémoire linéaire, interne et exportée, avec cette disposition :
[ header: bar count · write index ][ state cells ][ 6 bar columns ][ sink columns ]
Le header, ce sont deux champs i32, et le second mérite sa place. Le premier est le compteur
de barres. Le second, quatre octets plus loin, est l'index d'écriture des colonnes — combien de
barres ont réellement été écrites. Dans un run batch, et dans un run live sur l'historique, les deux
sont égaux et rien d'observable ne les sépare. Sous une fenêtre ils divergent : l'index
d'écriture est le compte des barres de fenêtre écrites, et chaque lecture de colonne suit lui,
pas le compteur de barres. Ils partagent une seule cellule de header, donc un snapshot et une
réinitialisation couvrent les deux d'un coup.
Les colonnes sink sont la sortie observable, et leur ensemble est clos : plot, mark, fill,
colorBars, alert, assert.
Tout ce qu'elle contient est dimensionné à la compilation, à partir de la capacité en barres
déclarée et des paramètres résolus. La mémoire est déclarée avec min = max = ceil(layout / 64 KiB)
pages, donc la croissance n'est pas simplement inutilisée — elle est impossible. Le plafond n'est
pas une vérification à l'exécution ; il est l'allocation.
C'est ce qui rend le budget honnête. L'empreinte d'un volet est connue avant qu'il ne s'ouvre : le Model borné, plus l'arène de vue, plus le plan propre du graphe.
Pourquoi les paramètres sont résolus à la compilation. L'unité de compilation est (graphe, paramètres résolus, capacité en barres). Changer un réglage recompile et re-passe au gate. La récompense est une disposition d'état 100 % statique, une mémoire
min = maxexacte, et — la partie porteuse — un gate de byte-identité qui valide les octets mêmes et l'instance même qui vont servir. Il n'y a aucun écart entre ce qui a été validé et ce qui s'exécute.
#Le plan de vivacité
Le graphe est pur, total, causal et sans aliasing, et chaque buffer a une taille const-foldée. Par conséquent la durée de vie de chaque buffer est statiquement exacte : sa première utilisation et sa dernière utilisation peuvent se lire sur l'ordonnancement. Le compilateur exploite cela avec une passe de vivacité obligatoire.
La passe a trois étapes :
- Calculer l'intervalle de chaque buffer
[première utilisation, dernière utilisation]sur l'ordre topologique canonique — l'unique linéarisation obtenue en départageant chaque égalité entre nœuds prêts par l'identité lexicale de nœud épinglée (la même identité qui ancre le hachage et l'index de tirage du générateur aléatoire, invariante sous l'inlining, l'élimination de code mort, l'élimination des sous-expressions communes et la recompilation). - Classifier. Un buffer est live-out — gardé pour toute l'instance — s'il alimente une
sortie observable, ou s'il survit à son tick (capturé par un
scan, un champ de Model, un checkpoint). Sinon il est transitoire et recyclable dès que sa dernière utilisation passe. - Colorer les intervalles. Les transitoires se voient assigner des slots par coloration d'intervalles gloutonne au sein d'une classe de taille : deux buffers dont les intervalles sont disjoints partagent le même slot, dans un ordre d'affectation épinglé au rang canonique. Jamais un index de première apparition, jamais un ordre d'itération de hash, jamais une adresse.
L'empreinte rapportée est alors le pic d'octets simultanément vivants, pas la somme de chaque
buffer qui a un jour existé. (Le produit maxNodes × N_max reste une borne de somme — il garantit
la terminaison et le rejet à la compilation, et il n'est délibérément pas le même nombre que le pic.)
Dans le back-end WebAssembly le plan fait double emploi : un local f64 par slot de vivacité,
donc le plan est littéralement la carte d'allocation des locals, et le pic est bien en dessous du
nombre de nœuds — surtout une fois que plusieurs scripts sont fusionnés en un seul graphe.
Donation en place (design scellé — le planificateur livré aujourd'hui ne partage les slots
qu'entre durées de vie disjointes ; la donation est spécifiée mais pas encore émise). Un nœud
fonctionnel (vec.setAt, une dérivation de colonne, une mise à jour de record) avec un unique
consommateur à sa dernière utilisation écrirait en place dans le slot de son entrée mourante au
lieu de copier — opt-in, et gardé par la validation de traduction de sorte qu'une donation non sûre
(deux consommateurs, ou une source encore vivante) est un échec de build, jamais un écrasement
silencieux.
Pourquoi le plan lui-même doit être déterministe. L'oracle de valeur compare des sorties ; il est aveugle à la disposition. Le partage de slot est invariant en valeur (un slot n'est réutilisé qu'après la dernière utilisation de son occupant, donc aucune lecture ne voit jamais une valeur écrasée) — donc deux plans différents produiraient des sorties identiques et l'oracle ne remarquerait rien. Mais le plan est gravé dans le module émis. S'il n'était pas une fonction déterministe du graphe, deux compilations du même programme différeraient en octets. Le plan rejoint donc les routines épinglées — les mathématiques épinglées, la routine decimal, les tables Unicode, la conversion de calendrier, le générateur aléatoire — comme quelque chose qui doit être identique sur chaque moteur et chaque machine.
#Bornes et budgets
| Borne | Valeur | Nature |
|---|---|---|
N_MAX |
10 000 (cible navigateur) | la fenêtre, période ou délai maximum d'un kernel |
N_MAX_SERVER |
100 000 | la même borne, cible serveur/backtest |
MAX_NODES |
3 072 | la taille du graphe — jugée sur le graphe après inlining, élimination et élimination des sous-expressions communes |
N_ACTIVE_MAX |
16 | scripts co-actifs fusionnés en un seul graphe global |
maxBricksPerBar |
1 000 | le plafond du nombre de boxes qu'une seule barre de temps peut traverser dans une représentation dirigée par le prix |
Trois propriétés de ces nombres comptent plus que les nombres eux-mêmes.
La borne de fenêtre est une borne de validation, pas un paramètre de calcul. Elle décide de
l'acceptation ; elle n'entre jamais dans un calcul. Un programme qui compile sous deux valeurs
différentes de N_MAX produit des octets identiques, et sa mémoire est dimensionnée par les
périodes qu'il utilise réellement. C'est ce qui permet au navigateur et au serveur de porter des
plafonds différents sans forker le langage : le compilateur enregistre la longueur maximale réelle du
programme, et le loader vérifie environnement ≥ programme. Un pack serveur avec une période de
50 000 barres est refusé d'emblée dans un navigateur — proprement, au chargement — au lieu
d'échouer à l'instanciation.
La borne de graphe est jugée sur le graphe. Les gardes du front-end (sur l'arbre syntaxique, sur l'expansion de l'inlining) siègent à 64× le budget de graphe : ce sont une mesure anti-abus du compilateur, pas un verdict sur votre programme.
Le verdict de compilation n'est que des compteurs déterministes. L'acceptation ou le rejet est une fonction pure de la source. Il y a un timeout de build interactif dans l'éditeur — autour de deux secondes — mais c'est une annulation, jamais un verdict. Un verdict à l'horloge murale signifierait que le même programme est accepté sur une machine et rejeté sur une autre, ce qui casserait le rejeu et l'anti-triche à la racine.
Il n'y a de même aucun timeout par barre. Le coût à l'exécution est statiquement borné par
maxNodes × N_max × maxBricksPerBar ; la totalité donne la terminaison, et ces plafonds donnent la
borne pratique. Dépasser le budget est un rejet à la compilation — un programme n'est jamais tué en
plein milieu d'une barre.
#Isolation : volets, workers, arènes
La couche worker décrite ici est livrée : le graphe est partitionné, ordonnancé et exécuté sur un pool aujourd'hui, et la preuve 1≡N dans Concurrence est la raison pour laquelle c'est sûr. Deux choses dans ce domaine sont Post-v1. — le realm applicatif (le Model par volet et son arène de vue) et le module de calcul partagé. Les deux sont marquées comme telles là où elles apparaissent ci-dessous, et ni l'une ni l'autre n'est une description du runtime.
Une instance de module par tâche, avec sa propre mémoire. C'est l'isolation livrée, et c'en est
la forme forte. Une tâche reçoit les octets compilés et est instanciée sur le worker qui va
l'exécuter. Le module déclare sa propre mémoire linéaire interne, min = max pages, et il est
instancié contre des imports de fonctions uniquement — les transcendantales épinglées, rien
d'autre. Il n'importe aucune mémoire, et il n'y a aucune mémoire à importer : pas une seule mémoire
linéaire dans le runtime n'est déclarée partagée.
Partagées, en lecture seule : les six colonnes d'entrée. L'hôte écrit time, open, high, low, close et volume une fois, dans un seul shared array buffer, et chaque worker y lit ses barres. C'est là tout le partage.
Possédé, en écriture : tout ce qu'une tâche produit. Une colonne sink est calculée dans la
mémoire privée du module et rendue par transfert de buffer — zéro-copie, et l'émetteur perd le
buffer en le donnant. Jamais par partage. Ce qui permet à la règle suivante d'être absolue : aucun
atomic ne touche jamais aux données. Les atomics ne servent que les compteurs claimed et done
de l'ordonnanceur, dans un petit buffer à eux.
Les arènes sont par worker. L'arène scratch pour les buffers transitoires est privée à chaque worker, dimensionnée au pic du sous-graphe de ce worker. Sous la règle ci-dessus elle pourrait difficilement être autre chose — rien d'inscriptible n'est partagé, donc il n'y a aucune arène partagée dans laquelle tomber.
Pourquoi les arènes par worker sont porteuses. Le plan de vivacité lit la disjonction sur l'ordre canonique séquentiel. Deux buffers peuvent légitimement partager un slot parce que leurs intervalles ne s'y chevauchent pas — et pourtant, sous un ordonnanceur dynamique, les nœuds qui les possèdent peuvent s'exécuter en même temps sur deux workers. Une arène partagée leur donnerait la même adresse : une course écriture-écriture, et un échec de byte-identité qu'aucun oracle de valeur n'attraperait. Les arènes privées rendent l'aliasing impossible par construction plutôt que par discipline d'ordonnancement.
Post-v1. Un module de calcul partagé. Le design éventuel a une forme différente, et il vaut la peine de l'énoncer précisément pour que personne ne le relise dans le runtime. Un seul module de calcul partagé — les kernels, le cœur du moteur — instancié par worker contre une seule mémoire linéaire partagée, chaque worker adressant sa propre région par offset ; chaque module applicatif l'importe alors au lieu de recompiler les kernels en lui-même, et ne porte donc que sa propre logique : petit, rapide à instancier, invalidé indépendamment. Rien de tout cela n'est livré. Aujourd'hui un kernel qui porte de l'état entre les barres est inliné par nœud dans le module qui l'utilise, et les scans d'anneau purs qui restent des fonctions partagées sont partagés au sein d'un module, jamais entre deux. La réécriture échangerait le zéro partage inscriptible contre un partage discipliné par région et rouvrirait le snapshot, le fenêtrage et la machinerie de vérification pour le faire — pour un futur dont le runtime actuel n'a pas encore besoin. Le raisonnement est exposé dans Concurrence.
Post-v1. Une instance par volet. Un volet applicatif est un module WebAssembly instancié pour ce volet — sa propre mémoire linéaire, pas de DOM, accès à l'hôte uniquement à travers des imports validés. Fermer le volet libère l'instance et tout ce qu'elle contient. Deux niveaux, délibérément distincts : le realm applicatif (un Model, une arène de vue) est isolé par volet, tandis que le pool de calcul exécute le graphe. Confondre les deux est l'erreur classique ici.
#Checkpoints et snapshots
Parce que tout l'état vit dans une seule région contiguë avec une disposition statique, un checkpoint est un memcpy de cette région — et une restauration est la même copie en sens inverse. La reprise est exacte à l'octet, ce qui fait du voyage dans le temps du débogage, du scrubbing de l'aperçu live et du rejeu côté serveur le même mécanisme plutôt que trois approximations d'un seul.
À la frontière de sérialisation la règle na canonique s'applique, donc un snapshot pris par un
moteur est byte-identique à un pris par un autre.
#Le plan APP : models bornés et slotmaps
Post-v1. Le Model d'une application n'admet que des kinds bornés — de sorte que son empreinte
est calculable à la compilation, exactement comme celle d'un graphe. Ses collections variables
utilisent le pattern slotmap : un vecteur borné, des tombstones au lieu de compaction, un masque
live, et une free list tenue dans un vecteur d'index parallèle de sorte que le tombstone
lui-même n'est jamais écrasé. Rien n'est jamais déplacé, donc aucun handle n'est jamais invalidé, et
le plan mémoire reste plat.
Voir Plan app.
#Ce qui n'existe pas
Pas de ramasse-miettes. Pas d'allocateur à l'exécution. Pas de fragmentation. Pas de manque de mémoire à l'exécution. Pas de « ça marchait sur ma machine ». Un programme soit tient dans son budget déclaré — et alors il y tient sur chaque machine, octet pour octet — soit ne compile pas.
#Voir aussi
- Compilateur et runtime — le pipeline, le gate de byte-identité, et les routines épinglées.
- Optimiseur — les passes qui façonnent le graphe à partir duquel le plan est calculé.
- Concurrence — l'ordonnanceur, la mémoire partagée, et la preuve 1 ≡ N.
- Kinds — d'où viennent
na,decimal,stringet les capacités devec. - collections — les structures d'arène bornées.
- Plan app — Models bornés, slotmaps, journaux et checkpoints.
Optimiseur
L'optimiseur de Flux est agressif, et personne n'a besoin de lui faire confiance. Ces deux faits sont le même fait, et cette page explique pourquoi.
La raison pour laquelle l'optimiseur d'un compilateur est d'ordinaire une source d'anxiété, c'est que sa correction est argumentée, pas vérifiée : une réécriture a l'air correcte, elle est livrée, et trois ans plus tard quelqu'un trouve l'entrée pour laquelle elle ne l'était pas. Flux prend l'autre route. La sémantique de référence d'un programme est l'évaluation canonique de son graphe non optimisé, et chaque compilation vérifie le module optimisé contre elle, bit pour bit, sur des données hostiles. Un optimiseur auquel on ne peut pas faire confiance, c'est très bien — ce qui compte, c'est qu'une mauvaise compilation ne puisse pas être livrée.
#La loi
Sémantique de référence = l'évaluation canonique du graphe non optimisé. À chaque compilation, le gate exécute cet oracle contre le module émis du graphe optimisé — en batch, puis barre par barre à travers le chemin live, sur un corpus hostile à graines mélangées et, là où elles sont disponibles, sur de vraies données — et exige l'égalité bit-exacte sur chaque colonne sink.
Une seule comparaison couvre l'optimiseur et l'émetteur de code, de bout en bout. C'est le même gate qui impose interpréteur ≡ WASM (compilateur et runtime), faisant double emploi : l'oracle contre lequel il compare est l'évaluation non optimisée, donc une réécriture qui change une valeur d'un seul bit échoue au même contrôle qu'une mauvaise sélection d'instruction.
Quand l'optimiseur ne change rien, le gate ne coûte rien — un chemin rapide d'identité le saute entièrement. Quand il se déclenche et passe, le coût est le run de l'oracle, qui était déjà payé.
#Quand une réécriture est fausse
Si la tentative optimisée diverge, la compilation réessaie avec le graphe non optimisé et sert celui-là, en portant un diagnostic. Deux conséquences, et les deux sont délibérées :
- Le produit est silencieusement correct. Un utilisateur ne voit jamais un mauvais nombre ; au pire il obtient le chemin non optimisé, qui est de toute façon le chemin par lequel la valeur était définie.
- La suite de tests traite un tel diagnostic comme un échec. Il est bruyant en intégration continue et invisible dans le produit — ce qui est exactement le bon sens.
#La borne de couverture honnête
Le gate prouve l'égalité sur la couverture en valeur du corpus (ses zones hostiles : na,
±infini, zéro négatif, égalités, magnitudes extrêmes, sous-normaux, proche de l'overflow) et sur les
périodes de réglages jusqu'à un plafond de balayage — un compromis anti-abus délibéré, puisque
balayer chaque période de chaque réglage à chaque compilation serait un déni de service sur le
compilateur lui-même.
Ainsi une règle dont la divergence ne se manifeste qu'à une période effective au-delà de ce plafond passerait le gate par compilation. Ce n'est pas un trou que nous masquons : c'est la raison pour laquelle la charte des règles porte une obligation de preuve explicite pour exactement cette classe de règle (obligation 5, ci-dessous). Dire « le gate prouve tout » serait plus confortable et moins vrai.
#La charte des règles
Chaque règle de réécriture doit satisfaire les cinq, et doit documenter son argument pour chacune :
- Bit-exacte en IEEE-754
f64pour chaque entrée — y compris les cheminsna, les zéros signés et les infinis. Aucune réassociation en virgule flottante, jamais. - Pureté et ordre. Les règles recâblent le dataflow pur. Un nœud à état (un délai, un croisement, un kernel) ne peut être partagé ou remplacé que lorsque le remplacement produit de façon prouvable la trajectoire d'état identique — mêmes dépendances, mêmes paramètres.
- Déterminisme. Aucune décision dépendante des données et aucune dépendante de l'environnement. Première correspondance par ordre de table. Chaque réécriture passe par le reconstructeur à hash-consing, donc le même graphe se reconstruit toujours de la même façon.
- L'ABI est intouchable. Une règle ne peut jamais éliminer, fusionner, re-typer ou réordonner un
nœud
input— le bloc de paramètres est un contrat avec l'hôte, et un optimiseur qui laisserait tomber « obligeamment » un réglage inutilisé casserait chaque chart sauvegardé. - Preuve de mise à l'échelle en période. Toute règle qui touche un kernel, un délai ou de l'état partagé est livrée avec un test dédié à la période maximale réelle, parce que le gate par compilation ne prouve que les périodes jusqu'au plafond de balayage. Les peepholes élément par élément sont indépendants de la période par construction et en sont exemptés.
#Les règles qui ont l'air correctes et ne le sont pas
Cette table est la chose la plus utile de cette page. Chacune de ces réécritures apparaît dans les manuels ; chacune d'elles est fausse en IEEE-754, et Flux les rejette toutes :
| Réécriture tentante | L'entrée qui la tue |
|---|---|
x + 0 → x |
x = −0. Alors −0 + 0 = +0, ce qui n'est pas −0. |
0 − x → neg(x) |
x = +0. Alors 0 − 0 = +0, tandis que neg(+0) = −0. |
select(c, x, x) → x |
c = na. Le résultat est na, pas x. |
x − x → 0 |
x = na ou ±∞. Le résultat est na. |
x * 0 → 0 |
x = na ou ±∞ ⇒ na ; et x = −1 ⇒ −0, pas +0. |
(a + b) + c → a + (b + c) |
La réassociation change l'arrondi. Interdite d'emblée. |
Et celles qui sont correctes, chacune avec son témoin :
x * 1 → x · 1 * x → x · x / 1 → x (la multiplication et la division par exactement 1.0 sont
exactes) · x + (−0) → x (parce que +0 + −0 = +0 et −0 + −0 = −0) · neg(neg(x)) → x (retourner
un bit de signe deux fois est l'identité) · x + x → 2 * x (la même opération arrondie).
Pourquoi le zéro négatif mérite autant de respect. Ce n'est pas une curiosité. Une valeur de
−0surgit constamment dans de vraies données (une différence qui arrondit à zéro par en dessous), elle se compare égale à+0, et elle s'affiche comme0— donc une réécriture qui transforme l'un en l'autre a l'air correcte dans chaque test qu'un humain écrit. Elle n'est visible que pour un oracle au niveau de l'octet, ce qui est précisément pourquoi l'oracle au niveau de l'octet existe.
#Le moteur de passes
Les règles sont la partie intéressante ; le moteur qui les exécute est la partie qui ne doit jamais être intéressante. Quatre invariants le maintiennent plat.
Chaque passe reconstruit, et le reconstructeur hash-conse. Une passe ne mute pas le graphe en
place — elle le reconstruit en ordre topologique à travers la même clé structurelle que
l'abaissement utilise (une seule fonction de clé, une seule source). Deux conséquences en découlent
gratuitement. L'élimination des sous-expressions communes en cascade après une réécriture ne coûte
rien : si une règle rend deux sous-graphes identiques, la reconstruction est la fusion. Et un
graphe arrivé non éliminé — un forgé, ou un construit à la main — est normalisé au passage. L'unique
exception est le nœud input, qui n'est jamais hash-consé : deux réglages avec le même défaut
sont deux réglages, et les fusionner fusionnerait silencieusement deux réglages qu'un utilisateur
peut déplacer indépendamment.
La renumérotation est monotone. L'ordre relatif des nœuds survivants est préservé à travers le
balayage de code mort. Ce n'est pas cosmétique. Les cellules du bloc de paramètres sont disposées
dans l'ordre des id de nœuds input, donc une passe qui permuterait les ids déplacerait la
cellule d'un réglage sous un hôte qui s'y était déjà lié. La renumérotation monotone est ce qui garde
les cellules de réglage stables à travers l'optimisation.
La terminaison est bornée, et l'échec est l'identité. Les passes se répètent jusqu'à un point fixe — zéro déclenchement de règle et zéro compaction structurelle — sous un plafond de huit passes. Le plafond est généreux avec une large marge : la cascade de réécriture la plus profonde que la table de règles peut produire est de trois de profondeur. Et le moteur est total. Une règle qui lève une exception, une réécriture qui produit un id de nœud invalide, une post-condition qui échoue — n'importe laquelle renvoie le graphe d'entrée, inchangé, signalé, et la compilation sert le chemin non optimisé. L'optimiseur n'a aucun mode de défaillance qui ne soit « l'optimiseur n'a rien fait ».
Les post-conditions sont re-vérifiées, pas supposées. Après la dernière passe, le validateur de
forme tourne à nouveau sur le graphe optimisé, et le plan mémoire (A13) en est re-dérivé plutôt
que reporté — une passe qui a changé le graphe a changé la vivacité, et un plan périmé serait un bug
de partage de buffer qu'aucun oracle de valeur ne pourrait voir. Un remap enregistre où chaque nœud
original a atterri ; un id absent de celui-ci est un nœud que l'optimiseur a prouvé mort.
Pourquoi le moteur est total plutôt que correct. Ces deux paragraphes décrivent un moteur qui est autorisé à échouer, à tout moment, pour n'importe quelle raison — et dont l'échec est indiscernable de n'avoir rien fait. C'est une inversion délibérée. Nous ne tentons pas de prouver le moteur de passes juste ; nous le rendons structurellement incapable de livrer un graphe dont il n'est pas sûr, et nous pointons le gate au niveau de l'octet vers tout ce qu'il produit. Correction par vérification, totalité par construction.
#Les paliers
T0–T1 — bit-sûr, et livré :
| Passe | Ce qu'elle fait |
|---|---|
| dispatch de kernel natif | une feuille devient le kernel natif — le mécanisme de la byte-identité lui-même |
| élimination globale des sous-expressions communes | la grosse : les sous-expressions identiques sont calculées une seule fois |
| élimination de code mort | une valeur que personne ne lit n'est jamais calculée |
| constant folding | les littéraux const-foldés s'effondrent |
| fusion préservant l'ordre | une chaîne élément par élément devient une seule passe |
| sélection récursive / fenêtrée / batch | choisir la forme d'évaluation la moins chère — celle que le kernel natif utilise déjà |
| partage de buffer | le plan de vivacité : les durées de vie disjointes partagent un slot (modèle mémoire) |
| boucle chaude à zéro allocation | chaque buffer est alloué une fois, depuis le graphe |
| live O(1) | la causalité rend un pas incrémental peu coûteux |
| élimination entre scripts | les scripts co-actifs sont fusionnés en un seul graphe, donc une sous-expression qu'ils partagent est calculée une fois pour eux tous |
#Élimination entre scripts — la passe qui paie vraiment
La dernière ligne de cette table mérite sa propre section, parce que c'est là que réside la
redondance dans un vrai chart. Un chart n'exécute pas un seul script. Il exécute la poignée que
vous avez ouverts, et ils se chevauchent : deux indicateurs veulent tous deux ema(close, 200) ; une
stratégie et son filtre veulent tous deux le même sma.
Donc les scripts co-actifs sont fusionnés en un seul graphe et optimisés ensemble. L'élimination
traverse alors la frontière de script sans savoir qu'il y en avait une : un ema(close, 200) dans
deux scripts est un seul nœud, calculé une fois. L'état s'effondre avec elle — un sma dans un script
et un sum de la même source et période dans un autre finissent par partager un seul ring buffer
plutôt que deux.
Trois règles gardent cela correct, et chacune ferme une façon spécifique dont cela aurait pu mal tourner :
- L'hôte déclare l'ensemble. Quels scripts sont co-actifs est une déclaration explicite, pas une inférence de ce qui se trouve être vivant. L'ensemble a une clé canonique (ses membres, triés), donc l'ordre dans lequel ils ont été ouverts est inobservable et le même ensemble réutilise le même module.
- Les knobs ne fusionnent jamais. Les nœuds
inputsont exclus de l'élimination à travers la frontière, donc deux scripts qui exposent tous deux un réglage « period » gardent deux réglages indépendants. Le graphe est partagé ; le bloc de paramètres ne l'est pas. - L'oracle reste par script. Le gate ne compare pas une évaluation du graphe fusionné. Il compare l'évaluation non optimisée propre de chaque script contre la tranche des sinks du module fusionné correspondant à ce script — batch, live, à graines mélangées, bit-exact. C'est le contrôle le plus fort, et c'est le moins cher : une divergence nomme le script coupable au lieu de pointer vers un graphe que personne n'a écrit.
L'ensemble est borné — au plus seize scripts co-actifs, chacun dans le budget de nœuds ordinaire — parce que au-delà de ce point le budget de frame agrégé et sa politique de dégradation (concurrence) sont le bon instrument, pas une fusion plus grande.
Deux frontières honnêtes. Un pack fermé livre un module et aucun graphe, donc il n'y a rien dans quoi le fusionner : il est exclu par construction plutôt que par politique. Et la fusion s'applique au chemin batch ; le chemin live barre par barre avance le module propre de chaque script, donc un ensemble co-actif partage sa compilation et son état, pas son pas live.
La canonicalisation qui ressemble à une pessimisation.
sma(x, p)est réécrit ensum(x, p) ÷ p— inconditionnellement, et non parce qu'une division est moins chère. C'est une normalisation : elle rend unsmaet unsumsur la même source et période le même nœud, ce qui est ce qui permet à deux scripts de partager un anneau. La réécriture porte une garde de domaine (la période doit être une constante, ou un réglage avec des bornes déclarées), parce qu'en dehors de ce domaine les deux formes clampent différemment — obligation 2 de la charte, acquittée en restreignant la règle plutôt qu'en espérant.
#T2 — le palier agressif, et ce qu'il en est advenu
La prémisse était inhabituelle : les scripts Flux sont courts. Un graphe de quelques dizaines de nœuds tient dans un budget sous les 16 millisecondes même sous des passes normalement infaisables — donc la cible pouvait être l'optimum plutôt qu'« assez bon ».
Deux de ces passes restent conçues et différées. La troisième a été étudiée et rejetée, et le rejet vaut plus que la passe n'aurait valu.
Post-v1. Ordonnancement et fusion optimaux — une recherche exhaustive, traitable à cette taille — et spécialisation et évaluation partielle à partir de constantes et de bornes de kind.
Saturation d'égalité : non. La passe est la réponse classique à « appliquer toutes les réécritures d'un coup » : construire un e-graph, unir chaque terme équivalent dedans, puis extraire le membre le moins cher sous un modèle de coût. Elle est prouvablement équivalente, et sur la bonne table de règles elle est véritablement plus forte qu'un point fixe. Sur cette table de règles elle est plus forte que rien, et l'argument repose sur deux jambes indépendantes :
- L'espace de recherche est déjà épuisé. Les règles livrées sont orthogonales — leurs membres gauches sont structurellement disjoints et linéaires à gauche — ce qui rend le système de réécriture confluent, et le point fixe direct d'un système confluent atteint l'unique forme normale. Il n'y a pas de meilleure forme qu'un e-graph puisse trouver. Ce n'est pas un accident de la table actuelle : c'est forcé par la loi de correction. Les classes de réécriture où la saturation brille vraiment — réassociation, commutativité, distributivité — sont exactement les classes que la loi de bit-exactitude interdit, parce qu'elles changent l'arrondi IEEE. Une table de règles qui ne peut pas être générative est une table de règles qu'un e-graph ne peut pas aider.
- Le seul endroit où l'extraction pourrait différer est un fantôme. La canonicalisation
sma → sum ÷ pci-dessus ressemble au cas pour l'extraction pilotée par le coût : pour unsmaisolé, le modèle de coût statique notesum ÷ pplus haut, donc un e-graph ré-extrairait la formesma. Il ne gagnerait rien. L'émetteur abaisse les deux formes vers la même séquence d'instructions, et la formesmaporte une gardenasupplémentaire par-dessus — donc l'extraction « moins chère » est marginalement plus grande, et la différence mesurée à cinquante mille barres est sous le pour cent. L'extraction aurait optimisé un artefact du modèle de coût, pas le programme.
Le verdict est un test, pas un paragraphe. L'orthogonalité sur laquelle repose la première jambe est une condition, et les conditions pourrissent. Elle est donc affirmée en permanence, dans la suite : une sonde sur le corpus de grammaire qui échoue à l'instant où une future règle en chevauche une existante ; un contrôle de confluence empirique qui pilote les règles dans des ordres ensemencés adversariaux et exige qu'elles atterrissent toutes sur le même nombre de nœuds et le même coût ; et une borne sur la profondeur de cascade de réécriture. Un échec de la première sonde ne fait pas que faire échouer un test — il invalide cette décision, et rouvre la passe.
Les conditions de réouverture nommées, dans l'ordre où elles sont susceptibles d'arriver : une
règle dont le membre gauche chevauche celui d'une autre ; un mode de tolérance (@fast,
ci-dessous), qui admet les classes génératives et avec elles l'espace de recherche pour lequel la
saturation a été construite ; une règle dont les deux formes émettent véritablement différemment,
faisant tomber la seconde jambe ; ou une divergence dans le contrôle de confluence. N'importe laquelle
d'entre elles, et la passe revient — avec l'extraction pilotée par le coût, et avec le départage
ci-dessous.
Ce qu'une réouverture devrait livrer dès le premier jour. Quand deux extractions ont un coût égal, l'égalité doit être départagée par l'identité lexicale de nœud épinglée — la même identité qui ancre le plan mémoire et l'index de tirage du générateur aléatoire. Sinon deux extractions de coût égal émettraient des octets différents, et l'oracle de valeur — qui compare des sorties, pas des dispositions — ne le remarquerait jamais. C'est écrit ici pour que ce soit un prérequis plutôt qu'une découverte.
Post-v1. T3 — opt-in, jamais le défaut : @fast relâche la virgule flottante (réassociation,
fused multiply-add). C'est plus rapide et ce n'est pas bit-exact, donc ses goldens porteraient une
tolérance. Le défaut reste déterministe, parce que la valeur d'un langage de charting, c'est son
no-repaint, son rejeu et ses goldens — et les trois sont des propriétés au niveau de l'octet. Il
attend un cas de bench qui montre que le relâchement vaut ce qu'il coûte ; l'audit jusqu'ici place le
f64 scalaire à un petit facteur près de la forme relâchée, ce qui n'est pas un cas.
#Les règles qui sont livrées, dans le WebAssembly qu'elles changent
Les paliers nomment les passes ; les voici de l'autre côté — chaque règle telle qu'elle existe dans le compilateur, et, pour deux d'entre elles, le WebAssembly avant et après. Le WAT est écrit à la main pour la lecture (les séries sont montrées comme des locals ; l'émetteur les charge depuis des colonnes mémoire), mais les formes sont celles qu'il produit. Chaque règle porte son argument IEEE écrit — la première obligation de la charte — et celles élément par élément sont indépendantes de la période, donc la preuve de mise à l'échelle en période ne s'applique pas à elles.
Les peepholes. Réécritures locales, chacune exacte pour chaque entrée :
- Les identités —
x·1 → x,x÷1 → x,−(−x) → x. Multiplier ou diviser par exactement1.0renvoiexintact, et la négation retourne un bit de signe, donc deux fois est l'identité —±0,±∞etnainclus. - Réduction de force — une division par une puissance de deux,
x ÷ 2^k, devient une multiplication par son inverse,x · 2^-k. La division est l'instruction coûteuse ; une puissance de deux et son inverse sont toutes deux exactes, donc la multiplication atterrit sur les mêmes bits. Un÷3est laissé tranquille, parce que1/3n'est pas représentable. (WAT ci-dessous.) - Constant folding — une opération arithmétique, de comparaison ou logique sur des constantes
s'effondre en une seule constante, à travers les mêmes opérations
f64que l'interpréteur et le module exécutent. Les transcendantales se replient aussi :log,exp,sin…powsur une constante sont évaluées à la compilation par la même libm épinglée que le run utilise, donc les bits repliés sont les bits qu'une barre aurait produits. - Peepholes du domaine booléen —
a < bsous une négation devienta ≥ b; une double négation¬¬bdevientb; et le quotidienif b then 1 else 0devientb. Chacun ne tient que là où le compilateur a prouvé que la valeur est0,1ouna— la sortie d'une comparaison, d'une opération logique ou d'un croisement. La preuve est la garde : hors de ce domaine,¬¬5est1, pas5. - Normalisation de kernel —
sma(x, p) → sum(x, p) ÷ p. Pas une réécriture de vitesse : la normalisation discutée sous la fusion entre scripts ci-dessus, celle qui rend unsmaet unsumsur la même source et période le même nœud, pour qu'ils puissent partager un anneau.
Les passes sans table de règles — elles sont la machine.
- L'élimination des sous-expressions communes est le reconstructeur. Chaque passe reconstruit le graphe à travers une seule clé structurelle et hash-conse au passage, donc une sous-expression identique est un seul nœud quel que soit le nombre de fois qu'elle a été écrite. Il n'y a rien à exécuter : la reconstruction est la fusion. (WAT ci-dessous.)
- Le code mort est balayé. Un nœud qu'aucun sink n'atteint n'est jamais émis, et le plan mémoire est re-dérivé de ce qui reste.
- L'ordonnancement est poussé pour aider le plan mémoire. Après réécriture, la vivacité (le plan A13) est recalculée, et les nœuds prêts sont réordonnés de façon gloutonne pour que chacun soit émis quand il libère le plus de slots — jamais-pire : si le pic ne baissait pas, l'ordre original tient et aucun octet ne bouge. Les buffers aux durées de vie disjointes partagent alors un slot. Voir modèle mémoire.
Réduction de force, ÷2 → ×0.5. La moyenne du high et du low de la barre —
fluxplot (high + low) / 2
— s'abaisse vers une division, puis devient une multiplication par l'inverse exact :
;; before — the division as written
local.get $high
local.get $low
f64.add
f64.const 2
f64.div
;; after — same bits, cheaper instruction
local.get $high
local.get $low
f64.add
f64.const 0.5
f64.mul
Les deux formes sont bit-pour-bit égales parce que 2 et 0.5 sont toutes deux exactes, donc chacune
arrondit le même nombre réel une fois. Cette égalité n'est pas argumentée : à chaque compilation le
gate ré-exécute le graphe non optimisé sur l'interpréteur et compare. (L'interpréteur et le module
sont les deux implémentations conformes de la FVM, liées par I7.)
Élimination des sous-expressions communes, un nœud à partir de deux. Alimentez la plage high-low dans deux plots —
fluxplot (high - low) * 2
plot (high - low) + close
— et le graphe naïf calculerait la soustraction deux fois ; le reconstructeur l'émet une fois, dans un slot, et les deux lecteurs le prennent de là :
;; before — the range, recomputed
local.get $high
local.get $low
f64.sub
f64.const 2
f64.mul
local.get $high
local.get $low
f64.sub ;; the same work, again
local.get $close
f64.add
;; after — computed once, reused
local.get $high
local.get $low
f64.sub
local.tee $hl ;; keep the range in a slot
f64.const 2
f64.mul
local.get $hl ;; reuse it — no second subtract
local.get $close
f64.add
La même machine, exécutée à travers les scripts que vous avez ouverts, est ce qui transforme un
ema(close, 200) partagé dans deux indicateurs en un seul kernel et un seul anneau — la fusion entre
scripts ci-dessus, là où la redondance dans un vrai chart réside vraiment.
La table reste courte exprès. Qu'une réécriture soit correcte est nécessaire, pas suffisant —
elle doit aussi mériter son slot. x + x → 2·x est correcte (le même add arrondi), mais elle échange
un add contre un add plus une constante, donc elle n'achète rien ; x + (−0) → x est correcte aussi,
mais le pattern ne survient pas dans de vrais graphes. Les deux sont laissées de côté sur le coût, pas
sur le doute — la même retenue qui garde l'ensemble de règles orthogonal et le palier agressif fermé.
#L'ABI est un contrat — et la provenance aussi
L'obligation 4 de la charte dit qu'une règle ne peut jamais toucher un nœud input. Cette section est
ce que cette obligation achète, et ce qui l'impose quand un auteur de règle l'oublie.
La garde est mécanique. Après l'optimisation, l'image des nœuds input doit être totale,
injective et toujours typée input — chaque réglage toujours là, aucun deux fusionnés en un, aucun
re-typé. Une règle qui viole l'un des trois ne produit pas un diagnostic et continue : toute
l'optimisation est rejetée comme l'identité, et la compilation sert le graphe non optimisé. Lever
cette garde n'est pas un changement de règle ; ce serait une décision de versionner le schéma de
paramètres.
Les surfaces publiques parlent les ids du graphe non optimisé. L'optimiseur renumérote, mais personne en dehors de lui ne voit jamais ces numéros. Les descripteurs de réglages et les cellules de paramètres sont réécrits vers les ids originaux à la sortie, à travers une back-map que la garde d'injectivité est précisément ce qui rend bien définie. Un hôte qui a sauvegardé un chart contre le réglage 3 trouve le réglage 3 là où il l'a laissé, quoi que l'optimiseur ait fait entre-temps.
Présentation et manifeste dérivent du graphe non optimisé — des deux côtés. La présentation déclarée d'un package (volets, échelles, lignes de référence, noms de séries) et son manifeste de capacités sont dérivés du graphe O0, au moment du build et au moment de la vérification. L'optimiseur affecte les octets du module et rien d'autre ; aucun graphe optimisé n'est jamais sérialisé ni livré. C'est ce qui garde les deux dérivations comparables : un vérificateur qui re-dériverait la présentation d'un graphe optimisé comparerait contre un graphe que l'auteur n'a jamais écrit.
La provenance tient par construction, pas par promesse. Build et vérification passent par le même point d'entrée de compilation, donc le même optimiseur, donc les mêmes octets — ce qui est pourquoi une reconstruction peut être vérifiée du tout. Deux conséquences suivent, et les deux sont tranchantes :
- Un pack n'est jamais scellé depuis un fallback. Si l'optimiseur a été rejeté pendant un build, le build refuse de sceller plutôt que de livrer l'artefact non optimisé. Sinon un vérificateur — dont le propre gate rejetterait l'optimiseur sur la même divergence — recompilerait vers des octets différents et accuserait un pack honnête de mentir. Un optimiseur rejeté au moment du build est un bug à corriger, pas à contourner.
- Une non-correspondance de hash est diagnostiquée, pas seulement rapportée. Quand une vérification trouve des octets différents, elle recompile aussi sans l'optimiseur et compare contre cela, pour pouvoir dire « construit par une toolchain plus ancienne ou non optimisée » au lieu de l'inutile « ce pack ne correspond pas à sa source ».
La toolchain fait partie de l'identité. La version du compilateur est estampillée dans le manifeste du package, et elle est aussi une composante de l'unique clé canonique sous laquelle un artefact compilé est mis en cache — aux côtés de la version épinglée de Binaryen, de la bibliothèque mathématique épinglée, de l'ensemble de fonctionnalités WebAssembly émis, et du programme lui-même. Une clé, une source. À partir du premier artefact publié, tout changement de la table de règles ou du moteur qui altère les octets émis doit faire bouger cette version : un module en cache compilé sous une table de règles différente est un module qui a été passé au gate contre un programme que le compilateur ne produit plus.
Pourquoi la provenance n'est pas la correction. Il est tentant de lire « les octets livrés sont la compilation par la toolchain de cette source » comme « les octets livrés sont corrects ». Ce ne l'est pas. La provenance garantit que les deux côtés ont exécuté le même compilateur ; elle ne dit rien sur les périodes que ce compilateur n'a jamais balayées (la borne de couverture honnête). Les deux côtés portent la même borne. Confondre les deux serait l'erreur la plus confortable de cette page.
#Le plafond honnête
Les kernels restent natifs. Donc l'optimiseur travaille au niveau du graphe — redondance, ordonnancement, spécialisation — et jamais à l'intérieur de l'arithmétique d'un kernel. La réassociation en virgule flottante est interdite par défaut. Par conséquent :
- Un
rsi(close, 14)nu n'a rien à optimiser. Il est déjà le kernel natif. - Le gain croît avec la complexité du script et avec le nombre de scripts co-actifs — c'est là que la redondance réside vraiment.
Revendiquer une accélération sur le cas simple serait du marketing. Le vrai travail de l'optimiseur, c'est que le cas compliqué ne coûte pas ce qu'il a l'air de coûter.
#Le modèle de coût
Le coût d'un nœud n'est pas deviné : des micro-benchmarks le mesurent, et les mesures calibrent la table. Le même modèle est partagé par l'optimiseur et l'ordonnanceur, de sorte que « ce nœud vaut-il un worker ? » et « cette réécriture en vaut-elle la peine ? » sont répondus à partir d'un seul jeu de nombres plutôt que de deux jeux d'opinions.
L'éditeur vous montre le résultat : une gouttière de coût sur le graphe optimisé, donc ce que vous lisez est ce que vous payez.
#Voir aussi
- Compilateur et runtime — le gate sur lequel cette loi chevauche, et les routines épinglées.
- Modèle mémoire — le plan de vivacité, qui est lui-même une passe T0–T1.
- Concurrence — l'ordonnanceur, et pourquoi le parallélisme ne peut pas changer une valeur.
- compute — la fusion et les pushdowns, au niveau du dataframe.
- Garanties — ce que « vérifié à chaque compilation » signifie pour un lecteur qui doit y faire confiance.
Concurrence
Flux tourne sur de nombreux cœurs, et l'auteur n'écrit jamais un verrou, un await, ou un thread. Ce
n'est pas une API de confort qui cache les parties difficiles — c'est une conséquence du langage : un
graphe dataflow pur, total, typé peut être ordonnancé sur n'importe quel nombre de workers sans
changer un seul bit de sa sortie.
Cette page explique pourquoi c'est vrai, ce que l'ordonnanceur fait réellement, et le seul endroit où « il n'y a pas d'aliasing, donc il n'y a pas de course » aurait été un raccourci fatal.
#L'ordonnanceur n'est pas le compilateur
Le compilateur produit un graphe trié topologiquement. Un ordonnanceur séparé assigne ses nœuds aux workers. L'exécution monothread et multithread partagent le même graphe — l'une est le cas dégénéré de l'autre, pas un mode différent avec un chemin de code différent.
Cette séparation est ce qui rend le parallélisme auditable : la chose ordonnancée est exactement la chose que le gate a vérifiée.
#Trois classes de nœud
Le parallélisme n'est pas appliqué uniformément. La représentation intermédiaire classifie chaque nœud, et chaque classe a exactement un traitement légal :
| Classe | Ce qu'elle contient | Comment elle peut être parallélisée |
|---|---|---|
| stateless | arithmétique, comparaison, logique, select, une projection — tout ce qui ne lit que cette barre |
indépendant de toute autre barre, donc data-parallèle en principe. Jamais avec SIMD — une réduction SIMD horizontale réassocie la virgule flottante et change les bits. |
| stateful | un kernel, un delay, un croisement — tout ce qui porte des cellules d'état entre les barres |
c'est une série le long du temps. Parallèle seulement à travers un scan préfixe associatif, et seulement là où l'opération est véritablement associative. |
| reduction | Réservée. Aucune opération n'est dans cette classe aujourd'hui. | La jonction est tenue ouverte, inerte. Réservée pour la clause de l'arbre pairwise ci-dessous, qui se lie quand les opérations matricielles et d'algèbre linéaire arrivent. |
Deux de ces lignes ont besoin qu'on en explicite la lecture honnête, parce qu'une table aussi bien rangée invite à une lecture généreuse.
La classe reduction est vide, et c'est délibéré. Ce n'est pas « la classe où vont les sommes et
les moyennes » — un sum, un mean, un stdev porte des cellules d'état et est donc classé
stateful, comme tout autre kernel : chacun agrège en interne, le long du temps, à travers son
propre état. La classe reduction existe pour que la règle qui la gouverne — une réduction ne peut
être parallélisée qu'à travers un arbre pairwise gelé, jamais en réassociant son intérieur —
soit écrite et imposée avant la première opération qui en a besoin, plutôt qu'argumentée après
coup. Le classificateur est total : une opération qu'il ne reconnaît pas est classée stateful, ce
qui est la réponse conservatrice, parce que le mode de défaillance de deviner faux dans l'autre
direction est une course de données silencieuse.
Le data-parallélisme par chunks sur des sous-graphes stateless n'est pas dans v1. La classe le
permet ; l'implémentation ne le fait pas. Les vrais programmes entrelacent leurs opérations
stateless dans le cône stateful — un ema lit une différence qui lit un close — donc récolter les
parties stateless signifierait un second chemin d'émission à travers le compilateur pour un gain que
le modèle de coût ne soutient pas. C'est documenté comme design futur, et la section ci-dessous dit ce
que v1 fait à la place.
La règle sous les trois classes : l'ordre de réduction est préservé. Le réordonnancement parallèle en virgule flottante n'existe que sous le mode relâché opt-in, et il n'est jamais le défaut.
Pourquoi nous renonçons à l'accélération facile. Sommer une colonne avec quatre threads et combiner les partiels est la première chose que quiconque essaie, et cela produit un dernier bit différent. Ce bit est la différence entre un golden qui tient et un golden qui dérive, entre un serveur qui peut vérifier le travail d'un client et un qui ne peut que l'approximer. Donc le parallélisme est trouvé là où il ne change pas une valeur : à travers des nœuds indépendants, à travers des groupes indépendants, à travers des scripts indépendants — jamais à l'intérieur d'une seule réduction.
#Le substrat
Des Web Workers, un shared array buffer, et des atomics, derrière l'isolation cross-origin — le seul chemin qui fonctionne vraiment dans un navigateur. La doctrine par-dessus a deux niveaux, et la séparation est la totalité de l'argument de sûreté mémoire :
- Partagées, en lecture seule : les colonnes d'entrée. L'hôte écrit les colonnes de barres — time, open, high, low, close, volume — une fois, dans un seul shared array buffer. Chaque worker les lit de là. Rien n'est sérialisé à travers un message, et quelques mégaoctets d'historique coûtent une copie mesurée en fractions de milliseconde plutôt qu'un aller-retour de structured-clone.
- Possédé, en écriture : tout ce qu'une tâche produit. Chaque tâche exécute sa propre instance de module compilé, avec sa propre mémoire linéaire, possédée par le worker qui l'exécute. Les colonnes de résultat reviennent par transfert de buffer — zéro-copie, et l'émetteur perd le buffer en le donnant — jamais par partage.
Deux règles gardent cela honnête :
- Aucun atomic ne touche jamais aux données. Les atomics ne servent que l'ordonnanceur : le curseur de réclamation de tâche, les compteurs done, l'époque — dans un petit buffer dédié à eux.
- Rien d'inscriptible n'est partagé. Pas « partagé avec une discipline ». Pas partagé du tout. Ce qui est une propriété plus forte que celle que le contrat demande, et c'est la raison pour laquelle la règle des arènes par worker ci-dessous tient par construction plutôt que par revue.
Pourquoi les mémoires de module ne sont pas une seule mémoire partagée. Le design éventuel met un seul module de calcul partagé contre une seule mémoire linéaire partagée, chaque worker adressant sa propre région par offset. C'est un vrai design et ce n'est pas celui-ci. Aujourd'hui, une instance de module par tâche avec sa propre mémoire donne le même parallélisme, donne zéro partage inscriptible au lieu d'un partage discipliné par région, et ne demande rien au snapshot, au fenêtrage et à la machinerie de vérification qui sont déjà construits contre des mémoires par module. La réécriture en mémoire partagée achète un futur dont le runtime actuel n'a pas encore besoin, et elle rouvrirait trois sous-systèmes pour le faire.
#L'unité de travail est une composante, pas un nœud
Avant qu'un ordonnanceur puisse assigner quoi que ce soit, quelque chose doit décider ce qu'est une tâche. Se tromper là-dessus est la façon dont les runtimes parallèles finissent plus lents que les séquentiels, et l'arithmétique ici est brutale : un nœud dans cette représentation intermédiaire coûte de l'ordre d'une nanoseconde, tandis que tout transfert entre deux workers coûte de l'ordre d'une microseconde. Paralléliser par nœud, par barre, dépenserait mille unités de surcoût pour en économiser une. C'est la forme concrète de la règle « ne pas lancer un worker pour un petit nœud » que le modèle de coût partagé existe pour répondre.
Donc l'unité de travail v1 est une composante connexe du graphe, pondérée par son coût mesuré par barre fois le nombre de barres.
Cette définition gagne sa place sur le graphe fusionné — celui que l'optimiseur construit déjà à partir des scripts co-actifs (optimiseur). Fusionnez seize scripts et les composantes se re-séparent le long des vraies dépendances de données, pas le long des frontières de fichier :
- les scripts qui partagent une sous-expression sont collés en une seule composante par le nœud partagé — ce qui est exactement juste, parce que l'état partagé ne doit jamais être divisé entre deux workers ;
- les scripts qui ne partagent rien s'effondrent en composantes indépendantes et se répartissent sur les workers ;
- un sink lie ses dépendances dans sa composante : une tâche possède chaque colonne qu'elle produit.
À cinquante mille barres, une composante coûte des millisecondes — trois ordres de grandeur au-dessus du coût de dispatch, ce qui est ce qui rend tout l'exercice digne d'être fait.
#L'ordonnanceur
L'affectation est longest-processing-time-first. Trier les composantes par coût, décroissant, et donner chacune au worker le moins chargé. Les égalités se départagent de façon déterministe — par id de composante, puis par index de worker — donc le même graphe produit toujours la même affectation. Pour des tâches indépendantes sur des workers identiques, c'est à 4/3 du makespan optimal, ce qui est le bon point sur la courbe : un ordonnancement auquel personne n'a à penser, avec une borne à laquelle personne n'a à faire confiance.
La barrière est par niveau topologique. Le niveau k+1 démarre après une barrière complète sur le
niveau k ; au sein d'un niveau chaque nœud est indépendant, donc toute affectation est correcte.
En v1 batch le graphe de tâches n'a aucune arête inter-tâches du tout — les composantes sont
indépendantes par définition — donc il y a exactement un niveau, et le contrat de barrière tient
trivialement. L'ordonnanceur émet quand même son plan sous forme de niveaux, parce que c'est la forme
dont le futur a besoin : quand les arêtes inter-tâches arrivent avec les pipelines matriciels et de
scan préfixe, elles s'insèrent dans la même barrière sans refonte.
Post-v1. Work-stealing — une déque de Chase–Lev par worker plus un compteur atomique de degré entrant par nœud, un worker prenant un nœud dès que son degré entrant atteint zéro et volant à un voisin quand il tombe à sec — est conçu, et parqué sur preuve de bench. C'est une politique d'affectation interchangeable derrière la même interface de plan, et le même harnais de stress la revalide. Ce qu'elle n'est pas, c'est une question sémantique, ce qui est tout l'intérêt du paragraphe suivant.
Pourquoi une mise à niveau de ce genre est sûre, précisément. Parce que l'affectation est inobservable. Les valeurs sont indépendantes de l'ordonnancement (le graphe est pur), et les slots mémoire sont indépendants de l'ordonnancement eux aussi (le plan de vivacité est calculé depuis l'ordre canonique, pas depuis le runtime). Donc passer d'une barrière au work-stealing serait un pur changement de politique d'affectation avec zéro changement de valeur — une décision de latence, pas une décision de sémantique, ce qui est exactement ce que vous voulez qu'un ordonnanceur soit. C'est aussi pourquoi il peut être parqué sans réserve : rien d'autre dans le design ne l'attend, et aucune garantie n'est plus faible en son absence.
#Le piège : zéro aliasing ne suffit pas
Voici l'erreur que ce design a dû ne pas faire.
Le plan de vivacité laisse deux buffers partager un slot mémoire quand leurs durées de vie sont disjointes. Disjointes dans l'ordre canonique séquentiel — c'est ainsi que le plan le lit. Mais sous un ordonnanceur dynamique, les deux nœuds qui possèdent ces buffers peuvent s'exécuter au même moment sur deux workers différents. Une arène partagée leur donnerait la même adresse, et une course écriture-écriture s'ensuivrait — une qu'aucun oracle de valeur ne pourrait attraper, parce que la divergence est dans quel déchet vous lisez, pas dans l'arithmétique.
Deux règles la ferment :
- L'arène scratch est par worker. Privée, dimensionnée au pic du sous-graphe de ce worker. L'aliasing de slot inter-worker devient impossible par construction, pas seulement improbable.
- Chaque transfert producteur → consommateur porte une arête happens-before — un release-store du compteur de prêt, un acquire-load, ou un notify/wait — même dans le cas écrivain-unique, lecteur-unique. « Zéro aliasing implique pas de course » est nécessaire mais pas suffisant sous les modèles mémoire de JavaScript et WebAssembly : sans l'arête, il n'est pas garanti que le consommateur voie du tout l'écriture du producteur.
#1 ≡ N est prouvé, pas affirmé
L'affirmation « un thread et N threads produisent des octets identiques » n'est pas un espoir soutenu par des tests. Elle découle d'une liste de propriétés, chacune imposée ailleurs :
- le graphe est pur, donc les valeurs sont indépendantes de l'ordonnancement ;
- la partition ne divise jamais l'intérieur d'une réduction — ni, en v1, l'intérieur de quoi que ce soit d'autre : une tâche est une composante entière, et une composante n'est jamais coupée ;
- l'arbre de réduction pairwise est gelé, et le graphe de référence adopte le même arbre — donc parallèle et séquentiel concordent par construction plutôt que par accident. La clause est inerte aujourd'hui, et se lie à l'instant où la classe réservée a un membre ;
- l'ordonnancement est canonique sur la valeur d'une clé, jamais sur l'ordre dans lequel un worker s'est trouvé la voir ;
- le générateur aléatoire est basé sur un compteur et donc indépendant de la position ;
- les arènes sont par worker.
Et puis c'est testé quand même, parce qu'une preuve sur une implémentation est une preuve sur l'implémentation que vous croyez avoir. Le harnais de stress est livré en v1 : il exécute le même graphe sous un worker et sous plusieurs, avec une affectation aléatoire et adversariale, et affirme
- l'égalité en octets de la sortie, et
- zéro écriture de slot concurrente.
La graine d'affectation est journalisée, donc un échec adversarial se reproduit exactement.
Ce que le harnais chasse réellement. Pas l'arithmétique. La liste ci-dessus règle déjà l'arithmétique, et aucune dose de stress ne la renforcerait. Ce qui peut véritablement casser, c'est la plomberie, donc c'est ce qui est stressé : que chaque tâche soit réclamée exactement une fois et jamais deux ; que chaque colonne sink soit écrite exactement une fois ; qu'une instance de module ne serve jamais deux tâches au même moment, et qu'en réutiliser une à travers les ticks réinitialise son état ; qu'un buffer transféré ne soit jamais lu après avoir été détaché. Ce sont les bugs qu'un langage dataflow pur peut encore avoir, et ils sont invisibles pour un oracle de valeur — une réclamation dupliquée calcule le bon nombre, deux fois.
Et il tourne deux fois, sur deux substrats : d'abord en-process, contre des workers simulés et des ordres de complétion adversariaux ensemencés ; puis inchangé, sur de vrais threads. Cet ordre est un instrument de diagnostic. Un échec qui se reproduit en-process est un bug dans la logique — la partition, le démultiplexage, le protocole de réclamation. Un échec qui n'apparaît que sur de vrais threads est un bug dans le substrat — un transfert, un atomic, une mesure. Exécuter les mêmes assertions aux deux endroits est ce qui permet à un échec de dire lequel des deux il est, avant que quiconque ne commence à deviner.
#Le navigateur n'est pas acquis
Un shared array buffer requiert l'isolation cross-origin, et l'isolation cross-origin requiert deux en-têtes de réponse qu'une page n'a pas toujours le droit d'avoir. Donc la flotte n'est pas une fondation sur laquelle le reste du design repose — c'est une accélération qui peut ou non être disponible, et le design le dit à voix haute :
- En l'absence des en-têtes, N = 1, automatiquement. Pas de flotte, pas de buffer partagé, pas de mode dégradé à tester : le chemin ordinaire à module unique est le cas N = 1, donc un visiteur sans isolation cross-origin exécute le même graphe, obtient les mêmes octets, et ne paie qu'en latence. Il n'y a aucun second chemin de code à garder correct, ce qui est la seule raison pour laquelle on peut faire confiance à ce fallback.
- La flotte n'est utilisée que lorsqu'elle paie. Même avec les en-têtes, l'ordonnanceur ne prend le chemin flotte que lorsqu'il y a plus d'un cœur et que le coût mesuré du graphe fusionné franchit un seuil. En dessous, le chemin à module unique fusionné est plus rapide, et c'est lui qui s'exécute.
- Une tâche bloquée est un timeout, pas un blocage. Un worker qui ne revient jamais ne peut pas être interrompu, donc le batch porte une échéance dérivée de sa propre estimation de coût. À l'expiration la flotte est démolie et relancée, et le batch est rejoué. La pureté est ce qui rend cela sûr : un batch rejoué produit des résultats byte-identiques, donc une nouvelle tentative n'est pas une seconde réponse — c'est la même réponse, atteinte à nouveau. Toute erreur de flotte quelconque se rabat sur le chemin à module unique pour ce tick : bruyante dans les statistiques, invisible pour l'utilisateur.
#Budgets entre scripts
Un budget de nœuds par script ne suffit pas quand un chart porte plusieurs scripts. Deux bornes supplémentaires s'appliquent :
- un plafond sur les scripts co-actifs fusionnés en un seul graphe, et
- un budget de frame agrégé.
En dépassement de budget, la réponse est une politique de dégradation déterministe. Les tâches sont différées — jamais tuées ; la couche au-dessus les reprogramme — jusqu'à ce que le reste tienne, et l'ordre dans lequel elles sont différées est un ordre total fixé à l'avance : priorité ascendante d'abord (la priorité vient de l'hôte, et n'est jamais dérivée des données), puis coût descendant à priorité égale (différer la plus grande libère le plus), puis index ascendant. Une tâche qui à elle seule dépasse le budget est différée aussi — le budget est un contrat dur, pas une suggestion. Une tâche gratuite n'est jamais différée, parce que la différer ne libérerait rien.
Relisez les départages et remarquez à quoi ils servent. Chacun d'eux existe pour faire de la réponse à « quel script est abandonné » une fonction de la déclaration et jamais des nombres qui la traversent. Une politique de dégradation qui consulterait les données ferait dépendre du marché l'ensemble des scripts qui s'exécutent, et un chart dont la composition change avec les données est un chart sur lequel personne ne peut raisonner — ni reproduire. Un frame qui saute est une décision, prise à l'avance, en un seul endroit.
Le pool d'instances, et pourquoi l'éviction est ennuyeuse exprès. Les workers gardent un pool d'instances de module avec une affinité tâche-vers-worker stable, de sorte qu'une tâche qui s'exécute à chaque tick — une mise à jour live, un pas de rejeu — trouve son instance chaude plutôt que de la ré-instancier. L'empreinte du pool est comptabilisée exactement : la somme, par worker, de la mémoire de pic que chacun de ses modules planifie, que le modèle mémoire calcule déjà à la compilation (modèle mémoire). Et quand le pool doit évincer, il évince selon un ordre de priorité explicite — jamais selon ce que les données se sont trouvées toucher le plus récemment. Le déterminisme n'est pas une propriété qu'on peut avoir dans l'arithmétique et abandonner dans le cache.
#Ce que v1 livre, et ce qu'elle ne livre pas
Exécution multi-worker, dès le départ — construite, stress-testée, et livrée, avec le monothread comme cas dégénéré. Concrètement, c'est : la classification des nœuds, la partition en composantes, l'affectation longest-processing-time sur une barrière de niveau, les arènes par worker, les atomics confinés aux compteurs de l'ordonnanceur, le budget agrégé avec sa dégradation déterministe, et le harnais 1 ≡ N qui maintient tout cela en place.
Trois choses n'y sont délibérément pas, et aucune n'est porteuse :
| Pas dans v1 | Pourquoi, et ce qu'il faudrait |
|---|---|
| work-stealing | Post-v1. Une optimisation de latence sur une affectation déjà correcte ; parquée jusqu'à ce qu'un cas de bench montre que la barrière de niveau est ce qui coûte le frame. |
| data-parallélisme par chunks sur des sous-graphes stateless | La classe le permet, l'émetteur devrait faire pousser un second chemin pour ça, et le modèle de coût ne justifie pas actuellement le compromis. Design futur. |
| l'arbre de réduction pairwise | Réservé. La règle est écrite ; aucune opération n'est dans la classe qu'elle gouverne. Il se lie quand les opérations matricielles et d'algèbre linéaire arrivent. |
Le seul report honnête dans ce domaine qui ne concerne pas l'ordonnancement : les transports réseau qui ont besoin d'une socket brute, que le navigateur ne peut pas ouvrir du tout.
#Voir aussi
- Modèle mémoire — le plan de vivacité, et pourquoi l'arène doit être par worker.
- Optimiseur — le modèle de coût partagé, et l'ordre de réduction gelé.
- Compilateur et runtime — le gate, et le harnais auquel cette suite de stress appartient.
- compute — là où le parallélisme paie vraiment : groupes indépendants, cellules indépendantes.
- Garanties — 1 ≡ N énoncé pour le lecteur qui doit s'y fier.
Intégration hôte — descripteurs, registres et jonctions d'extension
Flux ne dessine rien. Il exprime du contenu — indicateurs, transformations de représentation, géométrie de dessin, scènes, valeurs de profondeur — et l'hôte applique des modes d'exécution : une projection 2-D ou 3-D, le type de chart que l'utilisateur a choisi, la disposition des volets, la persistance. Le langage produit les artefacts ; les modes les consomment.
Cette séparation est toute l'architecture, et elle a une conséquence pratique tranchante : un script et un natif doivent être indiscernables pour l'hôte. Si l'implémentation Point & Figure d'un utilisateur s'enregistre dans la même table, dans la même forme, avec les mêmes hooks que le moteur de rendu de bougies natif, alors l'extensibilité n'est pas une fonctionnalité boulonnée sur le côté — c'est la même route que le code first-party emprunte déjà.
Cette page spécifie les contrats à cette frontière : ce qu'est un descripteur, ce que les registres promettent, quels deux trous dans l'hôte doivent se fermer pour que les représentations soient scriptables du tout, et quelles jonctions sont délibérément tenues ouvertes pour ce qui vient ensuite.
#La frontière de portée
À l'intérieur du langage — du contenu, consommé par un registre ou un descripteur :
indicateurs · représentations (types de chart) · dessins créés et outils de dessin personnalisés ·
scènes canvas et overlays · transitions · alertes · valeurs de profondeur et 3-D · volets,
déclarativement (inférés des kinds — il n'y a pas de createPane()) · UI de paramètres (dérivée de
input).
En dehors du cœur — muter l'état applicatif : activer un autre script, persister, reconfigurer
l'application. C'est le travail de l'hôte. L'interactivité d'un script reste cosmétique
(on click -> spawn/tween/flash), bornée, et sans repaint ; basculer la visibilité de sa propre
sortie est autorisé.
Une couche de commande — des boutons qui activent des scripts, changent la disposition — existe, mais comme un plan déclaratif séparé (le plan APP), jamais sur le plan d'analyse. C'est ce qui préserve la totalité, le firewall, et le no-repaint quelle que soit la richesse que l'application environnante atteint.
#Quatre verrous
Quatre décisions sont coûteuses à rétro-adapter et sont donc gelées d'emblée :
- L'axe x est un index ordinal plus une correspondance temporelle — jamais « le temps ». La
position est
dataX(i); le timestamp n'entre jamais dans le calcul de x. depth/z est une coordonnée de première classe, pas une fonctionnalité 3-D.- La séparation de plans et les descripteurs sont des cibles de compilation, pas des conventions.
- Les registres acceptent les entrées enregistrées par script dans la même forme que les natifs.
#Les cinq descripteurs
#① Horloge / ordinal
Une horloge est un producteur de série : un index ordinal, une longueur, le magasin de barres, et
deux correspondances — timeAt(i) (index → temps, la source du flux time) et idxAt(sec)
(temps → index, arrondi au plus proche et borné, utilisé pour ancrer les dessins).
Constructeurs : tf(token) est l'agrégation grossière en temps ; renko(box), pnf(box, rev) et
range(r) sont des re-répartiteurs de prix — le même slot, avec un seuil de prix au lieu d'un
seuil de temps. @ route vers l'un de trois chemins : même pas (un no-op natif), plus grossier (un
remap), plus fin (un échantillon à la clôture).
Sept invariants de codegen gouvernent tout ce qui compile à travers ce contrat :
| Invariant | |
|---|---|
| I1 | la position est l'index — jamais le timestamp |
| I2 | idxAt n'est qu'une graine : le localisateur de rééchantillonnage est un pointeur contenant-plancher (Tₖ ≤ t < Tₖ₊₁), jamais idxAt(t) − 1. L'arrondi au plus proche est du look-ahead, et le look-ahead est du repaint. |
| I3 | causal : une unité close seulement, jamais une encore en formation ⇒ le repaint est inexprimable |
| I4 | la grille est réelle (timeAt), jamais supposée uniforme |
| I5 | une horloge par série en v1 — une horloge d'une horloge n'est pas exprimable |
| I6 | un nœud feuille mappé à un kernel natif lui est byte-identique, warm-up compris : un indicateur Flux sur une horloge est le même citoyen qu'un natif |
| I7 | l'interpréteur et le WASM compilé produisent les mêmes octets, vérifié à chaque compilation |
I2 mérite sa propre phrase, parce que c'est celui qu'un implémenteur se trompe : ancrer un dessin veut la barre la plus proche ; rééchantillonner un indicateur veut la dernière close. Utiliser la correspondance d'ancrage pour le rééchantillonnage lit silencieusement le futur.
#② Profondeur / z
Le firewall ici n'est pas un argument — c'est une propriété du code. Le collecteur d'overlays ne prend
aucun paramètre 3-D ; seul l'hôte connaît l'angle de caméra, à travers un facteur de profondeur
dans [0,1] appliqué en aval par le shader. Donc un angle de zéro est pixel-identique au 2-D
simple, par construction plutôt que par soin.
Flux émet un nœud de profondeur (une série d'analyse ordinaire) et le binding at z: ; l'hôte le
projette et possède la fenêtre, le slider, la caméra et l'effondrement. Chaque instance d'overlay
pivote en z — ligne, bande, nuage, profil — donc 2-D et 3-D consomment le même artefact.
at z: accepte n'importe quel scalaire et le normalise automatiquement selon le kind de la source ;
une valeur depth, déjà une fraction normalisée, court-circuite la normalisation. Statut honnête :
aucun kernel du catalogue ne produit depth aujourd'hui, donc le kind est théorique en v1 tandis
que l'espace z est réel — il est gelé maintenant parce que rétro-adapter une coordonnée est coûteux.
#③ Descripteur de représentation
Un type de chart est un id, une classe, six hooks, et un membre de métadonnées :
RepresentationDescriptor = {
id, klass: 'A1' | 'A2' | 'B',
transform(raw, params) -> Series // A1 = identity ; A2 = a same-length derived store ; B = re-binned COLUMNS
reduce(bars, …) -> aggregate // the LOD decimator — breach #1
renderPrimitive(frame) -> elements // authored as `render`
updateLastUnit(el, …) -> bool // in-place mutation of the head unit
liveReduce(state, tick) -> extend | append // A = in place ; B = extend a column, or reverse → append
persistKey(unit) -> key // the non-lossy anchor — breach #2
capabilities { … } // METADATA — the 7th member, not a hook
}
klass et capabilities sont dérivés, jamais créés à la main. La grammaire admet l'id et les six
hooks ; le compilateur déduit le reste — exactement comme le volet et l'échelle sont déduits d'un kind :
| Ce que font les hooks | klass |
seriesKind |
persistance |
|---|---|---|---|
| la transform / horloge re-répartit le prix (un x ordinal, une correspondance temporelle non injective) | B |
suit la primitive de rendu (column, ou line pour une polyligne) |
son propre slot |
| la transform dérive un magasin de même longueur (une ligne, un Heikin-Ashi) | A2 |
ohlc |
partagé |
| la transform est l'identité (une bougie brute) | A1 |
ohlc |
partagé |
La distinction vaut d'être énoncée précisément parce qu'il est facile de l'inverser : la classe suit la RE-RÉPARTITION, pas la primitive de rendu. Un Kagi dessine une polyligne et un Renko dessine une brique, pourtant les deux sont de classe B — parce que les deux re-répartissent le prix. C'est ce qui les route vers le décimateur correct-en-colonnes et vers leur propre slot de persistance, plutôt que vers le chemin d'agrégation verbatim qu'une bougie utilise.
#④ Registres ouverts aux scripts
Les trois registres — indicateurs, dessins, représentations — sont déjà agnostiques au type. Rien en eux ne teste un flag « est-ce un script ? ». Une entrée écrite en Flux est indiscernable d'un natif dès qu'elle a la même forme :
| Registre | Forme d'entrée |
|---|---|
| indicateurs | { id, label, category, mode, defaults, params, series, compute } + un descripteur récursif/fenêtré/batch |
| dessins | { barExtent, priceExtent, render, hitTest, + LOD } — hitTest et le LOD sont dérivés par l'hôte de la géométrie de rendu, jamais créés à la main |
| représentations | le descripteur ci-dessus |
Ce qui doit être construit est un mécanisme d'enregistrement dynamique — les tables sont des littéraux statiques gelés au boot. La forme recommandée est une seconde table consultée après celle des natifs, de sorte que le chemin chaud natif n'est pas touché du tout. C'est une ouverture, pas un nouveau substrat.
#⑤ Descripteur de transition
Cosmétique, et délibérément hors des registres. Aujourd'hui le morph est piloté par un objet de plan
ad-hoc ; le contrat le réifie en un type nommé — durée, easing, vague, étalement du stagger, avance de
la mèche, politique de surplus, fondu du chrome, échéance de maintien, timing du flip — plus des
overrides par appel (over D, stagger, surplus:) et un hook morph: par représentation.
Le morph lourd par bougie reste natif. Flux l'orchestre : il injecte le plan une fois.
#Comment Flux compile vers ces contrats
Rien de nouveau n'est introduit sous le langage. Chaque construction atterrit sur une jonction qui existe déjà :
| Construction | Compile vers |
|---|---|
clock + @ |
un producteur de série ; le nœud @ route no-op / remap / échantillon, l'expression elle-même calculée par le moteur natif |
depth, at z: |
un nœud d'analyse exposé comme clé de série, consommé par la z-source et le projecteur |
representation |
une entrée dans la table de séries de rendu ; morph remplit le plan de transition |
| un indicateur | une entrée de registre (label, params, séries inférées des input et des kinds) plus un descripteur récursif/fenêtré/batch, accepté sans flag — et donc servi exactement comme un natif, octet pour octet |
Le chemin chaud — les kernels stepper, l'agrégation en colonnes, le moteur de rendu de bougies, le packing de profondeur, le morph — reste natif et byte-identique. Flux génère les artefacts que les jonctions consomment déjà.
#Les deux brèches
Deux trous dans l'hôte doivent se fermer avant que toute représentation dirigée par le prix — script ou native — puisse fonctionner. Ils ont été identifiés et chiffrés indépendamment de Flux ; Flux ne fait que chevaucher dessus.
Brèche n°1 — le hook reduce par type (niveau de détail). Le chart décime les barres pour le
niveau de zoom en les fusionnant, à l'aveugle, à travers un seul agrégateur en colonnes. Pour une série
OHLC c'est correct. Pour une série de colonnes re-répartie c'est faux : fusionner par min/max
effondre une alternance de colonnes montantes et descendantes en un seul corps épais avec une direction
fausse, hors de la grille. Le correctif est petit et byte-sûr : le magasin de barres gagne un kind ;
l'appel d'agrégation y est conditionné ; ohlc garde l'agrégateur existant verbatim (zéro pixel ne
change), tandis que column route vers le décimateur propre du descripteur — même signature, même
retour, un drop-in.
Brèche n°2 — persistance scopée par type. La clé des dessins est (asset, timeframe) sans
discriminant, et l'ancre est un timestamp brut. Les deux cassent pour une représentation re-répartie :
plusieurs colonnes peuvent partager le temps d'une barre (la correspondance temporelle est non
injective, donc un dessin atterrit sur la mauvaise colonne), et une seule clé mélange les dessins de
deux types de chart différents. Le correctif ajoute un discriminant de représentation à la clé et route
l'ancrage à travers le hook persistKey — des timestamps pour la classe A (inchangé), une ancre
stable-en-représentation (prix plus un ordinal de box) pour la classe B.
#Le test décisif : Point & Figure comme script
La question qui règle si l'architecture fonctionne est simple : un type de chart entièrement dirigé par le prix peut-il être écrit comme un script de bibliothèque, sans changement au cœur ? Point & Figure est le cas le plus difficile, donc c'est celui à répondre.
fluxrepresentation pnf(box, rev) {
transform: rebin(close, box, rev) // price → X/O columns: a price clock
render: column{ at: (clock.index, lo..hi), glyph: if dir == 1 then X else O }
reduce: columnDecimate(bars, k) // the column-correct decimator (breach #1)
liveReduce: extendOrAppend(state, tick) // extend the head column, or reverse → append
updateLastUnit: mutateHead(el, unit) // mutate the head column in place
persistKey: (lo, clock.index) // a price + box-ordinal anchor (breach #2)
}
Chaque hook prend une valeur — une expression, un bloc, ou une primitive de rendu. (Les corps ci-dessus sont nommés pour la lisibilité ; une vraie implémentation les inline.)
Chaque pièce type, et le système de types force la physique — la box doit être un level, un
déplacement, parce que anchor + count * box ne type-check que de cette façon (voir
Kinds). L'état de colonne est un scan borné ordinaire :
flux// the column state: a record whose kind is record{ dir: dir, extreme: price, count: num }
def column(box, rev) =
scan({ dir: 1, extreme: close, count: 0 }, (p) -> advance(p, box, rev))
count est sans dimension, donc count * box est un level et extreme + count * box est un
price. La causalité tient : la colonne avance sur le prix clos, et une colonne passée est gelée.
| Pièce | Contrat | Statut |
|---|---|---|
pnf(box, rev) comme horloge |
① | le re-répartiteur doit être construit (il dépend de la brèche n°1) |
transform / render / updateLastUnit |
③ | les formes existent déjà dans l'hôte |
klass: 'B', seriesKind: 'column', persistance en slot propre |
③ | dérivés des hooks — la déduction doit être construite |
reduce |
③ + brèche n°1 | le gate doit être construit |
liveReduce + une garde de longueur |
③ | à construire |
persistKey |
③ + brèche n°2 | à construire |
| l'état de colonne | le treillis | un script pur — le kind record rend le scan typable |
depth: optionnel (z proportionnel au volume de colonne) |
② | hérité gratuitement — la projection est générique |
| l'entrée de registre | ④ | l'enregistrement dynamique doit être construit |
La tension honnête. Une seule barre de temps peut traverser de nombreuses boxes dans un mouvement
éclair, donc le coût par barre n'est pas trivialement constant. C'est plafonné — maxBricksPerBar — et
au-delà du plafond l'hôte agrège plutôt que de faire exploser le budget. Le plafond est une constante
de design, pas quelque chose que le treillis peut dériver.
Point & Figure est donc entièrement exprimable comme un script de bibliothèque, avec les deux brèches comme seules modifications du cœur. Renko, Kagi, three-line-break et Range suivent a fortiori — ce sont des instances strictement plus simples de la même classe.
#Inter-séries et multi-actifs
Le chart est multi-actifs et multi-devises, donc le langage exprime le travail inter-séries dès le départ, sans nouvelle grammaire :
fluxbtc = series("BTC-USD")
eth = series("ETH-USD")
spread = btc.close / eth.close // ratio — plottable
corr = stat.correl(returns(btc.close), returns(eth.close), 30) // osc(-1,1)
- Référencement —
series(key)renvoie un record dont les colonnes portent le tag d'actif dérivé des métadonnées de l'hôte :open/high/low/close : price[base, quote],volume : volume[base]. La clé est une clé de résolution allowlistée par l'hôte, distincte du tag qu'elle produit. - L'alignement est as-of, et causal — la série étrangère est alignée sur l'axe ordinal du chart en
prenant la barre étrangère la plus récente dont le timestamp est ≤ au temps de la barre courante
(la même règle contenant-plancher que I2). Aucune barre future n'est jamais visible ; un trou
maintient la dernière valeur connue ; avant la première barre étrangère la valeur est
na. Le no-repaint est hérité, pas ré-argumenté. - Les kinds le rendent sûr —
price[BTC,USD] + price[ETH,USD]est[ErrDim], etprice[BTC,USD] + price[BTC,EUR]aussi. Un dollar n'est pas un euro (actif & devise). - Compilation — le graphe déclare ses dépendances d'actifs ; l'hôte les récupère et les aligne et les remet au moteur comme colonnes d'entrée additionnelles. Plus d'entrées ; pas de nouveau substrat.
#Jonctions d'extension réservées
Chaque capacité future entre par l'une de deux portes, ce qui est ce qui garde le cœur d'analyse intouché :
- Entrée — chaque signal du monde (une touche, un pointeur, un tick, un prix, un pick) devient soit un flux lisible soit un événement, à travers un unique point d'ingestion. Le rejeu et le déterminisme restent uniformes parce que tout est journalisé.
- Sortie — chaque rendu ou calcul lourd est exécuté par l'hôte sous une capacité, du côté présentation, contenu par le firewall. Le non-déterminisme du GPU et de l'horloge murale ne se propage jamais dans le cœur.
Les jonctions tenues ouvertes, avec leur statut honnête :
| Jonction | Statut |
|---|---|
Entrée de première classe (input.key, input.pointer, événements de bord, un modèle de focus/possession) |
v1 couvre le pointeur et le tactile ; clavier, pointer-lock et gamepad se branchent sans réécriture |
Une scène retenue avec des cibles enfichables — un moteur de rendu pour 2-D, chart et 3-D ; un espace world3D ; des primitives 3-D déclaratives validées |
Réservé. v1 livre le 2-D et le 3-D du chart ; une scène 3-D générale est post-v1 |
| Parallélisme | Réalisé en v1 — l'ordonnanceur exécute le graphe pur ; il n'ajoute aucune puissance au cœur |
Actifs et kernels par handle (asset:load, une échappatoire de kernel validé) |
conçu |
L'espace de noms de capacités (input:*, gpu:*, net:*, data:source, app:launch, wallet:*, social:* …) |
extensible par le même mécanisme |
Données externes par consentement (net:fetch), typées par un schéma déclaré |
v1, côté client ; un proxy serveur est Post-v1. |
Une source d'actif tierce (data:source) — enregistrer un producteur de série |
Post-v1., vérifiée par le vendeur ; l'ingestion est causale et append-only, donc le no-repaint survit |
Visibilité de module (private / package / pub) |
v1 |
| Une API de chart embarquable qui accepte des scripts Flux comme arguments, sandboxée | v1 |
Chaîne et wallet (wallet:* / chain:*) — le script construit une intention, le wallet signe |
v1 pour connect/read/simulate/send ; les appels de contrat sont Post-v1. |
Identité et social (social:* / present:*) — handles résolus par l'hôte, pairwise-opaques |
v1 pour les contacts, l'invitation et le canal de données ; palier vérifié par le vendeur, jamais anonyme — le même palier que data:source et chain:send. Les médias A/V sont conditionnés au consentement de capture différé |
Le principe derrière la table : aucune jonction n'ajoute jamais de puissance au cœur. Elle ajoute un flux d'entrée, ou une cible de sortie médiée par une capacité. Les jeux, les tableurs live, une scène 3-D — tous sont des cas particuliers de ces deux portes.
#Voir aussi
- Kinds — pourquoi
boxdoit être unlevel, et comment le treillis force la physique. - Temps et état — les horloges,
@, et la règle contenant-plancher (I2). - Plan app — contributions, slots, et la frontière de disposition.
- display — scènes, volets, la chaîne de draw-list, le modèle 3-D.
- Compilateur et runtime — I6 et I7, et contre quoi « byte-identique » est vérifié.
- Packages — comment une représentation tierce est distribuée et épinglée.
Packages et distribution
Une bibliothèque Flux est distribuée comme un artefact compilé, sandboxé, avec un manifeste scellé, épinglé par le hash de son contenu. Pas par une plage de versions. Pas par un nom qu'un registre résout au moment de l'installation. Par le hash.
Cette seule décision se propage dans tout ce qui est sur cette page : comment le diamant de dépendances est dissous plutôt que résolu, pourquoi le build est reproductible, pourquoi une bibliothèque achetée ne peut pas faire passer clandestinement une capacité dans votre app, et pourquoi un script que vous avez livré l'an dernier tourne encore, octet pour octet, aujourd'hui.
#Ce qu'est un package
Deux notions sont faciles à confondre, donc elles sont nommées séparément :
| un registre (indicateurs, représentations, outils de dessin) | un point d'extension au runtime — un script s'enregistre sous un id, et l'hôte le sert comme un natif |
| un package | un artefact de dépendance versionné, épinglé par hash, avec un manifeste de capacités agrégé — quelque chose que vous importez |
Un package est nommé par une coordonnée lisible — author/package — et importé :
fluximport author/indicators as ind
plot ind.superSmoother(close, 20) // its `pub` entries; everything else stays private
Seules les entrées marquées pub traversent une frontière d'import. Les visibilités private et
package restent une encapsulation intra-script, et sont orthogonales à la frontière de package.
#L'adressage par contenu dissout le diamant
La coordonnée author/package est une indirection lisible. Ce qui est réellement lié est un
hash de contenu.
Donc deux versions de la même bibliothèque sont deux hashs distincts qui coexistent, sans conflit
de nom. Le diamant classique — A dépend de C@x, B dépend de C@y, votre app tire à la fois A
et B — n'est pas résolu. Il ne survient pas :
Et la discipline de types monomorphes rend cela sûr plutôt que simplement possible : un record exporté
par C@x et un exporté par C@y sont deux types monomorphes distincts. La jonction entre A et
B ne peut jamais passer l'un là où l'autre est attendu — c'est [ErrField], à la compilation, par
inférence. Pas un avertissement. Pas une convention.
Le coût en taille de la coexistence est récupéré par l'élimination des sous-expressions communes à travers le graphe : deux versions qui partagent un sous-graphe identique le partagent au niveau du nœud, quels que soient leurs noms.
#Sélectionner une version, quand un humain est dans la boucle
La grammaire d'un import est exactement import author/package [as alias]. Il n'y a aucune
contrainte de version dans la source, et ce n'est pas un oubli — une source qui porterait une plage
serait une source dont le sens dépendrait de ce qu'un registre a répondu ce jour-là.
Post-v1. Une couche de version lisible au-dessus de la coordonnée — l'endroit où un humain énonce « au moins 1.2 » et un outil transforme cela en un hash — est conçue, et c'est un overlay optionnel sur la couche de nommage, jamais une production du langage. Là où elle s'applique, la résolution est la sélection de version minimale : prendre la version la plus basse satisfaisant chaque contrainte, puis épingler son hash de contenu, et écrire le hash dans le lock.
Pourquoi la plus basse, et pourquoi pas un solveur. La sélection de version minimale est déterministe par construction — pas de solveur, pas de recherche, pas de « la résolution a changé parce que le registre a changé ». Le build devient une fonction pure de l'ensemble de contraintes. L'alternative — « la version compatible la plus récente flotte sous vos pieds » — casserait la byte-identité et le rejeu côté serveur, parce que deux builds de la même source lieraient du code différent.
La version lisible vit sur la couche de nommage, et c'est un confort pour l'humain qui choisit. Sous l'artefact, l'exactitude est le hash — et le hash est ce que la source, le lock et le serveur parlent tous.
#Le lockfile est le hash de build
Une application résout son graphe une fois, en un ensemble de hashs de contenu — la clôture transitive — plus la version épinglée du compilateur et les routines épinglées. Cet ensemble est le hash de build.
Ce qui est ce qui rend cette phrase définissable, et vérifiable : le même graphe de dépendances produit les mêmes octets. La byte-identité entre les deux moteurs et le rejeu côté serveur re-lient tous deux la clôture exacte que le lock a épinglée — jamais une « version compatible » choisie au moment du link, ce qui désynchroniserait client et serveur.
Le manifeste est là où cette clôture devient inspectable. Quatre de ses champs sont des entrées du hash de build, ce qui est une autre façon de dire que changer l'un d'eux produit un artefact différent avec un nom différent :
| Épinglé dans le manifeste | Pourquoi c'est partie de l'identité |
|---|---|
| le hash de module | les octets scellés — ce qu'un serveur ré-exécute, et ce qu'un vérificateur recalcule |
| la toolchain — version du compilateur, et le back-end optimiseur épinglé | la même source à travers un compilateur différent, ce sont des octets différents. L'ignorer servirait un module en cache qu'aucune recompilation ne produirait plus jamais |
| les routines épinglées — le hash de la bibliothèque mathématique déterministe elle-même | le module a été passé au gate contre cette bibliothèque. Un consommateur qui en détient une différente exécute du code que personne n'a vérifié |
| la mémoire déclarée — pages, cellules d'état, capacité | vérifiée contre le module avant de l'instancier, donc une empreinte est un contrat plutôt qu'une surprise |
Pourquoi la bibliothèque mathématique est dans le hash et pas simplement « recommandée ». C'est la plus subtile des quatre, et celle qu'un système de packaging normal aurait manquée. Les octets d'un pack sont prouvés byte-identiques à l'évaluation de l'interpréteur contre une implémentation spécifique des fonctions transcendantes. Liez le même module contre une différente — un bug corrigé, un arrondi resserré, une véritable amélioration — et la preuve ne le couvre plus. Donc un consommateur dont la bibliothèque mathématique ne correspond pas à celle du manifeste refuse d'exécuter le pack, et le re-récupère. Pas un avertissement, pas un shim de compatibilité : un refus. La dérive que cela ferme est exactement la dérive que personne ne remarquerait, parce que les nombres auraient toujours l'air justes.
#Liaison
Une dépendance achetée ne peut pas être inlinée à la compilation : sa source n'est jamais livrée —
vous n'inlinez pas ce que vous n'êtes pas autorisé à recevoir. Donc une bibliothèque tierce est un
module WebAssembly séparé, signé, lié par des imports de module. Et c'est la règle pour chaque
dépendance, pas seulement l'achetée : une dépendance open livre sa source, mais elle est quand même
liée comme son propre module, de sorte que sa provenance, son palier de confiance et son hash restent
les siens plutôt que de se dissoudre dans les vôtres. Deux choses viennent avec ça :
- Une ABI typée. Les entrées
pubet leurs kinds monomorphes sont le contrat. Sa version vit dans une fenêtre d'interface épinglée par hash — le même idiome qu'un codec utilise pour sa plage de schémas acceptée. - Provenance par module. Chaque dépendance porte son propre palier de confiance et son propre hash.
Chaque app épingle le hash exact de chaque dépendance, donc une « mise à jour » produit un nouveau hash d'app — jamais une dérive silencieuse sous une app gelée. Les modules tiers partagés mutables sont interdits, parce qu'ils casseraient la byte-identité et le rejeu à la racine.
#Les capacités s'agrègent — et ne peuvent pas escalader
C'est la propriété de sécurité qui rend une marketplace tolérable :
manifest(A) = ( ⋃ emit Cap over the transitive closure of A ) ⊓ the user's grant
Trois conséquences, toutes normatives :
- L'appétit d'une dépendance transitive est visible. Si une bibliothèque trois niveaux plus bas veut le réseau, cette demande fait surface dans le manifeste de votre app, et la personne qui installe votre app la voit avant d'installer. Il n'y a aucune capacité cachée, et l'attaque du député confus est fermée à la racine.
- Aucune dépendance ne peut dépasser ce que l'utilisateur a accordé. L'autorité ne circule que le long des arêtes d'import, plafonnée par l'autorisation accordée.
- Une dépendance ne détient aucun objet de capacité du tout — donc elle ne peut ni en re-déléguer un ni en amplifier un.
La non-escalade est structurelle : elle est recalculée à la compilation et épinglée dans le hash de l'app.
#L'artefact
Un package distribué est un fluxpack : une archive contenant le module compilé, le manifeste scellé, les métadonnées compilées dont un consommateur a besoin pour attacher le module sans compilateur — et, quand l'auteur le distribue ouvertement, la source à partir de laquelle il a été compilé.
| Entrée | Ce que c'est |
|---|---|
| le manifeste | JSON canonique — la liste de capacités scellée, la provenance, le schéma de paramètres, la présentation déclarée |
| le module compilé | le WebAssembly que le consommateur exécute réellement |
| les métadonnées compilées | disposition des sinks, offsets de colonnes, noms de séries — pour qu'un consommateur attache le module sans rien inférer |
| la source | optionnelle, et la seule chose transparente dans l'archive. Présente ⇒ le pack est vérifiable |
| documentation, une icône, une signature | optionnelles ; l'icône est durement assainie au chargement, parce qu'un pack est une entrée non fiable |
La représentation intermédiaire compilée n'est jamais livrée, dans aucune classe de pack. Un vérificateur qui veut contrôler le module ne lit pas une IR fournie par l'auteur — il re-dérive l'IR de la source et recompile. Livrer une IR signifierait lui faire confiance.
#Trois classes de distribution, et open est le défaut
Que la source voyage ou non est une propriété déclarée du pack, et c'est le premier champ qu'un consommateur lit :
| Classe | La source | Ce que le consommateur peut faire |
|---|---|---|
open — le défaut |
livrée, dans l'archive | la recompiler localement et contrôler le module contre elle, octet pour octet. L'identité est vérifiable, pas promise |
closed |
non livrée | l'exécuter dans le sandbox, et inspecter le manifeste scellé — mais jamais re-dériver le module |
licensed |
non livrée, et le module est chiffré et gardé par clé | la même chose, sous une licence que l'hôte impose |
Ce défaut est porteur, et c'est l'opposé de l'habituel. L'honnêteté d'un package sur ce qu'il calcule
est vérifiable sauf si son auteur se désengage — et le désengagement est visible dans le manifeste,
avant l'installation, à côté de la liste de capacités. Un consommateur à qui on remet un pack closed
sait exactement ce à quoi il a renoncé.
Pourquoi un pack fermé est quand même sûr à exécuter. La vérifiabilité et la sûreté sont deux propriétés différentes, et il vaut la peine de refuser de les confondre. Le sandbox est la sûreté : un pack est une fonction pure sur des nombres, sans horloge, sans réseau, sans I/O et sans moyen de faire croître sa propre mémoire. Le pire qu'un pack malveillant puisse faire est de calculer des mauvais nombres — un mauvais signal, que le modèle de capacités et l'assainisseur contiennent, et qu'aucune dose de lecture de source n'aurait attrapé non plus. La vérifiabilité est une garantie différente : pas « ceci ne peut pas me nuire » mais « ceci est ce qu'il dit être ».
openvous donne les deux.closedvous donne la première, et le dit.
#L'archive est déterministe, et le hash est le nom
Un package est adressé par contenu par le hash de ses octets d'archive, donc l'archive elle-même doit être reproductible ou le nom n'est pas stable. Un zip ordinaire ne l'est pas : ordre des entrées, timestamps, bits de permission et compression varient tous. Celui-ci est contraint jusqu'à ce qu'il le soit :
- les entrées sont stockées, jamais compressées — la sortie de deflate diffère selon les builds de bibliothèque, et une archive compressée ferait dépendre le hash du pack de quel zlib l'auteur a lié ;
- l'ordre des entrées est le chemin canonique trié ; les timestamps sont une époque fixe ; les permissions sont fixes ; il n'y a aucun champ supplémentaire et aucun commentaire.
Stocker plutôt que compresser ne coûte presque rien — la transmission est de toute façon compressée par le transport, et un pack fait des kilooctets — et cela ferme la surface de bombe de décompression par construction plutôt qu'avec une limite que quelqu'un doit régler correctement.
Un pack est une entrée non fiable. Il est vérifié avant de s'exécuter : la structure, le manifeste, la provenance, les limites déclarées — et l'empreinte mémoire déclarée est vérifiée contre le module avant l'instanciation, donc l'empreinte est un contrat plutôt qu'une surprise. Et le gate de recompilation ferme le dernier trou — la même source et le même lock, recompilés deux fois sur des machines différentes avec des nombres de threads différents, doivent produire un module byte-identique. Un build non reproductible casserait le rejeu côté serveur silencieusement, parce qu'un oracle au niveau de la valeur ne peut pas voir les octets émis à travers deux compilations.
La compatibilité de licence est calculée sur la clôture à la publication et peut refuser une publication (un artefact fermé payant construit sur une dépendance copyleft, par exemple) — faite surface dans le même panneau inspecter-avant-installer que le manifeste.
Le runtime de calcul est retenu pour toujours, append-only. Le module partagé de kernels natifs est lié par hash comme toute autre dépendance, donc faire évoluer un kernel produit un nouveau hash et ne re-lie qu'à la republication — jamais une dérive sous une app gelée. Une app achetée il y a des années épingle son runtime et reste vérifiable ; un build dont le runtime atteint la fin de vie est marqué scoré localement, jamais silencieusement invalidé.
#Ce qui est délibérément exclu
| Exclu | Pourquoi |
|---|---|
| installations dupliquées imbriquées de la même bibliothèque | cela combat le déterminisme en octets ; l'adressage par contenu le remplace |
| résolution de contraintes | pas reproductible au bit — sélection de version minimale ou un hash à la place |
| chargement dynamique de source non fiable | eval et consorts sont interdits ; chaque « load » est une instanciation médiée par l'hôte d'un module pré-validé |
| génériques à travers les frontières de module | les exports sont monomorphes en v1 — ce qui est exactement ce qui rend le diamant sûr |
| feature flags / compilation conditionnelle à travers les modules | ils changeraient les octets ; seul le choix de dépendance peut le faire |
Post-v1. Le registre public est un déploiement, pas un mécanisme : le packaging, l'épinglage, l'agrégation et la vérification sont tous construits. Ce qui est différé, c'est déployer l'endroit où des inconnus publient pour des inconnus.
#Voir aussi
- Compilateur et runtime — le gate de recompilation, et les routines épinglées qu'un lock inclut.
- Plan app — les capacités, le manifeste scellé, les deux paliers de confiance.
- Aperçu FDK — le prélude, et où les modules s'insèrent.
- Garanties — ce que la reproductibilité promet réellement.
Cookbook
Des recettes qui fonctionnent, classées de la première ligne que vous écrirez à la dernière. Chacune est complète — collez-la et elle s'exécute.
Une convention utilisée partout : une ligne marquée // ✗ est un exemple rejeté. Elle est là
parce que savoir ce que le langage refuse, et pourquoi, apprend plus qu'un énième exemple qui
marche.
#Analytique
#Un indicateur, et tout ce qu'il infère
fluxplot rsi(close, input(14))
Panneau dédié, échelle fixe 0–100, ligne médiane, repères 30/70, un contrôle de paramètre. Tout ça à partir du kind.
#Une bande, et un remplissage
fluxbb = bollinger(close, 20, 2)
plot bb.upper, bb.middle, bb.lower
fill bb.upper..bb.lower
// ✗ fill bb.upper..rsi(close, 14) — [ErrDim]: a price and an oscillator do not bound a region
#Un histogramme à couleur pilotée par le signe
fluxm = macd(close)
plot m.hist { style: histogram, color: if m.hist > 0 then up else down }
plot m.macd, m.signal
#Votre propre fonction
fluxdef zscore(x, n = 20) = (x - sma(x, n)) / stdev(x, n)
plot zscore(close) // (price − price) ÷ level → ratio
#Un ruban
fluxrepeat 8 as i {
plot ema(close, 10 + i * 10) { color: mix(down, up, i / 7) }
}
#Une divergence
fluxdef bearDiv(n) =
let ph = pivot_high(close, n, n) in // (source, left, right) — confirms n bars later
let px = valuewhen(ph, close[n]) in // this pivot's price
let osc = valuewhen(ph, rsi(close, 14)[n]) in // and its RSI
px > valuewhen(ph, px[1]) and osc < valuewhen(ph, osc[1])
mark bearDiv(5) "bearish divergence"
Un sommet plus haut sur le prix contre un sommet plus bas sur le RSI — ce qui veut dire que la
recette doit remonter d'un pivot en arrière, et c'est la partie qui vaut la peine d'être volée.
valuewhen(ph, px[1]) : à la barre où un pivot confirme, px vient de prendre la valeur de ce
pivot, donc px[1] détient encore le précédent — l'échantillonner exactement là, et le
maintenir, voilà comment on compare deux pivots successifs.
valuewhen n'a pas d'argument d'occurrence, et ph[1] n'en est pas un substitut : il retarde le
signal d'une barre, pas d'un pivot.
Les pivots sont des pivots confirmés — ils portent un lag, c'est pourquoi le prix du pivot est
close[n] et non close, et ils sont définitifs une fois émis. Un pivot qui muterait jusqu'à sa
confirmation serait un repaint, et il n'y a aucun moyen d'en écrire un.
#Signaux, marques et alertes
fluxcross = close cross_up ema(close, 50)
mark cross "crossed at {fmt.price(close)}"
alert cross "EMA-50 crossed up"
assert rsi(close, 14) <= 100 "rsi is bounded" // a self-check; `na` during warm-up passes
Un setup multi-conditions se lit comme une seule expression, parce que c'est ce qu'il est :
fluxsetup = close > ema(close, 200)
and rsi(close, 14) < 35
and volume > sma(volume, 20) * 1.5
and in_session("09:30-16:00 America/New_York")
mark setup { shape: triangle }
// ✗ mark setup { shape: triangle, color: up } — [ErrArg]: `color:` is a `plot` channel; a mark has none
#Plus d'une horloge
flux// paint the bars by the daily trend, on whatever chart you are looking at
color bars: if close > ema(close, 200) @ tf("1d") then up else down
// a higher-timeframe oscillator, shown here
plot rsi(close, 14) @ tf("4h")
// a miniature of the daily series, in the corner
sparkline close @ tf("1d")
Chacune de ces lignes lit la dernière unité clôturée de l'horloge plus grossière. La valeur qu'une barre affichait hier est la valeur qu'elle affiche aujourd'hui.
#Inter-séries
fluxbtc = series("BTC-USD")
eth = series("ETH-USD")
plot btc.close / eth.close // ratio — relative strength
plot stat.correl(returns(btc.close), returns(eth.close), 30) // osc(-1,1)
// ✗ plot btc.close + eth.close — [ErrDim]: different bases
// ✗ plot btc.close + series("BTC-EUR").close — [ErrDim]: a dollar is not a euro
#État
flux// a stop that ratchets and never loosens
def trail(mult) =
let stop = close - mult * atr(14) in
scan(stop, (prev) -> math.max(prev, stop))
plot trail(3)
Un mode, sous forme de variant et de match :
fluxvariant Trend { Up | Down }
def step(p, n) = match p.dir {
Up -> if close < p.ref - atr(n) then { dir: Trend.Down, ref: close } else p
Down -> if close > p.ref + atr(n) then { dir: Trend.Up, ref: close } else p
}
def flip(n) = scan({ dir: Trend.Up, ref: close }, (p) -> step(p, n))
color bars: match flip(14).dir { Up -> up ; Down -> down }
Remarquez le def step sorti de l'appel scan(…) : un match écrit à l'intérieur des
parenthèses d'un appel exige des séparateurs explicites entre ses bras, et l'extraire est le
correctif lisible.
#Canvas
#Une comète
fluxcircle { at: (bar.i, spring(close)), r: 6, glow: 16, trail: 24 }
#Feux d'artifice sur une cassure
fluxon close cross_up highest(close, 250)[1] -> burst(40) ring { r: 6 -> 24, opacity: 100% -> 0%, life: 2s }
#Un battement de cœur
fluxon every(1 bar) -> spawn ring { at: (bar.i, close), r: 4 -> 20, opacity: 80% -> 0%, life: 900ms }
#Une aurore de tendance
fluxbackdrop { fill: mix(down, up, norm(ema(close, 50) - ema(close, 200))) }
#Une surbrillance de session
fluxbackdrop { fill: token.grid, opacity: 8% } when in_session("09:30-16:00 America/New_York")
#Support et résistance automatiques
fluxgroup {
line { at: (bar.i, valuewhen(pivot_high(close, 5, 5), close[5])), w: screen.w, stroke: down }
line { at: (bar.i, valuewhen(pivot_low(close, 5, 5), close[5])), w: screen.w, stroke: up }
}
Le dernier sommet de swing confirmé et le dernier creux de swing confirmé, chacun maintenu jusqu'à ce que le prochain pivot le remplace.
Pourquoi pas les quatre derniers pivots ? Parce qu'une fenêtre est une fenêtre sur des barres,
pas sur des pivots : window(valuewhen(ph, close), 4) prend quatre échantillons-barres d'une série
maintenue par paliers, et entre deux pivots c'est le même niveau, quatre fois de suite. Une série
creuse — N pivots, aussi éloignés soient-ils — n'est pas du tout une fenêtre. C'est une
représentation avec un maxPivots déclaré, le même pattern borné que le Point & Figure plus bas
sur cette page. Une liste non bornée de pivots n'est pas quelque chose que vous pouvez demander, et
c'est la règle de totalité qui fait son travail.
#Transitions
fluxon switch(asset) -> morph chart over 500ms { ease: inOutCubic ; stagger: 0.3 ; surplus: collapse }
on click -> focus(view, at: (bar.i, close), zoom: 2.0, over: 600ms, ease: outBack(1.2))
replay from close cross_up ema(close, 200) over 8s
replay from prend un signal, pas une barre : vous rejouez à partir du moment où quelque chose
est devenu vrai, et le moteur trouve la barre. Un rejeu ancré à un ordinal signifierait quelque
chose de différent sur chaque graphique où il tournerait.
#La barre en formation
fluxplot ema(live(close), 20) // ✓ display only — updates within the forming bar
// ✗ alert ema(live(close), 20) > 100 — [ErrFirewall]: a decision may not read a forming value
// ✗ plot rsi(live(close), 14) — [ErrFirewall]: analysis may not consume it either
La première ligne est signalée non rejouable dans le panneau des garanties — visiblement, au moment où vous passez le trade.
#Argent et exactitude
fluxqty = 3d // decimal(scale 0) — the glued `d` makes it exact
px = 41.25d // decimal(scale 2)
gross = qty * px // `×` sums the scales → decimal(scale 2)
fee = decimal.round(gross * 0.001d, 2) // to 2 decimals, half-even, deterministic
// ✗ plot toFloat(fee) + fee — [ErrRepr]: an f64 and a decimal do not mix
L'arrondi n'est pas un mode que vous choisissez : decimal.round est au pair le plus proche
(half-even) et épinglé, une seule routine partagée par l'interpréteur, le module compilé et le
serveur — car un arrondi qui différerait selon le moteur écarterait deux machines d'un centime, et
le byte-déterminisme ne serait qu'un mot.
#Texte
fluxsym = "BTC-USD"
mark close cross_up ema(close, 50) "{sym} {fmt.price(close)} ({fmt.pct(change(close, 1) / close[1])})"
Un emplacement de message prend un littéral de chaîne, jamais une liaison qui se trouve en contenir un. L'étiquette est donc écrite là où elle est lue.
L'interpolation est un appel de formateur, et le formateur est épinglé — donc l'étiquette se lit pareil sur chaque moteur.
#Calendrier
fluxexpiry = time + time.months(3) // period — calendar, DST-aware
cutoff = time + 86400s // duration — exactly 24 hours, DST or not
// ✗ plot time.days(1) + 86400s — [ErrRepr]: a calendar span and a machine span do not add
« Un jour » et « 24 heures » sont deux choses différentes deux fois par an, et le système de types
sait laquelle vous vouliez dire. Une période est construite à partir de time.* et résolue
contre un calendrier ; une durée est un littéral portant un suffixe s ou ms, et dure
exactement ce qu'elle dit.
#Une application
fluxvariant Msg { Tick | Reset | Got(v: num) }
app watch {
capabilities: [ clock, chart:read ]
init(p) = { n: 0, last: na }
update(m, msg) = match msg {
Tick -> { model: m with { n: m.n + 1 }, cmds: [] }
Reset -> { model: m with { n: 0 }, cmds: [ PlaySfx("reset") ] }
Got(v) -> { model: m with { last: v }, cmds: [] }
}
view(m) = row {
text("ticks: {m.n}")
text("rsi: {fmt.num(m.last)}")
button("reset", Reset)
}
subs(m) = [ OnTick(1000, Tick), OnSeries("rsi", Got).throttle(200) ]
}
Tout ce qui est ambiant arrive comme un message ; tout effet repart comme donnée. C'est pourquoi une
application se teste à quatre grains — pas (un seul update), trace (un fold sur une liste
littérale de messages), vue (un instantané de l'arbre de vue) et propriété (un invariant asserté
sur une trace générée) — tous des assertions sur des fonctions pures, sans aucun mock nulle part. Les
deux premiers :
fluxassert update({ n: 0, last: na }, Tick) == { model: { n: 1, last: na }, cmds: [] }
C'est le grain pas. Le grain trace folde une liste littérale de messages à travers le même
update — assert fold(init(p), [ Tick, Tick, Reset ]) == { n: 0, last: na } — et la liste
littérale est tout l'intérêt : elle est le mock. Il n'y a aucun Sub à stubber, aucune horloge à
truquer, aucun réseau à intercepter, parce qu'aucun d'eux n'atteint jamais update. Ils n'ont jamais
fait que produire des messages, et un message est une valeur que vous pouvez taper à la main.
#Un type de graphique, sous forme de script
fluxrepresentation pnf(box, rev) {
transform: rebin(close, box, rev) // re-bin price into X/O columns
render: column { at: (clock.index, lo..hi), glyph: if dir == 1 then X else O }
reduce: merge(cols) // the column-correct decimator
liveReduce: last(cols) // extend the head column, or reverse → append
updateLastUnit: patch(cols) // mutate the head column in place
persistKey: "pnf-v1" // a price + box-ordinal anchor
}
Les six hooks sont tous obligatoires et portent une valeur — pas question d'en éluder un derrière un commentaire. Une représentation incapable de dire comment elle décime, ou comment elle étend son unité de tête en direct, ne serait pas un type de graphique ; elle serait la première moitié d'un type de graphique.
Le système de types impose la physique : box doit être un level (un déplacement), parce que
anchor + count * box ne se type que comme ça. Modélisez la box comme un prix et le compilateur
refuse — c'est le moment où vous apprenez quelque chose sur le Point & Figure.
#Ce qui ne compile pas, et pourquoi
fluxclose + rsi(close, 14) // ✗ [ErrDim] — a point plus a dimensionless number
close[-1] // ✗ — there is no negative index; the future has no syntax
window(close, len) // ✗ [ErrTotal] — a window bound must be a constant
close @ renko(50) @ tf("1d") // ✗ — one clock per series in v1
w.filter((x) -> x > 0) // ✗ — a data-dependent length would break totality;
// use `vec.where(w, (x) -> x > 0)` — same length, `na` where false
match dir { 1 -> up } // ✗ — `dir` is a scalar; discriminate it with `==`
Chacun de ces cas est une décision de conception sur laquelle vous pouvez vous documenter, pas une limitation que vous devez contourner à l'aveugle : kinds, temps et état, les quatre plans.
#Voir aussi
- Prise en main — la version guidée des premières recettes.
- Aperçu du FDK — les espaces de noms sur lesquels ces recettes s'appuient.
- Kinds — pourquoi les exemples rejetés sont rejetés.
- Canvas — l'algèbre de signaux derrière celles qui bougent.
- Plan APP — le contrat complet derrière l'application.
- display —
viz.*, pour des données qui ne sont pas une série de prix.
Garanties
Cette page s'adresse au lecteur qui doit décider s'il peut confier à Flux quelque chose d'important. Elle énonce chaque garantie en une phrase, dit exactement ce qu'elle couvre et ne couvre pas, et nomme le contrôle machine qui la fait respecter — car une garantie dont la seule application est une promesse dans un document n'est pas une garantie.
Rien ici n'est un vœu pieux. Là où une limite est réelle, elle est énoncée comme une limite.
#Les sept garanties
| Garantie | En une phrase | Assurée par |
|---|---|---|
| Totalité | Tout programme se termine, et son coût par pas est connu avant son exécution. | Bornes const-foldées sur chaque fenêtre, boucle et collection ; un budget de taille de graphe ; [ErrTotal] à la compilation |
| Causalité (no-repaint) | Une valeur, une fois produite pour un pas, ne peut plus jamais changer. | Délais uniquement vers le passé ; rééchantillonnage sur unité clôturée ; tout cycle de rétroaction franchit un délai d'une unité ; [ErrCausal] |
| Byte-déterminisme | Le même programme sur les mêmes données produit les mêmes octets, sur tout moteur et toute machine. | Routines épinglées ; un ordre de réduction fixe ; un na canonique ; le gate I7 à chaque compilation |
| Solidité dimensionnelle | L'arithmétique dénuée de sens ne compile pas. | Le treillis des kinds et l'algèbre des opérateurs, énumérés et vérifiés par machine, famille par famille |
| Le pare-feu | La présentation peut lire l'analyse ; l'analyse ne peut jamais lire la présentation. | Une vérification statique des dépendances ; [ErrFirewall] |
| Sécurité par capacités | Un script n'a aucune autorité ambiante ; chaque effet est une donnée inerte que l'hôte exécute sous une capacité accordée. | Rejet à la compilation d'une requête non accordée ([ErrCapDenied]) ; un moniteur de capacités vérifié par model-checking |
| Optimisation vérifiée | L'optimiseur ne peut pas livrer une valeur fausse. | Validation de traduction contre le graphe non optimisé, à chaque compilation |
#Ce que chacune signifie vraiment
#Totalité
Chaque fenêtre, chaque boucle, chaque collection porte une borne constante, sous un plafond. Un programme incapable d'énoncer sa borne ne compile pas.
Ce qu'elle couvre. La terminaison, et un coût par pas calculable à la compilation. Il n'y a pas de timeout par barre, parce qu'il n'y a rien à faire expirer : un programme hors budget est rejeté, pas tué.
Et le verdict lui-même est déterministe. Accepter ou rejeter est une fonction pure de la source, décidée par les seuls compteurs — jamais par une horloge. Le timeout de build de l'éditeur (de l'ordre de deux secondes) est une annulation interactive, une question de réactivité de l'UI ; ce n'est jamais un verdict.
Pourquoi cette règle existe. Un verdict à l'horloge murale serait dépendant de la machine — le même script accepté sur une machine rapide et rejeté sur une lente. Deux utilisateurs n'exécuteraient alors pas le même langage, et le rejeu, qui suppose que ce qui a compilé là-bas compile ici, casserait ; l'anti-triche casserait avec lui. Le déterminisme doit commencer à la réponse du compilateur, sinon il ne tient nulle part en aval.
Ce qu'elle ne couvre pas. Elle ne rend pas votre algorithme rapide. Elle rend son coût connaissable.
#Causalité — « no-repaint »
Les délais ne portent que vers l'arrière. Un rééchantillonnage lit la dernière unité clôturée d'une horloge plus grossière, jamais celle encore en formation. Tout cycle de rétroaction doit franchir un délai d'une unité.
Ce qu'elle couvre. La valeur qu'une barre affichait hier est la valeur qu'elle affiche aujourd'hui. L'évaluation en direct et l'évaluation historique produisent les mêmes octets. Le repaint n'est pas découragé — il est inexprimable : il n'y a pas de syntaxe pour un indice négatif, et pas de nom pour l'unité en formation à l'intérieur de l'analyse.
L'unique exception, et son mur. live(e) lit la barre en formation, et il ne peut s'écouler
que vers des puits d'affichage. Le faire entrer dans une alerte, une assertion ou un calcul est
[ErrFirewall]. Un script qui l'utilise est signalé non rejouable, visiblement, dans le
panneau des garanties.
#Byte-déterminisme
f64 scalaire. Pas de SIMD dans le domaine déterministe. Pas de réassociation en virgule
flottante. Chaque transcendante, chaque opération décimale, chaque repli Unicode, chaque addition
calendaire, chaque tirage aléatoire et chaque tri sur des valeurs absentes passe par une seule
routine épinglée, partagée par l'interpréteur, le module compilé et le serveur.
Ce qu'elle couvre. Deux moteurs s'accordent bit à bit. Un golden tient. Le rejeu reconstruit un modèle exactement.
Post-v1. La ré-exécution serveur — un serveur qui rejoue le travail d'un client pour attraper un résultat falsifié — repose exactement sur ce déterminisme, et est conçue. Mais en v1, la jambe native/serveur est vérifiée côté client : la ré-exécution sur le serveur suit le portage serveur du grader, et est différée.
Ce qu'elle ne couvre pas. La présentation. Le GPU, le compositeur, l'aléa sans graine et le temps à l'horloge murale sont hors de l'oracle par conception — et le pare-feu garantit qu'ils n'y entrent jamais.
#Solidité dimensionnelle
Un prix n'est pas un volume ; un prix BTC n'est pas un prix ETH ; un décimal exact n'est pas un flottant. Les additionner est une erreur de compilation, pas une surprise à l'exécution ni un nombre silencieusement faux.
Ce qu'elle couvre. Toute une classe de bugs que d'autres systèmes trouvent en production, quand ils les trouvent.
Ce qu'elle ne couvre pas. Ce n'est pas un système de preuve. Une borne osc(0,100) est une
affirmation de présentation, pas un invariant à l'exécution — seul clamp rend une borne réelle.
Flux n'a délibérément aucun solveur, et le dit.
#Le pare-feu
Quatre choses ne peuvent jamais atteindre l'analyse : l'espace écran, l'horloge murale, l'aléa sans
graine et la barre en formation. Toutes les quatre lèvent [ErrFirewall].
Ce qu'elle couvre. La scène animée, aléatoire et interactive d'un inconnu peut tourner à côté du nombre sur lequel repose votre décision, sans pouvoir y toucher. C'est ce qui fait du contenu généré par les utilisateurs un acte de routine plutôt qu'une évaluation de risque.
#Sécurité par capacités
Un script ne détient aucun objet de capacité. Il émet une requête ; l'hôte, seul détenteur de la ressource, l'exécute — et seulement si le manifeste l'a déclarée et que l'utilisateur l'a accordée.
Ce qu'elle couvre. Aucune autorité ambiante. Aucun token dans le script. Aucune re-délégation. Un manifeste transitif qui fait remonter l'appétit d'une dépendance pour le réseau avant l'installation, plafonné par l'octroi de l'utilisateur. Une révocation en cours de session est journalisée, si bien qu'un re-fold la reproduit et que les commandes émises après elle échouent en mode fermé.
La limite honnête. Le langage est sûr par construction ; le moniteur de capacités et l'assainisseur de vue sont du code ordinaire, et ils constituent la surface d'attaque résiduelle. C'est précisément pourquoi ils sont le seul composant qui mérite un model-checking, et pourquoi les primitives de vue forment un ensemble fermé et typé plutôt qu'une chaîne.
#Optimisation vérifiée
La sémantique de référence d'un programme est l'évaluation de son graphe non optimisé. Chaque compilation vérifie le module optimisé contre elle, bit à bit, sur des données hostiles.
Ce qu'elle couvre. Une compilation erronée ne peut pas être livrée. Si l'optimiseur diverge, la compilation sert le chemin non optimisé et lève un diagnostic qui fait passer la suite de tests au rouge.
La limite honnête. Le gate prouve l'égalité sur la couverture de valeurs du corpus et jusqu'à un plafond de balayage des périodes. Une règle dont la divergence n'apparaît qu'au-delà de ce plafond passerait — c'est pourquoi les règles qui touchent aux noyaux ou à l'état portent une obligation de preuve explicite à la période maximale réelle.
#Le harnais de vérification
Les garanties sont vérifiées par une suite dont les sous-suites déclarent chacune un oracle et un corpus :
| Suite | Ce qu'elle affirme | Bloquant |
|---|---|---|
| Goldens | chaque exemple est un golden déterministe ; un golden inchangé reste byte-identique | oui |
| Propriétés | principalité ; confluence (le kind est invariant sous tout ordre topologique) ; re-typage incrémental ≡ inférence complète ; le plan mémoire est une fonction déterministe du graphe | oui |
| Fuzz + un générateur bien typé | le parseur et le vérificateur de types sont totaux (toute entrée produit un arbre ou un rejet propre) ; le générateur émet des graphes causaux type-corrects qui alimentent l'oracle | oui, dès lors qu'il alimente l'oracle |
| Oracle différentiel | interpréteur ↔ WASM ↔ noyau natif — couvrant I6, optimisé ≡ référence, et I7 | oui |
| Métamorphique | les relations préservant la sémantique, énumérées : optimisé ≡ référence · interpréteur ≡ WASM ≡ serveur · 1 ≡ N workers · plan-pic ≡ plan-somme · confluence sous tout ordre topologique · recompilation ≡ recompilation, byte-identique · la draw-list absolue est invariante à la cible et à la séquence | oui |
| Stress 1 ≡ N | le même graphe sous un worker et sous plusieurs, assignés de façon adverse : octets identiques, zéro écriture concurrente de slot | oui |
| Énumération du treillis | les lois et chaque jugement d'admissibilité, énumérés par famille | oui |
| Moniteur de capacités | « aucune commande hors du manifeste n'est jamais exécutée » | oui |
| Bench | le modèle de coût est calibré par la mesure ; le pic à l'exécution égale le pic planifié | indicatif |
Une subtilité mérite d'être connue, parce que c'est le genre de chose qu'un lecteur attentif demande : l'oracle à trois voies appelle la même routine épinglée des trois côtés, il est donc aveugle à un bug à l'intérieur d'une routine épinglée. Chaque routine épinglée porte donc une seconde implémentation de référence, indépendante, comparée bit à bit sur des entrées fuzzées. L'oracle attrape le désaccord ; seule la seconde implémentation attrape une erreur partagée.
#Builds reproductibles
Le hash du build est une fonction pure de la source, de la clôture des dépendances, de la version du compilateur, des routines épinglées et du plan mémoire canonique. Le gate de reconstruction recompile deux fois les mêmes entrées — sur des machines différentes, avec des nombres de threads différents — et affirme que le module émis est byte-identique.
Le rejeu côté serveur en dépend. Un build non reproductible casserait le rejeu silencieusement, parce qu'un oracle au niveau des valeurs ne peut pas voir les octets émis à travers deux compilations.
#Le panneau des garanties
Après une compilation, l'éditeur énonce ce que votre programme a réellement obtenu :
✓ No-repaint ✓ No look-ahead ✓ Deterministic
✓ Bounded memory ✓ Byte-identical ⚠ contains live() → non-replayable
Ce n'est pas de la décoration. Une garantie à laquelle vous avez renoncé devrait être visible au moment où vous y renoncez.
#Ce qui n'est pas garanti
Dit sans détour, car une page de confiance qui ne liste que les forces est une page de vente :
- La présentation n'est pas déterministe, et ne cherche pas à l'être. Le GPU, le compositeur et l'aléa sans graine sont hors de l'oracle — contenus par le pare-feu, jamais éliminés.
- Le rejeu prouve la cohérence, pas la véracité. Une charge utile poussée par l'hôte et journalisée comme donnée — un résultat de sélection (pick), un résultat pré-calculé — est re-foldée telle quelle ; le rejeu ne rejoue pas le ray-cast pour l'attester. Un score qui dépend d'un tel résultat exige que le serveur le re-dérive, ou doit être exclu d'un classement partagé. C'est un problème ouvert nommé, pas un problème caché.
- Post-v1. La ré-exécution serveur est conçue, pas livrée. Le déterminisme qui permettrait à un serveur de rejouer le travail d'un client et d'attraper un résultat falsifié est réel, et vérifié — mais en v1 il est vérifié sur le client. La jambe serveur attend le portage serveur du grader.
- Les bornes sont des affirmations, pas des invariants.
osc(0,100)dit ce qu'une valeur est par convention, pas ce qu'elle est de manière prouvée. Seulclampla rend réelle. - Le sandbox repose sur deux morceaux de code ordinaire — le moniteur de capacités et l'assainisseur de vue. Tout le reste est sûr par construction ; ces deux-là sont sûrs par relecture, par model-checking et par fuzzing.
#Voir aussi
- Piliers de conception — les mêmes propriétés, vues du côté conception.
- Compilateur et runtime — le gate et les routines épinglées.
- Optimiseur — la loi de correction, et les réécritures qui semblent valides et ne le sont pas.
- Concurrence — pourquoi le parallélisme ne peut pas changer une valeur.
- Plan APP — capacités, journaux et rejeu.
- FAQ — les questions que cette page provoque.
L'éditeur
Un éditeur pour Flux peut faire des choses qu'un éditeur pour un langage généraliste ne peut pas — non parce qu'on y a mis plus d'efforts, mais parce que le langage lui donne plus de matière. Chaque valeur a un kind, chaque programme est un graphe, et chaque évaluation est déterministe. L'éditeur peut donc filtrer une liste de complétion par dimension, vous montrer la valeur d'une liaison à la barre sous votre curseur, et vous dire pourquoi un signal est vrai — sans rien exécuter deux fois.
Cette page décrit ce que fait l'outillage et, plus utilement, pourquoi il le peut.
#Intelligence du code
#Complétion, filtrée par kind
Après un ., vous obtenez les membres du record. Après un stream, vous obtenez uniquement les
fonctions dont le premier paramètre accepte ce kind :
close. // → ema, sma, rsi, highest, … (everything that accepts a `price`)
rsi(close,14). // → ema, sma, change, … (kind-preserving families) — but NOT `vwap`
Les exclusions sont la moitié qui vaut la lecture. vwap veut un signal, il n'est donc jamais
proposé après un prix. atr non plus — pour une raison différente : il lit lui-même high, low et
close et ne prend qu'une longueur, il n'a donc aucun paramètre de source que close. pourrait
remplir. close.atr(14) est [ErrArg], et une liste qui le proposerait vous tendrait une ligne
qui ne compile pas.
C'est le bénéfice du chaînage façon méthode : il transforme le système de types en un mécanisme de découverte. Vous n'avez pas besoin de connaître le catalogue ; vous devez savoir ce que vous avez.
Et en tête de ligne vide, l'éditeur propose les verbes de sortie (plot, def, let, mark,
alert) — ainsi un nouveau venu découvre que plot est la façon dont une valeur atteint l'écran,
au lieu de devoir le savoir d'avance.
#Aide à la signature et survol
Taper ema( ouvre ema(source: price, length: lit) → price, avec l'argument courant mis en
évidence.
Un survol sur une opération donne sa documentation, sa signature de kind, un exemple miniature, et une sparkline en direct de cette opération sur les données actuellement à l'écran. Un survol sur une liaison donne son kind inféré et sa valeur à la barre sous le curseur.
Ce dernier point mérite qu'on s'y arrête : il est possible parce que l'évaluation est déterministe et que le graphe est déjà calculé. Il n'y a pas de « build de debug », et rien n'est ré-exécuté.
#Des diagnostics qui enseignent
price + osc — you are adding a price and a 0–100 oscillator.
close + rsi(close, 14)
^^^^^^^^^^^^^^ osc(0,100), a dimensionless bounded value
Did you mean close + atr(14) (a price + a displacement)?
Trois formes d'aide se cachent derrière cela :
- Explications dimensionnelles. Un désaccord de kind est expliqué en termes de ce que les valeurs sont, jamais en termes des rouages internes du vérificateur de types.
- « Vouliez-vous dire ». Un nom inconnu, un champ manquant, un noyau mal orthographié — appariés par distance d'édition contre les noms dans la portée, le catalogue et les champs du record (tous déjà matérialisés pour la complétion).
- « Vous avez oublié
plot». Une expression nue au niveau supérieur est une erreur de syntaxe — la grammaire n'a pas d'expressions-instructions, et cela reste figé. Mais quand le kind de l'expression est présentable, l'éditeur la rattrape pédagogiquement : « pour afficher ceci, enveloppez-le dansplot», avec le correctif rapide. Le langage reste strict ; l'expérience, non.
#Inlay hints
h = macd(close).hist ⟦level · −12.3⟧
m = ema(close, 20) @ tf("1h") ⟦price · @1h⟧
Les ⟦…⟧ ne sont pas du texte dans votre fichier — ils sont rendus à côté.
Le kind, la valeur à la barre du curseur, et — quand une liaison tourne sur une horloge non par défaut — sa provenance d'horloge. Vous pouvez voir qu'une valeur vient de la série horaire, sans que le langage ait à encoder l'unité de temps dans le type (ce qui casserait l'idiome de confluence que toute la conception est bâtie pour permettre).
#Le registre novice
Les avertissements et les lints de style sont différés jusqu'à votre première compilation verte, puis révélés en opt-in. Le premier jour ne montre jamais un mur de rappels agaçants. La classification dur/souple du canal d'erreur est inchangée — c'est une politique de présentation, pas une politique sémantique.
#Le formateur canonique
Formatage à la sauvegarde, aucune option à débattre — et une tâche précise au-delà de la propreté : il neutralise le piège du saut de ligne significatif. Il normalise les retours à la ligne et l'indentation de continuation de sorte que l'étendue de chaque instruction soit visible. Un nouveau venu n'a jamais à deviner où une expression s'est terminée.
#Coloration sémantique par kind
Les prix, les oscillateurs, les signaux et les primitives de canvas sont colorés différemment les uns des autres — non par catégorie syntaxique, mais par ce qu'ils sont. C'est une petite chose qui s'avère importante : vous voyez la forme des dimensions d'un programme avant de le lire.
#Aperçu en direct
L'éditeur compile au repos (un court debounce) et applique le résultat au graphique. Quand il y a
une erreur, il ne vide pas l'aperçu : il évalue le cône typable — la plus grande partie du
graphe dont chaque entrée est exempte d'erreurs — et rend le reste sous forme de —. La dernière
version valide n'est un fallback que si le cône est vide.
La conséquence, c'est qu'un nom à moitié saisi vous coûte une valeur, jamais l'écran.
La pastille de statut vous dit alors ce que votre programme a obtenu :
✓ No-repaint ✓ No look-ahead ✓ Deterministic ✓ Bounded memory ✓ Byte-identical
⚠ contains live() → non-replayable
Post-v1. Pour une application, le hot reload se généralise : l'hôte re-folde le journal de
messages retenu avec le nouveau update, sans appeler init — ainsi l'état de votre
application survit à une édition. Un changement de forme retombe sur le chemin de migration ; ce
n'est jamais une réinitialisation silencieuse.
#Déboguer un graphe
Un programme Flux n'a pas de pile d'appels, donc un débogueur impératif répondrait à une question que personne n'a posée. Ce qu'un programme a, c'est un graphe de signaux typés sur un axe ordinal — le curseur est donc bidimensionnel : quand (quelle barre) × quoi (quel nœud).
| Mouvement | Ce que c'est |
|---|---|
| Valeurs ambiantes | les inlay hints montrent déjà la valeur de chaque liaison à la barre du curseur. Aucun mode à activer. |
| Le graphique est le scrubber | une tête de lecture sur le graphique est le curseur de barre — glissez-la, ou utilisez les touches fléchées. Pas de timeline séparée. |
| La sonde | scrubbez jusqu'à une barre et lisez le tableau de la valeur de chaque liaison à cet endroit. Déterministe, donc exact plutôt qu'échantillonné. |
| La vue dataflow | le graphe compilé, rendu comme un graphe — parce que c'est ce qu'il est. Cliquez sur un nœud : la source se met en évidence et sa série apparaît. |
| Le cône causal | « pourquoi ce signal est-il vrai ici ? » — met en évidence tout ce dont la valeur à cette barre a réellement dépendu. |
| Points d'arrêt sur données | pas un point d'arrêt de ligne mais de donnée : « aller à la première barre où macd cross_up 0 ». La série est déjà calculée, donc le saut est une recherche, pas une ré-exécution — il est instantané. De même : le prochain événement, le prochain na, la prochaine divergence. |
| Voyage dans le temps | le rejeu est exact, donc reculer pas à pas n'est pas une approximation ; c'est le même calcul. |
Post-v1. Pour une application, ces mêmes mouvements se transposent en un curseur (événement,
champ) sur le journal de messages : le point d'arrêt sur données devient « le premier message où
score franchit 40 », le voyage dans le temps devient un pas arrière le long du journal, et la
lentille de confiance devient un diff du modèle contre une exécution de référence. Le débogueur sur
l'axe des barres ci-dessus est en v1 ; son jumeau application est la moitié différée.
#Curseurs, et le modèle de paramètres
input(14, 2..200) rend un curseur dans la gouttière. Ce qui se passe quand vous le glissez est
la partie qui vaut la peine d'être connue :
La valeur ajustée vit dans une surcouche de paramètres, pas dans le littéral de la source. La source porte la valeur par défaut. Glisser met à jour un paramètre et ré-exécute le pas incrémental — cela ne recompile pas, ne modifie pas votre source, et n'inonde pas votre historique d'annulation.
L'unité qui est persistée, partagée et rejouée est donc (hash de source, paramètres) — un module
compilé, de multiples réglages, avec byte-identité sur (artifact, parameters) → output. « Graver
le défaut » est une action explicite, et le seul chemin qui réécrit une valeur de surcouche dans la
source.
Un paramètre avec une plage déclarée est borné par son maximum : la mémoire est dimensionnée pour le pire cas, donc glisser un curseur ne peut jamais allouer.
#Le HUD de performance
Par script : le nombre de nœuds, le coût par barre, le budget de frame du canvas — et un avertissement quand un script est lourd. La gouttière de coût lit le graphe optimisé, donc ce que vous voyez est ce que vous payez.
#Doc-as-data
La carte de survol, la liste de complétion, cette documentation, les messages d'erreur et les snippets se rendent tous à partir d'un seul record structuré par fonction, kind, mot-clé et opérateur. Ils ne peuvent pas diverger, parce qu'ils sont la même donnée.
Et un lint de complétude le garde honnête : chaque construction du langage a un record de documentation et au moins un exemple exécutable — et chaque exemple est un golden. Une fonction documentée dont l'exemple cesse de fonctionner fait passer un test au rouge, ce qui est le seul genre de garantie de documentation qui vaille.
#Voir aussi
- Inférence — le cône typable, et pourquoi une ligne cassée ne vide pas l'aperçu.
- Kinds — ce sur quoi filtre la complétion filtrée par kind.
- Prise en main — l'éditeur tel que vous le rencontrez d'abord.
- Garanties — ce que la pastille de statut affirme réellement.
- Aperçu du FDK — doc-as-data, et le lint de complétude.
FAQ
La plupart des questions sur un langage total et causal se révèlent déjà répondues par le modèle — la garantie est dans la conception, mais elle n'est pas visible au premier coup d'œil. Cette page rend ces réponses explicites, et pointe vers la section qui fait autorité pour chacune.
La seconde moitié est l'inverse : les choses que Flux ne fait délibérément pas, et ce qu'il offre à la place.
#« Comment Flux gère-t-il… ? »
#Les unités de temps multiples
@ est un rééchantillonnage causal : ema(close, 20) @ tf("1h") lit la dernière unité horaire
clôturée. clock est un kind de première classe — composable, stockable, transmissible via un
input — et @ est son éliminateur. Une horloge par série en v1, ce qui écarte les mélanges
dangereux ; et l'idiome de confluence (close > ema(close, 50) @ "1d") est le but, pas une erreur
à interdire.
#Les valeurs intra-barre
live(e) lit la barre en formation, par frame, et il ne s'écoule que vers des puits
d'affichage. Le faire entrer dans une décision — une alerte, une assertion, un calcul — est
[ErrFirewall]. Ce mur est le no-repaint : vous pouvez regarder une valeur provisoire, mais vous
ne pouvez pas agir dessus comme si elle était définitive.
#Les types
rsi a vraiment le kind osc(0,100). price et osc sont vraiment incompatibles à la
compilation. Les bornes se propagent vraiment (rsi − 50 → osc(-50,50)). Et il n'y a aucun
solveur — le treillis est fini par famille, donc les lois sont vérifiées par énumération plutôt
que prouvées par une machinerie.
→ Kinds
#Le déterminisme bit à bit
Oui — à travers l'interpréteur, le module compilé et le serveur, sur ARM et sur x86. f64 scalaire,
pas de SIMD dans le domaine déterministe, pas de réassociation en virgule flottante, un ordre de
réduction épinglé, et une routine épinglée pour chaque opération où deux implémentations pourraient
diverger (transcendantes, décimaux, Unicode, le calendrier, l'aléa, le tri avec valeurs absentes,
et le motif de bits de na lui-même).
#État persistant, reducers, machines à états
scan(seed, (prev) -> …) fait avancer l'état d'un pas par barre. Un état composite est un record ;
une machine à états est un variant plus un match (exhaustif, sinon il ne compile pas) ;
l'itération bornée est loop(max, …).
#Données externes (funding, open interest, macro)
Elles entrent dans le plan APP par des abonnements typés — jamais l'analyse directement, car le
pare-feu interdit à une application d'écrire un indicateur. Pour devenir un indicateur, un flux
doit être foldé en barres clôturées et transmis via la jonction data:source, que l'hôte ingère en
append-only : une barre clôturée n'est jamais révisée, donc le no-repaint survit.
Réservé. La limite de v1 est nommée plutôt que cachée : un indicateur piloté par une série
non-prix n'est pas exprimable tant que le kind metric n'est pas armé. Cette jonction est conçue,
et inerte.
#Effets et « mondes »
Il n'y a pas d'annotation d'effet, parce qu'il y a des plans. ANALYSIS / CANVAS / TRANSITION / APP, avec un pare-feu à sens unique vérifié statiquement. Et vous ne déclarez jamais sur quel plan vous êtes — c'est inféré de ce que vous écrivez.
#Outillage : pourquoi ce signal est-il vrai ici ?
Élimination globale des sous-expressions communes, une vue dataflow avec un cône causal, un rapport
d'optimisation, un budget de performance, un temps réversible le long de l'axe des barres — pas
à pas, scrub et saut, dans les deux sens, parce que le rejeu est exact — et assert avec des
goldens. La question du débogueur n'est pas « quelle est la valeur » mais « pourquoi », et le graphe
peut y répondre.
#« Pourquoi Flux ne fait-il pas… ? »
#…taguer l'unité de temps dans le type (series<H1, price>) ?
La sécurité que cela apporterait — une unité de temps visible, une conversion explicite — est
déjà fournie par @ plus la règle d'une horloge par série. Et le tag rouvrirait le treillis
avec un nouvel axe, et casserait l'idiome de confluence : close > ema(close,50) @ "1d" est ce que
les gens veulent vraiment, pas une erreur à empêcher. Le temps est de première classe ici via le
kind clock et l'opérateur @ — pas via un paramètre de type.
#…supporter le scripting tick par tick et order-flow ?
Post-v1. Centré sur la barre en v1, et dit clairement : toute l'infrastructure live diffuse des
barres en continu, et live() opère sur la barre en formation, pas sur les ticks. La granularité
infra-barre est une extension nommée — une frontière de périmètre tracée exprès, avec un
mécanisme déjà derrière elle, pas un oubli.
#…prouver des invariants relationnels (high ≥ low) ?
Cela demande un solveur, et Flux n'en a explicitement aucun. Les bornes de kind sont des
affirmations de présentation, pas des invariants à l'exécution (seul clamp rend une borne
réelle). Refuser le solveur est ce qui garde le système de types décidable, rapide et honnête sur
ce qu'il vérifie — ce qui est précisément le piège que les gens qui réclament des invariants
cherchent d'ordinaire à éviter.
#…ajouter des raffinements de signe (price ≥ 0) ?
Un nouvel axe au faible bénéfice, qui contredit « les bornes sont des affirmations » — et faux sur
les données de toute façon : un volume signé est souhaité (c'est ainsi que fonctionne un
indicateur de volume cumulé), et un niveau de volatilité signé a du sens. Non retenu.
#…livrer un convertisseur automatique depuis un autre écosystème de scripting ?
Parce qu'un convertisseur fidèle est impossible. Flux exclut délibérément ce que ces écosystèmes permettent — les modifications rétroactives de l'historique, les boucles non bornées, les I/O ambiantes — si bien que toute une classe de programmes n'a aucune traduction fidèle, et un convertisseur qui produirait silencieusement quelque chose serait pire que rien. La documentation enseigne les patterns équivalents directement, à la main, ce qui est la version honnête de la même aide.
#…offrir un screener sur tout le marché ou de la gestion de portefeuille ?
Le travail inter-séries sur une poignée d'instruments nommés est de première classe. Scanner le marché entier, et gérer de nombreuses positions simultanées, ne sont pas des préoccupations de v1.
Post-v1. Un screener borné — une fonction totale mappée sur un univers à capacité fixe, avec un top-K par clé — est un pilier scellé à part entière : un map-reduce borné, jamais un scan non borné.
#Les questions qu'on pose en second
#Est-ce rapide ?
La vitesse vient des algorithmes et des noyaux natifs, pas du langage d'exécution. Un simple
rsi(close, 14) n'a rien à optimiser — il est le noyau natif. Ce que la conception vous apporte,
c'est que le cas compliqué ne coûte pas ce qu'il en a l'air : les sous-expressions partagées sont
calculées une fois, le code mort n'est jamais calculé, les chaînes élément par élément fusionnent en
une seule passe, et les parties d'une scène qui bougent le plus coûtent zéro JavaScript par
frame.
#Que se passe-t-il quand je fais une erreur ?
Vous obtenez une phrase, pas une stack trace : ce que vous avez fait, quels étaient les kinds, ce que cela aurait signifié, et un correctif rapide. Un repaint refusé est expliqué — « ceci ferait changer une valeur passée une fois la barre clôturée » — parce que le refus est la fonctionnalité.
#Puis-je faire confiance à un script écrit par quelqu'un d'autre ?
C'est la question à laquelle toute la conception répond. Il tourne dans le même sandbox que le nôtre, avec uniquement les capacités que son manifeste a déclarées et que vous avez accordées ; ce manifeste voyage avec le binaire, est inspectable avant que vous ne l'installiez, et s'agrège transitivement, de sorte qu'une dépendance ne peut pas cacher son appétit. Et quoi qu'il dessine, il ne peut pas toucher aux nombres sur lesquels repose votre décision.
#Qu'est-ce qui est vraiment difficile avec Flux ?
Deux choses, honnêtement. La totalité est une vraie contrainte — vous ne pouvez pas écrire une boucle non bornée, et certains algorithmes doivent être ré-exprimés avec un plafond déclaré. Et la chaîne asynchrone dans le plan APP est verbeuse — chaque étape est un message, parce que chaque étape doit être dans le journal pour que le rejeu fonctionne. Ce sont deux prix, et tous deux sont payés à dessein.
#Voir aussi
- Garanties — chaque promesse, et son contrôle machine.
- Qu'est-ce que Flux — le modèle mental, et les non-objectifs.
- Kinds — le système de types derrière la moitié des réponses ci-dessus.
- Les quatre plans — le pare-feu derrière l'autre moitié.
- Glossaire — chaque terme utilisé ici, défini.
Glossaire
Chaque terme que cette documentation emploie avec un sens précis, défini une fois pour toutes. Là où un terme a un sens courant ailleurs et un sens pointu ici, c'est le sens pointu qui est visé.
#Le modèle
Flux (stream). Une valeur au fil des pas. Toute valeur du langage en est un ; une constante est un flux dégénéré. Il n'y a ni indices ni boucles sur le temps — l'arithmétique est élément par élément et incrémentale en coulisses.
Kind. Le mot de Flux pour « type ». Il porte la dimension de la valeur, pas seulement sa
forme : price, level, osc(0,100), signal, clock. Les kinds sont ce qui fait échouer la
compilation de l'arithmétique dénuée de sens, et ce qui permet à la présentation d'être inférée
plutôt que configurée.
Sorte (sort). Une strate du treillis des kinds : scalaire, catégorielle (color,
clock, string, ui), structurelle (vec, record, variant). Entre deux sortes, la
borne supérieure est ⊤ et la borne inférieure est ⊥.
Treillis (lattice). L'ordre partiel sur les kinds, avec sa borne supérieure (⊔, unification) et
sa borne inférieure (⊓, contrainte). Fini par famille, ce qui permet de vérifier ses lois par énumération.
Borne supérieure (⊔) / borne inférieure (⊓). Unification (les branches d'un if, deux séries
co-tracées) et contrainte (ce qu'un paramètre exige). À noter que price − price = level n'est pas
une borne supérieure — c'est une règle d'opérateur.
⊤ (top) / ⊥ (bottom). L'unique canal d'erreur, et le kind de na. ⊤ n'est une erreur
dure que dans une position exigeante ; ailleurs, c'est un avertissement.
Position exigeante. Un opérande, une cible de plot, un argument — un endroit où une valeur
est réellement consommée. Un ⊤ à cet endroit est [ErrDim] ; un ⊤ dans une liaison non
consommée est [WarnTop].
Antichaîne. Les kinds dimensionnés (price, volume, time, …), deux à deux incomparables,
qui remontent vers quantity plutôt que vers num — délibérément, pour qu'un tracé mixte ne
puisse pas être silencieusement bien typé.
lit. Un littéral const-foldé, polymorphe en dimension : il se convertit sans risque en
chaque scalaire. C'est pourquoi close + 10 n'a besoin d'aucun cast.
quantity. La dimension effacée — le sommet de la sorte scalaire. Traçable, mais suspecte :
elle déclenche un avertissement.
Tag. Une annotation portée orthogonalement à la dimension d'un kind : la représentation
numérique (f64 / decimal), la représentation du temps (machine / calendar), l'identité
d'actif (B, Q [, @v]), la paire de devises sur ratio, l'unité meas[u] sur num.
#Temps et causalité
Horloge. L'axe des pas d'une série, en tant que kind de première classe : tf("1h"),
renko(box), pnf(box, rev), range(r). Les axes à grain temporel grossier et ceux pilotés par
le prix sont le même concept.
@ (rééchantillonnage). L'éliminateur d'une horloge. e @ c lit e sur l'horloge c, en
prenant la dernière unité clôturée — jamais celle encore en formation.
Causalité. output[t] ne dépend que de inputs[0..t]. Assurée par des délais uniquement vers
le passé, un rééchantillonnage sur unité clôturée, et la règle selon laquelle tout cycle de
rétroaction franchit un délai d'une unité.
No-repaint. La conséquence : une valeur, une fois produite pour un pas, ne peut plus jamais changer. L'évaluation en direct et l'évaluation historique produisent les mêmes octets. Le repaint n'est pas découragé — il est inexprimable.
Warm-up. Les premiers pas où un noyau n'a pas encore vu assez de données et renvoie na. Un
indicateur Flux hérite exactement du warm-up de son noyau, ce qui fait tenir la byte-identité dès
la première barre.
Lag (retard de confirmation). Une valeur qui est na pendant L pas puis définitive pour
toujours — un pivot confirmé, par exemple. Pas un repaint : rien de ce qui a été émis ne change.
live(e). Le lecteur de la barre en formation. Par frame, affichage uniquement : il peut
s'écouler vers plot, mark, fill, color bars et les scènes, et nulle part ailleurs. Tout le
reste est [ErrFirewall], et un script qui l'utilise est signalé non rejouable.
Alignement as-of. Aligner une série étrangère sur l'axe du graphique en prenant le pas étranger le plus récent à l'instant courant ou avant — une recherche par plancher (floor), jamais une correspondance au plus proche (qui lirait le futur).
na. Une valeur absente. Elle se propage à travers l'arithmétique (le kind est préservé), fait
que toute comparaison renvoie na (à tester avec is_na / is_some), et possède un seul motif
de bits canonique à chaque frontière de stockage — ce qui maintient l'accord entre deux moteurs
sur les octets d'une valeur qui n'est pas là.
#Les plans
Plan. L'un des quatre contextes d'exécution, chacun avec sa propre horloge et ses propres règles : ANALYSIS (la barre ; total, causal, déterministe), CANVAS (la frame ; présentation), TRANSITION (la frame ; interpolation entre états calculés), APP (événements ; état et effets). Vous ne déclarez jamais un plan — il est inféré de ce que vous écrivez.
Pare-feu. La règle à sens unique : la présentation peut lire l'analyse ; l'analyse ne peut
jamais lire la présentation. La violer est [ErrFirewall].
Strate (a) / strate (b). Au sein d'une scène : la géométrie retenue (une fonction pure du modèle — déterministe, à l'intérieur de l'oracle de rejeu) et les cosmétiques par frame (glow, pulse — routés vers le compositeur de l'hôte, hors de l'oracle, et coupés du modèle par le pare-feu).
Stabilisation (settle) vs trajectoire. La stabilisation (settle) d'une transition — là où elle atterrit — est dans l'oracle ; sa trajectoire (comment elle y arrive) ne l'est pas. C'est pourquoi le mouvement réduit ne change rien d'important.
#Le plan application
Modèle. L'état borné et typé d'une application. Seuls des kinds bornés peuvent y figurer ; tout
le reste est [ErrState].
Message (msg). La seule façon pour quoi que ce soit d'ambiant d'entrer dans une application —
le temps, l'entrée, l'aléa, le réseau, les valeurs d'analyse. Tout est journalisé.
Journal. La liste ordonnée des messages. C'est l'unique source de vérité : le re-folder reconstruit le modèle bit à bit. L'undo tronque jusqu'à une borne, pas jusqu'au message précédent.
Commande (Cmd). Un effet sortant, sous forme de donnée inerte — un nom, une clé, un
score. Jamais une socket, un token ni un handle. L'hôte l'exécute, sous une capacité.
Abonnement (Sub). Une entrée déclarative, recalculée à partir de chaque modèle, portant le
constructeur dans lequel l'hôte enveloppera sa charge utile (OnTick(1000, Tick)).
Slotmap. Le pattern officiel pour une collection bornée à suppression stable : un vecteur
borné, une pierre tombale (na) au lieu d'un compactage, un masque live, un compteur, et une
double identité — un identifiant de domaine pour les messages et la persistance, un handle
slot/génération pour l'exécution.
Capacité. Une permission nommée (net:fetch, storage:own, chart:read). Refus par défaut ;
un script émet une requête, ne détient jamais la ressource ; une requête non accordée est une
erreur de compilation ([ErrCapDenied]).
#Compilation et exécution
Graphe (DAG). Ce qu'un programme est, une fois les noms résolus et les fonctions inlinées : un graphe de dataflow typé et acyclique. L'unité d'optimisation, d'ordonnancement, de planification mémoire et de vérification.
I6. Un nœud feuille associé à un noyau natif est byte-identique à ce noyau, warm-up compris.
I7. L'interpréteur et le module compilé produisent les mêmes octets — vérifié à chaque compilation, sur des données hostiles, en batch et en direct. Une divergence bloque la livraison.
Le gate. Le contrôle bloquant qui fait respecter I7 (et, ce faisant, la correction de l'optimiseur). Son oracle est l'interpréteur ; son candidat est l'instance même qui servira.
Routine épinglée. Une opération où deux implémentations pourraient diverger — une transcendante, une division décimale, un repli Unicode, une addition calendaire, un tirage aléatoire, un tri sur des valeurs absentes — implémentée une seule fois et partagée par l'interpréteur, le module et le serveur. Pas le même algorithme : le même code.
Validation de traduction. Vérifier le graphe optimisé contre le graphe non optimisé, bit à bit, à chaque compilation. C'est ce qui permet à l'optimiseur d'être agressif sans être digne de confiance.
Plan de vivacité (pic, pas somme). La passe mémoire : la durée de vie de chaque buffer est statiquement exacte, si bien que des durées de vie disjointes partagent un slot et que l'empreinte rapportée est le pic de ce qui est vivant simultanément. Le plan lui-même est une fonction déterministe du graphe — parce que l'oracle de valeurs est aveugle à l'agencement.
Ordre canonique. L'unique linéarisation topologique, obtenue en départageant chaque égalité par l'identité lexicale épinglée du nœud. Elle ancre le plan mémoire, l'indice de tirage aléatoire et les départages de l'optimiseur.
1 ≡ N. Un worker et plusieurs workers produisent des octets identiques. Prouvé à partir de la pureté du graphe et de l'ordre de réduction figé — et stress-testé avec une assignation adverse.
#Distribution
fluxpack. L'artefact distribué : le module compilé, le manifeste scellé, la provenance (hash de contenu, version du compilateur, clôture des dépendances) et la licence.
Manifeste scellé. La liste des capacités, dérivée par le compilateur à partir des requêtes de la source — jamais auto-déclarée. Il voyage avec le binaire et est inspectable avant l'installation.
Adressage par contenu. Une référence de dépendance est un hash de contenu exact, pas une plage de versions. Deux versions d'une bibliothèque sont deux unités coexistantes, si bien que le diamant de dépendances ne survient pas.
Gate de reconstruction. Recompiler la même source et le même lock, sur des machines différentes avec des nombres de threads différents, doit produire un module byte-identique.
#Codes d'erreur
| Code | Se déclenche quand |
|---|---|
[ErrDim] |
l'algèbre dimensionnelle n'a pas de règle, ou les tags divergent |
[ErrRepr] |
même dimension, tags de représentation différents, pas de conversion explicite |
[ErrCausal] |
un cycle sans délai d'unité, ou une lecture non causale |
[ErrTotal] |
une borne qui n'est pas une constante, ou qui dépasse le plafond |
[ErrTotalRec] / [ErrTotalType] |
un cycle dans le graphe d'appels / dans le graphe de références de types |
[ErrTotalMatch] |
un match qui ne couvre pas tous les cas |
[ErrFirewall] |
l'analyse lit une valeur de présentation non déterministe |
[ErrLen] |
deux longueurs de vecteur déclarées sont incompatibles — des capacités qui ne peuvent pas s'élargir |
[ErrField] |
un champ manquant ou inconnu |
[ErrArg] |
le kind d'un argument n'est pas admissible |
[ErrPlot] |
la valeur n'est pas présentable |
[ErrUnbound] |
un identifiant non lié ou un trou syntaxique |
[ErrState] |
un champ de Model non borné (plan APP) |
[ErrCapDenied] |
une requête pour une capacité que le manifeste n'accorde pas |
[ErrCapRevoked] |
une commande sous une capacité révoquée en cours de session |
[ErrSceneBudget] |
une scène dépasse son budget de draw-list, d'instances ou de GPU |
[WarnTop] |
une liaison non consommée atterrit sur ⊤ ou quantity |
[WarnBranchDim] |
des branches ou des séries co-tracées de dimensions différentes |
[WarnScale], [WarnAffine], [WarnLit], [WarnNaNChain], [WarnBoundsØ] |
voir Inférence |
#Constantes
| Nom | Valeur | Ce qu'elle borne |
|---|---|---|
N_max |
10 000 (navigateur) / 100 000 (serveur) | la fenêtre, période ou délai maximal d'un noyau |
maxNodes |
3 072 | la taille du graphe, jugée après optimisation |
N_active |
16 | scripts co-actifs fusionnés en un seul graphe |
maxBricksPerBar |
1 000 | boîtes qu'une barre de temps peut traverser dans une représentation pilotée par le prix |
#Voir aussi
- Kinds · Inférence — le système de types en entier.
- Les quatre plans — le vocabulaire des plans en contexte.
- Garanties — les invariants, énoncés pour un lecteur qui doit leur faire confiance.
- FAQ — les questions que ces définitions provoquent.
- Aperçu du FDK — les espaces de noms et le modèle de capacités.