En programmation fonctionnelle, les programmes et les donnees sont de meme nature : un programme peut etre represente comme une donnee (un arbre), et manipule par des fonctions. C'est le principe des arbres syntaxiques abstraits (AST).
3.1 Arbres syntaxiques abstraits (AST)
(* Type des expressions arithmetiques *)
type expr =
| Cst of float
| Var of string
| Add of expr * expr
| Sub of expr * expr
| Mul of expr * expr
| Div of expr * expr
(* Exemple : (x + 2) * (x - 1) *)
let exemple =
Mul (Add (Var "x", Cst 2.0),
Sub (Var "x", Cst 1.0))
Un AST represente la structure syntaxique d'une expression sous forme d'arbre. Chaque noeud est un constructeur du type ; chaque feuille est une donnee primitive.
3.2 Interpretation
L'interprete ecute l'AST pour en extraire la valeur semantique.
(* Interpreteur : AST -> valeur *)
let rec eval env = function
| Cst c -> c
| Var x -> List.assoc x env
| Add (e1, e2) -> eval env e1 +. eval env e2
| Sub (e1, e2) -> eval env e1 -. eval env e2
| Mul (e1, e2) -> eval env e1 *. eval env e2
| Div (e1, e2) -> eval env e1 /. eval env e2
(* Utilisation *)
let resultat = eval [("x", 3.0)] exemple
(* = (3 + 2) * (3 - 1) = 10.0 *)
3.3 Transformation et fold sur les arbres
Le fold sur un AST parcourt recursivement l'arbre et combine les resultats des sous-arbres avec des fonctions specifiques a chaque constructeur. Schema :
- Cas de base (feuilles :
Cst,Var) : traitement immediat - Cas recursif (noeuds :
Add,Mul...) : combiner les resultats des sous-arbres
(* Affichage (pretty-printer) *)
let rec afficher = function
| Cst c -> string_of_float c
| Var x -> x
| Add (e1, e2) -> afficher e1 ^ " + " ^ afficher e2
| Sub (e1, e2) -> afficher e1 ^ " - " ^ afficher e2
| Mul (e1, e2) -> afficher e1 ^ " * " ^ afficher e2
| Div (e1, e2) -> afficher e1 ^ " / " ^ afficher e2
(* Comptage du nombre d'operateurs *)
let rec nb_op = function
| Cst _ | Var _ -> 0
| Add (e1, e2) | Sub (e1, e2) | Mul (e1, e2) | Div (e1, e2) ->
1 + nb_op e1 + nb_op e2
3.4 Application — Calcul symbolique
3.4.1 Derivation symbolique
(* Derivation symbolique : d/dx *)
let rec deriv x = function
| Cst _ -> Cst 0.0
| Var y -> if x = y then Cst 1.0 else Cst 0.0
| Add (e1, e2) -> Add (deriv x e1, deriv x e2)
| Sub (e1, e2) -> Sub (deriv x e1, deriv x e2)
| Mul (e1, e2) ->
Add (Mul (deriv x e1, e2), Mul (e1, deriv x e2)) (* Produit de Leibniz *)
| Div (e1, e2) ->
Div (Sub (Mul (deriv x e1, e2), Mul (e1, deriv x e2)),
Mul (e2, e2)) (* Quotient de Leibniz *)
3.4.2 Simplification symbolique
(* Simplification *)
let rec simplifier = function
| Add (e, Cst 0.) | Add (Cst 0., e) -> simplifier e
| Mul (_, Cst 0.) | Mul (Cst 0., _) -> Cst 0.
| Mul (e, Cst 1.) | Mul (Cst 1., e) -> simplifier e
| Add (e1, e2) -> Add (simplifier e1, simplifier e2)
| Mul (e1, e2) -> Mul (simplifier e1, simplifier e2)
| e -> e
(* Exemple : d/dx(x*x + 2*x) = 1*x + x*1 + 2*1 + x*0 = 2*x + 2 *)
3.5 Application — Mini compilateur / Machine virtuelle
Un compilateur transforme l'AST en une suite d'instructions pour une machine a pile. L'execution emploie/depile les valeurs.
(* Instructions de la machine a pile *)
type instruction =
| PUSH of float
| ADD | SUB | MUL | DIV
| LOAD of string
(* Compilateur : AST -> liste d'instructions *)
let rec compiler = function
| Cst c -> [PUSH c]
| Var x -> [LOAD x]
| Add (e1, e2) -> compiler e1 @ compiler e2 @ [ADD]
| Sub (e1, e2) -> compiler e1 @ compiler e2 @ [SUB]
| Mul (e1, e2) -> compiler e1 @ compiler e2 @ [MUL]
| Div (e1, e2) -> compiler e1 @ compiler e2 @ [DIV]
(* Executeur : simule la machine a pile *)
let executer env instrs =
let pile = Stack.create () in
let eval_instr = function
| PUSH c -> Stack.push c pile
| LOAD x -> Stack.push (List.assoc x env) pile
| ADD -> let b = Stack.pop pile in Stack.push (Stack.pop pile +. b) pile
| SUB -> let b = Stack.pop pile in Stack.push (Stack.pop pile -. b) pile
| MUL -> let b = Stack.pop pile in Stack.push (Stack.pop pile *. b) pile
| DIV -> let b = Stack.pop pile in Stack.push (Stack.pop pile /. b) pile
in
List.iter eval_instr instrs;
Stack.pop pile
(* Verif : executer env (compiler e) = eval env e *)
- Etendez le type
expravecPowetNeg. - Ameliorez le pretty-printer avec la precedence des operateurs.
- Compilez vers un code SSA (Static Single Assignment).
- Implementez la propagation de constantes.
En Seance 6, nous prouverons en Coq que le compilateur est
correct : exec (compile e) [] = Some (eval e).