English
flux · événements une VM dataflow — la FVM plusieurs apps

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

Les quatre plans
Figure — les quatre plans et le pare-feu à sens unique.

#Parcours de lecture

#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

#Voir aussi

↑ contents

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 :

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 quatre plans
Figure — les quatre plans : le calcul à gauche, la présentation à droite, les applications en dessous, et le pare-feu à sens unique entre eux.

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 :

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 :

Les cinq lectures de la flèche
Figure — un token ; le mot-clé de tête, ou le kind attendu par la position, décide de la lecture.

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 :

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 :

#Comment lire cette documentation

Le livre (cette section) construit le modèle mental dans l'ordre :

  1. Qu'est-ce que Flux — ce chapitre.
  2. Piliers de conception — les sept garanties, pourquoi chacune existe, et ce que chacune vous apporte.
  3. Premiers pas — une première session guidée, d'une ligne à une petite application.
  4. Les quatre plans — ANALYSIS, CANVAS, TRANSITION et APP en profondeur, y compris le pare-feu et les valeurs en direct.

Autour du livre :

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

↑ contents

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.

Les sept piliers
Figure — sept piliers soutenant un seul ensemble de garanties : coût borné, historique immuable, octets identiques sur chaque moteur, présentation inférée, effets contenus, et un optimiseur que vous n'avez pas besoin de croire.

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

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 :

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

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

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.

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.

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.

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 :

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.

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

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 :

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

↑ contents

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 :

Ce que produit une ligne
Figure — une ligne de source ; le kind décide du reste.

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

↑ contents

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.

Les quatre plans
Figure — les quatre plans et le pare-feu à sens unique.

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 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 :

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

↑ contents

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 :

  1. (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 un match multi-ligne au niveau supérieur n'a besoin d'aucune virgule :

    fluxdef 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
    
  2. (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é : dans x = p.on, le on final 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.)

  3. (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, s et bars sont 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.50d ne peut jamais être mal lu, d seul 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 bars avec un seul espace — est accepté parce que bars y est une tête réservée et que la forme span se lit comme de la prose dans les positions every(…) et life:.

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 :

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 :

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, rect et dot sont 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 un IDENT est 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, range ou z empoisonnerait d'immenses pans du nommage ordinaire (tout rectangle a un range, tout record un open quelconque), 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

↑ contents

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

Vue en rail de la colonne vertébrale des expressions, de expr jusqu'à primary
Figure — la colonne vertébrale stratifiée des expressions : chaque niveau n'appelle que le suivant, plus serré.

#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 grammaticaleArrow { 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.

Les cinq lectures contextuelles de l'unique token flèche
Figure — un token flèche, cinq lectures, chacune sélectionnée par son contexte gardien.

# 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 :

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

L'échelle de précédence avec un exemple d'expression détaillé
Figure — l'échelle du plus lâche au plus serré, et comment une expression réaliste s'analyse le long de celle-ci.

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 < c n'a aucune lecture booléenne dans un langage dimensionnel (la première comparaison produit un signal, 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 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 :

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

↑ contents

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 :

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.

Deux systèmes alimentant un seul canal d'erreur
Figure — le treillis de coercition (A) et l'algèbre des opérateurs (B) ne se rencontrent que dans l'unique canal d'erreur ⊤ ; 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 − open aurait le kind price ⊔ 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 ema26 aurait 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 :

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 dimensionlit < {ratio, osc(lo,hi), signal, dir, depth} < num :

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²·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 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é :

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

#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é :

#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

Le treillis des kinds
Figure — le treillis des kinds : chaque sorte sous un unique ⊤ et au-dessus d'un unique ⊥ ; à l'intérieur de la sorte scalaire, la colonne vertébrale sans dimension monte jusqu'à 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 dimensionprice ⊀ num. Les kinds dimensionnés montent jusqu'à quantity, jamais jusqu'à num.

Pourquoi cette règle existe. Si price ≤ num tenait, alors signal ⊔ 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 un if mélangeant des choses incompatibles). En routant plutôt tous les kinds dimensionnés vers quantity, un mélange n'est jamais silencieux : en arithmétique il est refusé ([ErrDim]), et en réconciliation de branches il compile vers quantity et 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 :

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 :

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 :

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 :

  1. un tag de représentation numériquef64 (défaut) ou decimal(scale) (règle A11) ;
  2. un tag de représentation du temps sur la dimension Tmachine (défaut ; temps écoulé exact — duration) ou calendar (period, règle A10) ;
  3. 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.

Axes de tags orthogonaux sur un kind
Figure — les axes de tags autour d'un kind, et les deux régimes : les tags de représentation refusent de se joindre ([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 :

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 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 :

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

#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é :

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 :

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

↑ contents

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 :

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 :

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 acceptant close + 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] :

