You are here: Home / Documentation / drets / dret105

RAPPORT D'AVANCEMENT No 2 ANALYSE SYNTAXIQUE ET SEMANTIQUE RT EMP 105

RAPPORT D'AVANCEMENT No 2
ANALYSE SYNTAXIQUE ET SEMANTIQUE
RT EMP 105

François IRIGOIN

Pierre JOUVELOT

Mars 1988

Rémi TRIOLET

 

Introduction

Ce rapport intermédiaire est consacré à la description de la représentation intermédiaire que nous avons choisie pour stocker les programmes scientifiques qui seront soumis à PIPS. Rappelons que ces programmes sont écrits dans un sous-ensemble du langage Fortran que nous avons défini dans un précédant rapport1.

Tout programme Fortran doit avoir une représentation intermédiaire sémantiquement équivalente à son texte Fortran.

Cette représentation intermédiaire doit permettre d'effectuer facilement des transformations et des analyses de programmes.

Le développement de PIPS est en partie basé sur un outil de génie logiciel que nous avons développé au CAII. Cet outil s'appelle NewGen, il est présenté dans la première partie de ce rapport.

La représentation intermédiaire est décrite dans la seconde partie. Nous expliquons notamment comment un programme Fortran est mappé sur cette représentation intermédiaire.

La troisième partie de ce rapport est consacrée à l'analyse sémantique. Nous y présentons d'abord les objets utilisés pour résoudre les équations aux polyèdres propres à la méthode de Cousot et Halbwachs, puis la structure de données représentant un tel système d'équations pour un module quelconque.

 

Présentation de NewGen

Introduction

NewGen est un outil de génie logiciel développé au CAII pour faciliter l'écriture de gros logiciels tels que PIPS. NewGen est dérivé de deux outils antérieurs, dont il est une amélioration:

 

  • GenPgm, développé au CAII par Rémi Triolet,
  • MetaGen, développé au groupe Bull par Pierre Jouvelot.

NewGen facilite à la fois la définition et l'implémentation des structures de données qui sont généralement utilisées pour réaliser des logiciels complexes. L'utilisation de Newgen se fait de la façon suivante.

 

  1. les types de données que l'on souhaite utiliser pour développer un logiciel (par exemple PIPS) sont définies à l'aide d'un langage de haut niveau appelé ``langage de définition de domaines''.
  2. la définition des domaines est compilée par NewGen, ce qui a pour effet de:
    • générer une implémentation des types de données en langage C ou en langage Lisp;
    • générer des fonctions de création, destruction et mise à jour d'objets de ces types;
    • générer des fonctions d'écriture et de lecture d'objets de ces types sur fichiers.

Le logiciel peut ensuite être développé en tirant profit des fonctions et définitions de types qui ont été générées automatiquement. Les répercussions sur le développement sont nombreuses.

 

Un langage plus riche

Quel que soit le langage de développement utilisé (Lisp ou C), NewGen l'enrichit de nouvelles constructions: les fonctions de création, destruction et mise à jour d'objets.

Cet enrichissement a plusieurs conséquences. Tout d'abord, tous les membres d'une équipe de développement utilisent les mêmes fonctions et les mêmes types de données, ce qui a pour effet d'uniformiser la façon de programmer. Ensuite, la disponibilité de ces fonctions permet à chacun de programmer plus vite, de comprendre plus facilement le code des autres, et de diminuer la quantité de commentaires nécessaires.

 

Vérification dynamique de types

Les fonctions de création, destruction et mise à jour d'objets sont créées par NewGen de telle sorte qu'une vérification dynamique soit effectuée. Par exemple, un appel à la fonction employe_nom sur un objet e vérifie d'abord que e est un objet de type employe, si oui renvoie le nom de cet employé et si non aborte avec un message d'erreur. Bien évidemment, cette vérification dynamique est optionnelle, de sorte que les programmes corrects ne sont pas ralentis.

 

Communications inter-passes par fichiers

Les fonctions d'entrées-sorties générées par NewGen permettent d'écrire ou de lire sur fichier un objet aussi compliqué qu'une représentation intermédiaire de programme Fortran, à l'aide d'une ligne de C ou de Lisp. Ceci a plusieurs conséquences.

