Xalg / Fix / La syntaxe de surface : déclarer une notation, dans les deux sens
Navigation
Apprendre
Recettes
Référence
Comprendre
Bonnes pratiques
En pratique
Explications

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 :

Programme complet et chargeable : examples/block-layout.fix. Provenance et preuve : spec 41b-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 :

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


À côté