Deux raffinements font que cette règle couvre l'algèbre réelle des indicateurs :

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) / 2 se normaliserait en Σλ = 1 et passerait la vérification de types comme un price valide, 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 , 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 :

#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 :

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 ?) :

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, ⊤}) :

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 :

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 na pour ses premières barres. Si na > 70 s'é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. Forcer na à travers les comparaisons fait propager l'absence jusqu'au signal, où is_na / is_some la gèrent délibérément. La même discipline traverse tout le langage : match doit couvrir na, 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

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

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 :

  1. recv.f sans ( — un champ de record (bb.upper), règle [Proj] ;
  2. recv.f(f se résout en une fonction — un appel UFCS, désucré en f(recv, …) ;
  3. mod.f / T.Ctor où 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 recv contre le premier paramètre de la fonction exactement comme si vous aviez écrit l'appel direct — mais il achète la découvrabilité : après close., l'éditeur peut proposer précisément les fonctions dont le premier paramètre accepte un price, 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]) :

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 with signifie 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

#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 :

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

↑ contents

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 - close pourrait synthétiser level ou, par l'arête d'effacement avec perte, quantity. Les deux sont dérivables. Mais plot x lit le registre de présentation au kind : level donne un pane centré sur zéro, quantity donne 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 :

Kinds circulant de bas en haut à travers une expression
Figure — les kinds circulent de bas en haut ; 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²·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.

Du kind à la présentation
Figure — 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 :

  1. Overlay si et seulement si la valeur partage l'axe des prix. Sinon elle reçoit un pane.
  2. 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 sur quantity, vous obtenez [WarnBranchDim] et une suggestion de scinder le pane. (Un overlay price à côté d'un pane ratio n'est pas un mélange — ce sont deux panes, et cela n'avertit de rien.)
  3. Le kind final décide. level + price → price, donc l'expression se met en overlay.
  4. 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 que rsi marque conventionnellement 30 et 70 est une propriété de rsi.

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

#Voir aussi

↑ contents

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 :

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 :

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] décale un flux d'un pas ; window(x,4) fait glisser un vecteur de capacité 4, les deux remplis de na pendant le warm-up
Figure — le délai 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 :

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, un count là 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é sur quatre pas : chaque pas lit l'état précédent à travers un délai d'une unité et la barre courante, et émet une valeur définitive
Figure — 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 :

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 » :

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.

Deux lignes de temps : les barres de 5 minutes lisent la dernière unité de 1 heure clôturée ; l'heure en formation est grisée et jamais lue
Figure — 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 t du graphique, une série étrangère lit son pas le plus récent d'horodatage l'instant de t.

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 une duration de 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 :

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

↑ contents

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

↑ contents

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.63 en 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

↑ contents

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.

La boucle de l'application
Figure — tout ce qui est ambiant entre par la porte d'entrée sous forme de message ; tout ce qui sort part en donnée inerte que l'hôte interprète sous une capacité.

Une note sur les exemples. Plusieurs exemples ci-dessous portent un membre d'un bloc app — un update, une view, un subs — 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 dans app name { … } ; les déclarations de type et de def à 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 :

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é :

Le pattern slotmap
Figure — la suppression écrit une pierre tombale ; rien ne bouge jamais, si bien que chaque handle reste valide et le plan mémoire reste plat.

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 :

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é lit na à la place. Et au chargement, le nextId persisté 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înerait A ⤳ B(A) ⤳ C et 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 fixe do … 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 planschartView, 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êteemit 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é.

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 de messages
Figure — le journal est l'unique source de vérité : l'undo tronque jusqu'à une borne et re-fold ; un checkpoint garde cela peu coûteux.