Les gros logiciels tels que PIPS sont généralement constitués de plusieurs passes. Il y a des dépendances entre passes, par exemple quand une passe utilise les résultats d'une autre passe. Certaines passes sont obligatoires, d'autres sont optionnelles (comme par exemple la phase d'optimisation dans les compilateurs).

L'organisation généralement adoptée est celle où chaque passe est une procédure appelée par un ``driver'' que l'utilisateur commande soit de façon interactive soit par des lignes de contrôle dans le programme source, ou bien encore par des options de lancement.

L'organisation choisie pour PIPS est radicalement différente. Grâce à NewGen nous avons la possibilité de rendre chaque passe indépendante: chaque passe lit un ou plusieurs fichiers de données à l'aide des fonctions NewGen, effectue ses calculs, puis produit à son tour un fichier résultat. Les données stockées sur un fichier peuvent être arbitrairement complexes (structures, tableaux, listes, graphes) car les fonctions générées par NewGen prennent en compte ce genre de données. Le driver n'est alors plus nécessaire, quelques script-shells suffisent.

Avec une telle organisation, le développement est considérablement simplifié, car on est amené à développer N petits logiciels plutôt qu'un unique gros. La mise au point est plus simple, les temps de compilation plus courts, le partage du travail facilité. Enfin, il est très facile d'utiliser plus d'un langage de programmation car un fichier de données écrit par une passe C peut être lu par une passe Lisp, et ce sans aucune modification du fichier ou des passes.

 

Définition des domaines NewGen

Les types de données NewGen sont appelés des domaines, et la spécification d'une structure de données NewGen est constituée d'une liste de définitions de domaines. Les définitions de données peuvent être récursives (ou cycliques), c'est à dire qu'un domaine peut être défini en fonction de lui-même, directement ou indirectement.

Les domaines sont définis à partir de deux constructeurs, l'union + et le produit x, de deux itérateurs, le tableau [] et la liste *, et enfin de domaines prédéfinis qui correspondent à peu près aux types simples de C ou Lisp.

A chaque domaine correspond un ensemble de valeurs qui sont les valeurs que peut prendre un objet de ce domaine. Ainsi, les valeurs du domaine prédéfini int sont les entiers relatifs de $\cal Z$.

Nous allons définir constructeurs et itérateurs en fonction de ces ensembles de valeurs.

 

Le produit

Un domaine D est le produit de n domaines SD1, SD2, ..., SDn si D est défini par:

 

D = SD1 x SD2 x ... x SDn ;

Les SDi sont appelés les sous-domaines de D. Un sous-domaine peut être un sous-domaine de base, un sous-domaine simple, un sous-domaine liste ou un sous-domaine tableau (voir plus loin).

L'ensemble des valeurs de D est le produit cartésien des ensembles de valeurs des sous-domaines SDi.

 

L'union

Un domaine D est l'union de n domaines SD1, SD2,..., SDn si D est défini par:

 

D = SD1 + SD2 + ... + SDn ;

De même, les SDi sont appelés les sous-domaines de D.

L'ensemble des valeurs de D est l'union des ensembles de valeurs des sous-domaines SDi.

 

Sous-domaines de base

Un sous-domaine de base est de la forme:

 

nom:dpd

nom est le nom du sous-domaine et dpd est un domaine pré-défini: unit , bool , char , string , int et float.

L'ensemble des valeurs d'un sous-domaine de base est égal à l'ensemble des valeurs du domaine pré-défini. Les types pré-définis autres que unit ont des ensembles de valeurs qui dépendent du langage utilisé. Par exemple, le sous-domaine float correspond aux nombres réels.

Le type pré-défini unit a un ensemble de valeurs réduit à un élément, conventionnellement noté constructeur +, dans le cas où le nom du sous-domaine à lui seul porte toute l'information utile.

Exemples:

employe = nom:string x age:int x salaire:float
complex = i:float x r:float
projet = pips:unit + vast:unit + paf:unit

 

Sous-domaine simple

Un sous-domaine simple est de la forme:

nom:dom

nom est le nom du sous-domaine et dom le nom d'un domaine construit par + ou x, dont la définition doit se trouver dans le même fichier de spécifications (les références en avant sont admises) ou doit être importée depuis un autre fichier par la commande import. Il est possible d'écrire nom à la place de nom:nom.

L'ensemble des valeurs du sous-domaine nom est égal à l'ensemble des valeurs du domaine dom.

Dans les exemples suivants, annee, mois, naissance, ... sont des sous-domaines simples.

 

date = annee:int x mois:int x jour:int
employe = nom:string x naissance:date x salaire:float

import expression from "expression.spec"
dimension = lower:expression x upper:expression

 

Sous-domaine liste

Un sous-domaine liste est de la forme:

nom:dom*

nom:dom est un sous-domaine de base ou un sous-domaine simple.

L'ensemble des valeurs du sous-domaine nom est égal aux listes à zéro ou plus éléments dont chaque élément à une valeur dans l'ensemble des valeurs du domaine dom.

Dans les exemples suivants, args et block sont des sous-domaines listes.

call = function:entity x args:expression*
instruction = block:statement* + loop:loop + ...

 

Sous-domaine tableau

Un sous-domaine tableau est de la forme:

 

nom:dom[d1][d2]...[dl]

nom:dom est un sous-domaine de base ou un sous-domaine simple et d1, d2, ..., dl sont l constantes entières.

L'ensemble des valeurs du sous-domaine nom est égal aux tableaux à l dimensions d1, d2, ..., dl d'éléments. Chaque élément du tableau ayant une valeur dans l'ensemble des valeurs du domaine dom.

 

Exemple: La représentation intermédiaire de PIPS

Les deux constructeurs + et x, les deux itérateurs [] et * et les domaines prédéfinis proposés par NewGen permettent de définir simplement toutes les structures de données.

Voici un exemple important: la représentation intermédiaire des programmes Fortran pour PIPS. La liste des domaines est donnée par ordre alphabétique; une présentation structurée en est faite dans la seconde partie. Il est recommandé d'avoir cet exemple à portée de main pour lire la suite de ce rapport.

 

array       = basic x dimensions:dimension* ;
basic       = int:int + float:int + logical:int + overloaded:unit + 
              complex:int + string:value ;
call        = function:entity x arguments:expression* ;
code        = declarations:entity* x statement ;
constant    = int + litteral:unit ;
dimension   = lower:expression x upper:expression ;
entity      = name:string x type x storage x initial:value ;
expression  = syntax ;
formal      = function:entity x offset:int ;
functional  = parameters:parameter* x result:type ;
instruction = block:statement* + test + loop + goto:statement + call ;
loop        = index:entity x range x body:statement x label:entity ;
mode        = value:unit + reference:unit ;
parameter   = type x mode ;
pgm         = modules:entity*
ram         = function:entity x section:entity x offset:int ;
range       = lower:expression x upper:expression x increment:expression ;
reference   = variable:entity x indices:expression* ;
statement   = label:entity x instruction ;
storage     = return:entity + ram + formal + rom:unit ;
symbolic    = expression x constant ;
syntax      = reference + range + call ;
test        = condition:expression x true:statement x false:statement ;
type        = statement:unit + area:int + array + functional + 
              unknown:unit + void:unit ;
value       = code + symbolic + constant + intrinsic:unit + unknown:unit ;

Certaines définitions sont simples à comprendre. Par exemple, une variable Fortran est représentée par un objet de type array, qui se compose d'un type de base basic et d'une liste de dimensions dimensions:dimension* (rappelons que basic est une abbréviation de basic:basic).

Un programme pgm se compose d'une liste modules de fonctions, chaque fonction étant représentée par un objet de type entity.

Le mode de passage des paramètres est un mode qui vaut soit value soit reference.

 

Implémentation C de NewGen

Nous allons à présent étudier les fonctions générées par NewGen-C, l'implémentation de NewGen pour le langage C. Nous ne décrirons pas NewGen-Lisp car cette seconde implémentation est tout à fait similaire.

Les fonctions que nous décrivons dans la suite sont générées par NewGen-C lors de la compilation d'un fichier de spécifications. Ces fonctions sont accessibles depuis un programme C à la condition d'inclure un fichier header nommé file.C_adt si file contient les spécifications2. Il faut en outre utiliser une bibliothèque lors de l'édition de liens.

 

Définitions de types - Déclarations d'objets

Pour chaque domaine produit ou union dom, une directive typedef est générée. Ceci permet de déclarer très simplement des objets de type dom:

#include "ri.C_adt"
...
dom d ;                 /* declare une variable d de type dom */

extern dom f() ;        /* definit une fonction f renvoyant 
                           un objet de type dom */

Exemples:

dimension d ;
entity e;
extern constant EvalExpr() ;

 

Fonction de création - Domaines Produits

Une fonction de création make_dom est créée pour chaque domaine produit dom. Cette fonction prend autant d'arguments qu'il y a de sous-domaines dans la définition de dom; chaque argument est un objet du sous-domaine concerné que l'on veut stocker dans l'objet en cours de création. Les problèmes d'allocation mémoire sont gérés par Newgen-C.

Une valeur spéciale dom_undefined existe pour chaque domaine dom.

Exemples

d = make_dimension(l,u) ;
e = make_entity("+", type_undefined, storage_undefined, value_undefined) ;

 

Fonction de création - Domaine union

Une fonction de création make_dom est générée pour chaque domaine union dom. Cette fonction prend 2 arguments:

 

  • une étiquette de la forme is_dom_sdom qui indique à quel sous-domaine appartient l'objet s que l'on veut stocker dans l'objet nouvellement créé.
  • l'objet s appartenant à un des sous-domaines de dom, dont le type s'accorde avec l'étiquette is_dom_sdom décrite ci-dessus.

Les étiquettes is_dom_sdom sont automatiquement créées par NewGen-C.

Exemples:

syntax s = make_syntax(is_syntax_call, make_call(f, args)) ; 
value  v = make_value(is_value_constant, 
                      make_constant(is_constant_int, 3)) ;

 

Fonctions d'accès et de modification - Domaines produits

Si dom est un domaine produit, une fonction d'accès dom_sdom est créée pour chaque sous-domaine sdom de dom. Le type de cette fonction est celui du sous-domaine sdom. Appliquée à un objet d de type dom, elle retourne l'objet de type sdom stocké dans d.

Cette fonction est implémentée de telle sorte qu'elle puisse être utilisée en partie droite d'une affectation - pour accéder à la valeur du sous-domaine - ou en partie gauche - pour changer la valeur du sous-domaine.

Exemples:

constant c = EvalExpr(dimension_upper(d)) ;  
fprintf(fd, "%s" , entity_name(e)) ;
constant_int(value_constant(v)) += 1;

 

Fonctions d'accès - Domaines Unions

Soit dom un domaine union. L'accès à la valeur d'un sous-domaine stocké dans un objet d de type dom nécessite de connaıtre le type du sous-objet contenu dans d. La fonction or_tag permet de connaıtre ce type.

Par exemple, la fonction or_tag appliquée à l'objet v de type value créé auparavant fournit la valeur is_value_constant, c'est-à-dire la valeur de l'étiquette passée en paramètre au moment de la création de v.

Une fois le type connu grâce à la fonction or_tag la fonction d'accès dom_sdom peut être utilisée comme pour les domaines produits.

Exemple:

if (or_tag(v) == is_value_constant) {
        constant c = value_constant(v) ;
        if (or_tag(c) == is_constant_int) {
                int valeur = constant_int(c) ;
                ...
        }
        ...
}

Le prédicat dom_sdom_p(x) est équivalent à (or_tag(x)==is_dom_sdom) et est défini pour chaque sous-domaine sdom. Il permet de simplifier l'écriture des programmes.

Exemple:

        if(value_constant_p(v) ) {
                ....
        }

La fonction or_tag est néammoins utile pour l'instruction C switch.

 

Implémentation des listes

La liste vide est notée NIL. Les listes sont constituées à l'aide de cellules que nous nommons cons, par référence à Lisp. Une variable devant contenir une liste doit être déclarée de type cons *.

Un cons se compose d'un élément car permettant de stocker un objet d quel que soit le type de cet objet, et d'un élément cdr permettant de pointer vers une autre cellule cons.

Le type C de l'élément car est unique; appelons le chunk. Comme cet élément est susceptible de contenir un objet de n'importe quel domaine, une fonction d'insertion/extraction DOM est construite par NewGen-C pour chaque domaine dom. Cette macro permet de convertir un objet de type dom en un objet de type chunk, et réciproquement.

On ajoute un élément d de type dom à une liste l (l peut valoir NIL) grâce à la fonction CONS dont le type vaut cons* et qui prend 3 arguments:

  • la fonction de conversion DOM,
  • l'objet à insérer d de type dom,
  • la liste l.

La fonction CONS retourne en résultat la nouvelle liste, qui comporte l'élément en tête de liste.

Dans l'exemple suivant, f est une entity, e1 et e2 des expressions:

cons *la;

call c = make_call(f, NIL);
la = CONS(EXPRESSION , e1 , NIL) ;
call_arguments(c) = CONS(EXPRESSION , e2 , la) ;

La valeur de l'objet d de type dom contenu dans une cellule cons est obtenue par la fonction CAR dont le type est le type de l'élément car, c'est-à-dire chunk. Le résultat de CAR doit donc être converti en un objet de type dom grâce à la macro d'insertion-extraction DOM.

Exemple:

expression e = EXPRESSION(CAR(call_arguments(c))) ;

La cellule suivant une cellule cons s'obtient par la fonction CDR de type cons*. La fonction CDR peut être utilisée en partie gauche ou droite des affectations.

Exemple:

cons * pc ;
for (pc = call_arguments(c) ; pc != NIL ; pc = CDR(pc)) {
        constant c ;

        c = EvalExpression(EXPRESSION(CAR(pc))) ;
        ...
}

 

Implémentation des tableaux

L'accès à un élément d'un tableau se fait avec des []. Comme pour les listes, les objets d'un tableau sont stockés dans des éléments de type chunk. La macro d'insertion-extraction DOM doit donc encore être utilisée pour chaque accès au tableau.

Exemple: soit la spécification suivante:

 

#define N 10
system = a:int[N][N] x b:int[N] ;

Le domaine system s'utilise alors de la façon suivante:

main( )
{
   system s ;

   s = make_system(NIL, NIL) ;

   for (i = 0 ; i < 10 ; i++) {
      for (j = 0 ; j < 10 ; j++) {
         INT(system_a(s)[i][j]) = i*j;
      }
   }

   print_matrice(system_a(s)) ;
}

print_matrice(mat)
chunk mat[N][N] ;
{
   ...
}

 

Fonction d'Entrées-Sorties

Si d est un objet du domaine dom, d peut être écrit sur fichier par un appel à la fonction gen_write.

Cette fonction reconnaıt le type de d (dom) grâce à une information cachée dans d, et lui applique le traitement approprié. Chaque sous-domaine est écrit par un appel récursif à gen_write, le processus se terminant avec les domaines prédéfinis, pour lesquels la valeur est écrite. La fonction gen_write gère les listes et les tableaux.

Un objet écrit sur fichier par gen_write peut être lu par gen_read. Un appel à gen_read a pour effet de recréer un objet d' de type dom identique à l'objet initial d aux adresses mémoire près.

Il peut arriver que deux objets partagent le même espace mémoire, comme par exemple dans le cas d'un graphe dont deux noeuds ont un même successeur. On parle alors de sharing. Un autre cas de sharing arrive avec les listes circulaires, lorsque le dernier élément pointe vers le premier. Le sharing et les listes circulaires sont parfaitement gérés par gen_write et gen_read. L'objet recréé d' par gen_read comportera le même sharing et les mêmes listes circulaires que d.

Un objet peut être détruit par gen_free, fonction qui a le même comportement que gen_write: tous les sous-domaines sont récursivement détruits.

Exemple: le parseur de pips a un programme principal équivalent à:

{
        FILE * fd ;
        pgm p ;

        p = parse_program() ;

        fd  = fopen("exemple.rï, "w") ;
        gen_write(fd, p) ;
        gen_free(p) ;
}

Le paralléliseur a un programme principal équivalent à:

{
        FILE * fd ;
        pgm p ;

        fd  = fopen("exemple.rï, "r") ;
        pgm p = (pgm) gen_read(fd) ;
        pretty_print(parallelize_program(p)) ;
}

Lorsque tout marchera correctement, il sera possible de créer un unique programme pour économiser les accès au fichier fd:

pretty_print(parallelize_program(parse_program())) ;

Dans ce cas, les modifications à apporter aux programmes sont nulles.

 

Remarques sur l'implémentation

L'implémentation de NewGen-C est particulièrement efficace, et notamment:

 

  • les domaines pré-définis sont expansés dans les domaines produits et unions, dans les listes et dans les tableaux; il n'y a donc pas de gaspillage de mémoire ni d'indirection superflue;

    par exemple, l'implémentation d'une liste d'entiers est conforme à ce que tout programmeur écrirait: chaque cons contient un entier et un pointeur sur le cons suivant;

     

  • la plupart des fonctions sont en fait des macros, d'où un gain en taille mémoire car il y a moins de fonctions, et un gain en rapidité;

     

  • les tests dynamiques de types et de cohérence des structures de données peuvent être inhibés pour améliorer la rapidité des parties du projet correctes.

Description de la représentation intermédiaire

 

Principe généraux

Cette représentation intermédiaire de programmes procéduraux a été définie pour traiter des programmes Fortran mais nous avons pris soin de nous appuyer sur les idées générales de la sémantique dénotationnelle pour obtenir une représentation à la fois concise, solide, extensible et aussi indépendante de Fortran que possible.

Cette représentation intermédiaire ne vise pas a rendre compte de tous les problèmes liés aux traitements interprocéduraux qui seront effectués dans le cadre du projet PIPS. Elle a pour but de représenter d'une manière sémantiquement équivalente un programme principal, une subroutine ou une function.

La conservation de la sémantique peut être estimée en essayant d'écrire un interpréteur Fortran basé sur cette représentation.

Il ne faut pas perdre de vue que cette représentation intermédiaire, comme toute structure de données, n'est que le squelette et que les procédures utilisant des informations issues d'elle ne verront en principe que la chair, c'est à dire les fonctions et procédures qui s'y appliqueront (évaluateur d'adresse, évaluateur d'expression, etc...) et qui utiliseront éventuellement d'autres structures de données privées.

Le présent document représente le résultat d'une première phase. La représentation intermédiaire sera ensuite augmentée pour prendre en compte les besoins de l'analyse interprocédurale ou du calcul des dépendances.

 

Spécification NewGen de la représentation intermédiaire

Le fichier de spécifications NewGen de la ri est donnée ci après. La liste définitions de domaines n'est pas donnée dans un ordre logique, mais plus simplement par ordre alphabétique.

 

array       = basic x dimensions:dimension* ;
basic       = int:int + float:int + logical:int + overloaded:unit + 
              complex:int + string:value ;
call        = function:entity x arguments:expression* ;
code        = declarations:entity* x statement ;
constant    = int + litteral:unit ;
dimension   = lower:expression x upper:expression ;
entity      = name:string x type x storage x initial:value ;
expression  = syntax ;
formal      = function:entity x offset:int ;
functional  = parameters:parameter* x result:type ;
instruction = block:statement* + test + loop + goto:statement + call ;
loop        = index:entity x range x body:statement x label:entity ;
mode        = value:unit + reference:unit ;
parameter   = type x mode ;
ram         = function:entity x section:entity x offset:int ;
range       = lower:expression x upper:expression x increment:expression ;
reference   = variable:entity x indices:expression* ;
statement   = label:entity x instruction ;
storage     = return:entity + ram + formal + rom:unit ;
symbolic    = expression x constant ;
syntax      = reference + range + call ;
test        = condition:expression x true:statement x false:statement ;
type        = statement:unit + area:int + array + functional + 
              unknown:unit + void:unit ;
value       = code + symbolic + constant + intrinsic:unit + unknown:unit ;

 

Analyse de la représentation intermédiaire

Dans cette section, nous allons montrer comment cette représentation intermédiaire permet de représenter les différents objets manipulés par un programme Fortran.

 

Domaines

Nous allons tout d'abord examiner chaque domaine pour expliquer brièvement à quoi il sert.

 

Entity = name:string x type x storage x initial:value

Tout objet ayant un nom dans un programme Fortran est représenté par une entity. Un tel objet peut être un module, une variable, un common, un opérateur, une constante, un label, etc. Pour chaque objet, le sous-domaine name de l'entité donne le nom de l'objet tel qu'il apparait dans le texte source du programme, le sous-domaine type donne le type de l'entité, le sous-domaine storage le genre d'allocation mémoire utilisé pour l'entité, et finalement, le sous-domaine initial donne la valeur initiale, si elle est connue, de l'entité. Le terme valeur initiale a ici un sens assez large, puisqu'il s'agit par exemple du code pour les entités représentant des modules.

 

Type = statement:unit + area:int + array + functional + unknown:unit + void:unit

Le domaine type représente le type d'une entité. Le sous-domaine statement est utilisé pour les labels d'instruction. Le sous-domaine area est utilisé pour les commons. Le sous-domaine array est utilisé pour toutes les variables, y compris les paramètres formels et le résultat d'une fonction. Le sous-domaine functional est utilisé pour les fonctions, pour les subroutines et pour le programme principal. Le sous-domaine void est utilisé pour le résultat d'une subroutine ou d'un programme principal.

 

Array = basic x dimensions:dimension*

Le domaine array représente le type d'une variable. Le sous-domaine basic donne le type Fortran de la variable. Le sous-domaine dimensions donne la liste des dimensions de la variable. Un scalaire est un tableau de zéro dimension.

Chaque dimension est une expression, qui n'est pas nécessairement constante dans le cas des tableaux formels. La constante prédéfinie de nom '*D*' est utilisée pour les tableaux de taille non définie (DIMENSION T(*)).

 

Basic = int:int + float:int + logical:int + overloaded:unit + complex:int + string:value

Le domaine basic permet de représenter un type Fortran tel que INTEGER ou REAL. La valeur de ce domaine donne la longueur en octets de la zone mémoire occuppée par une variable de ce type.

 

Dimension = lower:expression x upper:expression

Le domaine dimension permet de représenter une dimension d'un tableau, c'est à dire un couple borne inférieure - sous-domaine lower - borne supérieure - sous-domaine upper.

 

Functional = parameters:parameter* x result:type

Le domaine functional représente le type d'un module, c'est à dire une fonction, une subroutine ou un programme principal. Le sous-domaine parameters donne le type et le mode de passage de chaque paramètre, et le sous-domaine result donne le type du résultat. Ce dernier type vaut void pour les subroutines et les programmes principaux.

 

Parameter = type x mode

Le domaine parameter représente le type et le mode de passage d'un paramètre formel de module.

 

Mode = value:unit + reference:unit

Le domaine mode représente le mode de passage d'un paramètre formel de module. Le domaine contient un objet du domaine value pour le mode de passage par valeur et reference pour le passage par adresse.

 

Storage = return:entity + ram + formal + rom:unit

Le domaine storage permet de préciser dans quelle zone de la mémoire est stockée une entité. Il y a plusieurs zones, qui ne correspondent pas nécessairement à la réalité, c'est à dire aux zones de mémoire qui seraient affectées par un compilateur.

Le sous-domaine return permet de représenter les variables ayant pour nom le nom d'une fonction et auxquelles on affecte la valeur que la fonction doit retourner. L'entité pointée par return est la fonction concernée.

Le sous-domaine ram est reservé aux variables ayant une adresse en mémoire. Il permet de préciser dans quelle fonction et éventuellement dans quelle common ces variables ont été déclarées.

Le sous-domaine formal est réservé aux paramètres formels des modules.

Le sous-domaine rom est utilisé pour toutes les entités dont la valeur n'est pas modifiable, telles que les fonctions, les labels, les opérateurs, etc.

 

Ram = function:entity x section:entity x offset:int

Le domaine ram permet de préciser la déclaration d'une variable. Le sous-domaine function indique dans quel module une entité est déclarée. Le sous-domaine section indique dans quelle aire une entité est stockée; il y a une aire par common déclaré et deux aires spéciales nommées _STATIC et _DYNAMIC pour les entités locales. Enfin, le sous-domaine offset donne l'adresse dans l'aire de la variable.

 

Formal = function:entity x offset:int

Le domaine formal indique le module dans lequel un paramètre formel est déclaré grâce le sous-domaine function, et le rang de ce paramètre dans la liste des paramètres grâce au sous-domaine offset.

 

Value = code + instruction + symbolic + constant + intrinsic:unit + unknown:unit

Le domaine value permet de représenter les valeurs initiales des entités. Le sous-domaine code est utilisé pour les entités modules. Le sous-domaine symbolic est utilisé pour les entités constantes symboliques. Le sous-domaine constant est utilisé pour les entités constantes. Le sous-domaine intrinsic est utilisé pour toutes les entités qui ne dépendent que du langage, telles que les intrinsics Fortran, les opérateurs, les instructions, etc. Enfin le sous-domaine unknown est utilisé pour les valeurs initiales inconnues.

 

Symbolic = expression x constant

Le domaine symbolic est utilisé pour représenter la valeur initiale d'une entité constante symbolique, c'est à dire les PAAMETER de Fortran ou les CONST de Pascal. Le sous-domaine expression permet de stocker l'expression qui a permis d'évaluer la valeur initiale contenue dans le sous-domaine constant. Le sous-domaine expression n'est utile qui si on cherche à reproduire un texte source fidèle.

 

Constant = int + litteral:unit

Le domaine constant est utilisé pour représenter la valeur initiale des entités constantes. Seules les entités de type entier nous intéressent, ce qui explique qu'une constante puisse être soit un int soit un litteral dont on ne garde pas la valeur (type unit).

 

Code = declarations:entity* x statement

Le domaine code est utilisé pour stocker le corps des modules. Le sous-domaine declarations contient une liste d'entités qui sont les variables et commons déclarés dans la fonction. Le sous-domaine statement contient la séquence d'instructions du module.

 

Statement = label:entity x instruction

Le domaine statement permet de repérer les instructions d'un module. Le sous-domaine label contient une entité qui définit le label. Le sous-domaine instruction contient le corps de l'instruction.

 

Instruction = block:statement* + test + loop + goto:statement + call

Le domaine instruction permet de représenter les instructions d'un module. Une instruction peut être un sous-domaine block, c'est à dire une liste de statement, un sous-domaine test pour les instructions de test, un sous-domaine loop pour les boucles, un sous-domaine goto pour les goto qui contient le statement vers lequel le goto se branche, ou un sous-domaine call pour toutes les autres instructions: affectation, appel de subroutine, entrées-sorties, return, stop, etc. Toutes ces instructions sont représentées par des appels à des fonctions prédéfinies dont nous étudierons la nature plus loin.

 

Test = condition:expression x true:statement x false:statement

Le domaine test permet de représenter toutes les instructions à base de contrôle. Le sous-domaine condition contient l'expression à tester, et les deux sous-domaines true et false contiennent les instructions à exécuter selon la valeur du test.

Il faut noter que chaque instruction de contrôle de Fortran, à l'exception de l'instruction DO, est transformée en une combinaison sémantiquement équivalente de tests et de gotos.

 

Loop = index:entity x range x body:statement x label:entity

Le domaine loop permet de représenter les boucles du type DO Fortran ou FOR Pascal. Le sous-domaine index contient l'entité indice de boucle, le sous-domaine range contient les bornes de la boucle, le sous-domaine body contient le corps de la boucle, c'est à dire un statement, le sous-domaine label contient le label de fin de boucle, c'est à dire une entité.

 

Range = lower:expression x upper:expression x increment:expression

Le domaine range permet de représenter les bornes des boucles DO Fortran. Il y a trois sous-domaines lower, upper et increment de type expression qui sont respectivement la borne inférieure, la borne supérieure et l'incrément.

 

Call = function:entity x arguments:expression*

Le domaine call permet de représenter les appels de fonctions. Les fonctions jouent un rôle important dans notre représentation intermédiaire puisque les opérateurs et les instructions Fortran (READ, WRITE, RETURN, ...) sont représentées par des fonctions prédéfinies.

Le sous-domaine function est une entité qui définit la fonction appelée. Le sous-domaine arguments est une liste de sous-domaines expression qui représente les arguments d'appel de la fonction.

 

Expression = syntax

Le domaine expression permet de stocker les expressions. Pour l'instant, ce domaine ne se compose que d'un unique sous-domaine syntax, mais nous pensons ajouter ultérieurement d'autres sous-domaines, notamment pour conserver avec chaque expression linéaire un forme compilée, peut-être sous forme d'un vecteur.

Le sous-domaine syntax contient l'expression avec la même structure que celle du code source.

 

Syntax = reference + range + call

Le domaine syntax permet de représenter les expressions telles qu'elles apparaissent dans le texte source du programme. Un syntax est soit une reference à un élément de tableau (on rappelle que les scalaires sont des tableaux à 0 dimension) , soit un call à une fonction (les opérateurs sont représentés par des fonctions pré-définies), soit un range, dans le cas des expressions bornes de boucles.

 

Référence = variable:entity x indices:expression*

Le domaine reference est utilisé pour représenter une référence à un élément de tableau. Le sous-domaine variable contient une entité définissant la variable référencée. Le sous-domaine indices contient une liste expressions qui sont les indices de la référence.

 

Objets du langage Fortran

Nous montrons à présent comment les différents objets manipulés dans un programme Fortran sont traduits dans notre représentation intermédiaire.

 

Module

Un module est un programme principal, une fonction ou une subroutine.

Un module est représenté par une entity dont le name est le nom du module, le type est un functional qui indique le type des paramètres formels et du résultat, le storage vaut rom et le initial est un code qui contient le corps du module.

Les subroutines et le programme principal n'ont pas d'argument et retournent un void. Les noms des modules sont préfixés par des '_' et le nom du programme principal est prefixé par un '__' pour le différencier d'une subroutine.

 

Commons et aires

Une aire représente une partie de la mémoire où sont rangées les variables. Les commons sont des aires (voir plus loin).

Deux aires spéciales sont créées pour les variables qui n'appartiennent pas à un common (variables locales). Ces deux aires sont des entités qui ont pour name _STATIC et _DYNAMIC, pour type un area qui donne la longueur de l'aire, pour storage un rom et comme initial une value de type unknown.

L'appartenance d'une variable ou d'un common à l'une des ces deux aires spéciales indique si cette variable ou ce common est statique ou dynamique.

Un common est représenté par une entity dont le name est le nom du common, le type est un area qui donne la longueur du common en octets, le storage est un ram qui indique la fonction où le common est declaré (function) et l'aire où le common est rangé (section)..

Le nom des commons est préfixé par un '_', comme les autres identificateurs globaux.

 

Variables - Généralités

Les variables scalaires sont traitées comme des tableaux à 0 dimension.

Une variable est représentée par une entity dont le name est le nom de la variable.

 

Variables - Types

Le type d'une entité ``variable'' est un array qui donne le type fortran des éléments (basic), le nombre de dimensions (longueur de la liste dimensions) et les bornes de chaque dimension.

 

Variables - Allocation mémoire

Le storage d'une entité ``variable résultat de fonction'' est un return qui indique la fonction contenant cette variable.

Le storage d'une entité ``variable paramètre formel'' est un formal qui indique la fonction contenant ce paramètre et le rang de ce paramètre dans la liste des paramètres formels.

Le storage d'une entité ``variable locale ou globale'' est un ram qui indique dans quelle fonction la variable est déclarée (function), à quelle aire (common ou aire spéciale) elle appartient (section) et quelle est son adresse dans ce common (offset).

 

Variable - Valeur initiale

Le initial d'une entité ``variable'' vaut unknown sauf si cette variable est de type entier et est initialisée par data. Dans le cadre de la parallélisation, on ne s'intéresse pas aux autres variables.

 

Constantes numériques et symboliques

Les constantes sont considérées comme des fonctions. Elles sont donc représentées par des entités dont le name est le nom de la constante (12, 13.56E12, '*NOMBRE DE FACETTES:*', PI, MAXITER, etc.), dont le type est un functional à 0 paramètre et 1 résultat qui donne le type de la constante, dont le storage est un rom et dont le initial est un constant pour les constantes numériques et un symbolic pour les constantes symboliques.

 

Opérateurs

Les opérateurs Fortran sont considérés comme des fonctions. Ils sont donc représentés par des entités dont le name est le nom de l'opérateur, et dont le type est un functional qui indique l'arité de l'opérateur (longueur de la liste parameters) mais qui n'indique pas le type des paramètres ou du résultat car le sous-domaine basic vaut toujours overloaded. Le storage d'un opérateur est un rom et son initial un intrinsic.

 

Intrinsics

Les intrinsics Fortran (MAX, MIN, ABS, etc.) sont traités comme des opérateurs.

 

Labels

Les labels sont représentés par des entités dont le name est le nom du label préfixé par un '@', dont le type vaut statement, dont le storage vaut rom et dont le initial est une constante litterale.

 

Instructions simples

Les instructions simples de Fortran telles que RETURN, CONTINUE, STOP, READ, WRITE, PAUSE, END, ... sont considérées comme des fonctions intrinsèques. Elles sont donc représentées par des entités qui ont les mêmes caractéristiques qu'un opérateur à 0 paramètre. On ne tient pas a jour le nombre de paramètres car il ne sert à peu près à rien et que de toute façon, il est variable.

L'instruction PRINT est transformée en WRITE.

 

Instructions de contrôle

Toutes les instructions de contrôle du sous-Fortran que nous acceptons en entrée, à l'exception de l'instruction DO, sont transformées automatiquement en séquences équivalentes de tests à deux branches (une vraie et une fausse), de branchements inconditionnels et de boucles do.

 

      IF (I) 10, 20, 30

devient

      IF (I.LT.0) THEN
         GOTO 10
      ELSE
         IF (I.EQ.0) THEN
             GOTO 20
          ELSE
             GOTO 30
          ENDIF
      ENDIF

 

      IF (I.GT.J) I = I-J

devient

      IF (I.GT.J) THEN
         I=I-J
      ELSE
        CONTINUE
      ENDIF

 

Arguments des instructions d'entrées-sorties

Les arguments des instructions d'entrées-sorties sont soit des informations de contrôle (unité, format, longueur des enregistrements, etc.), soit des références à lire ou des expressions à écrire.

Dans notre représentation, les arguments des instructions d'entrées-sorties sont des listes de couples d'expressions. La première expression du couple est une constante chaine de caractères qui indique la nature de l'argument qui apparaissait dans l'instruction Fortran (UNIT=, FORMAT=, RECL=, ...). La seconde expression est la valeur de cet argument.

Le dernier couple de la liste d'un READ ou d'un WRITE n'est pas un vrai couple: le premier élément est une expression constante qui vaut IOLIST=, et le second élément est une liste d'expressions qui sont les références à lire ou les expressions à écrire.

 

Boucles implicites

Les boucles do implicites dans les entrées-sorties sont représentées par des appels à un opérateur prédéfini (en fait une fonction) de nom IMPLIED-DO, qui prend comme arguments une entité qui définit l'indice de la boucle, une expression range qui définit les bornes de la boucle, et une liste d'expressions.

 

Formats

Les formats sont conservés sous forme d'expressions constantes chaines de caractères. La constante de nom '*F*' est prédéfinie pour les formats libres (list-directed).

Voici un exemple d'instruction d'entrées-sorties.

      READ(2,'(5E16.6)') (IMD(I),I=1,NDOM), K

devient

      (READ 'FMT=' '(5E16.6)' 'UNIT=' 2 
            'IOLIST=' (IMPLIED-DO I 1,NDOM,1 IMD(I)) K)

 

Structures de données de l'analyse sémantique intraprocédurale

Le but de cette section est de présenter les structures de données utilisées pour implémenter les deux premières phases de la méthode d'analyse sémantique de Cousot et Halbwachs. Ces deux premières phases, traduction du graphe de contrôle en un système aux polyèdres et résolution de ce système, ont été introduites dans le précédent rapport intermédiaire.

Les ensembles de valeurs entières que peuvent prendre les variables scalaires entières des modules en chaque point de contrôle sont approximés par des polyèdres à bornes rationnelles et non entières pour diminuer la complexité des calculs.

Ces polyèdres peuvent être définis de deux manières équivalentes:

  • implicitement, par un système d'égalités et d'inegalités linéaires vérifié par les points appartenant au polyèdre,
  • ou explicitement, par un système générateur formé de sommets, rayons et droites dont les combinaisons linéaires sont les points du polyèdre.

Ces deux représentations, qui font l'objet des deux premières sous-sections, sont utilisées simultanément parce que certaines opérations s'effectuent mieux avec l'une qu'avec l'autre (intersection par système d'équations, union par système générateur) et parce qu'on les utilise toutes les deux pour éliminer la redondance qui apparaıt au fur et à mesure des calculs. Les polyèdres que nous manipulerons comporterons donc deux parties, l'une donnant la représentation par système d'équations et l'autre la représentation par système générateur.

La dernière sous-section est consacré à la représentation du système d'équations aux polyèdres construit à partir de la représentation intermédiaire produite par l'analyse syntaxique et à partir de son graphe de contrôle.

 

Représentation des systèmes d'égalités et d'inégalités linéaires

Le système de contraintes est représenté par la matrice de ses coefficients et par un vecteur de termes constants. Cette matrice, étant généralement très creuse, est représentée par des vecteurs creux et par lignes. En effet, il faut choisir entre une représentation par ligne et par colonne, et on effectue plus souvent des combinaisons linéaires de lignes dans l'algorithme de test de dépendance qui partage cette structure de données avec l'analyse sémantique.

Chaque colonne de la matrice correspond à une variable scalaire entière du module analysé, une entité de la représentation interne. Ces entités sont renommées 1, 2,... pour les traitements mathématiques mais on conserve un tableau de correspondance permettant de passer du numéro d'une variable à l'entité correspondante.

On complète chaque ligne de la matrice de contraintes par le terme constant qui reçoit conventionnellement le numéro de variable 0.

 

Vecteur

Un vecteur est une liste de couples (numéro de variable, valeur). Cette liste de couples n'est pas triée dans la première implémentation qui est faıte car nous faisons l'hypothèse que le nombre de coefficients non nuls restera très faible tout au long des calculs.

Le vecteur nul est représenté par une liste vide.

Ce type vecteur est utilisé pour représenter les contraintes, les sommets, les rayons et les droites.

 

Contrainte

Une contrainte est soit une égalité, soit une inégalité (on ne sait pas faire la distinction à ce niveau). Elle contient donc implicitement un terme constant associé à la variable de numéro 0.

On associe à chaque contrainte de type inégalité les éléments du système génerateur qui la sature au moyen de tableaux d'entiers.

Chaque contrainte contient aussi un pointeur vers une autre contrainte pour pouvoir constituer directement les deux listes d'égalités et d'inégalités d'un système.

 

Système

Un système linéaire est constitué de deux listes de contraintes, égalités et inégalités. C'est à ce niveau que l'on dispose de l'information n'ecessaire pour savoir comment traiter les termes constants. Il contient aussi le nombre de ces égalités et de ces inégalités, ainsi que le nombre de variables du système.

Pour permettre la sortie de messages compréhensifs, chaque système contient aussi un pointeur vers un tableau d'entiers permettant de retrouver les numéros d'entités correspondant à chacune de ses variables.

 

Représentation des systèmes générateurs

 

Définition d'un système générateur

Comme les systèmes générateurs sont moins connus que les systèmes d'inégalités, nous rappelons que ce sont des triplets de trois ensembles appelés respectivement sommets, rayons et droites. Le polyèdre $P$ généré par un système $S=\{\{\vec{s_i}\},\{\vec{r_j}\},\{\vec{d_k}\}\}$ est défini par l'équation:

 

\begin{displaymath} P = \left\{ { \vec{v} / \exists \lambda_i \geq 0 \; \wedge... ... \sum_j \mu_j \vec{r_j} + \sum_k \nu_k \vec{d_k} }\right\} \end{displaymath}


 

 

Sommets

Comme nous approximons les ensembles de valeurs entières par des ensembles de valeurs rationnelles, il se peut que les sommets aient des coordonnées rationnelles. Ils sont donc représentés par un vecteur à coefficients entiers et par un unique dénominateur.

On associe aussi à chaque sommet un tableau des équations qu'il sature (i.e. vérifie).

Enfin, chaque sommet contient un lien vers un sommet suivant, qui permet de constituer l'ensemble des sommets d'un système générateur.

Au plus haut niveau, on garde un pointeur vers le premier sommet, ainsi que le nombre total de sommets de la liste.

 

Rayons et droites

Les rayons et les droites sont des objets identiques. Seule change l'interprétation qu'on en donne et la manière dont on les traite.

Ils sont représentés comme les sommets, mais ne contiennent pas de champs dénominateur puisqu'ils sont définis à une constante multiplicative près. A chacun d'eux est associé un vecteur, un ensemble de numéro d'équations saturées, et un pointeur vers l'élément suivant. En tête de liste, on conserve le nombre d'éléments.

 

Système générateur

Un système générateur comporte donc trois champs, qui sont les têtes de liste des sommets, des rayons et des droites.

 

Représentation d'un système d'équations aux polyèdres

Un système d'équations aux polyèdres est une structure de données complexe qui mérite une description en plusieurs étapes.

 

Représentation d'un polyèdre

Un polyèdre est représenté par une paire de pointeurs, un vers le système générateur du polyèdre, l'autre vers son système d'inégalités et égalités.

 

Le système

Un système d'équations aux polyèdres comprend quatre champs. Le premier champ permet de passer des numéros de variables propres aux routines mathématiques aux numéros d'entité définis par l'analyseur syntaxique.

Le second champs permet de connaıtre le nombre des variables effectivement utilisées dans l'analyse sémantique. Le troisième champ est le système d'inégalités qui définit l'état initial du module.

Enfin le quatrième champ est composé d'une liste généralisée d'équations sémantiques. Les éléments primitifs sont les équations sémantiques, mais les équations relatives aux instructions d'un corps de boucle sont regroupées en une sous-liste, et ce, récursivement.

Cette structuration hiérarchique des équations, qui reflète la structure du graphe de contrôle, permettra de rechercher d'abord les points fixes du système sur les boucles les plus internes.

 

Equation sémantique

Une équation sémantique a cinq champs. Le premier est le polyèdre $P$ caractérisant les valeurs des variables en ce point du programme. Le second donne la liste des paramètres permettant de calculer $P$ en fonction des autres polyèdres $P_i$ et le troisième le type de relation existant entre $P$ et les $P_i$. Le quatrième champ permet de savoir si ce polyèdre doit être conservé ou non pour être ultérieurement utilisé dans le processus de parallélisation. Le cinquième et dernier champ permet de savoir à quel point de contrôle du module il faut rattacher l'information $P$.

 

Divers types d'équations sémantiques

Les différents types d'équations que nous savons traiter sont:

  • les affectations non-linéaires (
  • les affectations linéaires inversibles (
  • les affectations linéaires non-inversibles (
  • les tests non linéaires,
  • les tests linéaires de type inégalité pour les branches vrai et faux (
  • les tests linéaires à égalité, branches vrai et faux (
  • les noeuds de jonction qui sont les noeuds du graphe de contrôle qui ont plus d'un antécédent (regroupement après un ou en tête de d'un
  • les noeuds d'élargissements, qui caractérisent les boucles et qui permettent d'éviter les itérations infinies vers un point fixe (opérateur d'élargisssement défini par Cousot).

A chaque type d'équations est associé une structure de données différentes dont les champs contiennent exactement les paramètres nécessaires: polyèdre(s) en entrée, variable modifiée, expression linéaire, etc...

 

Conclusion

Les structures de données présentées dans ce rapport d'avancement numéro 2 permettent de construire les premiers programmes du projet PIPS. Notons que celles qui sont relatives à l'analyse sémantique seront probablement modifiées pour diminuer le temps d'exécution, qui est le talon d'Achille de la méthode d'analyse de Cousot et d'Halbwachs.

 


Footnotes

... rapport1
Ce rapport est en cours d'étude par l'équipe de Pierre Leca, à l'O.N.E.R.A.
... spécifications2
si l'ordre import est utilisé, il faut inclure plusieurs fichiers header; la liste des fichiers à inclure et l'ordre dans lequel ils doivent être inclus sont donnés par NewGen-C au moment de la compilation.

Document Actions

« April 2023 »
April
MoTuWeThFrSaSu
12
3456789
10111213141516
17181920212223
24252627282930