843 lines
27 KiB
OCaml
843 lines
27 KiB
OCaml
|
(*
|
||
|
|
||
|
Ce module implémente une boucle interactive pour le langage
|
||
|
"Marthe", un langage d'expressions arithmétiques avec un
|
||
|
opérateurs de sommation.
|
||
|
|
||
|
Pour le compiler, utilisez par exemple la commande :
|
||
|
|
||
|
ocamlopt -o marthe unix.cmxa marthe.ml
|
||
|
|
||
|
Vous pouvez aussi l'évaluer pas à pas dans une boucle interactive
|
||
|
ocaml, liée au module standard Unix. Pour cela, dans le mode
|
||
|
Tuareg d'emacs, lancez la fonction "Run Caml Toplevel" du menu
|
||
|
Tuareg avec la commande :
|
||
|
|
||
|
ocaml unix.cma
|
||
|
|
||
|
*)
|
||
|
|
||
|
(*
|
||
|
|
||
|
Le langage Marthe est un langage minimaliste dont voici quelques
|
||
|
exemples de programmes corrects syntaxiquement:
|
||
|
|
||
|
- "73"
|
||
|
- "6 * 7"
|
||
|
- "1 + (2 * 3)"
|
||
|
- "1 + 2 * 3"
|
||
|
- "sum (x, 1, 10, x * x)"
|
||
|
- "sum (x, 1, 10, sum (y, 1, 10, x + y))"
|
||
|
- "1
|
||
|
+ 2 * 3"
|
||
|
- "sum (x, 1, 10 * 10, y)"
|
||
|
|
||
|
Nous allons écrire une boucle interactive incluant un interpréteur
|
||
|
de Marthe ainsi qu'un compilateur de Marthe vers une machine
|
||
|
abstraite minimaliste à pile.
|
||
|
|
||
|
*)
|
||
|
|
||
|
(* Cette fonction implémente une boucle interactive à
|
||
|
l'aide de trois fonctions:
|
||
|
- [read ()] demande une chaîne [s] à l'utilisateur ;
|
||
|
- [eval s] évalue le programme Marthe ;
|
||
|
- [print r] affiche la valeur résultat de l'évaluation de [s]. *)
|
||
|
let loop read eval print =
|
||
|
let rec aux () =
|
||
|
try
|
||
|
let p = read () in
|
||
|
let v = eval p in
|
||
|
print v;
|
||
|
aux ()
|
||
|
with
|
||
|
| End_of_file ->
|
||
|
(* Si le flux entrant se termine, on arrête la boucle interactive après
|
||
|
avoir affiché une nouvelle ligne. *)
|
||
|
print_newline ()
|
||
|
| exn ->
|
||
|
(* On utilise le module de la bibliothèque standard de Caml
|
||
|
pour afficher l'exception. L'échappement ANSI "\033[31m"
|
||
|
permet d'afficher en rouge l'erreur dans un terminal.
|
||
|
http://tldp.org/HOWTO/Bash-Prompt-HOWTO/x329.html
|
||
|
*)
|
||
|
Printf.printf "\027[31mError:%s\n\027[0m" (Printexc.to_string exn);
|
||
|
aux ()
|
||
|
in
|
||
|
aux ()
|
||
|
|
||
|
(*
|
||
|
|
||
|
Pour définir la syntaxe d'un langage, on utilise une *grammaire*.
|
||
|
|
||
|
Une grammaire définit deux choses:
|
||
|
- un ensemble de mots (les programmes syntaxiquement corrects) ;
|
||
|
- la structure de ces mots.
|
||
|
|
||
|
Ainsi, le premier problème à résoudre est de transformer une chaîne
|
||
|
de caractères (qui est une donnée toute plate, sans structure) en
|
||
|
un *arbre de syntaxe abstraite* (qui met en avant la structure des
|
||
|
programmes). Avoir une structure d'arbre permet d'*interpréter* les
|
||
|
programmes récursivement. Typiquement, on veut évaluer un expression
|
||
|
de la forme "e₁ + e₂" en évaluant e₁ en un entier n₁ puis e₂ en
|
||
|
un entier n₂ puis en faisant la somme de n₁ et n₂.
|
||
|
|
||
|
Voici des exemples de programmes Marthe pour illustrent ces idées:
|
||
|
|
||
|
* Exemple 1:
|
||
|
On veut traduire "1 + 2" en "EPlus (EInt 1, EInt 2)".
|
||
|
La sous-chaîne "1" a été reconnue comme "EInt 1".
|
||
|
La sous-chaîne "2" a été reconnue comme "EInt 2".
|
||
|
La sous-chaîne "1 + 2" a été reconnue en "EPlus (EInt 1, EInt 2)".
|
||
|
|
||
|
* Exemple 2:
|
||
|
On veut traduire "1 + 2 * 3" en "EPlus (EInt 1, EMul (EInt 2, EInt 3))".
|
||
|
Le nombre d'espaces n'a pas de signification dans la grammaire du langage
|
||
|
Marthe. Il y a donc des caractères importants et des caractères à ignorer.
|
||
|
Nous allons voir que la phase d'analyse lexicale sert à se concentrer sur
|
||
|
les symboles importants.
|
||
|
|
||
|
* Exemple 3:
|
||
|
On veut transformer "1 + 2 * 3" en "EPlus (EInt 1, EMul (EInt 2, EInt 3))".
|
||
|
Attention, ici, il serait incorrect de traduire "1 + 2 * 3" en
|
||
|
"EMul (EPlus (EInt 1, EInt 2), EInt 3)" parce que l'on donnerait
|
||
|
alors la priorité à l'addition devant la multiplication !
|
||
|
|
||
|
Ces exemples nous apprennent plusieurs choses:
|
||
|
|
||
|
1. Le type de la fonction "read" doit être quelque chose comme:
|
||
|
|
||
|
string → expression
|
||
|
|
||
|
où "expression" est un type de données arborescent.
|
||
|
|
||
|
2. L'analyse syntaxique peut être précédée d'une première phase qui
|
||
|
élimine les caractères non significatifs.
|
||
|
|
||
|
3. L'analyse syntaxique doit prendre en charge la reconnaissance des
|
||
|
priorités des opérateurs.
|
||
|
|
||
|
*)
|
||
|
|
||
|
(* [read] transforme une chaîne de caractères d'entrées en un arbre
|
||
|
de syntaxe abstraite en composant l'analyse lexicale et l'analyse
|
||
|
syntaxique. *)
|
||
|
let read lexer parse =
|
||
|
Printf.printf "marthe> %!";
|
||
|
let s = input_line stdin in
|
||
|
let tokens = lexer s in
|
||
|
parse tokens
|
||
|
|
||
|
(* Les lexèmes, aussi appelés "terminaux", sur lesquels
|
||
|
la grammaire du langage est définie. *)
|
||
|
type token =
|
||
|
| Int of int (* Ex: "42", "0", "231", ... *)
|
||
|
| Id of string (* Ex: "x", "abc", "foo" *)
|
||
|
| Sum (* "sum" *)
|
||
|
| Plus (* "+" *)
|
||
|
| Star (* "*" *)
|
||
|
| Lparen (* "(" *)
|
||
|
| Rparen (* ")" *)
|
||
|
| Comma (* "," *)
|
||
|
| EOF (* La fin de l'entrée. *)
|
||
|
|
||
|
let string_of_token = function
|
||
|
| Int x -> "Int(" ^ string_of_int x ^ ")"
|
||
|
| Id x -> "Id(" ^ x ^ ")"
|
||
|
| Sum -> "Sum"
|
||
|
| Plus -> "Plus"
|
||
|
| Star -> "Star"
|
||
|
| Lparen -> "Lparen"
|
||
|
| Rparen -> "Rparen"
|
||
|
| Comma -> "Comma"
|
||
|
| EOF -> "EOF"
|
||
|
|
||
|
exception LexingError of string
|
||
|
|
||
|
(* L'analyse lexixale produit une liste de lexèmes à partir de la
|
||
|
chaîne de caractères d'entrée. Il s'agit essentiellement d'un
|
||
|
automate fini implémentée à la main. *)
|
||
|
let lexer : string -> token list =
|
||
|
fun s ->
|
||
|
let at_the_end i = i >= String.length s in
|
||
|
|
||
|
(* Itère sur la chaîne en partant de l'indice [start],
|
||
|
et avance tant que le caractère [c] est tel que
|
||
|
[char_class c = true]. *)
|
||
|
let word char_class =
|
||
|
let rec aux start i =
|
||
|
let return stop = (String.sub s start (i - start), stop) in
|
||
|
if at_the_end i then
|
||
|
return (i + 1)
|
||
|
else if char_class s.[i] then
|
||
|
aux start (i + 1)
|
||
|
else
|
||
|
return i
|
||
|
in
|
||
|
fun start -> aux start start
|
||
|
in
|
||
|
|
||
|
(* Les classes de caractères. *)
|
||
|
let is_digit c = c >= '0' && c <= '9' in
|
||
|
let is_letter c = c >= 'a' && c <= 'z' in
|
||
|
(* Les mots sur ces classes de caractères. *)
|
||
|
let number = word is_digit in
|
||
|
let identifier = word is_letter in
|
||
|
|
||
|
(* La fonction récursive suivante itère sur la chaîne
|
||
|
à partir de [i] et tente de reconnaître un lexème. *)
|
||
|
let rec aux i =
|
||
|
(* Par défaut, pour continuer sur le caractère suivant, on augmente
|
||
|
l'indice et on fait un appel récursif. Dans certains cas,
|
||
|
l'indice [where] est fourni. *)
|
||
|
let continue ?(where=(i + 1)) () = aux where in
|
||
|
|
||
|
(* Pour retourner un lexème reconnu, on le met en tête
|
||
|
de la liste des tokens produite par les appels récursifs. *)
|
||
|
let produce_and_continue ?where token = token :: (continue ?where ()) in
|
||
|
|
||
|
if at_the_end i then
|
||
|
(* Le lexème EOF marque la fin de l'entrée. *)
|
||
|
[EOF]
|
||
|
else
|
||
|
(* Sinon, on peut décider quel lexème essayer de reconnaître
|
||
|
à l'aide du premier caractère croisé. *)
|
||
|
match s.[i] with
|
||
|
(* On saute les espaces. *)
|
||
|
| ' ' -> continue ()
|
||
|
|
||
|
(* Les symboles. *)
|
||
|
| '*' -> produce_and_continue Star
|
||
|
| '+' -> produce_and_continue Plus
|
||
|
| '(' -> produce_and_continue Lparen
|
||
|
| ')' -> produce_and_continue Rparen
|
||
|
| ',' -> produce_and_continue Comma
|
||
|
|
||
|
(* Les nombres. *)
|
||
|
| c when is_digit c ->
|
||
|
let (n, eo_num) = number i in
|
||
|
(* [i] est l'indice du dernier caractère du nombre
|
||
|
reconnu. *)
|
||
|
produce_and_continue ~where:eo_num (Int (int_of_string n))
|
||
|
|
||
|
(* Les identificateurs. *)
|
||
|
| c when is_letter c ->
|
||
|
let (s, eo_id) = identifier i in
|
||
|
(* [i] est l'indice du dernier caractère de
|
||
|
l'identificateur reconnu. *)
|
||
|
produce_and_continue ~where:eo_id (if s = "sum" then Sum else Id s)
|
||
|
|
||
|
(* Sinon, le caractère n'est pas accepté par le lexeur. *)
|
||
|
| _ ->
|
||
|
raise (LexingError "Invalid character")
|
||
|
in
|
||
|
aux 0
|
||
|
|
||
|
(* Tests de l'analyseur lexical. *)
|
||
|
let test_title s =
|
||
|
let max_test_title_len = 30 in
|
||
|
let s = String.escaped s in
|
||
|
if String.length s > max_test_title_len then
|
||
|
String.sub s 0 max_test_title_len ^ "..."
|
||
|
else
|
||
|
s
|
||
|
|
||
|
let ok s = Printf.printf "\027[1;32m[OK] `%s'\027[0m\n" (test_title s)
|
||
|
let ko s = Printf.printf "\027[1;31m[KO] `%s'\027[0m\n" (test_title s)
|
||
|
let ( --> ) input output = (input, output)
|
||
|
let do_test positivity display test (input, expected) =
|
||
|
try
|
||
|
if positivity (test input = expected) then
|
||
|
ok (display input)
|
||
|
else
|
||
|
ko (display input)
|
||
|
with _ -> if positivity true then ko (display input) else ok (display input)
|
||
|
|
||
|
let valid x = x
|
||
|
let invalid x = not x
|
||
|
|
||
|
let test_lexer () =
|
||
|
Printf.printf "-*- Lexer -*-\n";
|
||
|
(* Tests positifs. *)
|
||
|
List.iter (do_test valid (fun s -> s) lexer) [
|
||
|
"1" --> [Int 1; EOF];
|
||
|
"42" --> [Int 42; EOF];
|
||
|
"231" --> [Int 231; EOF];
|
||
|
"+" --> [Plus; EOF];
|
||
|
"*" --> [Star; EOF];
|
||
|
"(" --> [Lparen; EOF];
|
||
|
")" --> [Rparen; EOF];
|
||
|
"," --> [Comma; EOF];
|
||
|
"sum" --> [Sum; EOF];
|
||
|
"a" --> [Id "a"; EOF];
|
||
|
"sumx" --> [Id "sumx"; EOF];
|
||
|
"( )" --> [Lparen; Rparen; EOF];
|
||
|
"()" --> [Lparen; Rparen; EOF];
|
||
|
"42," --> [Int 42; Comma; EOF];
|
||
|
"" --> [EOF]
|
||
|
];
|
||
|
|
||
|
(* Tests négatifs. *)
|
||
|
List.iter (do_test invalid (fun s -> s) lexer) [
|
||
|
"#" --> [];
|
||
|
"!" --> [];
|
||
|
"\n" --> [];
|
||
|
]
|
||
|
|
||
|
(* Exercices de programmation:
|
||
|
|
||
|
Étendre l'analyse lexicale, pour
|
||
|
1. ignorer les tabulations ;
|
||
|
2. rajouter la gestion des symboles '-' et '/' ;
|
||
|
3. ignorer des commentaires écrits entre '(*' et '*)'.
|
||
|
|
||
|
*)
|
||
|
|
||
|
(* Les arbres de syntaxe abstraite du langage Marthe.
|
||
|
|
||
|
Sont donnés ici en exemple des chaînes de caractères produisant
|
||
|
un arbre dont la racine est le constructeur de données de la
|
||
|
même ligne. *)
|
||
|
type e =
|
||
|
| EInt of int (* Ex: "42", "31" *)
|
||
|
| EVar of string (* Ex: "x", "y", "foo" *)
|
||
|
| EPlus of e * e (* Ex: "1 + 2", "2 * 3 + 4" *)
|
||
|
| EMult of e * e (* Ex: "1 * 2", "(1 + 2) * 3" *)
|
||
|
| ESum of string * e * e * e (* Ex: "sum (x, 1, 10, x * x)" *)
|
||
|
|
||
|
exception ParseError of string * token
|
||
|
|
||
|
(* On se donne la grammaire suivante pour les arbres de syntaxe
|
||
|
de Marthe:
|
||
|
|
||
|
phrase ::= expression EOF
|
||
|
|
||
|
expression ::=
|
||
|
term PLUS expression
|
||
|
| term
|
||
|
|
||
|
term ::=
|
||
|
factor STAR term
|
||
|
| factor
|
||
|
|
||
|
factor ::=
|
||
|
INT(x)
|
||
|
| VAR(x)
|
||
|
| SUM LPAREN VAR COMMA expression COMMA expression COMMA expression RPAREN
|
||
|
| LPAREN expression RPAREN
|
||
|
|
||
|
La fonction [parse] transforme une liste de lexèmes en un arbre du type [e]
|
||
|
via l'analyse induite par la grammaire.
|
||
|
*)
|
||
|
|
||
|
let parse : token list -> e = fun tokens ->
|
||
|
(* On utilise trois fonctions pour se construire une abstraction
|
||
|
au-dessus de la liste des lexèmes restant à traiter. À l'aide
|
||
|
des trois fonctions suivantes, on lit cette liste de gauche
|
||
|
à droite au fur et à mesure de l'analyse, qui accepte ou non
|
||
|
ces lexèmes comme étant à une position valide vis-à-vis de
|
||
|
la grammaire. *)
|
||
|
let (accept, current, next) =
|
||
|
|
||
|
(* En utilisant une référence locale, on s'assure que seules
|
||
|
les trois fonctions suivantes peuvent modifier la variable
|
||
|
[token_stream]. *)
|
||
|
let token_stream = ref tokens in
|
||
|
|
||
|
(* La fonction [next] supprime le lexème en tête de la liste
|
||
|
des lexèmes à traiter. *)
|
||
|
let next () =
|
||
|
match !token_stream with
|
||
|
| [] -> raise (ParseError ("No more tokens", EOF))
|
||
|
| _ :: tokens ->
|
||
|
token_stream := tokens
|
||
|
in
|
||
|
|
||
|
(* La fonction [current] renvoie le lexème courant. *)
|
||
|
let current () =
|
||
|
match !token_stream with
|
||
|
| [] -> assert false
|
||
|
| tok :: _ ->
|
||
|
tok
|
||
|
in
|
||
|
|
||
|
(* [accept t] vérifie que le lexème courante est [t] et
|
||
|
passe alors au lexème suivant. *)
|
||
|
let accept token =
|
||
|
if (current () <> token) then
|
||
|
raise (ParseError ("Unexpected token", token));
|
||
|
next ()
|
||
|
in
|
||
|
(accept, current, next)
|
||
|
in
|
||
|
|
||
|
(* L'analyseur syntaxique suit un algorithme récursif et
|
||
|
descendant.
|
||
|
|
||
|
Il est défini par 4 fonctions mutuellement récursives
|
||
|
correspondant à chaque cas de la grammaire définie plus
|
||
|
haut.
|
||
|
*)
|
||
|
|
||
|
(* Une phrase est une expression suivie obligatoirement
|
||
|
par la fin de l'entrée. *)
|
||
|
let rec phrase () =
|
||
|
let e = expression () in
|
||
|
accept EOF;
|
||
|
e
|
||
|
|
||
|
(* Pour analyser une expression, ... *)
|
||
|
and expression () =
|
||
|
(* ... on commence par analyser un terme. *)
|
||
|
let e = term () in
|
||
|
match current () with
|
||
|
(* Si ce terme est suivi par un "Plus", on
|
||
|
est dans la seconde règle de la grammaire,
|
||
|
on doit donc accepter ce "Plus" et passer à
|
||
|
la suite pour reconnaître une expression. *)
|
||
|
| Plus ->
|
||
|
next ();
|
||
|
EPlus (e, expression ())
|
||
|
|
||
|
(* Dans les autres cas, nous étions dans
|
||
|
la première règle et nous avons reconnu
|
||
|
une expression [e]. Le travail est terminé. *)
|
||
|
| _ ->
|
||
|
e
|
||
|
|
||
|
(* Pour analyser un terme, on suit le même schéma que pour
|
||
|
les expressions. *)
|
||
|
and term () =
|
||
|
let t = factor () in
|
||
|
match current () with
|
||
|
| Star ->
|
||
|
next ();
|
||
|
EMult (t, term ())
|
||
|
|
||
|
| _ -> t
|
||
|
|
||
|
(* Pour décider dans quelle règle on se trouve, ... *)
|
||
|
and factor () =
|
||
|
(* on commence par observer le lexème courant. *)
|
||
|
match current () with
|
||
|
(* C'est une parenthèse ouvrante ? C'est la règle 4. *)
|
||
|
| Lparen ->
|
||
|
next ();
|
||
|
(* On doit reconnaître une expression ... *)
|
||
|
let e = expression () in
|
||
|
(* ... suivie d'une parenthèse fermante. *)
|
||
|
accept Rparen;
|
||
|
e
|
||
|
|
||
|
(* C'est le mot-clé "sum" ? C'est la règle 3. *)
|
||
|
| Sum ->
|
||
|
next ();
|
||
|
(* On attend une parenthèse ouvrante. *)
|
||
|
accept Lparen;
|
||
|
(* Puis, un identificateur. *)
|
||
|
let id =
|
||
|
match current () with
|
||
|
| Id s -> next (); s
|
||
|
| token -> raise (ParseError ("Expecting an identifier", token))
|
||
|
in
|
||
|
(* Une virgule. *)
|
||
|
accept Comma;
|
||
|
(* L'expression correspondante à l'initialisation de la variable
|
||
|
de sommation. *)
|
||
|
let start = expression () in
|
||
|
(* Une virgule. *)
|
||
|
accept Comma;
|
||
|
(* L'expression correspondante à la valeur finale de la variable
|
||
|
de sommation. *)
|
||
|
let stop = expression () in
|
||
|
(* Une virgule. *)
|
||
|
accept Comma;
|
||
|
(* L'expression correspondante au corps de la sommation. *)
|
||
|
let body = expression () in
|
||
|
(* Et enfin, une parenthèse fermante. *)
|
||
|
accept Rparen;
|
||
|
ESum (id, start, stop, body)
|
||
|
|
||
|
(* C'est un identificateur ? C'est la règle 2. *)
|
||
|
| Id x ->
|
||
|
next ();
|
||
|
EVar x
|
||
|
|
||
|
(* C'est un entier ? C'est la règle 1. *)
|
||
|
| Int x ->
|
||
|
next ();
|
||
|
EInt x
|
||
|
|
||
|
(* Les autres cas sont des cas d'erreur. *)
|
||
|
| token ->
|
||
|
raise (ParseError ("Unexpected token", token))
|
||
|
|
||
|
in
|
||
|
phrase ()
|
||
|
|
||
|
let test_parser () =
|
||
|
Printf.printf "-*- Parser -*-\n";
|
||
|
let display_tokens t = String.concat " " (List.map string_of_token t) in
|
||
|
(* Tests positifs. *)
|
||
|
List.iter (do_test valid display_tokens parse) [
|
||
|
[Int 1; EOF] --> EInt 1;
|
||
|
[Int 1; Plus; Int 41; EOF] --> EPlus (EInt 1, EInt 41);
|
||
|
[Int 1; Star; Int 41; EOF] --> EMult (EInt 1, EInt 41);
|
||
|
(lexer "1 + 2 * 3") --> EPlus (EInt 1, EMult (EInt 2, EInt 3));
|
||
|
(lexer "1 * 2 + 3") --> EPlus (EMult (EInt 1, EInt 2), EInt 3);
|
||
|
(lexer "sum (x, 1, 2, x * x)")
|
||
|
--> ESum ("x", EInt 1, EInt 2, EMult (EVar "x", EVar "x"))
|
||
|
];
|
||
|
|
||
|
(* Tests négatifs. *)
|
||
|
|
||
|
(* Une valeur bidon de type [e]. *)
|
||
|
let fail = EInt (-42) in
|
||
|
|
||
|
List.iter (do_test invalid display_tokens parse) [
|
||
|
[EOF] --> fail;
|
||
|
(lexer "1 + 2 *") --> fail;
|
||
|
(lexer "1 * (2)) + 3") --> fail;
|
||
|
(lexer "sum (x, 1, 2, x * x") --> fail
|
||
|
]
|
||
|
|
||
|
(* Exercices de programmation
|
||
|
|
||
|
Étendre l'analyse syntaxique pour intégrer
|
||
|
la division et la soustraction. Comment reconnaissez-vous
|
||
|
2 - 3 - 4 ? Comme "(2 - 3) - 4", ce qui est correct ou
|
||
|
plutôt comme "2 - (3 - 4)", ce qui est incorrect ?
|
||
|
*)
|
||
|
|
||
|
(* Un interprète produit récursivement la valeur entière correspondante à
|
||
|
l'évaluation d'un arbre de syntaxe. *)
|
||
|
let interpret : e -> int =
|
||
|
(* Le paramètre [env] est une liste associative
|
||
|
contenant la valeur associée aux indices de
|
||
|
sommation.
|
||
|
|
||
|
La fonction d'évaluation est définie par
|
||
|
cas sur la forme de l'arbre. *)
|
||
|
let rec aux env = function
|
||
|
(* Pour évaluer une expression de la forme "e1 + e2",
|
||
|
on évalue [e1] en un entier, on évalue [e2] en
|
||
|
un autre entier, puis on fait la somme des deux
|
||
|
entiers. *)
|
||
|
| EPlus (e1, e2) -> aux env e1 + aux env e2
|
||
|
|
||
|
(* Même raisonnement pour la multiplication. *)
|
||
|
| EMult (e1, e2) -> aux env e1 * aux env e2
|
||
|
|
||
|
(* Une expression qui est un entier s'évalue en cet entier. *)
|
||
|
| EInt x -> x
|
||
|
|
||
|
(* Pour évaluer une expression de la forme
|
||
|
"sum (x, start, stop, body)". *)
|
||
|
| ESum (x, start, stop, body) ->
|
||
|
(* On évalue [start]. *)
|
||
|
let vstart = aux env start
|
||
|
(* On évalue [stop]. *)
|
||
|
and vstop = aux env stop
|
||
|
in
|
||
|
(* On itère sur toutes les valeurs [i] de
|
||
|
[start] à [stop] et on accumule les sommes
|
||
|
intermédiaires dans la variable [accu]. *)
|
||
|
let rec iter i accu =
|
||
|
if i > vstop then
|
||
|
accu
|
||
|
else
|
||
|
(* L'évaluation de [body] se fait dans un
|
||
|
environnement où l'indice [x] est associé
|
||
|
à la valeur [i]. *)
|
||
|
iter (i + 1) (accu + aux ((x, i) :: env) body)
|
||
|
in
|
||
|
iter vstart 0
|
||
|
|
||
|
(* Une expression qui est variable s'évalue en la valeur
|
||
|
associée à cette variable dans l'environnement. *)
|
||
|
| EVar x ->
|
||
|
List.assoc x env
|
||
|
in
|
||
|
aux []
|
||
|
|
||
|
(* En rejoignant toutes les composantes, on obtient une boucle interactive qui
|
||
|
utilise notre interprète pour évaluer le programme Marthe. *)
|
||
|
let interactive_loop () =
|
||
|
loop
|
||
|
(fun () -> read lexer parse)
|
||
|
interpret
|
||
|
(fun x -> Printf.printf ":- %d\n" x)
|
||
|
|
||
|
let eval s = interpret (parse (lexer s))
|
||
|
|
||
|
(* Test de l'interprète. *)
|
||
|
let test_interpreter () =
|
||
|
Printf.printf "-*- Interpreter -*-\n";
|
||
|
|
||
|
(* Tests positifs. *)
|
||
|
List.iter (do_test valid (fun x -> x) eval) [
|
||
|
"1" --> 1;
|
||
|
"1 + 1" --> 2;
|
||
|
"6 * 7" --> 42;
|
||
|
"1 + 2 * 3" --> 7;
|
||
|
"sum (i, 1, 10, i)" --> 55;
|
||
|
"sum (i, 1, 10, sum (j, 1, i, i * j))"
|
||
|
--> 1705
|
||
|
];
|
||
|
|
||
|
(* Tests négatifs. *)
|
||
|
let fail = 42 in
|
||
|
|
||
|
List.iter (do_test invalid (fun x -> x) eval) [
|
||
|
"i" --> fail;
|
||
|
"sum (i, 1, 10, j)" --> fail
|
||
|
]
|
||
|
|
||
|
|
||
|
(* Exercice de programmation
|
||
|
|
||
|
Étendre l'interprète pour traiter la division et la soustraction.
|
||
|
*)
|
||
|
|
||
|
(* Nous allons maintenant définir une compilation des programmes
|
||
|
marthe vers la machine suivante : *)
|
||
|
type machine = {
|
||
|
(* Le pointeur de code courant. *)
|
||
|
mutable pc : int;
|
||
|
(* Le code est une liste d'instructions. *)
|
||
|
code : instruction array;
|
||
|
(* Des emplacements mémoires pour un nombre borné de variables. *)
|
||
|
variables : int array;
|
||
|
(* Des accumulateurs pour sommer en itérant sur ces variables. *)
|
||
|
accumulators : int array;
|
||
|
(* Une pile d'entiers pour stocker les valeurs intermédiaires. *)
|
||
|
mutable sp : int;
|
||
|
stack : int array;
|
||
|
}
|
||
|
|
||
|
and instruction =
|
||
|
(* Pousse une valeur entière sur la pile. *)
|
||
|
| Push of int
|
||
|
(* Dépile la valeur au sommet de la pile. *)
|
||
|
| Pop
|
||
|
(* Dépile deux valeurs et empile leur somme. *)
|
||
|
| Add
|
||
|
(* Dépile deux valeurs et empile leur produit. *)
|
||
|
| Mul
|
||
|
(* Met l'accumulateur numéro x à zéro. *)
|
||
|
| ResetAccu of int
|
||
|
(* Pousse l'accumulateur numéro x au sommet de la pile. *)
|
||
|
| PushAccu of int
|
||
|
(* Augmente l'accumulateur numéro x de la valeur sur la pile. *)
|
||
|
| AddAccu of int
|
||
|
(* Pousse la valeur de la variable x sur pile. *)
|
||
|
| GetVar of int
|
||
|
(* Affecte la valeur au sommet de la pile à la variable numéro x. *)
|
||
|
| SetVar of int
|
||
|
(* Incrémente à la variable x. *)
|
||
|
| IncVar of int
|
||
|
(* L'instruction [JumpLe (x, a)] déplace le pointeur de code en a si la valeur
|
||
|
de la variable x est inférieure ou égale à celle présente au sommet de la
|
||
|
pile. *)
|
||
|
| JumpLe of int * int
|
||
|
(* Arrête la machine et renvoie l'entier au sommet de la pile. *)
|
||
|
| Halt
|
||
|
|
||
|
(* Initialisation d'une machine abstraite. *)
|
||
|
let vm_init stack_size nb_variables code : machine = {
|
||
|
pc = 0;
|
||
|
sp = -1;
|
||
|
code = code;
|
||
|
variables = Array.make nb_variables 0;
|
||
|
accumulators = Array.make nb_variables 0;
|
||
|
stack = Array.make stack_size 0;
|
||
|
}
|
||
|
|
||
|
(* Cette exception sera lancée pour interrompre le
|
||
|
calcul. *)
|
||
|
exception Exit of int
|
||
|
|
||
|
(* Interprète le code d'une machine en un entier
|
||
|
(si le code est correct et ne dépasse pas les capacités
|
||
|
de la machine, en termes de nombre de variables et de
|
||
|
profondeur de pile). *)
|
||
|
let vm_interpret : machine -> int =
|
||
|
fun vm ->
|
||
|
(* Les opérations standards sur les piles. *)
|
||
|
let top () = vm.stack.(vm.sp) in
|
||
|
let pop () = let x = top () in vm.sp <- vm.sp - 1; x in
|
||
|
let push x = vm.sp <- vm.sp + 1; vm.stack.(vm.sp) <- x in
|
||
|
|
||
|
let interpret_instruction = function
|
||
|
| Push x ->
|
||
|
push x
|
||
|
| Pop ->
|
||
|
vm.sp <- vm.sp - 1
|
||
|
| Add ->
|
||
|
(* Notez l'optimisation: on travaille en place sur la pile. *)
|
||
|
vm.stack.(vm.sp - 1) <- vm.stack.(vm.sp - 1) + vm.stack.(vm.sp);
|
||
|
vm.sp <- vm.sp - 1
|
||
|
| Mul ->
|
||
|
vm.stack.(vm.sp - 1) <- vm.stack.(vm.sp - 1) * vm.stack.(vm.sp);
|
||
|
vm.sp <- vm.sp - 1
|
||
|
| ResetAccu x ->
|
||
|
vm.accumulators.(x) <- 0
|
||
|
| PushAccu x ->
|
||
|
push vm.accumulators.(x)
|
||
|
| AddAccu x ->
|
||
|
vm.accumulators.(x) <- vm.accumulators.(x) + pop ()
|
||
|
| IncVar x ->
|
||
|
vm.variables.(x) <- vm.variables.(x) + 1
|
||
|
| SetVar x ->
|
||
|
vm.variables.(x) <- pop ()
|
||
|
| GetVar x ->
|
||
|
push vm.variables.(x)
|
||
|
| JumpLe (x, a) ->
|
||
|
if vm.variables.(x) <= top () then vm.pc <- a - 1
|
||
|
| Halt ->
|
||
|
raise (Exit (pop ()))
|
||
|
in
|
||
|
try
|
||
|
while true do
|
||
|
interpret_instruction (vm.code.(vm.pc));
|
||
|
vm.pc <- vm.pc + 1
|
||
|
done;
|
||
|
(* On peut sortir de la boucle précédente uniquement
|
||
|
si son corps lance une exception. On ne peut donc
|
||
|
pas atteindre le point de code suivant. Cette branche
|
||
|
est du code mort. *)
|
||
|
assert false
|
||
|
with Exit x -> x
|
||
|
|
||
|
(* Voici une fonction de compilation des arbres de syntaxe
|
||
|
abstraite vers un code pour la machine précédente. *)
|
||
|
let compile : e -> instruction array =
|
||
|
fun e ->
|
||
|
(* [nb_idx] est le dernier indice utilisé pour nommer les variables de
|
||
|
sommation.
|
||
|
|
||
|
[variable_idx] est une liste associative des noms de variable du code
|
||
|
source vers leurs indices associés.
|
||
|
|
||
|
[pos] est la position courante dans le code machine produit.
|
||
|
|
||
|
La fonction de compilation fait un parcours en profondeur de l'arbre de
|
||
|
syntaxe abstraite et pour chaque sous-arbre A produit un code machine C tel
|
||
|
que l'évaluation de C place le résultat de l'évaluation du sous-arbre A au
|
||
|
sommet de la pile de la machine, sans modifier celle-ci autrement.
|
||
|
|
||
|
*)
|
||
|
let rec aux nb_idx variable_idx pos = function
|
||
|
| EInt x ->
|
||
|
(pos + 1, [ Push x ])
|
||
|
|
||
|
| EPlus (e1, e2) ->
|
||
|
let (pos, instrs_e1) = aux nb_idx variable_idx pos e1 in
|
||
|
let (pos, instrs_e2) = aux nb_idx variable_idx pos e2 in
|
||
|
(pos + 1, instrs_e1 @ instrs_e2 @ [ Add ])
|
||
|
|
||
|
| EMult (e1, e2) ->
|
||
|
let (pos, instrs_e1) = aux nb_idx variable_idx pos e1 in
|
||
|
let (pos, instrs_e2) = aux nb_idx variable_idx pos e2 in
|
||
|
(pos + 1, instrs_e1 @ instrs_e2 @ [ Mul ])
|
||
|
|
||
|
| ESum (x, start, stop, body) ->
|
||
|
let variable_idx' = (x, nb_idx + 1) :: variable_idx in
|
||
|
let nb_idx' = nb_idx + 1 in
|
||
|
let (pos, instrs_start) = aux nb_idx variable_idx pos start in
|
||
|
let (pos, set_x) = (pos + 1, [ SetVar nb_idx' ]) in
|
||
|
let (pos, instrs_stop) = aux nb_idx variable_idx pos stop in
|
||
|
let (pos, init_accu) = (pos + 1, [ ResetAccu nb_idx' ]) in
|
||
|
let pos_body = pos in
|
||
|
let (pos, instrs_body) = aux nb_idx' variable_idx' pos body in
|
||
|
(pos + 5,
|
||
|
instrs_start
|
||
|
@ set_x
|
||
|
@ instrs_stop
|
||
|
@ init_accu
|
||
|
@ instrs_body
|
||
|
@ [ AddAccu nb_idx' ]
|
||
|
@ [ IncVar nb_idx';
|
||
|
JumpLe (nb_idx', pos_body);
|
||
|
Pop;
|
||
|
PushAccu nb_idx' ])
|
||
|
|
||
|
| EVar x ->
|
||
|
(pos + 1, [ GetVar (List.assoc x variable_idx) ])
|
||
|
in
|
||
|
let (_, code) = aux 0 [] 0 e in
|
||
|
Array.of_list (code @ [ Halt ])
|
||
|
|
||
|
let lot_of_variables = 128
|
||
|
let stack_size = 1024
|
||
|
|
||
|
let vm_eval s =
|
||
|
let vm = vm_init stack_size lot_of_variables (compile (parse (lexer s))) in
|
||
|
vm_interpret vm
|
||
|
|
||
|
(* Tests du compilateur. *)
|
||
|
|
||
|
let test_compiler () =
|
||
|
Printf.printf "-*- Compiler -*-\n";
|
||
|
|
||
|
(* Tests positifs. *)
|
||
|
let check s = s --> eval s in
|
||
|
|
||
|
List.iter (do_test valid (fun x -> x) vm_eval) [
|
||
|
check "1";
|
||
|
check "1 + 2";
|
||
|
check "1 + 2 * 3";
|
||
|
check "sum (x, 1, 10, x + x)";
|
||
|
check "1 + sum (x, 10 * 10, 10 * 10 * 10, x * sum (i, x, 2 * x, i + x))"
|
||
|
]
|
||
|
|
||
|
(* Exercice de programmation
|
||
|
|
||
|
Étendre la machine et la fonction de compilation pour traiter la division et
|
||
|
la soustraction.
|
||
|
*)
|
||
|
|
||
|
let benchmark_interactive_loop () =
|
||
|
let time f =
|
||
|
let start = Unix.gettimeofday () in
|
||
|
let y = f () in
|
||
|
let stop = Unix.gettimeofday () in
|
||
|
(stop -. start, y)
|
||
|
in
|
||
|
let eval e =
|
||
|
let (interpreted_time, x) = time (fun () -> interpret e) in
|
||
|
let vm = vm_init stack_size lot_of_variables (compile e) in
|
||
|
let (compiled_time, y) = time (fun () -> vm_interpret vm) in
|
||
|
(interpreted_time, compiled_time, x, y)
|
||
|
in
|
||
|
let print (interpreted_time, compiled_time, x, y) =
|
||
|
Printf.printf "(Interpreted: %f, Compiled: %f) :- %d\n"
|
||
|
interpreted_time compiled_time x;
|
||
|
if x <> y then Printf.printf "Warning: VM found %d\n" y
|
||
|
in
|
||
|
loop (fun () -> read lexer parse) eval print
|
||
|
|
||
|
let test_suite () =
|
||
|
test_lexer ();
|
||
|
test_parser ();
|
||
|
test_interpreter ();
|
||
|
test_compiler ()
|
||
|
|
||
|
let batch () =
|
||
|
Printf.printf "%d\n" (interpret (read lexer parse))
|
||
|
|
||
|
let batch_mode = ref false
|
||
|
|
||
|
let _ =
|
||
|
Arg.parse (Arg.align [
|
||
|
"-bench", Arg.Unit benchmark_interactive_loop,
|
||
|
" Launch a toplevel that uses several evaluation strategies.";
|
||
|
"--", Arg.Set batch_mode, " Only interpret stdin.";
|
||
|
"-test", Arg.Unit test_suite,
|
||
|
" Launch the test suite of the program."
|
||
|
]) ignore ("marthe [options]");
|
||
|
if !batch_mode then
|
||
|
batch ()
|
||
|
else
|
||
|
interactive_loop ()
|