Un interprète Pseudo-Pascal, solutions

L’énoncé est ici.

Type des valeurs

Les valeurs utilisées de façon interne par l’interprète (booléens, entiers et tableaux) sont représentées par un type somme. L’extension du type value est immédiate :

type value =
  | VBool of bool
  | VInt of int32
  | VArray of value array

Expressions arithmétiques

La fonction d’évaluation des expressions est étendue à de nouveaux cas, en prenant en compte le type booléen pour les constantes, et les opérations unaires ou binaires sur des entiers ou des booléens.

let rec interpret_expression defs genv env = function
  | EConst (ConstBool b) -> VBool b
  | EConst (ConstInt i) -> VInt i
  | EUnOp (UOpNeg, e) ->
      VInt (- (asInt (interpret_expression defs genv env e)))
  | EBinOp (op, e1, e2) ->
      begin
        let i1 = asInt (interpret_expression defs genv env e1) in
        let i2 = asInt (interpret_expression defs genv env e2) in
        match op with
        | OpAdd -> VInt (i1 + i2)
        | OpSub -> VInt (i1 - i2)
        | OpMul -> VInt (i1 * i2)
        | OpDiv -> VInt (i1 / i2)
        | OpLt -> VBool (i1 < i2)
        | OpLe -> VBool (i1 <= i2)
        | OpGt -> VBool (i1 > i2)
        | OpGe -> VBool (i1 >= i2)
        | OpEq -> VBool (i1 = i2)
        | OpNe -> VBool (i1 <> i2)
      end
  | _ -> failwith "Pas implémenté"

Environnements

Un environnement est un ensemble de liaisons entre des noms de variables (de type string) et des cellules contenant chacune une valeur mutable (de type valeur ref) d’une part, des noms de fonctions ou procédures (de type string) et des procédures Pseudo-Pascal (de type procedure) d’autre part.

On l’implémente sous la forme de 3 tables d’association passées par paramètre aux fonctions qui en ont besoin :

La fonction lookup cherche d’abord dans l’environnement local env une liaison pour le nom de variable qui lui est passé en paramètre. Si cette recherche échoue, elle cherche alors cette liaison dans l’environnement global genv.

let lookup (genv : environment) (env : environment) (x : string) : value ref =
  try
    StringMap.find x env
  with Not_found ->
    try
      StringMap.find x genv
    with Not_found ->
      assert false

On utilise la fonction de librairie Map.find, expliquée dans le manuel Objective Caml. L’explication permet de savoir que Map.find lève l’exception Not_found lorsqu’il n’y a pas de paire (x,...) dans la table et de se servir de ce fait.

Variables globales

La fonction interpret prend la forme suivante :

let interpret p =
  let genv =
    StringMap.map allocate p.globals
  in
  interpret_instruction p.defs genv StringMap.empty p.main

allocate est une fonction qui crée une valeur mutable ou ref en OCaml, qu’elle initialise à une valeur par défaut. Pour les entiers et les booléens, cette valeur par défaut est tout simplement 0 ou false ; pour les tableaux cela implique d’ajouter un nouveau type de valeur VNil pour les tableaux pas encore alloués.

type value =
  | VBool of bool
  | VInt of int32
  | VArray of value array
  | VNil

let default = function
  | TypBool -> VBool false
  | TypInt -> VInt 0l
  | TypArray _ -> VNil

let allocate typ = ref (default typ)

La lecture d’une variable nécessite de modifier interpret_expression pour lui ajouter le cas suivant :

  | EGetVar x -> !(lookup genv env x)

L’écriture d’une variable nécessite de modifier interpret_instruction pour lui ajouter le cas suivant :

  | ISetVar (x, e) ->
      (lookup genv env x) := (interpret_expression defs genv env e)

Fonctions et tableaux

Appels de fonctions

Le code de fabrication du nouvel environnement local est commun aux procédures et aux fonctions.

Il s’agit de redéfinir l’environnement env étendu par les nouvelles liaisons des variables locales à leur valeur par défaut ; et des noms des arguments (paramètres dits formels) à la valeur de l’argument correspondant (paramètres dits effectifs) :

      let env =
        List.fold_right2 (fun (formal, _) actual env ->
          StringMap.add formal (ref actual) env
        ) proc.formals actuals StringMap.empty
      in

      let env =
        StringMap.addm (StringMap.map allocate proc.locals) env
      in

On utilise ici une fonction d’itération, qui implémente une boucle de manière fonctionnelle : la fonction de librairie List.fold_right2.

Dans le cas des fonctions on augmente l’environnement d’appel d’une liaison entre le nom de la fonction et une valeur par défaut correspondant au type de retour attendu. Cette liaison est celle de la variable résultat de la fonction. Rappelons en effet qu’en Pascal, on procède ainsi :

function fact (x : integer) : integer
begin
  ...
  fact := ...
end

Ainsi, pour finir de construire l’environnement d’appel d’une fonction :

La fonction Option.fold est implémentée dans le fichier option.ml.

Ensuite il reste à évaluer le corps de la fonction dans l’environnement si chèrement construit :

      interpret_instruction defs genv env proc.body;

et à récupérer le résultat :

      Option.map (fun _ -> !(StringMap.find f env)) proc.result

L’appel de procédure est identique, moins la gestion de la variable résultat.

Structure de l’interprète

L’évaluation d’une expression peut entraîner l’évaluation d’une instruction, à travers l’appel d’une fonction. Réciproquement, l’évaluation d’une instruction peut entraîner l’évaluation d’une expression, par exemple à travers l’évaluation de la condition dans une construction if. Donc les fonctions d’évaluation seront mutuellement récursives :

let rec interpret_call defs genv env callee actuals : value option = ...
and interpret_expression defs genv env = function ...
and interpret_condition defs genv env = function ...
and interpret_instruction defs genv env = function ...

Tableaux

On utilise la librairie standard Array d’OCaml. Cela permet d’effectuer toutes les opérations autorisées par le langage Pseudo-Pascal sur les tableaux : création, lecture, écriture.

Vérifications de typage

L’interprète a souvent besoin d’évaluer une expression dont le type est contraint : à int pour un index lors d’un accès tableau, à bool dans une expression conditionnelle, etc. Afin de partager du code (éviter d’écrire plusieurs fois le même code), on ajoute des fonctions utilitaires asBool, asInt et asArray.

Ensuite, rien de plus facile que d’évaluer une expression pour obtenir un résultat booléen, entier ou tableau :

and interpret_expression defs genv env = function
    ...
  | EArrayGet (ea, ei) ->
      let a = asArray (interpret_expression defs genv env ea) in
      let i = asInt (interpret_expression defs genv env ei) in
      begin try
        Array.get a i
    ...

Finalement...

L’interprète Pseudo-Pascal complet se trouve dans interpretPP.ml.


Ce document a été traduit de LATEX par HEVEA