6.13 Créer votre propre langage en transpilant beaucoup... En LispE - naver/lispe GitHub Wiki

Création d'un nouveau langage de programmation

English Version

Tu en as assez des syntaxes toujours plus absconses que les nouveaux langages t'imposent. Tu rêves d'un langage épuré, simple, qui parle à ton moi intérieur, celui qui aime la méditation sous un olivier, les yeux fixés sur l'horizon lointain... Heu... Bon...

Il y a une réponse à ça... Tu peux créer ton propre langage ...

Oui, tu le peux... Et là tu regardes la littérature et tu te dis: «ptêtpafinaleman».

Parce que c'est plein de machins que personne n'utilise jamais, des concepts qui font peur juste à les prononcer à haute voix. L'arbre de la syntaxe abstraite, une grammaire BNF .

Alors frustré(e), tu abandonnes l'idée et tu reviens sur ce code Python qui plante parce que la variable toto qui contenait une liste à l'origine contient désormais un entier.

N'abandonne pas. Ce qu'il y a de merveilleux en informatique, c'est que les choses dont on rêve, on peut souvent les implanter...

Non... je déconne... Faut être honnête, la plupart des solutions simples sont souvent claquées au sol.

Sauf, celle que je vais présenter ici.

LispE

On va tout faire en LispE. LispE est un Lisp plutôt original qui est hébergé sur le GitHub de Naver, une compagnie qui offre en Corée tous les services rêvés sur Internet, du moteur de recherche au commerce en ligne.

La caractéristique principale de LispE est de reposer sur l'utilisation de tables plutôt que de listes chainées, comme c'est le cas traditionnellement. Cela a de nombreux avantages en particulier en matière d'accès à des objets au sein d'une liste via son index. Notons au passage, que les listes chainées font aussi partie du langage mais elles n'ont pas le rôle central des tables (ou vecteurs pour les puristes) qu'elles ont généralement dans les autres implantations.

L'article que vous lisez est d'ailleurs hébergé dans le Wiki du GitHub en question.

Important: Vous trouverez ici les versions pré-compilées pour Windows et Mac (M1 et Intel compris).

Pour Linux, je vous conseille de lire la page: compiler sur Linux

Allez, maintenant on va se créer notre petit langage à nous.

Notre langage

Si tu es arrivé(e) ici, c'est que tu veux quand même savoir.

Sache Oh! lect(eur)rice! que j'ai conçu quelqu'outil merveilleux dont la manipulation te plongera dans les affres d'une admiration sans borne.

Car LispE contient ce qu'il faut d'outillage pour réaliser un langage puissant et simple. Le nom m'est venu lors d'une transe à base d'herbes sacrées massilianes que les indigènes nomment PASSETICE. Ce langage je vais l'appeler: le Basique. Un nom simple et original qui porte en lui les prémisses d'une réminescence cos...

Tiens voilà un exemple de ce à quoi il ressemble:

function fact(a)
   if a <> 1 then
      a * fact(a-1)
   else
      1
   endif
endfunction

a = fact(10)
println a

Pas de «;» disgracieux, pas de «(...)» intempestif. Du code épuré, simple et puissant.

Evidemment, on pourrait créer une autre syntaxe, mais l'idée, c'est de se créer son premier langage à soi et autant qu'il soit simple, parce que sinon, ça pourrait être un poil compliqué.

Pour un exemple complet, je t'invite à regarder: Exemple.

Il contient l'ensemble des instructions pour exécuter l'exemple en Basique enregistré dans la variable code.

La grammaire BNF

Le rôle d'une grammaire, c'est de produire une description de notre nouveau langage sous une forme mathématique. Mais, une grammaire, c'est aussi un langage avec ses propres conventions, que nous allons maintenant décrire en détail.

Car il faut savoir que l'informatique n'est que cela: conventions, dont certaines ont été imaginées par de brillants esprits en lévitation au-dessus de Saturne.

Description de la grammaire

Oh! lect(eur)rice, regarde cette instruction: A = 10.

Nous allons écrire une règle pour en capter l'essence sacrée.

assignment := variable %= nombre

Voilà, c'est simple... On suppose que variable permet de reconnaitre un nom de variable et nombre, ben... un nombre. Le % sert à échapper l'opérateur =.

Sauf que nos variables, elles peuvent recevoir autre chose que des nombres non ?

