| # Copyright 2013-2016 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') |