def __init__(self, cfg):
     super(AbstractTypesAnalysis, self).__init__()
     
     self._cfg = cfg
             
     self._numeric_rel = NumericRelation(self._cfg)
     self._string_rel = StringRelation(self._cfg)
     self._bool_rel = BoolRelation(self._cfg)
     
     self._possible_type_relations = [self._numeric_rel,
                                     self._string_rel,
                                     self._bool_rel]
class AbstractTypesAnalysis(object):
    def __init__(self, cfg):
        super(AbstractTypesAnalysis, self).__init__()
        
        self._cfg = cfg
                
        self._numeric_rel = NumericRelation(self._cfg)
        self._string_rel = StringRelation(self._cfg)
        self._bool_rel = BoolRelation(self._cfg)
        
        self._possible_type_relations = [self._numeric_rel,
                                        self._string_rel,
                                        self._bool_rel]
            
    def lattice_bottom(self):
        return AbstractType(formula=False)
    
    def _operation_type_implies(self, concrete_transformation, ret_var, ret_type, operands_with_types):
        return z3.Implies(z3.And(*(type_relation.has_type_of_formula(operand_expr, concrete_transformation.current_location())
                                for (operand_expr, type_relation) in operands_with_types)),
                          ret_type.has_type_of_formula(ret_var, concrete_transformation.next_location()))

    def _type_propagated_formula(self, expression_propagated_from, current_var_name, concrete_transformation):
        return z3.And(*[self._operation_type_implies(concrete_transformation, current_var_name, type_rel,
                                                    [(expression_propagated_from, type_rel)])
                        for type_rel in self._possible_type_relations])

    def _type_preserved_for_variables(self, concrete_transformation, variables_names_unchanged):
        all_types_preserved_formula = True
        for var_name in variables_names_unchanged: 
            all_types_preserved_formula = z3.And(all_types_preserved_formula, 
                                                self._type_propagated_formula(var_name, var_name, concrete_transformation))
        return all_types_preserved_formula
        
    def _no_change(self, concrete_transformation, abstract_type):
        return AbstractType(self._type_preserved_for_variables(concrete_transformation, 
                                                               self._cfg.program_vars()))
    
    def _assign(self, concrete_transformation, abstract_type):
        assigned_to_var_name = concrete_transformation.current_location().instruction().gens[0]
        expression_assigned = concrete_transformation.current_location().instruction().uses[0]
        
        change_to_assigned = self._type_propagated_formula(expression_assigned, assigned_to_var_name, concrete_transformation)
        
        all_vars_except_changed = self._cfg.program_vars()
        all_vars_except_changed.remove(assigned_to_var_name)
        
        formula = z3.And(self._type_preserved_for_variables(concrete_transformation, all_vars_except_changed),
                         change_to_assigned)
        return AbstractType(formula)
    
    def _function_call_typing(self, concrete_transformation, ret_var, function_name, operands):
        all_string_implies_string = self._operation_type_implies(concrete_transformation, 
            ret_var, self._string_rel, 
            [(operand, self._string_rel) for operand in operands])
        
        all_numeric_implies_numeric = self._operation_type_implies(concrete_transformation, 
            ret_var, self._numeric_rel, 
            [(operand, self._numeric_rel) for operand in operands])
        
        all_boolean_implies_boolean = self._operation_type_implies(concrete_transformation, 
            ret_var, self._bool_rel, 
            [(operand, self._bool_rel) for operand in operands])
        
        if function_name in ['**', '*', '//', '/', '%', '-', '<<', '>>']:
            return all_numeric_implies_numeric
        if function_name == '+':
            return z3.Or(all_numeric_implies_numeric, all_string_implies_string)
        if function_name in ['&', '|', '^']:
            # TODO: not support implicit conversion to boolean
            return all_boolean_implies_boolean
        
        if function_name == 'not':
            return all_boolean_implies_boolean
        else:
            assert False, "Unknown function call"
            
    def _binary(self, concrete_transformation, abstract_type):
        instruction = concrete_transformation.current_location().instruction()
        op = instruction.op
        (assigned_to_var,) = instruction.gens
        (operand1, operand2) = instruction.uses
        return AbstractType(self._function_call_typing(concrete_transformation, assigned_to_var, op, [operand1, operand2]))
    
    def _call(self, concrete_transformation, abstract_type):
        instruction = concrete_transformation.current_location().instruction()
        args = instruction.uses[1:]  # TODO: add kargs (see TAC)
        ret_var = instruction.gens[0]
        function_name = instruction.func
        return AbstractType(self._function_call_typing(concrete_transformation, ret_var, function_name, args))

    def transform(self, concrete_transformation, abstract_type):
        instruction = concrete_transformation.current_location().instruction()
        op = instruction.opcode
        if op == tac.OP.NOP:
            return self._no_change(concrete_transformation, abstract_type)
        elif op == tac.OP.ASSIGN:
            return self._assign(concrete_transformation, abstract_type)
        elif op == tac.OP.IMPORT:
            return self._no_change(concrete_transformation, abstract_type)
        elif op == tac.OP.BINARY:
            return self._binary(concrete_transformation, abstract_type)
        elif op == tac.OP.INPLACE:
            assert False
        elif op == tac.OP.CALL:
            return self._call(concrete_transformation, abstract_type)
        elif op == tac.OP.JUMP:
            return self._no_change(concrete_transformation, abstract_type)
        elif op == tac.OP.FOR:
            assert False
        elif op == tac.OP.RET:
            assert False, "Function calls not supported"
        elif op == tac.OP.DEL:
            return self._no_change(concrete_transformation, abstract_type)
        else:
            assert False, "Unknown Three Address Code instruction in %s" % str(instruction) 
            
    def equiv_type(self, abstract_type1, abstract_type2):
        # Note: In the current implementation this has little use,
        # since in the chaotic iterations we really need to traverse each edge just once
        if z3_utils.sat_formula(z3.And(abstract_type1.formula(), z3.Not(abstract_type2.formula()))):
            return False
        if z3_utils.sat_formula(z3.And(abstract_type2.formula(), z3.Not(abstract_type1.formula()))):
            return False
        return True
        
    def join(self, abstract_type1, abstract_type2):
        return AbstractType(z3.Or(abstract_type1.formula(), abstract_type2.formula()))
    
    def _binary_op_constraint(self, program_location):
        op = program_location.instruction().op
        operands = program_location.instruction().uses
        
        all_operands_numeric_formula = z3.And(*(self._numeric_rel.has_type_of_formula(operand, program_location)
                                                for operand in operands))
        all_operands_string_formula = z3.And(*(self._string_rel.has_type_of_formula(operand, program_location)
                                                for operand in operands))
        all_operands_bool_formula = z3.And(*(self._bool_rel.has_type_of_formula(operand, program_location)
                                                for operand in operands))
        if op in ['**', '*', '//', '/', '%', '-', '<<', '>>']:
            return all_operands_numeric_formula
        elif op == '+':
            return z3.Or(all_operands_numeric_formula, all_operands_string_formula)
        elif op in ['&', '|', '^']: 
            return all_operands_bool_formula
        else:
            assert False, "Unknown type safety: op %s of instruction %s" % (op, program_location)
            
    def _call_constraints(self, program_location):
        instruction = program_location.instruction()
        function_name = instruction.func
        args = instruction.uses[1:]  # TODO: add kargs (see TAC)
        
        all_args_bool_formula = z3.And(*(self._bool_rel.has_type_of_formula(operand, program_location)
                                                for operand in args))
        
        if function_name == 'not':
            print(all_args_bool_formula)
            return all_args_bool_formula
        assert False, "Unknown function call for type constraints"
    
    def generate_safety_constraints(self, program_location):
        instruction = program_location.instruction()
        opcode = instruction.opcode
        if opcode == tac.OP.NOP:
            return True
        elif opcode == tac.OP.ASSIGN:
            return True
        elif opcode == tac.OP.IMPORT:
            return True
        elif opcode == tac.OP.BINARY:
            return self._binary_op_constraint(program_location)
        elif opcode == tac.OP.INPLACE:
            assert False
        elif opcode == tac.OP.CALL:
            return self._call_constraints(program_location)
        elif opcode == tac.OP.JUMP:
            # TODO: not supporting implicit conversion to bool
            return self._bool_rel.has_type_of_formula(program_location.instruction().uses[0], program_location)
        elif opcode == tac.OP.FOR:
            assert False
        elif opcode == tac.OP.RET:
            if instruction.uses[0] == 'None':
                return True
            assert False, "Function calls not supported %s" % str(instruction)
        elif opcode == tac.OP.DEL:
            return True
        else:
            assert False, "Unknown Three Address Code instruction in %s" % str(instruction) 
            
    def get_types_exclusion_theory(self):
        types_exclusion_theory = set()
        for program_location in self._cfg.all_locations():
            for var_name in self._cfg.program_vars():
                # TODO: should be more fine-grained when we support subtyping
                for possible_type1, possible_type2 in zip(self._possible_type_relations, self._possible_type_relations):
                    if possible_type1 != possible_type2:
                        exclusion_statement = z3.Not(z3.And(possible_type1.has_type_of_formula(var_name, program_location),
                                                            possible_type1.has_type_of_formula(var_name, program_location)))
                        types_exclusion_theory.add(exclusion_statement)
        return types_exclusion_theory