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. 1 · Principe et périmètre
  2. 2 · La source JSON v1
  3. 3 · Trois relations indépendantes
  4. 4 · Constructions et slots
  5. 5 · Portées, symboles, imports
  6. 6 · Typage statique strict
  7. 7 · Diagnostics
  8. 8 · De la source aux résultats
  9. 9 · Simulation, débogage et transpilation
  10. 10 · Invariants et refus

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

ChampNatureConsé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 )

FunctionDeclaration Double(valeur: int) → intmodule Calculs · racine
Parameter valeur: intslot parameters[]
Blockslot body
ReturnStatementslot argument
BinaryExpression *type dérivé : int
Reference valeur→ référence symbolique
Literal 2 : int

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

kindCatégoriePropriétés et slots
VariableDeclarationdeclarationname, type obligatoires ; initializer? expression
ConstantDeclarationdeclarationname, type ; initializer requis
FunctionDeclarationdeclarationname, type (retour), entryPoint? booléen, generic? historique, typeParameters? ; parameters[], body Block
LambdaExpressionexpressiontype (retour), parameters[], body expression unique ; capture lexicale implicite
Parameterparametername, type ; uniquement dans parameters[]
TypeDeclarationdeclarationname, type ; alias transparent d'un primitif
Blockstatementstatements[] : déclarations ou instructions
ReturnStatementstatementargument? expression ; uniquement dans une fonction
PrintStatementstatementvalue expression non void
ForEachStatementstatementarray tableau, callback fonction (E) -> void ; parcourt une copie du tableau
IfStatementstatementtest booléen, consequent Block, alternate? Block
WhileStatementstatementtest booléen, body Block
ForStatementstatementinitializer?, test?, update?, body Block
BreakStatement, ContinueStatementstatementaucun slot ; boucle obligatoire, sans traverser une frontière de fonction
ExpressionStatementstatementexpression : la forme retenue pour un appel dont on ignore le résultat
AssignmentStatementstatementtarget Reference, ArrayAccess ou déréférencement *p, value expression
BinaryExpressionexpressionoperator, left, right
UnaryExpressionexpressionoperator, operand
Literalexpressiontype obligatoire et value strictement compatible
Referenceexpressionreferences.target
CallExpressionexpressioncallee expression, arguments[]
MathCallexpressionname (opération native), arguments[] typés selon l'opération
CastExpressionexpressiontype cible, argument expression
Holeholeincomplétude explicite ; diagnostic bloquant, représentation conservée
ArrayLiteralexpressionelementType, elements[] du même type
ArrayCreationexpressionelementType, size entier, initializer? de l'élément
ArrayAccessexpressionarray 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.

Le même modèle dans l'application actuelle : les modules Library et App et leur flèche de dépendance.
  1. Ce que la page appelle ReturnStatement s'affiche « Retour » ; le libellé est une vue, la sorte est une donnée.
  2. Un BinaryExpression : aucun properties.type dans la source, un type dérivé par l'analyse.
  3. Un ArrayLiteral qui déclare le type de ses éléments, pas le sien.
  4. Un ExpressionStatement portant un CallExpression : la référence qui relie les deux modules.
Le schéma à l'écran. Chaque ligne correspond à un enregistrement de 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.

TypeDomaineNotes
short intentier signé 16 bits : -32 768 à 32 767Le calcul interne entier utilise BigInt, jamais persisté dans la source ni exposé comme type.
short uintentier non signé 16 bits : 0 à 65 535
intentier signé 32 bits
uintentier non signé 32 bits
floatIEEE 754 binaire32Chaque résultat arithmétique est de nouveau arrondi en binaire32 ; les textes sont arrondis directement, sans double arrondi via binaire64.
doubleIEEE 754 binaire64Arrondi au plus proche, égalités vers le significande pair ; subnormaux autorisés, infinis et NaN refusés.
stringchaîne, vide autoriséAucune conversion en nombre, aucune concaténation, aucune comparaison ordonnée.
charun scalaire Unicode uniqueY compris hors BMP ; ni paire de scalaires, ni surrogate isolé.
booleantrue / falseJamais 0/1 ni une chaîne ; aucune arithmétique.
voidabsence de valeurAnnotation 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érateurOpérandesRésultat
+ - * /deux numériques du même typece même type ; division entière tronquée vers zéro, sans promotion
%entiers du même typereste associé à la troncation, signe du dividende ; division ou reste par zéro interdits
< <= > >=mêmes types numériquesboolean
== !=même primitif non voidboolean ; pas de comparaison de signatures de fonctions
and or, notbooléens uniquementboolean
+ unairenumériquetype conservé, bornes comprises
- unairenumérique signé uniquementinterdit pour short uint et uint
& unairevaleur stockable (référence ou élément de tableau)pointeur vers l'opérande ; simulation uniquement
* unairepointeurvaleur 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 :

1 · AST Structure, identifiants, parentés, module, marques d'entrée.ast.js
2 · Sémantique Portées, symboles, résolution des références, imports et exports, placements.semantics.js
3 · Types Typage statique, signatures dérivées, vérification des retours, constantes.typeAnalysis.js
Résultat ast, 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

InvariantEffet observable
Les fonctions du moteur ne modifient pas leurs entréesUn é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ésUne sélection, une référence ou une position reste valable après une modification.
Un nœud a au plus un parent syntaxiqueSYNTAX_MULTIPLE_PARENTS et SYNTAX_CYCLE sont des erreurs dures, pas des avertissements.
Les résultats dérivés sont des instantanés en lecture seuleLes panneaux d'analyse ne peuvent pas écrire dans le document par accident.
La conservation JSON prime sur la normalisationLe 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:ast la compile et dépose le PDF à côté.
  • Documentation visuelle — ces mêmes règles, telles qu'elles se voient dans l'application.