Cours 5

Htmligure
Version & licenses
Creative Commons License

Représentation fonctionnelles des parseurs

Guyslain
Thursday, 21 July 2016

Retour au sommaire.

La plupart des données structurées sont décrites par des grammaires hors-contextes, en particulier les langages de programmation, mais aussi des formats comme XML ou json. Cela parait donc une bonne idée de vouloir écrire les fonctions de lectures sous la forme de grammaire aussi. Nous avons déterminé que ce que nous voulons est à l'image de cet exemple :

  1. type 'elt bin_tree =
  2. | Leaf
  3. | Node of 'elt bin_tree * 'elt * 'elt bin_tree

  4. let make_node (left,root,right) = Node (left,root,right)
  5. let make_leaf () = Leaf

  6. let bin_tree_to_string (elt: 'elt Read.t) : 'elt bin_tree Read.t =
  7. let open Read in
  8. fix @@ fun tree ->
  9. (make_node *> cst "Node " ++ triple tree elt tree)
  10. || (make_leaf *> cst "Leaf")

Il nous reste donc à définir le type Read.t, puis les différents combinateurs permettant de la manipuler. Read.tdoit représenter les fonctions de lectures, qui fournissent une valeur à partir d'une chaîne de caractères. On peut donc poser comme définition :

  1. type 'result t = string -> 'result

Il y a cependant plusieurs problèmes avec cette définition :

  • la lecture peut échouer, si le format de la chaîne de caractères n'est pas correct. La lecture ne devrait donc pas toujours retourner une valeur.
  • si la grammaire est ambigüe, il peut y avoir plusieurs façons de lire la chaîne de caractères, et donc plusieurs réponses possibles. On devrait donc retourner une liste de résultats, ce qui permettrait aussi de gérer la possibilité de ne pas avoir de résultat du tout.
  • ce type n'est pas combinable, parce que les lecteurs consomment toute la chaîne de caractères. On souhaite pouvoir enchaîner plusieurs lecteurs successivement qui chacun liront une partie de la chaîne de caractères. Pour cela, le résultat d'une lecture doit être composée de la valeur construite, et de ce qu'il reste à lire dans la chaîne de caractères.
  • en se restreignant aux chaînes de caractères, on perd en application. Par exemple pour lire un fichier il faudra d'abord extraire le fichier comme une chaîne de caractères, ce qui n'est pas souhaitable pour les grands fichiers. Si on lit dans la sortie d'un pipe, cela oblige à attendre la fin du pipe pour commencer à lire.
  • en cas d'erreur, il serait bon de pouvoir indiquer où l'erreur se produit, pourquoi, et même idéalement de pouvoir continuer la lecture.
  • pour des raisons d'efficacité, on voudrait aussi ajouter des structures de données efficaces pour encoder la fonction de lecture.

On ne va pas s'attaquer à tous ces problèmes, on va se contenter d'un petit prototype simple. D'abord on va abstraire les chaînes de caractères avec ce module, qui donne une interface itérable pour les séquences de caractères :

  1. module TextInput : sig

  2. module Position : sig
  3. type t =
  4. { origin : string;
  5. column : int;
  6. line : int;
  7. }

  8. val compare : t -> t -> int
  9. val min : t -> t -> t
  10. val max : t -> t -> t
  11. end


  12. module TextInterval : sig
  13. type t = Position.t * Position.t

  14. val of_position : Position.t -> t
  15. val merge : t -> t -> t
  16. end


  17. type t

  18. val move_forward : t -> t option
  19. val get_current_position : t -> Position.t
  20. val get_current_char : t -> (char * Position.t) option
  21. val observe : t -> ((char * Position.t) * t) option

  22. val of_string : ?offset:int -> ?name:string -> string -> t
  23. val of_list : ?name:string -> char list -> t
  24. val of_file : filename:string -> t
  25. val of_in_channel : ?name:string -> in_channel -> t
  26. end

Le module Position permet de représenter en quelle position d'une séquence se trouve un caractère donné, en terme de numéros de ligne et de colonne. Le champ origin permet de retenir l'origine du caractère, par exemple un nom de fichier.

Le module TextInterval permet d'associer à chaque valeur lue la position de la sous-séquence de caractères ayant été lus pour la produire.

Deuxièmement, chaque résultat contiendra à la fois la valeur construite, l'intervalle des caractères lus pour construire la valeur, et le reste de la séquence à lire.

Finalement, la lecture d'une valeur peut échouer, dans ce cas on veut retourner la position d'échec et un message d'erreur. Idéalement, on veut aussi savoir la liste des règles de dérivation utilisée pour aboutir à cette erreur. On ne traitera pas les ambiguïtés (en cas d'ambiguïté de la grammaire, la première dérivation possible sera choisie). Cela nous donne le type suivant :

  1. type 'value output =
  2. { value : 'value;
  3. interval : TextInput.TextInterval.t option;
  4. input : TextInput.t
  5. }

  6. type error =
  7. { stack : (string * TextInput.Position.t) list;
  8. position : TextInput.Position.t
  9. }

  10. type 'value result =
  11. | Output of 'value output
  12. | Error of error

  13. type 'value reader = TextInput.t -> 'value result option

mais on va, lors de la conception, considérer le type plus simple, qui concentre toute la partie calculatoire (pas de gestion des erreurs, des positions) :

  1. type 'value reader = TextInput.t -> ('value * TextInput.) option

Retour au sommaire.