Le journal n'est pas une liste plate de messages. Deux raffinements sont imposés par les vrais éditeurs :

#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 :

  1. 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é.
  2. 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 un migrate(old) -> Model total 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éralefold(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 ; 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 portrect(), 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 :

  1. 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é.
  2. Le plan APP lit l'analyse en lecture seule, à travers Sub OnSeries.
  3. 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.
  4. 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

↑ contents

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 :

#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 :

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 :

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

↑ contents

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

#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 dimensionlog(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
}

Une table est un enregistrement de colonnes
Figure — le slotmap du plan APP, transposé d'array-of-structs vers struct-of-arrays.

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 :

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 :

#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

↑ contents

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 placeEfficacité) : l'ergonomie d'une collection persistante avec la performance d'un tableau mutable — grâce aux contraintes, non pas malgré elles.

Les cinq kinds et le substrat unique
Figure — cinq kinds, trois visages, une arène bornée ; chaque ordre canonique est un ordre total sur la valeur de clé, jamais un ordre de hachage.

#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 :

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 :

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 toVecL'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 popFront vient 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-pression Latest(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 def ré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 :

  1. Plein ⇒ rejet et diagnostic. Jamais de croissance silencieuse. isFull vous laisse décider d'abord.
  2. Absent ⇒ na. Une clé manquante, un peek vide, un index hors plage — na, jamais une exception. na est conscient de na à travers le reste de l'algèbre, si bien qu'il se propage au lieu de piéger.
  3. Aucune opération de raccourcissement, jamais. Il n'y a pas de filter : une longueur dépendante des données briserait la borne. where et mask préservent la longueur et laissent na dans les trous, et l'itération est consciente de na, si bien que rien n'a besoin d'être compacté.
  4. 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 :

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é :

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 :

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 :

#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

↑ contents

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 :

#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 durationmeas[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

↑ contents

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 à 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

↑ contents

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 Unicodeune 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 API string qui signifie deux choses différentes. Donc str.* 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 f64 rend 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

Le pipeline de texte
Figure — les octets deviennent un arbre borné, l'hôte détient toute autorité, et seules les éditions validées entrent dans le journal.

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 filter qui 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 :

  1. 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.
  2. 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 comme DecodeError(field, reason) — jamais un na silencieux 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

↑ contents

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 dayOfWeek diffé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.

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 :

#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 un string. Il ne change pas x, 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 et sortBy sur 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 un string est 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 :

#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

↑ contents

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 Uint32Array et 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 :

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 :

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 :

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 libreurl(…), 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

↑ contents

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 d'une scène
Figure — la géométrie est identique octet pour octet ; le cosmétique est joli. Aucun des deux ne contamine l'autre.

#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 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. Un hex malformé produit na et 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 :

#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 purezé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 :

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.

La chaîne de la draw-list
Figure — trois classes de signaux, trois routes, et un coût par frame de zéro pour ceux qui bougent le plus.

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

↑ contents

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 scan causal qui folde les arrivées en barres closes, suivi de toSource, 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éeDropped(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. Sample perdrait silencieusement des ticks entre les edges d'horloge, et Latest(n) les perdrait sous une rafale. Ainsi le fold bars(tf) qui alimente la jonction vers l'analyse déclare back: 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 :

#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

↑ contents

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 string sans 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 un msg portant un nouveau handle ou des métadonnées — jamais du contenu.

La doctrine du resource-handle
Figure — la commande et le message traversent la membrane ; la ressource, jamais. Chaque famille de cette page est cette image avec des noms différents.

#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 :

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 :

  1. L'intention sort comme un Cmd inerte, sous une capabilité namespace:verb deny 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.
  2. Le résultat revient comme un msg journalisé, via le constructeur de complétion que la commande portait. Il n'y a pas de callback, pas de promise, pas d'await.
  3. 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.
  4. La révocation emprunte la membrane. Un grant retiré en cours de session écrit une borne CapRevoked dans 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:*

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.

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 params passé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

↑ contents

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'update est 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, Denied vous dit que le palier vous a refusé, QuotaExceeded vous dit la vérité sur le quota, et Conflict(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 :

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 :

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

↑ contents

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)

