Les processeurs ARM sont de plus en plus répandus y compris sur les laptops. C'est en particulier le cas de tous les Macintosh récents. Ils sont équipés de processeurs Apple Silicon (M1, M2, …) qui sont des processeurs très différents de ceux d'Intel (architecture ARM). Alors, comment faire pour tout de même faire les exercices ? Il y a plusieurs solutions (comme utiliser QEMU par exemple). L'une des plus simple et des plus rapide (~ 5 minutes) est d'utiliser Docker. Les étapes sont les suivantes sous macOS:
brew install --cask docker
)docker image pull --platform linux/amd64 ubuntu
docker run -it --name --platform linux/amd64 ubuntu bash
apt update
apt upgrade -y
apt install gcc gcc-multilib -y
apt install micro
Et voilà, vous pouvez maintenant faire les exercices du cous sur votre machine. Il est même possible d'éditer et de débugger des programmes dans ce container directement depuis votre Macintosh. Je décrirai la procédure ultérieurement.
Pour sortir (et stopper) le container, un simple exit
fait le job. Si vous voulez ensuite revenir dans ce même container, il faut utiliser start
à la place de run
(run
démarre toujours un nouveau container):
docker start -ai ubuntu
Il est préférable d'utiliser bison
et flex
plutot que yacc
et lex
. Meme s'il est possible d'uliliser ces derniers, les options et les fichiers générés ne sont pas les meme et vous allez vous compliquer la vie pour rien. Pour installer bison
et flex
sous Ubuntu:
sudo apt install flex bison
L'énoncé suggère que le programme a quelque chose à voir avec Héron d'Alexandrie et c'est bien le cas. Mais attention à ne pas confondre la méthode d'Héron (vue en cours d'Architecture des ordinateurs avec le même professeur), cas particulier de la méthode de Newton, et la formule d'Héron.
Bison possède un générateur de graphes intégré qui peut être utile pour aider à la résolution des exercices et à la compréhension des mécanismes. Pour cela, il suffit d'exécuter bison avec l'option -g :
bison -g test.y
Cela génère un fichier .gv qui peut être converti en image.
Il y a une erreur dans l’énoncé. Il est question du fichier ed-3-calc.y
, pas du fichier 1-calc.y
.
Lorsque l’on veut compiler le programme ppcm, on a un certain nombre de problèmes:
ppcm-
devant les noms.$ make a.out
bison ppcm.y
ppcm.y: warning: 1 shift/reduce conflict [-Wconflicts-sr]
ppcm.y: note: rerun with option '-Wcounterexamples' to generate conflict
,→ counterexamples
flex ppcm.l
gcc -g -O -c -o ppcm.tab.o ppcm.tab.c
ppcm.tab.c: In function ‘yyparse’:
ppcm.tab.c:1131:16: warning: implicit declaration of function ‘yylex’
,→ [-Wimplicit-function-declaration] 1131 | yychar = yylex ();
| ^~~~~
ppcm.y:95:52: warning: implicit declaration of function ‘label’
,→ [-Wimplicit-function-declaration]
95 | { printf("debut%d:\n", $<i>$ = label()); }
| ^~~~~
ppcm.tab.c:1493:7: warning: implicit declaration of function ‘yyerror’; did you mean
,→ ‘yyerrok’? [-Wimplicit-function-declaration]
1493 | | |
yyerror (YY_("syntax error"));
^~~~~~~
yyerrok
ppcm.y: At top level:
ppcm.y:181:1: warning: return type defaults to ‘int’ [-Wimplicit-int]
181 | label(){
| ^~~~~
In file included from ppcm.y:186:
ppcm.l: In function ‘chaine’:
ppcm.l:39:5: warning: implicit declaration of function ‘nomem’
,→ [-Wimplicit-function-declaration] 39 | strcpy(*p, s);
| ^~~~~
ppcm.l: At top level:
ppcm.l:45:1: warning: return type defaults to ‘int’ [-Wimplicit-int] ppcm.y:188:1: warning: return type defaults to ‘int’ [-Wimplicit-int]
188 | yyerror(char * message){
| ^~~~~~~
ppcm.y:195:1: warning: return type defaults to ‘int’ [-Wimplicit-int] 195 | nomem(){
| ^~~~~
gcc -g -O -c -o expr.o expr.c
gcc -g -O ppcm.tab.o expr.o
En effet, le programme est écrit dans un style de C assez ancien. On peut facilement corriger ces problèmes :
ppcm.y
, juste avant le premier %}
, on déclare les fonctions qui sont utilisées par la grammaire :// Declaration des fonctions utilisées dans la grammaire
int yylex(void);
void yyerror(char *);
int label();
void nomem();
%}
label
et nomem
:/* label -- renvoie un nouveau numero d'etiquette a chaque appel */
int label(){ // On ajoute explicitement le type de la valeur de retour
static int foo = 0;
return foo++;
}
void yyerror(char * message){ // On ajoute explicitement le type de la valeur de retour
extern int lineno;
extern char * yytext;
fprintf(stderr, "%d: %s at %s\n", lineno, message, yytext);
}
void nomem(){ // On ajoute explicitement le type de la valeur de retour
fprintf(stderr, "Pas assez de memoire\n");
exit(1);
}
char * chaine(char * s);
yywrap
:int yywrap(){ return 1; }
Voici une archive avec tous ces problèmes corrigés:
Si vous obtenez une erreur lors de la compilation avec make a.out
cc1: fatal error: ppcm.tab.c: No such file or directory
compilation terminated.
make: *** [<builtin>: ppcm.tab.o] Error 1
c'est par ce que vous utilisez yacc à la place de bison. Passez à bison, c'est le plus simple.
Il y a un certain nombre d'erreurs (cela a été confirmé par le professeur) dans le paragraphe 8.4.1 de ce chapitre, à propos de l'utilisation de const
. Les exemples à partir de «Un Autre Exemple» sont incorrectes et je suppose que cela remonte au prédécesseur xdi professeur actuel. Le cours sera modifié dans un futur plus ou moins proche.
En effet:
const char **
et:
char const **
sont strictement équivalents. Ce dernier ne veut pas dire «le pointeur sur les caractères n’est pas modifié mais [...] les caractères, eux, peuvent l’être». La déclaration correcte pour ce cas est:
char * const *
Pour référence, le standard ISO/IEC 9899:2018 (C18) donne l'exemple suivant (§ 6.7.6.1.3):
De même, «si ni l’un ni l’autre ne le sont» doit être déclaré ainsi:
char const * const *
ou ainsi:
const char * const *
La déclaration (sous «Préférable») est redondante:
const char const **
Les deux const
veulent dire la même chose. Certains compilateurs émettent une alerte pour le signaler:
$ gcc -Wall -o const0 const0.c
const0.c:2:20: warning: duplicate ‘const’ declaration specifier [-Wduplicate-decl-specifier]
2 | void f0(const char const ** p) {
| ^~~~~
A noter aussi qu'il manque une accolade ouvrante pour toutes les déclarations de foo
:
int foo (const char const * const *)
}
devrait être
int foo (char const * const * const) {
}
Concernant cette déclaration, le cours indique que cela «n’a pas grand sens puisque l’argument est une copie». C'est pourtant recommandé dans certaines sociétés ou sur certains projets (code de conduite interne à l'entreprise ou au projet). Cela évite des réassignations intempestives:
int foo (char const * const * const pstr) {
...
pstr = ...; // interdit, erreur de compilation
...
}
Le langage C permet même d'avoir, dans ce cas, une déclaration différente de sa définition, et le compilateur reconnait qu'il s'agit de la même fonction:
// Déclaration, sans const à la fin
int foo (char const * const * pstr);
// Définition, avec const à la fin
int foo (char const * const * const pstr) {
...
pstr = ...;
...
}
On trouve même parfois:
// Déclaration, sans const
int foo (int n);
// Définition, avec const
int foo (const int n) {
...
pstr = ...;
...
}
Le site https://isocpp.org dédié au C++ standard (const
provient du C++ et a été adopté par le C) a une page (FAQ) consacrée à l'utilisation de const
. Le site GeeksForGeeks présente un diagramme assez parlant: