Modèle logique · format v1
Ce que Sapphire 3 stocke vraiment
Sous les cartes, il y a un arbre. Pas un arbre d'images ni un arbre de widgets : un arbre syntaxique typé, sérialisé en JSON, indépendant de tout langage cible, validé par un moteur qui ignore l'existence de React, du DOM et de la fenêtre. Cette page décrit ce modèle, ses règles et ses refus.
Documents de référence
ARCHITECTURE_AST.pdf— le document technique complet, huit sections, généré depuis le LaTeX.ARCHITECTURE_AST.tex— la source LaTeX de ce document, relisible et modifiable telle quelle.- Transpilation — le périmètre exact de JavaScript, Python et du C expérimental.
- Dans le dépôt :
docs/LOGIC_API.md(contrats du moteur),docs/WORKSPACE_FILES.md(enveloppe.sp3),src/utils/logic/(l'implémentation).
1 · Principe et périmètre
L'interface est une vue sur un document. Un module est une portée ; une carte est la représentation d'un module ; une ligne d'arbre est la représentation d'un nœud. Supprimer la carte ne supprime pas l'AST, et deux interfaces différentes pourraient lire la même source.
Indépendance des cibles
Le modèle ne connaît ni point-virgule, ni mot-clé d'un langage hôte, ni signature de compilation. Il connaît des déclarations, des instructions, des expressions, des types et des références. Les langages cibles arrivent après, par des backend enregistrés.
Le moteur est pur
src/utils/logic/ n'importe ni React, ni DOM, ni Neutralinojs. Les fonctions ne modifient
pas leurs entrées ; elles renvoient de nouvelles sources et des résultats dérivés. Il est testé seul,
par une suite de tests Node et Python sans dépendance de test.
Périmètre volontairement borné
Classes avec membres public/private, constructeur, instances, new, this
et types de classe. Pas d'héritage, de polymorphisme, de structures, de sous-modules, de surcharge ni d'inférence de
secours. La généricité accepte plusieurs paramètres de type et des contraintes exactes, sans
génériques locaux ni tableaux imbriqués. Une
classe est simulée localement, mais sa transpilation est refusée explicitement.
2 · La source JSON v1
Un projet logique tient en quatre collections : une version, un identifiant, des modules, des nœuds
dans un tableau global à plat. Les listes rootNodeIds et les slots ordonnés portent l'ordre
syntaxique ; l'ordre du tableau nodes n'a aucune signification.
Le projet et un module
{
"formatVersion": 1,
"id": "projet-demo",
"modules": [
{
"id": "mod-affichage",
"name": "Affichage",
"kind": "module",
"rootNodeIds": ["n-principal"],
"imports": [
{ "id": "imp-double",
"moduleId": "mod-calculs",
"nodeId": "n-double",
"localName": "Double" }
],
"exports": []
}
],
"nodes": [ /* tous les nœuds, à plat */ ]
}
Un nœud
{
"id": "n-double",
"moduleId": "mod-calculs",
"kind": "FunctionDeclaration",
"properties": {
"name": "Double",
"type": { "kind": "primitive", "name": "int" },
"entryPoint": false
},
"children": {
"parameters": ["n-param"],
"body": "n-corps"
},
"references": {}
}
properties et references sont toujours des objets ;
les slots optionnels acceptent null ou l'absence de clé. Une liste vide est valide ; une
liste requise absente est diagnostiquée.
Ce qui n'est pas dans cette source
L'AST analysé, les diagnostics, l'index des symboles, les caches et toute la présentation sont recalculés, jamais enregistrés avec le modèle logique. La mise en page vit dans une enveloppe distincte, qui emballe la source et y ajoute les cartes :
{
"application": "Sapphire 3",
"workspaceVersion": 1,
"project": { "formatVersion": 1, "id": "…", "modules": [], "nodes": [] },
"presentation": {
"cards": [],
"camera": { "x": 0, "y": 0, "zoomLevel": 0 },
"selected": null
}
}
Une carte module partage l'identifiant et le nom de son module source — exactement une carte
par module. Les cartes comment et image n'existent que dans
presentation : elles ne passent jamais dans project, jamais dans l'AST,
jamais dans les commentaires du code généré. Une image est intégrée en Data URL ; les URL distantes et
les chemins locaux sont refusés avant remplacement. Le lecteur accepte aussi une source logique v1 seule,
sans présentation : il crée alors une disposition déterministe, tout replié, vue à l'origine.
3 · Trois relations indépendantes
C'est le point le plus facile à confondre, donc le plus explicitement séparé.
| Champ | Nature | Conséquence |
|---|---|---|
node.moduleId |
Propriété logique | Le module qui possède le nœud. Les identifiants de nœuds sont uniques dans le projet ; un enfant ne traverse pas la frontière d'un module. |
node.children |
Imbrication syntaxique | La parenté réelle de l'arbre. Un nœud a au plus un parent syntaxique ou une entrée racine. C'est
cette relation, et elle seule, que contains décrit dans les graphes. |
node.references.target |
Lien symbolique | Une lecture ou une écriture d'un nom. Elle ne déplace ni ne réplique la déclaration, ne crée pas de parenté, et peut viser une exportation d'un autre module. |
Une référence se donne par {kind:'name', name}, {kind:'node', nodeId} ou
{kind:'import', importId}. Une cible par identifiant reste soumise à l'accessibilité
lexicale ; un import peut être visé par son identifiant indépendamment de son alias local. Les
relations reads, writes, calls et type des vues
d'analyse proviennent de cette seule relation symbolique, résolue par l'analyse sémantique.
Le même graphe vu comme arbre et comme référence
Parenté syntaxique ( children )
Double(valeur: int) → intmodule Calculs · racine
valeur: intslot parameters[]body
argument
*type dérivé : int
valeur→ référence symbolique2 : intRéférence symbolique ( references )
La ligne Reference ci-dessus ne contient aucun enfant : elle porte
references.target vers le nœud Parameter. Dans le module Affichage, une
autre référence vise l'import de Double, qui pointe lui-même vers le nœud
Double du module Calculs. Le paramètre et l'appel restent dans des modules
différents : rien n'est copié, rien n'est remonté. C'est cette référence, une fois résolue, qui
produit la flèche entre les deux cartes.
4 · Constructions et slots
Vingt-huit sortes de nœuds, toutes décrites dans un seul objet figé, nodeSchema. Ce schéma
pilote la validation, le typage et les propositions du menu d'ajout : il n'existe aucune liste
d'actions codée en dur ailleurs.
kind | Catégorie | Propriétés et slots |
|---|---|---|
VariableDeclaration | declaration | name, type obligatoires ; initializer? expression |
ConstantDeclaration | declaration | name, type ; initializer requis |
FunctionDeclaration | declaration | name, type (retour), entryPoint? booléen, generic? historique, typeParameters? ; parameters[], body Block |
LambdaExpression | expression | type (retour), parameters[], body expression unique ; capture lexicale implicite |
Parameter | parameter | name, type ; uniquement dans parameters[] |
TypeDeclaration | declaration | name, type ; alias transparent d'un primitif |
Block | statement | statements[] : déclarations ou instructions |
ReturnStatement | statement | argument? expression ; uniquement dans une fonction |
PrintStatement | statement | value expression non void |
ForEachStatement | statement | array tableau, callback fonction (E) -> void ; parcourt une copie du tableau |
IfStatement | statement | test booléen, consequent Block, alternate? Block |
WhileStatement | statement | test booléen, body Block |
ForStatement | statement | initializer?, test?, update?, body Block |
BreakStatement, ContinueStatement | statement | aucun slot ; boucle obligatoire, sans traverser une frontière de fonction |
ExpressionStatement | statement | expression : la forme retenue pour un appel dont on ignore le résultat |
AssignmentStatement | statement | target Reference, ArrayAccess ou déréférencement *p, value expression |
BinaryExpression | expression | operator, left, right |
UnaryExpression | expression | operator, operand |
Literal | expression | type obligatoire et value strictement compatible |
Reference | expression | references.target |
CallExpression | expression | callee expression, arguments[] |
MathCall | expression | name (opération native), arguments[] typés selon l'opération |
CastExpression | expression | type cible, argument expression |
Hole | hole | incomplétude explicite ; diagnostic bloquant, représentation conservée |
ArrayLiteral | expression | elementType, elements[] du même type |
ArrayCreation | expression | elementType, size entier, initializer? de l'élément |
ArrayAccess | expression | array tableau, index entier ; résultat = type de l'élément |
Le schéma porte aussi, pour chaque construction, un group stable (variable,
type, function, parameter, condition,
loop, return, block, statement,
operator, value, array, incomplete). Ce champ sert à
l'interface seulement — couleur de bordure des lignes d'arbre et regroupement des propositions du menu. Il
n'intervient ni dans la validation ni dans le typage, et reste distinct de category, qui elle,
détermine la compatibilité syntaxique d'un slot.
declaresType marque les constructions autorisées à porter une annotation ;
declaresElementType celles qui portent un type d'élément de tableau. Une annotation posée sur
un opérateur ou une référence est une erreur structurelle : elle ne peut pas forcer un type.
- Ce que la page appelle
ReturnStatements'affiche « Retour » ; le libellé est une vue, la sorte est une donnée. - Un
BinaryExpression: aucunproperties.typedans la source, un type dérivé par l'analyse. - Un
ArrayLiteralqui déclare le type de ses éléments, pas le sien. - Un
ExpressionStatementportant unCallExpression: la référence qui relie les deux modules.
nodes ; aucune ne représente la présentation.5 · Portées, symboles, imports
- Une portée par module ; une par fonction, partagée avec ses paramètres et son bloc principal ;
une par bloc imbriqué et par
for. - Un espace de noms commun aux valeurs et aux types. Les déclarations d'une portée sont collectées avant résolution : pas de temporal dead zone, pas d'analyse d'ordre d'initialisation à ce stade.
- Un nom peut masquer une déclaration d'une portée externe. Un doublon dans la même portée est une erreur, et la première déclaration reste choisie pour permettre l'inspection.
- Export :
{nodeId, name}, uniquement pour une déclaration racine. Import :{id, moduleId, nodeId, localName}, uniquement vers un nœud explicitement exporté d'un autre module. - Les imports peuvent être cycliques : aucune exécution ni analyse de cycle d'initialisation n'est engagée. Pas de réexport, pas d'import étoilé, pas de résolution de chemin physique.
Le lien visuel entre deux cartes est une agrégation de ces références résolues :
getModuleLinks(index) renvoie, par couple de modules, une seule direction de l'exportateur
vers le récepteur, avec la liste des exportations réellement utilisées et leur nombre
d'utilisations. Un import jamais référencé ne produit aucun lien. La fonction est pure.
Le point d'entrée
Un projet valide contient zéro ou une seule fonction portant properties.entryPoint: true.
Absent ou false, la fonction est ordinaire, même si elle s'appelle main. Un
marqueur non booléen, ou posé sur une autre construction, donne ENTRY_POINT_INVALID ; un
second marqueur donne ENTRY_POINT_DUPLICATE, localisé sur son nœud et sur le champ, y compris
dans des fragments détachés. Les données invalides restent inspectables et sérialisables : aucune
suppression automatique, mais canGenerate devient faux. Aucune exécution implicite n'est
attachée à ce marqueur ; le simulateur l'exige simplement sans paramètres.
6 · Typage statique strict
Un seul catalogue, primitives.js. Les bornes sont celles du langage logique, pas d'un langage
hôte. Aucune conversion implicite, même sans perte de précision.
| Type | Domaine | Notes |
|---|---|---|
short int | entier signé 16 bits : -32 768 à 32 767 | Le calcul interne entier utilise BigInt, jamais persisté dans la source ni exposé comme type. |
short uint | entier non signé 16 bits : 0 à 65 535 | |
int | entier signé 32 bits | |
uint | entier non signé 32 bits | |
float | IEEE 754 binaire32 | Chaque résultat arithmétique est de nouveau arrondi en binaire32 ; les textes sont arrondis directement, sans double arrondi via binaire64. |
double | IEEE 754 binaire64 | Arrondi au plus proche, égalités vers le significande pair ; subnormaux autorisés, infinis et NaN refusés. |
string | chaîne, vide autorisé | Aucune conversion en nombre, aucune concaténation, aucune comparaison ordonnée. |
char | un scalaire Unicode unique | Y compris hors BMP ; ni paire de scalaires, ni surrogate isolé. |
boolean | true / false | Jamais 0/1 ni une chaîne ; aucune arithmétique. |
void | absence de valeur | Annotation de retour de fonction uniquement, et nulle part ailleurs. |
number, any et unknown sont rejetés comme types logiques. Toutes les
déclarations, tous les paramètres et tous les littéraux portent un type explicite ; une annotation
absente ou invalide est diagnostiquée et ne produit aucun type de secours. Une déclaration ne récupère
jamais le type de son initialiseur.
Trois types construits s'annotent en plus sur une variable, une constante, un paramètre ou le retour d'une
fonction : le pointeur {kind:'pointer', pointee} vers un primitif non
void, la signature de fonction
{kind:'function', parameters, returnType} et le tableau générique en retour.
& prend l'adresse d'une valeur stockable ; * relit la valeur pointée et
peut servir de cible d'affectation, la simulation écrivant alors dans l'emplacement d'origine ; les
pointeurs sont transpilés en JavaScript, Python et C expérimental. Une fonction nommée est une valeur de première classe :
une Reference vers une fonction porte sa signature et peut être stockée, transmise puis
appelée. Une lambda anonyme (LambdaExpression) possède des paramètres typés et une
expression de résultat, capture lexicalement les variables visibles et peut être stockée, passée en
argument ou appelée immédiatement. Une fonction peut retourner une signature concrète, ce qui permet de
renvoyer une fermeture. Chaque type construit reste limité à ses emplacements autorisés, sans conversion
implicite.
Généricité avancée
Une fonction peut déclarer jusqu'à huit paramètres de type nommés — T, U,
Element… — dont le nom suit [A-Z][A-Za-z0-9]*. Chacun peut porter une
contrainte exacte optionnelle (un type concret valide). Le paramètre s'utilise sur le retour et les
paramètres de sa fonction, directement ou comme élément de tableau générique (T[]). L'inférence
confronte chaque argument à son motif, lie un type réel, exige le même type pour toutes les occurrences,
vérifie la contrainte et substitue le retour. T*, T[][], les génériques imbriqués,
les variables/constantes génériques et l'usage de la fonction générique comme valeur de première classe
restent refusés. Aucune spécialisation n'est créée : la simulation exécute la même fonction et les
transpileurs émettent une fonction ordinaire, avec les types concrets publiés dans
semantic.genericCalls.
Tableaux
Un tableau est un type construit {kind:'array', elementType}, annotable sur une variable, une
constante ou un paramètre. Un retour de fonction ne peut être un tableau que si son élément est un paramètre
de type générique ; un alias ou une conversion tableau reste refusé (TYPE_INVALID).
L'élément est un primitif non void, ou un paramètre de type dans une signature générique ;
les tableaux imbriqués restent refusés. Trois constructions portent
properties.elementType : ArrayLiteral, ArrayCreation,
ArrayAccess. Une case jamais remplie ne reçoit aucune valeur inventée : sa lecture échoue
comme une variable sans initialiseur, et les éléments d'une constante ne sont pas modifiables.
Opérateurs
| Opérateur | Opérandes | Résultat |
|---|---|---|
+ - * / | deux numériques du même type | ce même type ; division entière tronquée vers zéro, sans promotion |
% | entiers du même type | reste associé à la troncation, signe du dividende ; division ou reste par zéro interdits |
< <= > >= | mêmes types numériques | boolean |
== != | même primitif non void | boolean ; pas de comparaison de signatures de fonctions |
and or, not | booléens uniquement | boolean |
+ unaire | numérique | type conservé, bornes comprises |
- unaire | numérique signé uniquement | interdit pour short uint et uint |
& unaire | valeur stockable (référence ou élément de tableau) | pointeur vers l'opérande ; simulation uniquement |
* unaire | pointeur | valeur pointée ; cible d'affectation possible (écriture à travers le pointeur) |
Les conditions de if, while et for exigent boolean. Le
nombre et les types des arguments d'un appel doivent correspondre exactement à la signature ; un appel
invalide n'obtient pas le type de retour comme secours. Une fonction non void doit retourner la
bonne valeur sur tous les chemins démontrés par un vérificateur simple — retour direct, séquence de blocs,
deux branches d'un if ; les boucles ne prouvent rien seules, et les cas non prouvés sont
refusés conservativement, sans exécuter aucune condition.
Conversions explicites
CastExpression porte le type cible et son argument. Les conversions entre les six primitifs
numériques sont permises, ainsi que l'identité d'un primitif non vide ; pas de conversion chaîne/nombre,
booléen/nombre ou char/string. Vers un entier : troncature vers zéro puis
contrôle exact du domaine — une négative ne passe pas en non signé, une réduction de largeur ne reboucle
pas. Vers un flottant : la perte de précision est acceptée parce que demandée, mais dépassement et
sous-flux vers zéro restent des erreurs.
7 · Diagnostics
Un diagnostic est un enregistrement sérialisable, jamais une exception : il peut être affiché, conservé dans un fichier, comparé dans un test.
{
"code": "TYPE_MISMATCH",
"severity": "error",
"message": "Les types doivent être strictement identiques ; une conversion explicite est nécessaire.",
"location": { "projectId": "…", "moduleId": "…", "nodeId": "…", "field": "children.right" }
}
severity vaut error, warning ou info ; seul
error bloque génération et simulation. Les identifiants inapplicables valent null.
Les messages techniques sont en français ; l'interface traduit le code, sans jamais analyser le
texte du message, ce qui garde le moteur indépendant de la langue affichée.
Structure et identité
SOURCE_INVALID, FORMAT_VERSION_UNSUPPORTED, ID_INVALID,
ID_DUPLICATE, MODULE_INVALID, NODE_NOT_FOUND,
STRUCTURE_INVALID, NODE_KIND_UNKNOWN, NODE_INCOMPLETE,
SYNTAX_CYCLE, SYNTAX_MULTIPLE_PARENTS, NODE_DETACHED,
DEPTH_EXCEEDED, PLACEMENT_ILLEGAL
Sens et interfaces
REFERENCE_UNRESOLVED, DECLARATION_CONFLICT, IMPORT_INVALID,
EXPORT_INVALID, ENTRY_POINT_INVALID, ENTRY_POINT_DUPLICATE
Types et nombres
TYPE_MISMATCH, TYPE_REQUIRED, TYPE_INVALID,
EXPRESSION_UNTYPED, VOID_NOT_ALLOWED, TYPE_CYCLE,
CONSTANT_CYCLE, LITERAL_INVALID, NUMERIC_OVERFLOW,
NUMERIC_UNDERFLOW, DIVISION_BY_ZERO, RETURN_MISSING,
JSON_INVALID
Une source incomplète reste représentable : un littéral sans valeur, un appel partiel, un retour sans
argument, un Hole sont acceptés comme document, signalés comme bloquants pour la production,
et en aucun cas supprimés pour « faire propre ». La séparation est volontaire : le droit
d'inspecter un programme invalide ne doit pas dépendre du droit de produire du code.
8 · De la source aux résultats
analyzeProgram(source) enchaîne trois passes pures et cumulatives, puis décide :
ast.jssemantics.jstypeAnalysis.jsast, semantic, diagnostics, index, et canGenerate = aucun diagnostic bloquant.Les vues dérivées consomment ce résultat sans jamais le recomposer à leur façon : le graphe de contrôle et le graphe de dépendances réutilisent les références déjà résolues, l'atteignabilité repart de l'initialisation globale et du point d'entrée en suivant les cibles d'appels (imports et récursion comprise), et l'agrégation des liens entre modules lit le même index.
Une analyse est conservatrice par conception : pas de preuve générale sur les valeurs des variables, les comparaisons numériques, les dépendances globales ou la terminaison des appels. Une condition inconnue garde tous les chemins possibles. Non parcouru lors d'une simulation ne veut pas dire inatteignable.
9 · Simulation, débogage et transpilation
Simuler
simulateProgram(project, { maxSteps = 10000 }) : exécution locale déterministe, sur
action explicite uniquement. Elle exige une analyse sans diagnostic bloquant et une fonction marquée
point d'entrée sans paramètre. Elle rend ok, output (lignes de texte),
trace (identifiants source dans l'ordre), puis le résultat et le nombre de pas, ou un code
d'erreur stable. Budget de pas et profondeur d'appel bornés ; sortie et trace restent en mémoire.
Aucune saisie interactive, aucun effet natif, aucun réseau.
Déboguer
createDebugSession(project) prépare avec le même interpréteur des états bornés, puis
moveDebugSession les parcourt : suivant entrant dans les appels, sortie de portée, retour au
point d'arrêt précédent et redémarrage. Les points d'arrêt visuels restent en mémoire de session ; un
saut s'y arrête. Memory space présente code et fonctions, données globales et pile d'appels avec des
adresses illustratives, sans allocation réelle. La simulation graphique simplifiée est une option
explicite de Run : elle anime les fonctions atteignables, leurs appels directs, la pile, les variables et
la sortie, sans persistance ni code mort ; les appels indirects via une variable de type fonction n'y
sont pas tracés.
Transpiler
transpileProgram(project, { target }) : réutilise analyzeProgram() puis
délègue à un backend enregistré. JavaScript, Python et un C expérimental sont opérationnels ; C++ et
Java restent indisponibles. Le C est volontairement restreint aux primitives, booléens, chaînes, fonctions,
contrôle, pointeurs, pointeurs de fonction nommés, lambdas sans capture et Print ; tableaux,
classes, lambdas capturantes, ForEach, opérations natives et conversions y sont refusés. Les déclarations de modules et de fonctions sont écrites
avant main(), l'initialisation globale y est exécutée, puis le point d'entrée désigné est
appelé. Le backend exige exactement un point d'entrée, ne modifie jamais la source et refuse avec des
diagnostics stables plutôt que d'émettre du code incertain. Le détail par cible est sur
la page Transpilation.
Un même calcul arithmétique sert les chemins de génération : les opérations numériques embarquées dans le code généré proviennent de la même fabrique que celles du simulateur — BigInt pour les entiers, arrondi binaire32, conversions, dépassements, sous-flux et division par zéro identiques. Le fichier produit n'embarque pas les budgets du simulateur et n'est jamais exécuté automatiquement par l'application.
Opérations natives SP3
PrintStatement ajoute la représentation textuelle d'une valeur à la sortie. MathCall
désigne une opération du registre pur mathNatives.js — mathématiques, texte et longueur de
tableau — typée sans conversion implicite. La simulation les exécute localement, avec des codes stables pour un
résultat non fini ou des bornes Unicode dépassées. JavaScript et Python les transpilent avec l'équivalent natif
le plus direct et de petits helpers lorsque la sémantique SP3 l'exige ; le C expérimental les refuse
encore. Aucun effet système, aucun réseau.
10 · Invariants et refus
| Invariant | Effet observable |
|---|---|
| Les fonctions du moteur ne modifient pas leurs entrées | Un échec d'insertion renvoie exactement la même source ; annuler et rétablir ne peuvent pas corrompre un document. |
| Les identifiants viennent de l'appelant et ne sont jamais régénérés | Une sélection, une référence ou une position reste valable après une modification. |
| Un nœud a au plus un parent syntaxique | SYNTAX_MULTIPLE_PARENTS et SYNTAX_CYCLE sont des erreurs dures, pas des avertissements. |
| Les résultats dérivés sont des instantanés en lecture seule | Les panneaux d'analyse ne peuvent pas écrire dans le document par accident. |
| La conservation JSON prime sur la normalisation | Le format source v1 conserve les champs inconnus et diagnostique une version future. Les workspaces versions 1 et 2 sont migrés vers la version 3 à la lecture : document interne et images liées dans un dossier choisi. Une version future est refusée avant remplacement. |
Ce modèle est la partie stable du projet. L'interface, les cartes, les thèmes et les panneaux changent plus vite que lui : c'est exactement le but. Une capacité nouvelle qui n'arrive pas à s'écrire dans ces enregistrements-là n'est pas une fonctionnalité à brancher sur un affichage, c'est une extension du modèle à décider, discuter et documenter d'abord.
Aller plus loin
- ARCHITECTURE_AST.pdf — la version longue, avec le raisonnement de conception et l'exemple à deux modules développé.
- ARCHITECTURE_AST.tex — la source LaTeX ;
pnpm docs:astla compile et dépose le PDF à côté. - Documentation visuelle — ces mêmes règles, telles qu'elles se voient dans l'application.