Le pipeline de compilation
Figure — un graphe, deux back-ends, un seul gate entre eux.

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 I7
Figure — le gate compare les deux moteurs sur des données adverses, en batch et en live, avant qu'un seul octet ne soit autorisé à sortir.

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

#Le chemin navigateur, tel qu'il est construit

Le thread principal n'importe jamais le compilateur. Il orchestre :

  1. Un script enregistré sert immédiatement, sur l'interpréteur — le chemin prouvé, sans aucune étape de compilation entre la frappe et l'affichage.
  2. 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.
  3. 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.nearestdé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, Date et Intl sont 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 :

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 :

  1. 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.
  2. 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 ».
  3. 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.
  4. 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 ppercentrank, kama ×2–3
Composites multi-étages — fisher, connorsRsi ×3.7–5
Composites lourds multi-sorties — stdErrorBands ×9–14

Combien plus vite le module tourne par rapport au même algorithme en TypeScript

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

↑ contents

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 :

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éei64 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 un na apparaî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.

Datapath columnar et état en anneau
Figure — les nœuds produisent des colonnes ; un kernel fenêtré met son historique en anneau, et la plupart des anneaux dérivent leur position du compteur de barres tandis que deux familles persistent un curseur.

Deux conséquences valent d'être nommées :

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 :

La carte de mémoire linéaire
Figure — le plan est l'allocation : min = max pages, et la croissance est impossible par construction.

[ 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 = max exacte, 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.

Pic, pas somme
Figure — deux buffers dont les intervalles ne se chevauchent pas partagent un slot ; l'empreinte rapportée est le pic de ce qui est simultanément vivant, jamais la somme de tout ce qui a été alloué.

La passe a trois étapes :

  1. 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).
  2. 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.
  3. 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

↑ contents

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 :

#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 :

  1. Bit-exacte en IEEE-754 f64 pour chaque entrée — y compris les chemins na, les zéros signés et les infinis. Aucune réassociation en virgule flottante, jamais.
  2. 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.
  3. 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.
  4. 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é.
  5. 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 −0 surgit 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 comme 0 — 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'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 en sum(x, p) ÷ p — inconditionnellement, et non parce qu'une division est moins chère. C'est une normalisation : elle rend un sma et un sum sur 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 :

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 passes sans table de règles — elles sont la machine.

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 :

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 :

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

↑ contents

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 :

Deux règles gardent cela honnête :

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 :

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

Ordonnancement à barrière sur les niveaux du graphe
Figure — au sein d'un niveau les nœuds sont indépendants, donc l'affectation des nœuds aux workers est inobservable et ne peut pas changer un octet.

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 :

#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 :

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

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 :

#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 :

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

↑ contents

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 :

  1. 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.
  2. depth/z est une coordonnée de première classe, pas une fonctionnalité 3-D.
  3. La séparation de plans et les descripteurs sont des cibles de compilation, pas des conventions.
  4. Les registres acceptent les entrées enregistrées par script dans la même forme que les natifs.

#Les cinq descripteurs

Les cinq descripteurs
Figure — quatre contrats lient le langage aux registres et à l'horloge ; un cinquième, cosmétique, pilote les transitions.

#① 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
}

Hooks de représentation
Figure — les six hooks et où chacun rencontre l'hôte : la classe est dérivée de la transform, pas de la primitive de rendu.

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)

#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é :

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

↑ contents

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 :

Le diamant de dépendances, dissous
Figure — deux versions d'une bibliothèque sont deux unités liées ; il n'y a aucune version à sélectionner, et donc aucun conflit à résoudre.

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 :

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 :

  1. 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.
  2. 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.
  3. 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.

Ce que contient un fluxpack
Figure — l'artefact porte tout ce dont un consommateur a besoin pour décider, et rien qu'un consommateur doit croire.

#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 ». open vous donne les deux. closed vous 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 :

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

↑ contents

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 grainspas (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 updateassert 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

↑ contents

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 :

#Voir aussi

↑ contents

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 :

#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

↑ contents

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.

Temps et état

#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 quatre plans

#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).

Compilateur et runtime

#É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, …).

Temps et état

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

net · compute

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

Les quatre plans

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

L'éditeur

#« 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

↑ contents

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

↑ contents