diff --git a/driver.py b/driver.py new file mode 100644 index 0000000..af6fdc0 --- /dev/null +++ b/driver.py @@ -0,0 +1,22 @@ +import sys +from programs import * + +if __name__ == "__main__": + lines = sys.stdin.readlines() + option = lines[0].strip() + if option == "test_min": + print(test_min(int(lines[1]), int(lines[2]))) + elif option == "test_min3": + print(test_min3(int(lines[1]), int(lines[2]), int(lines[3]))) + elif option == "test_div": + print(test_div(int(lines[1]), int(lines[2]))) + elif option == "fact": + print(test_fact(int(lines[1]))) + elif option == "fib": + print(test_fib(int(lines[1]))) + elif option == "fib_swap_problem": + print(test_fib_swap_problem(int(lines[1]))) + elif option == "test_fib_swap_problem_fixed_with_phi_blocks": + print(test_fib_swap_problem_fixed_with_phi_blocks(int(lines[1]))) + else: + print("Invalid option: {option}") \ No newline at end of file diff --git a/lang.py b/lang.py new file mode 100644 index 0000000..b3964a9 --- /dev/null +++ b/lang.py @@ -0,0 +1,512 @@ +""" +This file contains the implementation of a simple interpreter of low-level +instructions. The interpreter takes a program, represented as its first +instruction, plus an environment, which is a stack of bindings. Bindings are +pairs of variable names and values. New bindings are added to the stack +whenever new variables are defined. Bindings are never removed from the stack. +In this way, we can inspect the history of state transformations caused by the +interpretation of a program. The difference between this file and the files of +same name in the previous lab is the presence of phi-functions. In other words, +this new language contains two extra instructions: phi-functions and phi-blocks. +The latter represents the set of phi-functions that exist at the beginning of +a basic block. + +This file uses doctests all over. To test it, just run python 3 as follows: +"python3 -m doctest main.py". The program uses syntax that is excluive of +Python 3. It will not work with standard Python 2. +""" + +from collections import deque +from abc import ABC, abstractmethod + + +class Env: + """ + A table that associates variables with values. The environment is + implemented as a stack, so that previous bindings of a variable V remain + available in the environment if V is overassigned. + + Example: + >>> e = Env() + >>> e.set("a", 2) + >>> e.set("a", 3) + >>> e.get("a") + 3 + + >>> e = Env({"b": 5}) + >>> e.set("a", 2) + >>> e.get("a") + e.get("b") + 7 + """ + + def __init__(s, initial_args={}): + s.env = deque() + for var, value in initial_args.items(): + s.env.appendleft((var, value)) + + def get(self, var): + """ + Finds the first occurrence of variable 'var' in the environment stack, + and returns the value associated with it. + """ + val = next((value for (e_var, value) in self.env if e_var == var), None) + if val is not None: + return val + else: + raise LookupError(f"Absent key {var}") + + def get_from_list(self, vars): + """ + Finds the first occurrence of any variable 'vr' in the list 'vars' that + has a binding in the environment, and returns the associated value. + + Example: + >>> e = Env() + >>> e.set("b", 1) + >>> e.set("a", 2) + >>> e.set("b", 3) + >>> e.get_from_list(["b", "a"]) + 3 + + >>> e = Env() + >>> e.set("b", 1) + >>> e.set("a", 2) + >>> e.set("b", 3) + >>> e.set("a", 4) + >>> e.get_from_list(["b", "a"]) + 4 + """ + # TODO: Implement this method + return 0 + + def set(s, var, value): + """ + This method adds 'var' to the environment, by placing the binding + '(var, value)' onto the top of the environment stack. + """ + s.env.appendleft((var, value)) + + def dump(s): + """ + Prints the contents of the environment. This method is mostly used for + debugging purposes. + """ + for var, value in s.env: + print(f"{var}: {value}") + + +class Inst(ABC): + """ + The representation of instructions. All that an instruction has, that is + common among all the instructions, is the next_inst attribute. This + attribute determines the next instruction that will be fetched after this + instruction runs. Also, every instruction has an index, which is always + different. The index is incremented whenever a new instruction is created. + """ + + next_index = 0 + + def __init__(self): + self.nexts = [] + self.preds = [] + self.ID = Inst.next_index + Inst.next_index += 1 + + def add_next(self, next_inst): + self.nexts.append(next_inst) + next_inst.preds.append(self) + + @classmethod + @abstractmethod + def definition(self): + raise NotImplementedError + + @classmethod + @abstractmethod + def uses(self): + raise NotImplementedError + + def get_next(self): + if len(self.nexts) > 0: + return self.nexts[0] + else: + return None + + +class Phi(Inst): + """ + A Phi-Function is an abstract notation used to facilitate the implementation + of static analyses. They were not really conceived to have a dynamic + semantics. Nevertheless, we can still interpret programs containing + phi-functions. A possible semantics of 'a = phi(a0, a1, a2)' is to + recover, from the environment, the first binding of either a0, a1 or a2. + If our program were in the so-called "Conventional-SSA Form", this + semantics would be perfect. But our program is not in such a format, and + we might have issues with swaps, for instance. That's why we shall use + phi-blocks to implement phi-functions. All the same, you can still write + programs using phi-functions without using phi-blocks, as long as variables + that are related by phi-functions do not have overlapping live ranges. + + Example: + >>> a = Phi("a", ["b0", "b1", "b2"]) + >>> e = Env() + >>> e.set("b0", 1) + >>> e.set("b1", 3) + >>> a.eval(e) + >>> e.get("a") + 3 + + >>> a = Phi("a", ["b0", "b1"]) + >>> e = Env() + >>> e.set("b1", 3) + >>> e.set("b0", 1) + >>> a.eval(e) + >>> e.get("a") + 1 + """ + + def __init__(s, dst, args): + s.dst = dst + s.args = args + super().__init__() + + def definition(s): + return s.dst + + def uses(s): + return s.args + + def eval(s, env): + """ + If the program were in Conventional-SSA form, then we could correctly + implement the semantics of phi-functions simply retrieving the first + occurrence of each variable in the list of uses. However, notice what + would happen with swaps: + + >>> a0 = Phi("a0", ["a1", "a0"]) + >>> a1 = Phi("a1", ["a0", "a1"]) + >>> e = Env() + >>> e.set("a0", 1) + >>> e.set("a1", 3) + >>> a0.eval(e) + >>> a1.eval(e) + >>> e.get("a0") - e.get("a1") + 0 + + In the example above, we would like to evaluate the two phi-functions in + parallel, e.g.: (a0, a1) = (a0:1, a1:3). In this way, after the + evaluation, we would like to have a0 == 3 and a1 == 1. However, there is + no way we can do it: our phi-functions are evaluated once at a time! The + problem is that variables a0 and a1 are defined by different + phi-functions, but they have overlapping live ranges. So, this + program is not in conventional SSA-form (as per Definition 1 in the + paper 'SSA Elimination after Register Allocation' - 2009). + """ + env.set(s.dst, env.get_from_list(s.uses())) + + def __str__(self): + use_list = ", ".join(self.uses()) + inst_s = f"{self.ID}: {self.dst} = phi[{use_list}]" + pred_s = f"\n P: {', '.join([str(inst.ID) for inst in self.preds])}" + next_s = f"\n N: {self.nexts[0].ID if len(self.nexts) > 0 else ''}" + return inst_s + pred_s + next_s + + +class PhiBlock(Inst): + """ + PhiBlocks implement a correct semantics for groups of phi-functions. A + phi-block groups a number of phi-functions as a matrix. Once a phi-block + is evaluated, all the values in a given column of this matrix are read and + saved, and then the definitions are updated --- all in parallel. To see a + more detailed explanation of this semantics, please, refer to Section 3 of + the paper 'SSA Elimination after Register Allocation'. In particular, take + a look into Figure 1 of that paper. + + Example: + >>> a0 = Phi("a0", ["a0", "a1"]) + >>> a1 = Phi("a1", ["a1", "a0"]) + >>> aa = PhiBlock([a0, a1], [10, 31]) + >>> e = Env() + >>> e.set("a0", 1) + >>> e.set("a1", 3) + >>> aa.eval(e, 10) + >>> e.get("a0") - e.get("a1") + -2 + + >>> a0 = Phi("a0", ["a0", "a1"]) + >>> a1 = Phi("a1", ["a1", "a0"]) + >>> aa = PhiBlock([a0, a1], [10, 31]) + >>> e = Env() + >>> e.set("a0", 1) + >>> e.set("a1", 3) + >>> aa.eval(e, 31) + >>> e.get("a0") - e.get("a1") + 2 + """ + + def __init__(self, phis, selector_IDs): + """ + A phi-block represents an M*N matrix, where each one of the M lines is + a phi-function, and each phi-function reads from N different parameters. + Each one of these N columns is associated with a 'selector', which is + the ID of the instruction that leads to that parallel assignment. + + Examples: + >>> a0 = Phi("a0", ["a0", "a1"]) + >>> a1 = Phi("a1", ["a1", "a0"]) + >>> aa = PhiBlock([a0, a1], [10, 31]) + >>> sorted(aa.selectors.items()) + [(10, 0), (31, 1)] + + >>> a0 = Phi("a0", ["a0", "a1"]) + >>> a1 = Phi("a1", ["a1", "a0"]) + >>> aa = PhiBlock([a0, a1], [10, 31]) + >>> sorted([phi.definition() for phi in aa.phis]) + ['a0', 'a1'] + """ + self.phis = phis + # TODO: implement the rest of this method + # here... + ######################################### + super().__init__() + + def definition(self): + """ + We consider that a phi-block defines multiple variables. These are the + variables assignment by the phi-functions that the phi-block contains. + + Example: + >>> a0 = Phi("a0", ["a0", "a1"]) + >>> a1 = Phi("a1", ["a1", "a0"]) + >>> aa = PhiBlock([a0, a1], [10, 31]) + >>> sorted(aa.definition()) + ['a0', 'a1'] + """ + return [phi.definition() for phi in self.phis] + + def uses(self): + """ + The uses of a phi-block are all the variables used by the phi-functions + that it contains. Notice that we don't need this method for anything; it + is here rather to help understand the structure of phi-blocks. + + Example: + >>> a0 = Phi("a0", ["a0", "x"]) + >>> a1 = Phi("a1", ["y", "a0"]) + >>> aa = PhiBlock([a0, a1], [10, 31]) + >>> sorted(aa.uses()) + ['a0', 'a0', 'x', 'y'] + """ + return sum([phi.uses() for phi in self.phis], []) + + def eval(self, env, PC): + # TODO: Read all the definitions + # TODO: Assign all the uses: + + def __str__(self): + block_str = "\n".join([str(phi) for phi in self.phis]) + return f"PHI_BLOCK [\n{block_str}\n]" + + +class BinOp(Inst): + """ + The general class of binary instructions. These instructions define a + value, and use two values. As such, it contains a routine to extract the + defined value, and the list of used values. + """ + + def __init__(s, dst, src0, src1): + s.dst = dst + s.src0 = src0 + s.src1 = src1 + super().__init__() + + @classmethod + @abstractmethod + def get_opcode(self): + raise NotImplementedError + + def definition(s): + return set([s.dst]) + + def uses(s): + return set([s.src0, s.src1]) + + def __str__(self): + op = self.get_opcode() + inst_s = f"{self.ID}: {self.dst} = {self.src0}{op}{self.src1}" + pred_s = f"\n P: {', '.join([str(inst.ID) for inst in self.preds])}" + next_s = f"\n N: {self.nexts[0].ID if len(self.nexts) > 0 else ''}" + return inst_s + pred_s + next_s + + +class Add(BinOp): + """ + Example: + >>> a = Add("a", "b0", "b1") + >>> e = Env({"b0":2, "b1":3}) + >>> a.eval(e) + >>> e.get("a") + 5 + + >>> a = Add("a", "b0", "b1") + >>> a.get_next() == None + True + """ + + def eval(self, env): + env.set(self.dst, env.get(self.src0) + env.get(self.src1)) + + def get_opcode(self): + return "+" + + +class Mul(BinOp): + """ + Example: + >>> a = Mul("a", "b0", "b1") + >>> e = Env({"b0":2, "b1":3}) + >>> a.eval(e) + >>> e.get("a") + 6 + """ + + def eval(s, env): + env.set(s.dst, env.get(s.src0) * env.get(s.src1)) + + def get_opcode(self): + return "*" + + +class Lth(BinOp): + """ + Example: + >>> a = Lth("a", "b0", "b1") + >>> e = Env({"b0":2, "b1":3}) + >>> a.eval(e) + >>> e.get("a") + True + """ + + def eval(s, env): + env.set(s.dst, env.get(s.src0) < env.get(s.src1)) + + def get_opcode(self): + return "<" + + +class Geq(BinOp): + """ + Example: + >>> a = Geq("a", "b0", "b1") + >>> e = Env({"b0":2, "b1":3}) + >>> a.eval(e) + >>> e.get("a") + False + """ + + def eval(s, env): + env.set(s.dst, env.get(s.src0) >= env.get(s.src1)) + + def get_opcode(self): + return ">=" + + +class Bt(Inst): + """ + This is a Branch-If-True instruction, which diverts the control flow to the + 'true_dst' if the predicate 'pred' is true, and to the 'false_dst' + otherwise. + + Example: + >>> e = Env({"t": True, "x": 0}) + >>> a = Add("x", "x", "x") + >>> m = Mul("x", "x", "x") + >>> b = Bt("t", a, m) + >>> b.eval(e) + >>> b.get_next() == a + True + """ + + def __init__(s, cond, true_dst=None, false_dst=None): + super().__init__() + s.cond = cond + s.nexts = [true_dst, false_dst] + if true_dst != None: + true_dst.preds.append(s) + if false_dst != None: + false_dst.preds.append(s) + + def definition(s): + return set() + + def uses(s): + return set([s.cond]) + + def add_true_next(s, true_dst): + s.nexts[0] = true_dst + true_dst.preds.append(s) + + def add_next(s, false_dst): + s.nexts[1] = false_dst + false_dst.preds.append(s) + + def eval(s, env): + """ + The evaluation of the condition sets the next_iter to the instruction. + This value determines which successor instruction is to be evaluated. + Any values greater than 0 are evaluated as True, while 0 corresponds to + False. + """ + if env.get(s.cond): + s.next_iter = 0 + else: + s.next_iter = 1 + + def get_next(s): + return s.nexts[s.next_iter] + + def __str__(self): + inst_s = f"{self.ID}: bt {self.cond}" + pred_s = f"\n P: {', '.join([str(inst.ID) for inst in self.preds])}" + next_s = f"\n NT:{self.nexts[0].ID} NF:{self.nexts[1].ID}" + return inst_s + pred_s + next_s + + +def interp(instruction, environment, PC=0): + """ + This function evaluates a program until there is no more instructions to + evaluate. Notice that, in contrast to the previous labs, the interpreter + now receives three arguments. The third argument is necessary to implement + the correct semantics of phi-functions using phi-blocks. This argument can + be used to select the correct parallel copy that a PhiBlock implements. + + Parameters: + ----------- + instruction: the instruction that will be interpreted + environment: the list that associates variable names with their values + PC: the identifier of the last instruction that was interpreted. + + Example: + >>> env = Env({"m": 3, "n": 2, "zero": 0}) + >>> m_min = Add("answer", "m", "zero") + >>> n_min = Add("answer", "n", "zero") + >>> p = Lth("p", "n", "m") + >>> b = Bt("p", n_min, m_min) + >>> p.add_next(b) + >>> interp(p, env).get("answer") + 2 + """ + if instruction: + print("----------------------------------------------------------") + print(instruction) + environment.dump() + if isinstance(instruction, PhiBlock): + # TODO: implement this part: + pass + else: + # TODO: implement this part: + pass + return interp(instruction.get_next(), environment, instruction.ID) + else: + return environment \ No newline at end of file diff --git a/programs.py b/programs.py new file mode 100644 index 0000000..9c94a52 --- /dev/null +++ b/programs.py @@ -0,0 +1,212 @@ +from lang import * + + +def print_instructions(instructions): + for inst in instructions: + print(inst) + + +def test_min(m, n): + """ + Stores in the variable 'answer' the minimum of 'm' and 'n' + + Examples: + >>> test_min(3, 4) + 3 + + >>> test_min(4, 3) + 3 + """ + env = Env({"m": m, "n": n, "x0": m, "zero": 0}) + p = Lth("p", "n", "x0") + x1 = Add("x1", "n", "zero") + answer = Phi("answer", ["x0", "x1"]) + b = Bt("p", x1, answer) + p.add_next(b) + x1.add_next(answer) + interp(p, env) + return env.get("answer") + + +def test_min3(x, y, z): + """ + Stores in the variable 'answer' the minimum of 'x', 'y' and 'z' + + Examples: + >>> test_min3(3, 4, 5) + 3 + + >>> test_min3(5, 4, 3) + 3 + """ + env = Env({"min0": x, "y": y, "z": z, "zero": 0}) + p0 = Lth("p0", "y", "min0") + min1 = Add("min1", "y", "zero") + min2 = Phi("min2", ["min1", "min0"]) + p1 = Lth("p1", "z", "min2") + min3 = Add("min3", "z", "zero") + answer = Phi("answer", ["min3", "min2"]) + b0 = Bt("p0", min1, min2) + p0.add_next(b0) + min1.add_next(min2) + min2.add_next(p1) + b1 = Bt("p1", min3, answer) + p1.add_next(b1) + min3.add_next(answer) + interp(p0, env) + return env.get("answer") + + +def test_div(m, n): + """ + Stores in the variable 'answer' the integer division of 'm' and 'n'. + + Examples: + >>> test_div(30, 4) + 7 + + >>> test_div(4, 3) + 1 + + >>> test_div(1, 3) + 0 + """ + env = Env({"d0": 0, "m0": m, "one": 1, "n": n, "minus_n": -n, "zero": 0}) + d1 = Phi("d1", ["d0", "d2"]) + m1 = Phi("m1", ["m0", "m2"]) + p = Geq("p", "m1", "n") + m2 = Add("m2", "m1", "minus_n") + d2 = Add("d2", "d1", "one") + answer = Add("answer", "d1", "zero") + b = Bt("p", m2, answer) + d1.add_next(m1) + m1.add_next(p) + p.add_next(b) + m2.add_next(d2) + d2.add_next(d1) + interp(d1, env) + return env.get("answer") + + +def test_fact(n): + """ + Stores in the variable 'answer' the factorial of 'n'. + + Examples: + >>> test_fact(3) + 6 + """ + env = Env({"two": 2, "n0": n, "f0": 1, "m_one": -1, "zero": 0}) + n1 = Phi("n1", ["n0", "n2"]) + f1 = Phi("f1", ["f0", "f2"]) + p = Geq("p", "n1", "two") + f2 = Mul("f2", "f1", "n1") + n2 = Add("n2", "n1", "m_one") + answer = Add("answer", "f1", "zero") + b = Bt("p", f2, answer) + n1.add_next(f1) + f1.add_next(p) + p.add_next(b) + f2.add_next(n2) + n2.add_next(n1) + interp(n1, env) + return env.get("answer") + + +def test_fib(n): + """ + Stores in the variable 'answer' the n-th number of the Fibonacci sequence, + considering that the sequence is 0, 1, 1, 2, 3, 5, ... + + Examples: + >>> test_fib(2) + 1 + >>> test_fib(3) + 2 + >>> test_fib(6) + 8 + """ + env = Env({"N": n, "zero": 0, "one": 1}) + a = Phi("a", ["zero", "b"]) + b = Phi("b", ["one", "sum"]) + c1 = Phi("c1", ["zero", "c2"]) + p = Lth("p", "c1", "N") + answer = Add("answer", "a", "zero") + sum_ = Add("sum", "a", "b") + c2 = Add("c2", "c1", "one") + b_aux = Add("b_aux", "b", "zero") + branch = Bt("p", sum_, answer) + a.add_next(b) + b.add_next(c1) + c1.add_next(p) + p.add_next(branch) + sum_.add_next(c2) + c2.add_next(b_aux) + b_aux.add_next(a) + interp(a, env) + return env.get("answer") + + +def test_fib_swap_problem(n): + """ + This implementation of the Fibonacci Sequence illustrates the so-called + swap problem. If we do not evaluate the phi-functions in blocks, then we + might get wrong results. + + Examples: + >>> test_fib_swap_problem(2) + 4 + >>> test_fib_swap_problem(3) + 8 + >>> test_fib_swap_problem(6) + 64 + """ + env = Env({"N": n, "zero": 0, "one": 1}) + b = Phi("b", ["one", "sum"]) + a = Phi("a", ["zero", "b"]) + c1 = Phi("c1", ["zero", "c2"]) + p = Lth("p", "c1", "N") + answer = Add("answer", "a", "zero") + sum_ = Add("sum", "a", "b") + c2 = Add("c2", "c1", "one") + branch = Bt("p", sum_, answer) + b.add_next(a) + a.add_next(c1) + c1.add_next(p) + p.add_next(branch) + sum_.add_next(c2) + c2.add_next(b) + interp(b, env) + return env.get("answer") + + +def test_fib_swap_problem_fixed_with_phi_blocks(n): + """ + This implementation of the Fibonacci Sequence illustrates the so-called + swap problem. If we do not evaluate the phi-functions in blocks, then we + might get wrong results. + + Examples: + >>> test_fib_swap_problem_fixed_with_phi_blocks(2) + 1 + >>> test_fib_swap_problem_fixed_with_phi_blocks(3) + 2 + >>> test_fib_swap_problem_fixed_with_phi_blocks(6) + 8 + """ + env = Env({"N": n, "zero": 0, "one": 1}) + b = Phi("b", ["one", "sum"]) + a = Phi("a", ["zero", "b"]) + c1 = Phi("c1", ["zero", "c2"]) + p = Lth("p", "c1", "N") + answer = Add("answer", "a", "zero") + sum_ = Add("sum", "a", "b") + c2 = Add("c2", "c1", "one") + branch = Bt("p", sum_, answer) + phi_block = PhiBlock([b, a, c1], [0, c2.ID]) + phi_block.add_next(p) + p.add_next(branch) + sum_.add_next(c2) + c2.add_next(phi_block) + interp(phi_block, env) + return env.get("answer") \ No newline at end of file