Précompilation

1) La précompilation

A partir du langage Go, qui améliore sensiblement le langage C tout en restant de bas niveau c'est à dire proche du langage machine produisant toujours du code exécutable optimal pour la rapidité d'exécution, on se propose de concevoir une précompilation dont l'objectif est d'augmenter le pouvoir d'expression du langage en intégrant de nouveaux paradigmes de programmation. Le langage ainsi étendu s'appelle en toute logique Go+, et nous allons le construire petit à petit, par étapes successives, chacune donnant lieu à une version du langage Go+ et à une version du précompilateur go+.

La première fonctionnalité que l'on souhaite mettre en oeuvre est la possibilité de manipuler des variables vectorielles en float64 et d'appliquer à ces variables une série d'opérations unaires et binaires prédéfinies. Pour cela on définie un nouveau type float64^3.

Lorsque le précompilateur rencontre ce type, il démultiplira les variables associées à ce type en chacune `3` variables numérotés de `1` à `3`. Ainsi par exemple l'expression suivante, qu'elle soit au sein d'une structure ou non :

`x,v`  float64^3

sera transformée lors de la précompilation en l'expression suivante :

`x_1, x_2, x_3, v_1, v_2, v_3`  float64

Le précompilateur devra reconnaitre les opérateurs `x, v` et devra reconnaitre tous les opérateurs unaires `-, ||` et binaires `-, +, **, "/", •`, susceptibles de s'appliquer à eux avec un niveau de priorité prédéfini par défaut et tout en respectant d'éventuels jeux de parenthèses. Par soucis de simplification, on associe à toute variable de même nom le même type. Et ainsi, lorsque le précompilateur rencontre une expression du genre :

`s"."x = p"."x"/"6 + q"."v"/sqrt"(|v|) - x**("sqrt"(v • x)+1)`

il la remplace par :

`"tmp"_1 = "sqrt"(v_1**v_1 + v_2**v_2 + v_3**v_3)`
`"tmp"_2 = "sqrt"(v_1**x_1 + v_2**x_2 + v_3**x_3)+1`

`s"."x_1 = p"."x_1"/"6 + q"."v_1"/tmp"_1 - x_1**"tmp"_2`
`s"."x_2 = p"."x_2"/"6 + q"."v_2"/tmp"_1 - x_2**"tmp"_2`
`s"."x_3 = p"."x_3"/"6 + q"."v_3"/tmp"_1 - x_3**"tmp"_2`

`"tmp"_1` et `"tmp"_2` sont préalablement déclarés comme étant des float64.

La seconde fonctionnalité consistera à appliquer cette méthode pour des types vectoriels en bigfloat.

Dans les deux cas, les expressions sont analysée par le précompilateur comme des arbres que l'on parcourt. Et il est alors nécessaire d'intégrer la syntaxe du langage Go pour une partie assez importante de ses expressions. Et c'est là une chose difficile et fastidieuse qui peut finalement nous amener à proposer non pas une précompilation mais finalement une compilation d'un nouveau langage qui produirait du code en Go.

La question devient plus générale : Comment programmer un moteur capable d'analyser une expression écrite dans un langage d'opérateurs. Ce moteur constitue l'élément d'entrée essentiel d'un interpréteur ou compilateur. Et il s'appuit sur deux colonnes ; une base de données sémantiques qui gouvernera le typage des opérateurs, et une base de données syntaxique qui gouvernera la forme syntaxique des opérateurs.

2) La sémantique et l'inférence de type

On se place dans un cadre plus générale et formel. On pose par exemples des types `B, ZZ, F, E` qui sont en faite des ensembles, respectivement ; `B` l'ensemble des booléens, `ZZ` l'ensemble des entiers relatifs, `F` l'ensemble des nombres à virgule avec telle précision et `E` un espace vectoriel. Et on pose une liste d'opérateurs pouvant posséder le même nom mais de définition d'ensemble de départ et d'arrivé différente. Par exemple :

`0 -> B`
`1 -> B`
`!B -> B`
`B "et" B -> B`
`B "ou" B -> B`
`B "==" B -> B`
`B->ZZ`
`0 -> ZZ`
`1 -> ZZ`
`- ZZ -> ZZ`
`ZZ + ZZ -> ZZ`
`ZZ - ZZ -> ZZ`
`ZZ ** ZZ -> ZZ`
`ZZ "^" ZZ -> ZZ`
`ZZ "==" ZZ -> B`
`ZZ <= ZZ ->B`
`ZZ < ZZ->B`
`ZZ >= ZZ->B`
`ZZ >ZZ->B`
`ZZ->F`
odd`(ZZ)->B`
`0 -> F`
`1 -> F`
`- F -> F`
`F + F -> F`
`F - F -> F`
`F ** F -> F`
`F "/" F -> F`
`F "^" F -> F`
`F "==" F -> B`
`F <= F ->B`
`F < F->B`
`F >= F->B`
`F >F->B`
`"exp"(F)->F`
`"trunc"(F)->ZZ`
`0 -> E`
`-E -> E`
`E + E -> E`
`E - E -> E`
`E • E -> E`
`E ** F -> E`
`F ** E -> E`
`E "/" F -> E`
`E"=="E -> B`

