#   Copyright 2013-2018 Free Software Foundation, Inc.
#
#   This is free software: you can redistribute it and/or modify it
#   under the terms of the GNU General Public License as published by
#   the Free Software Foundation, either version 3 of the License, or
#   (at your option) any later version.
#
#   This program is distributed in the hope that it will be useful, but
#   WITHOUT ANY WARRANTY; without even the implied warranty of
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
#   General Public License for more details.
#
#   You should have received a copy of the GNU General Public License
#   along with this program.  If not, see
#   <http://www.gnu.org/licenses/>.

import gcc
import gccutils
import sys

want_raii_info = False

logging = False
show_cfg = False

def log(msg, indent=0):
    global logging
    if logging:
        sys.stderr.write('%s%s\n' % ('  ' * indent, msg))
        sys.stderr.flush()

def is_cleanup_type(return_type):
    if not isinstance(return_type, gcc.PointerType):
        return False
    if not isinstance(return_type.dereference, gcc.RecordType):
        return False
    if str(return_type.dereference.name) == 'cleanup':
        return True
    return False

def is_constructor(decl):
    "Return True if the function DECL is a cleanup constructor; False otherwise"
    return is_cleanup_type(decl.type.type) and (not decl.name or str(decl.name) != 'make_final_cleanup')

destructor_names = set(['do_cleanups', 'discard_cleanups'])

def is_destructor(decl):
    return decl.name in destructor_names

# This list is just much too long... we should probably have an
# attribute instead.
special_names = set(['do_final_cleanups', 'discard_final_cleanups',
                     'save_cleanups', 'save_final_cleanups',
                     'restore_cleanups', 'restore_final_cleanups',
                     'exceptions_state_mc_init',
                     'make_my_cleanup2', 'make_final_cleanup', 'all_cleanups',
                     'save_my_cleanups', 'quit_target'])

def needs_special_treatment(decl):
    return decl.name in special_names

# Sometimes we need a new placeholder object that isn't the same as
# anything else.
class Dummy(object):
    def __init__(self, location):
        self.location = location

# A wrapper for a cleanup which has been assigned to a variable.
# This holds the variable and the location.
class Cleanup(object):
    def __init__(self, var, location):
        self.var = var
        self.location = location

