Le langage PIR, première partie

Article publié dans Linux Magazine 122, décembre 2009.

Copyright © 2009 Christian Aperghis-Tramoni

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

Chapeau

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

Introduction.

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.

Langage intermédiaire.

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.

Les déclarations.

Un rapide survol des caractéristiques de 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.

Architecture de la machine virtuelle.

Rappelons que Parrot est organisée autour de quatre jeux de 32 registres chacun.

Organisation générale de 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.

Déclaration des variables.

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 :

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.

Représentation des constantes.

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 :

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.

Les opérations.

Les opérateurs arithmétiques.

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.

Les opérations logiques.

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 ||, &&, ~~.

Les opérations Booléennes.

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 |, &, ~.

Les unités de compilation.

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.

Premier exemple.

  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.

Second exemple.

  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.

Troisième exemple.

  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.

Quatrième exemple.

  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

Terminer un programme.

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

Pour les adeptes de 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.

Chaînes, encodage et jeu de caractères.

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.

Le codage.

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.

Les fonctions sur les chaînes.

L'instruction 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

L'instruction 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.

L'instruction 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".

L'instruction 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$

La fonction 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.

Les variables nommées.

Utilisation des noms symboliques.

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.

L'allocation de registres.

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.

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

Principaux types de 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 :

Nombre d'autres PMC sont disponibles pour de multiples utilisations.

La liste complète est disponible sur le site de Parrot [PMC].

Utilisation des 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$

Changement de type.

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.

L'autoboxing.

À 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$

Les constantes nommées.

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$

Conclusion provisoire.

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.

Références

[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

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