Oui, bien sûr, dans ce cas on va séparer les valeurs possibles par un ^, appelé aussi opérateur de disjonction.

assignment := variable %= [calcul^variable^nombre^chaine]

On met la disjonction entre deux [..] pour indiquer qu'elle porte sur ces quatre éléments.

nombre, chaine et variable seront définis hors grammaire quand on va transpiler le tout... Car, je vous le dis, ça va transpiler beaucoup.

Mais calcul, hein!!!

On va écrire une autre règle, deux en fait:

opérateur := [%< %<]^[%> %>]^[%^ %^]^[%* %*]^%&^%|^%+^%-^%*^%/^%%^%^
calcul := [nombre^chaine] [opérateur [nombre^chaine]]+

Transpilons

Qu'est que la transpilation?

Tout d'abord, cela n'a rien à voir avec le fait de freiner brutalement au travers d'un train.

La transpilation consiste à transformer un code écrit dans un langage X en un code écrit dans un langage Y. Par exemple, un transpilateur peut prendre un code Java et le transformer en un code C++ équivalent. Même si dans ce cas, cela relève plus de la cryptographie.

Dans le cas qui nous intéresse, nous allons transformer notre grammaire en un programme LispE. Et ça va être plutôt simple et direct. En fait, chaque nom de règle va devenir le nom d'une fonction. Les disjonctions seront compilées sous la forme d'un or et les conjonctions sous la forme d'un and.

Reprenons, l'exemple précédent:

assignment := variable %= [calcul^variable^nombre^chaine]
opérateur := [%< %<]^[%> %>]^[%^ %^]^[%* %*]^%&^%|^%+^%-^%*^%/^%%^%^
calcul := [nombre^chaine] [opérateur [nombre^chaine]]+

La règle assignment va devenir une fonction LispE. Elle est composé d'une conjonction et d'une disjonction:

(defun C_assignment (tokens i0 v)
   (check (< (car i0) (size tokens))
      (setq v0 ())
      (if (and
            (setq i1 (clone i0))
            (setq v1 ())
            (C_variable tokens i1 v1)
            (compare tokens "=" i1 v1 nil)
            (or
               (C_calcul tokens i1 v1)
               (C_variable tokens i1 v1)
               (C_nombre tokens i1 v1)
               (C_chaine tokens i1 v1)
            )
            (set@ i0 0 (car i1))
            (setq v0 v1)
         )
         (push v (cons 'assignment v0)) ; retenez soigneusement cette ligne
      )
   )
)

On met un petit C_ devant le nom pour éviter les confusions avec de vraies instructions de LispE. Comme on le voit sur cet exemple, C_variable, C_compare, C_nombre et C_chaine peuvent parfaitement être définies en dehors de la grammaire. De cette façon, on peut moduler le fonctionnement de notre parseur de la façon la plus fine possible.

Tokens

La méthode ci-dessus s'attend à une liste de tokens (ou jetons en français). Et pour produire des jetons, on effectue une tokenization. La tokenization (Pour être franc, jetonisation, j'ai pas pu) consiste à découper une chaine en segments significatifs que l'on place dans une liste.

"A = 10" --> ("A" "=" "10")

Et en fait, cette opération, LispE vous l'offre gratuit: Il suffit d'utiliser tokenize_rules.

En revanche, son utilisation nécessite une pointe d'explication. La tokenization dans ce cas précis est faite avec un jeu de règles que vous pouvez d'ailleurs modifier.

; On récupère le jeu de règles par défaut
(setq tok (tokenizer_rules))

;Et maintenant on peut tokenizer
(setq tokens (tokenize_rules tok "A=10")) ; ("A" "=" "10")

Et maintenant qu'on a nos jetons, on peut facilement appliquer nos règles dessus:

; On récupère le jeu de règles par défaut
(setq tok (tokenizer_rules))

;Et maintenant on peut tokenizer
(setq tokens (tokenize_rules tok "A=10")) ; ("A" "=" "10")

(setq v ())
(C_assignment tokens '(0) v)

Et c'est là que le miracle s'opère. v contient désormais: (assignment (variable "A") (nombre 10)).

J'en vois certains qui ne partagent pas la divine surprise. Car ce qui est produit ici est un arbre. Un arbre de la syntaxe abstraite.

Allons plus loin, et montrons à ces sceptiques à quel point, cette solution est puissante.

Mais cette fois, nous allons utiliser notre véritable exemple, celui dont la grammaire est ici: Basique.

On va tenter d'analyser: if a < 10 then a = a + 1 endif:

(setq tokens (tokenize_rules tok "if a < 10 then a = a + 1 endif"))

; tokens: ("if" "a" "<" "10" "then" "a" "=" "a" "+" "1" "endif")

(setq v ())
(C_analyse tokens '(0) v)

v contient maintenant: 
(if 
   (comparison
    (variable "a")
    (comparator "<")
    (anumber 10)
   )
   (then
     (declaration
         (variable "a")
         (computing
           (variable "a")
           (operator "-")
           (anumber 1)
         )
     )
   )
)

Notons immédiatement un truc cool. Le nombre d'éléments dans les sous-listes est défini par la grammaire. Ça parait évident dit comme ça, mais c'est abzolument fondamental. Car, l'étape suivante va consister à transpiler cet arbre en un code LispE.

Et là, la lumière s'allumera à tous les étages.

Premier niveau de transpilation

En passant, le programme qui permet de transpiler la grammaire est situé ici: compilateur.

Il prend en entrée la grammaire et il génère le fichier basic.lisp qui contient le code transpilé.

Grosso modo, la grammaire est tokenizée ligne par ligne, et on génère pour chacune de ces lignes une fonction LispE équivalente.

Certaines fonctions comme: compare, C_anumber, C_Word, C_astring sont prédéfinies en dehors de la grammaire. Elles permettent de reconnaître certains types de token comme un nombre ou une chaine de caractères par exemple.

Motif à donf

Nous utilisons massivement la notion de programmation par motif (pattern programming) pour pouvoir effectuer cette transformation.

En effet, nous nous déplaçons token par token dans une liste et nous cherchons certains cas de figure particuliers, comme la présence de l'opérateur de disjonction. Par défaut, la règle est transformée en une structure (and..) et les disjonctions en structures (or..).

Instructions dans les conditions

Ce qu'il y a de bath avec LispE, c'est qu'on peut parfaitement initialiser une variable dans un and ou dans un or. Ça simplifie grandement la génération de code.

Les variable i0 et v sont pour la première l'index du token courant dans tokens et l'autre la variable de réception des sous-arbres déjà reconnus. Dans le cas d'une structure and, on crée des variables locales qui conservent les différents sous-arbres reconnus. Si on va jusqu'au bout du and, on recopie alors ces sous-arbres dans la variable principale, sinon on sort sans rien changer. De cette façon, on peut tester plusieurs chemins d'analyse sans problème.

Notons aussi que l'utilisation des crochets [..] se traduit par une structure (and...) supplémentaire, dont le but est de garantir que la séquence des éléments sera correctement reconnu. (voir le code de: comparator pour un chouette example. Il y a même la règle d'origine dans le fichier.)

Notons finalement que lorsqu'une règle contient l'un des opérateurs de Kleene ?+*, nous transformons cette partie de la règle en une fonction particulière commençant respectivement par O_, P_ ou S_, O pour optionnel, P pour plus et S pour star.

Evidemment, cette compilation n'a pas besoin d'être répétée chaque fois que vous voulez compiler un programme en Basique. Une fois basic.lisp produit, vous pouvez l'utiliser autant de fois que vous voulez.

Là où nous dévoilons au lecteur un secret de l'Univers

Sinon, en passant, voilà à quoi ressemble l'Arbre de la Syntaxe Abstraite pour notre fonction: fact, une fois passée à la moulinette de notre grammaire sous forme Lispiène.

(function
   "fact"
   (variables (variable "a"))
   (if (comparison
         (variable "a")
         (comparator "<" ">")
         (anumber 1)
      )
      (then
         (computing
            (variable "a")
            (operator "*")
            (call
               "fact"
               (computing
                  (variable "a")
                  (operator "-")
                  (anumber 1)
               )
            )
         )
      )
      (else (anumber 1))
   )
)

Comme on le voit sur le résultat, cet arbre commence à ressembler à un programme Lisp. On retrouve des parenthèses dans tous les coins, et ça commence à transpirer les appels préfixés.

«Ok! Tu es en train de nous vendre du Lisp, ça va on a compris, j'ai des trucs super importants à terminer. Je me casse.»

Et là, le regard lointain et le coeur plein de pitié, je t'arrête et je te dis: «Ma soeur ou mon frère, n'aie point de courroux. Si l'ASA ressemble à du Lisp, c'est parce que Lisp exprime son code directement sous la forme d'un Arbre de la Syntaxe Abstraite. Car au début du code était l'ASA et le Lisp en est l'expression la plus pure.»

Ode à Lisp

Voilà Oh! lect(eur)rice! Je viens de te dévoiler le secret le mieux garder de l'informatique. Lisp, c'est le langage dont la compilation est la plus simple, parce que en Lisp tout est arbre préfixé. Il y aussi Forth qui est dans le même cas, mais lui, il te la fait à l'envers.

Autrement dit, là où les autres langages souffrent d'un grave déficit de végétation, LispE lui est végane depuis l'origine. Là où les autres langages sont obligés de passer par du code de ouf pour produire l'ASA dont on a besoin pour la suite des émissions, en Lisp, c'est toi qui écris cet arbre. Avec Lisp, tu te confonds avec le compilateur. TU ES LE COMPILATEUR.

Les parenthèses sont l'ultime incarnation de la pureté du code. Elles dégagent le code de sa gangue binaire pour en exposer la structure arborescente.

Transpilation, le retour

Oui, nous avons désormais produit un ASA: l'arbre de la syntaxe abstraite.

Mais cela ne suffit pas, il faut maintenant produire le code Lisp correspondant.

Grosso modo, là où on a: A=10+20, on voudrait produire: (setq A (+ 10 20)).

Et là où on a:

function fact(a)
   if a <> 1 then
      a * fact(a-1)
   else
      1
   endif
endfunction

Ce serait pas mal, si on avait:

(defun fact(a)
  (if (neq a 1)
      (* a (fact (- a 1)))
      1
   )
)

Parce que ce code là, LispE sait comment l'exécuter.

Programmation par motif

Autant tout à l'heure, j'ai un peu exagéré sur l'importance de la programmation par motif, autant là, c'est vraiment fondamental.

Vous avez vu la gueule de l'ASA au-dessus. J'ai mis de belles indentations dans ce qui est de toute façon une liste.

Et cette liste, elle contient des tas de mots clefs dont on va se servir. Des mots clefs qui sont systématiquement en position 0 de nos sous-listes.

Vous suivez?

Bon je vous montre sur un bout d'arbre:

(computing
   (variable "a")
   (operator "-")
   (anumber 1)
)

Vous les voyez les mots clefs: computing, variable, operator et anumber.

Ils sont systématiquement en première position, parce qu'évidemment, c'est comme ça qu'ils ont été construit par basic.lisp.

Rappelez-vous la ligne dans le code de C_assignment:

(push v (cons 'assignment v0)) ; retenez soigneusement cette ligne

v0 c'est le sous-arbre produit par C_assignment, et on le range dans notre structure finale en lui poussant assignment en tête.

Et tout le code dans basic.lisp est construit comme ça.

Weight Watcher

Ainsi, chaque élément de la règle devrait produire un et un seul élément dans v0, à la position qui lui correspond.

assignment := variable %= [calcul^variable^nombre^chaine]

Dans le cas ci-dessus, il devrait donc y avoir un élément pour la variable initiale, un pour le signe = et un élément possible parmi calcul, variable, nombre ou chaine.

Mais, si on reprend l'ASA de: A=10, on retrouve seulement 2 éléments:

(assignment
    (variable "A")
    (anumber 10)
)

Pas de panique, il y a des flags dans la grammaire pour éviter de surcharger l'arbre avec des infos inutiles. Par exemple, le signe = est totalement redondant, donc inutile, pour le reste de l'analyse. Il a servi à identifier une affectation, mais pour la suite, on peut s'en passer.

Ça permet d'éviter de se retrouver avec un arbre boursoufflé, avec de l'info qu'il faudra en plus analyser lors de la transpilation, le retour.

# Le ! devant assignment indique que les opérateurs et chaines peuvent être ignorés
!assignment := variable %= nombre

Les fonctions par motif

Donc, on sait maintenant plusieurs choses dont on va se servir.

  • C'est une structure arborescente que l'on va parcourir de façon récursive
  • Les listes commencent par un mot clef
  • Les règles ont été compilées de telle façon que l'on connait le nombre d'éléments attendus à l'avance

Notons que pour le nombre d'éléments, la présence des opérateurs de Kleene peut parfois rendre les choses un peu plus compliqué.

Tout d'abord, le transpilateur spécial que nous allons écrire dépends de façon étroite de la grammaire que nous avons écrit.

Autant, le compilateur peut prendre n'importe quelle grammaire en entrée pour générer le programme adéquat, autant le transpilateur lui dépend des mots clefs particuliers de la grammaire pour générer le bon code.

Si vous pensiez que cela allait être simple, désolé.

Pour Basique, nous avons déjà écrit le programme correspondant: transpilateur.

On voit tout de suite la présence d'une fonction: parsing définie avec le mot clef defpat pour définition de pattern.

La programmation par motif consiste à proposer plusieurs configurations possibles pour une liste donnée. On laisse ensuite l'interpréteur décider quelle fonction choisir.

Voici par exemple les différentes possibilités présentes dans notre transpilateur:

(defpat parsing ( ['assignment $ d] )...)
(defpat parsing ( ['indexes $ d] )...)
(defpat parsing ( ['dim  n $ d] )...)
(defpat parsing ( ['dimstring  n $ d] )...)
(defpat parsing ( ['dimvariablestring n  $ d] )...)
(defpat parsing ( ['dimvariable n  $ d] )...)
(defpat parsing ( ['variable d] )...)
(defpat parsing ( ['stringvariable $ d] )...)
(defpat parsing ( ['minus d] )...)
(defpat parsing ( ['anumber d] )...)
(defpat parsing ( ['string d] )...)
(defpat parsing ( ['comparator $ d] )...)
(defpat parsing ( ['method n m $ arguments] )...)
(defpat parsing ( ['call n $ arguments] )...)
(defpat parsing ( ['function  name parameters  $ code] )...)
(defpat parsing ( ['forin init rg $ code] )...)
(defpat parsing ( ['for init test inc $ code] )...)
(defpat parsing ( ['while test $ code] )...)
(defpat parsing ( ['then $ code])...)
(defpat parsing ( ['else $ code])...)
(defpat parsing ( ['if test then $ else] )...)
(defpat parsing ( ['computing $ reste] )...)
(defpat parsing ( ['comparison a1 op a2 $ d] )...)
(defpat parsing ( ['multiop x $ arguments] )...)
(defpat parsing ( ['parenthetic $ code] )...)

; Sauter une étiquette
(defpat parsing ( [ [atom_ x] $ d] )...)

; Fonctions de repli
(defpat parsing ( [ x $ d] )...)
(defpat parsing ( x )...)
(defpat parsing ( () )...)

Evidemment nous n'avons donné ici que les entêtes des fonctions.

Quelques détails au passage, important pour la compréhension du système.

  • Chaque paramètre décrit une liste composée d'un certain nombre d'éléments, dont on donne pour certain la valeur en dur.
  • Le $ est utilisé ici comme opérateur de découpage de fin de liste. Autrement dit, après le $, on range le reste de la liste dans la variable qui suit.
  • Le premier élément de chacune de ces listes provient directement des têtes de règle de la grammaire. Comme on l'a expliqué plus haut, on s'est arrangé pour que cette étiquette occupe systématiquement cette première place.

Il faut savoir de plus que chacun de ces patterns est indexé sur son premier élément. Ainsi, le premier élément de la liste en argument permet d'exécuter directement cette fonction. Si plusieurs fonctions sont indexées sur le même élément, elles seront testées dans l'ordre dans lequel elles ont été déclarées.

; Si l'on exécute: 
(parsing '(if ...))

; On saute directement à la règle suivante grâce à l'indexation sur le "if":
(defpat parsing ( ['if test then $ else] )...))

Les trois dernières fonctions présentent un paramètre trop général pour permettre l'indexation. Elles nous servent de fonctions de repli si aucune autre fonction n'a pu s'appliquer.

Notons au passage la règle qui permet de sauter une étiquette. L'idée c'est que la grammaire peut construire des sous-arbres sans que forcément ceux-ci ait une véritable interprétation sémantique. Par exemple, lorsqu'une combinaison est particulièrement lourde ou redondante, on peut créer une entrée dans la grammaire pour éviter de se trimbaler un truc trop lourd.

Par exemple, dans la grammaire, on peut admirer la règle callitem dont l'utilité d'un point de vue sémantique est on ne peut plus limité. C'est juste une grosse disjonction qui aurait fait tache dans les autres règles du fait de sa taille.

Le déroulé

On va maintenant passer à l'exécution du code et on va montrer comment un bout de code Basique peut devenir du LispE.

Pour ça, j'ai déjà préparé un bout de code qui devrait vous éclairer: Exemple.

La variable code conserve en son sein un bout de code en Basique. On va appeler la fonction transpile qui va nous transpiler ce code en LispE.

(defun transpile (code)
   ; Transpiler en LispE
   (setq tree (abstract_tree code))

   ; __root__ est une fonction spéciale qui définit le bloc principal d'un programme LispE
   (setq code '(__root__))
   (push code '(trace true))

   (ife (in (car tree) "Error")
      (setq code (list 'println (join tree " ")))
      ; Ici a on boucle élément par élément et on appelle parsing
      ; On range le résultat dans code
      (loop line (cdr tree)
         (setq c (parsing line))
         (push code c)
      )
   )
   code
)

Comme on le voit, c'est plutôt carré. On appelle la fameuse fonction abstract_tree qu'on a créé avec le compilateur.

Elle va automatiquement prendre notre code et nous générer une liste qui contient l'Arbre de la Syntaxe Abstraite.

Cet arbre on va alors lui appliquer élément par élément la fonction parsing.

Prenons un exemple:

Supposons qu'on veuille analyser fact.

Nous produisons son arbre avec abstract_tree.

(function
   "fact"
   (variables (variable "a"))
   (if (comparison
         (variable "a")
         (comparator "<" ">")
         (anumber 1)
      )
      (then
         (computing
            (variable "a")
            (operator "*")
            (call
               "fact"
               (computing
                  (variable "a")
                  (operator "-")
                  (anumber 1)
               )
            )
         )
      )
      (else (anumber 1))
   )
)

La présence du mot clef function en tête de notre liste va automatiquement déclencher la fonction suivante:

(defpat parsing ( ['function name parameters  $ code] )...)

Ce qui va nous permettre de donner une valeur à name, à parameters et à code:

name: "fact"
parameters: (variables (variable "a"))
code: (if (comparison...))

Et là, on peut commencer à apprécier la simplicité de ce type de programmation.

Voici le corps de parsing pour le mot clef: 'function

(defpat parsing ( ['function  name parameters  $ code] )
   (setq code (maplist 'parsing code false))   
   (nconcn (list 'defun (atom name) (map 'parsing (cdr parameters))) code)
)

On applique parsing sur chaque élément de code, ce qui nous permet de produire le corps de notre fonction. Ce qui revient à faire:

(parsing '(if ...))

Il n'y a qu'un seul élément dans ce cas.

Mais chaque élément dans le if va à son tour être analysé avec un appel de parsing. De cette façon, chaque élément, quelque soit sa profondeur dans l'arbre reçoit le même type d'analyse, assurant à l'ensemble cohérence et équilibre.

Voici une version simple de la façon dont on peut analyser le if.

J'ai mis la règle au-dessus de façon à bien mettre en lumière le choix du motif dans la fonction.

; !if := $If comparison then else? $EndIf
; Comme on a mis un ! devant le if, tous les mots clefs: 
; IF et ENDIF sont sautés dans la génération de l'arbre.
; Pas besoin de s'en préoccuper dans le motif.

(defpat parsing ( ['if test then $ else] )
   (setq test (parsing test))
   (setq then (parsing then))
   (if else
       ; après le $, on place le résultat dans une liste d'où l'appel à car pour le else
       (list 'if test then (parsing (car else))) 
       (list 'if test then)
   )   
)

Un if est composé de deux ou trois sections: test, then et else.

Chacune de ces sections est analysée avec un appel récursif à parsing.

; Then... On regarde la taille de liste pour introduire un block
(defpat parsing ( ['then $ code])
   (if (eq 1 (size code))
      (parsing code)
      (consb 'block (maplist 'parsing code false))
   )
)

; Else...
(defpat parsing ( ['else $ code])
   (if (eq 1 (size code))
      (parsing code)
      (consb 'block (maplist 'parsing code false))
   )
)

On fusionne ensuite les différentes listes ainsi obtenues pour renvoyer le résultat final.

On applique la même chose aux parameters.

Il suffit ensuite de fusionner (nconcn) toutes ces listes pour générer notre fonction, que l'on renvoie comme résultat final.

Cas particuliers

Ok, je l'avoue, j'ai un peu simplifié les fonctions ci-dessus, mais pas tant que ça si vous examinez le vrai code. Par exemple, j'ai rajouté dans le langage Basique, la possibilité de manipuler des variables dont les dimensions sont plutôt compliquées: dim compliqué[5][5][5].

Si vous jetez un coup d'oeil au code, vous verrez que je les implémente comme une liste plate composée de 5*5*5 éléments (125). Dans le reste du code, on accède au contenu de compliqué via l'opérateur atshape qui a besoin de la liste '(5 5 5) comme argument.

Quand on lit le code, ça ne saute pas forcément au visage...

De la même façon, on peut déclarer une liste en exécutant: Data 10,20,30,"A","B" EndData.

Histoire de disposer d'un moyen de créer ces listes à la mode de chez nous.

Exemple de code

Pour avoir des exemples du code qu'on peut écrire en Basique, je vous invite à regarder: exemple, vous aurez une image de l'ensemble du formalisme qu'on a défini pour ce langage.

Et Python...

On va être franc, la méthode de la grammaire ne peut pas s'appliquer aux langages indentés. Certains vont penser que je dis ça par pure méchanceté.

En fait, même pas, la raison principale, c'est que les grammaires peuvent difficilement intégrer la notion d'espaces variables dont on se sert pour distinguer les blocs en Python.

Malgré tout, il existe une solution qui consiste à insérer des étiquettes fictives à des endroits stratégiques. Et ça tombe bien, j'ai écrit un programme LispE qui fait exactement ça: indentation python

Il vous prend un programme Python et il vous génère:

def Loss(a, X,Y):
     s = []
     for x in X:
         s.append(sum([w * e for (w,e) in zip(a,x)]))
     end#
     return sum([(yy - e)**2 for (yy,e) in zip(Y,s)])
end#

Il vous rajoute alors un end# pour indiquer où un bloc se termine. Il devient alors possible d'écrire une grammaire pour transpiler du code Python en LispE.

Je vous laisse écrire la grammaire, si ça vous amuse.

Conclusion

Cet article a un objectif très simple, celui de montrer les étapes nécessaires pour créer un langage. Nous avons choisi LispE comme référence parce que cette famille de langages (les lisp) est particulièrement adaptée à ce genre de travail. Rappelons que nombre de langages dans le passé, en particulier les langages objets, ont commencé leur carrière sous la forme d'une implantation Lisp.

Ce qui est sympa avec cette méthode, c'est qu'il suffit de modifier la grammaire un poil, et rapidement vous pouvez vous faire votre langage à votre sauce. Par exemple, j'ai créé une version en français de Basique qui s'appelle basicois. Il vous suffit d'appliquer le compilateur dessus et hop! vous avez un parseur d'un langage en français. (En fait, c'est déjà disponible: basicois.lisp et aussi basicois_example.lisp).

Et là, fact devient:

fonction fact(a)
   si a <> 1 alors
      a * fact(a-1)
   sinon
      1
   finsi
finfonction

Evidemment, vous pouvez vous faire votre version en grec ou en allemand, si le coeur vous en dit. En plus, vous n'avez rien à modifier dans le transpilateur si vous conservez les mêmes têtes de règle. Il est même possible de mettre des caractères accentués si besoin.

La grammaire contient même un mécanisme pour surcharger certain noms de LispE avec de nouveaux noms:

; La règle suivante est une règle de surcharge
; Elle est compilée sous la forme d'appel à link
; @affiche := print

; Désormais affiche sera interprété comme un appel à print
(link "affiche" 'print)

Ce n'est pas la façon la plus définitive de créer de nouveaux langages, bien sûr, mais c'est un moyen agréable de jouer avec différents formalismes. Vous pouvez facilement créer de nouveaux langages mieux adaptés à des publics spécifiques, avec une syntaxe plus flexible par exemple.

LispE est assez riche en termes de fonctionnalités (voir le Manuel). De nombreux aspects de la programmation moderne, comme la programmation fonctionnelle ou la programmation par tableaux, font partie de l'expérience.

Il est clair que la syntaxe de Lisp peut s'avérer trop absconse pour certaines personnes, en revanche, avec cette méthode, rien ne vous empêche de construire et créer des langages qui sauront exploiter au mieux les fonctionnalités de ce langage.

⚠️ **GitHub.com Fallback** ⚠️