Les opérateurs et constantes sont contextualisées. C'est à dire qu'un même nom d'opérateur ou de constante peut désigner différents opérateurs et constantes à condition qu'ils soient de types différents. C'est le typage qui déterminera quelles sont les opérateurs et constantes en question.

A l'aide de cette liste de déclaration, l'interpréteur pourra analyser une formule en typant les opérateurs et les variables sans type, par un mécanisme d'inférence de type.

Si le type d'une variable reste indéfinie ou valable pour plusieurs types différents, c'est que le programme est inachevé. Pour compléter le programme il faudra simplement entrer le type voulu ou choisir un type par défaut ou selon une priorité. Par contre si un opérateur ou une variable se voit attribuer par inférence, 2 types différents, dans un premier temps on exclura ce cas comme constituant une erreur de syntaxe.

3) La syntaxes des opérateurs

Puisque tout est opérateur, il convient de définir précisement ce que peut être la syntaxe d'un opérateur :

Arité
Syntaxe
Exemple
0
Chaine
abc
abc
0
Caract
+
+
1
Gauche
-
-x
1
Droit
!
x!
1
Enveloppant
<
>
<x>
2
Gauche
f
(
,
)
f(x,y)
2
Droit
f
<
,
>
<x,y>f
2
Central
+
x+y
2
Enveloppant
<
,
>
<x,y>
3
Gauche
f
(
,
,
)
f(x,y,z)
3
Droit
f
<
:
!
>
<x:y!z>f
3
Central
:
:
x:y:z
3
Enveloppant
<
,
,
,
>
<x,y,z>

Et il convient de définir les règles de séparation d'opérateurs :

  1. Par des caractères blancs.
  2. Par certaines alternances de types de caractère.
  3. Selon la liste des opérateurs déjà définis.

4) Méthode de développement

Ce que l'on recherche dans un langage de programmation :

1- On veut pouvoir écrire un programme rapidement et sans effort. C'est pourquoi le code sera court, les règles seront génériques et simples, et le par défaut répondra déjà à toutes les questions.

2- L'équation que le programme doit mettre en oeuvre doit être transcrite aussi fidelement que possible dans le programme afin de pouvoir la vérifier directement comme on vérifie une copie. C'est pourquoi on se donne autant de souplesse dans la notation des opérateurs.

3- Le développement ne doit pas être ingras pour motiver les personnes qui construise le compilateur. C'est pourquoi le développement doit se faire par étape et à chaque étape une version du compilateur est produite.

5) Les éléments opérateurs

tout est opérateur, et les éléments eux-mêmes peuvent constituer des opérateurs. Les opérateurs ont donc un type et peuvent être le résultat d'une expression.

Comme dans le langage alpha, le résultat d'une expression peut constituer un opérateur et être appliqué, selon une syntaxe prédéfinie par le type, à d'autres expressions.

il convient de définir précisement le type de ces éléments opérateurs. On pose par exemples des types `F1, F2,` qui sont en faite des ensembles, respectivement ; `F1` l'ensemble des fonctions de `E->E`, `F2` l'ensemble des fonctions de `E×E->E`

`F1(E) -> E`
`F2(E,E)-> E`

Et il convient de définir précisement la syntaxe de ces éléments opérateurs pour chaque type :

Arité
Syntaxe
 
Exemple
1
Gauche
F1 [
]
expr[x]
1
Enveloppant Gauche
F1
<
,
>
<expr, x>
2
Gauche
F2
f
(
,
)
expr(x,y)
2
Droit
F2
f
<
,
>
<x,y>expr
2
Central
F2
+
x expr y
2
Enveloppant Gauche
F2
<
,
>
<expr,x,y>

6) Syntaxe du langage

Tout est opérateur, même les déclarations dans un programme. Il convient donc de définir précisement ce que peut être la syntaxe des autres opérateurs spécifiques de programmation, afin de parachever la syntaxe du langage.

C'est une autre façon de présenter la syntaxe du langage, autrement que par une grammaire, en posant la syntaxe individuelle de chacun de ses éléments et en laissant le soin à un moteur de recherche de déterminer par élimination la syntaxe globale.

 

 

 

---- 9 décembre 2017 ----


 

 


Dominique Mabboux-Stromberg