La machine Parrot, troisième partie - Plus loin avec Parrot

Article publié dans Linux Magazine 99, novembre 2007.

Copyright © 2007 Christian Aperghis-Tramoni

[+ del.icio.us] [+ Developers Zone] [+ Bookmarks.fr] [Digg this] [+ My Yahoo!]

Chapeau

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.

Les Macro-instructions

Qu'est-ce qu'une macro ?

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))

Définition d'une macro en Parrot

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

Les paramètres

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.

Les références locales

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.

Les macros prédéfinies

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$

Application sur un programme

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.

La programmation orientée objet.

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é.

Les classes.

Définition d'une classe.

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$

Les attributs de classe

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.

Définition d'une sous-classe

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.

Les objets

Définition d'un objet

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.

Les attributs

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.

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.

Une application

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$

Les méthodes

Les méthodes prédéfinies

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$

Les méthodes définies par le programmeur

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$

Test de l'existence d'une méthode

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$

Quelques particularités notables

Evaluation d'une chaîne

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.

Les coroutines

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$

Application des coroutines

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$

Récupération des valeurs de la ligne de commande

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$

Suivi du déroulement.

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.

Gestion du temps

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

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$

Conclusion

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.

Références

[Macro] http://fr.wikipedia.org/wiki/Macro-définition

[Romain] http://fr.wikipedia.org/wiki/Numération_romaine

[poo] https://formation-perl.fr/guide-perl-11.html

[Aléat] http://fr.wikipedia.org/wiki/Générateur_de_nombres_pseudo-aléatoires

[IE7, par Dean Edwards] [Validation du HTML] [Validation du CSS]