Philippe Gac Université de PARIS 7 Mémoire de DEA Stage effectué à l'INRIA en été 88 sous la direction de Gérard Huet WIMEL Un éditeur en CAML Impression: novembre 4, 2001 Wimel 2 I. Introduction Wimel est un éditeur entièrement écrit en CAML dont l'intérêt, plus que de fournir un éditeur utilisable et performant, est de formaliser dans ce langage ce qu'est un éditeur plein écran de lignes, c'est à dire un programme permettant d'éditer de manière interactive un texte structuré en lignes de caractères. Renonçant à l'efficacité, les données ont été structurées au maximum et les actions décomposées selon ces structures. Dans son état actuel, Wimel comporte toutes les fonctions fondamentales d'un éditeur (insertion, suppression, déplacement) et gère les scrollings horizontaux et verticaux. Il peut manipuler plusieurs textes affichés dans des fenêtres différentes, un même texte pouvant être visible dans plusieurs fenêtres. Wimel 3 II. Présentation A. Conventions typographiques dans le rapport " Encadre un mot utilisé en un sens légèrement différent de son sens intuitif ou habituel; seule la première occurence est ainsi mise en relief «module:nom»» Fait référence à une fonction dans un source Si le nom de module est omis, il s'agit du module courant Error! Reference source not found. mémoire Fait référence à un chapitre dans le présent souligné Définition explicite nom{n} Concatène au nom le texte encadré (ici un entier) pour former un nouveau nom nom1/nom2 Alternative, si plusieurs signes se suivent, ils signifient "respectivement"; précédence supérieure à celle de la juxtaposition des mots Les intervalles (partie d'un d'ensemble totalement ordonné qui contient toutes ses valeurs intermédiaires pour l'ordre) sont en dans la mesure du possible définis par le premier élément inclus et l'après-dernier (c'est à dire celui qui suit le dernier) exclu. B. Utilisation Sous CAML, lancer load "Winit";; et suivre les instructions. L'affichage du texte s'effectue dans une fenêtre xterm spécifique distincte de celle de CAML, il faut cependant laisser le pointeur de la souris dans cette dernière où l'éditeur écrit les éventuels messages d'erreur. Les fonctions accessibles au clavier sont les suivantes, affectées aux mêmes touches que dans Winnie: Wimel 4 caractères insertion des caractères flèches backspace del ^D return ^O R9 R15 tab ^A ^E déplacement dans le texte effacement dernier caractère effacement caractère courant passage à la ligne suivante insertion ligne blanche scroll-up scroll-down tabulation début de ligne fin de ligne L2 quitte, le texte est conservé dans le contenu référencé par la variable dt NE PAS TAPER: ^C ^Y ^Z ^\ ^S Ne rien entrer dans la fenêtre d'affichage, si elle est perturbée par un affichage intempestif, F8 réaffiche le texte On a en outre les fonctions "système": F1 réinitialisation (cf «dtxt:DTrebuild»») F2 liste les caractéristiques du buffer (cf «dtxt:DTdiagram»») F3 idem (cf «dtxt:VPdiagram»») F7 recentre le texte autour du point (cf «dtxt:DTcenter»») F8 réaffiche C. Fichiers Ceux indiqués dans la figure ci-dessous suffixés par .ml (seul Wcore se compile sous l'option open_core()) et les fichiers suivants: Wdebug Wkey_names_sun Utilitaires de debugging Noms de touches pour le clavier virtuel Wimel Wmarks > 5 Wprelude | Wlvect | Wgvect | Wvpart | Wtext | Wlines | Wdtxt < | Wimel < Wwin Wkey2 < Wterm < Wkey1 < Wcore Les dépendances des fichiers sont indiquées dans la figure qui précède; sauf celles de Wprélude nécessaire à presque tous les fichiers. Tous leurs noms commencent par la lettre W; l'extension ".ml" est implicite. La plupart des identificateurs contiennent un préfixe qui désigne leur fichier de définition. Voici une présentation rapide de leur contenu qui sera développé par la suite: fichier préfixe(s) contenu Wprelude Wlvect Wgvect Wvpart Wlines Wmark Wtext Wterm Wwin Wdtxt Wlt Wkey1 Wkey2 Wimel Définitions générales LV Vecteurs linéaires GV Vecteurs troués VP CHAR Partitions et vecteurs de caractères LN Alignement vertical DB MQ Marques TX Textes TERM Terminal virtuel WIN Fenêtre d'édition DT Texte affiché LT Arbres étiquetés K1 Clavier virtuel K2 Assignations de touches Boucle principale et initialisation D. Conventions 1. Dénomination des identificateurs Les identificateurs sont en général précédés d'un préfixe en majuscules qui indique le module d'origine et souvent le type de l'argument "principal"; on appelle transformation une fonction XXname t1 .. tn x -> x' telle que x' (dont le type, identique à celui de x, sera souvent celui rattaché au préfixe XX) puisse être considéré comme une transformation de x par XXname. Les arguments t1..tn sont alors appelés paramètres. La place de x permet de composer des Wimel 6 transformations de paramètres fixés. Les constructeurs, comme c'est l'usage en CAML, ont un nom dont la première lettre est majuscule, ce qui génère des noms de la forme XX_Nom tandis que les types s'écrivent soit entièrement en minuscules s'ils définissent une structure (ex: vector), soit en majuscules lorsqu'il s'agit de types discrets (type ON_OFF = ON | OFF) ou lorsqu'ils servent simplement à restreindre le typage (type POSITION = Pos of num). Certaines fonctions sur les structures composées admettent des formes analogues sur plusieurs types (tel l'itérateur XXdo qui applique une fonction à chacun des éléments de la structure, qu'il s'agisse de liste, de vecteur, de vecteur troué...). D'autres admettent des extensions naturelles à un type contenant le type considéré. Dans le premier cas, la fonction aura un nom séparé de son préfixe comme LIST_do, GV_do... et serait candidate à la surcharge. Dans le second cas, cette écriture sera réservée au cas où les deux types sont isomorphes, c'est à dire si la structure supplémentaire peut être déduite; ainsi, un texte (suite de caractères, CR compris) est isomorphe à la structure qui contient en plus des pointeurs sur les débuts de ligne. On définit souvent, lorsque Y est un composant du type XX: XXY projection sur la composante Y du type XX XXdoY f applique f à la composante Y de XX XXtrY f applique (l'endomorphisme) f à la composante Y, fournit un XX avec le résultat comme nouvelle composante Y 2. Annotation des sources Les fonctions sont documentées dans les sources du programme. On rencontrera les sigles suivants: (DEBUG) fonction de debugging non utilisée en temps normal (*##*) partie obsolète faisant double emploi avec d'autres fonctions maintenue provisoirement (BUG XXX) partie comportant ou subissant un bug non corrigé déjà mentionné Le source ne sera pas paraphrasé dans le présent mémoire qui explique les structures de données, l'architecture du programme, et surtout les choix qui ont été faits. On évoque aussi les extensions possibles du programme, non réalisées faute de temps. Wimel 7 E. Style de programmation 1. Généralités Le programme est écrit pour une version de CAML V2-5. Il utilise autant que possible les particularités de CAML. Les références ne sont quasiment pas utilisées, les modifications de vecteur (cf Error! Reference source not found.) ne le sont que pour des raisons de rapidité et les effets de bord ainsi générés ne sont utilisés qu'au sein de la fonction où ils sont introduits (et en aucun cas d'une fonction sur l'autre) si bien qu'il serait facile de les remplacer par des fonctions avec recopie. La fonctionnalité permet une formulation concise mais ralentit l'exécution, elle pourrait souvent être implémentée par des macros. On effectue parfois des coercions qui restreignent le type proposé par CAML pour les arguments d'une fonction afin de clarifier son utilisation et d'homogénéiser les types des fonctions semblables. Le type PRINT est préféré à void pour les fonctions d'affichage du niveau de print_~. On définit pour la plupart des types un nouveau printer (découvert tardivement, mériterait une mention en caractères gras dans le chapitre "trace" du manuel de référence) qui n'impriment que les éléments significatifs des structures. 2. Typage des structures L'inexistence de l'opérateur & du C (fournissant un pointeur sur le contenu d'une variable déjà définie à l'inverse du ref de CAML où l'identificateur désigne l'adresse) et le caractère explicite de la déréférence incitent à transmettre les structures et donc à les décomposer afin de ne transmettre que la partie nécessaire. Renoncer à cette utilisation des références est un choix de style, il appartient au compilateur de s'apercevoir que, la structure n'étant pas modifiée, il est inutile d'en faire une copie. Dans la version V2-5 de CAML, celle-ci n'est pas modifiée puisque non modifiable; le cas des vecteurs sera examiné plus loin. Les types record avec lecture des sous-champs annoncés pour le CAML V2-6 permettront d'éviter de nombreux appels de fonctions. Les types produits "à composantes positionnelles" imposent une forte structuration des données (la structure la plus générale comporterait 30 éléments au moins, elle a été décomposée sur 4 niveaux). L'accès à un élément devient difficile, il faut regrouper les composantes judicieusement afin de ne transmettre aux fonctions que celles qui lui sont utiles. L'approche est ici très différente de celle en C où l'on regroupe tout en une seule structure linéaire dont on transmet l'adresse à toutes les fonctions qui lisent et "charcutent" les éléments voulus. Outre l'absence (rédhibitoire) des records dans la V25, cette méthode est peu sûre pour les données et inintéressante, la décomposition des structures et des tâches étant justement l'un des rares intérêts du programme. En revanche, l'auteur ne voit pas d'inconvénient à ce que le compilateur reconstitue cette structure linéaire et génère le code idoine... Le concept de type mutable, s'il était implémenté et utilisé entraînerait un gain de temps et d'espace mémoire important. De plus, il apporterait une vision différente du programme, le texte étant considéré non plus comme objet mathématique mais comme une base de donnée, aspect bien plus naturel. Il se poserait aussi des problèmes Wimel 8 nouveaux, ainsi, lors de la modification des structures, celles-ci sont temporairement invalides (par exemple, lorsque les composantes sont incohérentes car partiellement actualisées), il se pose alors le problème de détecter à la compilation, par des mécanismes à définir, les cas les plus flagrants de transmission en argument de telles structures. 3. Itérateurs et combinateurs On utilise rarement des fonctions récursives, des itérateurs définis pour chaque structure permettent de ne pas se soucier de l'implémentation des structures (cf Error! Reference source not found.). Ils sont définis, souvent pour plusieurs structures, dans les modules correspondants et sont préfixés en conséquence. La position au sein s'une structure peut être indiquée par un entier (indice) dans le cas d'un vecteur, d'une suite d'entiers pour un arbre, d'un pointeur pour une liste, ... Notons que le ref de CAML qui crée un objet ne permet pas d'obtenir un pointeur sur un élément de tableau (ce qui serait normal pour un type non modifiable) et ne permet pas de représenter toute position par un pointeur, ce qui introduirait une relation polymorphe entre le type de la donnée et celui de la position TYP <--> (TYP POINTER). De nombreuses structures sont orientables, le "balayage" peut alors s'effectuer en "avant" ou en "arrière"; dans le cas d'un liste/ chaîne/ vecteur le sens avant est du début vers la fin. Les fonctions qui balaient la structure auront 2 formes indiquées par les infixes _f_ (forward) et _b_ (backward). On dit qu'un élément a de la structure est antérieur / postérieur à b si le balayage avant / arrière lit successivement a puis b; pour les structures linéaires, on peut définir la (sous-structure) partie antérieure / postérieure d'une structure par rapport à un élément. Une structure est pointée si on lui associe une position parfois appelée position courante. Un texte_en_édition est en première approximation un élément pointé du monoïde libre engendré par les caractères. Noter enfin l'abus de langage qui consiste à désigner par structure tant l'espèce de structure que ses réalisations (la structure est à l'espèce de structure ce qu'un objet CAML est à son type) et son implémentation en CAML. Voici les principaux itérateurs, avec leur définition intuitive générique; pour les définitions précises et leur implémentation, on se reportera aux sources (en particulier, Wlvect et Wgvect, ceux de Wgvect utilisant parfois dans leur définition ceux de Wlvect). do f exécute f pour chacun des éléments map f exécute f pour chacun des éléments, retourne la structure des résultats {b/f}_accu f accumule f en avant/arrière {b/f}_i_accu f accumule f, fournit en plus à f, la position courante r_accu f on fournit à f la structure postérieure (à la position courante) et la position; Wimel 9 f retourne une position suivante fold f map + f_accu f retournant une paire de résultats on accumule l'un et met en liste l'autre ins del insère un élément à la position courante supprime l'élément à la position courante sub_to_end sub_to_beg sub_inter extrait la sous-structure postérieure extrait la sous-structure antérieure extrait une sous-structure comprise entre 2 positions Ces sous-structures ne sont pas modifiables Des fonctions analogues trsub_ effectuent une transformation et réinjectent le résultat On définit divers opérateurs de composition qui ne calculent jamais plusieurs fois le même résultat à cause des effets de bord potentiels des transformations (cf Error! Reference source not found.). L'existence de nombreuses transformations (au sens précisé plus haut, de la forme T p1 .. pn X -> X') conduit à définir le combinateur S1 utilisé lorsque les paramètres dépendent de l'objet transformé: S1 nth length extrait le dernier élément d'une liste. Le combinateur S=S2 est peu utilisé. 4. Exceptions Les exceptions sont largement utilisées. Les failures sont réservées à des erreurs non récupérables, des erreurs système ou dénotent des erreurs de programmation. Les autres exceptions sont définies dans les modules auquels elles se rapportent; dans le Wprelude, on définit les exceptions (of void) suivantes: STOP sortie avancée d'une itération OUT tentative d'accéder au delà du dernier ou premier élément d'une structure ordonnée PASS dans une itération de construction d'une structure linéaire formée des résultats d'une fonction appliquée à chaque élément d'une autre structure, indique, lorsque raisée dans la fonction, que l'élément courant ne génère pas d'élément On définit d'autre part XXX_STOPPED of XXX (XXX étant un type) afin de récupérer le résultat partiel d'une itération dont on est sorti avant la fin. Enfin, on pourrait éviter l'emploi de références externes à une fonction lorsqu'on veut, au retour normal d'une fonction transmettre une information supplémentaire Wimel 10 exceptionnelle, à capter dynamiquement par une fonction appelante et produisant une erreur (comme exception not trapped) si elle n'est pas captée d'ici le retour au toplevel. Un tel objet, une exception douce, serait géré comme une exception mais ne provoquerait pas de rupture de séquence des instructions et éviterait donc la cascade de try .. reraise. Ainsi, type special_key = ctrl_C | ctrl_P ; soft_exception sp_key of special_key ;; permettrait de traiter l'appui sur la touche lorsque c'est possible. Par exemple: exception ESC;; (* touche ESCAPE enfoncée *) let x = try input() with ESC -> x in ... (* si ESC est enfoncée en cours d'entrée, la valeur est inchangée *) when sp_key(x) do match x with ctrl_C -> do ask_if_abort(); (*raise ABORT si confirme*) | ctrl_P -> stop_printing(); On peut aussi envisager la syntaxe: soft_catch input(x) with special_key(x) ... Wimel 11 III. Les structures vectorielles de Wimel A. Vecteurs CAML 1. Primitives Les vecteurs CAML sont des tableaux à une seule dimension de longueur à définir lors de la création via l'instruction vector de syntaxe vector n of x qui crée un vecteur de n éléments de valeur x. Il est possible par la suite de modifier les éléments individuellement par vect_assign(v,i,x') et d'en lire la valeur par vect_item (v,i). Le programme définit quelques autres fonctions permettant des affectations multiples; cela est détaillé dans le début du fichier Wlvect. 2. Restrictions Afin de garantir que tous les éléments sont de même type, celui de la valeur initiale ne peut avoir d'indéterminées (et donc ne peut être polymorphe): let v = vector 10 of [] ;; value v: 'a list vect genère en CAML V2-5 attempt to create a polymorphic vector; si on pouvait écrire: let f (v:'a vect) = vect_assign(v,0,0) ; vect_assign(v,1,"") ; v ;; le typechecker ne pourrait garantir l'homogénéité du vecteur à moins de préciser le type de v a posteriori et selon le séquencement des affectations (ce qui est facile ici, et réalisable dans le cas général s'il n'y a pas de toplevel). Notons qu'il n'y a plus de problème avec: let f (v:'a vect) = let v = Vect_assign(v,0,0) in let v = Vect_assign(v,1,1) in v ;; si Vect_assign retourne une copie du vecteur ou s'il retourne v "modifié" et si le programmeur s'impose de ne plus utiliser l'identificateur lié au vecteur non modifié. Cette convention impose donc aux fonctions de manipulation de vecteurs de ne pas utiliser d'effets de bord. D'autre part, les combinateurs ne doivent jamais effectuer 2 transformations sur le même vecteur, la donnée n'étant plus valide après le premier calcul. La version actuelle de CAML interdit purement et simplement les vecteurs polymorphes (c.à.d dont le type des éléments n'est pas entièrement défini); Wimel respecte cependant la seconde restriction; on pourrait donc y remplacer toutes les Wimel 12 affectations d'éléments de vecteurs par des affectations avec recopie sans autre conséquence qu'une perte de rapidité. 3. Vocabulaire On appelle désormais élément d'un vecteur le couple de son indice et de sa valeur, segment une liste d'éléments consécutifs. On s'autorisera cependant à dire qu'un élément est égal à sa valeur et qu'on lui applique une fonction (de domaine celui de sa valeur)... Remplacer un élément ou un segment d'un vecteur, c'est fournir un vecteur dont les éléments sont identiques sauf les éléments des indices spécifiés. Si le nouveau vecteur est le même, modifié physiquement par vect_assign, on considère que l'ancien n'est plus accessible (cf note sur les "types modifiables"); cela sera implicite chaque fois qu'on utilisera le terme "fournir". Déplacer (ou décaler, recopier) un élément/segment en (une position d'indice) i, c'est fournir un vecteur dont les éléments sont identiques sauf ceux d'indice i /(et les suivants) prennent la/les valeurs de l'élément/segment; phrase résumée en "fournir un (nom du vecteur) dont ...". Remplir un vecteur avec une valeur (du type des éléments du vecteur), c'est fournir un vecteur de même dimension dont tous les éléments sont égaux à cette valeur. B. Vecteurs linéaires 1. Définitions Un vecteur linéaire est un segment d'un vecteur CAML; il n'apporte donc rien de plus sinon un accès plus facile à l'indice maximal du vecteur et la possibilité de gérer plusieurs vecteurs linéaires de même type dans un même vecteur CAML, permettant ainsi une gestion explicite de la mémoire. Cette possibilité n'a toutefois pas été utilisée. Le vecteur linéaire est aussi un moyen simple d'implémenter des vecteurs de longueur variable (bornée) en considérant des segments de vecteur CAML et en affectant aux autres éléments une valeur quelconque. Insérer un élément ou un segment à une position d'indice i consistera à l'écrire après avoir décalé les éléments d'indice >= i vers la fin du vecteur CAML et repoussé d'autant la fin du vecteur linéaire; si cette dernière excède la fin du vecteur CAML, il se produit une erreur appelée "FULL" associée à l'exception FULL of string où string est le nom de la fonction où elle s'est produite. De même, on dit que supprimer un élément consiste à déplacer les éléments suivant la partie à supprimer au début de celle-ci. Cette structure est définie dans le fichier Wlvect et les fonctions qui s'y rapportent sont préfixées par LV. 2. Opérations On définit sur cette structure divers itérateurs à l'image de ceux déjà définis pour les listes. Le nom subit parfois quelques changements, it_list a pour analogue LV_f_accu afin que le nom ne dépende pas du type. Ceci est détaillé dans Wlvect. Notons l'introduction des LV_xi_accu (qui auraient pu être déjà définis pour les listes) et qui Wimel 13 s'imposent pour noter des positions dans un vecteur. Enfin, LV_r_accu permet, en autorisant la lecture des éléments suivants et une incrémentation variable, d'implémenter facilement des algorithmes de recherche de séquences. On peut aussi extraire un sous-segment mais celui-ci devrait ne pas être modifiable; LVtr_sub f applique l'endomorphisme f à un sous-segment et "injecte" le résultat de même longueur dans le vecteur. C. Vecteurs troués 1. Motivation Les vecteurs linéaires imposent, pour insérer ou supprimer de décaler tous les éléments qui suivent; pour l'éviter, on peut imaginer un décaleur paresseux qui attend d'avoir un certain nombre d'opérations, certaines s'annulant entre elles , avant d'effectuer le décalage. En édition de texte, on peut par exemple éditer la ligne courante dans un petit (donc dans lequel les décalages sont rapides) vecteur de caractères et insérer la ligne modifiée à la place de l'ancienne dans le vecteur principal dès qu'on change de ligne. Cette méthode présente l'inconvénient de devoir gérer 2 structures, ce qui devient inextricable lorsqu'un texte peut être affiché dans plusieurs fenêtres différentes ou bien si on veut en écrire une partie en inversion vidéo afin de matérialiser la sélection d'un bloc. Une autre solution aurait consisté à utiliser une liste modifiable bidirectionnelle permettant donc l'insertion immédiate en un endroit quelconque, mais nécessitant 3 pointeurs par élément en LISP et 5 en CAML ([element; ref element_precedent; ref element_suivant]); on imagine aussi la difficulté à rechercher une séquence d'éléments... Dans le vecteur troué, la partie "inutilisée" (du vecteur CAML), au lieu d'être à la fin, est placée à l'endroit où s'effectue l'insertion dit point (d'insertion); un vecteur CAML ainsi utilisé se décompose donc en trois segments: le segment des éléments avant le point, celui des éléments du vecteur CAML inutilisés et celui des éléments après le point. Par la suite, on les désignera par segment1, trou, segment2. Insérer ou supprimer un élément/segment consiste simplement à remplacer des éléments inutilisés par cet élément/segment ou vice-versa. En contrepartie, on doit, pour changer la position du point d'insertion, décaler les éléments entre l'ancien et le nouveau point. Ces opérations se comprennent mieux avec des figures. La solution idéale consisterait à utiliser une unité de gestion mémoire qui permettrait d'adresser les 2 blocs continûment par l'égalisation des adresses logiques du début et de la fin du trou. La continuité n'est utilisable que pour les automates de scrutation du texte (recherche...) puisque que les opérations d'écriture nécessitent la connaissance du trou. Les composants électroniques actuels ne permettent qu'un ajustement des adresses à un multiple de 256 (?) ce qui en réduit l'intérêt (il faut alors combler le mini-trou d'au plus 256 caractères avec un caractère neutre, le temps passé à scruter ces caractères risque de dépasser celui qu'on aurait dépensé à tester l'atteinte du début du trou). Il existe des dispositifs qui permettent de générer une exception microprocesseur lors de la tentative d'accès à une adresse. Enfin des composants spécialisés habituellement utilisés pour la gestion des écrans graphiques de haute résolution effectuent les décalages mémoire très rapidement. Si ces solutions sont fortement dépendantes de la machine, elles montrent l'intérêt de disposer de fonctions de mouvement de "blocs" (pour les objets du langage, vecteurs...) comme primitives du langage. 2. Sémantique des dessins... Un vecteur troué est un vecteur linéaire composé de 3 segments qui sera représenté graphiquement ainsi: Wimel 14 |AAAAAAAAA|XXXXXXXXXX|BBBBBBBBBBBB| |ib |i1 |i2 |ie les éléments seront toujours désignés par une lettre minuscule; une suite de lettres majuscules formant un identificateur désigne un segment d'un vecteur linéaire; une suite de X designe un mot quelconque et se comporte comme un joker ("wildcard"). La présence d'espaces dans la 2ème ligne indique qu'elle doit être interprétée comme définissant les valeurs des identificateurs qui y figurent comme égales à l'indice de l'élément d'une ligne supérieure suivant la barre verticale alignée; si plusieurs lignes de cette sorte se suivent, il n'est pas nécessaire de compléter l'alignement des barres sur toutes les lignes; une ligne vide termine cette convention. Enfin, les chevrons > permettent de spécifier la longueur d'un segment. 3. Vocabulaire L'indice d'un élément d'un vecteur troué est celui qu'il a dans le vecteur CAML sousjacent; i2 est l'indice succédant à i1-1 dans le vecteur troué. On définit aussi l'élément suivant/précédent d'un élément. Le contenu d'un vecteur troué est le mot (du langage engendré par les éléments) obtenu par concaténation des mots associés à chacun de ses 2 segments. Le contenu pointé est le contenu associé à l'indice dans ce dernier de l'élément suivant le trou appelé élément courant. Deux vecteurs troués sont dits égaux/identiques si leurs contenus /pointés sont égaux en tant que mots. On appelle position un "interstice" entre éléments, l'interstice d'un élément est celui qui le précède; l'indice d'une position est celui de l'élément qui la suit; ce qui amène à introduire un élément fictif à la fin du vecteur troué, l'élément extrême de fin (d'indice ie) inaccessible en tant qu'élément. De manière analogue, on introduit l'élément extrême de début d'indice i1-1 suivi de l'interstice de début et l'élément fictif d'indice i1 qui désigne le même interstice que i2 mais il est utile de pouvoir insérer des éléments de part et d'autre du trou, évitant parfois un déplacement du point. La structure de vecteur troué est définie dans le fichier Wgvect et les fonctions qui s'y rapportent sont préfixées par GV. 4. Opérations sur les vecteurs troués Une opération essentielle est le déplacement du trou "en avant" (sens > 0) ou "en arrière" (sens < 0) d'un nombre arbitraire d'éléments: contenu (1) |QWERTY| W |XXXXXXXXXX|ASDFGHJ| |ib | |i1 |i2 |ie > n > contenu (2) |QWERTY|XXXXXXXXXX| W |ASDFGHJ| |ib |i1' |i2' |ie |i2'+n Wimel 15 --[déplacement arrière de n éléments]-> (1) (2) <--[déplacement avant de n éléments]--- Le déplacement en avant/arrière porte le nom de fwd/bwd. Ces opérations (appelées bfwd) entraînent une modification des indices des éléments déplacés tandis que les indices dans le contenu sont invariants. Donnons les schémas correspondants pour l'insertion: Avant: |QWERTY|XXXXXXXXXXXXXX|ASDFGHJ| |ib |i1 |i2 |ie Après insertion du mot UIO: |QWERTY|UIO|XXXXXXXXXX|ASDFGHJ| |ib | |i1' |i2 |ie La suppression de UIO permet de revenir à la situation initiale en "rabaissant" simplement l'indice i1. Ces transformations portent le nom de ins1 et del1; on définit de même ins2 et del2 opérant "du côté" de i2 du trou. On parlera d'écriture pour désigner ces 4 opérations. Enfin, on définit sur cette structure divers itérateurs à l'image de ceux déjà définis pour les vecteurs linéaires. Ceux ne faisant pas intervenir l'indice sont semblables à ceux qu'on aurait pu définir sur les mots, on ne se soucie pas du trou tant que l'on n'effectue pas d'insertions ou de suppressions; de l'extérieur, un vecteur troué devrait être simplement un mot pointé. GVsub permet d'extraire un sous-vecteur troué quelconque dont le trou est éventuellement de longueur nulle si les bornes ne sont pas de part et d'autre du trou; GVtrsub f applique f à un sous-vecteur, qui peut déplacer le trou ou écrire dans la limite de celui-ci, et injecte le résultat dans le vecteur. 5. Fonctions de base de la structure On obtient donc 3 opérations fondamentales: déplacement, insertion, suppression ayant chacune 2 formes duales, de part et d'autre du trou. Les 4 opérations (bwd, fwd, ins1, del2) ou (ins1, ins2, del1, del2) ou ... permettent par composition de générer les autres. Le chapitre Error! Reference source not found. indique comment ces transformations peuvent être utilisées pour l'édition. Cette structure ne se ramène pas exactement à 2 piles (LIFO) car on peut en outre lire la valeur d'un élément quelconque (seule l'écriture nécessite le positionnement du trou) à partir d'un indice absolu. Wimel 16 D. Partitions 1. Définitions Une segmentation d'un vecteur (CAML ou linéaire ou troué) ou d'un mot est une partition de ses éléments en segments. Une sous-segmentation est une sous-partition en segments, elle est dite incluse dans l'autre. Deux partitions sont dites étrangères si aucune n'est incluse dans l'autre. Un mot segmenté est invariant par les transformations bfwd. L'insertion pose le problème du positionnement avant ou après la séparation entre deux segments consécutifs, positions qui correspondent au même interstice (l'élément inséré sera-t-il dans le segment précedent ou suivant). Plutôt que d'introduire des indices fractionnaires, on adopte l'une des 2 solutions: soit interdire purement et simplement l'insertion de l'un des 2 côtés (ce qui est discuté dans Error! Reference source not found.) soit matérialiser les limites par un élément éventuellement réservé à cet usage, autorisant l'insertion de part et d'autre. Une partition lourde de piquet p (élément) est une partition avec parties vides autorisées dont les parties sont soit le singleton du piquet, soit un segment éventuellement vide ne contenant pas le piquet, 2 segments de cette dernière sorte ne pouvant se succéder (cela revient à insérer un piquet dans chaque interstice de segments consécutifs; le nombre de parties étant alors majoré par celui des piquets, on peut autoriser les parties vides tout en maintenant fini le nombre de partitions d'un vecteur fini). Un vecteur partitionné (par un piquet p) est le couple vecteur-partition. Le mot partitionné sous-jacent est invariant par les transformations bfwd. La matérialisation par le piquet permet de "fusionner" deux parties consécutives en supprimant un piquet ou de d'en scinder une en en insérant un. Le piquet se comporte donc comme un élément banal; ce n'est plus le cas si on désire conserver par ailleurs la liste des indices des piquets mise à jour après chaque transformation ce qui permet d'accéder directement à un segment d'ordre donné. 2. Implémentation La liste des indices des piquets s'adapte bien à la structure de vecteur troué, il faut de plus adopter certaines conventions afin que la correspondance entre les 2 vecteurs soit la plus naturelle possible. On décide donc d'encadrer le vecteur (des éléments) par des piquets, (assurant ainsi qu'une partie quelconque est encadrée par 2 piquets) appelés éléments extrêmes. Dans les schémas, ils seront séparés du reste du vecteur par un ! au lieu de la barre verticale. Le premier est inaccessible aux fonctions fondamentales, le second est l'élément fictif de fin inamovible permettant seulement l'insertion en son interstice (juste avant). Chaque partie est associée au piquet qui lui succède, une partie pleine est le segment de la partie et du piquet associé. Enfin, le vecteur de partition sera celui des indices des piquets, piquet extrême de fin inclus (cet élément du vecteur de partition sera appelé dernier élément) ;. l'élément extrême de début contenant l'indice du piquet extrême de début. Cette convention pose des problèmes si on veut manipuler plusieurs partitions lourdes étrangères à la fois; si l'une est incluse dans l'autre, on peut introduire la notion de partition suivant un ensemble de piquets. On obtient donc un schéma de la forme: Wimel 17 |$!QWERTY|$|UIO|XXXXXXXXXX|ASDF|$|GHJ!$| |ib |i |i1 |i2 |j |ie |ib!i|XXXXXX|j|ie!X| vecteur de partition si $ représente le piquet; le vecteur de partition contenant des indices sur le vecteur des éléments. Les 2 vecteurs sont cohérents si chaque piquet et son indice sont du même côté du trou de leur vecteur. Cette convention pour les éléments extrêmes permet avant tout aux "itérateurs avant" c.à.d ceux qui transforment ou accumulent selon les indices croissants de générer des expressions simples lors d'itérations sur les segments (les itérateurs arrière étant sacrifiés cf «VP_fi_accu»») car l'indice du piquet extrême de début est alors transmis comme valeur initiale de l'accumulation, à chaque itération, la valeur d'accumulation est l'indice du piquet de début du segment et l'argument courant celui du piquet de fin, qui devient valeur d'accumulation à l'étape suivante (cf «VP_fi_accu»»). A la dernière itération (dernier segment), l'argument est l'indice de l'élément extrême de fin et c'est pour cela qu'il fait partie du vecteur de partition. D'autre part, en édition, c'est le "CR" de la fin de ligne qui est sur la même ligne... 3. Transformations d'un vecteur partitionné Le vecteur de partition est invariant tant qu'on se (déplace sur / supprime / insère) des éléments autres que le piquet. On vérifie alors facilement que les transformations sur le vecteur doivent être associées aux opérations suivantes sur le vecteur de partition: fwd lorsque l'élément courant est un piquet bwd lorsque l'élément courant devient un piquet del{n} suppression d'un piquet ins{n} insertion d'un piquet fwd bwd del{n} ins{n} de i{n} où ins{n} signifie ins1 ou ins2 et i{n}, i1 ou i2, indice de début ou fin du trou du vecteur des éléments Tout cela est développé dans le module Wvpart et la fonction VPexe se charge de coordonner les transformations. On a d'autre part une structure de catégorie: soit T la catégorie d'éléments les vecteurs troués de type fixé et dont un élément (le piquet) est particularisé, et VP celle des vecteurs troués d'entiers, les flèches étant les transformations engendrées par les opérations élémentaires. On alors un foncteur R:T>VP, dit foncteur de reconstruction, qui à un vecteur associe son vecteur de partition. Le choix des fonctions ci-dessus définit les images des flèches de façon à assurer la covariance de R. On peut ausi voir R comme morphisme de préordre total pointé ce qui fournit, avec le groupe des transformations, une structure de 2-catégorie; R est le morphisme attaché à un vecteur par la transformation naturelle qu'est ici la reconstruction. Un sous-vecteur partitionné est le couple du sous-vecteur et du vecteur de partition de la partition induite; son élément extrême de début est encore l'indice du début du vecteur. Le traitement des erreurs nécessite quelques précautions, l'erreur FULL peut se produire indépendamment sur chacun d'entre eux, et dans l'état actuel du module, Wimel 18 l'erreur est fatale. D'autre part il est a priori possible de positionner le trou après le dernier élément du vecteur de partition (ce qui ne correspond à aucune configuration possible du vecteur), il ne faut donc effectuer le déplacement que si l'opération sur le vecteur des éléments est valide, le dernier élément est alors en fait immobile. E. Marques et blocs 1. Définitions On peut aussi pointer le mot en d'autres endroits que le trou. Ces pointages ou marques doivent être invariants lors des déplacement du trou; dans l'implémentation, ce sont des indices sur le vecteur des éléments qui doivent être éventuellement recalculés après une transformation. Il faut préciser le comportement de celles-ci vis-à-vis des autres opérations (ins, del...) sur le vecteur, ce qui amène à en distinguer plusieurs sortes. Une marque lourde est associée à un piquet comme pour les partitions lourdes (d'ailleurs définissables comme suite ordonnée de marques lourdes), l'insertion est possible de part et d'autre et la suppression du piquet est interprétée comme suppression (en plus) de la marque. Une marque (légère) désigne un interstice. Une marque gauche/droite est "rattachée" à l'élément suivant/précédent et le point d'insertion de l'interstice est situé avant/après la marque (autrement dit, pas d'insertion entre la marque et son élément de rattachement). La suppression de l'élément de liaison peut entraîner une suppression de la marque qui sera alors dite très légère ou le rattachement à l'élément remplaçant (marque permanente). Le choix dépendra souvent du contexte et on parlera alors de marque flottante. On dira que l'existence d'une marque est liée à celle d'une autre si la suppression de l'une doit entraîner celle de l'autre. Le point d'insertion se comporte comme une marque permanente gauche liée à l'élément courant. L'interstice de début/fin est une marque droite/gauche liée à l'élément extrême de début/fin. 2. Blocs Un bloc fermé est un segment délimité par 2 marques très légères d'existences liées l'une étant gauche et précédant l'autre, droite. Similairement, on récupère le vocabulaire des intervalles (en mathématiques). La 1ère/2ème marque est dite de début/fin (de bloc) et est dite liée à l'élément dit tuteur de début/fin (de bloc) sous réserve que celui-ci soit dans le segment, sinon il s'appelle butoir de début/fin. Le bloc total est le vecteur tout entier. Un bloc étiqueté est associé à un élément, sa sorte, d'un ensemble préalablement défini. Le schéma d'imbrication d'un vecteur est la grammaire de réécriture dont les sortes sont les non-terminaux et les éléments du vecteur des terminaux et tel que le bloc total soit lié au non-terminal initial. Un vecteur est bien imbriqué si tout bloc est générable (tant les éléments que les sous-blocs) par dérivation du non-terminal associé. (etc...). Wimel 19 3. Implémentation Comme on peut s'y attendre... les marques sont regroupées dans un vecteur troué, de manière analogue aux pointeurs de partitions lourdes. Ceux-ci auraient pu être traités comme des marques mais leur utilisation dans l'édition imposait leur séparation (cf Error! Reference source not found.). Elles sont ordonnées suivant la valeur de l'indice (de l'élément de rattachement). Un identificateur unique permet de retrouver une marque. Les marques sont gérées par le module Wmark. Wimel 20 IV. Les structures "textuelles" de Wimel A. Caractères 1. Vocabulaire L'édition de texte consiste à transformer de manière interactive (du moins avec les éditeurs performants comme Winnie) un vecteur de lettres destiné à un (programme d')application. Certaines de ces lettres, non significatives, servent seulement à faciliter l'écriture et surtout la relecture du texte. L'éditeur se réserve la possibilité d'augmenter l'alphabet par des méta-lettres quitte à générer un texte purgé pour l'application. Un caractère sera un élément d'un vecteur constitué de lettres et de méta-lettres. On appelle caractère banal un caractère dont la représentation à l'écran est un dessin de largeur fixe indépendant du contexte et de la position, caractères de contrôle tous les autres. Les caractères banals ne sont pas forcément tous de la même longueur, sans évoquer l'espacement proportionnel, les caractères "non affichables" sont matérialisés à l'écran par un \ suivi du code ascii en octal ou hexadécimal. Les caractères simples sont ceux dont la largeur est unitaire (c.à.d, pour un écran de texte, composé d'un seul caractère). 2. Implémentation Par souci de clarté dans les programmes et de manière à pouvoir utiliser le filtrage sur les patterns de CAML, la définition d'un nouveau type CHAR à été préférée à l'utilisation du type string; le texte est alors un vecteur troué de CHAR. Cette représentation est très coûteuse en temps et en espace. Ce type est défini dans Wprelude: type CHAR = Char of string | CR | TAB | Word of string | Hexa of string ... Une optimisation intéressante consisterait à utiliser des chaînes de 1 caractère pour les caractères simples et des chaînes avec des codes ascii inutilisés par les premiers pour les autres et de les concaténer pour représenter le texte. Le codage devra toutefois permettre le déplacement d'un caractère à l'autre tant en avant qu'en arrière. Il faudrait alors définir les fonctions sur les vecteurs troués pour cet agglomérat de chaînes; la surcharge permettrait une rédaction élégante. Wimel 21 B. Textes 1. Le vecteur partitionné Les transformations de vecteurs s'appliquent, bwd/fwd reculent/avancent d'un caractère, del1 efface le caractère avant le point ("Backspace"), del2 efface le caractère courant, ins1 insère des caractères en laissant le point immobile, ins2 en l'avançant après les caractères insérés. Le texte est partitionné (lourdement) en lignes, le caractère CR servant de piquet. Un CR est rattaché à la ligne qui le précède. Le caractère TAB génère des blancs jusqu'à la prochaine tabulation, ici, position multiple d'un nombre (donné pour le texte). Pour des raisons de rapidité, on a choisi de conserver des pointeurs de ligne (et non de rechercher les débuts et fin de ligne en scrutant le texte). Le délai encore acceptable en CAML sur une ligne (quelques dizaines de caractères) ou en C en toutes circonstances ne l'est plus en CAML lorsqu'il faut rechercher le début du texte affiché par exemple pour effectuer un scrolling. On aurait certes pu ne conserver que 2 pointeurs de début et fin d'écran... mais le stockage intégral de la structure de ligne et son actualisation locale semblait être une idée séduisante (au début du moins, car elle causa un grand nombre de bugs) qui pouvait préfigurer l'édition structurée où c'est une nécessité. 2. Les marques Un texte est donc un mot pointé de caractères, implémenté comme vecteur troué de CHAR structuré en partition lourde de piquet CR, pointé par des marques, pointages supplémentaires du mot. qui sont éventuellement recalculés (par les fonctions qui agissent sur text) lors des déplacements. Ce recalcul sera appelé relogement. On aurait pu considérer les CR comme des marques, mais leur grand nombre, la nécessité d'utiliser les pointeurs de lignes pour l'affichage et le besoin de pouvoir passer rapidement d'une ligne à l'autre justifient un vecteur particulier bien que cela double toutes les opérations de relogement des indices. Une possibilité non implémentée est d'introduire le texte "complété", augmenté de méta-caractères liant les marques légères et les transformant en marques lourdes dont l'édition permettrait d'insérer entre les marques et leur caractère de rattachement, voire entre les marques elles-mêmes si plusieurs d'entre elles sont rattachées au même caractère. 3. La structure text Un texte est formalisé par la structure text, définie dans «Wtext»». sur laquelle on définit les mêmes opérations que sur les vecteurs partitionnés, et qui effectuent en plus le relogement des marques. Les itérateurs, inutilisés par la suite dans l'éditeur, non définis devraient l'être dans le cadre d'une bibliothèque. Ainsi on définit les analogues des transformations mono-caractères bfwd/ins/del (préfixés par TX) dont l'action sur les le vecteur partitionné et les marques est coordonnée par TXexe. Wimel 22 C. Représentation visuelle 1. La structure "mur de pierres" Wimel sait gérer des textes de caractères de largeur quelconque éventuellement variable. Leur hauteur est cependant constante et dépend tout au plus de la fenêtre d'affichage, les lignes de texte sont forcément linéaires. Une ligne de texte est représentée éventuellement tronquée sur une ligne d'écran; l'unité de largeur qui est actuellement le caractère pourrait être le pixel d'un écran bitmap. La représentation d'un texte est la donnée pour chaque caractère de chaque ligne de sa position, somme des largeurs des caractères qui le précèdent sur la ligne. Cela permet d'introduire l'alignement vertical, pour des caractères de lignes différentes mais de même positions. Notons qu'en fait, il y a au moins 3 représentations possibles: celle (très facile à calculer) de position dans le buffer, celle (choix de Wimel) de l'image formée à l'écran et celle du document final sur papier. Il est souhaitable de disposer de la première pour créer son texte (affiché "complètement" ou presque) et de la troisième pour le modifier et le mettre en page. Il n'est possible, sur un écran, que d'afficher un rectangle de texte, c.à.d une série de segments de lignes dont les positions des caractères sont comprises entre 2 valeurs fixes dites positions de début (ou base) et de fin d'affichage (dans Wdtxt, posb et posb+wd, wd étant la largeur de l'écran). A cause des largeurs de caractères variables, ces valeurs peuvent ne pas coïncider avec le début d'un caractère, d'où l'utilisation de p1 et p2, début et fin effectifs d'affichage car si on peut afficher un caractère partiellement, le caractère courant doit être visible intégralement. 2. Implémentation Le module Wlines traite l'alignement vertical; contrairement à ce qui a été fait pour les lignes, on ne cherche pas à stocker toutes les positions de tous les caractères (en un vecteur troué !) ni même pour ceux de la ligne. Le premier caractère d'une ligne est implicitement en position 0 (il n'y a pas d'indentations de blocs); on se rappelle la position et l'indice du 1er/après-dernier caractère visible et du point. Le "scrolling" horizontal permet de visualiser des textes dont les lignes sont de longueur quelconque, on introduit ms, nombre de positions dont le texte se décale lorsque le point arrive au caractère en extrémité d'écran (pour des raisons de rapidité). Pour chaque transformation mono-caractère sur le texte, on définit une fonction de recalcul de ces paramètres qui se charge de prévoir un éventuel scrolling horizontal. D. Fenêtres 1. Le terminal (écran) virtuel La solution la plus simple, une fois exclue l'utilisation de la fenêtre CAML, était de créer une fenêtre xterm via un comline (permettant l'exécution d'une ligne de commande UNIX Wimel 23 à partir d'un programme CAML) à partir de CAML. On récupère alors le nom du fichier /dev/ttyp associé en tapant tty sous le shell (de la fenêtre créée). On donne ce nom au programme (qui n'a aucun moyen de le connaître car il n'est pas possible de rediriger dans un fichier le numéro de process renvoyé par le shell) qui ouvre ttyp en concaténation et peut alors envoyer du texte mais aussi toutes sortes de séquences de contrôle reconnues par l'émulateur xterm. Afin de minimiser les appels de procédures, les chaînes composant celles-ci nécessaires à la réalisation d'un effet complexe sont concaténées et envoyées ensemble. Ceci est défini dans le module Wterm. Xterm permet tous les décalages verticaux et même la définition d'une limite de scrolling. Une fenêtre Xterm étant iconifiable sous X globalement (et une seule à la fois), il est utile de pouvoir la scinder en plusieurs sous-fenêtres où peuvent s'afficher des textes que l'on désire éditer ou iconifier ensemble. Xterm (mais il s'agit d'une possibilité non obligatoirement implémentée sur tous les émulateurs vt100) permet la scission verticale. En revanche, ni la limitation horizontale du scrolling (même vertical), ni le scrolling horizontal ne sont disponibles. D'autre part, le curseur n'est pas contrôlable. L'interfaçage avec X, indépendamment de celui avec Winnie semble donc souhaitable. 2. La structure "rectangle d'affichage" Le texte est affiché dans un rectangle d'une fenêtre xterm. La position du point dans le texte est matérialisée par un curseur. Dans l'idéal, celui-ci devrait être de la largeur du caractère courant et être un pavé plein lorsque le contenu affiché est en édition (lorsque les entrées du clavier agissent sur ce texte), grisâtre quand l'édition a lieu sur un autre buffer de la même fenêtre xterm ou dans un "mini-buffer" et creux si dans une autre fenêtre (effet habituel). Dans une version antérieure, l'éditeur dessinait le curseur en écrivant le caractère courant en inversion vidéo; l'effet, lent et inesthétique a été abandonné, les segments de sources correspondants ont pour certains été laissés au cas où... Cela et ce qui suit se trouve dans le module Wwin et la structure s'appelle WIN_REG. Il se chargera de convertir les coordonnées relatives dans le rectangle ((0,0) en haut à gauche) en coordonnées absolues de la fenêtre. Les fonctions agissant sur la structure WIN_REG font en fait 3 choses: 1 - une action verticale 2 - une action horizontale 3 - une action sur l'écran La philosophie du programme est de dire que 3 n'est rien puisqu'on ne demande jamais aucune information à l'écran, ce qui permet de dire qu'il n'y a pas d'effets de bord. Le scrolling horizontal est détecté en amont dans Wlines et annule l'éventuel scrolling vertical. Les fonctions n'ont à détecter que ce dernier et doivent alors raiser SCROLL UP/DOWN (1 seule ligne à la fois). L'action horizontale consiste simplement à actualiser l'abcisse du curseur. On a ainsi défini WIN{c}{bwd/fwd / ins1/ins2 / del1/del2} pour chacune des fonctions élémentaires. WIN{f} est utilisée lorsque le caractère intervenant n'est pas un CR, il n'y a alors pas d'action verticale. WINc{f} est exécutée si c'en est un; lorsqu'il y a scrolling horizontal, l'affichage normalement produit par la fonction est inutile car le texte sera entièrement réaffiché (on ne dispose pas de scrolling horizontal) mais il faut tout de même effectuer l'action verticale (position verticale du curseur), c'est pourquoi la fonction est alors appelée avec une position curseur (relative au rectangle) égale à -1. Wimel 24 A priori, on peut éditer le même fichier dans plusieurs fenêtres différentes, le point pouvant être à une position différente dans chaque fenêtre. Il faut alors éventuellement répercuter les actions dans le texte lié à la "fenêtre courante" dans les autres fenêtres dites secondaires. Dans son état actuel, le module confond position du curseur (matérialisation du point d'édition dans le texte pointé lié à la fenêtre) et position d'action (où sont insérés (etc...) les caractères, en fait le point d'édition dans le texte pointé courant). Cela n'est pas gênant avec une seule fenêtre puisqu'ils sont confondus. 3. Hiérarchies de fenêtres (non implémenté) E. Textes affichés 1. La structure dtxt Un texte affiché est un texte avec les informations pour l'affichage dans la fenêtre courante et les autres; la structure dtxt dans le module Wdtxt l'implémente. La gestion du multifenêtrage n'est qu'esquissée, les points des autres fenêtres sont des marques banales dont on ne garde que l'identificateur. Le curseur est immobile dans les fenêtres secondaires. Les paramètres l1 et pb indiquent la première ligne affichée et la position de début d'affichage de la fenêtre courante. l1 est nécessaire car si WINdl1 permet de connaître le nombre de lignes avant le curseur, le trou peut entre temps avoir été déplacé (une fonction sur un texte affiché excécute d'abord la transformation du vecteur) et on a préféré inclure ce paramètre dans la structure dtxt plutôt que de transmettre son ancienne valeur en plus de la structure; ainsi DTmove appelle DTredisplay plus simplement. De plus, mettre l1 à -1 est un moyen facile d'indiquer que le texte n'est pas ou plus affiché. Le paramètre pb existe pour des raisons semblables quoiqu'on aurait pu, dans Wlines, connaissant l'(ancienne) base d'affichage de la ligne courante détecter la nécessité de scrolling et le transmettre au module Wdtxt (ce fut le cas dans une version précédente). 2. Les transformations Il a semblé intéressant de bien séparer les transformations: du texte (Wtxt), de la représentation (Wlines), de l'écran (Wwin); Wdtxt effectue la synthèse et se charge de transmettre les résultats calculés, ainsi la représentation sert à l'affichage. Celle de l'écran aurait du, dans l'idéal, être divisée entre effet horizontal ou vertical, mais le second, très simple ne justifiait pas un module supplémentaire. Une transformation sur un texte affiché est donc un triplet de trois transformations; notons qu'on ne peut parler de covariance, comme pour le couple vecteur-partition puisqu'on peut choisir le nombre de lignes affichées avant le point et le cadrage horizontal d'un texte pointé; la fonction DTcenter propose un cadrage optimal pour le nombre de lignes visualisées mais celui-ci est abandonné lors des déplacements. Chaque fonction génère donc elle-même le réaffichage du texte qu'elle a engendré, qui consiste souvent en des décalages plus ou moins locaux plus rapides qu'un affichage Wimel 25 complet. On n'a pas utilisé le procédé consistant à comparer le texte avant et après la transformation, à rechercher les changements, et à générer une suite optimale de transformations des caractères affichés: cela est impraticable en CAML et génère parfois des décalages certes rapides, mais sans aucun rapport avec l'opération effectuée. Les actions des fonctions mono-caractères sont coordonnées... par DTexe qui se charge du scrolling vertical. DT(line)move effectue un positionnement en un endroit quelconque du texte (de la ligne) puis appelle DTredisplay qui réaffiche le texte en effectuant un scrolling dans la direction du déplacement si le nouveau point n'est pas trop éloigné (c'est visuellement plus agréable!). F. Clavier 1. Le clavier virtuel Le clavier est le seul élément irréductiblement à effets de bords. X (le window manager) envoie les codes de touches clavier sur la fenêtre pointée par la souris; xterm dans la fenêtre duquel on exécute CAML et tape les caractères pour l'édition transforme ces codes simples en séquences vt100 plus ou moins longues (encore une bonne raison d'interfacer directement X) qu'il faut ensuite décoder afin que "^]]A" devienne "UP ARROW" et que l'appui sur 1 touche corresponde à 1 code lisible. Le module Wkey1 effectue ce décodage en utilisant un module Wlt de gestion d'arbres étiquetés ici par des chaînes représentant des codes ascii et dont les feuilles sont de type KEY1. Celui-ci désigne un nom de touche qualifié de ascii, control ou numarg (escape suivi d'une suite de chiffres). Les noms sont ceux du clavier de la console, ils sont définis dans le fichier Wsun_key_names pour un SUN. Une exception: "ESC ESC" qui correspond à deux appuis consécutifs ("ESC" n'est jamais retourné car préfixe des séquences de contrôle vt100). key1 est l'automate de décodage qui garde avec une référence son état entre 2 appels consécutifs (si la séquence est incomplète, il ne retourne rien, un appel ultérieur récupère le nom de touche associé à la séquence complète). Il se produit parfois une failure "Unknown suffix" quand l'un des codes de la séquence a été perdu. Elle doit actuellement être trappée dans la fonction appelante. 2. affectations de touches A partir de séquences de touches, il faut générer l'appel de la fonction CAML qu'on lui a affectée. Cela aurait été fait dans le module Wkey2, inachevé et non raccordé au reste du programme. On utilise encore un arbre étiqueté, cette fois par des KEY1 dont les feuilles sont comme des noms de fonction (en fait un nombre indiçant une table pour accélérer). Outre le décodage, il devait regrouper les appels consécutifs de fonctions répétables (tels les mouvements du point) et fournir un argument numérique pour accélérer le traitement. De plus il devait se charger de l'acquisition des arguments standards (nom de fichier, chaîne, expression rationnelle...) spécifiés par le type EF_ARG. Wimel 26 V. Extensions A. Types modifiables et mutables Dans tout ce chapitre, A, B, T, T1... désigne un shéma de type CAML; P,Q('a..'b) des shémas dont 'a..'b sont les seules variables libres. Cette partie fera ultérieurement l'objet d'un article autonome. 1. Les types modifiables, une vérification dynamique Les problèmes rencontrés avec les effets de bord sur les vecteurs sont ceux des structures modifiables (potentiellement, listes, records, ...). Toutes celles de Wimel (sauf les vecteurs) sont recopiées et la fatalité des "erreur pile pleine" incite à chercher une solution sûre et efficace. La restriction évoquée plus haut de non utilisation des structures (passées en argument à des transformations) altérées peut être vérifiée à l'exécution par le mécanisme suivant: type 'a value = Val of 'a | INVALID ;; type 'a modifiable = Modif of 'a value ref ;; Si T est un type connu à la compilation, let tr (Modif x0) = if !x0 = INVALID then failwith "Invalid modified value" else let (Val x) = !x0 in .... let x' = (* résultat de la transformation *) in x0:=INVALID; Modif (ref (Val x')) ;; Value tr = -: T modifiable -> T modifiable Cela n'a pas été utilisé à cause de la lourdeur, il serait néanmoins intéressant de carosser ainsi les fonctions exportées dans une bibliothèque. On peut d'ailleurs définir un opérateur prot{n}: (f:'a1 ..'an T -> T) -> (F:'a1..'an (T modifiable) -> (T modifiable)). Le type modifiable ainsi défini s'oppose au type mutable ((c) Formel...) qui considère des bases de données modifiables. Ici, le seul souci est la rapidité, si X'=f(X) et qu'il est probable que la valeur de X ne sera plus "consultée", on préfère que l'implémentation de X' soit obtenue par charcutage de celle de X, quitte à conserver des informations indiquant comment obtenir X à partir de X' (chaînes d'UNDO), plutôt que de reconstruire intégralement l'implémentation de X' à chaque fois. Cela dispense par ailleurs ces structures de garbage collection. Wimel 27 2. Les types mutables et le polymorphisme statique Le problème de typage des vecteurs polymorphes n'existe pas si ceux-ci sont des structures modifiables. Dans le cas des structures mutables, on peut résoudre le problème en bloquant le polymorphisme, introduisant un soupçon de second ordre. Une utilité immédiate est d'éviter l'utilisation de types factices: on peut représenter les entiers par une liste de longueur donnée d'un type fixé, mais il est plus élégant d'utiliser des listes de listes vides, de type '* list = (tout 'a) list. Il est d'ailleurs possible de générer, à partir des shémas de type CAML (quantification externe) et des '* (quantification "atomique") les types où le quantificateur apparait arbitrairement en définissant des shémas de type intermédiaires. D'autre part, si un objet d'espèce S est représentable par un objet de type P(T) où P est un type composé dans lequel T est quelconque mais fixé, on peut être amené à définir une fonction (correspondant à une fonction de l'espèce S) prenant en argument un objet de type P(T) mais en retournant un de type P(Q(T)). Ce problème apparait avec les entiers de Church (cf Huet, a uniform approach to type theory): type 'a endo == 'a -> 'a type 'a nat == 'a endo -> 'a endo let add m n = fun f -> m f o n f let mult m n = m o n let expon m n = m n Values add, mult : 'a nat -> 'a nat -> 'a nat Value expon : 'a endo nat -> 'a nat -> 'a nat La création d'un tableau d'entiers est impossible car celui-ci devra pouvoir contenir des objets de type 'a endo ... nat. Le problème est ici, une fois défini un ensemble de fonctions sur un type polymorphe (limité par exemple par un module), de trouver le type le moins défini possible stable par ces fonctions, ici NAT = '* nat. La synthèse de tels types semble difficile, ici il aurait suffit d'inférer si typ est le type cherché: si typ <= P('a) & typ <= P('a X) alors typ <= P('*) (le <= signifie ici "est moins défini que"). Ici, le polymorphisme était factice en ce que la restriction n'enlève rien à la représentation des entiers, et statique parce que, une fois défini (statiquement dans le bloc du module) un isomorphisme n:num -> N:Nat, on connait toutes les formes possibles du type, qui peut même être caché à l'utilisateur. On appellera ce procédé gel de l'indéterminée. 3. Les types mutables et le polymorphisme dynamique Si le type est modifiable au toplevel (dans le cas d'une référence polymorphe par exemple), celui-ci peut être précisé après des délais indéfinis. Une solution simple consiste à geler toutes ses indéterminées. Ainsi, la création d'une référence de type P('a) ref entraîne le typage P('*) ref. Dans Wimel, on voulait une fonction polymorphe permettant de créer un vecteur troué de type arbitraire: GVcreate nul n -> v : 'a -> num -> 'a gapvect retournant un vecteur troué vide de trou comblé avec nul. Pour empêcher la création d'un vecteur polymorphe, cette fonction étant accessible au toplevel, il aurait fallu la typer: GVcreate : 'a -> num -> *'a gapvect L'opérateur * indique au toplevel de geler le type de l'objet qu'il récupère après exécution Wimel 28 de la fonction. Si le type était dynamique, * n'aurait pas de raison d'être puisqu'il suffirait à la fonction elle-même de le faire. On a avec cette notation: ref : 'a -> *'a ref prefix := : 'a ref -> 'a -> 'a ref L'opérateur * qui indique le gel des indéterminées du type du résultat peut s'appliquer à tout type, dernier composant d'un type fonctionnel (le type xn d'un type composé x1 -> .. > xn), sous-terme arbitraire d'un type (il associe à l'exécution d'une fonction un calcul de type; il semble inutile d'en autoriser la présence ailleurs). Geler partiellement (seulement certaines variables d'un type composé) peut être utile (dans assoc_ref 'a 'b -> 'a & *'b ref; le gel de 'a est inutile). C'est pourquoi, pour obtenir une forme normale, on propage * vers l'intérieur, sur les variables: *P(*'a..*'b,'x) = P(*'a..*'b,*'x) et on ne gèle que s'il y a effectivement exécution: (Q('a..'b,'x)->P(*'a..*'b,'x)) App Q(A..B,X) = P(fr A..fr B, X) où fr est une fonction du synthétiseur de type qui transforme les indéterminées en '*. Enfin, * est transparent pour le filtrage (unif(*'a,typ) = typ ...) sinon que unif('a,*'b) = unif(*'b,'a) = *'b. B. Les attributs 1. Les attributs comme types CAML Le compilateur pourrait aussi vérifier statiquement la validité des structures. Pour cela, on va préciser la notion d'attribut. On entend par attribut un qualificatif portant sur un objet CAML et procurant un "typage" plus précis (que le typage standard CAML) et non obligatoirement permanent: l'attribut non permanent, d'une valeur affectée à une variable peut changer lorsqu'on lui applique certaines fonctions à effets de bord. Enfin, contrairement au type, on ne requiert pas de l'attribut qu'il "s'efface" lors de la compilation, son information est éventuellement disponible à l'exécution. L'attribut est donc une information facultative (le compilateur n'en n'a pas besoin pour générer son code) dont la seule utilité est de procurer des vérifications de cohérence du programme plus précises que celles fournies par le typage. Le typage apparait alors comme un attribut fondamental auquel s'ajoutent divers renseignements. Si l'attribut permanent a prend ses valeurs dans un ensemble A et s'applique aux objets x de type t pour t dans T, on note x:t:a. Si A est contenu dans l'ensemble des types de CAML l'introduction de a revient à considérer (x,ax):(t&a). Un exemple: on veut typer des listes avec leur longueur afin d'assurer la cohérence de fonctions telles que map. On accouple donc chaque liste de longueur n à (0 & .. & 0) (n+1 fois); on introduit Map de type ('n & 'a list -> 'n & 'a list) -> 'n & 'a list -> 'n & 'a list. La construction des listes s'effectue par Nil=([],0) et Cons: 'a -> 'n & 'a list -> (int & 'n) & 'a list Autre exemple: les attributs null, open, write, close indiquant comment une fonction accepte et modifie l'état d'un fichier fixé (la sortie du formateur print par exemple). Chaque fonction appelée au sein d'une procédure a l'un des 4 attributs nul (aucun effet), open (ajoute un niveau d'imbrication), close (diminue d'un niveau), write (ne change pas Wimel 29 le niveau, mais nécessite un niveau non nul); ce qui fournit une suite d'attributs (correspondant à la séquence des fonctions exécutées) à laquelle on applique les règles de réécriture suivantes: nul -> (vide) write write -> write write close -> close open close -> nul Ce qui aboutit à une forme normale open* | close* | write | nul On peut réaliser cela de manière tordue en CAML en typant chaque fonction de type A -> B avec un type fonctionnel produit (A * T1 -> A * T2) où T1 -> T2 indique la transformation de l'état du fichier: nul: 'a -> 'a open: 'a -> int & 'a close: 'a & 'b -> 'b write: int & 'a -> int & 'a La réécriture est obtenue pr la contraction des types Ti->T'i par la composition (T'i=Ti+1). On obtient donc pour la séquence d'appels de fonctions un type T0->Tn, T0 décrivant l'état minimal requisau début et Tn l'état final correspondant après exécution de la séquence. On peut écrire par exemple: let print_end_of_vector x = let x1 = write x "|]\c" in ... in let x2 = close x1 in x2 ;; value print_end_of_vector = -: int & 'a -> 'a L'attribut permet de repérer immédiatement et statiquement une mauvaise imbrication. Celui-ci ne peut être défini si la fonction est récursive... Les attributs non permanents peuvent eux aussi être modélisés en CAML: si f modifie l'attribut de x, on écrit à l'image de la convention précédente let (y,ax) = f (x,ax) in ... le changement de type étant réalisé par le changement de variable d'attribut ax. Dans le cas des valeurs modifiables, on peut écrire: type INVALID = Invalid ;; type VALID = Valid ;; let tr (X, a:VALID) = ... in ((X',Valid),(X,Invalid)) ;; value tr:T & VALID -> (T & VALID) & (T & INVALID) value x:T & VALID let (x',x) = tr (x) ;; value x':T & VALID value x:T & INVALID L'introduction des types (IN)VALID n'est rien d'autre que la traduction sur le plan des types (donc vérifiable statiquement) du carossage évoqué au paragraphe précédent. Cette vérification est facile si la variable est locale et qu'on n'utilise pas d'effets de bord. Ce codage des attributs est très lourd car il impose ce codage à toutes les fonctions composables (plus précisément, de la fermeture transitive de la relation de composabilité) avec les fonctions pour lesquelles il est utilisé. De plus, il introduit une multitude de variables inutiles pour l'exécution. On voudrait donc que cette information reste optionnelle, la cohérence n'étant vérifiée que lorsqu'elle est présente. Cela est possible soit parce que l'information reste disponible à l'exécution (si, avec le type Liste, on veut renoncer au test pour pouvoir utiliser les fonctions prédéfinies), soit parce qu'elle n'intervient ni dans la génération du code ni dans les inférences ultérieures (fonction close_all pour les attributs de fichiers). Enfin, il faut pouvoir définir un attribut par défaut (par exemple, pour les fichiers, nul). Wimel 30 2. Une syntaxe simple pour les attributs On veut que l'objet associé à l'attribut (sans intérêt à l'exécution) ne fasse pas générer de code et qu'il n'apparaisse pas en tant qu'objet CAML à passer comme argument. On introduit donc la notation x:T:id1(T1):..:idn(Tn); idi nommant les attributs de type Ti, ainsi, [|1;2|]:num list:Length(int&int&int). Les attributs peuvent être des schémas de type et leur absence correspond à '_ ou à une valeur par défaut à définir. Une fois définies les primitives, il est facile de synthétiser les attributs par l'algorithme de filtrage habituel. On doit définir les attributs en utilisant la syntaxe: attribute Length of 'a; let Cons x (l:Length(n)) = (x::l):Length(0,n); value Cons = - : 'a -> ('a list:Length('b)) -> ('a list:Length(int&'b)) La syntaxe prolonge celle de CAML. Les attributs peuvent être placés aux mêmes endroits que les types. Lorsque les deux sont présents, deux points de vue sont permis et synonymes: (v : T) : A ou v : (T ; A) Les parenthèses peuvent être omises car les expressions suivent une grammaire de précédence avec l'ordre suivant: : introducteur de type ou d'attribut en général pour une variable de paramètre formel (associatif à gauche) -> constructeur de types fonctionnels ; adjonction de type ou d'attributs (associatif et commutatif pour les attributs) = dans un type, comme : mais plutôt pour une variable d'attribut (évite le recours systématique aux parenthèses) | somme de types & ou * produit de types :: etc constructeurs de patterns (cf manuel CAML) Cette syntaxe présente en outre l'avantage d'utiliser pour les types le même ordre de précédences que pour les expressions. La construction d'un objet impose un choix. Soit l'attribut de l'objet créé est indéfini, ce qui n'est en fait pas gênant puisque sa raison d'être était simplement la vérification de la cohérence des fonctions conservant la longueur composées entre elles. Soit on définit des méta-fonctions exécutables uniquement à la compilation, non typées ou plutôt opérant sur des constantes typées dynamiquement; on aurait ainsi un opérateur make_List analogue au constructeur [| ... |] pour les listes classiques qui retournerait le type et l'attribut. On pourrait même envisager de rendre ce niveau accessible à l'utilisateur: #pragma let Length_List = fun [] -> []:0 | a::l -> (0 & Length_List l);; value Length_List = -: pragma 'a list -> ? #pragma let make_List l = l:Length(Length_List l);; value Length_List = -: pragma 'a list -> 'a list:Length(?) make_List [| 1 ; 2 ; 3 |] ;; value [| 1 ; 2 ; 3 |] : num list : Length(3) (NB: la sémantique de #pragma a peut-être été étendue de façon incohérente). Si f est une fonction f: (x,y) -> (f1 x, f2 y), on peut être amené, pour simplifier l'écriture ou parce que les valeurs ne sont pas accessibles, à manipuler la seconde composante Wimel 31 implicitement en la conservant dans une variable modifiable; ces variables seront dites externes. Le type de f2 devient alors un attribut de f dit à composition car si f:T1:a->b1 et g:T2:b2->c alors b1=b2 et (g o f)() autant que f();g(); seront typées T2oT1:a->c. L'attribut n'intervenant que lors d'une exécution effective, il faut geler celui-ci tant que l'expression n'est pas "complète", la fonction se typant alors: T1 -> .. -> Tn -> T0 : void -> .. -> void -> Ta -> Ta'. la règle de composition n'intervient que pour des attributs de la forme Ta -> Ta'. Dans le cas le plus général, on écrit: (Vi:Pi('ai)) -> (V'j:P'j('aj)) signifiant que lors de l'exécution, les types des variables Vi fournissent les valeurs des 'ai qui fournissent en sortie ceux des variables V'j. L'attribut d'une procédure est calculé en attribuant un type indéfini aux Vi au début de la séquence (des fonctions exécutées dans la procédure), on a pour chaque Vi et au k-ième appel de fonction (Vi:Pi,k('ai)) -> (V'j:P'j,k('ai)) et en composant comme ci-dessus, on obtient pour la séquence l'attribut (Vi:Pi,1('ai)) -> (V'j:P'j,n('aj)), qui est l'attribut de la fonction. Noter enfin que les variables Vi n'existent pas obligatoirement, la cohérence de la composition étant seule recherchée ici et que ces identificateurs de variables (factices ou réelles) dispense d'utiliser un constructeur pour nommer l'attribut. La valeur par défaut est 'a -> 'a. Un exemple, le typage de print où l'état du printer est symbolisé par le contenu de la variable factice prt: nul: 'x ; prt='a -> 'y ; prt='a (synonyme de nul: 'x->'y : (prt:'a) -> (prt:'a)) open: void ; prt = 'a -> void ; prt = int & 'a close: void ; prt = 'a & 'b -> void ; prt='b write_num: num -> void : prt = int & 'a -> prt = int & 'a) et on peut écrire par exemple: let print_end_of_vector = write "|]\c"; close() ;; value print_end_of_vector = -: void ; prt = 'a & 'b -> void ; prt = 'b On a par ailleurs: close_all : void -> void ; prt='a -> prt=int open_n_boxes : int -> void : prt='a -> prt='b on pourrait envisager une abréviation ...T : (prt : 'a -> int & 'a) mais non se dispenser d'écrire void lorsqu'il y a un attribut car il serait alors impossible de déterminer lorsqu'on écrit V:T si V est un paramètre formel ou une variable d'attribut. Réserver le signe : aux premières et : aux secondes le permettrait mais cette distinction n'est pas toujours souhaitable (cf fonction incr_v au paragraphe suivant). Ce modèle convient bien pour print (encore qu'on pourrait coder de façon plus efficace le type par un entier) car ici, le type et la valeur de la variable externe n'interviennent pas; cela est heureux car une telle variable ne pourrait être typée. Cet attribut se rapproche des types en ce qu'il disparait à la compilation et des attributs par son caractère optionnel. 3. Les informations optionnelles On voit aussi que les attributs seraient un moyen commode de coder les informations optionnelles telles: volatile indique qu'une valeur est modifiable par des éléments extérieurs au Wimel 32 programmme et que le compilateur ne peut effectuer certaines optimisations comme la considérer comme invariant de boucle et ne la lire qu'à l'entrée de celleci. exemple: time : num ; volatile pseudo-variable contenant le temps printable indique que la donnée contient l'information qui permet de l'imprimer par la fonction surchargée print. exemple: print: 'a ; printable -> void dynamic indique que la donnée contient son type. Noter que dans un objet de type paramétrable, il se peut que seuls certains paramètres soient dynamiques. exemple: input : void -> 'a ; dynamic ;; type 'a 'b pair = Pair of 'a & 'b ;; let input_pair_num () = Pair(input (), input_num ()) ;; value input_pair_num= -: void -> (('a;dynamic) & num) pair let input_string () = let (x,n) = input_pair_num () in match_type x with string -> x | num -> string_of_num_in_radix (x,radix) | _ -> failwith "incorrect input data type" ;; l'attribut dynamic généralise printable si chaque type est imprimable et de manière unique. L'implémentation en est plus lourde. reref indique qu'une valeur peut encore être associée à une adresse; c'est le cas de tout contenu d'une variable venant d'être lu. On peut alors définir un opérateur addr qui retourne une référence sur ce contenu: exemples: attribute Reref = reref | not_reref let a = 2 and x = ref a ;; values a:num:reref = 2, x:num ref:reref = ref 2 2 ;; value 2:num let y = addr b and test = (x=y) ;; value y:num ref:reref = ref 2 value true:boolean let z = addr 2 ;; Attribute checking error 2 has not reref attribute (ie has attribute reref=none) which should match attribute Reref=reref in addr cet opérateur est typé addr : 'a ; reref -> 'a ref Ce point de vue un peu tordu est à mon avis le meilleur car il permet d'éviter le dilemme suivant: Wimel 33 - manipulation des références et donc obligation de déréférencer à chaque appel de fonction - manipulation des valeurs et donc obligation de conserver l'adresse dans une variable séparée Le traitement des fonctions prédéfinies pose problème: id : 'a -> 'a 'a doit-il contenir l'état de tous les attributs ? La question se pose pour les combinateurs purs. Chaque attribut doit avoir une valeur undefined qui invite le compilateur à présumer le pire. On les typages suivants: vect_item : ('a vect ; reref) & num -> 'a ; reref cdr_modifiable : 'a list ; reref -> 'a list ; reref Il faudrait de plus pouvoir définir des mécanismes d'hérédité: lors de la définition d'un type composé, spécifier les composants qui ont l'attribut reref (cf le cas des listes modifiables ci-dessus). 4. Les types mixtes Si on veut manipuler la valeur d'une variable transformée par un ensemble de fonctions, il faut introduire des types "mixtes" comportant des vrais noms de variables. Si cnt est une variable globale modifiable non typée (ou plutôt typée dynamiquement, contenant son type, qui évolue au cours du temps) contenant un arbre dont le symbole de noeud unique est d'arité 1, et dont la profondeur représente un entier: incr_cnt : void ; cnt='a list -> void ; cnt='a list list incrémente cette variable. On peut étendre cette possibilité aux variables locales en écrivant par exemple (incr_v prend en argument une variable et l'incrémente): incr_v : V = 'a list -> void ; V = 'a list list NB : V peut être un pattern quelconque... On peut formaliser les types modifiables de la manière suivante: attribute modifiability = not_modifiable | modifiable | invalid ; (définit 3 types utilisables seulement pour cet attribut ce qui dispense d'utiliser un constructeur et 3 types factices). Si tr est une fonction qui transforme un objet de type modifiable T (et rend donc l'implémentation de son ancienne valeur inaccessible) et retourne un objet de même type encore modifiable: tr: (X:T:modifiable) -> (T;modifiable) ; X=invalid prefix := : (T;modifiable) ref -> void not_modifiable sera la valeur par défaut (on peut convenir avec cette syntaxe que la première alternative soit la valeur par défaut; si c'est ?, la valeur par défaut est indéfinie) de l'attribut modifiability. Lors de la définition d'une structure modifiable, il faut définir les fonctions d'extraction de sous-structure ou de synthèse de façon à propager l'attribut correctement. Une autre utilisation des attributs, particulièrement utile serait de typer les indices (ou positions dans le cas général) comme indices (et non comme entiers) sur un vecteur donné: Wimel vect_item : vect_assign index_add : index_sub : 34 (V:'a vect) & V index -> 'a : (V:'a vect) & V index & 'a -> void num & ''V index -> ''V index ''V index & ''V index -> num Cela procure une vérification de cohérence beaucoup plus précise mais nécessite la surcharge afin d'alléger l'écriture et l'introduction de sortes (au moins TYPE et OBJECT); des conventions lexicales (ici, double quote pour les objets mais qui ne peut en fait s'appliquer qu'ux vecteurs) permettant d'en synthétiser le type très facilement. La gestion des sous-objets (quel est le type d'un indice sur un vecteur qui n'est qu'un composant de la valeur passée en argument ?) et de la récursivité (si le vecteur sert à stocker une structure, et contient des indices sur lui-même) imposent de pouvoir définir le type ''* index (=num). Le second ordre (?) peut donc apparaître dans des problèmes très concrets. Ce point de vue permet aussi de traiter de façon homogène les tableaux par un polymorphisme sur les dimensions: map : 'a 'dim vect modifiable -> ('a->'a) -> 'a 'dim vect serait une des primitives de scrutation d'un tableau de dimensions quelconques. On aurait alors (et bien que la dimension soit disponible à l'exécution): add : ('d1,'d2) vect & ('d1,'d2) vect -> ('d1,'d2) vect mult: ('d1,'d2) vect & ('d2,'d3) vect -> ('d1,'d3) vect En outre, connaître statiquement l'égalité de certaines dimensions permet d'optimiser le code en conséquence (pour remettre 2 vecteurs de même longueur à 0, une seule boucle suffit). Enfin, on peut typer les listes de longueur fixée par: attribute Length of #int signifiant ainsi que la longueur est comme les types, un objet de valeur connue à la compilation. Le symbole # est d'ailleurs superflu. On peut alors englober les attributs de ADA (si l:'a list: Length(n) alors l'Length = n). Il n'est pas nécessaire d'écrire List'Length(l) pour préciser le type. Length est polymorphe pour le type des éléments et est surchargé pour les types composés (listes, chaînes...). Mais cette surcharge "statique", cad lorsque le type est connu non-polymorphe à la compilation, ne pose pas de problème de choix de la fonction car on sait à la compilation que l est une liste. On a enfin: Cons : 'a -> ('a list:Length(n)) -> ('a list:Length(succ n)) Il est important de noter qu'on n'introduit pas l'arithmétique dans le typage, il s'agit simplement de coder efficacement un arbre, ici en utilisant l'isomorphisme entre l'algèbre libre engendrée par un symbole d'arité 1 et les entiers. Le type List aurait pu être défini: type 'a list = []:Length(0) | (Cons of 'a & 'a list;Length(n)):Length(succ n); Cette partie sera développée dans un article autonome. C. Interfaçage avec un éditeur externe 1. Remarques préliminaires Actuellement, Wimel n'accède au monde extérieur que pour la lecture du clavier et l'écriture d'écran. Il faudra y ajouter les manipulations de fichiers UNIX. L'écriture écran peut être considérée comme une simple transformation de la stucture WIN_REG car on n'utilise jamais les informations extérieures. Il ne peut plus en être de même si on interface à un niveau plus élevé, à moins de dupliquer toutes les structures. Wimel 35 Il faut donc, pour ne pas transmettre des structures de taille importante, ne garder que des références (qui peuvent être des pointeurs ou des numéros dans une table) manipulées soit explicitement comme références, soit comme valeur à évaluation paresseuse. Il resterait à définir la sémantique des types modifiables externes, tous les types tant en Pascal qu'en C étant modifiables. Les types non modifiables peuvent être utilisés pour des données non modifiables par l'affectation standard ou dont la modification nécessite des actions complémentaires. 2. Formalisation de l'extérieur On décrit ci-dessous une (petite) extension du langage permettant de rédiger de manière simple des programmes utilisant des structures extérieures. Une fois défini en CAML le type externe T, T teleref type les références externes. L'idéal serait de conserver le style du programme actuel pour les transformations avec des valeurs fictives: l'appel d'une procédure externe ne transmet que les informations pour reconstituer la structure. L'attribut tele, héréditaire pour les sous-champs fait générer une évaluation paresseuse. Le code peut être très optimisé si on détecte tele à la compilation et si on connait pour chaque procédure externe les éléments modifiés de la structure. Il est possible de lire la valeur d'une donnée de type 'a;tele composantes. On introduit un opérateur de déréférence externe: ou celle de ses !! : 'a teleref -> 'a ; tele et un opérateur d'affectation externe: prefix ::= : 'a teleref & 'a -> 'a tele On peut donc lire et modifier le contenu d'un 'a teleref comme s'il s'agissait d'une variable CAML. Le langage se charge du protocole de communication (y compris pour les éventuelles sous-structures) et de la transcription des données. Cette méthode a pour seul inconvénient (?) que la facilité de rédaction fait oublier la lenteur relative des communications entre processus. Il serait d'autre part nécessaire d'introduire l'attribut volatile si la donnée est susceptible de modification par l'extérieur afin d'éviter les optimisations éventuelles du compilateur basées sur le non recalcul des valeurs apparemment non modifiées. La gestion des échanges avec l'extérieur est un cas particulier de parallélisme. L' "externement" des structures rend insuffisants les itérateurs et combinateurs. On peut certes réaliser un compilateur qui se charge de compiler les fonctions CAML dans le langage qui gère les structures extérieures, l'itérateur étant alors lui-même extérieur. L'autre solution, plus réalisable, consiste à introduire des itérateurs interruptibles, le commencement d'une itération devenant similaire à l'ouverture d'un fichier sinon que les ouvertures et fermetures doivent être imbriquées et chaque pas d'itération étant effectué par un appel externe explicite. On se rapprocherait alors des générateurs du langage ICON. 3. Esquisse d'une bibliothèque Si on souhaite juste interfacer un éditeur, il n'est pas nécessaire de pouvoir accéder à toutes les structures. (Partie non rédigée) Wimel 36 VI. Conclusion La formalisation d'un éditeur en CAML s'est avérée plus fertile que prévu, par l'usage de la fonctionnalité, des patterns et des exceptions. La notation "applicative" entraîne un style probablement inédit pour un éditeur. Le programme est exécutable et pourrait être utilisable si le langage était muni de types modifiables. Enfin, il contribue, par les problèmes rencontrés, à préciser certains concepts de programmation tels les types modifiables, mutables, les structures, le second ordre et des possibilités d'extension future de CAML. Il apporte sur le typage un point de vue orienté programmation apparemment peu étudié en informatique pure. I. Introduction 1 II. Présentation A. Conventions typographiques dans le rapport B. Utilisation C. Fichiers D. Conventions 1. Dénomination des identificateurs 2. Annotation des sources E. Style de programmation 1. Généralités 2. Typage des structures 3. Itérateurs et combinateurs 4. Exceptions 2 2 2 3 4 4 5 5 5 6 6 7 III. Les structures vectorielles de Wimel A. Vecteurs CAML 1. Primitives 2. Restrictions 3. Vocabulaire B. Vecteurs linéaires 1. Définitions 2. Opérations C. Vecteurs troués 1. Motivation 2. Sémantique des dessins... 3. Vocabulaire 4. Opérations sur les vecteurs troués 5. Fonctions de base de la structure D. Partitions 1. Définitions 2. Implémentation 3. Transformations d'un vecteur partitionné E. Marques et blocs 1. Définitions 2. Blocs 3. Implémentation 9 9 9 9 10 10 10 10 11 11 11 12 12 13 13 13 14 15 15 15 16 16 IV. Les structures "textuelles" de Wimel A. Caractères 1. Vocabulaire 2. Implémentation B. Textes 1. Le vecteur partitionné 2. Les marques 3. La structure text C. Représentation visuelle 1. La structure "mur de pierres" 2. Implémentation D. Fenêtres 1. Le terminal (écran) virtuel 2. La structure "rectangle d'affichage" 3. Hiérarchies de fenêtres E. Textes affichés 1. La structure dtxt 2. Les transformations F. Clavier 1. Le clavier virtuel 2. affectations de touches 17 17 17 17 17 17 18 18 18 18 19 19 19 19 20 20 20 21 21 21 22 V. Extensions A. Types modifiables et mutables 1. Les types modifiables, une vérification dynamique 2. Les types mutables et le polymorphisme statique 3. Les types mutables et le polymorphisme dynamique B. Les attributs 1. Les attributs comme types CAML 2. Une syntaxe simple pour les attributs 3. Les informations optionnelles 4. Les types mixtes C. Interfaçage avec un éditeur externe 1. Remarques préliminaires 2. Formalisation de l'extérieur 3. Esquisse d'une bibliothèque 23 23 23 23 24 25 25 26 28 30 31 31 31 32 VI. Conclusion 33