Article publié dans Linux Magazine 122, décembre 2009.
Copyright © 2009 Christian Aperghis-Tramoni
Nous entamons ici une série d'articles consacrés au langage PIR
,
et ce premier chapitre a pour but de le présenter. PIR
signifie
Parrot Intermediate Representation
, qui comme son nom l'indique
est un langage moins rustique, plus abordable et plus lisible
que l'assembleur de base PASM
.
Tous les programmes présentés ici sont disponibles en téléchargement sur mon site : http://www.dil.univ-mrs.fr/~chris/Documents/progs01.pod
Parrot Intermediate Representation
est le langage de niveau
intermédiaire [Lang Int] installé de manière native
sur la machine virtuelle Parrot
.
Après quelques rappels sur les notions de base nécessaires pour en comprendre la philosophie générale, nous étudierons de manière plus détaillée les caractéristiques propres à ce langage.
Il y a quelques temps, une série d'articles sur l'assembleur Parrot [PASM01], [PASM02], [PASM03] nous avait permis de démystifier le langage de base de la machine virtuelle.
Cet assembleur, extrêmement proche de la structure interne de la machine pouvait poser, comme tout langage de très bas niveau, un certain nombre de problèmes à ceux qui ne sont pas familiers avec la manipulation d'un jeu d'instructions élémentaire. L'obligation de devoir, en toute circonstance, respecter les contraintes imposées par l'architecture de la machine, serait-elle virtuelle, peut apparaître comme une lourde servitude.
Fondamentalement, si Parrot Intermediate Representation
peut être
presque considéré comme un langage d'assemblage, il dispose de quelques
caractéristiques de haut niveau qui en rendent l'utilisation plus
abordable.
PIR
sera principalement utilisé pour écrire les bibliothèques ainsi
que certains des compilateurs, et c'est en PIR
que seront traduits
les langages de haut niveau.
C'est ce langage que nous allons détailler. Un langage un peu plus évolué offrant de multiples possibilités tout en permettant à ceux qui le désirent de rester lié à la machine.
Cette première partie qui traite essentiellement des données pourrait sembler quelque peu rébarbative. C'est pourtant le passage incontournable pour pouvoir aborder facilement la programmation dont nous parlerons ultérieurement.
Un langage intermédiaire est généralement défini comme étant le moyen de programmer une machine virtuelle.
Typiquement, un tel langage répond à trois critères fondamentaux :
Chaque instruction doit représenter exactement un code opération
Il ne peut pas comporter de structures de contrôle.
On dispose d'un grand nombre (voire un nombre infini) de registres.
Parrot Intermediate Representation
(PIR
) est ainsi un nouveau
moyen de programmer directement la machine Parrot [Parrot].
Ce langage va, dans un certain sens, permettre de s'affranchir
quelque peu de certaines des contraintes liées au contexte de la
machine virtuelle tout en conservant un niveau de programmation
suffisamment bas pour pouvoir tirer au mieux parti de la structure
interne de la plate-forme.
Ces deux points peuvent, à priori, paraître contradictoires, mais nous verrons qu'il n'en est rien.
Il faut aussi noter que si à l'origine PIR
devait se présenter comme une
couche au dessus de l'assembleur (PASM
), ces deux représentations ont,
sémantiquement, quelque peu divergé et ne sont plus dans l'état actuel
du développement aussi directement liées l'une à l'autre.
PIR
peut-il être considéré comme un vrai langage ?
Tant par la manière d'aborder les programmes que par la syntaxe de base de ses opérateurs le langage devrait paraître familier aux programmeurs. À quelques détails près, par exemple la représentation des expressions arithmétiques, ses caractéristiques ne sont pas si éloignées de l'ancien Fortran IV des années 1950 [Back01].
Il n'en demeure pas moins, si on le compare aux outils de programmation actuellement disponibles, un dialecte d'assez bas niveau, qui reste par de nombreux côtés étroitement lié aux caractéristiques intrinsèques de la machine virtuelle.
C'est cette double spécificité qui aurait tendance à le rendre particulièrement attractif et agréable à utiliser.
Une troisième langage, de niveau nettement supérieur, est également
proposé dans la distribution [NQP].
Il s'agit de NQP
(Not Quite Perl
) qui, comme
son nom l'indique est un petit langage représentant un sous-ensemble de
Perl 6 [Perl6] et développé à l'origine comme un outil destiné à la
réalisation du compilateur Perl 6.
Il est lui aussi intégré dans la boite à outils PCT
(Parrot
Compiler Toolkit
).
Par convention, les fichiers Parrot sont identifiés par leur extension.
Rappelons que l'extension .pasm
(parrot assembly) indique qu'il
s'agit d'assembleur de base, et que l'extension .pcb
(parrot
byte code) identifie un fichier exécutable en byte code parrot.
Dans notre cas, c'est l'extension .pir
qui va permettre de
spécifier que le fichier soumis à Parrot contient du code PIR
.
Dans l'avenir, plusieurs langages de haut niveau seront disponibles sur
la machine Parrot. PIR
est principalement destiné à la concrétisation
de ces futurs langages.
PIR
.C'est dans la syntaxe de ses déclarations que PIR se distingue de la majorité des langages de bas niveau par une meilleure flexibilité. Mais, si cette caractéristique facilite son utilisation en regard à ce qui est de mise dans les assembleurs, il faut reconnaître que la manipulation des données est beaucoup plus rigide et plus proche de l'organisation interne de la machine qu'elle ne l'est dans les ensembles plus évolués de type Perl.
En fait, PIR
peut être, si on le désire, assez étroitement lié à
l'agencement de propre de la plate-forme Parrot, et on constate
vite que, moyennant quelques changements mineurs, les instructions
PIR
rappellent fortement la programmation de base du système.
Nombre d'options supplémentaires qui lui ont été ajoutées ont pour but d'en faciliter la lisibilité et d'améliorer la qualité de la programmation.
Dans PIR
il existe un délimiteur d'instruction qui est
le saut de ligne (\n
), de plus, en fonction des principes des
langages intermédiaires, chaque instruction qui ne peut
définir qu'une unique opération doit impérativement se présenter sur
une seule ligne.
À noter par ailleurs que les lignes vides ne sont pas prises en compte,
elles sont considérées comme une instruction vide.
Toutes les instructions, y compris les instructions vides, peuvent être labellisées, c'est à dire repérées par une référence symbolique. Ceci est fondamental pour pouvoir programmer les sauts et les branchements.
Tout ce qui est compris entre le caractère dièse (#
) et la fin de la
ligne sera considéré comme un commentaire et il est possible d'utiliser
les marqueurs POD
pour réaliser la documentation en ligne.
Rappelons que Parrot
est organisée autour de quatre jeux de 32 registres
chacun.
32 registres IV
(entiers) (I0
.. I31
)
32 registres NV
(flottants) (N0
.. N31
)
32 registres STRING
(chaînes de caractères) (S0
.. S31
)
32 registres PMC
(Parrot Magic Cookie) (P0
.. P31
)
PIR
.Afin de faciliter la lecture et la réalisation des applications,
PIR
propose à son utilisateur quelques représentations de haut niveau.
Toutefois, si on désire rester aussi proche que possible de la structure
de Parrot, le programme peut accéder à chaque registre explicitement
désigné.
Dans ce cas, le nom de l'emplacement concerné est précédé du caractère
dollar ($
).
$N10 = 3.14 $S1 = "Bonjour\n"
Le registre réel 10 est positionné à la valeur 3,14 et on stocke la chaîne considérée dans le registre chaîne de caractères numéro 1.
Mais, comme nous le verrons plus loin, on peut disposer de noms symboliques pour désigner ses données. Dans ce cas, une variable nommée apparaîtra telle quelle.
e = 2.71828183
La variable nommée e
prend la valeur 2.71828183
Nous détaillerons aussi dans la seconde partie comment construire des commandes moins élémentaires à partir des mots clés et des opérateurs.
if compteur <= limite goto LABEL
Dans cette instruction, le séquencement se poursuit
à l'adresse référencée LABEL
si le contenu de la variable compteur
est inférieur ou égal au contenu de la variable limite
, dans le cas
contraire, la séquence normale est respectée.
Toutes ces caractéristiques seront détaillées au fur et à mesure.
Il est toutefois important de noter que PIR
n'a pas et n'aura jamais
de structures de haut niveau telles que les boucles bornées (for
) ou non
bornées (while
, until
). Lorsque leur usage s'avérera indispensable,
elles devront être réalisées au moyen des instructions de base proposées
par le langage if
, unless
et goto
.
Cet état de choses peut rendre la programmation quelque peu amphigourique, il existe toutefois des règles d'écriture qui, si elles sont respectées, permettent de rendre ce type de codage lisible et accessible à tout le monde.
Comme sur toute plate-forme, le programmeur dispose de plusieurs emplacements pour stocker les données sur lesquelles il travaille.
Il faut cependant noter que, au niveau de la machine virtuelle, si les
registres représentent les seuls endroits où il soit possible de
mémoriser une valeur destinée à être manipulée, il existe
de multiples manières d'y faire référence, et PIR
est là pour nous aider
à le faire.
Nous avons vu que le nom d'un registre commence systématiquement par le
caractère dollar ($
). Ce caractère est toujours suivi d'une lettre en
majuscule qui indique sur quel type de valeur on doit travailler :
I
pour un registre entier.
N
pour un registre flottant.
S
pour un registre chaîne de caractères.
P
pour un registre PMC
Cette lettre est obligatoirement suivie du numéro de l'emplacement concerné.
$S25 = "Bonjour tout le monde.\n" $N10 = 2.71828183 $I31 = 1984
Mais, bien que le nombre de registres de la machine soit de 32 pour chacun des types (numérotés de 0 à 31), il est possible d'en utiliser un nombre quasiment infini. C'est ainsi que pour toutes les valeurs supérieures à 31, les positions spécifiées seront considérées comme des variables ne nécessitant pas de déclaration préalable. Pour simplifier les choses, on peut imaginer que les quatre ensembles de stockage ne sont en définitive que des vecteurs indicés par leur numéro.
$S2555 = "Bonjour tout le monde.\n" $N9999 = 2.71828183 $I2948 = 1984
En revanche, le fait d'en multiplier le nombre va automatiquement avoir une influence sur l'efficacité du programme, en raison des problèmes liés à l'allocation et à la récupération des données.
Si on désire optimiser les performances, il est préférable de n'utiliser que
les 32 registres réels de la machine pour chacun des types disponibles.
Ceci étant, il faudra garder en mémoire que le numéro de l'emplacement que
l'on spécifie ne correspondra pas automatiquement au numéro réel.
C'est un allocateur de mémoire qui va se charger de traduire
les références utilisés dans PIR
$S10
, $I20
$N19
en une
adresse mémoire.
Ce mécanisme a été mis en place afin d'utiliser au mieux l'espace de stockage en récupérant les emplacements libérés au lieu d'en utiliser de nouveaux.
En fin de compte, le programmeur n'aura à se soucier ni du nombre ni de l'allocation des données qu'il utilise. Il est libre d'utiliser autant de ressources qu'il le désire tout en restant conscient que ses choix auront une incidence sur les performances finales de son programme.
La machine virtuelle parrot
, comme nous l'avons vu, dispose de quatre
types de données de base, les nombres entiers, les nombres flottants,
les chaînes de caractères, et les PMC
. PIR
est donc capable de
manipuler les valeurs correspondantes.
Entiers :
$I10 = 2009 # Nombre positif. $I15 = -1 # Nombre négatif. $I20 = 0xA5 # Valeur hexadécimale. $I25 = 0b01010 # Valeur binaire.
Flottants :
$N0 = 3.14 # Nombre décimal. $N1 = 4 # Nombre décimal. $N2 = 1.2e-4 # Notation scientifique.
Chaîne de caractères en simple ou double quote :
$S20 = "Bonjour tout le monde" $S21 = 'Bonjour tout le monde'
Dans une chaîne représentée entre double quotes le mécanisme de substitution est activé.
$S30 = "Voici une chaîne\nsur deux lignes"
Alors qu'il est désactivé dans une chaîne située entre des simples quotes.
$S0 = '... \n Cette chaîne contient le caractère \ suivi du caractère n."
Le caractère \ (antislash) permet de générer des séquences d'échappement :
\xhh
un caractère en représentation hexadécimale à 2 chiffres.
\ooo
un caractère en représentation octale jusqu'à 3 chiffres.
\uhhhh
un caractère en représentation hexadécimale à 4 chiffres
\Uhhhhhhhh
un caractère en représentation hexadécimale à 8 chiffres
\x(h.. h)
un caractère en représentation hexadécimale jusqu'à 8 chiffres
\CX
le caractère de contrôle X
\a
, \b
, \t
, \n
, \v
, \f
, \r
, \e
, \\
, \"
ont leur signification usuelle
Il est aussi possible de déclarer un here document
à la manière de Perl.
$S28 = <<"FIN" Le nombre pi, noté par la lettre grecque du même nom, toujours en minuscule est le rapport constant entre la circonférence d'un cercle et son diamètre. Il est appelé aussi constante d'Archimède. Des valeurs approchées de pi courantes sont Approximativement 3,1416 Approximativement sqrt(10) Approximativement 22/7. FIN
Attention, la chaîne de terminaison (FIN
) qui se trouve sur la dernière
ligne ne doit être précédée d'aucune espace.
Ce sont, sans surprise les opérateurs classiques.
$I3 = $I0 + $I1 # Addition $I8 = $I0 - $I1 # Soustraction $I1 = $I0 * $I1 # Multiplication $I5 = $I0 / $I1 # Division $I6 = $I0 % $I1 # Modulo
La seule différence par rapport aux langages évolués étant de ne pouvoir représenter qu'une opération par ligne, il faut donc décomposer les expressions arithmétiques en autant de calculs élémentaires que nécessaire.
Elles fonctionnent en court circuit, c'est à dire que, aussitôt que le résultat peut être déterminé, l'évaluation s'arrête, sinon elle se poursuit.
coruscant chris$ cat logique.pir .sub main $I0 = 0 && 1 # Va retourner 0 $I1 = and 1, 2 # Va retourner 2 $I2 = 1 || 0 # Va retourner 1 $I3 = or 0, 5 # Va retourner 5 $I4 = 1 ~~ 5 # Va retourner 0 $I5 = xor 0, 9 # Va retourner 9 print $I0 print " " print $I1 print "\n" print $I2 print " " print $I3 print "\n" print $I4 print " " print $I5 print "\n" .end coruscant chris$ parrot logique.pir 0 2 1 5 0 9 coruscant chris$
Il est possible d'utiliser indifféremment les fonctions or
, and
, xor
ou les opérateurs ||
, &&
, ~~
.
Ce sont les opérateurs bit à bit.
coruscant chris$ cat booleen.pir .sub main $I0 = 98 & 121 # 01100010 & 01111001 = 01100000 $I1 = band 98, 121 # C'est a dire 96. $I2 = 98 | 121 # 01100010 | 01111001 = 01111011 $I3 = bor 98, 121 # C'est a dire 123 $I4 = 98 ~ 121 # 01100010 | 01111001 = 00011011 $I5 = bxor 98, 121 # C'est a dire 27 print $I0 print " " print $I1 print "\n" print $I2 print " " print $I3 print "\n" print $I4 print " " print $I5 print "\n" .end coruscant chris$ parrot booleen.pir 96 96 123 123 27 27 coruscant chris$
Il ici aussi possible d'utiliser indifféremment les fonctions
bor
, band
, bxor
ou les opérateurs |
, &
, ~
.
La notion d'unité de compilation
en PIR
rappelle fortement
ce que l'on appelle sous-programme ou méthode dans les langages de
plus haut niveau.
En Parrot Intermediate Representation
, tout code doit impérativement
être défini dans une unité de compilation.
Celle-ci commence toujours par la directive .sub
et se
termine par la directive .end
.
Une des caractéristiques principales de ce type de structuration est que, sauf spécification explicite sur laquelle nous reviendrons ultérieurement, toute variable ou, de manière plus surprenante, tout registre, ne sera visible que dans l'unité de compilation dans laquelle elle (il) est déclarée.
Voyons sur quelques exemples le comportement d'un programme PIR
.
coruscant chris$ cat unite.pir .sub main print "Bonjour tout le monde.\n" .end coruscant chris$ parrot unite.pir Bonjour tout le monde. coruscant chris$
Dans ces quelques lignes de programme, nous avons défini une unité de
compilation et nous l'avons référencée main
. Si nous ne précisons rien
de plus, ce sera toujours la première unité de compilation rencontrée
dans le programme qui sera exécutée au lancement, et ce, quel que soit
son nom. Ici, il n'y en a qu'une.
coruscant chris$ cat unite.pir .sub premiere print "Unite de compilation : Premiere.\n" .end .sub main print "Unite de compilation : Main.\n" .end coruscant chris$ parrot unite.pir Unite de compilation : Premiere. coruscant chris$
Dans ce second exemple, nous avons déclaré deux unités de compilation.
La première est identifiée premiere
, elle sera exécutée lors du
lancement du programme et va afficher un message. Une fois ce travail
effectué, l'apparition de la directive .end
va mettre fin au
programme et, de ce fait, la seconde unité de compilation main
sera ignorée.
coruscant chris$ cat unite.pir .sub premiere print "Unite de compilation : Premiere.\n" main() print "Fin de : Premiere.\n" .end .sub main print "Unite de compilation : Main.\n" .end coruscant chris$ parrot unite.pir Unite de compilation : Premiere. Unite de compilation : Main. Fin de : Premiere. coruscant chris$
Dans ce nouvel exemple, la première unité de compilation rencontrée est
toujours celle référencée premiere
, c'est donc elle qui sera exécutée
lors du lancement, mais, elle fait ici appel à l'autre unité main
qui
sera alors considérée comme un sous-programme et exécutée à son tour.
Le contrôle sera rendu à l'unité de compilation appelante à
la fin de main
.
coruscant chris$ cat unite.pir .sub premiere print "Unite de compilation : Premiere.\n" .end .sub "main" :main print "Unite de compilation : Main.\n" .end coruscant chris$ parrot unite.pir Unite de compilation : Main. coruscant chris$
Dans ces nouvelles lignes de code l'unité de compilation main
a été
identifiée par une chaîne de caractères, et, bien que ce ne soit pas la
première du programme, c'est elle qui prendra la main au moment du
lancement. Il est important de noter que, dans ce cas de figure, c'est
uniquement le fait que l'unité en question soit identifiée par une
chaîne de caractères qui lui confère ce statut et en aucun cas le
contenu de cette chaîne.
Le programme aurait tout aussi bien se présenter comme suit :
coruscant chris$ cat unite.pir .sub premiere print "Unite de compilation : Premiere.\n" .end .sub "Le programme commence ici." :main print "Le programme commence ici.\n" .end coruscant chris$ parrot unite.pir Le programme commence ici. coruscant chris$
On peut aussi ne pas donner de nom à l'unité de compilation que l'on désire voir s'exécuter en premier en remplaçant la chaîne par le caractère blanc souligné (_).
.sub _ :main print "Le programme commence ici.\n" .end
Le programme se terminera dès qu'il
trouve une instruction end
, et ce, où qu'elle se présente.
coruscant chris$ cat unite.pir .sub premiere print "Unite de compilation : Premiere.\n" .end .sub "Le programme commence ici" :main premiere() print "Avant le 'end' dans main.\n" # Directive de fin de programme. end print "Apres le 'end' dans main.\n" .end coruscant chris$ parrot unite.pir Unite de compilation : Premiere. Avant le 'end' dans main. coruscant chris$
Dans ce cas de figure, l'apparition de l'instruction end
, met fin à
l'exécution du programme, le second message de main
ne sera donc
jamais affiché.
PASM
.Il est toujours possible à partir d'un programme écrit en PIR
de générer
le programme PASM
correspondant. C'est au moyen de l'option -o
que
cette propriété est activée.
coruscant chris$ cat assemb.pir .sub "main" :main $S10 = "Bonjour tout le monde.\n" print $S10 .end coruscant chris$ parrot -o assemb.pasm assemb.pir coruscant chris$ cat assemb.pasm main: set S0, "Bonjour tout le monde.\n" print S0 set_returns returncc coruscant chris$
À l'analyse, on constate avec surprise que le programme généré n'a pas suivi les directives qui lui avaient été données en utilisant un registre différent de celui qui lui a été indiqué. Nous comprendrons ultérieurement pourquoi.
Dans la machine virtuelle, chaque chaîne est associée à un codage et à un jeu de caractères. Par défaut, le jeu de caractères est le 8-bit ASCII. Il est simple à utiliser et universellement reconnu. Il est toutefois possible d'utiliser d'autres formats.
Dans le cas d'une constante de chaîne déclarée entre doubles quotes, un préfixe optionnel permet de préciser soit seulement le jeu de caractères, soit simultanément le jeu de caractères et l'encodage de la chaîne en question.
Les chaînes ainsi déclarées seront alors automatiquement converties lorsque cela s'avérera nécessaire afin de préserver l'intégrité de l'information.
Il se présente sous la forme suivante :
chaine = encodage:jeu_de_caractères:"Chaîne de caractères."
Par exemple :
ch1 = utf8:unicode:"Chaîne en Unicode utf8." ch2 = utf16:unicode:"Chaîne en Unicode utf16" ch3 = ascii:"Chaîne en Ascii 8 bits." ch4 = binary:"Chaîne binaire non formatée."
À noter que, en ce qui concerne l'encodage binaire, la structure sera traitée comme un tampon de données brutes non formatées, en fait, ce n'est pas vraiment une chaîne en soi, car les données binaires ne peuvent pas toujours être considérées comme des caractères réellement lisibles. Ce type de codage sera principalement utilisé dans le cas de des bibliothèques de fonctions qui seraient susceptibles de retourner des données binaires difficilement rattachables à un type standard connu.
Pour que le codage utf16:unicode
soit pris en compte, il est
impératif que le support ICU
soit activé [ICU].
Il est aussi important de prendre en compte le fait que, pour spécifier
les encodages et le jeu de caractères, le mécanisme de substitution doit
avoir été activé ("
) dans les chaînes considérées. Si tel n'est pas le
cas ('
) ni l'encodage ni le jeu de caractères ne seront pris en compte.
Si, dans une concaténation, deux chaînes sont regroupées, le jeu de
caractères et l'encodage doivent impérativement être identiques. Dans le
cas contraire, PIR
procédera à l'actualisation des chaînes mises
en présence en utilisant le format compatible immédiatement supérieur.
Les chaînes ASCII seront converties en UTF-8 et UTF-8 sera converti en UTF-16.
concat
.C'est l'instruction qui va nous permettre de concaténer deux chaînes de caractères.
coruscant chris$ cat concatene.pir .sub "main" :main $S10 = "Linux" $S11 = "Magazine" $S0 = concat $S10, " " $S0 = concat $S0, $S11 print $S0 print "\n" .end coruscant chris$ parrot concatene.pir Linux Magazine coruscant chris$
Si on le désire, il est aussi possible d'utiliser l'opérateur .
(point).
$S10 = $S10 . $S11
substr
.L'instruction substr
supporte jusqu'à quatre paramètres.
Elle se présente sous la forme : substr ss_ch,ch_ref,debut,longueur
.
coruscant chris$ cat chaine.pir .sub "main" :main $S0 = "Linux Magazine." substr $S1,$S0,0,9 print $S1 print "\n" .end coruscant chris$ parrot chaine.pir Linux Mag coruscant chris$
Elle se comporte exactement comme la fonction Perl, la longueur peut être omise si on désire conserver toute la fin de la chaîne, et l'index peut être négatif si on désire compter les caractères à partir de la droite.
Si aucune destination n'est spécifiée pour la sous-chaîne, c'est à dire si le second paramètre est une valeur numérique, la sous-chaîne spécifiée sera remplacée par la chaîne transmise en tant que quatrième paramètre.
index
.L'instruction index
permet de déterminer l'indice du début d'une
sous-chaîne dans une chaîne de référence, index ch_ref, ss_ch
.
Cette information peut alors être utilisée dans l'instruction substr
que nous venons de voir pour procéder à une substitution de sous-chaîne.
coruscant chris$ cat index.pir .sub "main" :main .local string chaine chaine = "Il fait beau et chaud." $I1 = index chaine, "beau" print "La sous chaine commence a l'indice " print $I1 print "\n" # On substitue "mauvais" à "beau" substr chaine, $I1, 4, "mauvais" print chaine print "\n" .end coruscant chris$ parrot index.pir La sous chaine commence a l'indice 8 Il fait mauvais et chaud. coruscant chris$
C'est la sous-chaîne qui commence en $I1
de longueur 4 (beau) qui est
la cible de la substitution. La chaîne qui a été remplacée peut être
récupérée si nécessaire :
$S0 = substr chaine, $I1, 4, "mauvais"
Dans ce cas de figure, le registre $S0 contient la chaîne "beau"
.
length
.Appliquée à une chaîne de caractères, elle va permettre d'en récupérer la longueur.
coruscant chris$ cat longueur.pir .sub "main" :main .local string chaine .local int longueur chaine = "Bonjour tout le monde.\n" longueur = length chaine print "Longueur de la chaine : " print longueur $S0 = "Il fait beau.\n" $I1 = length $S0 print "\nLongueur de la chaine : " print $I1 print "\n" .end coruscant chris$ parrot longueur.pir Longueur de la chaine : 23 Longueur de la chaine : 14 coruscant chris$
chopn
Pour retirer un certain nombre de caractères dans une chaîne de référence,
on dispose de l'instruction chopn
qui se présente sous la forme
générale :
destination = chopn chaine, val
Elle permet de retirer val
caractères à la fin de la chaîne de référence.
coruscant chris$ cat chop.pir .sub "main" :main .local string ch, dest ch = "0123456789" dest = chopn ch, 3 print dest print "\n" .end coruscant chris$ parrot chop.pir 0123456 coruscant chris$
Si le nombre spécifié dans l'instruction chopn
est négatif, il indique
le nombre de caractères à conserver en début de chaîne.
coruscant chris$ cat chop.pir .sub "main" :main .local string ch, dest ch = "0123456789" dest = chopn ch, -4 print dest print "\n" .end coruscant chris$ parrot chop.pir 0123 coruscant chris$
Si la destination n'est pas précisée, l'opération est effective sur la chaîne elle-même.
coruscant chris$ cat chop.pir .sub "main" :main .local string ch ch = "0123456789" chopn ch, 3 print ch print "\n" .end coruscant chris$ parrot chop.pir 0123456 coruscant chris$
Comme c'est le cas dans Perl, cette instruction va nous
permettre, lors des accès en lecture de retirer le caractère \n
à la fin de la chaîne qui vient d'être acquise.
Il n'est jamais très parlant pour un programmeur d'adresser
une donnée sous la forme brutte $S10
. Il est beaucoup plus pratique et
infiniment plus agréable de faire référence à des variables en utilisant
des noms explicites indice
, limite
, prenom
...
Si nous avons déjà profité de cette facilité dans quelques-uns des programmes qui viennent d'être présentés, nous allons maintenant en détailler l'utilisation.
Avant de pouvoir accéder à une variable, il est nécessaire de la déclarer,
c'est la directive .local
qui va permettre de le faire et de procéder
au typage de la donnée qui lui sera affectée.
.local type liste_de_variables
Les quatre types disponibles sont :
int
pour déclarer une variable entière.
num
pour déclarer une variable flottante.
string
pour déclarer une chaîne de caractères.
pmc
pour déclarer une classe parrot Parrot Magic Cookie
.
Ils correspondent aux quatre types de registres disponibles sur la machine virtuelle.
Une fois déclarées, ces variables peuvent être référencées dans toutes les lignes de code de l'unité de compilation à laquelle elles appartiennent.
coruscant chris$ cat variables.pir .sub _ :main .local string bonjour .local num e .local int limite bonjour = "Bonjour tout le monde.\n" print bonjour e = 2.71828183 print e print "\n" limite = 1000 print limite print "\n" .end coruscant chris$ parrot variables.pir Bonjour tout le monde. 2.71828183 1000 coruscant chris$
Le nom d'une variable répond aux mêmes règles que dans Perl (lettres, chiffres et blanc souligné), le premier caractère étant impérativement une lettre ou un blanc souligné.
Il n'y a pas de limite pour le nombre de caractères pouvant constituer un nom de variable. Il ne faut cependant pas oublier que gérer des identificateurs trop longs demande un gros travail d'allocation mémoire et un certain manque d'efficacité au moment de l'analyse syntaxique.
Lorsque nous avons demandé la génération du programme PASM
correspondant à un code PIR
, nous avons constaté que la machine ne
suivait pas les directives qui lui avaient été données.
Nous avions fait référence à un registre chaîne de caractères
($S10
) et la lecture du fichier PASM
correspondant fait apparaître que
c'est en définitive le registre $S0
que la machine décide d'utiliser.
Cette décision arbitraire peut s'expliquer par le fait que, en théorie,
le nombre de registres adressables est, comme nous l'avons vu, illimité
et qu'il ne sera pas toujours possible au système d'obéir aveuglément aux
directives qui lui sont données.
En fait, PIR
utilise un mécanisme d'allocation qui affecte à chaque
structure déclarée un emplacement spécifique en mémoire, l'allocateur
est en définitive un optimiseur qui va calculer et analyser la durée de
vie de chaque élément (registre ou variable) afin de déterminer à quel
moment il va être utilisé et à quel moment il ne le sera plus.
La réutilisation des espaces ainsi libérés permet de diminuer les
exigences en ressources.
C'est d'ailleurs cette phase d'allocation qui réclame le plus de temps et de moyens en terme de calcul lors de la compilation du programme.
Si on en éprouve le besoin ou la nécessité, il est parfaitement possible d'invalider ce mécanisme en demandant que les affectations de registres soient maintenues.
Cette caractéristique peut se révéler particulièrement utile dans un
sous-programme qui n'utilise qu'un nombre réduit d'espace pour des
variables locales qui ne seront utilisables que dans l'unité
de compilation courante, ou bien lorsqu'un pointeur doit faire référence
à une variable pour retourner une valeur. Par exemple dans le cas d'un appel
de fonction NCI
Native Call Interface
qui permet d'accéder à la
plupart des modules écrits en C
.
C'est la directive spécifique :unique_reg
qui va permettre de
mettre en application cette contrainte.
coruscant chris$ cat unique.pir .sub _ :main .local string chaine :unique_reg .local int entier :unique_reg .local num pi :unique_reg .local pmc ch :unique_reg chaine = "Bonjour.\n" print chaine entier = 10 print entier print "\n" pi = 3.14159 print pi print "\n" ch = new 'String' ch = "Salut a tous.\n" print ch .end coruscant chris$ parrot unique.pir Bonjour. 10 3.14159 Salut a tous. coruscant chris$
Il est important de noter que la directive unique_reg
n'affecte en aucun
cas le comportement du programme mais se contente de modifier la manière
dont les registres sont alloués et affectés aux variables concernées.
Ce n'est en définitive qu'un compromis entre l'occupation mémoire et
l'optimisation du temps d'exécution du sous-programme, ce dernier n'ayant
plus à se préoccuper de l'allocation des ressources.
Parrot Magic Cookie
.La notion de PMC
a déjà été longuement décrite dans les précédents articles
sur l'assembleur Parrot. Rappelons que ce sont des registres spécifiques
qui ont la capacité de représenter n'importe quel type de structure
(nombre entier, nombre flottant, chaîne de caractères, objet).
On peut dire, pour faire court, qu'un PMC
permet de définir un type qui se
comporte de manière spécifique et qui va utiliser un agencement caractéristique
appelée v-table
pour référencer des méthodes particulières applicables
aux objets qu'il doit décrire [v-table].
De plus, un certain nombre de fonctions appropriées permettent à
l'utilisateur de remplacer l'implémentation d'une méthode de base,
héritée de la définition de la classe, par
une séquence alternative qu'il aura lui même écrite, cette opération,
la surcharge
ou polymorphisme ad-hoc
consiste à traiter certains
opérateurs comme des fonctions qui peuvent être définis ou redéfinis
pour de nouveaux types de données [Surcharge].
Physiquement, un registre PMC
va contenir une référence vers une v-table
qui n'est elle-même rien d'autre qu'une liste de pointeurs vers des
fonctions dont le code réalise l'opération voulue pour le PMC
concerné.
En fait, une v-table
n'est rien d'autre qu'un descripteur d'objet
qui contient la représentation les caractéristiques et les références
des méthodes dynamiquement liées à l'objet en question.
On active une méthode en accédant à l'adresse du code définissant l'action à effectuer dans la table correspondant au descriptif de l'objet considéré.
Toute instruction qui fait référence à un PMC
, utilise de ce fait
la v-table
qui lui a été associée au moment de sa création pour accéder
à la méthode appropriée pour l'opération demandée.
Essentiellement, les PMC
héritent d'une classe de base définie par le
langage et exécutent les opérations réclamées en accord avec les
caractéristiques spécifiques inhérentes aux structures concernées.
Toutes ces notions seront largement détaillées lorsque nous aborderons la programmation orientée objet.
PMC
.Il existe un nombre conséquent de PMC
disponibles dans la distribution
de base.
Par la suite, nous en utiliserons principalement deux types pour déclarer des scalaires ou des structures :
les PMC permettant de déclarer un type scalaire.
String
Pour déclarer une chaîne de caractères.
Integer
Pour déclarer un entier.
Float
Pour déclarer un nombre en virgule flottante.
les PMC permettant de déclarer une structure.
Array
Pour déclarer une liste d'éléments hétérogènes.
FixedBooleanArray
ou ResizableBooleanArray
Pour déclarer une liste de booléens de longueur fixe ou variable.
FixedStringArray
ou ResizableStringArray
Pour déclarer une liste de chaînes de caractères de longueur fixe ou variable.
FixedIntegerArray
ou ResizableIntegerArray
Pour déclarer une liste de valeurs entières de longueur fixe ou variable.
FixedFloatArray
ou ResizableFloatArray
Pour déclarer une liste de valeurs flottantes de longueur fixe ou variable.
FixedPMCArray
ou ResizablePMCArray
Pour déclarer une liste de valeurs flottantes de longueur fixe ou variable.
Nombre d'autres PMC
sont disponibles pour de multiples utilisations.
La liste complète est disponible sur le site de Parrot [PMC].
PMC
.Un PMC
doit être impérativement déclaré et instancié avant toute utilisation.
coruscant chris$ cat cookie.pir .sub "PMC":main .local pmc chaine .local pmc limite .local pmc pi chaine = new 'String' limite = new 'Integer' pi = new 'Float' chaine = "Salut a tous.\n" print chaine limite = 1000 print limite print "\n" pi = 3.14159265 print pi print "\n" .end coruscant chris$ parrot cookie.pir Salut a tous. 1000 3.14159265 coruscant chris$
Dans cet exemple on commence par déclarer trois PMC
, la directive .local
nous permet de créer les variables qui, par la suite, référenceront le PMC
.
On détermine alors pour chacune des variables le type spécifique de PMC
qu'elles vont représenter. C'est par l'intermédiaire du constructeur new
que nous pouvons
spécifier que le premier, référencé chaine
va contenir une chaîne de
caractères (type String
), que le second, référencé limite
, une valeur
entière (type Integer
) et le dernier, pi
, un nombre flottant
(type Float
).
Il est maintenant possible de les utiliser en leur affectant une valeur et en affichant leur contenu.
Il aurait aussi été possible d'adresser directement un registre PMC
sans le référencer par l'intermédiaire d'une variable. Le programme
obtenu est tout aussi valide bien que moins facile à interpréter.
coruscant chris$ cat cookie.pir .sub "PMC":main $P0 = new 'String' $P1 = new 'Integer' $P2 = new 'Float' $P0 = "Salut a tous.\n" print $P0 $P1 = 1000 print $P1 print "\n" $P2 = 3.14159265 print $P2 print "\n" .end coruscant chris$ parrot cookie.pir Salut a tous. 1000 3.14159265 coruscant chris$
Si on désire savoir à quel type d'objet un PMC
a été rattaché,
on dispose de l'instruction typeof
.
On lui transmet la référence d'un PMC
et elle retourne une chaîne de
caractères indiquant à quel type est rattaché le PMC
correspondant.
coruscant chris$ cat type.pir .sub "PMC":main $P0 = new 'String' $P1 = new 'Integer' $P2 = new 'Float' $S0 = typeof $P0 print "Le PMC P0 est de type " print $S0 print ".\n" $S0 = typeof $P1 print "Le PMC P1 est de type " print $S0 print ".\n" $S0 = typeof $P2 print "Le PMC P2 est de type " print $S0 print ".\n" .end coruscant chris$ parrot type.pir Le PMC P0 est de type String. Le PMC P1 est de type Integer. Le PMC P2 est de type Float. coruscant chris$
Le même programme peut être entièrement réécrit de manière beaucoup accessible en utilisant des noms symboliques en lieu et place des références directes aux registres.
coruscant chris$ cat type.pir .sub "PMC":main .local string type .local pmc chaine .local pmc entier .local pmc flottant chaine = new 'String' entier = new 'Integer' flottant = new 'Float' type = typeof chaine print "La variable 'chaine' est de type " print type print ".\n" type = typeof entier print "La variable 'entier' est de type " print type print ".\n" type = typeof flottant print "La variable 'flottant' est de type " print type print ".\n" .end coruscant chris$ parrot type.pir La variable 'chaine' est de type String. La variable 'entier' est de type Integer. La variable 'flottant' est de type Float. coruscant chris$
Une autre possibilité est offerte par l'instruction does
.
Elle se présente sous la forme : does booleen, PMC, "type"
ou
bien booleen = does PMC, "type"
. On obtient en retour
la valeur Vrai
si le PMC
est du type spécifié,
Faux
dans le cas contraire.
coruscant chris$ cat does.pir .sub _main .local int bool1 .local pmc liste liste = new ['ResizableFloatArray'] .local pmc chaine chaine = new ['String'] .local string type .local pmc chaine .local pmc entier .local pmc flottant chaine = new 'String' entier = new 'Integer' flottant = new 'Float' does bool1, chaine, "scalar" print bool1 bool1 = does liste, "array" print bool1 does bool1, chaine, "string" print bool1 bool1 = does entier, "integer" print bool1 does bool1, flottant, "float" print bool1 .end coruscant chris$ parrot does.pir 11111 coruscant chris$
C'est dans ce chapitre que nous allons découvrir tout ce qui se cache sous
l'opération d'affectation (=
).
Comme c'est déjà le cas dans l'assembleur, l'affectation s'accompagne d'un changement de type si cette transformation s'avère nécessaire en fonction du contexte dans lequel elle doit s'exécuter.
C'est le type de la variable ou du registre qui sert de destination à la donnée qui va déterminer si il est nécessaire ou non de procéder à cette conversion.
coruscant chris$ cat conversion.pir .sub "Conversion" :main # Stockage d'un entier. $I0 = 50 print "Affichage de l'entier : " print $I0 print "\n" # Transformation de l'entier en chaîne de caractères. $S0 = $I0 print "Affichage du caractere : " print $S0 print "\n" # Transformation de la chaîne de caractères en un flottant. $N0 = $S0 print "Affichage du flottant : " print $N0 print "\n" # Transformation du flottant en entier. $I0 = $N0 print "Affichage de l'entier : " print $I0 print "\n" .end coruscant chris$ parrot conversion.pir Affichage de l'entier : 50 Affichage du caractere : 50 Affichage du flottant : 50 Affichage de l'entier : 50 coruscant chris$
Bien entendu, cette transformation est aussi effective lorsqu'on utilise des variables.
coruscant chris$ cat conversion.pir .sub "Conversion" :main .local num e .local string chaine .local int chiffre # Stockage d'une chaîne. chaine = "2.71828183" print "Affichage de la chaine : " print chaine print "\n" # Transformation de la chaîne de caractères en flottant. e = chaine print "Affichage du flottant : " print e print "\n" # Transformation de la chaîne de caractères en entier. chiffre = chaine print "Affichage de l'entier : " print chiffre print "\n" .end coruscant chris$ parrot conversion.pir Affichage de la chaine : 2.71828183 Affichage du flottant : 2.71828183 Affichage de l'entier : 2 coruscant chris$
Cette transformation d'une chaîne en entier trouvera son application naturelle dès que nous désirerons lire une information sur un fichier.
Les règles de transformation d'une chaîne de caractères en valeur numérique sont les mêmes que celles qui s'appliquent dans Perl.
coruscant chris$ cat conversion.pir .sub "Conversion" :main .local int entier .local string chaine # Stockage d'une chaîne. chaine = "50 et plus." print "Affichage de la chaine : " print chaine print "\n" # Transformation de la chaîne de caractères en entier. entier = chaine print "Affichage de l'entier : " print entier print "\n" .end coruscant chris$ parrot conversion.pir Affichage de la chaine : 50 et plus. Affichage de l'entier : 50 coruscant chris$
C'est cette caractéristique, reliée à l'opération autoboxing
que
nous allons détailler maintenant, qui permet de procéder à tous les
changements possibles entre les divers types de données.
À la base, Parrot Intermediate Representation
est un langage dynamique,
cette caractéristique apparaît de manière particulièrement significative
dans la manière de gérer les PMC
.
Nous avons vu qu'il existe des registres typés (chaîne, entiers et
flottants) et que les PMC
peuvent de leur côté définir des classes
capables de représenter n'importe lequel des types que nous venons
d'évoquer.
C'est ainsi que les classes de PMC
disponibles chaînes, entiers et
flottants peuvent être sans aucune difficulté transformées en données
scalaires chaînes, entières et flottantes.
PIR
appelle autoboxing
l'opération qui consiste à convertir
l'information lorsqu'on le transfère d'un registre typé S
,
I
ou N
vers une classe PMC
ou inversement.
coruscant chris$ cat autobox.pir .sub "Autoboxing":main # Déclaration et instantiation des PMC. .local pmc chaine chaine = new 'String' .local pmc entier entier = new 'Integer' .local pmc flottant flottant = new 'Float' # Déclaration des valeurs natives. .local string hello .local num pi .local int indice # Affectation des valeurs aux PMC. chaine = "Salut a tous.\n" entier = 1000 flottant = 3.14 # Autoboxing PMC -> valeurs natives. hello = chaine indice = entier pi = flottant # Affectation des valeurs natives. hello = "Comment allez vous ?\n" pi = 3.14159 indice = 25 # Autoboxing valeurs natives -> PMC. chaine = hello flottant = pi entier = indice .end coruscant chris$
La directive .const
va nous permettre de déclarer un nom de
constante. Elle est très semblable à la directive .local
car,
comme elle, elle requiert un type et un nom.
Une constante se voit attribuer une valeur au moment de sa déclaration, et comme pour les variables, elles ne sont visibles que dans l'unité de compilation dans laquelle elles ont été déclarées.
coruscant chris$ cat constantes.pir .sub "constantes":main .const string salut = "Bonjour tout le monde\n" .const int dix = 10 .const num pi = 3.14159265 print salut print dix print "\n" print pi print "\n" .end coruscant chris$ parrot constantes.pir Bonjour tout le monde 10 3.14159265 coruscant chris$
Toute constante doit être déclarée avant d'être utilisée, et, comme on peut s'en douter, il est interdit de modifier une constante dans le cours du programme.
coruscant chris$ cat constantes.pir .sub "constantes":main .const int dix = 10 print dix print "\n" dix = 11 .end coruscant chris$ parrot constantes.pir error:imcc:The opcode 'set_ic_ic' (set<2>) was not found. Check the type and number of the arguments in file 'test.pir' line 6 coruscant chris$
Il est facile de comprendre que l'unité de compilation voit
apparaître une référence à une variable dix
qui n'a jamais
été déclarée dans une directive .local
.
Si on désire déclarer une constante globale, c'est la directive
.globalconst
qui doit être utilisée.
coruscant chris$ cat constantes.pir .sub "constantes":main .globalconst string salut = "Bonjour tout le monde\n" .globalconst int dix = 10 .globalconst num pi = 3.14159265 suite () .end .sub suite print salut print dix print "\n" print pi print "\n" .end coruscant chris$ parrot constantes.pir Bonjour tout le monde 10 3.14159265 coruscant chris$
Cette première approche du langage PIR nous aura permis de définir toutes les notions dont nous aurons besoin pour aborder les divers aspects de la programmation que nous allons détailler dans les futures présentations.
Cette manière d'aborder la programmation pourra surprendre les familiers des langages de bas niveau mais aussi ceux qui utilisent des langages évolués. Elle a néanmoins d'indéniables qualités, permettre l'écriture facile et rapide de programmes grâce à un jeu d'instruction extrêmement varié et particulièrement développé et mettre à la disposition des usagers un système d'allocation qui permet de se détacher quelque peu de la structure de base de la machine virtuelle.
Comme son nom l'indique bien, PIR
est un langage intermédiaire.
[Lang Int] La notion de langage intermédiaire., http://fr.wikipedia.org/wiki/Langage_intermédiaire
[PASM01] La machine Parrot (1), in GNU/Linux Magazine France n°97, septembre 2007, http://articles.mongueurs.net/magazines/linuxmag97.html
[PASM02] La machine Parrot (2), in GNU/Linux Magazine France n°98, octobre 2007, http://articles.mongueurs.net/magazines/linuxmag98.html
[PASM03] La machine Parrot (3), in GNU/Linux Magazine France n°99, novembre 2007, http://articles.mongueurs.net/magazines/linuxmag99.html
[Parrot] Site officiel de la machine virtuelle Parrot, http://www.parrot.org/
[Back01] Specifications for the IBM Mathematical FORmula TRANSlating System par John Backus, International Business Machines Corporation (IBM), 10 Novembre 1954, http://www.computerhistory.org/collections/accession/102679231, http://www.columbia.edu/acis/history/backus.html
[NQP] Not Quite Perl, http://docs.parrot.org/parrot/latest/html/docs/book/ch06_nqp.pod.html
[Perl6] Le site officiel de Perl 6, http://www.perl6.org/
[ICU] International Components for Unicode, http://site.icu-project.org/
[v-table] Virtual method table, http://en.wikipedia.org/wiki/Virtual_table/
[Surcharge] Surcharge des opérateurs, http://fr.wikipedia.org/wiki/Surcharge_des_opérateurs
[PMC] Site officiel de la machine Parrot, http://docs.parrot.org/parrot/latest/html/pmc.html
Copyright © Les Mongueurs de Perl, 2001-2011
pour le site.
Les auteurs conservent le copyright de leurs articles.