File: adding-feature.md

package info (click to toggle)
faust 2.54.9%2Bds0-1
  • links: PTS, VCS
  • area: main
  • in suites: bookworm
  • size: 352,732 kB
  • sloc: cpp: 260,515; javascript: 33,578; ansic: 16,680; vhdl: 14,052; sh: 13,271; java: 5,900; objc: 3,875; makefile: 3,228; python: 2,176; cs: 1,642; lisp: 1,140; ruby: 951; xml: 762; yacc: 564; lex: 247; awk: 110; tcl: 26
file content (171 lines) | stat: -rw-r--r-- 8,735 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
# Sur l'ajout de fonctionnalités dans les langages

## Avant toute chose

Ajouter une fonctionnalité à un très haut coût de développement, principalement dû au fait que cette nouvelle fonctionnalité induit une cassure dans les versions : il y aura le code d'après qui ne sera plus compilable par le langage d'avant (ce qui peut être encore pire si votre fonctionnalité n'assure pas la rétrocompatibilité : la cassure s'opère dans les deux sens). À ce sujet, il peut être utile d'écrire un script de compatibilité. Aussi, la première question à se poser au moment d'ajouter une fonctionnalité est :

La fonctionnalité que vous êtes sur le point d'ajouter est-elle vraiment nécessaire ? Autrement dit : est-elle utile uniquement dans un nombre négligeable de cas ? Est-elle compréhensible seulement par les quelques gourous qui ont écrit le langage ? Peut-elle être émulée simplement directement dans le langage (ou dans le compilateur) ? Va-t-elle être difficilement maintenable ?

La base de code n'est-elle pas trop instable pour se permettre d'ajouter une nouvelle source de bugs potentielle ? Si vous avez hésité à répondre non à une de ces questions, vous devriez fermer ce fichier et reconsidérer l'ajout d'une fonctionnalité.

Ceci étant dit, passons aux choses sérieuses.

## Exemple pratique

Le problème considéré ici est l'ajout d'un système d'annotations d'intervalles de signaux dans le langage Faust, permettant d'indiquer et de tester des majorants/minorants calculés par le compilateur. Ce langage (comme tous les DSL) n'est certes pas un très bon exemple pour le cas général, cependant, certaines techniques devraient être assez similaires.

On se propose d'ajouter deux paires de primitives à Faust :

 * `highest(s)` et `lowest(s)`, signaux à une entrée retournant respectivement le majorant et le minorant du signal `s` calculé par le compilateur. D'un point de vue du treillis des types Faust, il s'agit de constantes, calculable à la compilation (bien sûr), flottantes (donc pas booléennes), parallélisables (je crois), et ce, quelque soit le signal en entrée.

 * `assertbounds(lo, hi, sig)`, avec `lo` et `hi` deux constantes connues à la compilation, qui aura deux comportements : en mode normal, elle crée un signal dont la valeur est celle de `sig` mais dont l'intervalle est `[lo, hi]`, en mode debug, elle vérifie pendant l'exécution que cet intervalle est bien vérifié.

N.B. Il faudra sans doute ajouter deux autres primitives concernant la
résolution des signaux.

## Lexer/Parser

La première étape est d'étendre l'ensemble des programmes Faust valides pour qu'ils contiennent (de préférence exactement) les chaînes de caractère représentant ces primitives, en modifiant la syntaxe (lexing) et la sémantique (parsing) de Faust. En effet, si l'on demande à Faust de compiler le programme suivant :

    process = assertbounds(-1, 1);

on obtient l'erreur suivante : 

    1 : ERROR : undefined symbol : assertbounds

Pour ce faire, il nous faut modifier les fichiers décrivant le lexer et le parser, ici écrits en Flex/Bison (pour plus de détails, voir le Dragon Book)

Dans Faust, ces fichiers se trouvent dans `compiler/parser`.

### Lexing

En Bison, la déclaration des tokens se fait dans le parser, ici `faustparser.y`, déclarons donc 4 nouveaux tokens:

     %token ASSERTBOUNDS
     %token LOWEST
     %token HIGHEST

Il faut ensuite que ces Tokens soient associés à des chaînes de caractères Faust, pour cela, modifions le lexer. Le fichier contenant le lexer se trouve dans `faustlexer.l`. Il suffit d'ajouter (entre les deux `%%`) :

    "assertbounds" return ASSERTBOUNDS;
    "lowest" return LOWEST;
    "highest" return HIGHEST;

Recompilons le parser et le compilateur (`make parser` et `make` à la racine). En compilant l'exemple :

    process = assertbounds(-1, 1);

on obtient à présent : 

    1 : ERROR : syntax error

### Parsing

Puisque nous allons ajouter une primitive, celle-ci va logiquement être issue du non-terminal `primitive`. Puisqu'il s'agit de primitives, nos tokens seront d'ailleurs terminaux.

Attention : on parle ici de l'implantation d'une nouvelle primitive, pas d'une `xtended`.

