Introduction

Bienvenue dans le projet compilation. Dans ce projet, vous allez réaliser en pratique un compilateur pour un langage de programmation impératif semblable à ceux que vous utilisez quotidiennement. Même si le développement des compilateurs n’est pas le travail habituel de la majorité des programmeurs, ce projet vous offre les compétences nécessaires dans l’avenir notamment la meilleure compréhension du processus de compilation et de ses résultats y compris les erreurs de compilation, la vision plus générale sur les langages de programmation, leurs points forts et limitations, l’analyse systématique des données textuelles structurées telles que les fichiers des configuration ou les formats de présentation d’information comme XML.

Équipe enseignante

Responsable du cours – Frédéric Voisin Chargés des TP – Oleksandr Zinenko, Martin Clochard

Installation des logiciels

Dans ce projet, vous allez utiliser l’analyseur lexical GNU Flex et le générateur d’analyseurs syntaxiques GNU Bison, logiciels libres faisant partie du système d’exploitation GNU. Ces logiciels sont installés sur les ordinateurs de l’École sous Linux est sont accessibles depuis le terminal.

Nous conseillons d’utiliser les dernières versions: Flex 2.5.39 et Bison 3.0.4 au moment du début de projet.

Pour les installer sur vos ordinateurs personnels sous Linux ou MacOS, utilisez votre gestionnaire de paquets préféré (apt-get, macports) ou suivez l’instruction :

  • téléchargez les archives de code source flex.tar.gz et bison.tar.gz ;
  • téléchargez le script d’installation installer.sh et mettez-le dans le même répertoire que les archives;
  • ouvrez le terminal et naviguez au répertoire avec les fichiers téléchargés ;
  • exécutez le script ./installer.sh qui créera le répertoire usr dans votre répertoire personnel, y installera Flex et Bison et configurera l’environnement pour leur utilisation dans la ligne de commande ;
  • fermez le terminal et ouvrez-le à nouveau pour que les changements d’environnement s’appliquent.

L’utilisation de système d’exploitation Windows est fortement déconseillée pour ce projet en raison d’un lien étroit entre Flex, Bison et système d’exploitation Unix, prédécesseur de Linux ou MacOS, qui impose un mode d’interaction basé sur la ligne de commande et difficile à reproduire exactement sous Windows.

Travail pratique

Le but de ce TP est d’introduire les outils Flex et Bison qui serviront en projet. Ils sont ici mis en œuvre sur un exemple de portée très réduite, un interpréteur d’un langage de programmation très simple, afin d’en comprendre les mécanismes généraux.

Téléchargez l’énoncé du TP ainsi que les ébauches du code sur www.lri.fr/~fv/teaching.html, section “Compilation et Projet Compilation (S7)”. Avant de commencer, assurez-vous que Flex et Bison sont installés est fonctionnent correctement.

Ce TP n’est pas noté.

Projet

Énoncé du projet et les ébauches des fichiers source à télécharger.

Rendu intermédiaire (analyseur lexique et syntaxique) – 8 décembre.

Questions liées au langage C

Comportement non défini, non spécifié ou défini par la réalisation

Le langage C est réglé par une norme internationale ISO/IEC 9899, la dernière version de laquelle était adoptée en 2011. Cette norme qualifie certains comportements comme non définis (undefined behavior), non spécifiés (unspecified behavior) ou définis par la réalisation (implementation-defined behavior).

Le comportement non défini existe souvent dans le cas où la sémantique de programme n’est pas claire ou ne peut pas être déterminée sans ambiguïté. Si un programme continent même un seul comportement non défini, il est considéré mal formé (ill-formed) et le compilateur C n’a plus d’obligation de créer un exécutable qui correspond au code source du programme. En théorie, le programme mal formé peut avoir n’importe quel comportement, par exemple il peut supprimer tous les fichiers dans votre répertoire personnel. Même si cela n’arrive jamais en pratique, les programmes mal formés ont souvent une sortie erronée ou imprédictible. Les exemples d’un comportement non défini sont : l’accès aux éléments d’une structure à travers le pointeur nul, division d’entier par zéro, dépassement d’entier.

Important : le comportement non défini n’est pas une erreur syntaxique et le compilateur n’émet un avertissement que très rarement. Pire, il peut être impossible à vérifier en phase de compilation, par exemple dans le cas de dépassement d’entier en expression i+1 où la valeur de i est entrée par l’utilisateur.

