L'idée de départ est la suivante : si un automate fini reconnaît le langage correspondant à l'expression régulière r, alors à toute lettre d'un mot reconnu on peut faire correspondre une occurrence d'une lettre apparaissant dans r. Pour distinguer les différentes occurrences d'une même lettre dans r, on va les indexer par des entiers. Considérons par exemple l'expression régulière (a|b)* a (a|b), qui définit les mots sur l'alphabet {a,b} dont l'avant-dernière lettre est un a. Si on indice les caractères, on obtient
(a1|b1)* a2 (a3|b2)
a1a1b1a1a2b2
L'idée est alors de construire un automate dont les états sont des ensembles de lettres indexées, correspondant aux occurrences qui peuvent être lues à un instant. Ainsi l'état initial contient les premières lettres possibles des mots reconnus. Dans notre exemple, il s'agit de a1,b1,a2. Pour construire les transitions, il suffit de calculer, pour chaque occurrence d'une lettre, l'ensemble des occurrences qui peuvent la suivre. Dans notre exemple, si on vient de lire a1, alors les caractères possibles suivants sont a1,b1,a2 ; si on vient de lire a2, alors ce sont a3,b2.
type ichar = char * int type regexp = | Epsilon | Character of ichar | Union of regexp * regexp | Concat of regexp * regexp | Star of regexpÉcrire une fonction
val null : regexp -> boolqui détermine si epsilon (le mot vide) appartient au langage reconnu par une expression régulière.
Tester avec
let () = let a = Character ('a', 0) in assert (not (null a)); assert (null (Star a)); assert (null (Concat (Epsilon, Star Epsilon))); assert (null (Union (Epsilon, a))); assert (not (null (Concat (a, Star a))))
module Cset = Set.Make(struct type t = ichar let compare = Stdlib.compare end)
Écrire une fonction
val first : regexp -> Cset.tqui calcule l'ensemble des premières lettres des mots reconnus par une expression régulière. (Il faut se servir de null.)
De même, écrire une fonction
val last : regexp -> Cset.tqui calcule l'ensemble des dernières lettres des mots reconnus.
Tester avec
let () = let ca = ('a', 0) and cb = ('b', 0) in let a = Character ca and b = Character cb in let ab = Concat (a, b) in let eq = Cset.equal in assert (eq (first a) (Cset.singleton ca)); assert (eq (first ab) (Cset.singleton ca)); assert (eq (first (Star ab)) (Cset.singleton ca)); assert (eq (last b) (Cset.singleton cb)); assert (eq (last ab) (Cset.singleton cb)); assert (Cset.cardinal (first (Union (a, b))) = 2); assert (Cset.cardinal (first (Concat (Star a, b))) = 2); assert (Cset.cardinal (last (Concat (a, Star b))) = 2)
val follow : ichar -> regexp -> Cset.tqui calcule l'ensemble des lettres qui peuvent suivre une lettre donnée dans l'ensemble des mots reconnus.
On notera que la lettre d appartient à l'ensemble follow c r si et seulement si
Tester avec
let () = let ca = ('a', 0) and cb = ('b', 0) in let a = Character ca and b = Character cb in let ab = Concat (a, b) in assert (Cset.equal (follow ca ab) (Cset.singleton cb)); assert (Cset.is_empty (follow cb ab)); let r = Star (Union (a, b)) in assert (Cset.cardinal (follow ca r) = 2); assert (Cset.cardinal (follow cb r) = 2); let r2 = Star (Concat (a, Star b)) in assert (Cset.cardinal (follow cb r2) = 2); let r3 = Concat (Star a, b) in assert (Cset.cardinal (follow ca r3) = 2)
Écrire une fonction
val next_state : regexp -> Cset.t -> char -> Cset.tqui calcule l'état résultant d'une transition.
Pour représenter l'automate fini, on se donne le type autom suivant :
type state = Cset.t (* un état est un ensemble de caractères *) module Cmap = Map.Make(Char) (* dictionnaire dont les clés sont des caractères *) module Smap = Map.Make(Cset) (* dictionnaire dont les clés sont des états *) type autom = { start : state; trans : state Cmap.t Smap.t (* dictionnaire état -> (dictionnaire caractère -> état) *) }On peut choisir de représenter ainsi le caractère # :
let eof = ('#', -1)Écrire une fonction
val make_dfa : regexp -> automqui construit l'automate correspondant à une expression régulière. L'idée est de construire les états par nécessité, en partant de l'état initial. On pourra par exemple adopter l'approche suivante :
let make_dfa r = let r = Concat (r, Character eof) in (* transitions en cours de construction *) let trans = ref Smap.empty in let rec transitions q = (* la fonction transitions construit toutes les transitions de l'état q, si c'est la première fois que q est visité *) ... in let q0 = first r in transitions q0; { start = q0; trans = !trans }
Note : il est bien entendu possible de construire au final un automate dont les états ne sont pas des ensembles mais, par exemple, des entiers, pour plus d'efficacité dans l'exécution de l'automate. Cela pourrait d'ailleurs être fait pendant la construction, ou a posteriori. Mais ce n'est pas ce qui nous intéresse ici.
let fprint_state fmt q = Cset.iter (fun (c,i) -> if c = '#' then Format.fprintf fmt "# " else Format.fprintf fmt "%c%i " c i) q let fprint_transition fmt q c q' = Format.fprintf fmt "\"%a\" -> \"%a\" [label=\"%c\"];@\n" fprint_state q fprint_state q' c let fprint_autom fmt a = Format.fprintf fmt "digraph A {@\n"; Format.fprintf fmt " @[\"%a\" [ shape = \"rect\"];@\n" fprint_state a.start; Smap.iter (fun q t -> Cmap.iter (fun c q' -> fprint_transition fmt q c q') t) a.trans; Format.fprintf fmt "@]@\n}@." let save_autom file a = let ch = open_out file in Format.fprintf (Format.formatter_of_out_channel ch) "%a" fprint_autom a; close_out chPour tester, on reprend l'exemple ci-dessus :
(* (a|b)*a(a|b) *) let r = Concat (Star (Union (Character ('a', 1), Character ('b', 1))), Concat (Character ('a', 2), Union (Character ('a', 3), Character ('b', 2)))) let a = make_dfa r let () = save_autom "autom.dot" a
L'exécution produit un fichier autom.dot qui peut être ensuite visualisé avec la commande Unix
dotty autom.dotou encore avec l'une des deux commandes suivantes :
dot -Tps autom.dot | gv - dot -Tpdf autom.dot > autom.pdf && evince autom.pdfOn doit obtenir quelque chose comme la figure de droite.
val recognize : autom -> string -> boolqui détermine si un mot est reconnu par un automate.
Voici quelques tests positifs :
let () = assert (recognize a "aa") let () = assert (recognize a "ab") let () = assert (recognize a "abababaab") let () = assert (recognize a "babababab") let () = assert (recognize a (String.make 1000 'b' ^ "ab"))et quelques tests négatifs :
let () = assert (not (recognize a "")) let () = assert (not (recognize a "a")) let () = assert (not (recognize a "b")) let () = assert (not (recognize a "ba")) let () = assert (not (recognize a "aba")) let () = assert (not (recognize a "abababaaba"))
Voici un autre test avec une expression régulière caractérisant un nombre pair de b :
let r = Star (Union (Star (Character ('a', 1)), Concat (Character ('b', 1), Concat (Star (Character ('a',2)), Character ('b', 2))))) let a = make_dfa r let () = save_autom "autom2.dot" aQuelques tests positifs :
let () = assert (recognize a "") let () = assert (recognize a "bb") let () = assert (recognize a "aaa") let () = assert (recognize a "aaabbaaababaaa") let () = assert (recognize a "bbbbbbbbbbbbbb") let () = assert (recognize a "bbbbabbbbabbbabbb")et quelques tests négatifs :
let () = assert (not (recognize a "b")) let () = assert (not (recognize a "ba")) let () = assert (not (recognize a "ab")) let () = assert (not (recognize a "aaabbaaaaabaaa")) let () = assert (not (recognize a "bbbbbbbbbbbbb")) let () = assert (not (recognize a "bbbbabbbbabbbabbbb"))
Plus précisément, on va produire un code de la forme suivante :
type buffer = { text: string; mutable current: int; mutable last: int } let next_char b = if b.current = String.length b.text then raise End_of_file; let c = b.text.[b.current] in b.current <- b.current + 1; c let rec state1 b = ... and state2 b = ... and state3 b = ...Le type buffer contient la chaîne à analyser (champ text), la position du prochain caractère à examiner (champ current) et la position suivant le dernier lexème reconnu (champ last). La fonction next_char renvoie le prochain caractère à analyser et incrémente le champ current. Si la fin de la chaîne est atteinte, elle lève l'exception End_of_file.
À chaque état de l'automate correspond une fonction statei avec un argument b de type buffer. Cette fonction réalise le travail suivant :
Écrire une fonction
val generate: string -> autom -> unitqui prend en arguments le nom d'un fichier et un automate et produit dans ce fichier le code OCaml correspondant à cet automate, selon la forme ci-dessus. Indications :
let start = state42correspondant à l'état initial de l'automate.
Pour tester, écrire (dans un autre fichier lexer.ml, cette fois écrit à la main) un programme qui découpe une chaîne de caractères en lexèmes en utilisant le code produit automatiquement (en fixant le nom du fichier, par exemple a.ml). Le principe est une boucle qui effectue les actions suivantes :
On pourra tester par exemple avec l'expression régulière a*b, en exécutant
let r3 = Concat (Star (Character ('a', 1)), Character ('b', 1)) let a = make_dfa r3 let () = generate "a.ml" apuis en liant le code produit avec le fichier lexer.ml :
% ocamlopt a.ml lexer.mlSur la chaîne abbaaab, l'analyse doit produire trois lexèmes et réussir :
--> "ab" --> "b" --> "aaab"Sur la chaîne aba, l'analyse doit produire un premier lexème puis échouer :
--> "ab" exception End_of_fileEnfin, sur la chaîne aac, l'analyse doit échouer sur une erreur lexicale :
exception Failure("lexical error")On pourra aussi tester avec l'expression régulière (b|espilon)(ab)*(a|epsilon) des mots alternant les lettres a et b. Sur la chaîne abbac, on doit obtenir trois lexèmes :
--> "ab" --> "ba" --> "" would now loopLe dernier lexème étant la chaîne vide, le programme s'arrête en signifiant qu'on obtiendrait maintenant indéfiniment ce lexème vide.