Article publié dans Linux Magazine 99, novembre 2007.
Copyright © 2007 Christian Aperghis-Tramoni
Lors des deux premiers articles, nous avons détaillé le fonctionnement des instructions de base de la machine virtuelle Parrot. Nous allons maintenant nous intéresser davantage aux particularités du langage d'assemblage et découvrir que l'on peut faire de la programmation orientée objet en assembleur.
Une macro-instruction [Macro] est un moyen de définir des opérations non répertoriées par le langage de base de la machine à partir du jeu d'instructions réellement disponible. Plus spécifiquement, elle peut être vue comme la création de toutes pièces par le programmeur d'un nouveau code opération dont l'exécution entraînera celle de plusieurs commandes de base. Le comportement est toutefois fondamentalement différent de celui d'un sous-programme, une macro-instruction doit être considérée comme l'association d'un texte de substitution au code qui l'identifie, et ceci chaque fois que nécessaire. De plus, une macro-instruction pourra s'adapter à son environnement au moyen des paramètres syntaxiques qui lui seront transmis.
Il faut par ailleurs noter que cette technique de programmation n'est pas
l'apanage de l'assembleur. En langage C, define permet d'introduire
une macro-définition. Si on désire créer une macro-instruction permettant
de déterminer la valeur absolue d'un nombre, il sera nécessaire de la
déclarer au préalable :
#define abs(x) ((x) < 0 ? - (x) : (x))
Ensuite, chaque fois que le programme y fera référence sous la forme
abs(exp) cette construction sera développée comme :
((exp) < 0 ? - (exp) : (exp))
La définition d'une macro instruction commence par le mot clé .macro suivi
du nom qui lui servira de référence, puis, éventuellement, d'une liste de
paramètres.
Commençons par définir une opération simple. À l'image de l'instruction
readline qui lit une chaîne de caractères terminée par un \n, nous
allons créer une macro-instruction printline qui affiche une valeur
suivie d'un \n.
chris$ cat macro.pasm
.macro printline (A)
print .A
print "\n"
.endm
# Affichage d'un entier.
set I1, 12513
.printline (I1)
# Affichage d'un réel.
set N1, 3.14159
.printline (N1)
# Affichage d'une chaîne.
set S1, "Bonjour tout le monde."
.printline (S1)
end
chris$ parrot macro.pasm
12513
3.141590
Bonjour tout le monde..
chris$
Cette séquence amène quelques commentaires.
La première ligne nous permet de définir le nom et la liste d'appel de
notre macro-instruction.
Cette définition débute par la directive .macro suivie du nom par
lequel elle sera référencée, ici printline.
Le paramètre qui lui est passé A représente la valeur que l'on désire
afficher. Comme partout dans Parrot, ce peut être une référence à un
registre ou une valeur explicite.
Après cette ligne, il est nécessaire de définir le corps de la macro-instruction,
ici il se compose de deux lignes, l'affichage de la donnée qui lui a été
transmise suivie d'un \n.
La séquence doit se terminer par la directive .endm.
Dans le programme, on y fait référence par l'intermédiaire de son nom
précédé d'un point : .printline, suivi du paramètre.
Si on examinait un peu plus en détail ce qui a été fait, on constaterait qu'un prétraitement syntaxique aurait substitué à chaque appel les deux lignes qu'il représente. Le programme final soumis au système est, en définitive, le suivant :
# Impression d'un entier.
set I1, 12513
print I1
print "\n"
# Impression d'un réel.
set N1, 3.14159
print N1
print "\n"
# Impression d'une chaîne.
set S1, "Bonjour tout le monde."
print S1
print "\n"
end
Un paramètre peut représenter pratiquement n'importe quel objet, un registre
(I, N, S ou P), un label ou une constante.
Par exemple, pour accéder à un fichier.
chris$ cat macro.pasm
.macro lire (PMC)
LIRE:
readline S0, .PMC
unless S0, FIN
print P1, S0
branch LIRE
FIN:
.endm
getstdout P1
open P3, "dial.txt", "<"
.lire (P3)
end
chris$ parrot macro.pasm
What about the name of the new language?
GvR: Well, that was my idea. We went over lots of possible names:
Chimera, Pylon, Perth, before finally coming up with Parrot.
We had a few basic ideas: we wanted it to begin with "P";
it had to be something that wouldn't sound stupid on the end
of /usr/bin/.
LW: We also wanted the name of an animal, to represent the combination
of the camel and the python. It also helps with the book covers...
GvR: Eventually, I came up with Parrot after thinking about Monty Python's
finest hour, the Parrot sketch.
LW: It just sounded right - dynamic, colourful, exotic. I love it!
chris$
Ou, lorsqu'une macro doit gérer plusieurs sorties.
chris$ cat macro.pasm
.macro printline (PMC, A)
print .PMC, .A
print "\n"
.endm
.macro quot (DIVID, DIVIS, BON, MAUVAIS)
eq .DIVIS, 0, .MAUVAIS
div I2, .DIVID, .DIVIS
branch .BON
.endm
getstdin P0
getstdout P1
readline S1, P0
set I0, S1
readline S1, P0
set I1, S1
.quot (I0, I1, OK, PASOK)
OK:
print P1, "Le quotient de "
print P1, I0
print P1, " par "
print P1, I1
print P1, " est egal a "
.printline (P1, I2)
branch SUITE
PASOK:
.printline (P1, "Erreur, division par zero.")
SUITE:
end
coruscant:~/Langages/asmparrot chris$ parrot macro.pasm
25
5
Le quotient de 25 par 5 est egal a 5
chris $parrot macro.pasm
25
0
Erreur, division par zero.
chris$
Explication. La macro-instruction .quot comporte quatre paramètres, le
dividende, le diviseur, l'adresse où aller si tout se passe bien et l'adresse
concernée en cas d'erreur.
Lors de la première exécution, le diviseur étant différent de zéro, il n'y
a aucun problème, l'opération est réalisable, la sortie se fait vers l'adresse
transmise par l'intermédiaire du paramètre BON soit OK. La seconde fois
que le programme s'exécute, le diviseur est à zéro, cette impossibilité est
détectée par le test et la sortie se fait vers l'adresse PASOK transmise
via le paramètre MAUVAIS.
Lorsque nous avons écrit la macro lire, nous avons dû déclarer deux
adresses LIRE: et FIN:. Ce qui ne nous avait posé aucun problème
lors de l'exécution. Si nous reprenons cette macro dans le programme
suivant :
chris$ cat macro.pasm
.macro lire (PMC)
LIRE:
readline S0, .PMC
unless S0, FIN
print P1, S0
branch LIRE
FIN:
.endm
getstdout P1
open P3, "dial1.txt", "<"
.lire (P3)
open P4, "dial2.txt", "<"
.lire (P4)
end
chris$ parrot macro.pasm
error:imcc:Label 'LIRE' already defined
in macro '.lire' line 2
included from 'st.pasm' line 1
chris$
L'interpréteur détecte une erreur syntaxique. En effet, l'opération de substitution a produit le résultat suivant :
getstdout P1
open P3, "dial1.txt", "<"
LIRE:
readline S0, P3
unless S0, FIN
print P1, S0
branch LIRE
FIN:
open P4, "dial2.txt", "<"
LIRE:
readline S0, P4
unless S0, FIN
print P1, S0
branch LIRE
FIN:
end
Qui met bien en évidence la duplication des labels ainsi que nous le précise le message d'erreur.
Pour parer à ce problème, chaque fois qu'une macro qui contient une référence
est susceptible d'être développée plusieurs fois, il est nécessaire de déclarer
tous ses labels comme étant locaux au moyen de la directive .local.
D'une manière générale, comme la notion même de macro-instruction sous-entend de
multiples utilisations, il est bon de prendre l'habitude de le faire de manière
systématique.
chris$ cat macro.pasm
.macro lire (PMC)
.local $LIRE:
readline S0, .PMC
unless S0, .$FIN
print P1, S0
branch .$LIRE
.local $FIN:
close .PMC
.endm
getstdout P1
print P1, "** Fichier 1 **\n"
open P3, "dial1.txt", "<"
.lire (P3)
print P1, "** Fichier 2 **\n"
open P4, "dial2.txt", "<"
.lire (P4)
end
chris$ parrot macro.pasm
** Fichier 1 **
What about the name of the new language?
GvR: Well, that was my idea. We went over lots of possible names:
Chimera, Pylon, Perth, before finally coming up with Parrot.
We had a few basic ideas: we wanted it to begin with "P";
it had to be something that wouldn't sound stupid on the end
of /usr/bin/.
** Fichier 2 **
GvR: Eventually, I came up with Parrot after thinking about Monty Python's
finest hour, the Parrot sketch.
LW: It just sounded right - dynamic, colourful, exotic. I love it!
chris$
Il est maintenant possible de faire référence à cette macro autant de fois que nécessaire sans que la moindre erreur soit générée.
Parrot prédéfinit un certain nombre de macros, nous en avons utilisé
quelques-unes, par exemple include dont le paramètre est une chaîne de
caractères va permettre d'insérer dans le programme le contenu littéral du
fichier spécifié.
Une macro est aussi disponible pour déclarer des constantes. Elle est de
la forme : .constant nom valeur. Cette directive ne générera pas de
ligne de code mais uniquement une équivalence entre le nom et la valeur.
Chaque fois que le nom en question apparaîtra dans le source du programme,
on lui substituera la valeur qu'il représente.
chris$ cat cons.pasm
.macro printline (PMC, A)
print .PMC, .A
print "\n"
.endm
.constant Registre_Entier1 I1
.constant Registre_Reel1 N1
.constant Registre_Chaine1 S1
.constant Registre_PMC1 P1
.constant Dix 10
.constant Cent 100
.constant Hello "Bonjour tout le monde\n"
set .Registre_Entier1, .Dix
set .Registre_Reel1, .Cent
set .Registre_Chaine1, .Hello
getstdout .Registre_PMC1
.printline (.Registre_PMC1, .Registre_Entier1)
.printline (.Registre_PMC1, .Registre_Reel1)
.printline (.Registre_PMC1, .Registre_Chaine1)
end
chris$ parrot cons.pasm
10
100.000000
Bonjour tout le monde
chris$
Ainsi, rien n'empêche de créer un fichier qui va contenir toutes les
déclarations de constantes possibles et imaginables qui sera inséré dans
le programme au moyen de la directive .include et qui permettra de faire
référence aux diverses valeurs par le nom qui leur aura été affecté.
chris$ cat constantes.txt
.constant Zero 0
.constant Euler 0.57721
.constant Un 1
.constant Apery 1.20205
.constant Racine_Deux 1.41421
.constant Mills 1.30637
.constant Nombre_d_or 1.61803
.constant Niven 1.70521
.constant Racine_Trois 1.73205
.constant Feigenbaum 2.50290
.constant Sierpinski 2.58498
.constant e 2.71828
.constant pi 3.14159
chris$ cat cons.pasm
.macro printline (PMC, A)
print .PMC, .A
print "\n"
.endm
.include "constantes.txt"
getstdout P1
print P1, "Racine de deux : "
.printline (P1, .Racine_Deux)
print P1, "Racine de trois : "
.printline (P1, .Racine_Trois)
print P1, "Valeur de e : "
.printline (P1, .e)
print P1, "Valeur de pi : "
.printline (P1, .pi)
end
chris$ parrot cons.pasm
Racine de deux : 1.414210
Racine de trois : 1.732050
Valeur de e : 2.718280
Valeur de pi : 3.141590
chris$
Le programme suivant permet de convertir un nombre décimal en numération romaine [Romain].
chris$ cat romains.pasm
# Declaration des macros.
.macro set_liste (PMC, CHAINE)
new .PMC, .ResizablePMCArray
split .PMC, ",", .CHAINE
.endm
.macro print_line (LIGNE)
print P1, .LIGNE
print P1, "\n"
.endm
.macro lire (REGISTRE)
.local $LIRE:
.print_line ("Nombre de depart?")
readline S0, P0
set .REGISTRE, S0
lt I0, 4000, .$SORTIE
.print_line ("Le nombre doit etre inferieur a 4000.")
branch .$LIRE
.local $SORTIE:
.endm
.macro convertir (A, R)
set I1, 0 # Indice d'exploration des listes.
.local $CALCUL:
set I2, P3[I1] # Récupération diviseur.
div I3, .A, I2 # Occurrences du chiffre romain.
set S2, P4[I1] # Récupération de chiffre romain.
repeat S2, S2, I3
concat .R, S2 # Construction du nombre romain.
mod .A, .A, I2 # Reste de la division.
inc I1
if .A, .$CALCUL
.endm
.macro question (OUI)
.print_line ("Un autre calcul (o/n) ?")
readline S0, P0
eq S0, "o\n", .OUI
.endm
# Déclaration et initialisation liste chiffres arabes.
.set_liste (P3, "1000,900,500,400,100,90,50,40,10,9,5,4,1")
# Déclaration et initialisation liste chiffres romains
.set_liste (P4, "M,CM,D,CD,C,XC,L,XL,X,IX,V,IV,I")
getstdin P0 # Entrée standard.
getstdout P1 # Sortie standard.
LEC:
.lire (I0)
CONV:
set S1, ""
print P1, "Le nombre "
print P1, I0
print " s'ecrit : "
.convertir (I0, S1)
.print_line (S1)
.question (LEC)
end
chris$ parrot romains.pasm
Nombre de depart?
666
Le nombre 666 s'ecrit : DCLXVI
Un autre calcul (o/n) ?
o
Nombre de depart?
3333
Le nombre 3333 s'ecrit : MMMCCCXXXIII
Un autre calcul (o/n) ?
n
chris$
La première macro, set_liste permet de déclarer et d'initialiser les
listes utilisées. Elle prend comme paramètre un registre PMC et une chaîne
représentative de valeurs d'initialisation séparées par des virgules.
La liste sera déclarée par l'intermédiaire du PMC et initialisée avec
les valeurs correspondantes.
La seconde macro print_line a déjà été vue.
La troisième macro lire lit la donnée à convertir et vérifie qu'elle
est inférieure à 4000. En effet, la numération romaine n'écrit pas au-delà
de cette valeur.
La quatrième macro convertir effectue la conversion du nombre, la valeur
d'origine est contenue dans le registre entier passé par l'intermédiaire
du premier paramètre et le résultat est récupéré dans le registre chaîne
passé par l'intermédiaire du second argument.
Enfin, la dernière macro question demande si on souhaite convertir une
nouvelle valeur ou terminer le programme.
En fin de compte, le programme se résume à quelques instructions et des appels de macro.
Il peut sembler surprenant de penser que l'on puisse écrire des applications orientées objet [poo] en assembleur. En fait, il existe dans Parrot des fonctions permettant ce type d'approche. La longueur des programmes ne me permettra pas d'écrire comme je l'ai fait jusqu'à présent des ensembles complets. Je me contenterai de définir les diverses opérations afin que ceux qui désireraient approfondir le sujet puissent le faire sans trop de difficulté.
C'est bien entendu par l'intermédiaire d'un PMC que sera déclarée une
classe. La directive newclass permet de le faire et d'en affecter
la référence à un PMC. Il faut garder en mémoire le fait que les PMC
gèrent des références vers des v-tables pour comprendre les subtilités
de la programmation objet en Parrot.
newclass P0, "Humain"
Un nouvel objet référencé Humain est créé. Le PMC P0 pointe alors vers
une v-table qui va en contenir les diverses caractéristiques. En définitive,
la création d'une classe revient à créer une nouvelle entrée dans la liste
des PMC
chris$ parrot romains.pasm
print "Avant la creation de l'objet.\n"
bsr LISTEPMC
newclass P10, "Humain"
print "\n---------------------------\n\n"
print "Apres la creation de l'objet.\n"
bsr LISTEPMC
end
LISTEPMC:
set I0, 1
BOUCLE:
# Le numéro correspond à un type de PMC valide.
valid_type I1, I0
eq I1, 0, FIN
# Récupération du nom du PMC.
typeof S0, I0
# Edition des résultats.
set S1, I0
concat S1, " ", S1
substr S2, S1, -2
print "Nom du PMC numero "
print S2
print " : "
print S0
print "\n"
inc I0
branch BOUCLE
FIN:
ret
chris$
Avant la creation de l'objet.
Nom du PMC numero 1 : Null
Nom du PMC numero 2 : Env
* * * * *
Nom du PMC numero 80 : Super
Nom du PMC numero 81 : Undef
---------------------------
Apres la creation de l'objet.
Nom du PMC numero 1 : Null
Nom du PMC numero 2 : Env
* * * * *
Nom du PMC numero 80 : Super
Nom du PMC numero 81 : Undef
Nom du PMC numero 82 : ParrotClass
chris$
La classe qui vient d'être créée est vierge de toute caractéristique. Il est possible de définir des attributs qui lui seront rattachés et desquels hériteront tous ses descendants (sous-classes ou objets).
Un attribut est défini par son nom et par la classe à laquelle il est rattaché.
addattribute P0, "Prenom" addattribute P0, "Nom" addattribute P0, "Age"
Nous dirons par exemple qu'un être humain, quel qu'il soit, comporte trois caractéristiques, son prénom, son nom et son âge.
Les trois instructions que nous venons d'écrire spécifient cet état de chose.
Les attributs Prenom, Nom et Age sont attachés à la classe Humain
et tout ce qui sera défini à partir d'elle héritera de ces caractéristiques.
L'instruction
subclass Pi, Pj, "Nom_Classe"
Permet de définir une sous-classe appelée Nom_Classe rattachée à
celle dont la référence se trouve dans le PMC Pj et qui sera
elle-même définie par le PMC Pi.
Dans notre exemple, nous pouvons procéder aux déclarations :
subclass P1, P0, "Homme" subclass P2, P0, "Femme" subclass P3, P0, "Enfant"
Qui nous permettent de créer dans la classe principale Humain (P0) les trois
sous-classes Homme, Femme et Enfant définies respectivement par
les trois PMC P1, P2 et P3 et qui hériteront par là même des
attributs Prenom, Nom et Age de la classe Humain. Il est
possible de connaître le nom d'une classe au moyen de l'instruction
classname.
classname SO, P1
Va nous retourner dans le registre SO le nom de la classe correspondant au
PMC P1 soit Homme.
Pour instancier un objet dans une classe donnée, il faut, au préalable, déterminer quel est le numéro qui a été affecté à la classe en question lors de sa déclaration. Le code opération pour réaliser cette fonction est
find_type Ix, "Nom_de_la_classe"
L'entier correspondant à la référence de la classe se trouve dans le registre spécifié, c'est lui qui va nous permettre d'instancier un objet.
new Px, Ix
Si nous reprenons l'exemple que nous avons décrit jusqu'à présent, nous
pouvons créer un objet Homme au moyen de la séquence.
find_type I0, "Homme" new P10, I0
La référence de l'objet qui vient d'être créé se trouve dans le PMC P10.
Nous verrons aussi plus loin que l'opération new a aussi pour effet de
vérifier l'existence d'une méthode spécifique __init et de l'exécuter
si c'est le cas.
L'objet vient d'hériter des caractéristiques de la classe dans laquelle il a
été créé, nous pouvons donc maintenant leur affecter des valeurs. C'est
l'instruction setattribute qui réalise cette opération.
setattribute Px, A, Pz
Elle a pour effet de positionner l'attribut A de l'objet Px à la
valeur définie par Pz. L'attribut concerné peut être spécifié de
plusieurs manières distinctes.
Par son nom s'il n'y a pas d'ambiguïté : "Prenom"
Par sa hiérarchie "Humain\00Prenom"
Par son numéro d'ordre dans la déclaration Ix
Voyons les différentes manières de procéder pour affecter un prénom à
l'homme qui vient d'être créé et dont le descripteur est défini par P10.
new P5, .String set P5, "Christian" setattribute P10, "Prenom", P5 new P5, .String set P5, "Christian" setattribute P10, "Humain\00Prenom", P5 new P5, .String set P5, "Christian" classoffset I1, P10, "Humain" setattribute P10, I1, P5
Dans la troisième méthode l'instruction
classoffset I1, P10, "Humain"
Va permettre de récupérer le numéro d'ordre du premier attribut que la
classe Humain a transmis en héritage à l'objet qui a été créé et dont
le descripteur se trouve dans le PMC P10. Si on utilise cette procédure,
il faut penser à incrémenter le numéro d'ordre, I1 dans notre cas,
avant l'affectation de l'attribut suivant. Nous pouvons voir maintenant
comment récupérer les valeurs qui viennent d'être positionnées. C'est
l'instruction getattribute qui va nous le permettre.
getattribute Px, Py, A
Qui a pour effet de récupérer la valeur de l'attribut A de l'objet Py
dans Pz. Les manières de spécifier l'attribut sont identiques à celle
de l'instruction setattribute.
Le programme suivant reprend les divers points qui ont été vus. Il permet de créer plusieurs objets et d'en stocker les définitions dans une liste de PMC (.ResizablePMCArray) en vue d'une exploitation ultérieure.
Les divers morceaux le composant seront détaillés les uns après les autres. Il suffira de les réunir pour obtenir le programme complet.
Tout d'abord, les macros. La première affiche trois données sur la même ligne, la seconde permet de lire la valeur requise dans la destination souhaitée.
.macro print_line (A1, A2, A3)
print P1, .A1
print P1, .A2
print P1, .A3
.endm
.macro lecture (DESTINATION)
readline .DESTINATION, P0
chopn .DESTINATION, 1
.endm
On déclare maintenant les PMC. C'est dans une liste de PMC référencée par P4, que seront mémorisés les divers objets au fur et à mesure de leur création.
getstdin P0
getstdout P1
new P4, .ResizablePMCArray
Comme vu précédemment, on déclare la classe de base Humain et ses
attributs, puis la sous-classe Homme qui lui est rattachée et qui va
hériter de ses attributs.
# Déclaration de la classe Humain
newclass P10, "Humain"
# Attributs de l'humain.
addattribute P10, "Prenom"
addattribute P10, "Nom"
addattribute P10, "Age"
# Déclaration de la sous-classe Homme
subclass P11, P10, "Homme"
À partir de maintenant, le programme entre dans un mode conversationnel. Un nouvel objet possédant ses propres attributs sera créé chaque fois que nécessaire et sa référence sera conservée dans la liste créée pour la circonstance.
# Récupération du type de "Homme"
find_type I0, "Homme"
# Lecture informations.
print P1, "Prenom,Nom,Age ou ligne vide pour terminer\n"
ENTREE:
.lecture (S1)
unless S1, EDITION
new P2, .ResizableStringArray
# Découpage de la chaîne lue en ses composantes.
split P2, ",", S1
# Nouvel objet de la sous-classe "Homme"
new P20, I0
# PMC intermédiaire pour affecter les attributs.
new P30, .String
set P30, P2[0]
setattribute P20, "Humain\x00Prenom", P30
set P30, P2[1]
setattribute P20, "Humain\x00Nom", P30
set P30, P2[2]
setattribute P20, "Humain\x00Age", P30
push P4, P20
branch ENTREE
À la fin des créations, il ne reste plus qu'à explorer la liste pour récupérer l'une après l'autre les références mémorisées afin de les éditer.
EDITION:
set I0, P4
.print_line ("Nombre de creations : ", I0, "\n")
set I1, 0
SUIVANT:
eq I1, I0, FINI
.print_line ("Homme numero ", I1, " : ")
set P25, P4[I1]
classname S1, P11
getattribute P9, P25, "Humain\x00Prenom"
.print_line ("Prenom : ", P9, " - ")
getattribute P9, P25, "Humain\x00Nom"
.print_line ("Nom : ", P9, " - ")
getattribute P9, P25, "Humain\x00Age"
.print_line ("Age : ", P9, "\n")
inc I1
branch SUIVANT
FINI:
end
Lorsque l'ensemble des morceaux du programme sont mis bout à bout, l'exécution donne le résultat suivant.
chris$ parrot homme.pasm Prenom,Nom,Age ou ligne vide pour terminer Christian,Aperghis-Tramoni,59 Sebastien,Aperghis-Tramoni,29 Nombre de creations : 2 Homme numero 0 : Prenom : Christian - Nom : Aperghis-Tramoni - Age : 59 Homme numero 1 : Prenom : Sebastien - Nom : Aperghis-Tramoni - Age : 29 chris$
Tous les objets héritent d'un ensemble de fonctions prédéfinies par le
PMC ParrotObject. Les noms de ces méthodes commencent systématiquement
par un double blanc souligné (__). Alors qu'une méthode définie par l'usager
doit être appelée de manière explicite, celles qui sont prédéfinies dans
la v-table sont implicitement exécutées en fonction du contexte.
Ainsi que nous l'avons précédemment évoqué, la création d'un
nouvel objet entraîne automatiquement l'activation de la méthode __init
si cette dernière a été définie pour la classe considérée. On peut récupérer
les paramètres qui sont automatiquement transmis au moyen de l'instruction
get_params. Dans le cas d'une création la liste ne comporte qu'une
valeur, le PMC correspondant à l'objet qui vient d'être créé.
chris$ cat init.pasm
# Déclaration de la classe Humain
newclass P10, "Humain"
# Attributs de l'humain.
addattribute P10, "Prenom"
# Déclaration de la sous-classe Homme
subclass P11, P10, "Homme"
# Recuperation du type de "Homme"
find_type I0, "Homme"
# Nouvel objet de la sous-classe "Homme"
new P20, I0
end
.namespace ["Humain"]
.pcc_sub __init:
get_params "(0)", P31
classname S0, P31
print "Un nouvel objet de type "
print S0
print " vient de se creer.\n"
returncc
chris$ parrot init.pasm
Un nouvel objet de type Homme vient de se creer.
chris$
Voyons un autre exemple qui va lui aussi faire référence à une méthode prédéfinie. Soient les instructions suivantes
newclass P10, "Arithmetique"
addattribute P10, "Entier"
subclass P11, P10, "Valeur"
find_type I0, "Valeur"
# Objet de la sous-classe "Valeur"
new P20, I0
set P20, 30
end
Son exécution va produire le message d'erreur :
Can't find method '__set_integer_native' for object 'Valeur' current instr.: '(null)' pc 16 (prog.pasm:7)
Nous faisant savoir que l'instruction numéro 7 a fait appel à la méthode
__set_integer_native que nous avons omis de définir. Ainsi donc, pour
plusieurs instructions qui feront référence à un objet, il sera indispensable
de définir la méthode qui leur est attachée. Afin qu'elle soit appelée au
moment voulu.
set Px, Iy : __set_integer_native get Ix, Py : __get_integer add Px, Py, Pz : __add inc Px : __increment print Px : __get_string
Voyons ceci sur un exemple. Nous allons déclarer deux objets composés d'un entier.
newclass P1, "Nombre"
addattribute P1, "Entier"
find_type I1, "Nombre"
# Création d'un premier objet nombre.
new P3, I1
# Affectation d'une valeur à l'objet.
set P3, 300
# Création d'un second objet nombre.
new P4, I1
# Affectation d'une valeur à l'objet.
set P4, 400
get_results "(0)", P5
# Addition des deux objets.
add P5, P4, P3
# Affichage des résultats.
print "Resultat de l'addition : "
print P5
print "\n"
end
On désire au moment où ils sont créés leur affecter comme attribut un
entier. Dans l'espace de nom Nombre nous allons donc déclarer la
méthode __init.
.namespace ["Nombre"]
.pcc_sub __init:
get_params "(0)", P31
new P10, .Integer
setattribute P31, "Entier", P10
returncc
Comme il s'agit d'objets, le code opération set va faire à son tour
appel à une méthode prédéfinie. Dans ce cas, il s'agit d'affecter une
valeur entière à l'attribut d'un objet. La méthode qui est requise pour
cette opération est __set_integer_native. L'espace de nom Nombre
s'enrichit d'une nouvelle méthode.
.pcc_sub __set_integer_native:
get_params "(0,0)", P31, I31
new P30, .Integer
set P30, I31
returncc
L'opération que nous désirons effectuer est l'addition des
valeurs entières correspondant aux attributs des deux objets qui viennent
d'être créés. Le résultat de cette opération sera récupéré dans un PMC
de type .Integer qui, pour terminer, sera affiché.
La méthode requise pour effectuer cette opération est __add. Rajoutons-la
à son tour dans notre espace de nom.
.pcc_sub __add:
get_params "(0,0,0)", P25, P24, P23
getattribute P15, P25, "Entier"
getattribute P14, P24, "Entier"
new P13, .Integer
add P13, P14, P15
set_returns "(0)", P13
returncc
Notre programme est maintenant terminé, il ne nous reste plus qu'à l'exécuter.
chris$ parrot add.pasm Resultat de l'addition : 700 chris$
Une méthode attachée à une classe doit avoir un nom qui sera défini par l'intermédiaire d'un registre « string ».
set S0, "methode"
***
callmethodcc P0, S0
***
.namespace ["Objet"]
.pcc_sub methode:
***
returncc
Une fois déclarée, la méthode devra être rattachée à l'espace concerné
au moyen de la fonction .mamespace et son contenu défini.
chris$ cat obj.pasm
# Declaration de la classe Humain
newclass P0, "Humain"
# Declaration de la methode Identite
set S0, "Identite"
# Appel de la methode Identite
callmethodcc P0, S0
end
# Definition de la methode
.namespace ["Humain"]
.pcc_sub Identite:
print "Bonjour, je suis un etre humain.\n"
returncc
chris$ parrot obj.pasm
Bonjour, je suis un etre humain.
chris$
Avant l'appel d'une méthode, il est possible de tester son existence.
can Ix, Py, "Nom"
Va tester si la méthode Nom a été déclarée dans l'espace de nom défini
par le PMC Py. La réponse retransmise par l'intermédiaire du registre
Ix sera undef si elle n'existe pas, non undef dans le cas contraire.
L'exemple suivant va déclarer une classe Chris auquel est attachée la
méthode Bonjour.
Avant de l'appeler, on vérifiera qu'elle est bien présente. La même opération
sera effectuée sur la méthode Salut qui, elle, ne correspond à rien.
chris$ cat obj.pasm
.macro existe (A, B, OUI, NON)
print "La methode "
print .A
unless .B, .$FAUX
print " existe, on l'appelle.\n"
branch .OUI
.local $FAUX:
print " n'existe pas, on passe a la suite.\n"
branch .NON
.endm
newclass P2, "Chris"
set S0, "Bonjour"
# Test de l'existence de la methode Bonjour.
set S1, "Bonjour"
can I0, P2, S1
.existe (S1, I0, E1, SUITE)
E1:
callmethodcc P2, S0
SUITE:
# Test de l'existence de la methode Salut.
set S1, "Salut"
can I0, P2, S1
.existe (S1, I0, E2, FIN)
E2:
callmethodcc P2, S0
FIN:
end
# Definition de la methode
.namespace ["Chris"]
.pcc_sub Bonjour:
print " Chris vous souhaite le bonjour.\n"
returncc
chris$ parrot obj.pasm
La methode Bonjour existe, on l'appelle.
Chris vous souhaite le bonjour.
La methode Salut n'existe pas, on passe a la suite.
chris$
Le langage d'assemblage nous permet, comme cela est possible en Perl, d'évaluer une chaîne de caractères, contenue dans un registre, comme un programme.
Prenons par exemple l'instruction suivante :
set S0, "set S1, 10\nprint \"Valeur de S1 : \"\nprint S1\nprint \"\\n\"\nreturncc\n"
L'impression du contenu du registre S0 donnera le résultat suivant :
set S1, 10 print "Valeur de S1 : " print S1 print "\n" returncc
Soit un sous programme Parrot parfaitement constitué. Cette chaîne peut donc être soumise à un évaluateur qui la considérera comme une suite d'instructions à exécuter.
chris$ cat evaluer.pasm
print " Debut du programme.\n"
compreg P1, "PASM"
set S0, "set S1, 10\nprint \"Valeur de S1 : \"\nprint S1\nprint \"\\n\"\nreturncc\n"
set_args "(0)", S0
get_results "(0)", P0
print " Evaluation du programme contenu dans S0.\n"
invokecc P1
invokecc P0
print " Retour au programme principal.\n"
end
chris$ parrot evaluer.pasm
Debut du programme.
Evaluation du programme contenu dans S0.
Valeur de S1 : 10
Retour au programme principal.
chris$
Le registre S0 contient la donnée représentative du programme qui doit
être évalué. L'instruction compreg P1, "PASM" indique que le langage
est de l'assembleur Parrot et devra être traité en conséquence. Le registre
S0 sera transmis en tant que paramètre au programme d'assemblage.
C'est le rôle de l'instruction set_args "(0)", S0 et le résultat,
soit le module exécutable, sera récupéré dans un PMC (P0), c'est ce
que dit l'instruction get_results "(0)", P0 il ne nous reste plus
qu'à appeler le programme d'assemblage dont la référence se trouve dans
le PMC P1 au moyen de l'instruction invokecc P1 puis de faire appel
au résultat que nous récupérons dans P0 comme s'il s'agissait d'un
sous-programme invokecc P0. C'est la raison pour laquelle le morceau
de code soumis à l'évaluation doit se terminer par l'instruction de retour
returncc.
Une coroutine est un sous-programme dont l'exécution peut être suspendue
et qui, au moment de l'appel suivant reprendra son exécution au point où
elle aura été interrompue. Tout sous-programme qui contient le code
opération yield sera considéré comme une coroutine, et le point de
reprise sera justement l'emplacement de l'instruction en question.
chris$ cat evaluer.pasm
.include "interpinfo.pasm"
.pcc_sub _main:
.const .Sub P0 = "_japh"
new P2, .String
set P2, ""
store_global "car", P2
find_global P10, "car"
IMP:
invokecc P0
unless P10, FIN
print P10
branch IMP
FIN:
end
.pcc_sub _japh:
set S1, "Just Another Perl Hacker.\n"
set I1, 0
SUITE:
find_global P10, "car"
substr S10, S1, I1, 1
set P10, S10
inc I1
yield
branch SUITE
chris$ parrot evaluer.pasm
Just Another Perl Hacker.
chris$
Le mécanisme se révèle être d'une grande utilité lorsqu'une fonction doit mémoriser une valeur au fur et à mesure de ses appels successifs.
Un des premiers exemple qui vient à l'esprit est le génération d'une série de valeurs pseudo-aléatoires [Aléat]. L'algorithme standard part avec une valeur initiale (la racine) qui sert pour produire la première valeur. Cette dernière sera alors utilisée pour en générer une nouvelle, et ainsi de suite.
Prenons un algorithme classique de génération de suite pseudo-aléatoire, qui, en Perl, se présente comme suit.
sub rand { $x = int(($x * 1103515245 + 12345) / 65536) % 32768; return $x; }
Et réalisons-le en Parrot au moyen d'une coroutine.
chris$ cat rand.pasm
.const .Sub P1 = "_rand"
set I1, 10
AUTRE:
get_results "(0)", I0
invokecc P1
print I0
print " "
dec I1
if I1 , AUTRE
print "\n"
end
.pcc_sub _rand:
new P0, .Float
set P0, 1
store_global "racine", P0
find_global P0, "racine"
NOUVEAU:
mul P0, 1103515245
add P0, 12345
div P0, 65536
mod P0, 32768
set I31, P0
set_returns "(0)", I31
yield
branch NOUVEAU
chris$ parrot rand.pasm
16838 22996 7307 3007 20531 15468 29688 23554 1052 20045
chris$
Les informations entrées sur la ligne de commande sont les premiers
résultats (get_params) que peut récupérer un programme Parrot. C'est un PMC de type
.ResizableStringArray qui leur servira de réceptacle.
chris$ cat cmd.pasm
# Récupération des paramètres de la ligne de commande.
# P0 est de type .ResizableStringArray.
get_params "(0)", P0
set I1, P0
print I1
print " parametres : "
set I0, 0
AUTRE:
set S0, P0[I0]
print S0
print " - "
inc I0
lt I0, I1 , AUTRE
print "\n"
end
chris$ parrot cmd.pasm Bonjour tout le monde.
5 parametres : cmd.pasm - Bonjour - tout - le - monde. -
chris$
On dispose d'instructions spécifiques permettant de suivre pas à pas le
déroulement du programme. Le code opération getfile permet de récupérer
dans un registre string le nom du fichier représentatif du programme
sans avoir à procéder à l'acquisition de la totalité de la ligne de
commande, et le code getline permet de connaître le numéro de la
ligne courante.
chris$ cat info.pasm
.macro set_liste (PMC, CHAINE)
new .PMC, .ResizablePMCArray
split .PMC, ",", .CHAINE
.endm
.macro info (FICHIER, LIGNE)
print "Fichier : "
print .FICHIER
print ", Ligne : "
print .LIGNE
print "\n"
.endm
getfile S0
.set_liste (P3, "1,2,3,4,5")
set I1, P3
getline I30
.info (S0, I30)
set I0, 0
SUITE:
set S1, P3[I0]
print S1
print "\n"
inc I0
getline I30
.info (S0, I30)
lt I0, I1 , SUITE
end
chris$ parrot info.pasm
Fichier : info.pasm, Ligne : 17
1
Fichier : info.pasm, Ligne : 25
2
Fichier : info.pasm, Ligne : 25
3
Fichier : info.pasm, Ligne : 25
4
Fichier : info.pasm, Ligne : 25
5
Fichier : info.pasm, Ligne : 25
chris$
Dans le même ordre d'idée, il est possible de procéder au tracé des
instructions. L'instruction trace 1 permet d'activer le mécanisme de
tracé du programme alors que l'instruction trace 0 y met fin.
chris$ cat trace.pasm
.macro set_liste (PMC, CHAINE)
new .PMC, .ResizablePMCArray
split .PMC, ",", .CHAINE
.endm
.set_liste (P3, "1,2,3,4,5")
trace 1
set I1, P3
set I0, 0
trace 0
SUITE:
set S1, P3[I0]
print S1
print "\n"
trace 1
inc I0
trace 0
lt I0, I1, SUITE
end
chris$ parrot trace.pasm
9 set I1, P3 I1=-888 P3=ResizableStringArray=PMC(0xf9bf7c)
12 set I0, 0 I0=-888
15 trace 0 1
27 inc I0 I0=0
29 trace 0 2
27 inc I0 I0=1
29 trace 0 3
27 inc I0 I0=2
29 trace 0 4
27 inc I0 I0=3
29 trace 0 5
27 inc I0 I0=4
29 trace 0
chris$
Pour chaque instruction tracée, on voit apparaître le numéro de la ligne considérée, l'instruction correspondante et, si cette dernière est une affectation, le registre de destination et sa valeur avant l'exécution de l'opération demandée. C'est ainsi que lors de la première exécution des lignes 9 et 12, les contenus sont égaux à -888, valeur qui correspond à un registre qui n'a reçu aucune affectation.
Comme en Perl, l'instruction sleep permet de mettre le programme en
attente pendant le nombre de secondes spécifié. Par ailleurs, un ensemble
d'instructions permettent de récupérer le temps en nombre de secondes
time de le décoder en heure locale decodelocaltime dans un PMC.
Le fichier tm.pasm est un fichier de constantes symboliques qui va
permettre d'accéder aux divers éléments du PMC ainsi créé pour en extraire
les données. Son contenu est le suivant :
.constant TM_SEC 0 .constant TM_MIN 1 .constant TM_HOUR 2 .constant TM_MDAY 3 .constant TM_MON 4 .constant TM_YEAR 5 .constant TM_WDAY 6 .constant TM_YDAY 7 .constant TM_ISDST 8
.TM_SEC Secondes (0-60)
.TM_MIN Minutes (0-59)
.TM_HOUR Heure (0-23)
.TM_MDAY Jour dans le mois (1-31)
.TM_Mon Mois dans l'année (1-12)
.TM_YEAR Année réelle, 2007 est bien 2007, pas 107
.TM_WDAY Jour dans la semaine (0-6), 0 représentant le dimanche.
.TM_YDAY Jour dans l'année (0-365)
.TM_ISDST Heure d'été (Vrai ou Faux)
Le programme suivant est construit autour d'une coroutine, les cinq appels successifs permettent d'initialiser les données pour le premier, puis d'afficher une ligne pour chacun des quatre suivants.
chris$ cat time.pasm
.macro set_liste (PMC, CHAINE)
new .PMC, .ResizablePMCArray
split .PMC, ",", .CHAINE
.endm
.macro print2 (V1, V2)
print .V1
print .V2
.endm
.const .Sub P1 = "_date"
# Initialisations données de la coroutine.
invokecc P1
# Affichage du jour.
invokecc P1
# IAffichage de l'heure
invokecc P1
# Affichage du numéro du jour.
invokecc P1
# Affichage heure d'été.
invokecc P1
end
.pcc_sub _date:
.set_liste (P10, "Dim,Lun,Mar,Mer,Jeu,Ven,Sam")
.set_liste (P11, "*,Jan,Fev,Mar,Avr,Mai,Jun,Jui,Aou,Sep,Oct,Nov,Dec")
.set_liste (P12, "hiver,ete")
time I0
decodelocaltime P2, I0
yield
.include "tm.pasm"
.print2 ("Nous sommes le", " ")
set I1, P2[.TM_WDAY]
set S1, P10[I1]
.print2 (S1, " ")
set I1, P2[.TM_MDAY]
.print2 (I1, " ")
set I1, P2[.TM_MON]
set S1, P11[I1]
.print2 (S1, " ")
set I1, P2[.TM_YEAR]
.print2 (I1, "\n")
yield
.print2 ("Il est", " ")
set I1, P2[.TM_HOUR]
.print2 (I1, " heures ")
set I1, P2[.TM_MIN]
.print2 (I1, " minutes ")
set I1, P2[.TM_SEC]
.print2 (I1, " secondes\n")
yield
print "Ce jour est le "
set I1, P2[.TM_YDAY]
.print2 (I1, "eme de l'annee\n")
yield
print "Nous sommes en heure d'"
set I1, P2[.TM_ISDST]
set S1, P12[I1]
.print2 (S1, ".\n")
yield
chris$ parrot time.pasm
Nous sommes le Mar 10 Avr 2007
Il est 11 heures 20 minutes 34 secondes
Ce jour est le 99eme de l'annee
Nous sommes en heure d'ete.
chris$
J'ai tenté, dans cette série d'articles, de mettre en évidence le fait qu'un langage d'assemblage pouvait avoir une approche différente. La machine virtuelle Parrot servira de support à de nombreux langages, si le niveau de son assembleur devrait permettre le développement rapide de compilateurs, il permet à ceux qui le désirent de disposer d'une plate-forme performante et d'un abord agréable.
Copyright © Les Mongueurs de Perl, 2001-2020
pour le site.
Les auteurs conservent le copyright de leurs articles.