Une erreur de segmentation (segmentation fault) est souvent provoqué par un comportement non défini. Or elle n’est pas obligatoire ni reproductible, la norme autorisant en fait le comportement aléatoire. Le programme mal formé peut donc s’exécuter sans problème et générer un résultat correct une fois, mais s’arrêter avec une erreur une autre fois.

Le comportement non spécifié est un comportement défini mais inconnu de programmeur. Le compilateur est libre de choisir le comportement qui lui convient au cas par cas. Pour le même programme, ce comportement peut changer après recompilation. L’exemple d’un comportement non spécifié est l’ordre d’exécution des arguments dans un appel de fonction : il n’est pas garanti que dans function(f(), g()) l’appel f() soit exécute avant ou après l’appel g(). Vous ne devez donc pas baser vos programmes sur quelconque ordre.

Le comportement défini par la réalisation, comme son nom l’indique, est défini dans la documentation de compilateur pour chaque système en particulier. La taille d’entier int est l’exemple de tel comportement : sur l’architecture x86 sous Linux avec GCC sizeof(int) == 4 alors que pour le microcontrôleur PIC avec sdcc sizeof(int) == 2. Même si dans ce projet vous développez pour une seule architecture, soyez conscients de ce genre de comportement dans les choix des types et dans les vérifications contextuelles. La norme C propose une famille des types int32_t, int16_t qui garantie la taille des entiers.

La différences entre une structure et une union en C

Malgré le syntaxe similaire,

struct S {
  int i;
  char c;
};

union U {
  int i;
  char c;
};

les structures et les unions sont les structure de données très différentes. La structure contient toujours l’ensemble de ses éléments et on peut accéder à chaque élément. Dans notre exemple, la structure S comprend à la fois l’entier i et le caractère c et on peut leur affecter les valeurs différentes. Par contre, l’union ne contient que l’un de ses éléments, les autres n’étant pas accessibles. Dans le même exemple, l’union U peut contenir soit l’entier soit le caractère. En C, il est possible de accéder l’élément de l’union différent de celui qui était écrit. La conversion se fait au niveau de la représentation interne, n’attendez donc pas que un flottant soit automatiquement converti à l’entier correspondant. En C++, tel accès peut causer un comportement non défini.

Plus d’information sur l’accès aux éléments non-actifs dans l’union en C et C++ ici.

Unions emboîtées dans une structure

On peut mettre certains champs d’une structure en union :

struct S {
  int type;
  union {
    int i;
    double d;
  };
};

Dans ce cas, on peut accéder directement à l’entier i ou au flottant d dans la structure

struct S s1, s2;
s1.i = 42;
s2.d = 42.0;
s1.d = 43.0; // écrase l'ancienne valeur s1.i 

L’union ne contenant qu’un élément, l’écriture d’un autre élément remplacera l’ancien automatiquement. L’entier type en dehors de l’union peut être utilisé comme une étiquette permettant de savoir quel type est enregistré dans l’union actuellement. Par exemple, on peut lui affecter 1 chaque fois quand on écrit l’entier i et 2 quand on écrit le flottant. Pour simplifier la lecture de code, ces valeurs-étiquettes peuvent être associées à des constantes ou des macros, #define ENTIER 1.

Caractères et chaînes de caractères

En C, un caractère est représenté par le type char. Ce type est entier de 1 octet. Chaque valeur de l’entier est associé à un caractère selon le tableau ASCII. Puisque c’est un entier, toutes les opérations arithmétiques sont autorisées.

Une chaîne de caractères est représenté comme un tableau de type char terminé par la valeur entière zéro '\0' (et non pas le caractère '0'). Tableau en fait est un pointeur vers le premier élément, donc le type d’une chaîne de caractères est char *. Par conséquent, l’opération s1 == s2 compare les pointeurs et non pas les chaînes de caractères correspondantes. Pour les comparer proprement, utilisez la fonction strcmp qui renvoie 0 en cas de l’égalité. Faites attention à l’allocation et à la deallocation de la mémoire utilisé par les chaînes de caractères. En plus, le pointeur étant lui aussi un entier, toutes les opérations arithmétiques sont autorisées.

Valeurs immédiates des chaînes de caractères