# A class representing a master cleanup.  This holds a stack of
# cleanup objects and supports a merging operation.
class MasterCleanup(object):
    # Create a new MasterCleanup object.  OTHER, if given, is a
    # MasterCleanup object to copy.
    def __init__(self, other = None):
        # 'cleanups' is a list of cleanups.  Each element is either a
        # Dummy, for an anonymous cleanup, or a Cleanup, for a cleanup
        # which was assigned to a variable.
        if other is None:
            self.cleanups = []
            self.aliases = {}
        else:
            self.cleanups = other.cleanups[:]
            self.aliases = dict(other.aliases)

    def compare_vars(self, definition, argument):
        if definition == argument:
            return True
        if argument in self.aliases:
            argument = self.aliases[argument]
        if definition in self.aliases:
            definition = self.aliases[definition]
        return definition == argument

    def note_assignment(self, lhs, rhs):
        log('noting assignment %s = %s' % (lhs, rhs), 4)
        self.aliases[lhs] = rhs

    # Merge with another MasterCleanup.
    # Returns True if this resulted in a change to our state.
    def merge(self, other):
        # We do explicit iteration like this so we can easily
        # update the list after the loop.
        counter = -1
        found_named = False
        for counter in range(len(self.cleanups) - 1, -1, -1):
            var = self.cleanups[counter]
            log('merge checking %s' % var, 4)
            # Only interested in named cleanups.
            if isinstance(var, Dummy):
                log('=> merge dummy', 5)
                continue
            # Now see if VAR is found in OTHER.
            if other._find_var(var.var) >= 0:
                log ('=> merge found', 5)
                break
            log('=>merge not found', 5)
            found_named = True
        if found_named and counter < len(self.cleanups) - 1:
            log ('merging to %d' % counter, 4)
            if counter < 0:
                self.cleanups = []
            else:
                self.cleanups = self.cleanups[0:counter]
            return True
        # If SELF is empty but OTHER has some cleanups, then consider
        # that a change as well.
        if len(self.cleanups) == 0 and len(other.cleanups) > 0:
            log('merging non-empty other', 4)
            self.cleanups = other.cleanups[:]
            return True
        return False

    # Push a new constructor onto our stack.  LHS is the
    # left-hand-side of the GimpleCall statement.  It may be None,
    # meaning that this constructor's value wasn't used.
    def push(self, location, lhs):
        if lhs is None:
            obj = Dummy(location)
        else:
            obj = Cleanup(lhs, location)
        log('pushing %s' % lhs, 4)
        idx = self._find_var(lhs)
        if idx >= 0:
            gcc.permerror(location, 'reassigning to known cleanup')
            gcc.inform(self.cleanups[idx].location,
                       'previous assignment is here')
        self.cleanups.append(obj)

    # A helper for merge and pop that finds BACK_TO in self.cleanups,
    # and returns the index, or -1 if not found.
    def _find_var(self, back_to):
        for i in range(len(self.cleanups) - 1, -1, -1):
            if isinstance(self.cleanups[i], Dummy):
                continue
            if self.compare_vars(self.cleanups[i].var, back_to):
                return i
        return -1

    # Pop constructors until we find one matching BACK_TO.
    # This is invoked when we see a do_cleanups call.
    def pop(self, location, back_to):
        log('pop:', 4)
        i = self._find_var(back_to)
        if i >= 0:
            self.cleanups = self.cleanups[0:i]
        else:
            gcc.permerror(location, 'destructor call with unknown argument')

    # Check whether ARG is the current master cleanup.  Return True if
    # all is well.
    def verify(self, location, arg):
        log('verify %s' % arg, 4)
        return (len(self.cleanups) > 0
                and not isinstance(self.cleanups[0], Dummy)
                and self.compare_vars(self.cleanups[0].var, arg))

    # Check whether SELF is empty.
    def isempty(self):
        log('isempty: len = %d' % len(self.cleanups), 4)
        return len(self.cleanups) == 0

    # Emit informational warnings about the cleanup stack.
    def inform(self):
        for item in reversed(self.cleanups):
            gcc.inform(item.location, 'leaked cleanup')

