Skip to content

Commit

Permalink
#18 Compute instr and var
Browse files Browse the repository at this point in the history
  • Loading branch information
alexcere committed Dec 6, 2024
1 parent d597670 commit 27842a5
Showing 1 changed file with 218 additions and 26 deletions.
244 changes: 218 additions & 26 deletions src/greedy/greedy_new_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import itertools
import json
import logging
import resource
import sys
import os
from typing import List, Dict, Tuple, Any, Set, Optional, Generator
Expand Down Expand Up @@ -73,7 +74,7 @@ def cheap(instr: instr_JSON_T) -> bool:
"""
Cheap computations are those who take one instruction (i.e. inpt_sk is empty)
"""
return len(instr['inpt_sk']) == 0 and instr["inpt_sk"] <= 2
return len(instr['inpt_sk']) == 0 and instr["size"] <= 2


class SymbolicState:
Expand All @@ -99,7 +100,7 @@ def _computer_var_uses(self):

return var_uses

def swap(self, x: int) -> None:
def swap(self, x: int) -> List[instr_id_T]:
"""
Stores the top of the stack in the local with index x. in_position marks whether the element is
solved in flocals
Expand All @@ -108,8 +109,9 @@ def swap(self, x: int) -> None:
self.stack[0], self.stack[x] = self.stack[x], self.stack[0]

# Var uses: no modification, as we are just moving two elements
return [f"SWAP{x}"]

def dup(self, x: int) -> None:
def dup(self, x: int) -> List[instr_id_T]:
"""
Tee instruction in local with index x. in_position marks whether the element is solved in flocals
"""
Expand All @@ -118,17 +120,19 @@ def dup(self, x: int) -> None:

# Var uses: we increment the element that we have in its corresponding position
self.var_uses[self.stack[0]] += 1
return [f"DUP{x}"]

def pop(self):
def pop(self) -> List[instr_id_T]:
"""
Drops the last element
"""
stack_var = self.stack.pop(0)

# Var uses: we subtract one because the stack var is totally removed from the encoding
self.var_uses[stack_var] -= 1
return ["POP"]

def uf(self, instr: instr_JSON_T):
def uf(self, instr: instr_JSON_T) -> List[instr_id_T]:
"""
Symbolic execution of instruction instr. Additionally, checks the arguments match if debug mode flag is enabled
"""
Expand All @@ -153,22 +157,65 @@ def uf(self, instr: instr_JSON_T):
# Var uses: increase one for each generated stack var
self.var_uses[output_var] += 1

return instr['outpt_sk']
return [instr["id"]]

def from_memory(self, var_elem: var_id_T) -> List[instr_id_T]:
"""
Assumes the value is retrieved from memory
"""
self.stack.insert(0, var_elem)

# Var uses: increase one for the variable
self.var_uses[var_elem] += 1

return [f"MEM({var_elem})"]

def top_stack(self) -> Optional[var_id_T]:
return None if len(self.stack) == 0 else self.stack[0]

def is_accessible_swap(self, var_elem: var_id_T) -> bool:
"""
Checks whether the variable element can be accessed for a swap instruction
"""
return self.stack.index(var_elem) <= STACK_DEPTH

def is_accessible_dup(self, var_elem: var_id_T) -> bool:
"""
Checks whether the variable element can be accessed for a dup instruction
"""
return self.stack.index(var_elem) < STACK_DEPTH

def first_occurrence(self, var_elem: var_id_T) -> int:
"""
Returns the first position in which the element appears
"""
try:
return self.stack.index(var_elem)
except:
return -1

def last_accessible_occurrence(self, var_elem: var_id_T) -> int:
"""
Returns the first position in which the element appears
"""
try:
return self.stack.index(var_elem)
except:
return -1

def __repr__(self):
return str(self.stack)


class SMSgreedy:

def __init__(self, json_format: SMS_T):
self.debug_mode: bool = True
# How many elements are placed in the correct position and cannot be moved further in a computation
self.fixed_elements: int = 0
self._user_instr: List[instr_JSON_T] = json_format['user_instrs']
self._initial_stack: List[var_id_T] = json_format['src_ws']
self._final_stack: List[var_id_T] = json_format['tgt_ws']
self._vars: List[var_id_T] = json_format['vars']
self._deps: List[Tuple[var_id_T, var_id_T]] = json_format['dependencies']
self.debug_logger = DebugLogger()

Expand Down Expand Up @@ -344,14 +391,14 @@ def greedy(self) -> List[instr_id_T]:
# operation and there are no operations left
while True:
var_top = cstate.top_stack()
self.fixed_elements = 0

self.debug_logger.debug_loop(dep_graph, optg, cstate)

# Case 1: Top of the stack must be removed, as it appears more time it is being used
if var_top is not None and cstate.var_uses[var_top] > self._var_total_uses[var_top]:
self.debug_logger.debug_pop(var_top, cstate)
cstate.pop()
optg.append("POP")
optg.extend(cstate.pop())

# Case 2: Top of the stack must be placed in some other position
elif var_top is not None and self.var_must_be_moved(var_top, cstate):
Expand All @@ -368,19 +415,11 @@ def greedy(self) -> List[instr_id_T]:
next_instr = self._id2instr[next_id]
self.debug_logger.debug_choose_computation(next_id, cstate)

if cheap(next_instr):
# Cheap instructions are just computed, there is no need to erase elements from the lists
ops = self.compute_instr(next_instr, cstate)
else:
next_instr = self._id2instr[next_id]
# After choosing a value that must be computed, we store intermediate values that are used in locals
ops = self.store_needed_value(next_instr, cstate)
self._debug_store_intermediate(next_id, ops)
terms_to_dup = self._identify_subterms_to_dup(next_instr, cstate)
ops = self.compute_instr(next_instr, cstate, terms_to_dup)

other_ops = self.compute_instr(next_instr, cstate)
self._debug_compute_instr(next_id, other_ops)
ops.extend(other_ops)
dep_graph.remove_node(next_id)
# Cheap instructions are just computed, there is no need to erase elements from the lists
dep_graph.remove_node(next_id)

optg.extend(ops)

Expand All @@ -389,7 +428,7 @@ def greedy(self) -> List[instr_id_T]:

return optg

def _available_positions(self, var_elem: var_id_T, cstate: SymbolicState) -> Generator[cstack_pos_T]:
def _available_positions(self, var_elem: var_id_T, cstate: SymbolicState) -> Generator[cstack_pos_T, None, None]:
"""
Generator for the set of available positions in cstack where the var element can be placed
"""
Expand Down Expand Up @@ -419,15 +458,20 @@ def var_must_be_moved(self, var_elem: var_id_T, cstate: SymbolicState) -> bool:

def move_top_to_position(self, var_elem: var_id_T, cstate: SymbolicState) -> List[instr_id_T]:
"""
Stores the current element in all the positions in which it is available to be moved.
Stores the current element in all the deepest available position.
"""
# TODO: decide if we just want to move one element or duplicate it as many
# times as it is possible
# We just need to retrieve the deepest element
deepest_position_available = next(self._available_positions(var_elem, cstate))
deepest_position_available = next(self._available_positions(var_elem, cstate), None)

assert deepest_position_available is not None, f"There is no available position to move {var_elem}"

cstate.swap(deepest_position_available)
return [f"SWAP{deepest_position_available}"]
# There is no need to move the element to other position
if deepest_position_available == 0:
return []

return cstate.swap(deepest_position_available)

def choose_next_computation(self, cstate: SymbolicState, dep_graph: networkx.DiGraph) -> instr_id_T:
"""
Expand Down Expand Up @@ -485,6 +529,133 @@ def _le_ranked_options(self, option1: Tuple[bool, Dict[var_id_T, int]], option2:
opt2_deepest = max(option2[1].values(), default=-1)
return opt1_deepest <= opt2_deepest

def compute_instr(self, instr: instr_JSON_T, cstate: SymbolicState, terms_to_dup: List[var_id_T]) -> List[instr_id_T]:
"""
Given an instr, the current state and the terms that need to be duplicated, computes the corresponding term.
This function is separated from compute_op because there
are terms, such as var accesses or memory accesses that produce no var element as a result.
"""
seq = []

# First, we compute the subterms to dup
for term in terms_to_dup:
# TODO: decide order in terms to dup
seq.extend(self.compute_var(term, cstate))

# Decide in which order computations must be done (after computing the subterms)
input_vars = self._computation_order(instr, cstate)
first_element = True

for stack_var in input_vars:
top_elem = cstate.top_stack()
if first_element and top_elem is not None and top_elem == stack_var:
if cstate.var_uses[top_elem] < self._var_total_uses[top_elem]:
top_instr = self._var2instr.get(top_elem, None)

# Only storee it in a local if needed
if not cheap(top_instr):
seq.extend(self.move_top_to_position(top_elem, cstate))
else:
seq.extend(self.compute_var(top_elem, cstate))

else:
# Otherwise, we must return generate it with a recursive call
seq.extend(self.compute_var(stack_var, cstate))

first_element = False

# Finally, we compute the element
seq.extend(cstate.uf(instr))

# Update the number of fixed elements afterwards
self.fixed_elements -= len(instr['inpt_sk'])
self.fixed_elements += len(instr['outpt_sk'])
return seq

def _computation_order(self, instr: instr_JSON_T, cstate: SymbolicState) -> List[var_id_T]:
"""
Decides in which order the arguments of the instruction must be computed
"""
if instr['commutative']:
# If it's commutative, study its dependencies.
if self.debug_mode:
assert len(instr['inpt_sk']) == 2, \
f'Commutative instruction {instr["id"]} has arity != 2'

# Condition: the top of the stack can be reused
topmost_element = cstate.top_stack()
first_arg_instr = self._var2instr.get(instr['inpt_sk'][0], None)

# Condition: the topmost element can be reused by the first argument instruction or is the first argument
if (topmost_element is not None and topmost_element in self._top_can_be_used[instr["id"]] and
first_arg_instr is not None and
(first_arg_instr["outpt_sk"][0] == topmost_element or
topmost_element in self._top_can_be_used[first_arg_instr["id"]])):
input_vars = instr['inpt_sk']
else:
input_vars = list(reversed(instr['inpt_sk']))
else:
input_vars = list(reversed(instr['inpt_sk']))
return input_vars

def _identify_subterms_to_dup(self, instr: instr_JSON_T, cstate: SymbolicState) -> List[var_id_T]:
"""
First we identify which subterms must be duplicated in order to compute them first
"""
# We return the variables that are not needed twice
return [stack_var for stack_var in self._values_used[instr["id"]]
if cstate.var_uses[stack_var] < self._var_total_uses[stack_var] - 1 and
(((associated_instr := self._var2instr.get(stack_var, None)) is None) or not cheap(associated_instr))]

def compute_var(self, var_elem: var_id_T, cstate: SymbolicState) -> List[instr_id_T]:
"""
Given a stack_var and current state, computes the element and updates cstate accordingly. Returns the sequence of ids.
Compute var considers it the var elem is already stored in the stack
"""
# First case: the element has not been computed previously. We have to compute it, as it
# corresponds to a stack variable
if cstate.var_uses[var_elem] == 0:
instr = self._var2instr[var_elem]
seq = self.compute_instr(instr, cstate, [])

# Second case: the variable has already been computed (i.e. var_uses > 0).
# In this case, we duplicate it or retrieve it from memory
else:

# If the instruction is cheap, we compute it again
instr = self._var2instr.get(var_elem, None)
if instr is not None and cheap(instr):
seq = self.compute_instr(instr, cstate, [])
else:
assert var_elem in cstate.stack, f"Variable {var_elem} must appear in the stack, " \
f"as it was previously computed and it is not a cheap computation"
# TODO: case for recomputing the element?
# Case I: We swap the element the number of copies required is met, the position is accessible
# and there is no fixed stack elements
if cstate.is_accessible_swap(var_elem) and cstate.var_uses[var_elem] == self._var_total_uses[var_elem] \
and self.fixed_elements == 0:
# We swap to the deepest accesible copy
idx = cstate.last_accessible_occurrence(var_elem) + 1
seq = cstate.swap(idx)

# Case II: we duplicate the element that is within reach
elif cstate.is_accessible_dup(var_elem):
idx = cstate.first_occurrence(var_elem) + 1
seq = cstate.dup(idx)

# Case III: we retrieve the element from memory
else:
seq = cstate.from_memory(var_elem)

return seq

def solve_permutation(self, cstate: SymbolicState) -> List[instr_id_T]:
"""
Places all the elements in their corresponding positions
"""
# TODO: complete code
return []


class DebugLogger:

Expand Down Expand Up @@ -532,3 +703,24 @@ def debug_after_permutation(self, cstate: SymbolicState, optg: List[instr_id_T])
self._logger.debug(cstate)
self._logger.debug(optg)
self._logger.debug("")


def greedy_standalone(sms: Dict) -> Tuple[str, float, List[str]]:
"""
Executes the greedy algorithm as a standalone configuration. Returns whether the execution has been
sucessful or not ("non_optimal" or "error"), the total time and the sequence of ids returned.
"""
error = 0
usage_start = resource.getrusage(resource.RUSAGE_SELF)
try:
seq_ids = SMSgreedy(sms).greedy()
usage_stop = resource.getrusage(resource.RUSAGE_SELF)
except Exception as e:
usage_stop = resource.getrusage(resource.RUSAGE_SELF)
_, _, tb = sys.exc_info()
traceback.print_tb(tb)
error = 1
seq_ids = []
optimization_outcome = "error" if error == 1 else "non_optimal"
print(seq_ids)
return optimization_outcome, usage_stop.ru_utime + usage_stop.ru_stime - usage_start.ru_utime - usage_start.ru_stime, seq_ids

0 comments on commit 27842a5

Please sign in to comment.