La valeur immédiate d’un caractère (character literal) est introduite dans le code entre les guillemets simples, char c = 'a' et remplacé par sa valeur ASCII pendant la compilation. Certains caractères spéciaux, tel le retour de chariot, sont impossibles à entrer sur le clavier. Dans ce cas, vous pouvez soit utiliser la notation numérique octale \015 (antislash suivi d’un numéro octal avec les zéros) ou hexadécimal \x0D (antislash suivi d’un x et d’un numéro hexadécimal avec les zéros). Pour les caractères spéciaux fréquents dans le texte, les séquences d’échappement peuvent être utilisées : nouvelle ligne \n, retour de chariot \r, tabulation \t, etc. Il est possible d’affecter une valeur entière au caractère char c = 13.

La valeur immédiate d’une chaîne de caractères (string literal) est introduite entre les guillemets doubles char *string = "hello". Six octets de mémoire, cinq caractères et le zéro terminal, sont alloués par le compilateur de sorte qu’ils restent accessible pendant toute la durée d’exécution (leur emplacement exacte et un comportement non spécifié). L’adresse du premier caractère est sauvegardé dans la variable string. Le compilateur ajoute automatiquement le zéro à la fin de cette valeur immédiate. La deallocation de ce pointeur ou toute autre modification de cette chaîne de caractères constitue un comportement non défini.

Contrairement à char *, l’utilisation d’un tableau sans bords pour sauvegarder une valeur immédiate de la chaîne de caractères char s[] = "hello" alloue la mémoire sur la pile est y copie la chaîne. Elle est donc modifiable à condition de ne pas supprimer le zéro terminal et de ne pas dépasser sa taille initiale. Comme toutes les variables sur la pile, cette chaîne ne sera plus accessible dès que la variable s est hors de portée (sa lecture est un comportement indéfini). Il est donc nécessaire de faire une copie dans la mémoire dynamique pour, e.g., la renvoyer d’une fonction.

Comme résultat, on peut comparer les valeurs immédiates de caractères, mais pas celles de chaînes de caractères.

'a' == 'a';
'a' != "a"; 'a' == "a"  // comportement non spécifié
"a" != "a"; "a" == "a"  // comportement non spécifié

Traitement des chaînes de caractères

Les chaînes de caractères en C n’ont pas de type dédié, mais peuvent être manipulées par de fonctions spéciales déclarées dans le fichier d’en-tête string.h. Ces fonctions acceptent des pointeurs char * comme arguments et les interprètent comme s’ils étaient des chaînes terminées par zéro : chaque caractère peut être traité séparément jusqu’au premier caractère '\0' rencontré. Si la chaîne contient plusieurs caractères '\0', le traitement s’arrêtera après le premier. Si la chaîne ne contient pas de '\0', la fonction continuera de lire les octets de mémoire un par un en dehors de la mémoire allouée (ce qui constitue un comportement non défini). En pratique, tel comportement se manifeste comme la sortie de programme contenant des caractères aléatoires ou l’erreur de segmentation. Ce genre de comportement non défini est très dangereux pour la sécurité de programmes parce qu’il est à l’origine des vulnérabilités “débordement de pile”, notamment Heartbleed.

Voici la liste non-exhaustive des fonctions pour manipuler les chaînes de caractères :

  • size_t strlen(char *s) renvoie la longueur de la chaîne s, i.e. le nombre de caractères jusqu’au premier '\0' sans compter ce dernier ;
  • char *strcpy(char *dest, char *src) copie la chaîne src, zéro terminal compris, à l’adresse dest et renvoie le pointeur au nouveau zéro terminal de dest ; la chaîne dest doit être déjà allouée et être suffisamment longue pour contenir la chaîne src ;
  • char *strstr(char *string, char *pattern) recherche dans la chaîne string la première entrée de la sous-chaîne pattern et renvoie le pointeur vers le premier caractère en cas de succès et NULL en cas d’échec ; le résultat de cette fonction est un pointeur au milieu de la chaîne existante, pas une copie, et ne contient pas nécessairement le zéro terminal juste après la sous-chaîne ;
  • char *strcat(char *dest, char *src) met la chaîne src à la fin de la chaîne dest et renvoie cette dernière ; la chaîne dest doit être suffisamment longue pour son contenu originel, celui de src et le zéro terminal.
  • int strcmp(char *s1, char *s2) compare les deux chaînes de caractères et renvoie 0 si elles sont égales, une valeur négative si s1 précède s2 dans l’ordre lexicographique et une valeur positive si s1 suit s2 ; l’ordre lexicographique correspond à la comparaison caractère par caractère en utilisant les codes ASCII respectifs ; si deux caractères sont égaux, la comparaison avance jusqu’à première différence ; si une chaîne est un préfixe de l’autre, elle la précède.