Dans la plupart des compilateurs, pour une primitive d'arité `n`, il est d'usage de demander au parser de construire un objet symbolisant la primitive en appelant le constructeur `C++` de la primitive avec pour arguments le résultat du parsing des arguments de la primitive. Cependant, comme le langage Faust décrit une algèbre de blocs, les arguments ne sont pas toujours explicitement passés à la primitive, ils peuvent être routés. D'où un petit _wrapper_ autour du constructeur de la primitive, dépendant de l'airté de celle-ci.

Comme ils ont une arité de 1 et 3, il faut définir deux nouvelles boîtes

	| ASSERTBOUNDS			{ $$ = boxPrim3(sigAssertBounds);}
	| LOWEST						{ $$ = boxPrim1(sigLowest);}
	| HIGHEST						{ $$ = boxPrim1(sigHighest);}

le parsing est terminé.

## Constructeurs de signaux

Les constructeurs Faust sont extrêmement classiques d'un point de vue compilation : chaque signal est un arbre avec dans sa racine un symbole unique à l'opération du signal (par exemple un symbole ADD pour une addition) et comme enfants ses arguments. (boilerplate code incoming)

Ils sont définis dans `signals.cpp` avec leurs destructeurs (dans le sens programmation fonctionnel du terme, et pas gestion mémoire).

Il nous faut cependant d'abord définir les symboles des signaux. Pour des raisons d'optimisation, ils sont définis une fois pour toute dans `global.hh` et `global.cpp` (un objet unique mutable appelé `gGlobal` qui émule les variables globales à l'ensemble du code) :

`global.hh`

```c++
Sym SIGASSERTBOUNDS;
Sym SIGHIGHEST;
Sym SIGLOWEST;
```

`global.cpp`

```c++
SIGASSERTBOUNDS    = symbol("sigAssertBounds");
SIGHIGHEST         = symbol("sigHighest");
SIGLOWEST          = symbol("sigLowest");
```

Nous pouvons à présent définir le constructeur du signal `assertbounds` ainsi que son destructeur :

`signals.hh`

```c++
Tree sigAssertBounds(Tree s1, Tree s2, Tree s3);
Tree sigLowest(Tree s);
Tree sigHighest(Tree s);

bool isSigAssertBounds(Tree t, Tree& s1, Tree& s2, Tree& s3);
bool isSigLowest(Tree t, Tree& s);
bool isSigHighest(Tree t, Tree& s);
```

`signals.cpp`

```c++
Tree sigAssertBounds(Tree s1, Tree s2, Tree s3)
{
    return tree(gGlobal->SIGASSERTBOUNDS, s1, s2, s3);
}

bool isSigAssertBounds(Tree t, Tree& s1, Tree& s2, Tree& s3)
{
    return isTree(t, gGlobal->SIGASSERTBOUNDS, s1, s2, s3);
}
```

Et maintenant ça compile, malheureusement. En effet, si le compilateur est désormais capable de créer un objet pour les primitives, il n'a toujours aucune idée de comment le compiler. Et en effet, si on compile l'exemple : 

	process = assertbounds;

On obtient

    ERROR : getSubSignals unrecognized signal : sigAssertBounds[-1,1,SigInput[0]]


## Compilation

Les signaux étant construits, nous pouvons entrer dans la compilation proprement dite.  Modifions donc la fonction `getSubSignals`. Cette fonction se trouve dans le fichier `subsignals.cpp` (on pourra par exemple utiliser la documentation auto-générée avec Doxygen `make doc` à la racine, ou encore les fonctionnalités de recherche de définitions dans des éditeurs modernes comme Emacs), elle extrait juste les signaux des sous-arbres de l'arbre signal (n'oublions pas qu'un signal est stocké sous forme d'arbre). Nos boîtes ont toutes deux signaux, on pourra s'inspirer du cas `sigPrefix`.

Après compilation et exécution du code, on obtient la nouvelle erreur plus intéressante : 

	ERROR inferring signal type : unrecognized signal

Il faut donc modifier le système d'inférence de types présent dans le fichier `signals/sigtyperules.cpp`. La définition formelle des types Faust peut se trouver en commentaire de l'en-tête du fichier `sigtype.hh`.

Commençons par `assertbounds`, le principe de cette fonction étant d'ajouter des bornes à un signal, il suffit d'utiliser la méthode `promoteInterval`

autres fichiers à changer pour le backend -ocpp

+ `signals/sigToGraph.cpp`, pour les graphes de signaux

+ `signals/sigIdentity.cpp`

+ `boxes/ppbox.cpp`, pour les diagrammes

+ `generator/compile_scal.cpp`, ce fichier est celui qui contient la compilation proprement dite

+ `sigprint.cpp`, si a besoin d'un dessin spécial

Réfléchir à des tests par rapport au : 

1. code généré

2. intervalles

Que se passe-t-il avec des constantes non décimale dans la version de F2D ?

Que se passe-t-il si l'intervalle donne une version exacte en double mais qu'il y a un dépassement en float (ex gênant des délais) ?