import gcc
from gccutils import get_src_for_loc, pformat, cfg_to_dot, invoke_dot

# Need to add `-fplugin=python.so -fplugin-arg-python-script=tools/check-throw-without-free.py` to CFLAGS in the Makefile

# We'll implement this as a custom pass, to be called directly after the
# builtin 'cfg' pass, which generates the CFG:

def collect_control_flows(bb, path, seen):
    if bb.index in seen:
        seen[bb.index] = 2
    else:
        seen[bb.index] = 1
    paths = []
    new_path = [bb]
    new_path.extend(path)
    if not bb.preds:
        paths.append(new_path)
    for edge in bb.preds:
        pred = edge.src
        if pred.index in seen and seen[pred.index] > 1:
            continue
        paths.extend(collect_control_flows(pred, new_path, seen))
    return paths

#alloc_funcs = {'MVM_string_utf8_c8_encode_C_string'} # currently adding this causes a segfault in gcc when compiling src/io/filewatchers.c
alloc_funcs = {'MVM_malloc', 'MVM_calloc', 'MVM_fixed_size_allocate', 'MVM_fixed_size_allocate_zeroed', 'ANSIToUTF8', 'MVM_bytecode_dump', 'MVM_exception_backtrace_line', 'MVM_nativecall_unmarshal_string', 'MVM_serialization_read_cstr', 'MVM_spesh_dump', 'MVM_spesh_dump_arg_guard', 'MVM_spesh_dump_planned', 'MVM_spesh_dump_stats', 'MVM_staticframe_file_location', 'MVM_string_ascii_encode', 'MVM_string_ascii_encode_any', 'MVM_string_ascii_encode_substr', 'MVM_string_encode', 'MVM_string_gb18030_encode_substr', 'MVM_string_gb2312_encode_substr', 'MVM_string_latin1_encode', 'MVM_string_latin1_encode_substr', 'MVM_string_shiftjis_encode_substr', 'MVM_string_utf16_encode', 'MVM_string_utf16_encode_substr', 'MVM_string_utf16_encode_substr_main', 'MVM_string_utf16be_encode_substr', 'MVM_string_utf16le_encode_substr', 'MVM_string_utf8_c8_encode', 'MVM_string_utf8_c8_encode_substr', 'MVM_string_utf8_encode', 'MVM_string_utf8_encode_C_string', 'MVM_string_utf8_encode_substr', 'MVM_string_utf8_maybe_encode_C_string', 'MVM_string_windows1251_encode_substr', 'MVM_string_windows1252_encode_substr', 'MVM_string_windows125X_encode_substr', 'NFG_check_make_debug_string', 'NFG_checker', 'UnicodeToUTF8', 'UnicodeToUTF8', 'base64_encode', 'callback_handler', 'get_signature_char', 'twoway_memmem_uint32', 'u64toa_naive_worker'}
free_funcs = {'MVM_free', 'MVM_free_null', 'MVM_fixed_size_free', 'MVM_fixed_size_free_at_safepoint', 'free_repr_data', 'cleanup_all', 'MVM_tc_destroy'}

def check_code_for_throw_without_free(fun):
    for bb in fun.cfg.basic_blocks:
        for ins in bb.gimple:
            if isinstance(ins, gcc.GimpleCall) and isinstance(ins.fn, gcc.AddrExpr) and ins.fn.operand.name == 'MVM_exception_throw_adhoc':
                cfs = collect_control_flows(bb, [], {})

                for cf in cfs:
                    allocs = set()
                    for bb in cf:
                        for ins in bb.gimple:
                            if isinstance(ins, gcc.GimpleCall):
                                if isinstance(ins.fn, gcc.AddrExpr): # plain function call is AddrExpr, other things could be function pointers
                                    if ins.fn.operand.name in alloc_funcs:
                                        allocs.add(ins.lhs)
                                    if ins.fn.operand.name in free_funcs:
                                        if ins.args[0] in allocs: # there can be false positives because the lhs of the alloc and the argument to the free can get different temp names from gcc
                                            allocs.remove(ins.args[0])
                                    if ins.fn.operand.name == 'MVM_exception_throw_adhoc':
                                        if allocs:
                                            print('\nPossible missing free before a throw in ' + str(fun.decl.name) + ' at ' + str(cf[-2].gimple[-1].loc) + ', things that might need to be freed: ' + str(allocs) + '\n')

class CheckThrowWithoutFree(gcc.GimplePass):
    def execute(self, fun):
        # (the CFG should be set up by this point, and the GIMPLE is not yet
        # in SSA form)
        if fun and fun.cfg:
            check_code_for_throw_without_free(fun)

ps = CheckThrowWithoutFree(name='check-throw-without-free')
ps.register_after('cfg')