Attention, ces fonctions n’allouent jamais la mémoire, c’est à vous d’assurer que la chaîne de la taille suffisante était allouée avant de les appeler. N’oubliez pas d’allouer un octet pour le zéro terminal.

char *literal = "Hello";
char *copy = (char *) malloc(strlen(literal) + 1);//+1 octet pour zéro terminal
strcpy(copy, literal);
// <...>
free(copy);

Pour éviter l’allocation manuelle, plusieurs systèmes, y compris Linux et Mac OS X, proposent la fonction non standard

  • char *strdup(char *str) qui alloue la mémoire et renvoie une copie de la chaîne str.

En plus, la bibliothèque standard propose les versions des fonctions limitées par le nombre de caractères à traiter : leurs noms commencent par strn à la place de str, par exemple char *strncpy(char *dest, char *src, size_t n). Ces fonctions ont un argument supplémentaire n qui correspond au nombre de caractères excluant le zéro terminal. Si le caractère '\0' ne fait pas partie de n premiers, le traitement s’arrête après n caractères, sinon, les règles habituelles s’appliquent. Par contre, ces fonctions ne rajoutent pas le zéro terminal automatiquement.

Communication entre Flex et Bison

Typage des lexèmes et des règles grammaticales

Il est souvent nécessaire d’associer une valeur à un lexème, par exemple le lexème “Constante Entière” doit communiquer la valeur de constante. Flex et Bison partagent le système de typage basé sur l’union qui regroupe tous les types que l’on peut associer aux lexèmes ou aux symboles de la grammaire. Cette union, appelée YYSTYPE, est définie dans le code source pour Bison parce que l’analyse syntaxique utilise d’autres types que ceux des lexème pour construire l’arbre de syntaxe. Chaque symbole, terminal ou non terminal, peut avoir un type défini dans la directive %token pour le terminaux et %type pour le non terminaux entre deux chevrons <type>. Attention, le type pour Bison correspond au nom de l’élément de l’union, et non pas au type C. Si un lexème n’a pas besoin de communiquer une valeur, il suffit de ne pas déclarer son type dans la directive %token, ce qui sera interprété comme type void. Pour les symboles de la grammaire, la directive %type est optionnelle, son absence étant interprété comme type void également.

%union {       // YYSTYPE
  int I;
  char *S;
};

%token     OPERATOR
%token <S> ID
%type  <S> statement

statement : ID ';' { $$ = $1; }

Ce code sera transformé par Bison selon le modèle ci-dessous. Pour les actions associées à chaque règle de grammaire, Bison génère une fonction avec autant d’arguments que de symboles typés dans la partie droite de la règle. Ces arguments sont accessibles à l’intérieur de la fonction sous les noms $1, $2, etc. suivant la position du symbole. Les arguments de la fonction ont le type d’union YYSTYPE, mais Bison sélectionne l’élément correspondant au type du lexème ou du symbole avant d’affecter $1, $2. Les valeurs de ces arguments correspondent soit aux valeurs crées par Flex (voir dessous) pour les lexèmes, soit aux résultats renvoyés par les fonctions générées pour les symboles non terminaux. Pour renvoyer une valeur, il suffit d’affecter la variable $$ qui a le type du symbole à gauche dans la règle, Bison se chargera de la création de l’union.

typedef union {
  int I;
  char *S;
} YYSTYPE;

YYSTYPE bison_rule_statement(YYSTYPE flex_id) {
  char *$1 = flex_id.S;
  void $2;
  char *$$;
  YYSTYPE result;
  /* user code */
    $$ = $1;
  /* end of user code */
  result.S = $$;
  return result;
}

Pour sauvegarder une valeur associée au lexème, Flex déclare une variable yylval du type YYSTYPE accessible dans l’action correspondante au lexème. Puisque cette variable a un type d’union, il est nécessaire d’accéder à un élément particulier. Or le mécanisme de contrôle de cet élément n’existe pas : Bison va toujours accéder à l’élément le nom duquel est déclaré dans la directive %token. C’est à vous d’assurer que le bon élément est utilisé dans Flex.

