Un petit interpréteur pseudo-pascal (énoncé)

1  La solution

La solution est un fichier source interpret.ml.

2  Quelques détails

2.1  Les valeurs et les environnements

Les valeurs utlisées de façon interne par l'interpréteur (booléns, entiers, tableaux et valeurs indéfinies) sont rendues par un bête type somme :
type valeur =
  | Vint of int
  | Vbool of bool
  | Varray of valeur array
  | Undefined

Au plus simple un environnement est un tas de liaisons entre des noms de variables (des string) et des cellules contenant chacune une valeurs (valeur ref). Le plus simple est de représenter tout ça par une liste de paires.

La stucture du langage interprété (fonctions globales) conduit à distinguer variables globales, fonctions (globales) et variables locales et à représenter les environnements par des enregistrements à trois champs :
type env_t = {
  fonctions : (string * definitionlist ;
  globals : (string * valeur reflist ;
  locals : (string * valeur reflist
}
Pour trouver la cellule associée à une variable, on cherchera d'abord dans les variables locales puis dans les globales :
let trouve_var {globals = glob ; locals = loc } x =
  try List.assoc x loc with
  | Not_found ->
      try List.assoc x glob with
      | Not_found -> erreur (Inconnue ("variable: "^x))
On utilise la fonction de librairie List.assoc, expliquée dans le manuel Objective Caml L'explication permet de savoir que List.assoc lève l'exception Not_found lorsque qu'il n'y a pas de paire (x,...) dans la liste et de se servir de ce fait.

Lors des appels de fonctions (ou de procédure) on aura besoin de fabriquer un nouvel environnement. Voici une fonction qui renvoie l'environnement env étendu d'une liaison locale entre la variable x et une nouvelle cellule contenant initialement la valeur v :
and extend x v env =
  {env with locals = (x,ref v) :: env.locals}
On utilise la copie d'enregistrement avec mise à jour, expliquée à la section 6.7.3 du manuel OCaml.

3  Structure de l'interpréteur

Initialement on sait que l'on aura à evaluer des expressions et des instructions, l'evaluation des expressions pouvant entraîner l'evaluation d'une instruction (appel de fonction) et réciproquement (evaluation de la condition dans un If). Soit :
let rec expr env = function
Int i -> Vint i

and instr env
 = function
Set (x,e) -> ...

Pour évaluer un programme, il suffit de fabriquer un premier environnement, puis de lancer l'évaluation de l'instruction ``main'' :
let eval
    {global_vars = globs ;
      definitions = defs ;
      main = i} =
  let start_env =
    {globals = List.map (fun (x,_) -> xref Undefinedglobs ;
      fonctions  = defs ;
      locals = []} in
  instr start_env i

4  Quelques astuces de structure

4.1  Partage de code

Afin de partager du code (d'éviter d'écrire dix fois le même code), on ajoute plein de fonctions. Par exemple, l'interpréteur à souvent besoin d'évaluer une expression comme un entier (primitives, accès aux tableaux...) on écrit donc une fonction utilitaire expr_int :
let expr env = function ...

and expr_int env e = match expr env e with
Vint i -> i
Undefined -> erreur PasDef
v -> erreur (Type (Integer,v))

and instr env = function ...
Ensuite rien de plus facile que d'évaluer une expression vers un entier, un booléen ou un tableau :
let expr env = function
 ...
Geti (etei) ->
    let vt = expr_array env et in
    let
 vi
 = expr_int env ei in
    vt
.(vi)
  ...

4.2  Les primitives

Le code de l'évaluation des primitives est mis dans une fonction préliminaire qui prend en argument la primitive et deux entiers. Cette technique localise l'évaluation des primitives et permet des modifications plus aisées, si par exemple, on ajoute une primitive :
let binop op i1 i2 = match op with
Plus -> Vint (i1 + i2)
| ...

let expr env = function
 ...
Binop (ope1e2) -> binop op (expr_int env e1) (expr_int env e2)

5  Appel de fonction

Le code de fabrication du nouvel environnement est commun aux procédures et aux fonctions (fonction call_env). Il s'agit de renvoyer l'environnement global étendu par les nouvelles liaisons des variables locales à Undefined ; et des noms des arguments (paramètres dits formels) à la valeur de l'argument correspondant (paramètres dits effectifs) :
and env_locs env = function
| [] -> env
| (x,_) :: reste ->
      extend x Undefined (env_locs env reste)

and env_args env f xs es = match xs,es with
| [],[] -> take_globs env
| (x,t)::xse::es ->
    let v = expr env e in
    extend x v
 (env_args env f xs es)
_ -> erreur (NumArgs f)

and call_env env f fdef es  =
  env_locs
    (env_args env f fdef.arguments es)
    fdef.local_vars
On notera l'appel récursif à expr dans le corps de env_args.

Dans le cas des fonctions on augmente l'environnement d'appel d'une liaison entre le nom de la fonction et Undefined, 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

Soit pour, par exemple construire l'environnement d'appel d'une fonction :
let rec expr env = function
  ...
Function_call (f,e_args) ->
    let fdef = trouve_fun env f in
    let
 new_env
 =
      extend
        f Undefined
 (* variable re'sultat *)
        (call_env env f fdef e_argsin
  ...
Ensuite il reste à évaluer le coprs de la fonction dans l'environnement si chèrement construit :
  instr new_env fdef.body ;
et à récupérer le résultat :
  let fcell = trouve_var new_env f in (* recuperer le re'sultat *)
  !fcell

L'appel de procédure est identique, moins la manip sur la variable résultat.

6  Sur le typage

L'interpréteur présenté fait le minimum de tests de types : il ne vérifie le type des valeurs que lorsqu'il a réellement besoin de la valeur (par un appel à expr_int, expr_bool, etc.), par exemple pour tester une condition (booléen) ou accéder à un tableau (type tableau et entier).

Il n'est pas du tout exclu de faire plus de vérifications, par exemple, on peut vérifier le type des arguments lors de l'appel de fonction. Les erreurs de type seront alors détectées plus tôt et l'utlisateur de l'interpréteur corrigera son programme plus facilement.


Ce document a été traduit de LATEX par HEVEA.