Un éditeur en CAML

publicité
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
Téléchargement