[a-zA-Z_][a-zA-Z0-9_]+  { yylval.S = strdup(yytext);
                          return ID; }

Vous pouvez considérer que Flex génère pour chaque action le code correspondant au modèle ci-dessous (même si en réalité, il génère une seule fonction avec un opérateur switch géant). Faites attention au fait que yylval est une variable globale externe, définie par Bison, qui est utilisé dans toutes les actions, même peut être modifiée en dehors et ne doit donc en aucun cas être utilisé pour communiquer l’information entre les différentes action. Sa seule utilisation possible est de transmettre une valeur de Flex à Bison. Notez aussi que la variable yytext est une chaîne de caractères globale, modifiée et réutilisée par plusieurs actions. Il est donc absolument nécessaire de faire une copie de cette chaîne de caractères si vous voulez l’utiliser en dehors de l’action Flex, e.g. en Bison.

Les noms des lexèmes sont générés par Bison à partir des directives %token et correspondent aux entiers positifs supérieurs à 256. Vous pouvez les voir dans le fichier tp_y.h mais évitez d’utiliser les entiers directement parce qu’ils sont susceptibles à changer après chaque compilation du fichier Bison.

#define ID 258
#define OPERATOR 259
extern YYSTYPE yylval;
char *yytext;

int flex_token_42(char *yytext) {
  /* user code */
    yylval.S = strdup(yytext);
    return ID;
  /* end of user code */
}

void scan() {
  while (*yytext != '\0') {
/* ... */
    int matched_size = regex_42(yytext);
    if (matched_size != 0) {
      char c = yytext[matched_size];
      yytext[matched_size] = '\0';
      int token = flex_token_42(yytext);
      yytext[matched_size] = c;
      yytext += matched_size;
      emit_token(token, yylval);
    }
/* ... */
  }
}

Suivez les flèches dans le schéma pour retrouver le type C de chaque variable spéciale utilisée par Flex ou Bison. Dans les cas où YYSTYPE est défini directement, sans utiliser %union, sa définition se trouve dans le fichier d’en-tête.

Schéma de choix des types

Utiliser les caractères directement dans Bison

Grâce au fait que Bison commence les identificateurs entiers des lexèmes à 257, il est possible de passer les caractères ASCII comme des lexèmes entre Flex et Bison. En C, un caractère est représenté par un entier correspondant à son code ASCII compris entre 0 et 255. Cela permet d’utiliser le caractère comme son nom de lexème.

Symbol [.()]
%%
";"		{ return ';'; }
","		{ return COMMA; }
{Symbol}	{ return yytext[0]; }

Dans ce cas, à la place d’un lexème SEMICOLON défini en Bison et associé automatiquement à un entier généré, par exemple 258, l’action Flex renvoie l’entier 59, le code ASCII du point-virgule. On peut regrouper l’ensemble des symboles à passer directement dans Bison avec une expression régulière et utiliser la variable yytext pour renvoyer le code de symbole nécessaire. Cette variable contient le texte correspondant à l’expression régulière, dans notre cas un seul caractère qu’on peut accéder directement et passer par valeur.

Avant la génération du code Bison, le compilateur remplace les noms des lexèmes par les entiers associés et transforme les caractère en leurs valeurs ASCII. Donc la règle

rule: COMMA ';'

sera transformée en

rule: 257 59

et l’étape suivante ignorera la différence entres les “vrai” lexèmes et les caractères.

Attention, cette technique ne fonctionne que pour un caractère seul. Une combinaison des caractères telle que := doit obligatoirement constituer un lexème. L’analyse lexical peut reconnaître certains parties du texte d’entrée et les ignorer, en particulier les caractères des espacement et les commentaires. Si : et = sont passés directement au Bison, ils sont considérés comme des lexèmes différents et peuvent cacher le texte ignoré en analyse lexical entre eux, par conséquent la grammaire accepte les textes avec le mauvais syntaxe :/*erreur*/=.

Mise à point des logiciels

Analyser les erreurs et les avertissements de compilateurs

Après chaque compilation de votre projet, lisez attentivement les erreurs et les avertissements émis par le compilateur. Ces dernières signalent souvent un comportement non défini ou potentiellement dangereux. Par exemple, l’avertissement

type clash for the rule "expression";

