Article publié dans Linux Magazine 123, janvier 2010.
Copyright © 2009 Christian Aperghis-Tramoni
Après une (longue) présentation des types de données de PIR
, nous allons
aborder une partie plus attractive dans l'apprentissage d'un langage,
en l'occurrence, la programmation.
De nombreux exemples émailleront cette présentation, l'ensemble des programmes présentés peut être téléchargé sur mon site : http://www.dil.univ-mrs.fr/~chris/Documents/progs02.pod
Les types disponibles dans le langage Parrot Intermediate Representation
n'ont maintenant plus aucun secret pour nous, il est donc grand temps de se
pencher sur les instructions et les particularités de ce langage.
Nous verrons à travers plusieurs exemples qu'il offre des perspectives intéressantes tant au niveau de sa syntaxe que de la manière de l'utiliser.
Intéressons-nous dans un premier temps aux échanges avec l'environnement.
Nous avons déjà vu et utilisé l'instruction print
qui permet d'afficher
une information sur l'écran (STDOUT
).
On dispose comme en Perl 5.10 de l'instruction say
qui ajoute
un \n
en fin de ligne.
coruscant chris$ cat say.pir .sub 'main' :main $S10 = "Linux" $S11 = "Magazine" say $S10 say $S11 .end coruscant chris$ parrot say.pir Linux Magazine coruscant chris$
Par contre, c'est par l'intermédiaire d'un PMC
spécifique getstdin
que le programme accédera aux informations introduites au clavier.
L'instruction permettant de lire des informations sur une entrée se présente sous deux formes :
chaine = read DESCRIPTEUR, nombre
qui permet de lire un nombre donné de caractères, et
chaine = readline DESCRIPTEUR
qui permet de lire la totalité d'une ligne jusqu'au \n
compris.
coruscant chris$ cat lecture.pir .sub "Lecture" :main .local pmc STDIN STDIN = getstdin print "Quel est votre prenom ? " $S0 = readline STDIN chopn $S0, 1 print "Bonjour " print $S0 print " !\n" .end coruscant chris$ parrot lecture.pir Quel est votre prenom ? Christian Bonjour Christian ! coruscant chris$
Comme nous en avons l'habitude en Perl, l'instruction chopn
nous
permet, lorsque cela s'avère nécessaire, de supprimer le caractère
\n
à la fin de la chaîne lue.
L'accès à un fichier nécessite lui aussi la création d'un PMC
descripteur. Il sera initialisé au moyen de la directive open
à laquelle sont transmis deux paramètres. Le premier représente le
nom du fichier à ouvrir sous forme d'une chaîne de caractères, le
second dans quel sens sera ouvert le fichier considéré 'r'
pour
l'ouvrir en lecture et 'w'
pour l'ouvrir en écriture.
coruscant chris$ cat fichier.pir .sub "Fichier" :main .local string texte texte = <<"FIN" Le nombre pi, note par la lettre grecque du meme nom, toujours en minuscule est le rapport constant entre la circonference d'un cercle et son diametre. Il est appele aussi constante d'Archimede. Des valeurs approchees de pi courantes sont Approximativement 3,1416 Approximativement sqrt(10) Approximativement 22/7. FIN .local string Nom .local pmc STDIN STDIN = getstdin print "Quel est le nom du fichier ? " Nom = readline STDIN chopn Nom, 1 # Ouverture du fichier en ecriture. $P0 = open Nom, 'w' # Ecriture du fichier. print $P0, texte close $P0 # Ouverture du fichier en lecture. $P0 = open Nom, 'r' say "Relecture du fichier." $I0 = 0 LECTURE: # Lecture du fichier. $S0 = readline $P0 unless $P0 goto FINI $I0 += 1 print "Ligne " print $I0 print " : " print $S0 goto LECTURE FINI: say "Effacement du fichier." $P1 = new "OS" $P1."rm"(Nom) .end coruscant chris$ parrot fichier.pir Quel est le nom du fichier ? NombrePi.txt Relecture du fichier. Ligne 1 : Le nombre pi, note par la lettre grecque du meme nom, Ligne 2 : toujours en minuscule est le rapport constant entre Ligne 3 : la circonference d'un cercle et son diametre. Ligne 4 : Il est appele aussi constante d'Archimede. Ligne 5 : Des valeurs approchees de pi courantes sont Ligne 6 : Approximativement 3,1416 Ligne 7 : Approximativement sqrt(10) Ligne 8 : Approximativement 22/7. Effacement du fichier. coruscant chris$
Intéressons-nous pour commencer aux informations concernant notre système
d'exploitation auxquelles PIR
peut avoir accès.
Pour cela, on dispose aussi d'une instruction sysinfo
.
Cette instruction fait référence à un fichier de macros sysinfo.pasm
qui se trouve dans le répertoire parrot-1.x.0/runtime/parrot/include/
coruscant chris$ cat systeme.pir .include 'sysinfo.pasm' .sub main :main $I0 = sysinfo .SYSINFO_PARROT_INTSIZE print "Taille des entiers : " say $I0 $I0 = sysinfo .SYSINFO_PARROT_FLOATSIZE print "Taille des flottants : " say $I0 $I0 = sysinfo .SYSINFO_PARROT_POINTERSIZE print "Taille des references : " say $I0 $S0 = sysinfo .SYSINFO_PARROT_OS print "Systeme d'exploitation : " say $S0 $S0 = sysinfo .SYSINFO_PARROT_OS_VERSION print "Version du systeme d'exploitation : " say $S0 $S0 = sysinfo .SYSINFO_CPU_ARCH print "Architecture du processeur : " say $S0 .end coruscant chris$ parrot systeme.pir Taille des entiers : 4 Taille des flottants : 8 Taille des references : 4 Systeme d'exploitation : darwin Version du systeme d'exploitation : Darwin Kernel Version 9.7.0: Tue Mar 31 22:52:17 PDT 2009; root:xnu-1228.12.14~1/RELEASE_I386 Architecture du processeur : i386 coruscant chris$
Nous l'avons vu dans l'article précédent, PIR
dispose des opérateurs
arithmétiques nécessaires pour effectuer toutes les opérations de base
exigées par la programmation. La nouveauté est que toutes les opérations
peuvent être utilisées au moyen d'un opérateur d'assignement (+=
, -=
,
*=
, /=
, >>=
...).
coruscant chris$ cat assigne.pir .sub main .local int nombre .const int dix = 10 nombre = 25 nombre += dix say nombre $I3 = 500 $I3 /= dix say $I3 nombre = 1 nombre <<= 4 say nombre .end coruscant chris$ parrot assigne.pir 35 50 16 coruscant chris$
Nous avons déjà dit que la seule contrainte est de ne pouvoir effectuer qu'une seule opération à la fois. Toute expression arithmétique doit donc être décomposée en autant de calculs élémentaires que nécessaire.
Par contre, PIR
est aussi parfaitement outillé en ce qui concerne les
instructions capables de réaliser des fonctions d'un certain niveau
de complexité (factorielle, exponentiation...).
coruscant chris$ cat facto.pir .sub _main .local int nombre, factorielle .local pmc STDIN STDIN = getstdin print "Quel est le nombre ? " $S0 = readline STDIN chopn $S0, 1 nombre = $S0 factorielle = fact nombre print "La factorielle de " print nombre print " est egale a : " say factorielle .end coruscant chris$ parrot facto.pir Quel est le nombre ? 14 La factorielle de 14 est egale a : 1278945280 coruscant chris$
PIR
, nous l'avons dit, ne dispose pas d'instructions évoluées de type
boucle
. L'instruction goto
honnie par des générations de programmeurs,
est de ce fait incontournable, et comme dans tout langage autorisant
l'utilisation du goto
, chaque instruction peut être étiquetée.
les étiquettes sont représentatives d'adresses symboliques, elles seront utilisées comme destination dans les ruptures de séquence conditionnelles ou inconditionnelles.
Dès qu'il est question de cette instruction tant décriée, je recommande de consulter l'article paru dans Linux Mag N° 72 [goto].
En PIR
une étiquette peut se présenter sous deux formes :
LABEL: _LABEL:
Les caractères disponibles sont ceux autorisés pour les noms de variables, à savoir les lettres majuscules ou minuscules, les chiffres et le blanc souligné. Toutefois, pour des raisons de lisibilité, on recommande toujours d'identifier les étiquettes par des lettres majuscules et de les présenter sur une ligne vide.
Instruction ... Instruction ... ETIQUETTE: Instruction ... Instruction ...
Une étiquette servant d'adresse de destination chaque fois qu'une rupture de séquence conditionnelle ou inconditionnelle apparaît, cette représentation permet de mettre en évidence le début du bloc d'instructions susceptible d'être référencé lors d'un branchement. Ainsi, le programme en gagne en clarté et en lisibilité.
Les règles qui régissent les étiquettes sont les suivantes :
les étiquettes peuvent être locales ou globales
un nom d'étiquette locale doit impérativement commencer par une lettre
un nom d'étiquette globale commence toujours par un blanc souligné (_
)
un nom d'étiquette doit être unique dans son unité de compilation (étiquette locale)
un label local ne sera accessible que dans l'unité de compilation dans laquelle il a été défini.
Cette dernière remarque nous donne donc la possibilité d'utiliser des noms de label locaux identiques dans des unités de compilation distinctes.
Ces instructions vont permettre de rompre de manière conditionnelle ou inconditionnelle le déroulement séquentiel du programme.
PIR
devant rester assez proche de la machine virtuelle et des
instructions de base, nous avons déjà remarqué qu'il ne dispose pas
de structures complexes (for
, while
ou until
).
goto
.C'est l'instruction de base. Elle permet de dérouter le programme de manière arbitraire vers une adresse donnée repérée par son label.
coruscant chris$ cat goto.pir .sub "Application goto" :main goto L1 L2: print "Second affichage.\n" end L1: print "Premier affichage. \n" goto L2 .end coruscant chris$ parrot goto.pir Premier affichage. Second affichage. coruscant chris$
Cette instruction est incontournable, mais il ne faut pas en abuser et son utilisation doit respecter les règles de la programmation. Vu sous cet angle, le programme que nous venons d'écrire n'aurait jamais du exister.
Comme en PASM
elle peut être réalisée qu'en utilisant les instructions
if
ou unless
et leur comportement est exactement le même qu'en
Perl
ou PASM
.
Ces instructions (if
ou unless
) seront contrôlées par le résultat de
l'évaluation d'une expression booléenne qui peut prendre plusieurs formes.
Première forme, le test d'une variable. Dans ce cas, le résultat sera
considéré comme faux si la variable contient la valeur undef
vraie
dans le cas contraire.
.sub condition .local int x * * * x = ligne if x goto NU $S0 = "x est undef.\n" goto FIN NU: $S0 = "x n'est pas undef.\n" FIN: print S0 * * * .end
C'est ce type de test qui va nous permettre lors de l'accès en lecture
à un fichier d'en détecter la fin (Ctrl D
).
Seconde forme, l'évaluation d'une expression booléenne au moyen des
opérateurs de comparaison <
, <=
, ==
, !=
, >
, >=
.
.sub condition .local int x * * * if x < 10 goto INF $S0 .= "x est superieur ou egal a 10.\n" goto FIN INF: $S0 .= "x est inferieur a 10.\n" FIN: print $S0 * * * .end
Troisième forme, l'évaluation d'un objet PMC
.
Nous avons déjà évoqué le fait qu'un PMC
est représentatif d'une
classe, il peut donc avoir été déclaré mais ne pas avoir été instancié.
.sub condition .local pmc chaine # chaine = new 'String' # chaine = "Salut a tous." if null chaine goto NE print "Le pmc existe.\n" goto FIN NE: print "Le pmc n'existe pas.\n" FIN: .end
Dans ce dernier exemple, le fait de laisser les deux lignes commentées,
permet de déclarer le PMC
mais pas de l'instancier, il n'a de ce fait
pas d'existence et son test donnera un résultat faux
.
Pour que le résultat du test soit vrai, il suffit de dé-commenter les
deux lignes d'instanciation.
while
et until
.Elles doivent être construites au moyen de :
if
pour le until.
unless
pour le while.
Si nous considérons le programme Perl construit autour d'une boucle
while
:
$x = 0; while ($x <= 5) { print "Valeur : $x.\n"; $x += 1 } print "Fin de la boucle.\n";
Le programme effectuant la même opération en PIR
sera le suivant :
coruscant chris$ cat while.pir .sub "while" :main .local int x x = 0 $S0 = "Valeur : " DEBUT: unless x < 5 goto FIN print $S0 print x print ".\n" x += 1 goto DEBUT FIN: print "Fin de la boucle. \n" .end coruscant chris$ parrot while.pir Valeur : 0. Valeur : 1. Valeur : 2. Valeur : 3. Valeur : 4. Fin de la boucle. coruscant chris$
Ecrivons un programme identique mais utilisant cette fois une boucle until :
$x = 0; until ($x == 5) { print "Valeur : $x.\n"; $x += 1 } print "Fin de la boucle\n";
Sa traduction en PIR
sera :
coruscant chris$ cat until.pir .sub "until" :main .local int x x = 0 $S0 = "Valeur : " DEBUT: if x == 5 goto FIN print $S0 print x print ".\n" x += 1 goto DEBUT FIN: print "Fin de la boucle. \n" .end coruscant chris$ parrot until.pir Valeur : 0. Valeur : 1. Valeur : 2. Valeur : 3. Valeur : 4. Fin de la boucle. coruscant chris$
for
.Prenons une nouvelle fois comme référence un programme Perl :
for ($i=0; $i<5; $i++) { print "Valeur : $i.\n"; } print "Fin de la boucle\n";
Il sera écrit en PIR :
coruscant chris$ cat for.pir .sub "until" :main .local int x x = 0 DEBUT: unless x < 5 goto FIN print "Valeur : " print x print ".\n" x += 1 goto DEBUT FIN: print "Fin de la boucle. \n" .end coruscant chris$ parrot for.pir Valeur : 0. Valeur : 1. Valeur : 2. Valeur : 3. Valeur : 4. Fin de la boucle. coruscant chris$
Ce qui est démontré dans ces quelques exemples c'est qu'il est parfaitement possible de limiter l'utilisation des ruptures de séquence au strict minimum nécessaire.
Pour ceux qui le souhaitent, il existe dans le répertoire
parrot-1.x.0/runtime/parrot/include
un fichier qui s'appelle
hllmacros.pir
.
Ce fichier met à la disposition des usagers un ensemble de macros permettant d'émuler un certain nombre d'instructions de haut niveau.
.macro IfElse(conditional, true, false) .macro While(conditional, code) .macro DoWhile(code, conditional) .macro For(start, conditional, cont, code) .macro Foreach(name, array, code)
Elles sont accompagnées de nombreux exemples et leur utilisation facilite la programmation pour les usagers qui le souhaitent.
Nous utilisons fréquemment le terme Unité de compilation et nous l'avons déjà défini comme étant un morceau de code qui représente une entité de programmation.
Dans certain cas de figure, il peut être utilisé pour désigner la totalité du fichier source, mais dans la majorité des cas, il ne désignera qu'un groupe de lignes représentatives d'un ensemble compact d'instructions.
Toutefois, cette organisation devra respecter certaines contraintes sur lesquelles nous allons revenir en détail.
Pour commencer, prenons le programme suivant qui va calculer et imprimer la factorielle d'un nombre. Il a été écrit dans une unique unité de compilation et sans appel de sous-programme.
coruscant chris$ cat factorielle.pir .sub "facto" :main .local string chaine .local int nombre .local int factorielle .local pmc STDIN print "Quel est le nombre : " STDIN = getstdin chaine = readline STDIN nombre = chaine factorielle = 1 print "La factorielle de " print nombre BOUCLE: factorielle *= nombre nombre -= 1 if nombre goto BOUCLE print " est egale a " print factorielle print "\n" .end coruscant chris$ parrot factorielle.pir Quel est le nombre : 10 La factorielle de 10 est egale a 3628800 coruscant chris$
Une première évolution évidente nous conduit à la création d'un sous-programme tout en conservant une unique unité de compilation.
Cette modification nous conduit au code suivant :
coruscant chris$ cat factorielle.pir .sub "Factorielle" :main .local string chaine .local int nombre .local int factorielle .local int compteur .local pmc STDIN print "Quel est le nombre : " STDIN = getstdin chaine = readline STDIN nombre = chaine # Appel du sous-programme. bsr FACTORIELLE print "La factorielle de " print nombre print " est egale a " print factorielle print "\n" end # sous-programme. FACTORIELLE: factorielle = 1 compteur = nombre BOUCLE: factorielle *= compteur compteur -= 1 if compteur goto BOUCLE ret .end coruscant chris$ parrot factorielle.pir Quel est le nombre : 5 La factorielle de 5 est egale a 120 coruscant chris$
Dans ces lignes de code, l'ensemble d'instructions qui commence à
l'étiquette FACTORIELLE:
et qui se termine par l'instruction ret
représente du code réutilisable en tant que fonction.
Ce type d'approche va néanmoins poser un certain nombre de problèmes.
Tout d'abord en terme d'interface entre le programme appelant et le
sous-programme. Ici, il n'y a un argument à transmettre au
sous-programme FACTORIELLE
. Ce passage se fait par nom en
utilisant la variable (nombre
) de l'unité de compilation.
Ceci implique que l'appelant doit savoir quel est le nom et quel
est le type du paramètre que va récupérer la fonction.
Le même problème se pose pour la valeur de retour qui se fait par
l'intermédiaire de la variable factorielle
.
Le fait que le module principal et le sous-programme doivent se partager
la même unité de compilation n'est donc pas une bonne solution.
Les deux blocs d'instruction seront analysés et traités comme un unique
morceau de code et devront se partager un environnement commun,
en particulier deux PMC
sur lesquels
nous reviendrons ultérieurement, LexInfo
et LexPad
.
La bonne méthode, celle que nous allons présenter maintenant, consiste à déclarer deux sous-sections représentatives de deux unités de compilation, la première qui va regrouper les instructions du sous-programme, la seconde celles du module principal
Tout programmeur sait qu'il n'est pas envisageable d'écrire une quelconque application sans avoir la possibilité de définir des sous-programmes.
Dans la majorité des applications, on dispose de lignes de code stockées dans des bibliothèques de fonctions et de modules réutilisables dans de multiples endroits. Le sous-programme représente la base incontournable dans la notion de code réutilisable.
Cette fonctionnalité est constamment utilisée lorsqu'on écrit du code en
PIR
, en fait, comme nous l'avons déjà constaté, le langage est
entièrement basé sur cette présentation
car tout code PIR
est un sous-programme qui est déclaré et ne peut
exister qu'en tant que tel.
Il a été dit à plusieurs reprises que le programme de plus haut niveau,
le programme principal, est lui même un sous-programme qu'on a coutume de
référencer :main
par la suite, d'autres sous-programmes sont créés
et appelés pour réaliser l'ensemble des opérations nécessaires.
C'est aussi l'utilisation de sous-programmes, au sens PIR
du terme
qui nous permettra d'écrire des morceaux de code pour déclarer des objets
et y attacher des méthodes.
Nous allons maintenant présenter dans le détail la manière de réaliser et de déclarer les sous-programmes, de quelle façon s'opère transmission de la liste de paramètres et enfin, de savoir comment les utiliser au mieux pour développer des applications complexes.
Nous avons vu que la machine virtuelle Parrot est, au final, destinée à supporter de multiples langages [Langages], chacun d'eux disposant de ses propres conventions et sa propre syntaxe pour gérer la définition et l'appel de ses fonctions.
Le but de PIR
n'étant pas d'être lui même un langage de haut niveau,
il se doit de proposer les outils de base afin que chaque langage qui sera
implémenté par son intermédiaire puisse les utiliser pour réalises ses
propres fonctionnalités.
C'est pour cette raison que la syntaxe de PIR
pour l'utilisation des sous
programmes est d'une grande simplicité.
Les PPC
(Parrot Calling Conventions) [PPC] décrivent en détail comment
la machine virtuelle doit faire référence à un sous-programme, doit gérer
la rupture de séquence puis récupérer l'adresse de retour.
Elles définissent aussi comment sera transmise la liste des paramètres et
de quelle manière seront récupérés les résultats. Elles sont écrites
en partie en langage C
et en partie en PASM
(Parrot Assembly).
Les détails de fonctionnement d'un sous-programme ou d'une fonction restent
cachés à la grande majorité des programmeurs car ces derniers n'ont
généralement pas besoin de les connaître. PIR
dispose de multiples
constructions pour procéder à cette dissimulation.
Le principe des Parrot Calling Conventions est basé sur la CPS
(Continuation Passing Style) pour transférer le contrôle à un
sous-programme et gérer la pile des adresses de retour.
Si, comme nous venons de le dire, le grande majorité des utilisateurs peuvent totalement ignorer les détails du mécanisme qui gèrent ces actions, il n'en est pas de même pour tous ceux qui souhaitent en exploiter toute les capacités et qui, de ce fait, doivent en connaître le fonctionnement détaillé.
Il ne faut pas se le cacher, la gestion d'un sous-programme cache une grande complexité.
Au niveau de la structure de base de la machine virtuelle, cela va se
traduire par l'instanciation d'un PMC
sous-programme.
Il est ensuite nécessaire de créer un PMC
de continuation pour gérer
l'adresse de retour à la fin du sous-programme, puis transmettre la
liste d'arguments, résoudre l'adresse symbolique représentée par le nom
du sous-programme en question et, en fin de compte, renvoyer les résultats
et les mémoriser aux emplacements qui auront été spécifiés au moment de
l'appel (variables ou registres).
Tout ce travail étant destiné à la gestion d'une seule et unique instruction, l'appel à la fonction, ne prend pas en compte le complexité de l'exécution du sous-programme lui-même. Il est donc évident que l'instruction d'appel à un sous-programme apparaîtra incroyablement simple en comparaison de la difficulté du travail sous-jacent.
À la base, l'appel d'un sous-programme en PIR
est très proche, bien que
moins flexible, de ce qu'on a l'habitude de voir dans n'importe quel
langage de haut niveau.
Nous avons aussi vu dans nos précédents exemples que la syntaxe de PIR
est
particulièrement prolixe, cet état de choses présente certains avantages.
Ainsi, l'exemple suivant montre comment sera extraire la référence à un
sous-programme nom
de la table des symboles globaux pour le stocker dans
un PMC $P1
afin d'y faire référence.
find_global $P1, "nom" .begin_call .arg Valeur1 .arg Valeur2 .call $P1 .result $I0 .end_call
L'ensemble des instructions que l'on trouve entre les deux directives
.begin_call
et .end_call
se comporte comme un bloc, la directive
.arg
positionne et transmet les arguments de la liste d'appel, et en
fin de compte, l'instruction .call
fait référence au PMC $P1
préalablement
positionné. Enfin l'instruction .result
nous indique que le résultat renvoyé
par le sous-programme sera stocké dans le registre $I0
.
Nous utiliserons ultérieurement la fonctionnalité que nous venons de décrire.
La définition de l'instruction d'appel d'un sous-programme n'est qu'une partie du problème. Il est aussi important de savoir déclarer le sous-programme et en écrire les lignes de code.
Nous l'avons déjà vu à de nombreuses reprises, c'est la directive .sub
qui
permet de déclarer un sous-programme, en fait, une unité de compilation,
et la directive .end
qui en indique la fin.
.sub "Main" :main * * * .end
La description et le typage des paramètres se fait au moyen de la directive
.param
. Cette dernière, outre le fait qu'elle définit les paramètres,
créée aussi pour chacun d'eux une variable locale.
.param int c
Enfin, la directive .return
indique que le sous-programme se termine et,
optionnellement, positionne la valeur qui sera retournée en fin de calcul.
.return (valeur)
Si nous reprenons l'exemple de la factorielle en appliquant ce qui vient
d'être dit, le sous-programme FACTORIELLE
devient une unité de
compilation au même titre que le programme principal main
.
Dans ces conditions, PIR
résoudra le nom des diverses unités de compilation
de la même manière que sont traitées les étiquettes dans un programme.
coruscant chris$ cat factorielle.pir # sous-programme. .sub factoriel .param int nombre .local int factorielle factorielle = 1 BOUCLE: if nombre == 1 goto FIN factorielle *= nombre nombre -= 1 branch BOUCLE FIN: .return (factorielle) .end .sub "main" :main $P0 = getstdin .local int nb .local int resultat print "Donnez moi une valeur entiere : " $S0 = readline $P0 nb = $S0 resultat = factoriel(nb) print "La factorielle de " print nb print " est egale a " print resultat print ".\n" end .end coruscant chris$ parrot factorielle.pir Donnez moi une valeur entiere : 6 La factorielle de 6 est egale a 720. coruscant chris$
Analysons les lignes que nous venons d'écrire.
On commence par définir une unité de compilation factoriel
, qui sera aussi
un sous-programme, au moyen de la directive que nous connaissons .sub
.
Ce sous-programme va récupérer comme paramètre, une valeur entière, qui lui
sera transmise par l'intermédiaire de la variable locale nb
.
Comme nous l'avons dit, c'est la directive .param
qui fait savoir au sous
programme qu'il y a un paramètre à récupérer, que ce paramètre est une valeur
entière (int
) et qu'elle sera mémorisée dans la variable locale nombre
.
Nous aurons par ailleurs besoin d'une variable locale entière factorielle
qui sera initialisée à 1 et qui nous servira à effectuer le calcul.
La boucle permet de manière très classique d'effectuer le
produit des valeurs successives de nb
à 1
au moyen de la variable
compteur
.
À la fin, on retourne la valeur qui a été calculée et mémorisée dans la
variable factorielle
au moyen de l'instruction .return (factorielle)
.
Le programme principal se contente de définir une valeur entière nb
qui
sera lue et contiendra la valeur dont nous désirons calculer la factorielle.
Elle sera passée comme paramètre au sous-programme qui vient d'être défini.
La valeur de retour récupérée dans la variable locale resultat
sera alors
affichée.
Pour illustrer ceci, revoici un classique de la programmation, l'énumération de Conway.
Ce programme va nous permettre de mettre en évidence, outre le passage de la liste de paramètres, plusieurs caractéristiques que nous avons étudié jusqu'à présent.
coruscant chris$ cat conway.pir .sub "Enumeration_Conway" :main .local string ch_ref, ch_res, car_ref, car_comp .local int occurence, nb_lignes nb_lignes = 11 print "1\n" ch_ref = "1" CALCUL: occurence = 0 ch_res = "" substr car_ref,ch_ref,0,1 EXPLORE: occurence += 1 substr ch_ref,ch_ref,1 unless ch_ref goto FINI substr car_comp,ch_ref,0,1 if car_ref == car_comp goto EXPLORE ch_res = CONSTRUIRE(occurence, car_ref, ch_res) car_ref = car_comp occurence = 0 goto EXPLORE FINI: ch_res = CONSTRUIRE(occurence, car_ref, ch_res) print ch_res print "\n" ch_ref = ch_res nb_lignes -= 1 if nb_lignes goto CALCUL .end .sub CONSTRUIRE .param int occurence .param string car_ref .param string ch_res .local string c c = occurence c .= car_ref ch_res .= c .return (ch_res) .end coruscant chris$ parrot conway.pir 1 11 21 1211 111221 312211 13112221 1113213211 31131211131221 13211311123113112211 11131221133112132113212221 3113112221232112111312211312113211 coruscant chris$
Ici, le sous-programme CONSTRUIRE
récupère une liste de trois valeurs
représentatives de trois chaînes de caractères et retourne un résultat, lui
aussi sous forme d'une chaîne de caractères.
Dans la méthode de transmission que nous venons de voir, il y a plusieurs paramètres, et ils doivent être transmis dans un ordre strict.
Les paramètres sont alors appelés positionnels et c'est la correspondance entre leur ordre dans la liste d'appel et l'ordre dans lequel ils sont déclarés en tant que paramètres dans le sous-programme qui permet d'effectuer le passage des valeurs.
Il existe une autre manière de transmettre les paramètres à un sous
programme, on appelle cette méthode les paramètres nommés
.
Ici, l'ordre est quelconque et c'est le nom qui va permettre
d'effectuer l'affectation de la bonne valeur au bon paramètre.
coruscant chris$ cat parametres.pir .sub ident .param string prenom :named ("prenom") .param string nom :named ("nom") .param string mail :named ("mail") print "Votre prenom est : " print prenom print ".\n" print "Votre nom est : " print nom print ".\n" print "Votre mail est : " print mail print ".\n" .end .sub "main" :main .local string n .local string p .local string m n = "Aperghis" p = "Christian" m = "chris@aperghis.fr" ident("mail" => m, "nom" => n, "prenom" => p) .end coruscant chris$ parrot parametres.pir Votre prenom est : Christian. Votre nom est : Aperghis. Votre mail est : chris@aperghis.fr. coruscant chris$
La liste d'appel du sous-programme indique que la valeur d'appel contenue
dans la variable locale m
sera récupérée dans le corps du sous-programme
par la variable locale mail
, et le raisonnement est identique pour
les autres valeurs.
C'est dans le corps du sous-programme que la variable mail
est déclarée
comme étant une chaîne de caractères passée par l'intermédiaire d'un
paramètre nommé (:named
).
L'intérêt des arguments nommés est de s'affranchir, dans le cas de listes de paramètres un peu trop longues, d'éventuelles erreurs dues à une inversion accidentelle dans l'ordre des valeurs de la liste d'appel.
Certains paramètres peuvent ne pas être présents lors de l'appel. On parle dans ce cas de paramètres optionnels.
Dans les langages qui proposent cette facilité, le principe est de pouvoir transmettre une valeur si le paramètre est présent dans la liste d'appel ou de prendre une valeur par défaut dans le cas contraire.
En fait, PIR
ne dispose pas cette caractéristique en tant que telle, mais
il propose une solution pour faire en sorte que certains paramètres puissent
ne pas se présenter.
C'est un indicateur spécifique attaché au paramètre qui va nous permettre de savoir si le paramètre optionnel en question s'est vu affecter une valeur par l'intermédiaire de la liste d'appel.
En définitive, un paramètre déclaré comme étant optionnel peut être vu comme contenant deux informations distinctes, la première étant la valeur éventuellement transmise, la seconde représentant l'indicateur qui va permettre de savoir si, effectivement, une valeur lui a été affectée.
.param string parametre :optional .param int present :opt_flag
La directive :optional
permet d'indiquer que le paramètre
correspondant représente une valeur optionnelle et que, de ce fait, va
lui être attaché un indicateur present
défini par la directive
:opt_flag
.
C'est son contenu qui permet de savoir de manière effective si une valeur
a été transmise au paramètre par l'intermédiaire de la liste d'appel (1
)
ou si aucune valeur n'a été spécifiée (0
).
Le test de cet indicateur permet alors, si nécessaire, d'affecter une valeur par défaut au paramètre en question ou de prendre toute décision en fonction du calcul à effectuer.
coruscant chris$ cat optionnel.pir .sub valeur .param num valeur :optional .param int defaut :opt_flag print "Indicateur : " print defaut print ".\n" if defaut==0 goto DEFAUT print "Parametre transmis : " print valeur print ".\n" .return() DEFAUT: valeur = 0 print "Aucun parametre transmis, on donne la valeur par defaut.\n" .end .sub "main" :main .local num n print "Appel avec une valeur effective (3,14159265).\n" n = 3.14159265 valeur(n) print "Appel sans parametre.\n" valeur() .end coruscant chris$ parrot optionnel.pir Appel avec une valeur effective (3,14159265). Indicateur : 1. Parametre transmis : 3.14159265. Appel sans parametre. Indicateur : 0. Aucun parametre transmis, on donne la valeur par defaut. coruscant chris$
Il est à noter que les paramètres optionnels peuvent indifféremment être
des paramètres positionnels ou des paramètres nommés, toutefois, lorsqu'on
les utilise avec des paramètres nommés, ils doivent impérativement
apparaître à la fin de la liste, après les paramètres positionnels.
De plus, la directive :opt_flag
doit nécessairement se trouver
immédiatement après la directive :optional
.
Première erreur :
.sub 'parametres' .param int valeur_optionnelle :optional .param int indicateur :opt_flag .param pmc valeur <- Cette ligne est fausse.
On aurait dû écrire :
.sub 'parametres' .param pmc valeur .param int valeur_optionnelle :optional .param int indicateur :opt_flag
Seconde erreur :
.sub 'parametres' .param int indicateur :opt_flag .param int valeur_optionnelle :optional <- Faux.
On aurait dû écrire :
.sub 'parametres' .param int valeur_optionnelle :optional .param int indicateur :opt_flag
Troisième erreur :
.sub 'parametres' .param int valeur_optionnelle :optional .param pmc valeur <- Faux. .param int indicateur :opt_flag
On aurait dû écrire :
.sub 'parametres' .param pmc valeur .param int valeur_optionnelle :optional .param int indicateur :opt_flag
Et il est aussi possible de mélanger des paramètres optionnels et des paramètres nommés.
Nous allons illustrer ceci avec l'exemple d'un sous-programme qui calcule la racine n-ième d'un nombre, et ce, quel que soit n.
Le sous-programme récupère deux paramètres nommées, la Valeur
et la
Racine
, la particularité étant que le second paramètre peut être
absent. Si c'est la cas, la valeur par défaut sera 2 et on procédera
au calcul de la racine carrée.
coruscant chris$ cat optionnel.pir .sub racine .param num N :named ("Valeur") .param num Exp :named ("Racine") :optional .param int ind :opt_flag .local num X0, X1 .local int I if ind == 1 goto RACINE # Parametre optionnel absent, on prend 2 par defaut. Exp = 2 RACINE: X0 = N I = 0 BOUCLE: I += 1 $N2 = Exp - 1.0 $N1 = X0 * $N2 $N2 = X0 ** $N2 $N2 = N / $N2 X1 = $N1 + $N2 X1 /= Exp $N2 = X0 - X1 if $N2 > 0 goto OK $N2 = - $N2 OK: if $N2 < 0.00000000000001 goto FIN X0 = X1 goto BOUCLE FIN: .return (I, X1) .end .sub "Racine nieme d'un nombre" :main .local num e, pi, Rac .local int i e = 2.71828183 pi = 3.14159265 (i, Rac) = racine ("Valeur" => pi, "Racine" => e) print "Racine e-ieme de pi = " say Rac print "Trouvee en " print i say " iterations." (i, Rac) = racine ("Valeur" => 10) print "Racine carree de 10 = " say Rac print "Trouvee en " print i say " iterations." .end coruscant chris$ parrot optionnel.pir Racine e-ieme de pi = 1.52367105385469 Trouvee en 7 iterations. Racine carree de 10 = 3.16227766016838 Trouvee en 7 iterations. coruscant chris$
N'abandonnons pas les bonnes habitudes. La tradition veut que pour illustrer
la notion de récursivité on prenne comme exemple la fonction factorielle
.
return (n > 1 ? n * _fact(n - 1) : 1)
Pour changer un peu, au lieu de se contenter de calculer une simple factorielle, le programme proposé va calculer les factorielles des n premiers nombres entiers.
coruscant chris$ cat facto.pir .sub _factoriel .param int valeur .local int factorielle if valeur > 1 goto RECURSION factorielle = 1 goto RETOUR RECURSION: $I0 = valeur - 1 factorielle = _factoriel($I0) factorielle *= valeur RETOUR: .return (factorielle) .end .sub _main :main .local int facto, nombre print "Calcul des factorielles des cinq premiers nombres entiers.\n" nombre = 0 BOUCLE: facto = _factoriel(nombre) print "La factorielle de " print nombre print " est egale a " print facto print ".\n" inc nombre if nombre <= 5 goto BOUCLE .end coruscant chris$ parrot facto.pir Calcul des factorielles des cinq premiers nombres entiers. La factorielle de 0 est egale a 1. La factorielle de 1 est egale a 1. La factorielle de 2 est egale a 2. La factorielle de 3 est egale a 6. La factorielle de 4 est egale a 24. La factorielle de 5 est egale a 120. coruscant chris$
Une continuation peut être considérée comme une photographie instantanée, une sorte d'image figée de l'état courant de l'exécution de la machine virtuelle. Une fois qu'une continuation aura été définie, elle peut être invoquée pour retourner à l'emplacement du programme ou elle a été créée.
C'est une étape dans le déroulement séquentiel du programme qui permet au développeur de transférer le contrôle du programme à une adresse précédent enregistrée.
En fait, ce concept n'est pas vraiment une nouveauté. Des langages comme Lisp ou Scheme proposent ce type d'outil depuis longtemps [Continuation].
Toutefois, il faut noter que en dépit de son intérêt, cette facilité n'a pas vraiment été utilisé de manière optimale quel que soit le langage considéré.
Le but affiché par la machine virtuelle Parrot et par le langage Parrot Intermediate Representation est de modifier profondément cette tendance.
Sur cette plate-forme, toute manipulation du contrôle de flux y compris les appels de méthodes, de sous-programmes ou de coroutines sont réalisés au moyen du mécanisme de continuation.
Si ce mécanisme est généralement caché aux développeurs qui se contentent de réaliser des applications, il est disponible pour tous ceux qui souhaitent d'en utiliser toute la puissance et la flexibilité dans la gestion de leurs sous-programmes.
On appellera CPS
(Continuation Passing Style) l'ensemble des contrôles
de flux utilisant le mécanisme de continuation.
Cette technique permet à la machine virtuelle de proposer tout un ensemble de fonctionnalités telles l'optimisation de l'appel en queue (Tail Calls) ou les sous-programmes lexicaux.
Il existe des cas de figure dans lesquels une routine sera mise en place simplement pour faire appel à un autre sous-programme [TailCall1], le but étant en définitive de retourner le résultat du second appel.
On appelle cette technique tail call [TailCall2] et elle représente une occasion à ne pas manquer pour optimiser le code.
Voici un exemple Perl :
coruscant chris$ cat TC.pl sub plus_deux { my ($valeur) = @_; $valeur = plus_un($valeur); return plus_un($valeur); } sub plus_un { my ($val) = @_; return (++$val) } $A = 10; print " Resultat final : ", plus_deux($A), "\n"; coruscant chris$ perl TC.pl Resultat final : 12 coruscant chris$
Si nous regardons cet exemple de manière attentive, nous constatons que le
sous-programme plus_deux
fait deux appels successifs au sous-programme
plus_un
, le second appel étant simplement utilisé comme valeur de retour.
Jamais une valeur renvoyée par le sous-programme plus_un
n'est mémorisée
à un quelconque emplacement mémoire spécifique dans le sous-programme
plus_deux
.
Ce type de situation peut facilement être optimisé en utilisant le même emplacement mémoire pour récupérer la valeur renvoyée. C'est ainsi que les deux appels réutiliseront un espace commun qui va aussi servir pour renvoyer la valeur de retour au lieu d'en créer un nouveau à chaque appel.
En PIR
, ceci pourrait se présenter comme suit :
coruscant chris$ cat tailcall.pir .sub plus_un .param int val val = val + 1 .return (val) .end .sub plus_deux .param int valeur valeur = plus_un (valeur) valeur = plus_un (valeur) .return (valeur) .end .sub "main" :main .local num n n = 10 n = plus_deux(n) print "Valeur finale : " say n .end coruscant chris$ parrot tailcall.pir Valeur finale : 12 coruscant chris$
En fait, il existe en PIR
une directive .tailcall
qui permet de réaliser
cette opération de manière plus efficace que la directive .return
.
coruscant chris$ cat tailcall.pir .sub plus_un .param int val val = val + 1 .return (val) .end .sub plus_deux .param int valeur valeur = plus_un (valeur) .tailcall plus_un (valeur) .end .sub "main" :main .local num n n = 10 n = plus_deux (n) print "Valeur finale : " say n .end coruscant chris$ parrot tailcall.pir Valeur finale : 12 coruscant chris$
C'est cette directive qui permet d'optimiser le processus en réutilisant la continuation de la fonction père pour effectuer l'appel.
Dans la majorité des cas, les continuations sont utilisées de manière implicite dans le flot de contrôle de multiples opérations de la machine virtuelle. C'est ce que nous avons vu jusqu'à présent.
Toutefois le programmeur peut les gérer de manière explicite lorsque
qu'il le désire. Dans ce cas, une continuation sera un PMC
tout à fait
ordinaire et, de ce fait, déclaré comme tel au moyen du constructeur new
.
$P0 = new 'Continuation'
Lors de sa création, cette continuation possède un état indéfini, le fait d'y faire référence immédiatement après sa création se soldera par la génération d'une exception.
Pour positionner la continuation dans le but de l'exécuter, il est nécessaire
de lui assigner une étiquette au moyen de la directive set_addr
.
$P0 = new 'Continuation' set_addr $P0, LABEL
Voyons ce mécanisme sur un exemple simple.
coruscant chris$ cat continuation.pir .sub Produit .param int a .param int b .local int s s = a + b .begin_return .set_return s .end_return .end .sub "main" :main .const "Sub" $P0 = "Produit" $P1 = new 'Continuation' set_addr $P1, RETOUR .local int x .local int y x = 10 y = 25 .begin_call .set_arg x .set_arg y .call $P0, $P1 RETOUR: .local int r .get_result r .end_call print "Valeur de retour : " say r .end coruscant chris$ parrot continuation.pir Valeur de retour : 35 coruscant chris$
La ligne .call $P0, $P1
indique que l'on désire exécuter le sous
programme Produit
référencé par l'intermédiaire de $P0
et que
l'adresse ou doit se continuer le programme après l'exécution du
sous-programme est celle indiquée par le contenu de $P1
.
Deux instructions permettent de disposer d'informations sur le
déroulement du programme. La première getfile
permet de récupérer
dans une variable string
le nom du fichier représentatif du programme
sans avoir à acquérir la totalité de la ligne de
commande, la seconde getline
permet de connaître le numéro de la
ligne courante.
coruscant chris$ cat suivi.pir .sub "Main" :main .local int x, Ligne .local string Nom x = 0 Nom = getfile print "Nom du programme : " say Nom DEBUT: unless x < 3 goto FIN print "Valeur : " print x print ".\n" Ligne = getline print " On est sur la ligne : " say Ligne x += 1 goto DEBUT FIN: Ligne = getline print " Maintenant on est sur la ligne : " say Ligne print "Fin de la boucle. \n" .end coruscant chris$ parrot suivi.pir Nom du programme : test.pir Valeur : 0. On est sur la ligne : 13 Valeur : 1. On est sur la ligne : 13 Valeur : 2. On est sur la ligne : 13 Maintenant on est sur la ligne : 19 Fin de la boucle. coruscant chris$
Ces indications ne permettent pas vraiment une mise au point du programme, tout au plus, elles donnent des indications sur son déroulement.
Si on désire un suivi beaucoup plus précis de l'exécution du programme,
on dispose d'une opération spécifique trace Booleen
. L'instruction
trace 1
permet d'activer le mécanisme de
tracé du programme alors que l'instruction trace 0
y met fin.
Lorsqu'il est activé, ce suivi donne nombre d'indications qui permettent connaître avec beaucoup de précision l'instruction qui est en cours d'exécution et l'état des variables à ce moment.
coruscant chris$ cat trace.pir .sub "until" :main .local int x x = 0 trace 1 DEBUT: unless x < 2 goto FIN print "Valeur : " say x x += 1 goto DEBUT FIN: trace 0 print "Fin de la boucle. \n" .end coruscant chris$ parrot trace.pir 5 le 2, I0, 13 I0=0 9 print "Valeur : " Valeur : 11 say I0 I0=0 0 13 add I0, 1 I0=0 16 branch -11 5 le 2, I0, 13 I0=1 9 print "Valeur : " Valeur : 11 say I0 I0=1 1 13 add I0, 1 I0=1 16 branch -11 5 le 2, I0, 13 I0=2 18 trace 0 Fin de la boucle. coruscant chris$
On voit bien que la rencontre de l'instruction trace 1
active l'affichage de toutes les instructions qui s'exécutent
et la valeur de la donnée qui est concernée. Si de plus, l'instruction
est une rupture de séquence conditionnelle, l'adresse du label
est lui aussi précisé.
L'instruction unless x < 2 goto FIN
apparaît sous la forme
le 2, I0, 13
indiquant que si la valeur 2 est strictement
inférieure à $I0, on continue à l'adresse 13
, représentative
du label FIN
. La valeur du registre apparaît elle aussi ce
qui permet de suivre l'exécution de l'instruction.
On dispose d'un PMC
Timer
qui permet de procéder à un décompte
du temps.
Les macros décrites dans le fichier timer.pasm
seront utilisées
pour positionner les diverses valeurs. Les deux valeurs principales sont
.PARROT_TIMER_SEC
qui donne le nombre de secondes à décompter et
.PARROT_TIMER_HANDLER
qui spécifie quel est le sous-programme à
appeler lorsque le décompte de temps arrive à zéro.
coruscant chris$ cat chronometre.pir .include 'timer.pasm' # Constantes .sub Termine print "\n" say "Fin du decompte." exit 0 .end .sub main :main $P0 = new 'Timer' $P1 = get_global 'Termine' # sous-programme à appeler en fin de decompte. $P0[.PARROT_TIMER_HANDLER] = $P1 # Decompte de 5 secondes. $P0[.PARROT_TIMER_SEC] = 5 # Lancement du decompte. $P0[.PARROT_TIMER_RUNNING] = 1 $I0 = 0 BOUCLE: # Decompte des secondes. print $I0 print " " $I0 += 1 sleep 1 goto BOUCLE .end coruscant chris$ parrot chronometre.pir 0 1 2 3 4 5 Fin du decompte coruscant chris$
Ce type de code peut servir à temporiser une action pour éviter que le programme se bloque sur un quelconque événement.
Nous avons exploré dans ce second volet un certain nombre des particularités
de PIR
. On peut se rendre compte que ce type de programmation ne se
rattache à rien vraiment défini.
De l'assembleur PASM
il a conservé la rusticité et la manière de
programmer.
Mais, certaines de ses tournures syntaxiques sont proches de celles qu'on peut trouver dans les langages de plus haut niveau.
Contrairement à l'assembleur de base, il nous propose un ensemble d'abstractions qui vont permettre à un utilisateur de ne pas avoir à se préoccuper de l'architecture de la machine sur laquelle il programme, voire même à l'ignorer totalement, écrivant cependant rapidement un code extrêmement optimisé pour la plate-forme considérée.
La plupart des éléments qui, au niveau de l'assembleur, présentent
une quelconque difficulté sont cachés par l'ensemble des directives
proposées par PIR
.
En définitive, PIR
est d'un usage plus facile tout en permettant de
conserver toutes les fonctionnalités d'un langage d'assemblage.
[goto] goto Perl, par Philippe Bruhat et Jean Forget, in GNU/Linux Magazine France n°72, mai 2005, http://articles.mongueurs.net/magazines/linuxmag72.html
[Langages] Site officiel de la machine virtuelle Parrot, http://www.parrot.org/languages
[PPC] PDD 3: Calling Conventions, http://www.parrotcode.org/docs/pdd/pdd03_calling_conventions.html
[Continuation] Continuation dans les langages de programmation http://fr.wikipedia.org/wiki/Continuation
[TailCalls1] Squawks of the Parrot http://www.sidhe.org/~dan/blog/archives/000211.html
[TailCalls2] Tail Call http://en.wikipedia.org/wiki/Tail_call
Copyright © Les Mongueurs de Perl, 2001-2011
pour le site.
Les auteurs conservent le copyright de leurs articles.