La syntaxe de surface : déclarer une notation, dans les deux sens
Tes données ont souvent déjà une notation : un DSL métier, un format d'échange, un langage de modélisation. Fix te laisse déclarer cette notation — et la déclaration marche dans les deux sens : le même programme lit le texte vers l'AST et écrit l'AST vers le texte.
L'AST commande, la syntaxe suit
Le principe tient en une phrase : la structure d'un langage vit dans son type (les constructeurs, leur récursivité) ; la syntaxe ne fait que donner une apparence à chaque constructeur. On déclare donc le type d'abord, puis on l'habille :
% module(er) = le langage er COMPLET : son AST (les types) ET sa syntaxe
module(er, []).
% l'AST d'abord. Vocabulaire de TYPES fixe → FERMÉ ; NOMS libres → OUVERTS.
type list(T) = nil | cons(T, list(T)). % liste générique (type paramétré)
type atype = string | int. % FERMÉ : les types du DSL
type attr_t = attr(aname, atype). % aname OUVERT (nom) · atype fermé
type ent_t = ent(ename, list(attr_t)). % ename OUVERT · liste = list(attr_t)
% la syntaxe ensuite — une apparence par constructeur
ent(N, L) := "entity" N "fields" L "end".
attr(N, T) := "attr" N T.
end_module.
Une clause de syntaxe se lit : « dans le langage er, le constructeur ent(N, L) s'écrit entity, puis le nom, puis fields, puis la liste, puis end ». Les guillemets sont des mots-clés ; les variables sont les trous, remplis récursivement selon leur type.
Remarque ce qui manque : aucune règle de grammaire pour « une liste d'attributs », aucune répétition, aucun choix. La récursion vient du type (attrs_t = nil | cons(...)), pas de la syntaxe. Déclarer un langage = déclarer un type + l'habiller.
Plusieurs vues d'un même AST
La syntaxe est déclarée par langage — et le langage, c'est le module : les clauses := d'un module(er, …) produisent syntax(er, …). Le même constructeur peut donc porter plusieurs notations, une par module — deux vues du même arbre :
module(er, []).
attr(N, T) := "attr" N T. % vue ER : attr name string
end_module.
module(uml, []).
attr(N, T) := "field" N T. % vue UML : field name string
end_module.
C'est ce qui rend la transformation de modèles si courte : la règle transforme des AST, et chaque côté garde sa notation.
classe(cls(N, L)) :- modele(ent(N, L)). % ER → UML : la transformation, une ligne
Texte ER → AST ent → règle → AST cls → texte UML. La notation d'entrée et celle de sortie sont des déclarations, pas du code.
Programme complet et exécutable :
examples/er-to-uml.fix.
Le sucre pour les listes délimitées
Le cas courant « liste entre crochets avec séparateur » a un raccourci, sequence — qui se désucre vers le type cons/nil, il n'ajoute rien au mécanisme :
syntax(er, cons, sequence("[", ",", "]", cons, nil)). % [ a , b , c ]
syntax(uml, cons, sequence("[", ";", "]", cons, nil)). % [ a ; b ; c ]
Même liste, deux séparateurs — un par langage.
L'indentation signifiante : les blocs offside
Une liste n'est pas obligée d'être délimitée. Le même type-liste peut s'habiller en bloc indenté (façon Python/Haskell), avec le mot-clé block :
type item_t = num(int).
type items_t = nil | cons(item_t, items_t).
module(blk, []).
num(N) := N.
items_t := block.
end_module.$ fix block-layout.fix
Fix v3.1.0 — 20 faits, 0 règles → fixpoint : 20 faits
:= block est un sucre du star (le même mécanisme que sequence, GF2) : block(C, Items) := star( nl space(C), Item ), où le séparateur délimité est remplacé par un saut de ligne et une colonne C. Sur le terme cons(1, cons(2, cons(3, nil))), := block rend trois lignes indentées d'une unité (deux espaces) sous leur ancre — au lieu de [ 1, 2, 3 ] :
1
2
3
À la lecture, la règle offside tranche la fin du bloc : le premier token qui revient à gauche de la colonne du bloc le ferme. Pas de délimiteurs — la colonne fait le travail des crochets.
Deux points de doctrine :
- L'AST reste pur. Aucun champ de mise en page dans le terme : le layout vit à 100 % dans la syntaxe (même doctrine que le reste de la page). Le terme lu depuis une vue bloc est identique à celui lu depuis une vue délimitée — c'est prouvé (round-trip, ci-dessous), pas seulement énoncé.
- Multi-vues, encore. Le mode bloc est déclaré par langage — ce n'est pas un réglage global, c'est une apparence par vue, comme les séparateurs de
sequence.
Programme complet et chargeable :
examples/block-layout.fix. Provenance et preuve : spec41b-layout(validée) — round-trip R1/R2 prouvés et signés Paul ; témoin bout en bout par le chemin RÉEL du chargement (serialisation::tde::tests::block_layout_charge_et_roundtrip_bout_en_bout).
Bidirectionnel par construction
Il n'y a qu'une déclaration par constructeur, et elle sert aux deux sens :
- lire : le texte
entity person fields [ attr name string ] enddevient le termeent(person, cons(attr(name, string), nil)); - écrire : le même terme, rendu dans le langage
uml, redonneclass person members [ field name string ] end.
Pas de parseur à écrire, pas d'imprimeur à maintenir en miroir : c'est la même clause, parcourue dans un sens ou dans l'autre. Ce qui est lu puis écrit retombe sur ses pieds (aller-retour exact).
La frontière hôte
Le texte lui-même — la chaîne de caractères — n'est pas un terme Fix. Les ponts lire(texte, langage) et ecrire(terme, langage) vivent à la frontière hôte (l'application qui embarque Fix) : la logique manipule des termes, l'hôte manipule des chaînes. Dans le REPL tu travailles donc côté termes (charge le programme, interroge les AST) ; la conversion texte↔terme est l'affaire de l'application.
Les bords honnêtes
- Une syntaxe n'invente jamais de structure : répétition, récursion, choix viennent du type. Décrire une structure dans la syntaxe est une faute — elle appartient au
type. - La notation doit rester déterministe : à une position donnée, le premier mot doit suffire à choisir le constructeur. C'est ce qui garantit l'aller-retour exact.
- L'indentation signifiante (
:= block, ci-dessus) est un usage avancé — commence par les formes délimitées.
À côté
- Comprendre : le TDE — la transformation de modèles, dont la syntaxe de surface est la porte d'entrée et de sortie.
- Référence : déclarations propres à Fix — la forme exacte.