Homework 11: Compiling MiniPython
Due: Friday, April 3rd, 11:59pm
Start with the template code here.
- Replace each
failwith "TODO"with your solution. - Be sure to test your solutions in
utopor in tests alongside your submission. To load your file intoutop, use#use "hw11.ml";;. - Submit your version of
hw11.mlto the HW11 assignment on Gradescope. Only submithw11.ml; NOT any other files or folders. - At the top of the template file, there is a comment for you to write in approximately how many hours you spent on the assignment. Filling this in is optional, but helpful feedback for the instructors.
Compiling from MiniPython to StackLang
In this homework, we will continue analyzing MiniPython, which we looked at in HW10. Below is our language, MiniPython (also found in the template file):
MiniPython
module StringMap = Map.Make(String)
type value = int
type op = | Add | Mul | EQ | Sub | Gt
let eval_binop (o : op) (v1 : value) (v2 : value) : value =
match o with
| Add -> v1 + v2
| Mul -> v1 * v2
| Sub -> v1 - v2
| EQ -> if v1 = v2 then 1 else 0
| Gt -> if v1 > v2 then 1 else 0
module MiniPython = struct
type expr = | Value of value | Var of string | App of op * expr list
type cmd = | Assign of string * expr | Ite of expr * cmd * cmd | Seq of cmd * cmd | While of expr * cmd | Skip
let rec eval_expr (m : value StringMap.t) (e: expr) : value =
match e with
| Value v -> v
| Var x -> StringMap.find x m
| App (o, es) ->
(match es with
| [e1; e2] -> eval_binop o (eval_expr m e1) (eval_expr m e2)
| _ -> failwith "Got wrong number of arguments"
)
let rec eval_cmd (m : value StringMap.t) (c : cmd) : value StringMap.t =
match c with
| Assign (x, e) -> StringMap.add x (eval_expr m e) m
| Ite (e, c1, c2) -> if eval_expr m e = 1 then eval_cmd m c1 else eval_cmd m c2
| Seq (c1, c2) -> let m' = eval_cmd m c1 in eval_cmd m' c2
| While (e, c) ->
if eval_expr m e = 1 then
let m' = eval_cmd m c in
eval_cmd m' (While (e, c))
else
m
| Skip -> m
end
Just as we compiled CalcLang to StackLang in Week 11, we now will compile MiniPython to StackLang.
Our version of StackLang will be a bit different than class. In particular, to handle MiniPython, we will extend the evaluator to also allow jumps backwards instead of jumps forwards. We do this by using the following definitions:
module StackLang = struct
type label = { lbl : int }
type instr =
| Push of value
| PrimOp of op
| Get of string
| Set of string
| Label of label
| JumpIfZero of label
| Jump of label
type config = {
pc : int;
ins : instr list;
stack : value list;
env : value StringMap.t
}
...
end
In the notes, a configuration was a tuple of a list of instructions, a stack, and an environment, and we consumed (i.e., dropped) instructions as we executed them, and finished execution once the list of instructions was empty. Now, we are using a different approach: the list of instructions, ins will stay constant through execution. Instead, we have a program counter pc that will index into this list, and evolve as the StackLang program executes.
Let's now look at the evaluator for StackLang:
StackLang
let find_label prfx { lbl } prog : int =
let pc_opt = List.find_index (fun instr -> instr = Label { lbl }) prog in
match pc_opt with
| Some pc -> pc
| None -> failwith (prfx ^ ": Undefined label " ^ string_of_int lbl)
let step (cfg : config) : config =
match List.nth cfg.ins cfg.pc with
| Push v -> { cfg with pc = cfg.pc + 1; stack = v :: cfg.stack }
| Set s ->
(match cfg.stack with
| [] -> failwith ("Stack empty: " ^ s ^ " pc: " ^ string_of_int cfg.pc)
| v::stack' -> { cfg with pc = cfg.pc + 1; stack = stack'; env = StringMap.add s v cfg.env }
)
| Get s ->
(match StringMap.find_opt s cfg.env with
| None -> failwith "Unknown variable"
| Some v -> { cfg with pc = cfg.pc + 1; stack = v :: cfg.stack }
)
| PrimOp op ->
(match cfg.stack with
| v1 :: v2 :: stack' ->
(* Important: ordering of v1 and v2 is flipped. This is on purpose. *)
let v = eval_binop op v2 v1 in { cfg with pc = cfg.pc + 1; stack = v :: stack' }
| _ -> failwith "Not enough elements on stack."
)
| Label l -> { cfg with pc = cfg.pc + 1 }
| Jump l -> { cfg with pc = find_label "Jump" l cfg.ins }
| JumpIfZero l ->
(match cfg.stack with
| v :: stack' ->
if v = 0
then { cfg with pc = find_label "JumpIfZero" l cfg.ins; stack = stack' }
else { cfg with pc = cfg.pc + 1; stack = stack' }
| _ -> failwith "Not enough elements on stack")
type program = instr list
let rec loop (cfg : config) : config =
if 0 <= cfg.pc && cfg.pc < List.length cfg.ins then
loop (step cfg)
else cfg
let init_config p = { pc = 0; ins = p; stack = []; env = StringMap.empty }
let execute (p : program) : value StringMap.t =
(loop (init_config p)).env
let get_pc_list (p : program) : int list * value StringMap.t = ...
For every "normal" instruction (Push/Get/Set/PrimOp), the program counter pc advances by one. For Jump l, the program counter changes to label l's index. For JumpIfZero l, the program counter advances to label l if the top of the stack is zero; otherwise, it also changes to pc.
To run the program, we initialize pc = 0, with an empty stack an environment. We then run the program until pc is out of bounds, at which point we stop.
Because we only run MiniPython for its side effects to the environment, we let the return type of execute be an environment.
In addition to execute, we also give you a function get_pc_list that also returns the history of the program counter as the program evaluated.
Your task is to compile MiniPython to StackLang.
let rec compile_expr (e : MiniPython.expr) : StackLang.program =
failwith "TODO"
let rec compile_cmd (c : MiniPython.cmd) : StackLang.program =
failwith "TODO"
Your compiler will be evaluated via correctness, meaning that, given a command c : MiniPython.cmd, if c successfully executes via MiniPython.eval under the empty environment to return a final environment env, then compile_cmd c must execute via StackLang.execute and return the same environment.
Grading Criteria
(30 points, autograded)
Correctness of your compiler.
Hint
compile_expr will behave similarly to how the corresponding cases behave in our compiler from Week 11.
For every expression e, if we run compile_expr e under the empty stack, we should get a stack with exactly one value left on top.
For compile_cmd, Ite will be more or less similar to how control flow was achieved in our compiler from Week 11.
While is a bit trickier; it is similar to Ite, but we jump over the entire rest of the while-loop code if the guard is false.
If the guard is true, we will instead run the body of the while loop, then perform a jump with a negative offset so that we run the while loop again.
For every command c, if we run compile_cmd c under the empty stack, we should have an empty stack after the compiled program finishes running.
Example Programs
In the template, we provide you with three example programs in MiniPython:
if_cmd, which uses Assign, Seq, and Ite; while_cmd, which uses
Assign and While; and fib_cmd, which uses the entire language.
Here, fib_cmd : int -> MiniPython.cmd computes the ith Fibonacci number using MiniPython, given input i.
To test your compiler, we provide a function test_compile : MiniPython.cmd -> string -> (value option * value option) which, runs the given command
in MiniPython, runs the compiled command in StackLang, and returns the final value of the two environments on the variable given as the second argument.
We also give you helper functions as a convenience for running test_compile on the three example programs; you can run these in utop to test your compiler.
Be sure to test your compiler on many small example programs to make sure it runs correctly before you debug the correctness on the Fibonacci example.