class CleanupChecker:
    def __init__(self, fun):
        self.fun = fun
        self.seen_edges = set()
        self.bad_returns = set()

        # This maps BB indices to a list of master cleanups for the
        # BB.
        self.master_cleanups = {}

    # Pick a reasonable location for the basic block BB.
    def guess_bb_location(self, bb):
        if isinstance(bb.gimple, list):
            for stmt in bb.gimple:
                if stmt.loc:
                    return stmt.loc
        return self.fun.end

    # Compute the master cleanup list for BB.
    # Modifies MASTER_CLEANUP in place.
    def compute_master(self, bb, bb_from, master_cleanup):
        if not isinstance(bb.gimple, list):
            return
        curloc = self.fun.end
        for stmt in bb.gimple:
            if stmt.loc:
                curloc = stmt.loc
            if isinstance(stmt, gcc.GimpleCall) and stmt.fndecl:
                if is_constructor(stmt.fndecl):
                    log('saw constructor %s in bb=%d' % (str(stmt.fndecl), bb.index), 2)
                    self.cleanup_aware = True
                    master_cleanup.push(curloc, stmt.lhs)
                elif is_destructor(stmt.fndecl):
                    if str(stmt.fndecl.name) != 'do_cleanups':
                        self.only_do_cleanups_seen = False
                    log('saw destructor %s in bb=%d, bb_from=%d, argument=%s'
                        % (str(stmt.fndecl.name), bb.index, bb_from, str(stmt.args[0])),
                        2)
                    master_cleanup.pop(curloc, stmt.args[0])
                elif needs_special_treatment(stmt.fndecl):
                    pass
                    # gcc.permerror(curloc, 'function needs special treatment')
            elif isinstance(stmt, gcc.GimpleAssign):
                if isinstance(stmt.lhs, gcc.VarDecl) and isinstance(stmt.rhs[0], gcc.VarDecl):
                    master_cleanup.note_assignment(stmt.lhs, stmt.rhs[0])
            elif isinstance(stmt, gcc.GimpleReturn):
                if self.is_constructor:
                    if not master_cleanup.verify(curloc, stmt.retval):
                        gcc.permerror(curloc,
                                      'constructor does not return master cleanup')
                elif not self.is_special_constructor:
                    if not master_cleanup.isempty():
                        if curloc not in self.bad_returns:
                            gcc.permerror(curloc, 'cleanup stack is not empty at return')
                            self.bad_returns.add(curloc)
                            master_cleanup.inform()

    # Traverse a basic block, updating the master cleanup information
    # and propagating to other blocks.
    def traverse_bbs(self, edge, bb, bb_from, entry_master):
        log('traverse_bbs %d from %d' % (bb.index, bb_from), 1)

        # Propagate the entry MasterCleanup though this block.
        master_cleanup = MasterCleanup(entry_master)
        self.compute_master(bb, bb_from, master_cleanup)

        modified = False
        if bb.index in self.master_cleanups:
            # Merge the newly-computed MasterCleanup into the one we
            # have already computed.  If this resulted in a
            # significant change, then we need to re-propagate.
            modified = self.master_cleanups[bb.index].merge(master_cleanup)
        else:
            self.master_cleanups[bb.index] = master_cleanup
            modified = True

        # EDGE is None for the entry BB.
        if edge is not None:
            # If merging cleanups caused a change, check to see if we
            # have a bad loop.
            if edge in self.seen_edges:
                # This error doesn't really help.
                # if modified:
                #     gcc.permerror(self.guess_bb_location(bb),
                #                   'invalid cleanup use in loop')
                return
            self.seen_edges.add(edge)

        if not modified:
            return

        # Now propagate to successor nodes.
        for edge in bb.succs:
            self.traverse_bbs(edge, edge.dest, bb.index, master_cleanup)

    def check_cleanups(self):
        if not self.fun.cfg or not self.fun.decl:
            return 'ignored'
        if is_destructor(self.fun.decl):
            return 'destructor'
        if needs_special_treatment(self.fun.decl):
            return 'special'

        self.is_constructor = is_constructor(self.fun.decl)
        self.is_special_constructor = not self.is_constructor and str(self.fun.decl.name).find('with_cleanup') > -1
        # Yuck.
        if str(self.fun.decl.name) == 'gdb_xml_create_parser_and_cleanup_1':
            self.is_special_constructor = True

        if self.is_special_constructor:
            gcc.inform(self.fun.start, 'function %s is a special constructor' % (self.fun.decl.name))

        # If we only see do_cleanups calls, and this function is not
        # itself a constructor, then we can convert it easily to RAII.
        self.only_do_cleanups_seen = not self.is_constructor
        # If we ever call a constructor, then we are "cleanup-aware".
        self.cleanup_aware = False

        entry_bb = self.fun.cfg.entry
        master_cleanup = MasterCleanup()
        self.traverse_bbs(None, entry_bb, -1, master_cleanup)
        if want_raii_info and self.only_do_cleanups_seen and self.cleanup_aware:
            gcc.inform(self.fun.decl.location,
                       'function %s could be converted to RAII' % (self.fun.decl.name))
        if self.is_constructor:
            return 'constructor'
        return 'OK'

class CheckerPass(gcc.GimplePass):
    def execute(self, fun):
        if fun.decl:
            log("Starting " + fun.decl.name)
            if show_cfg:
                dot = gccutils.cfg_to_dot(fun.cfg, fun.decl.name)
                gccutils.invoke_dot(dot, name=fun.decl.name)
        checker = CleanupChecker(fun)
        what = checker.check_cleanups()
        if fun.decl:
            log(fun.decl.name + ': ' + what, 2)

ps = CheckerPass(name = 'check-cleanups')
# We need the cfg, but we want a relatively high-level Gimple.
ps.register_after('cfg')