signifie que le plus souvent que vous n’affectez pas la variable $$ dans l’action d’une règle typée avec ‘%type’. Dans ce cas, l’accès au résultat de réduction selon cette règle dans l’autre endroit de la grammaire entraînera un comportement non défini.

GCC a des différents niveau de verbosité pour les avertissements. Vous pouvez en avoir plus en rajoutant les options “-W” pour les avertissements basiques, “-Wall” pour les avertissements basiques et certains problèmes de typage, “-Wextra” pour la plupart des avertissements disponibles. Le fichier make fourni effectue la compilation avec “-Wall”.

Le compilateur Clang, utilisé par défaut sur MacOS comme alias de GCC, émet d’habitude plus d’avertissement avec “-Wall”.

Utiliser un débogueur

Vous avec l’habitude d’utiliser la sortie de programme pour la mettre à point, mais il existe des outils spécifiques pour simplifier ce processus. La chaîne des outils GNU comprend le débogueur gdb et celle de LLVM propose son analogue lldb. Tout les deux ont les fonctionnalités basiques identiques, mais leur syntaxe de dialogue devient différent pour les commandes les plus puissants, cf tableau de comparaison.

Le débogage nécessite une option spéciale, -g, à être passée au compilateur. Cette option est présente dans le fichier make fourni.

Pour lancer gdb, vous pouvez l’appeler dans le terminale en lui passant le nom du fichier exécutable à déboguer. gdb ./tp

Toute option supplémentaire sera interprétée comme celle du gdb et non pas de votre programme. Cette commande ouvrira l’interface de dialogue avec gdb. Pour lancer votre programme et lui passer les paramètres nécessaires, tapez run parameter_1 parameter_2 ... dans le dialogue. Dans certains cas, notamment en cas d’un accès interdit, qui normalement entraînerait une erreur de segmentation, gdb arrêtera l’exécution de programme automatiquement. Vous pourrez alors examiner l’état de programme au moment de cet accès. La commande backtrace permet d’examiner la pile d’exécution, i.e. la séquences des appels des fonctions avec leurs arguments. La pile contient des cadres d’exécution numérotés que vous pouvez changer en utilisant la commande frame <number>, où le numéro est affiché par backtrace. Les commandes up et down permettent de remonter ou de descendre dans la pile. Juste après l’arrêt, gdb se trouve dans le cadre de la dernière fonction appelée. Dans n’importe quel cadre, il est possible d’examiner l’état des variables à l’aide de la commande print <expression>. Cette commande affiche le résultat d’évaluation d’expression C y compris l’accès par pointeur et le résultats d’appel d’une fonction. Notez que l’affichage d’un pointeur sans déréférencement correspond à l’adresse et non pas au contenu du pointeur. Gdb peut aussi afficher le contenu des structures C. Dans le cas d’arrêt automatique, il est impossible de continuer l’exécution, vous devez impérativement le relancer avec run.

Si vous voulez arrêter l’exécution à un endroit spécifique, définissez un point d’arrêt grâce à la commande break <param>. Elle peut être paramétrée par le nom de fichier et le numéro de ligne, break tp.c:431, ou par le nom de fonction, break makeTree(). L’exécution s’arrêtera juste avant que la ligne ou la fonction demandée soit sur le point d’être exécutée. Dès lors, vous pouvez utiliser les commandes d’analyse backtrace, frame, print. Vous pouvez également continuer l’exécution jusqu’au prochain point d’arrêt avec la commande continue. Pour ne pas avoir à examiner le même point d’arrêt plusieurs fois, vous pouvez définir un point d’arrêt soumis à condition avec break <endroit> if <expression> où l’expression peut référencer toute variable visible à l’endroit d’arrêt. Dans ce cas, l’exécution ne s’arrêtera que si la condition et vraie.

Les points d’arrêt peuvent servir aussi pour exécuter le programme ligne par ligne. Une fois le programme arrêté, la commande step exécute la ligne actuelle. Si cette ligne contient un appel de fonction, la commande entre dans le cadre de cette fonction. La commande next est identique sauf qu’elle évalue l’appel de fonction sans entrer à l’intérieur. Ces commandes prennent un argument optionnel numérique qui correspond au nombre de ligne à exécuter de suite sans s’arrêter. Finalement, la commande return permet d’envoyer immédiatement comme résultat de la fonction la valeur fournie comme son argument.