From cdd9e375d232710802affa4b57ce0cc94cd0208f Mon Sep 17 00:00:00 2001
From: David Malcolm <dmalcolm@redhat.com>
Date: Fri, 27 Sep 2019 09:31:10 -0400
Subject: [PATCH 38/63] {HAS FIXMES}: analyzer: new files: engine.{cc|h}

This patch adds the core analysis code, which explores "interesting"
interprocedual paths in the code, updating state machines to check
for API misuses, and issuing diagnostics for such misuses.

gcc/ChangeLog:
	* analyzer/engine.cc: New file.
	* analyzer/engine.h: New file.
---
 gcc/analyzer/engine.cc | 9243 ++++++++++++++++++++++++++++++++++++++++++++++++
 gcc/analyzer/engine.h  |   26 +
 2 files changed, 9269 insertions(+)
 create mode 100644 gcc/analyzer/engine.cc
 create mode 100644 gcc/analyzer/engine.h

diff --git a/gcc/analyzer/engine.cc b/gcc/analyzer/engine.cc
new file mode 100644
index 0000000..c67bf29
--- /dev/null
+++ b/gcc/analyzer/engine.cc
@@ -0,0 +1,9243 @@
+/* The analysis "engine".
+   Copyright (C) 2019 Free Software Foundation, Inc.
+   Contributed by David Malcolm <dmalcolm@redhat.com>.
+
+This file is part of GCC.
+
+GCC 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, or (at your option)
+any later version.
+
+GCC 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 GCC; see the file COPYING3.  If not see
+<http://www.gnu.org/licenses/>.  */
+/* FIXME.  */
+#include "config.h"
+#include "gcc-plugin.h"
+#include "system.h"
+#include "coretypes.h"
+#include "tree.h"
+#include "tm.h"
+#include "toplev.h"
+#include "hash-table.h"
+#include "vec.h"
+#include "ggc.h"
+#include "basic-block.h"
+#include "tree-ssa-alias.h"
+#include "internal-fn.h"
+#include "gimple-fold.h"
+#include "tree-eh.h"
+#include "gimple-expr.h"
+#include "is-a.h"
+#include "gimple.h"
+#include "tree-pass.h"
+#include "intl.h"
+#include "context.h"
+#include "diagnostic.h"
+#include "bitmap.h"
+#include "gimple.h"
+#include "gimple-iterator.h"
+#include "gimple-pretty-print.h"
+#include "tree-pretty-print.h"
+#include "analyzer/analyzer.h"
+#include "analyzer/graphviz.h"
+#include "cgraph.h"
+#include "diagnostic-path.h"
+#include "gcc-rich-location.h"
+#include "tree-dfa.h"
+#include "analyzer/supergraph.h"
+#include "analyzer/sm.h"
+#include "analyzer/analyzer-logging.h"
+#include "stringpool.h"
+#include "selftest.h"
+#include "fold-const.h"
+#include "analyzer/tristate.h"
+#include "analyzer/digraph.h"
+#include "tree-ssa-operands.h"
+#include "tree-vrp.h"
+#include "gimple-ssa.h"
+#include "tree-ssanames.h"
+#include "tree-phinodes.h"
+#include "ssa-iterators.h"
+#include "analyzer/region-model.h"
+#include "tree-diagnostic.h"
+#include "analyzer/constraint-manager.h"
+#include "analyzer/pending-diagnostic.h"
+#include "analyzer/analyzer-selftests.h"
+#include "fibonacci_heap.h"
+#include "params.h"
+#include "ipa-utils.h"
+
+/* For an overview, see gcc/doc/analyzer.texi.  */
+
+class exploded_graph;
+class exploded_node;
+class exploded_edge;
+class exploded_cluster;
+class exploded_path;
+class checker_path;
+class stmt_finder;
+class state_purge_per_ssa_name;
+class shortest_paths;
+
+static int readability_comparator (const void *p1, const void *p2);
+
+////////////////////////////////////////////////////////////////////////////
+
+/* FIXME.  */
+
+static label_text
+make_label_text (bool can_colorize, const char *fmt, ...)
+{
+  /* FIXME: all this cloning is likely to be expensive.  */
+  pretty_printer *pp = global_dc->printer->clone ();
+  pp_clear_output_area (pp);
+
+  if (!can_colorize)
+    pp_show_color (pp) = false;
+
+  text_info ti;
+  rich_location rich_loc (line_table, UNKNOWN_LOCATION);
+
+  va_list ap;
+
+  va_start (ap, fmt);
+
+  ti.format_spec = _(fmt);
+  ti.args_ptr = &ap;
+  ti.err_no = 0;
+  ti.x_data = NULL;
+  ti.m_richloc = &rich_loc;
+
+  pp_format (pp, &ti);
+  pp_output_formatted_text (pp);
+
+  va_end (ap);
+
+  label_text result = label_text::take (xstrdup (pp_formatted_text (pp)));
+  delete pp;
+  return result;
+}
+
+////////////////////////////////////////////////////////////////////////////
+
+template <typename Type>
+struct pod_hash_traits : typed_noop_remove<Type>
+{
+  typedef Type value_type;
+  typedef Type compare_type;
+  static inline hashval_t hash (value_type);
+  static inline bool equal (const value_type &existing,
+			    const value_type &candidate);
+  static inline void mark_deleted (Type &);
+  static inline void mark_empty (Type &);
+  static inline bool is_deleted (Type);
+  static inline bool is_empty (Type);
+};
+
+////////////////////////////////////////////////////////////////////////////
+
+class format
+{
+public:
+  format (bool newlines) : m_newlines (newlines) {}
+
+  void spacer (pretty_printer *pp) const
+  {
+    if (m_newlines)
+      pp_newline (pp);
+    else
+      pp_space (pp);
+  }
+
+  bool m_newlines;
+};
+
+///////////////////////////////////////////////////////////////////////////
+
+/* A string of return_superedge pointers, representing a call stack
+   at a program point.
+
+   This is used to ensure that we generate interprocedurally valid paths
+   i.e. that we return to the same callsite that called us.
+
+   The class actually stores the return edges, rather than the call edges,
+   since that's what we need to compare against.  */
+
+class call_string
+{
+public:
+  call_string () : m_return_edges () {}
+  call_string (const call_string &other);
+  call_string& operator= (const call_string &other);
+
+  bool operator== (const call_string &other) const;
+
+  void print (pretty_printer *pp) const;
+
+  hashval_t hash () const;
+
+  bool empty_p () const { return m_return_edges.is_empty (); }
+
+  void push_call (const supergraph &sg,
+		  const call_superedge *sedge);
+  const return_superedge *pop () { return m_return_edges.pop (); }
+
+  int calc_recursion_depth () const;
+
+  static int cmp (const call_string &a,
+		  const call_string &b);
+
+  unsigned length () const { return m_return_edges.length (); }
+  const return_superedge *operator[] (unsigned idx) const
+  {
+    return m_return_edges[idx];
+  }
+
+private:
+  static int cmp_1 (const call_string &a,
+		    const call_string &b);
+
+  auto_vec<const return_superedge *> m_return_edges;
+};
+
+////////////////////////////////////////////////////////////////////////////
+
+/* An enum for distinguishing the various kinds of program_point.  */
+
+enum point_kind {
+  /* A "fake" node which has edges to all entrypoints.  */
+  PK_ORIGIN,
+
+  PK_BEFORE_SUPERNODE,
+  PK_BEFORE_STMT,
+  PK_AFTER_SUPERNODE,
+
+  /* Special values used for hash_map:  */
+  PK_EMPTY,
+  PK_DELETED,
+
+  NUM_POINT_KINDS
+};
+
+/* Get a string for PK.  */
+
+static const char *
+point_kind_to_string (enum point_kind pk)
+{
+  switch (pk)
+    {
+    default:
+      gcc_unreachable ();
+    case PK_ORIGIN:
+      return "PK_ORIGIN";
+    case PK_BEFORE_SUPERNODE:
+      return "PK_BEFORE_SUPERNODE";
+    case PK_BEFORE_STMT:
+      return "PK_BEFORE_STMT";
+    case PK_AFTER_SUPERNODE:
+      return "PK_AFTER_SUPERNODE";
+    case PK_EMPTY:
+      return "PK_EMPTY";
+    case PK_DELETED:
+      return "PK_DELETED";
+    }
+}
+
+/* A class for representing the location within the program.
+
+   This represents a fine-grained location within the supergraph (or
+   within one of its nodes), along with a call string giving the
+   interprocedual context.  */
+
+class program_point
+{
+public:
+  program_point (const supernode *supernode,
+		 const superedge *from_edge,
+		 unsigned stmt_idx,
+		 enum point_kind kind,
+		 const call_string &call_string)
+  : m_supernode (supernode), m_from_edge (from_edge),
+    m_stmt_idx (stmt_idx), m_kind (kind),
+    m_call_string (call_string)
+  {
+  }
+
+  void print (pretty_printer *pp, const format &f) const;
+  void print_source_line (pretty_printer *pp) const;
+  void dump () const;
+
+  hashval_t hash () const;
+  bool operator== (const program_point &other) const
+  {
+    return (m_supernode == other.m_supernode
+	    && m_from_edge == other.m_from_edge
+	    && m_stmt_idx == other.m_stmt_idx
+	    && m_kind == other.m_kind
+	    && m_call_string == other.m_call_string);
+  }
+
+  const supernode *get_supernode () const { return m_supernode; }
+  function *get_function () const
+  {
+    if (m_supernode)
+      return m_supernode->m_fun;
+    else
+      return NULL;
+  }
+
+  /* Get the number of frames we expect at this program point.
+     This will be one more than the length of the call_string
+     (which stores the parent callsites), apart from the origin
+     node, which doesn't have any frames.  */
+  int get_stack_depth () const
+  {
+    if (m_kind == PK_ORIGIN)
+      return 0;
+    return m_call_string.length () + 1;
+  }
+
+  const gimple *get_stmt () const;
+  location_t get_location () const;
+
+  static program_point from_function_entry (const supergraph &sg,
+					    function *fun)
+  {
+    return before_supernode (sg.get_node_for_function_entry (fun),
+			     NULL, call_string ());
+  }
+
+  static program_point before_supernode (const supernode *supernode,
+					 const superedge *from_edge,
+					 const call_string &call_string)
+  {
+    return program_point (supernode, from_edge, 0, PK_BEFORE_SUPERNODE,
+			  call_string);
+  }
+
+  static program_point before_stmt (const supernode *supernode,
+				    unsigned stmt_idx,
+				    const call_string &call_string)
+  {
+    return program_point (supernode, NULL, stmt_idx, PK_BEFORE_STMT,
+			  call_string);
+  }
+
+  static program_point after_supernode (const supernode *supernode,
+					const call_string &call_string)
+  {
+    return program_point (supernode, NULL, 0, PK_AFTER_SUPERNODE,
+			  call_string);
+  }
+
+  static program_point empty ()
+  {
+    return program_point (NULL, NULL, 0, PK_EMPTY, call_string ());
+  }
+  static program_point deleted ()
+  {
+    return program_point (NULL, NULL, 0, PK_DELETED, call_string ());
+  }
+
+  bool on_edge (exploded_graph &eg,
+		const superedge *succ);
+
+  static int cmp_within_supernode_1 (const program_point &point_a,
+				     const program_point &point_b);
+  static int cmp_within_supernode (const program_point &point_a,
+				   const program_point &point_b);
+
+//private:
+  const supernode *m_supernode;
+  const superedge *m_from_edge; /* for PK_BEFORE_SUPERNODE.  */
+  unsigned m_stmt_idx;
+  enum point_kind m_kind;
+  call_string m_call_string;
+};
+
+template <> struct default_hash_traits<program_point>
+: public pod_hash_traits<program_point>
+{
+};
+
+template <>
+inline hashval_t
+pod_hash_traits<program_point>::hash (value_type v)
+{
+  return v.hash ();
+}
+
+template <>
+inline bool
+pod_hash_traits<program_point>::equal (const value_type &existing,
+                                 const value_type &candidate)
+{
+  return existing == candidate;
+}
+template <>
+inline void
+pod_hash_traits<program_point>::mark_deleted (value_type &v)
+{
+  v = program_point::deleted ();
+}
+template <>
+inline void
+pod_hash_traits<program_point>::mark_empty (value_type &v)
+{
+  v = program_point::empty ();
+}
+template <>
+inline bool
+pod_hash_traits<program_point>::is_deleted (value_type v)
+{
+  return v.m_kind == PK_DELETED;
+}
+template <>
+inline bool
+pod_hash_traits<program_point>::is_empty (value_type v)
+{
+  return v.m_kind == PK_EMPTY;
+}
+
+////////////////////////////////////////////////////////////////////////////
+
+/* The result of analyzing which SSA names can be purged from state at
+   different points in the program, so that we can simplify program_state
+   objects, in the hope of reducing state-blowup.  */
+
+class state_purge_map : public log_user
+{
+public:
+  typedef ordered_hash_map<tree, state_purge_per_ssa_name *> map_t;
+  typedef map_t::iterator iterator;
+
+  state_purge_map (const supergraph &sg, logger *logger);
+  ~state_purge_map ();
+
+  const state_purge_per_ssa_name &get_data_for_ssa_name (tree var) const
+  {
+    gcc_assert (TREE_CODE (var) == SSA_NAME);
+    state_purge_per_ssa_name **slot
+      = const_cast <map_t&> (m_map).get (var);
+    return **slot;
+  }
+
+  const supergraph &get_sg () const { return m_sg; }
+
+  iterator begin () const { return m_map.begin (); }
+  iterator end () const { return m_map.end (); }
+
+private:
+  const supergraph &m_sg;
+  map_t m_map;
+};
+
+/* The part of a state_purge_map relating to a specific SSA name.
+
+   The result of analyzing a given SSA name, recording which
+   program_points need to retain state information about it to handle
+   their successor states, so that we can simplify program_state objects,
+   in the hope of reducing state-blowup.  */
+
+class state_purge_per_ssa_name
+{
+public:
+  state_purge_per_ssa_name (const state_purge_map &map,
+			    tree var,
+			    function *fun);
+
+  bool needed_at_point_p (const program_point &point) const;
+
+  /* This stores points, but ignores interprocedural issues;
+     everything will be at depth 0.
+     TODO: should we build a program_point graph first???  */
+
+private:
+  static program_point before_use_stmt (const state_purge_map &map,
+					const gimple *use_stmt);
+
+  void add_to_worklist (const program_point &point,
+			auto_vec<program_point *> *worklist,
+			logger *logger);
+
+  void process_point (const program_point &point,
+		      auto_vec<program_point *> *worklist,
+		      logger *logger);
+
+  typedef hash_set<program_point> point_set_t;
+  point_set_t m_points_needing_var;
+  tree m_var;
+};
+
+////////////////////////////////////////////////////////////////////////////
+
+/* Data shared by all program_state instances.  */
+
+class extrinsic_state
+{
+public:
+  extrinsic_state (auto_delete_vec <state_machine> &checkers)
+  : m_checkers (checkers)
+  {
+  }
+
+  const state_machine &get_sm (int idx) const
+  {
+    return *m_checkers[idx];
+  }
+
+  const char *get_name (int idx) const
+  {
+    return m_checkers[idx]->get_name ();
+  }
+
+  unsigned get_num_checkers () const { return m_checkers.length (); }
+
+  /* The state machines.  */
+  auto_delete_vec <state_machine> &m_checkers;
+};
+
+////////////////////////////////////////////////////////////////////////////
+
+/* A class to encapsulate decisions about how the analysis should happen.
+   Examples:
+   - the order in which functions should be analyzed, so that function
+     summaries are created before analysis of call sites that might use
+     them
+   - which callgraph edges should use call summaries
+   TODO: the above is a work-in-progress.  */
+
+class analysis_plan : public log_user
+{
+public:
+  analysis_plan (const supergraph &sg, logger *logger);
+  ~analysis_plan ();
+
+  int cmp_function (function *fun_a, function *fun_b) const;
+
+  bool use_summary_p (const cgraph_edge *edge) const;
+
+private:
+  const supergraph &m_sg;
+
+  /* Result of ipa_reverse_postorder.  */
+  cgraph_node **m_cgraph_node_postorder;
+  int m_num_cgraph_nodes;
+
+  /* Index of each node within the postorder ordering,
+     accessed via the "m_uid" field.  */
+  auto_vec<int> m_index_by_uid;
+};
+
+////////////////////////////////////////////////////////////////////////////
+
+template <> struct default_hash_traits<svalue_id>
+: public pod_hash_traits<svalue_id>
+{
+};
+
+template <>
+inline hashval_t
+pod_hash_traits<svalue_id>::hash (value_type v)
+{
+  return v.as_int ();
+}
+
+template <>
+inline bool
+pod_hash_traits<svalue_id>::equal (const value_type &existing,
+				   const value_type &candidate)
+{
+  return existing == candidate;
+}
+template <>
+inline void
+pod_hash_traits<svalue_id>::mark_deleted (value_type &v)
+{
+  v = svalue_id::from_int (-2);
+}
+template <>
+inline void
+pod_hash_traits<svalue_id>::mark_empty (value_type &v)
+{
+  v = svalue_id::null ();
+}
+template <>
+inline bool
+pod_hash_traits<svalue_id>::is_deleted (value_type v)
+{
+  return v.as_int () == -2;
+}
+template <>
+inline bool
+pod_hash_traits<svalue_id>::is_empty (value_type v)
+{
+  return v.null_p ();
+}
+
+/* Map from svalue_id to state machine state, also capturing the origin of
+   each state.  */
+
+class sm_state_map
+{
+public:
+  /* An entry in the hash_map.  */
+  struct entry_t
+  {
+    /* Default ctor needed by hash_map::empty.  */
+    entry_t ()
+    : m_state (0), m_origin (svalue_id::null ())
+    {
+    }
+
+    entry_t (state_machine::state_t state,
+	     svalue_id origin)
+    : m_state (state), m_origin (origin)
+    {}
+
+    bool operator== (const entry_t &other) const
+    {
+      return (m_state == other.m_state
+	      && m_origin == other.m_origin);
+    }
+    bool operator!= (const entry_t &other) const
+    {
+      return !(*this == other);
+    }
+
+    state_machine::state_t m_state;
+    svalue_id m_origin;
+  };
+  typedef hash_map <svalue_id, entry_t> map_t;
+  typedef typename map_t::iterator iterator_t;
+
+  sm_state_map *clone () const;
+
+  sm_state_map *
+  clone_with_remapping (const one_way_svalue_id_map &id_map) const;
+
+  void print (const state_machine &sm, pretty_printer *pp) const;
+  void dump (const state_machine &sm) const;
+
+  bool is_empty_p () const;
+
+  hashval_t hash () const;
+
+  bool operator== (const sm_state_map &other) const;
+  bool operator!= (const sm_state_map &other) const
+  {
+    return !(*this == other);
+  }
+
+  state_machine::state_t get_state (svalue_id sid) const;
+  svalue_id get_origin (svalue_id sid) const;
+
+  void set_state (region_model *model,
+		  svalue_id sid,
+		  state_machine::state_t state,
+		  svalue_id origin);
+  void set_state (const equiv_class &ec,
+		  state_machine::state_t state,
+		  svalue_id origin);
+  void impl_set_state (svalue_id sid,
+		       state_machine::state_t state,
+		       svalue_id origin);
+
+  void purge_for_unknown_fncall (const exploded_graph &eg,
+				 const state_machine &sm,
+				 const gcall *call, tree fndecl,
+				 region_model *new_model);
+
+  void remap_svalue_ids (const svalue_id_map &map);
+
+  int on_svalue_purge (const state_machine &sm,
+		       int sm_idx,
+		       svalue_id first_unused_sid,
+		       const svalue_id_map &map,
+		       region_model_context *ctxt);
+
+  void on_inherited_svalue (svalue_id parent_sid,
+			    svalue_id child_sid);
+
+  void validate (const state_machine &sm, int num_svalues) const;
+
+  iterator_t begin () const { return m_map.begin (); }
+  iterator_t end () const { return m_map.end (); }
+
+private:
+  map_t m_map;
+};
+
+/* A class for representing the state of interest at a given path of
+   analysis.
+
+   Currently this is a combination of:
+   (a) a region_model, giving:
+      (a.1) a hierarchy of memory regions
+      (a.2) values for the regions
+      (a.3) inequalities between values
+   (b) sm_state_maps per state machine, giving a sparse mapping of
+       values to states.  */
+
+class program_state
+{
+public:
+  program_state (const extrinsic_state &ext_state);
+  program_state (const program_state &other);
+  program_state& operator= (const program_state &other);
+
+#if __cplusplus >= 201103
+  program_state (program_state &&other);
+  program_state& operator= (program_state &&other); // doesn't seem to be used
+#endif
+
+  ~program_state ();
+
+  hashval_t hash () const;
+  bool operator== (const program_state &other) const;
+  bool operator!= (const program_state &other) const
+  {
+    return !(*this == other);
+  }
+
+  void print (const extrinsic_state &ext_state,
+	      pretty_printer *pp) const;
+
+  void dump_to_pp (const extrinsic_state &ext_state, bool summarize,
+		   pretty_printer *pp) const;
+  void dump_to_file (const extrinsic_state &ext_state, bool summarize,
+		     FILE *outf) const;
+  void dump (const extrinsic_state &ext_state, bool summarize) const;
+
+  bool on_edge (exploded_graph &eg,
+		const exploded_node &enode,
+		const superedge *succ);
+
+  program_state prune_for_point (exploded_graph &eg,
+				 const program_point &point) const;
+
+  void remap_svalue_ids (const svalue_id_map &map);
+
+  tree get_representative_tree (svalue_id sid) const;
+
+  /* FIXME: does this ever get called?  */
+  bool can_purge_p (const extrinsic_state &ext_state,
+		    svalue_id sid)
+  {
+    /* Don't purge vars that have non-purgeable sm state, to avoid
+       generating false "leak" complaints.  */
+    int i;
+    sm_state_map *smap;
+    FOR_EACH_VEC_ELT (m_checker_states, i, smap)
+      {
+	const state_machine &sm = ext_state.get_sm (i);
+	if (!sm.can_purge_p (smap->get_state (sid)))
+	  return false;
+      }
+    return true;
+  }
+
+  bool can_merge_with_p (const program_state &other,
+			 const extrinsic_state &ext_state,
+			 program_state *out) const;
+
+  void validate (const extrinsic_state &ext_state) const;
+
+  /* TODO: lose the pointer here (const-correctness issues?).  */
+  region_model *m_region_model;
+  auto_delete_vec<sm_state_map> m_checker_states;
+};
+
+/* A <program_point, program_state> pair, used internally by
+   exploded_node as its immutable data, and as a key for identifying
+   exploded_nodes we've already seen in the graph.  */
+
+struct point_and_state
+{
+  point_and_state (const program_point &point,
+		   const program_state &state)
+  : m_point (point),
+    m_state (state),
+    m_hash (m_point.hash () ^ m_state.hash ())
+  {
+  }
+
+  hashval_t hash () const
+  {
+    return m_hash;
+  }
+  bool operator== (const point_and_state &other) const
+  {
+    return m_point == other.m_point && m_state == other.m_state;
+  }
+
+  void set_state (const program_state &state)
+  {
+    m_state = state;
+    m_hash = m_point.hash () ^ m_state.hash ();
+  }
+
+  program_point m_point;
+  program_state m_state;
+  hashval_t m_hash;
+};
+
+/* A traits class for exploded graphs and their nodes and edges.  */
+
+struct eg_traits
+{
+  typedef exploded_node node_t;
+  typedef exploded_edge edge_t;
+  typedef exploded_graph graph_t;
+  struct dump_args_t {};
+  typedef exploded_cluster cluster_t;
+};
+
+/* An exploded_node is a unique, immutable <point, state> pair within the
+   exploded_graph.
+   Each exploded_node has a unique index within the graph
+   (for ease of debugging).  */
+
+class exploded_node : public dnode<eg_traits>
+{
+ public:
+  exploded_node (point_and_state ps,
+		 const extrinsic_state &ext_state,
+		 int index)
+  : m_ps (ps), m_ext_state (ext_state), m_index (index)
+  {
+    gcc_assert (ps.m_state.m_region_model->canonicalized_p ());
+  }
+
+  hashval_t hash () const { return m_ps.hash (); }
+
+  void dump_dot (graphviz_out *gv, const dump_args_t &args)
+    const FINAL OVERRIDE;
+  void dump_dot_id (pretty_printer *pp) const;
+
+  void dump_to_pp (pretty_printer *pp) const;
+  void dump (FILE *fp) const;
+  void dump () const;
+
+  bool on_stmt (exploded_graph &eg,
+		const supernode *snode,
+		const gimple *stmt,
+		program_state *state) const;
+  bool on_edge (exploded_graph &eg,
+		const superedge *succ,
+		program_point *next_point,
+		program_state *next_state) const;
+  void on_longjmp (exploded_graph &eg,
+		   const gcall *call,
+		   program_state *new_state,
+		   region_model_context *ctxt) const;
+
+  void detect_leaks (exploded_graph &eg) const;
+
+  const program_point &get_point () const { return m_ps.m_point; }
+  const supernode *get_supernode () const
+  {
+    return get_point ().get_supernode ();
+  }
+  function *get_function () const
+  {
+    return get_point ().get_function ();
+  }
+  int get_stack_depth () const
+  {
+    return get_point ().get_stack_depth ();
+  }
+  const gimple *get_stmt () const { return get_point ().get_stmt (); }
+
+  const program_state &get_state () const { return m_ps.m_state; }
+
+  const point_and_state *get_ps_key () const { return &m_ps; }
+  const program_point *get_point_key () const { return &m_ps.m_point; }
+
+  void dump_succs_and_preds (FILE *outf) const;
+
+private:
+  const char * get_dot_fillcolor () const;
+
+  /* The <program_point, program_state> pair.  This is const, as it
+     is immutable once the exploded_node has been created.  */
+  const point_and_state m_ps;
+
+  /* FIXME: this is within the node since dump_dot is a vfunc
+     In theory we could just pass this around instead.  */
+  const extrinsic_state &m_ext_state;
+
+public:
+  /* The index of this exploded_node.  */
+  const int m_index;
+};
+
+/* Extra data for an exploded_edge that represents a rewind from a
+   longjmp to a setjmp.  */
+
+class rewind_info_t
+{
+public:
+  rewind_info_t (const exploded_node *enode_origin)
+  : m_enode_origin (enode_origin)
+  {}
+
+  const program_point &get_setjmp_point () const
+  {
+    const program_point &origin_point = m_enode_origin->get_point ();
+
+    /* "origin_point" ought to be before the call to "setjmp".  */
+    gcc_assert (origin_point.m_kind == PK_BEFORE_STMT);
+
+    /* TODO: assert that it's the final stmt in its supernode.  */
+
+    return origin_point;
+  }
+
+  const gcall *get_setjmp_call () const
+  {
+    return as_a <const gcall *> (get_setjmp_point ().get_stmt ());
+  }
+
+  const exploded_node *get_enode_origin () const { return m_enode_origin; }
+
+private:
+  const exploded_node *m_enode_origin;
+};
+
+/* An edge within the exploded graph.
+   Some exploded_edges have an underlying superedge; others don't.  */
+
+class exploded_edge : public dedge<eg_traits>
+{
+ public:
+  exploded_edge (exploded_node *src, exploded_node *dest,
+		 const superedge *sedge, rewind_info_t *rewind_info);
+  ~exploded_edge ();
+  void dump_dot (graphviz_out *gv, const dump_args_t &args)
+    const FINAL OVERRIDE;
+
+  //private:
+  const superedge *const m_sedge;
+
+  /* NULL for most edges; will be non-NULL for an unwind from a longjmp
+     to a setjmp (owned by this class).  */
+  rewind_info_t *m_rewind_info;
+};
+
+/* Statistics about aspects of an exploded_graph.  */
+
+struct stats
+{
+  stats (int num_supernodes);
+  void log (logger *logger) const;
+  void dump (FILE *out) const;
+
+  int m_num_nodes[NUM_POINT_KINDS];
+  int m_node_reuse_count;
+  int m_node_reuse_after_merge_count;
+  int m_num_supernodes;
+};
+
+/* Traits class for ensuring uniqueness of point_and_state data within
+   an exploded_graph.  */
+
+struct eg_hash_map_traits
+{
+  typedef const point_and_state *key_type;
+  typedef exploded_node *value_type;
+  typedef exploded_node *compare_type;
+
+  static inline hashval_t hash (const key_type &k)
+  {
+    gcc_assert (k != NULL);
+    gcc_assert (k != reinterpret_cast<key_type> (1));
+    return k->hash ();
+  }
+  static inline bool equal_keys (const key_type &k1, const key_type &k2)
+  {
+    gcc_assert (k1 != NULL);
+    gcc_assert (k2 != NULL);
+    gcc_assert (k1 != reinterpret_cast<key_type> (1));
+    gcc_assert (k2 != reinterpret_cast<key_type> (1));
+    if (k1 && k2)
+      return *k1 == *k2;
+    else
+      /* Otherwise they must both be non-NULL.  */
+      return k1 == k2;
+  }
+  template <typename T>
+  static inline void remove (T &)
+  {
+    /* empty; the nodes are handled elsewhere.  */
+  }
+  template <typename T>
+  static inline void mark_deleted (T &entry)
+  {
+    entry.m_key = reinterpret_cast<key_type> (1);
+  }
+  template <typename T>
+  static inline void mark_empty (T &entry)
+  {
+    entry.m_key = NULL;
+  }
+  template <typename T>
+  static inline bool is_deleted (const T &entry)
+  {
+    return entry.m_key == reinterpret_cast<key_type> (1);
+  }
+  template <typename T>
+  static inline bool is_empty (const T &entry)
+  {
+    return entry.m_key == NULL;
+  }
+};
+
+/* FIXME.  */
+
+struct per_program_point_data
+{
+  per_program_point_data (const program_point &key)
+  : m_key (key)
+  {}
+
+  program_point m_key;
+  auto_vec<exploded_node *> m_enodes;
+};
+
+/* Traits class for storing per-program_point data within
+   an exploded_graph.  */
+
+struct eg_point_hash_map_traits
+{
+  typedef const program_point *key_type;
+  typedef per_program_point_data *value_type;
+  typedef per_program_point_data *compare_type;
+
+  static inline hashval_t hash (const key_type &k)
+  {
+    gcc_assert (k != NULL);
+    gcc_assert (k != reinterpret_cast<key_type> (1));
+    return k->hash ();
+  }
+  static inline bool equal_keys (const key_type &k1, const key_type &k2)
+  {
+    gcc_assert (k1 != NULL);
+    gcc_assert (k2 != NULL);
+    gcc_assert (k1 != reinterpret_cast<key_type> (1));
+    gcc_assert (k2 != reinterpret_cast<key_type> (1));
+    if (k1 && k2)
+      return *k1 == *k2;
+    else
+      /* Otherwise they must both be non-NULL.  */
+      return k1 == k2;
+  }
+  template <typename T>
+  static inline void remove (T &)
+  {
+    /* empty; the nodes are handled elsewhere.  */
+  }
+  template <typename T>
+  static inline void mark_deleted (T &entry)
+  {
+    entry.m_key = reinterpret_cast<key_type> (1);
+  }
+  template <typename T>
+  static inline void mark_empty (T &entry)
+  {
+    entry.m_key = NULL;
+  }
+  template <typename T>
+  static inline bool is_deleted (const T &entry)
+  {
+    return entry.m_key == reinterpret_cast<key_type> (1);
+  }
+  template <typename T>
+  static inline bool is_empty (const T &entry)
+  {
+    return entry.m_key == NULL;
+  }
+};
+
+/* Data about a particular call_string within an exploded_graph.  */
+
+struct per_call_string_data
+{
+  per_call_string_data (const call_string &key, int num_supernodes)
+  : m_key (key), m_stats (num_supernodes)
+  {}
+
+  call_string m_key;
+  stats m_stats;
+};
+
+/* Traits class for storing per-call_string data within
+   an exploded_graph.  */
+
+struct eg_call_string_hash_map_traits
+{
+  typedef const call_string *key_type;
+  typedef per_call_string_data *value_type;
+  typedef per_call_string_data *compare_type;
+
+  static inline hashval_t hash (const key_type &k)
+  {
+    gcc_assert (k != NULL);
+    gcc_assert (k != reinterpret_cast<key_type> (1));
+    return k->hash ();
+  }
+  static inline bool equal_keys (const key_type &k1, const key_type &k2)
+  {
+    gcc_assert (k1 != NULL);
+    gcc_assert (k2 != NULL);
+    gcc_assert (k1 != reinterpret_cast<key_type> (1));
+    gcc_assert (k2 != reinterpret_cast<key_type> (1));
+    if (k1 && k2)
+      return *k1 == *k2;
+    else
+      /* Otherwise they must both be non-NULL.  */
+      return k1 == k2;
+  }
+  template <typename T>
+  static inline void remove (T &)
+  {
+    /* empty; the nodes are handled elsewhere.  */
+  }
+  template <typename T>
+  static inline void mark_deleted (T &entry)
+  {
+    entry.m_key = reinterpret_cast<key_type> (1);
+  }
+  template <typename T>
+  static inline void mark_empty (T &entry)
+  {
+    entry.m_key = NULL;
+  }
+  template <typename T>
+  static inline bool is_deleted (const T &entry)
+  {
+    return entry.m_key == reinterpret_cast<key_type> (1);
+  }
+  template <typename T>
+  static inline bool is_empty (const T &entry)
+  {
+    return entry.m_key == NULL;
+  }
+};
+
+/* Data about a particular function within an exploded_graph.  */
+
+struct per_function_data
+{
+  per_function_data () {}
+
+  void add_call_summary (exploded_node *node)
+  {
+    m_summaries.safe_push (node);
+  }
+
+  auto_vec<exploded_node *> m_summaries;
+};
+
+
+/* A to-be-emitted diagnostic stored within diagnostic_manager.  */
+
+class saved_diagnostic
+{
+public:
+  saved_diagnostic (const state_machine *sm,
+		    const exploded_node *enode,
+		    const supernode *snode, const gimple *stmt,
+		    stmt_finder *stmt_finder,
+		    tree var, state_machine::state_t state,
+		    pending_diagnostic *d);
+  ~saved_diagnostic ();
+
+  bool operator== (const saved_diagnostic &other) const
+  {
+    return (m_sm == other.m_sm
+	    /* We don't compare m_enode.  */
+	    && m_snode == other.m_snode
+	    && m_stmt == other.m_stmt
+	    /* We don't compare m_stmt_finder.  */
+	    && m_var == other.m_var
+	    && m_state == other.m_state
+	    && m_d->equal_p (*other.m_d));
+  }
+
+  //private:
+  const state_machine *m_sm;
+  const exploded_node *m_enode;
+  const supernode *m_snode;
+  const gimple *m_stmt;
+  stmt_finder *m_stmt_finder;
+  tree m_var;
+  state_machine::state_t m_state;
+  pending_diagnostic *m_d;
+};
+
+/* A class with responsibility for saving pending diagnostics, so that
+   they can be emitted after the exploded_graph is complete.
+   This lets us de-duplicate diagnostics, and find the shortest path
+   for each similar diagnostic, potentially using edges that might
+   not have been found when each diagnostic was first saved.
+
+   This also lets us compute shortest_paths once, rather than
+   per-diagnostic.  */
+
+class diagnostic_manager : public log_user
+{
+public:
+  diagnostic_manager (logger *logger, int verbosity);
+
+  void add_diagnostic (const state_machine *sm,
+		       const exploded_node *enode,
+		       const supernode *snode, const gimple *stmt,
+		       stmt_finder *finder,
+		       tree var, state_machine::state_t state,
+		       pending_diagnostic *d);
+
+  void add_diagnostic (const exploded_node *enode,
+		       const supernode *snode, const gimple *stmt,
+		       stmt_finder *finder,
+		       pending_diagnostic *d);
+
+  void emit_saved_diagnostics (const exploded_graph &eg);
+
+  void emit_saved_diagnostic (const exploded_graph &eg,
+			      const saved_diagnostic &sd,
+			      const exploded_path &epath,
+			      const gimple *stmt,
+			      int num_dupes);
+
+private:
+  void build_emission_path (const exploded_graph &eg,
+			    const exploded_path &epath,
+			    checker_path *emission_path) const;
+
+  void add_events_for_eedge (const exploded_edge &eedge,
+			     const extrinsic_state &ext_state,
+			     checker_path *emission_path) const;
+
+  void add_events_for_superedge (const exploded_edge &eedge,
+				 checker_path *emission_path) const;
+
+  void add_state_change_events (const exploded_edge &eedge,
+				const state_machine &sm,
+				const sm_state_map &src_smap,
+				const sm_state_map &dst_smap,
+				checker_path *emission_path) const;
+
+  void prune_path (checker_path *path,
+		   const state_machine *sm,
+		   tree var, state_machine::state_t state) const;
+
+  auto_delete_vec<saved_diagnostic> m_saved_diagnostics;
+  const int m_verbosity;
+};
+
+/* The strongly connected components of a supergraph.
+   In particular, this allows us to compute a partial ordering
+   of supernodes.  */
+
+class strongly_connected_components
+{
+public:
+  strongly_connected_components (const supergraph &sg, logger *logger);
+
+  int get_scc_id (int node_index) const
+  {
+    return m_per_node[node_index].m_lowlink;
+  }
+
+  void dump () const;
+
+private:
+  struct per_node_data
+  {
+    per_node_data ()
+      : m_index (-1), m_lowlink (-1), m_on_stack (false)
+    {}
+
+    int m_index;
+    int m_lowlink;
+    bool m_on_stack;
+  };
+
+  void strong_connect (unsigned index);
+
+  const supergraph &m_sg;
+  auto_vec<unsigned> m_stack;
+  auto_vec<per_node_data> m_per_node;
+};
+
+/* The worklist of exploded_node instances that have been added to
+   an exploded_graph, but that haven't yet been processed to find
+   their successors (or warnings).
+
+   The enodes are stored in a priority queue, ordered by a topological
+   sort of the SCCs in the supergraph, so that enodes for the same
+   program_point should appear at the front of the queue together.
+   This allows for state-merging at CFG join-points, so that
+   sufficiently-similar enodes can be merged into one.  */
+
+class worklist
+{
+public:
+  worklist (const exploded_graph &eg, const analysis_plan &plan);
+  unsigned length () const;
+  exploded_node *take_next ();
+  exploded_node *peek_next ();
+  void add_node (exploded_node *enode);
+
+private:
+  class key_t
+  {
+  public:
+    key_t (const worklist &w, exploded_node *enode)
+    : m_worklist (w), m_enode (enode)
+    {}
+
+    bool operator< (const key_t &other) const
+    {
+      return cmp (*this, other) < 0;
+    }
+
+    bool operator== (const key_t &other) const
+    {
+      return cmp (*this, other) == 0;
+    }
+
+    bool operator> (const key_t &other) const
+    {
+      return !(*this == other || *this < other);
+    }
+
+  private:
+    static int cmp_1 (const key_t &ka, const key_t &kb);
+    static int cmp (const key_t &ka, const key_t &kb);
+
+    int get_scc_id (const exploded_node *enode) const
+    {
+      const supernode *snode = enode->get_supernode ();
+      if (snode == NULL)
+	return 0;
+      return m_worklist.m_scc.get_scc_id (snode->m_index);
+    }
+
+    const worklist &m_worklist;
+    exploded_node *m_enode;
+  };
+
+  /* The order is important here: m_scc needs to stick around
+     until after m_queue has finished being cleaned up (the dtor
+     calls the ordering fns).  */
+  const exploded_graph &m_eg;
+  strongly_connected_components m_scc;
+  const analysis_plan &m_plan;
+
+  /* Priority queue, backed by a fibonacci_heap.  */
+  typedef fibonacci_heap<key_t, exploded_node> queue_t;
+  queue_t m_queue;
+};
+
+/* An exploded_graph is a directed graph of unique <point, state> pairs.
+   It also has a worklist of nodes that are waiting for their successors
+   to be added to the graph.  */
+
+class exploded_graph : public digraph<eg_traits>, public log_user
+{
+public:
+  typedef hash_map <const call_string *, per_call_string_data *,
+		    eg_call_string_hash_map_traits> call_string_data_map_t;
+
+  exploded_graph (const supergraph &sg, logger *logger,
+		  const extrinsic_state &ext_state,
+		  const state_purge_map *purge_map,
+		  const analysis_plan &plan,
+		  int verbosity);
+  ~exploded_graph ();
+
+  const supergraph &get_supergraph () const { return m_sg; }
+  const extrinsic_state &get_ext_state () const { return m_ext_state; }
+  const state_purge_map *get_purge_map () const { return m_purge_map; }
+  const analysis_plan &get_analysis_plan () const { return m_plan; }
+
+  exploded_node *get_origin () const { return m_origin; }
+
+  exploded_node *add_function_entry (function *fun);
+
+  void build_initial_worklist ();
+  void process_worklist ();
+  void process_node (exploded_node *node);
+
+  exploded_node *get_or_create_node (const program_point &point,
+				     const program_state &state);
+  void add_edge (exploded_node *src, exploded_node *dest,
+		 const superedge *sedge,
+		 rewind_info_t *rewind_info = NULL);
+
+  per_program_point_data *
+  get_or_create_per_program_point_data (const program_point &);
+
+  per_call_string_data *
+  get_or_create_per_call_string_data (const call_string &);
+
+  per_function_data *
+  get_or_create_per_function_data (function *);
+  per_function_data *get_per_function_data (function *) const;
+
+  void save_diagnostic (const state_machine &sm,
+			const exploded_node *enode,
+			const supernode *node, const gimple *stmt,
+			stmt_finder *finder,
+			tree var, state_machine::state_t state,
+			pending_diagnostic *d);
+
+  diagnostic_manager &get_diagnostic_manager ()
+  {
+    return m_diagnostic_manager;
+  }
+
+  stats *get_global_stats () { return &m_global_stats; }
+  stats *get_or_create_function_stats (function *fn);
+  void log_stats () const;
+  void dump_stats (FILE *) const;
+  void dump_states_for_supernode (FILE *, const supernode *snode) const;
+  void dump_exploded_nodes () const;
+
+  const call_string_data_map_t *get_per_call_string_data () const
+  { return &m_per_call_string_data; }
+
+private:
+  const supergraph &m_sg;
+
+  /* Map from point/state to exploded node.
+     To avoid duplication we store point_and_state
+     *pointers* as keys, rather than point_and_state, using the
+     instance from within the exploded_node, with a custom hasher.  */
+  typedef hash_map <const point_and_state *, exploded_node *,
+		    eg_hash_map_traits> map_t;
+  map_t m_point_and_state_to_node;
+
+  /* Map from program_point to per-program_point data.  */
+  typedef hash_map <const program_point *, per_program_point_data *,
+		    eg_point_hash_map_traits> point_map_t;
+  point_map_t m_per_point_data;
+
+  worklist m_worklist;
+
+  exploded_node *m_origin;
+
+  const extrinsic_state &m_ext_state;
+
+  const state_purge_map *const m_purge_map;
+
+  const analysis_plan &m_plan;
+
+  typedef hash_map<function *, per_function_data *> per_function_data_t;
+  per_function_data_t m_per_function_data;
+
+  diagnostic_manager m_diagnostic_manager;
+
+  /* Stats.  */
+  stats m_global_stats;
+  typedef ordered_hash_map<function *, stats *> function_stat_map_t;
+  function_stat_map_t m_per_function_stats;
+  stats m_functionless_stats;
+
+  call_string_data_map_t m_per_call_string_data;
+
+
+  auto_vec<int> m_PK_AFTER_SUPERNODE_per_snode;
+};
+
+/* A path within an exploded_graph: a sequence of edges.  */
+
+class exploded_path
+{
+public:
+  exploded_path () : m_edges () {}
+  exploded_path (const exploded_path &other);
+  exploded_path & operator= (const exploded_path &other);
+
+  unsigned length () const { return m_edges.length (); }
+
+  bool find_stmt_backwards (const gimple *search_stmt,
+			    int *out_idx) const;
+
+  exploded_node *get_final_enode () const;
+
+  void dump_to_pp (pretty_printer *pp) const;
+  void dump (FILE *fp) const;
+  void dump () const;
+
+  bool feasible_p (logger *logger) const;
+
+  auto_vec<const exploded_edge *> m_edges;
+};
+
+/* A record of the shortest path to each node in an exploded_graph
+   from the origin node.
+   The constructor runs Dijkstra's algorithm, and the results are
+   stored in this class.  */
+
+class shortest_paths
+{
+public:
+  shortest_paths (const exploded_graph &eg);
+
+  exploded_path get_shortest_path (const exploded_node *to) const;
+
+private:
+  const exploded_graph &m_eg;
+
+  /* For each exploded_node (by index), the minimal distance to that
+     node from the origin.  */
+  auto_vec<int> m_dist;
+
+  /* For each exploded_node (by index), the previous exploded_edge in
+     the shortest path from the origin.  */
+  auto_vec<const exploded_edge *> m_prev;
+};
+
+/* Abstract base class for use when passing NULL as the stmt for
+   a possible warning, allowing the choice of stmt to be deferred
+   until after we have an emission path (and know we're emitting a
+   warning).  */
+
+class stmt_finder
+{
+public:
+  virtual ~stmt_finder () {}
+  virtual stmt_finder *clone () const = 0;
+  virtual const gimple *find_stmt (const exploded_path &epath) = 0;
+};
+
+////////////////////////////////////////////////////////////////////////////
+
+/* An enum for discriminating between the concrete subclasses of
+   checker_event.  */
+
+enum event_kind
+{
+  EK_DEBUG,
+  EK_STMT,
+  EK_FUNCTION_ENTRY,
+  EK_STATE_CHANGE,
+  EK_START_CFG_EDGE,
+  EK_END_CFG_EDGE,
+  EK_CALL_EDGE,
+  EK_RETURN_EDGE,
+  EK_SETJMP,
+  EK_REWIND_FROM_LONGJMP,
+  EK_REWIND_TO_SETJMP,
+  EK_WARNING
+};
+
+static DEBUG_FUNCTION const char *
+event_kind_to_string (enum event_kind ek)
+{
+  switch (ek)
+    {
+    default:
+      gcc_unreachable ();
+    case EK_DEBUG:
+      return "EK_DEBUG";
+    case EK_STMT:
+      return "EK_STMT";
+    case EK_FUNCTION_ENTRY:
+      return "EK_FUNCTION_ENTRY";
+    case EK_STATE_CHANGE:
+      return "EK_STATE_CHANGE";
+    case EK_START_CFG_EDGE:
+      return "EK_START_CFG_EDGE";
+    case EK_END_CFG_EDGE:
+      return "EK_END_CFG_EDGE";
+    case EK_CALL_EDGE:
+      return "EK_CALL_EDGE";
+    case EK_RETURN_EDGE:
+      return "EK_RETURN_EDGE";
+    case EK_SETJMP:
+      return "EK_SETJMP";
+    case EK_REWIND_FROM_LONGJMP:
+      return "EK_REWIND_FROM_LONGJMP";
+    case EK_REWIND_TO_SETJMP:
+      return "EK_REWIND_TO_SETJMP";
+    case EK_WARNING:
+      return "EK_WARNING";
+    }
+}
+
+/* Subclass of diagnostic_event for use in analyzer diagnostic_paths.  */
+
+class checker_event : public diagnostic_event
+{
+public:
+  checker_event (enum event_kind kind,
+		  location_t loc, tree fndecl, int depth)
+    : m_kind (kind), m_loc (loc), m_fndecl (fndecl), m_depth (depth),
+      m_pending_diagnostic (NULL), m_emission_id ()
+  {
+  }
+
+  location_t get_location () const FINAL OVERRIDE { return m_loc; }
+  tree get_fndecl () const FINAL OVERRIDE { return m_fndecl; }
+  int get_stack_depth () const FINAL OVERRIDE { return m_depth; }
+
+  virtual checker_event *clone () const = 0;
+
+  void dump (pretty_printer *pp) const
+  {
+    label_text event_desc (get_desc (false));
+    pp_printf (pp, "\"%s\" (depth %i, m_loc=%x)",
+	       event_desc.m_buffer,
+	       get_stack_depth (),
+	       get_location ());
+    event_desc.maybe_free ();
+  }
+
+  virtual void prepare_for_emission (checker_path *,
+				     pending_diagnostic *pd,
+				     diagnostic_event_id_t emission_id)
+  {
+    m_pending_diagnostic = pd;
+    m_emission_id = emission_id;
+
+    /* Call get_desc now, to ensure that any side-effects take place
+       before pending_diagnostic::emit is called.
+       In particular, state_change_event::get_desc can call
+       pending_diagnostic::describe_state_change;
+       free_of_non_heap can use this to tweak the message.
+       (Would be neater to simply capture this within the state).  */
+    label_text desc = get_desc (false);
+    desc.maybe_free ();
+  }
+
+  virtual bool is_call_p () const { return false; }
+  virtual bool is_function_entry_p () const  { return false; }
+  virtual bool is_return_p () const  { return false; }
+
+  enum event_kind m_kind;
+  location_t m_loc;
+  tree m_fndecl;
+  int m_depth;
+  pending_diagnostic *m_pending_diagnostic;
+  diagnostic_event_id_t m_emission_id; // only set once all pruning has occurred
+};
+
+/* A purely textual event, for use in debugging path creation and filtering.  */
+
+class debug_event : public checker_event
+{
+public:
+  debug_event (location_t loc, tree fndecl, int depth,
+	      const char *desc)
+  : checker_event (EK_DEBUG, loc, fndecl, depth),
+    m_desc (xstrdup (desc))
+  {
+  }
+  ~debug_event ()
+  {
+    free (m_desc);
+  }
+
+  label_text get_desc (bool) const FINAL OVERRIDE
+  {
+    return label_text::borrow (m_desc);
+  }
+
+  checker_event *clone () const FINAL OVERRIDE
+  {
+    return new debug_event (m_loc, m_fndecl, m_depth, m_desc);
+  }
+
+private:
+  char *m_desc;
+};
+
+/* An event subclass for the execution of a gimple statement, for use
+   at high verbosity levels when debugging paths.  */
+
+class statement_event : public checker_event
+{
+public:
+  statement_event (const gimple *stmt, tree fndecl, int depth,
+		   const program_state &dst_state)
+  : checker_event (EK_STMT, gimple_location (stmt), fndecl, depth),
+    m_stmt (stmt),
+    m_dst_state (dst_state)
+  {
+  }
+
+  label_text get_desc (bool) const FINAL OVERRIDE
+  {
+    pretty_printer pp;
+    pp_string (&pp, "stmt: ");
+    pp_gimple_stmt_1 (&pp, m_stmt, 0, (dump_flags_t)0);
+    return label_text::take (xstrdup (pp_formatted_text (&pp)));
+  }
+
+  checker_event *clone () const FINAL OVERRIDE
+  {
+    return new statement_event (m_stmt, m_fndecl, m_depth, m_dst_state);
+  }
+
+  const gimple *m_stmt;
+  program_state m_dst_state;
+};
+
+/* An event subclass describing the entry to a function.  */
+
+class function_entry_event : public checker_event
+{
+public:
+  function_entry_event (location_t loc, tree fndecl, int depth)
+  : checker_event (EK_FUNCTION_ENTRY, loc, fndecl, depth)
+  {
+  }
+
+  label_text get_desc (bool can_colorize) const FINAL OVERRIDE
+  {
+    return make_label_text (can_colorize, "entry to %qE", m_fndecl);
+  }
+
+  checker_event *clone () const FINAL OVERRIDE
+  {
+    return new function_entry_event (m_loc, m_fndecl, m_depth);
+  }
+
+  bool is_function_entry_p () const FINAL OVERRIDE { return true; }
+};
+
+/* Subclass of checker_event describing a state change.  */
+
+class state_change_event : public checker_event
+{
+public:
+  state_change_event (const supernode *node, const gimple *stmt,
+		      int stack_depth,
+		      const state_machine &sm,
+		      tree var,
+		      state_machine::state_t from,
+		      state_machine::state_t to,
+		      tree origin,
+		      const program_state &dst_state)
+  : checker_event (EK_STATE_CHANGE,
+		   stmt->location, node->m_fun->decl,
+		   stack_depth),
+    m_node (node), m_stmt (stmt), m_sm (sm),
+    m_var (var), m_from (from), m_to (to),
+    m_origin (origin),
+    m_dst_state (dst_state)
+  {
+  }
+
+  /* Attempt to generate a nicer human-readable description.
+     For greatest precision-of-wording, give the pending diagnostic
+     a chance to describe this state change (in terms of the
+     diagnostic).
+     Note that we only have a pending_diagnostic set on the event once
+     the diagnostic is about to being emitted, so the description for
+     an event can change.  */
+
+  label_text get_desc (bool can_colorize) const FINAL OVERRIDE
+  {
+    if (m_pending_diagnostic)
+      {
+	/* FIXME: pass m_origin to it.  */
+	label_text custom_desc
+	  = m_pending_diagnostic->describe_state_change
+	  (evdesc::state_change (can_colorize, m_var, m_origin,
+				 m_from, m_to, m_emission_id));
+	if (custom_desc.m_buffer)
+	  {
+	    if (flag_analyzer_verbose_state_changes)
+	      {
+		/* Append debug version.  */
+		label_text result;
+		if (m_origin)
+		  result = make_label_text
+		    (can_colorize,
+		     "%s (state of %qE: %qs -> %qs, origin: %qE)",
+		     custom_desc.m_buffer,
+		     m_var,
+		     m_sm.get_state_name (m_from),
+		     m_sm.get_state_name (m_to),
+		     m_origin);
+		else
+		  result = make_label_text
+		    (can_colorize,
+		     "%s (state of %qE: %qs -> %qs, origin: NULL)",
+		     custom_desc.m_buffer,
+		     m_var,
+		     m_sm.get_state_name (m_from),
+		     m_sm.get_state_name (m_to));
+		custom_desc.maybe_free ();
+		return result;
+	      }
+	    else
+	      return custom_desc;
+	  }
+      }
+
+    /* Fallback description.  */
+    if (m_origin)
+      return make_label_text
+	(can_colorize,
+	 "state of %qE: %qs -> %qs (origin: %qE)",
+	 m_var,
+	 m_sm.get_state_name (m_from),
+	 m_sm.get_state_name (m_to),
+	 m_origin);
+    else
+      return make_label_text
+	(can_colorize,
+	 "state of %qE: %qs -> %qs (origin: NULL)",
+	 m_var,
+	 m_sm.get_state_name (m_from),
+	 m_sm.get_state_name (m_to));
+  }
+
+  checker_event *clone () const FINAL OVERRIDE
+  {
+    return new state_change_event (m_node, m_stmt, m_depth,
+				   m_sm, m_var, m_from, m_to, m_origin,
+				   m_dst_state);
+  }
+
+  region_id get_lvalue (tree expr) const
+  {
+    return m_dst_state.m_region_model->get_lvalue (expr, NULL);
+  }
+
+  const supernode *m_node;
+  const gimple *m_stmt;
+  const state_machine &m_sm;
+  tree m_var;
+  state_machine::state_t m_from;
+  state_machine::state_t m_to;
+  tree m_origin;
+  program_state m_dst_state;
+};
+
+/* Subclass of checker_event; parent class for subclasses that relate to
+   a superedge.  */
+
+class superedge_event : public checker_event
+{
+public:
+  superedge_event (enum event_kind kind, const exploded_edge &eedge,
+		   location_t loc, tree fndecl, int depth)
+  : checker_event (kind, loc, fndecl, depth),
+    m_eedge (eedge), m_sedge (eedge.m_sedge),
+    m_var (NULL_TREE), m_critical_state (0)
+  {
+  }
+
+  /* Mark this edge event as being either an interprocedural call or
+     return in which VAR is in STATE, and that this is critical to the
+     diagnostic (so that get_desc can attempt to get a better description
+     from any pending_diagnostic).  */
+  void record_critical_state (tree var, state_machine::state_t state)
+  {
+    m_var = var;
+    m_critical_state = state;
+  }
+
+  const callgraph_superedge& get_callgraph_superedge () const
+  {
+    gcc_assert (m_sedge->m_kind != SUPEREDGE_CFG_EDGE);
+    return *m_sedge->dyn_cast_callgraph_superedge ();
+  }
+
+  bool should_filter_p (int verbosity) const
+  {
+    switch (m_sedge->m_kind)
+      {
+      case SUPEREDGE_CFG_EDGE:
+	{
+	  if (verbosity < 2)
+	    return true;
+
+	  if (verbosity == 2)
+	    {
+	      /* Filter events with empty descriptions.  This ought to filter
+		 FALLTHRU, but retain true/false/switch edges.  */
+	      label_text desc = get_desc (false);
+	      gcc_assert (desc.m_buffer);
+	      if (desc.m_buffer[0] == '\0')
+		return true;
+	      desc.maybe_free ();
+	    }
+	}
+	break;
+
+	default:
+	  break;
+      }
+    return false;
+  }
+
+  const exploded_edge &m_eedge;
+  const superedge *m_sedge;
+  tree m_var;
+  state_machine::state_t m_critical_state;
+};
+
+/* An event subclass for when a CFG edge is followed; it has two
+   subclasses, representing the start of the edge and the end of the
+   edge, which come in pairs.  */
+
+class cfg_edge_event : public superedge_event
+{
+public:
+  cfg_edge_event (enum event_kind kind, const exploded_edge &eedge,
+		  location_t loc, tree fndecl, int depth)
+  : superedge_event (kind, eedge, loc, fndecl, depth)
+  {
+    gcc_assert (eedge.m_sedge->m_kind == SUPEREDGE_CFG_EDGE);
+  }
+
+  const cfg_superedge& get_cfg_superedge () const
+  {
+    return *m_sedge->dyn_cast_cfg_superedge ();
+  }
+};
+
+/* An event subclass for the start of a CFG edge
+   e.g. "following 'false' branch...'.  */
+
+class start_cfg_edge_event : public cfg_edge_event
+{
+public:
+  start_cfg_edge_event (const exploded_edge &eedge,
+			location_t loc, tree fndecl, int depth)
+  : cfg_edge_event (EK_START_CFG_EDGE, eedge, loc, fndecl, depth)
+  {
+  }
+
+  checker_event *clone () const FINAL OVERRIDE
+  {
+    return new start_cfg_edge_event (m_eedge, m_loc, m_fndecl, m_depth);
+  }
+
+  label_text get_desc (bool can_colorize) const FINAL OVERRIDE
+  {
+    bool user_facing = !flag_analyzer_verbose_edges;
+    char *edge_desc = m_sedge->get_description (user_facing);
+    if (user_facing)
+      {
+	if (edge_desc && strlen (edge_desc) > 0)
+	  {
+	    label_text result = make_label_text (can_colorize,
+						 "following %qs branch...",
+						 edge_desc);
+	    free (edge_desc);
+	    return result;
+	  }
+	else
+	  {
+	    free (edge_desc);
+	    return label_text::borrow ("");
+	  }
+      }
+    else
+      {
+	if (strlen (edge_desc) > 0)
+	  {
+	    label_text result
+	      = make_label_text (can_colorize,
+				 "taking %qs edge SN:%i -> SN:%i",
+				 edge_desc,
+				 m_sedge->m_src->m_index,
+				 m_sedge->m_dest->m_index);
+	    free (edge_desc);
+	    return result;
+	  }
+	else
+	  {
+	    free (edge_desc);
+	    return make_label_text (can_colorize,
+				    "taking edge SN:%i -> SN:%i",
+				    m_sedge->m_src->m_index,
+				    m_sedge->m_dest->m_index);
+	  }
+      }
+  }
+};
+
+/* An event subclass for the end of a CFG edge
+   e.g. "...to here'.  */
+
+class end_cfg_edge_event : public cfg_edge_event
+{
+public:
+  end_cfg_edge_event (const exploded_edge &eedge,
+		      location_t loc, tree fndecl, int depth)
+  : cfg_edge_event (EK_END_CFG_EDGE, eedge, loc, fndecl, depth)
+  {
+  }
+
+  checker_event *clone () const FINAL OVERRIDE
+  {
+    return new end_cfg_edge_event (m_eedge, m_loc, m_fndecl, m_depth);
+  }
+
+  label_text get_desc (bool /*can_colorize*/) const FINAL OVERRIDE
+  {
+    return label_text::borrow ("...to here");
+  }
+};
+
+/* An event subclass for an interprocedural call.  */
+
+class call_event : public superedge_event
+{
+public:
+  call_event (const exploded_edge &eedge,
+	      location_t loc, tree fndecl, int depth)
+  : superedge_event (EK_CALL_EDGE, eedge, loc, fndecl, depth)
+  {
+    gcc_assert (eedge.m_sedge->m_kind == SUPEREDGE_CALL);
+  }
+
+  checker_event *clone () const FINAL OVERRIDE
+  {
+    return new call_event (m_eedge, m_loc, m_fndecl, m_depth);
+  }
+
+  label_text get_desc (bool can_colorize) const FINAL OVERRIDE
+  {
+    if (m_critical_state && m_pending_diagnostic)
+      {
+	gcc_assert (m_var);
+	label_text custom_desc
+	  = m_pending_diagnostic->describe_call_with_state
+	  (evdesc::call_with_state (can_colorize,
+				    m_sedge->m_src->m_fun->decl,
+				    m_sedge->m_dest->m_fun->decl,
+				    m_var,
+				    m_critical_state));
+	if (custom_desc.m_buffer)
+	  return custom_desc;
+      }
+
+    return make_label_text (can_colorize,
+			    "calling %qE from %qE",
+			    m_sedge->m_dest->m_fun->decl,
+			    m_sedge->m_src->m_fun->decl);
+  }
+
+  bool is_call_p () const FINAL OVERRIDE
+  {
+    return m_sedge->m_kind == SUPEREDGE_CALL;
+  }
+};
+
+/* An event subclass for an interprocedural return.  */
+
+class return_event : public superedge_event
+{
+public:
+  return_event (const exploded_edge &eedge,
+		location_t loc, tree fndecl, int depth)
+  : superedge_event (EK_RETURN_EDGE, eedge, loc, fndecl, depth)
+  {
+    gcc_assert (eedge.m_sedge->m_kind == SUPEREDGE_RETURN);
+  }
+
+  checker_event *clone () const FINAL OVERRIDE
+  {
+    return new return_event (m_eedge, m_loc, m_fndecl, m_depth);
+  }
+
+  label_text get_desc (bool can_colorize) const FINAL OVERRIDE
+  {
+    /*  For greatest precision-of-wording, if this is returning the
+	state involved in the pending diagnostic, give the pending
+	diagnostic a chance to describe this return (in terms of
+	itself).  */
+    if (m_critical_state && m_pending_diagnostic)
+      {
+	label_text custom_desc
+	  = m_pending_diagnostic->describe_return_of_state
+	  (evdesc::return_of_state (can_colorize,
+				    m_sedge->m_dest->m_fun->decl,
+				    m_sedge->m_src->m_fun->decl,
+				    m_critical_state));
+	if (custom_desc.m_buffer)
+	  return custom_desc;
+      }
+    return make_label_text (can_colorize,
+			    "returning to %qE from %qE",
+			    m_sedge->m_dest->m_fun->decl,
+			    m_sedge->m_src->m_fun->decl);
+  }
+
+  bool is_return_p () const FINAL OVERRIDE
+  {
+    return m_sedge->m_kind == SUPEREDGE_RETURN;
+  }
+};
+
+/* An event subclass for a setjmp call.  */
+
+class setjmp_event : public checker_event
+{
+public:
+  setjmp_event (location_t loc, const exploded_node *enode,
+		tree fndecl, int depth)
+  : checker_event (EK_SETJMP, loc, fndecl, depth),
+    m_enode (enode)
+  {
+  }
+
+  setjmp_event *clone () const FINAL OVERRIDE
+  {
+    return new setjmp_event (m_loc, m_enode, m_fndecl, m_depth);
+  }
+
+  label_text get_desc (bool can_colorize) const FINAL OVERRIDE
+  {
+    return make_label_text (can_colorize,
+			    "%qs called here",
+			    "setjmp");
+  }
+
+  void prepare_for_emission (checker_path *path,
+			     pending_diagnostic *pd,
+			     diagnostic_event_id_t emission_id) FINAL OVERRIDE;
+
+private:
+  const exploded_node *m_enode;
+};
+
+/* An event subclass for rewinding from a longjmp to a setjmp.
+   Base class for two from/to subclasses, showing the two halves of the
+   rewind.  */
+
+class rewind_event : public checker_event
+{
+public:
+  rewind_event (const exploded_edge *eedge,
+		enum event_kind kind,
+		location_t loc, tree fndecl, int depth)
+  : checker_event (kind, loc, fndecl, depth),
+    m_eedge (eedge)
+  {
+    gcc_assert (m_eedge->m_rewind_info);
+  }
+
+  tree get_longjmp_caller () const
+  {
+    return m_eedge->m_src->get_function ()->decl;
+  }
+
+  tree get_setjmp_caller () const
+  {
+    return m_eedge->m_dest->get_function ()->decl;
+  }
+
+  const exploded_edge *get_eedge () const { return m_eedge; }
+
+private:
+  const exploded_edge *m_eedge;
+};
+
+/* An event subclass for rewinding from a longjmp to a setjmp,
+   showing the longjmp.  */
+
+class rewind_from_longjmp_event : public rewind_event
+{
+public:
+  rewind_from_longjmp_event (const exploded_edge *eedge,
+			     location_t loc, tree fndecl, int depth)
+  : rewind_event (eedge, EK_REWIND_FROM_LONGJMP, loc, fndecl, depth)
+  {
+  }
+
+  rewind_from_longjmp_event *clone () const FINAL OVERRIDE
+  {
+    return new rewind_from_longjmp_event (get_eedge (),
+					  m_loc, m_fndecl, m_depth);
+  }
+
+  label_text get_desc (bool can_colorize) const FINAL OVERRIDE
+  {
+    const char *src_name = "longjmp";
+
+    if (get_longjmp_caller () == get_setjmp_caller ())
+      /* Special-case: purely intraprocedural rewind.  */
+      return make_label_text (can_colorize,
+			      "rewinding within %qE from %qs...",
+			      get_longjmp_caller (),
+			      src_name);
+    else
+      return make_label_text (can_colorize,
+			      "rewinding from %qs in %qE...",
+			      src_name,
+			      get_longjmp_caller ());
+  }
+};
+
+/* An event subclass for rewinding from a longjmp to a setjmp,
+   showing the setjmp.  */
+
+class rewind_to_setjmp_event : public rewind_event
+{
+public:
+  rewind_to_setjmp_event (const exploded_edge *eedge,
+			  location_t loc, tree fndecl, int depth)
+  : rewind_event (eedge, EK_REWIND_TO_SETJMP, loc, fndecl, depth)
+  {
+  }
+
+  rewind_to_setjmp_event *clone () const FINAL OVERRIDE
+  {
+    return new rewind_to_setjmp_event (get_eedge (),
+				       m_loc, m_fndecl, m_depth);
+  }
+
+  void prepare_for_emission (checker_path *path,
+			     pending_diagnostic *pd,
+			     diagnostic_event_id_t emission_id) FINAL OVERRIDE;
+
+  label_text get_desc (bool can_colorize) const FINAL OVERRIDE
+  {
+    const char *dst_name = "setjmp";
+
+    /* If we can, identify the ID of the setjmp_event.  */
+    if (m_original_setjmp_event_id.known_p ())
+      {
+	if (get_longjmp_caller () == get_setjmp_caller ())
+	  /* Special-case: purely intraprocedural rewind.  */
+	  return make_label_text (can_colorize,
+				  "...to %qs (saved at %@)",
+				  dst_name,
+				  &m_original_setjmp_event_id);
+	else
+	  return make_label_text (can_colorize,
+				  "...to %qs in %qE (saved at %@)",
+				  dst_name,
+				  get_setjmp_caller (),
+				  &m_original_setjmp_event_id);
+      }
+    else
+      {
+	if (get_longjmp_caller () == get_setjmp_caller ())
+	  /* Special-case: purely intraprocedural rewind.  */
+	  return make_label_text (can_colorize,
+				  "...to %qs",
+				  dst_name,
+				  get_setjmp_caller ());
+	else
+	  return make_label_text (can_colorize,
+				  "...to %qs in %qE",
+				  dst_name,
+				  get_setjmp_caller ());
+      }
+  }
+
+private:
+  diagnostic_event_id_t m_original_setjmp_event_id;
+};
+
+/* Subclass of checker_event for use at the end of a path: a repeat of
+   the warning message at the end of the path, at the point where the
+   problem occurs.  */
+
+class warning_event : public checker_event
+{
+public:
+  warning_event (location_t loc, tree fndecl, int depth,
+		 const state_machine *sm,
+		 tree var, state_machine::state_t state)
+  : checker_event (EK_WARNING, loc, fndecl, depth),
+    m_sm (sm), m_var (var), m_state (state)
+  {
+  }
+
+  label_text get_desc (bool can_colorize) const FINAL OVERRIDE
+  {
+    if (m_pending_diagnostic)
+      {
+	label_text ev_desc = m_pending_diagnostic->describe_final_event
+	  (evdesc::final_event (can_colorize, m_var, m_state));
+	if (ev_desc.m_buffer)
+	  {
+	    if (m_sm && flag_analyzer_verbose_state_changes)
+	      {
+		label_text result
+		  = make_label_text (can_colorize,
+				     "%s (%qE is in state %qs)",
+				     ev_desc.m_buffer,
+				     m_var,m_sm->get_state_name (m_state));
+		ev_desc.maybe_free ();
+		return result;
+	      }
+	    else
+	      return ev_desc;
+	  }
+      }
+
+    if (m_sm)
+      return make_label_text (can_colorize,
+			      "here (%qE is in state %qs)",
+			      m_var,
+			      m_sm->get_state_name (m_state));
+    else
+      return label_text::borrow ("here");
+  }
+
+  warning_event *clone () const FINAL OVERRIDE
+  {
+    return new warning_event (m_loc, m_fndecl, m_depth, m_sm, m_var, m_state);
+  }
+
+private:
+  const state_machine *m_sm;
+  tree m_var;
+  state_machine::state_t m_state;
+};
+
+////////////////////////////////////////////////////////////////////////////
+
+/* Subclass of diagnostic_path for analyzer diagnostics.  */
+
+class checker_path : public diagnostic_path
+{
+public:
+  checker_path () : diagnostic_path () {}
+
+  void dump (pretty_printer *pp) const;
+
+  void maybe_log (logger *logger, const char *desc) const
+  {
+    if (!logger)
+      return;
+    logger->start_log_line ();
+    logger->log_partial ("%s: ", desc);
+    dump (logger->get_printer ());
+    logger->end_log_line ();
+    for (unsigned i = 0; i < m_events.length (); i++)
+      {
+	logger->start_log_line ();
+	logger->log_partial ("%s[%i]: %s ", desc, i,
+			     event_kind_to_string (m_events[i]->m_kind));
+	m_events[i]->dump (logger->get_printer ());
+	logger->end_log_line ();
+      }
+  }
+
+  unsigned num_events () const FINAL OVERRIDE
+  {
+    return m_events.length ();
+  }
+
+  const diagnostic_event & get_event (int idx) const FINAL OVERRIDE
+  {
+    return *m_events[idx];
+  }
+
+  void add_event (checker_event *event)
+  {
+    m_events.safe_push (event);
+  }
+
+  void delete_event (int idx)
+  {
+    checker_event *event = m_events[idx];
+    m_events.ordered_remove (idx);
+    delete event;
+  }
+
+  void add_final_event (const state_machine *sm,
+			const exploded_node *enode, const gimple *stmt,
+			tree var, state_machine::state_t state)
+  {
+    checker_event *end_of_path
+      = new warning_event (stmt->location,
+			   enode->get_function ()->decl,
+			   enode->get_stack_depth (),
+			   sm, var, state);
+    add_event (end_of_path);
+  }
+
+  /* After all event-pruning, a hook for notifying each event what
+     its ID will be.  The events are notified in order, allowing
+     for later events to refer to the IDs of earlier events in
+     their descriptions.  */
+  void prepare_for_emission (pending_diagnostic *pd)
+  {
+    checker_event *e;
+    int i;
+    FOR_EACH_VEC_ELT (m_events, i, e)
+      e->prepare_for_emission (this, pd, diagnostic_event_id_t (i));
+  }
+
+  void record_setjmp_event (const exploded_node *enode,
+			    diagnostic_event_id_t setjmp_emission_id)
+  {
+    m_setjmp_event_ids.put (enode, setjmp_emission_id);
+  }
+
+  bool get_setjmp_event (const exploded_node *enode,
+			 diagnostic_event_id_t *out_emission_id)
+  {
+    if (diagnostic_event_id_t *emission_id = m_setjmp_event_ids.get (enode))
+      {
+	*out_emission_id = *emission_id;
+	return true;
+      }
+    return false;
+  }
+
+  ////////////////////////////////////////////////////////////////////////////
+
+  /* The events that have occurred along this path.  */
+  auto_delete_vec<checker_event> m_events;
+
+  /* During prepare_for_emission (and after), the setjmp_event for each
+     exploded_node *, so that rewind events can refer to them in their
+     descriptions.  */
+  hash_map <const exploded_node *, diagnostic_event_id_t> m_setjmp_event_ids;
+};
+
+////////////////////////////////////////////////////////////////////////////
+
+/* class setjmp_event : public checker_event.  */
+
+/* Implemenation of checker_event::prepare_for_emission vfunc for setjmp_event.
+
+   Record this setjmp's event ID into the path, so that rewind events can
+   use it.  */
+
+void
+setjmp_event::prepare_for_emission (checker_path *path,
+				    pending_diagnostic *pd,
+				    diagnostic_event_id_t emission_id)
+{
+  checker_event::prepare_for_emission (path, pd, emission_id);
+  path->record_setjmp_event (m_enode, emission_id);
+}
+
+/* class rewind_to_setjmp_event : public rewind_event.  */
+
+/* Implemenation of checker_event::prepare_for_emission vfunc for
+   rewind_to_setjmp_event.
+
+   Attempt to look up the setjmp event ID that recorded the jmp_buf
+   for this rewind.  */
+
+void
+rewind_to_setjmp_event::prepare_for_emission (checker_path *path,
+					      pending_diagnostic *pd,
+					      diagnostic_event_id_t emission_id)
+{
+  checker_event::prepare_for_emission (path, pd, emission_id);
+  path->get_setjmp_event (get_eedge ()->m_rewind_info->get_enode_origin (),
+			  &m_original_setjmp_event_id);
+}
+
+////////////////////////////////////////////////////////////////////////////
+
+/* class setjmp_svalue : public svalue.  */
+
+/* Compare the fields of this setjmp_svalue with OTHER, returning true
+   if they are equal.
+   For use by svalue::operator==.  */
+
+bool
+setjmp_svalue::compare_fields (const setjmp_svalue &other) const
+{
+  return m_enode == other.m_enode;
+}
+
+/* Implementation of svalue::hash vfunc for setjmp_svalue.  */
+
+hashval_t
+setjmp_svalue::hash () const
+{
+  return m_enode->m_index;
+}
+
+/* Get the index of the stored exploded_node.  */
+
+int
+setjmp_svalue::get_index () const
+{
+  return m_enode->m_index;
+}
+
+/* Implementation of svalue::print_details vfunc for setjmp_svalue.  */
+
+void
+setjmp_svalue::print_details (const region_model &model ATTRIBUTE_UNUSED,
+			      svalue_id this_sid ATTRIBUTE_UNUSED,
+			      pretty_printer *pp) const
+{
+  pp_printf (pp, "setjmp: EN: %i", m_enode->m_index);
+}
+
+
+////////////////////////////////////////////////////////////////////////////
+
+/* class call_string.  */
+
+/* call_string's copy ctor.  */
+
+call_string::call_string (const call_string &other)
+: m_return_edges (other.m_return_edges.length ())
+{
+  const return_superedge *e;
+  int i;
+  FOR_EACH_VEC_ELT (other.m_return_edges, i, e)
+    m_return_edges.quick_push (e);
+}
+
+/* call_string's assignment operator.  */
+
+call_string&
+call_string::operator= (const call_string &other)
+{
+  // would be much simpler if we could rely on vec<> assignment op
+  m_return_edges.truncate (0);
+  m_return_edges.reserve (other.m_return_edges.length (), true);
+  const return_superedge *e;
+  int i;
+  FOR_EACH_VEC_ELT (other.m_return_edges, i, e)
+    m_return_edges.quick_push (e);
+  return *this;
+}
+
+/* call_string's equality operator.  */
+
+bool
+call_string::operator== (const call_string &other) const
+{
+  if (m_return_edges.length () != other.m_return_edges.length ())
+    return false;
+  const return_superedge *e;
+  int i;
+  FOR_EACH_VEC_ELT (m_return_edges, i, e)
+    if (e != other.m_return_edges[i])
+      return false;
+  return true;
+}
+
+/* Print this to PP.  */
+
+void
+call_string::print (pretty_printer *pp) const
+{
+  pp_string (pp, "[");
+
+  const return_superedge *e;
+  int i;
+  FOR_EACH_VEC_ELT (m_return_edges, i, e)
+    {
+      if (i > 0)
+	pp_string (pp, ", ");
+      pp_printf (pp, "(SN: %i -> SN: %i in %s)",
+		 e->m_src->m_index, e->m_dest->m_index,
+		 function_name (e->m_dest->m_fun));
+    }
+
+  pp_string (pp, "]");
+}
+
+/* Generate a hash value for this call_string.  */
+
+hashval_t
+call_string::hash () const
+{
+  inchash::hash hstate;
+  int i;
+  const return_superedge *e;
+  FOR_EACH_VEC_ELT (m_return_edges, i, e)
+    hstate.add_ptr (e);
+  return hstate.end ();
+}
+
+/* Push the return superedge for CALL_SEDGE onto the end of this
+   call_string.  */
+
+void
+call_string::push_call (const supergraph &sg,
+			const call_superedge *call_sedge)
+{
+  gcc_assert (call_sedge);
+  const return_superedge *return_sedge = call_sedge->get_edge_for_return (sg);
+  gcc_assert (return_sedge);
+  m_return_edges.safe_push (return_sedge);
+}
+
+/* Count the number of times the top-most call site appears in the
+   stack.  */
+
+int
+call_string::calc_recursion_depth () const
+{
+  if (m_return_edges.is_empty ())
+    return 0;
+  const return_superedge *top_return_sedge
+    = m_return_edges[m_return_edges.length () - 1];
+
+  int result = 0;
+  const return_superedge *e;
+  int i;
+  FOR_EACH_VEC_ELT (m_return_edges, i, e)
+    if (e == top_return_sedge)
+      ++result;
+  return result;
+}
+
+/* Comparator for call strings.
+   Return negative if A is before B.
+   Return positive if B is after A.
+   Return 0 if they are equal.  */
+
+int
+call_string::cmp (const call_string &a,
+		  const call_string &b)
+{
+  int result = cmp_1 (a, b);
+
+  /* Check that the ordering is symmetric  */
+#if CHECKING_P
+  int reversed = cmp_1 (b, a);
+  gcc_assert (reversed == -result);
+#endif
+
+  /* We should only have 0 for equal pairs.  */
+  gcc_assert (result != 0
+	      || a == b);
+
+  return result;
+}
+
+/* Implementation of call_string::cmp.
+   This implements a version of lexicographical order.  */
+
+int
+call_string::cmp_1 (const call_string &a,
+		    const call_string &b)
+{
+  unsigned len_a = a.length ();
+  unsigned len_b = b.length ();
+
+  unsigned i = 0;
+  while (1)
+    {
+      /* Consider index i; the strings have been equal up to it.  */
+
+      /* Have both strings run out?  */
+      if (i >= len_a && i >= len_b)
+	return 0;
+
+      /* Otherwise, has just one of the strings run out?  */
+      if (i >= len_a)
+	return 1;
+      if (i >= len_b)
+	return -1;
+
+      /* Otherwise, compare the edges.  */
+      const return_superedge *edge_a = a[i];
+      const return_superedge *edge_b = b[i];
+      int src_cmp = edge_a->m_src->m_index - edge_b->m_src->m_index;
+      if (src_cmp)
+	return src_cmp;
+      int dest_cmp = edge_a->m_dest->m_index - edge_b->m_dest->m_index;
+      if (dest_cmp)
+	return dest_cmp;
+      i++;
+      // TODO: test coverage for this
+    }
+}
+
+////////////////////////////////////////////////////////////////////////////
+
+/* Print this program_point to PP.  */
+
+void
+program_point::print (pretty_printer *pp, const format &f) const
+{
+  pp_string (pp, "callstring: ");
+  m_call_string.print (pp);
+  f.spacer (pp);
+
+  switch (m_kind)
+    {
+    default:
+      gcc_unreachable ();
+
+    case PK_ORIGIN:
+      pp_printf (pp, "origin");
+      break;
+
+    case PK_BEFORE_SUPERNODE:
+      {
+	if (m_from_edge)
+	  pp_printf (pp, "before SN: %i (from SN: %i)",
+		     m_supernode->m_index, m_from_edge->m_src->m_index);
+	else
+	  pp_printf (pp, "before SN: %i (NULL from-edge)",
+		     m_supernode->m_index);
+	f.spacer (pp);
+	for (gphi_iterator gpi
+	       = const_cast<supernode *>(get_supernode ())->start_phis ();
+	     !gsi_end_p (gpi); gsi_next (&gpi))
+	  {
+	    const gphi *phi = gpi.phi ();
+	    pp_gimple_stmt_1 (pp, phi, 0, (dump_flags_t)0);
+	  }
+      }
+      break;
+
+    case PK_BEFORE_STMT:
+      pp_printf (pp, "before (SN: %i stmt: %i): ", m_supernode->m_index,
+		 m_stmt_idx);
+      f.spacer (pp);
+      pp_gimple_stmt_1 (pp, get_stmt (), 0, (dump_flags_t)0);
+      if (f.m_newlines)
+	{
+	  pp_newline (pp);
+	  print_source_line (pp);
+	}
+      break;
+
+    case PK_AFTER_SUPERNODE:
+      pp_printf (pp, "after SN: %i", m_supernode->m_index);
+      break;
+    }
+}
+
+/* A subclass of diagnostic_context for use by
+   program_point::print_source_line.  */
+
+class debug_diagnostic_context : public diagnostic_context
+{
+public:
+  debug_diagnostic_context ()
+  {
+    diagnostic_initialize (this, 0);
+    show_line_numbers_p = true;
+    show_caret = true;
+  }
+  ~debug_diagnostic_context ()
+  {
+    diagnostic_finish (this);
+  }
+};
+
+/* Print the source line (if any) for this program_point to PP.  */
+
+void
+program_point::print_source_line (pretty_printer *pp) const
+{
+  const gimple *stmt = get_stmt ();
+  if (!stmt)
+    return;
+  // TODO: monospace font
+  debug_diagnostic_context tmp_dc;
+  gcc_rich_location richloc (stmt->location);
+  diagnostic_show_locus (&tmp_dc, &richloc, DK_ERROR);
+  pp_string (pp, pp_formatted_text (tmp_dc.printer));
+}
+
+/* Dump this point to stderr.  */
+
+DEBUG_FUNCTION void
+program_point::dump () const
+{
+  pretty_printer pp;
+  pp_show_color (&pp) = pp_show_color (global_dc->printer);
+  pp.buffer->stream = stderr;
+  print (&pp, format (true));
+  pp_flush (&pp);
+}
+
+/* Generate a hash value for this program_point.  */
+
+hashval_t
+program_point::hash () const
+{
+  inchash::hash hstate;
+  if (m_supernode)
+    hstate.add_int (m_supernode->m_index);
+  hstate.add_ptr (m_from_edge);
+  hstate.add_int (m_stmt_idx);
+  hstate.add_int (m_kind);
+  hstate.merge_hash (m_call_string.hash ());
+  return hstate.end ();
+}
+
+/* Get the gimple stmt for this program_point, if any.  */
+
+const gimple *
+program_point::get_stmt () const
+{
+  if (m_kind == PK_BEFORE_STMT)
+    return m_supernode->m_stmts[m_stmt_idx];
+  else if (m_kind == PK_AFTER_SUPERNODE)
+    return m_supernode->get_last_stmt ();
+  else
+    return NULL;
+}
+
+/* Get a location for this program_point, if any.  */
+
+location_t
+program_point::get_location () const
+{
+  const gimple *stmt = get_stmt ();
+  if (stmt)
+    return stmt->location;
+
+  return UNKNOWN_LOCATION;
+}
+
+/* Check to see if SUCC is a valid edge to take (ensuring that we have
+   interprocedurally valid paths in the exploded graph, and enforcing
+   recursion limits).
+
+   Return true if SUCC can be taken, or false otherwise.
+
+   This is the "point" half of exploded_node::on_edge.  */
+
+bool
+program_point::on_edge (exploded_graph &eg,
+			const superedge *succ)
+{
+  LOG_FUNC (eg.get_logger ());
+  switch (succ->m_kind)
+    {
+    case SUPEREDGE_CFG_EDGE:
+      {
+	const cfg_superedge *cfg_sedge = as_a <const cfg_superedge *> (succ);
+
+	/* Reject abnormal edges; we special-case setjmp/longjmp.  */
+	if (cfg_sedge->get_flags () & EDGE_ABNORMAL)
+	  return false;
+      }
+      break;
+
+    case SUPEREDGE_CALL:
+      {
+	const call_superedge *call_sedge = as_a <const call_superedge *> (succ);
+
+	if (eg.get_analysis_plan ().use_summary_p (call_sedge->m_cedge))
+	  {
+	    eg.log ("rejecting call edge: using summary instead");
+	    return false;
+	  }
+
+	/* Add the callsite to the call string.  */
+	m_call_string.push_call (eg.get_supergraph (), call_sedge);
+
+	/* Impose a maximum recursion depth and don't analyze paths
+	   that exceed it further.
+	   This is something of a blunt workaround, but it only
+	   applies to recursion (and mutual recursion), not to
+	   general call stacks.  */
+	// FIXME: make this a param:
+	const int MAX_RECURSION_DEPTH = 2;
+	if (m_call_string.calc_recursion_depth () > MAX_RECURSION_DEPTH)
+	  {
+	    eg.log ("rejecting call edge: recursion limit exceeded");
+	    // TODO: issue a sorry for this?
+	    return false;
+	  }
+      }
+      break;
+
+    case SUPEREDGE_RETURN:
+      {
+	/* Require that we return to the call site in the call string.  */
+	if (m_call_string.empty_p ())
+	  {
+	    eg.log ("rejecting return edge: empty call string");
+	    return false;
+	  }
+	const return_superedge *top_of_stack = m_call_string.pop ();
+	if (top_of_stack != succ)
+	  {
+	    eg.log ("rejecting return edge: return to wrong callsite");
+	    return false;
+	  }
+      }
+      break;
+
+    case SUPEREDGE_INTRAPROCEDURAL_CALL:
+      {
+	const callgraph_superedge *cg_sedge
+	  = as_a <const callgraph_superedge *> (succ);
+	/* Consider turning this edge into a use of an
+	   interprocedural summary.  */
+	if (eg.get_analysis_plan ().use_summary_p (cg_sedge->m_cedge))
+	  {
+	    eg.log ("using function summary for %qE in %qE",
+		    cg_sedge->get_callee_decl (),
+		    cg_sedge->get_caller_decl ());
+	    return true;
+	  }
+	else
+	  {
+	    /* Otherwise, we ignore these edges  */
+	    eg.log ("rejecting interprocedural edge");
+	    return false;
+	  }
+      }
+    }
+
+  return true;
+}
+
+/* Comparator for program points within the same supernode,
+   for implementing worklist::key_t comparison operators.
+   Return negative if POINT_A is before POINT_B
+   Return positive if POINT_A is after POINT_B
+   Return 0 if they are equal.  */
+
+int
+program_point::cmp_within_supernode_1 (const program_point &point_a,
+				       const program_point &point_b)
+{
+  gcc_assert (point_a.m_supernode == point_b.m_supernode);
+
+  switch (point_a.m_kind)
+    {
+    default:
+      gcc_unreachable ();
+    case PK_BEFORE_SUPERNODE:
+      switch (point_b.m_kind)
+	{
+	default:
+	  gcc_unreachable ();
+	case PK_BEFORE_SUPERNODE:
+	  {
+	    int a_src_idx = -1;
+	    int b_src_idx = -1;
+	    if (point_a.m_from_edge)
+	      a_src_idx = point_a.m_from_edge->m_src->m_index;
+	    if (point_b.m_from_edge)
+	      b_src_idx = point_b.m_from_edge->m_src->m_index;
+	    return a_src_idx - b_src_idx;
+	  }
+	  break;
+
+	case PK_BEFORE_STMT:
+	case PK_AFTER_SUPERNODE:
+	  return -1;
+	}
+      break;
+    case PK_BEFORE_STMT:
+      switch (point_b.m_kind)
+	{
+	default:
+	  gcc_unreachable ();
+	case PK_BEFORE_SUPERNODE:
+	  return 1;
+
+	case PK_BEFORE_STMT:
+	  return point_a.m_stmt_idx - point_b.m_stmt_idx;
+
+	case PK_AFTER_SUPERNODE:
+	  return -1;
+	}
+      break;
+    case PK_AFTER_SUPERNODE:
+      switch (point_b.m_kind)
+	{
+	default:
+	  gcc_unreachable ();
+	case PK_BEFORE_SUPERNODE:
+	case PK_BEFORE_STMT:
+	  return 1;
+
+	case PK_AFTER_SUPERNODE:
+	  return 0;
+	}
+      break;
+    }
+}
+
+/* Comparator for program points within the same supernode,
+   for implementing worklist::key_t comparison operators.
+   Return negative if POINT_A is before POINT_B
+   Return positive if POINT_A is after POINT_B
+   Return 0 if they are equal.  */
+
+int
+program_point::cmp_within_supernode (const program_point &point_a,
+				     const program_point &point_b)
+{
+  int result = cmp_within_supernode_1 (point_a, point_b);
+
+  /* Check that the ordering is symmetric  */
+#if CHECKING_P
+  int reversed = cmp_within_supernode_1 (point_b, point_a);
+  gcc_assert (reversed == -result);
+#endif
+
+  return result;
+}
+
+////////////////////////////////////////////////////////////////////////////
+
+/* Clone the sm_state_map.  */
+
+sm_state_map *
+sm_state_map::clone () const
+{
+  return new sm_state_map (*this);
+}
+
+/* Clone this sm_state_map, remapping all svalue_ids within it with ID_MAP.  */
+
+sm_state_map *
+sm_state_map::clone_with_remapping (const one_way_svalue_id_map &id_map) const
+{
+  sm_state_map *result = new sm_state_map ();
+  for (typename map_t::iterator iter = m_map.begin ();
+       iter != m_map.end ();
+       ++iter)
+    {
+      svalue_id sid = (*iter).first;
+      entry_t e = (*iter).second;
+      id_map.update (&e.m_origin);
+      result->m_map.put (id_map.get_dst_for_src (sid), e);
+    }
+  return result;
+}
+
+/* Print this sm_state_map (for SM) to PP.  */
+
+void
+sm_state_map::print (const state_machine &sm, pretty_printer *pp) const
+{
+  pp_string (pp, "{");
+  for (typename map_t::iterator iter = m_map.begin ();
+       iter != m_map.end ();
+       ++iter)
+    {
+      if (iter != m_map.begin ())
+	pp_string (pp, ", ");
+      svalue_id sid = (*iter).first;
+      sid.print (pp);
+
+      entry_t e = (*iter).second;
+      pp_printf (pp, ": %s (origin: ",
+		 sm.get_state_name (e.m_state));
+      e.m_origin.print (pp);
+      pp_string (pp, ")");
+    }
+  pp_string (pp, "}");
+}
+
+/* Dump this object (for SM) to stderr.  */
+
+DEBUG_FUNCTION void
+sm_state_map::dump (const state_machine &sm) const
+{
+  pretty_printer pp;
+  pp_show_color (&pp) = pp_show_color (global_dc->printer);
+  pp.buffer->stream = stderr;
+  print (sm, &pp);
+  pp_newline (&pp);
+  pp_flush (&pp);
+}
+
+/* Return true if no states have been set within this map
+   (all expressions are for the start state).  */
+
+bool
+sm_state_map::is_empty_p () const
+{
+  return m_map.elements () == 0;
+}
+
+/* Generate a hash value for this sm_state_map.  */
+
+hashval_t
+sm_state_map::hash () const
+{
+  inchash::hash hstate;
+
+  for (typename map_t::iterator iter = m_map.begin ();
+       iter != m_map.end ();
+       ++iter)
+    {
+      inchash::add ((*iter).first, hstate);
+      entry_t e = (*iter).second;
+      hstate.add_int (e.m_state);
+      inchash::add (e.m_origin, hstate);
+    }
+
+  return hstate.end ();
+}
+
+/* Equality operator for sm_state_map.  */
+
+bool
+sm_state_map::operator== (const sm_state_map &other) const
+{
+  if (m_map.elements () != other.m_map.elements ())
+    return false;
+
+  for (typename map_t::iterator iter = m_map.begin ();
+       iter != m_map.end ();
+       ++iter)
+    {
+      svalue_id sid = (*iter).first;
+      entry_t e = (*iter).second;
+      entry_t *other_slot = const_cast <map_t &> (other.m_map).get (sid);
+      if (other_slot == NULL)
+	return false;
+      if (e != *other_slot)
+	return false;
+    }
+
+  return true;
+}
+
+/* Get the state of SID within this object.
+   States default to the start state.  */
+
+state_machine::state_t
+sm_state_map::get_state (svalue_id sid) const
+{
+  if (entry_t *slot
+      = const_cast <map_t &> (m_map).get (sid))
+    return slot->m_state;
+  else
+    return 0;
+}
+
+/* Get the "origin" svalue_id for any state of SID.  */
+
+svalue_id
+sm_state_map::get_origin (svalue_id sid) const
+{
+  entry_t *slot
+    = const_cast <map_t &> (m_map).get (sid);
+  gcc_assert (slot);
+  return slot->m_origin;
+}
+
+/* Set the state of SID within MODEL to STATE, recording that
+   the state came from ORIGIN.  */
+
+void
+sm_state_map::set_state (region_model *model,
+			 svalue_id sid,
+			 state_machine::state_t state,
+			 svalue_id origin)
+{
+  if (model == NULL)
+    return;
+  equiv_class &ec = model->get_constraints ()->get_equiv_class (sid);
+  set_state (ec, state, origin);
+}
+
+/* Set the state of EC to STATE, recording that the state came from
+   ORIGIN.  */
+
+void
+sm_state_map::set_state (const equiv_class &ec,
+			 state_machine::state_t state,
+			 svalue_id origin)
+{
+  int i;
+  svalue_id *sid;
+  FOR_EACH_VEC_ELT (ec.m_vars, i, sid)
+    impl_set_state (*sid, state, origin);
+}
+
+/* Set state of PV to STATE, bypassing equivalence classes.  */
+
+void
+sm_state_map::impl_set_state (svalue_id sid, state_machine::state_t state,
+			      svalue_id origin)
+{
+  /* Special-case state 0 as the default value.  */
+  if (state == 0)
+    {
+      if (m_map.get (sid))
+	m_map.remove (sid);
+      return;
+    }
+  m_map.put (sid, entry_t (state, origin));
+}
+
+/* Handle CALL to unknown FNDECL with an unknown function body, which
+   could do anything to the states passed to it.
+   Clear any state for SM for the params and any LHS.
+   Note that the function might be known to other state machines, but
+   not to this one.  */
+
+void
+sm_state_map::purge_for_unknown_fncall (const exploded_graph &eg,
+					const state_machine &sm,
+					const gcall *call,
+					tree fndecl,
+					region_model *new_model)
+{
+  if (fndecl)
+    eg.log ("function %qE is unknown to checker %qs",
+	    fndecl, sm.get_name ());
+  else
+    eg.log ("unknown function pointer for checker %qs",
+	    sm.get_name ());
+
+  /* Purge any state for parms.  */
+  tree iter_param_types = NULL_TREE;
+  if (fndecl)
+    iter_param_types = TYPE_ARG_TYPES (TREE_TYPE (fndecl));
+  for (unsigned arg_idx = 0; arg_idx < gimple_call_num_args (call); arg_idx++)
+    {
+      /* Track expected param type, where available.  */
+      if (iter_param_types)
+	{
+	  tree param_type = TREE_VALUE (iter_param_types);
+	  gcc_assert (param_type);
+	  iter_param_types = TREE_CHAIN (iter_param_types);
+
+	  /* Don't purge state if it was passed as a const pointer
+	     e.g. for things like strlen (PTR).  */
+	  if (TREE_CODE (param_type) == POINTER_TYPE)
+	    if (TYPE_READONLY (TREE_TYPE (param_type)))
+	      continue;
+	}
+      tree parm = gimple_call_arg (call, arg_idx);
+      svalue_id parm_sid = new_model->get_rvalue (parm, NULL); // FIXME: ctxt!
+      set_state (new_model, parm_sid, 0, svalue_id::null ());
+    }
+
+  /* Purge any state for any LHS.  */
+  if (tree lhs = gimple_call_lhs (call))
+    {
+      svalue_id lhs_sid = new_model->get_rvalue (lhs, NULL); // FIXME: ctxt!
+      set_state (new_model, lhs_sid, 0, svalue_id::null ());
+    }
+}
+
+/* Update this map based on MAP.  */
+
+void
+sm_state_map::remap_svalue_ids (const svalue_id_map &map)
+{
+  map_t tmp_map;
+
+  /* Build an intermediate map, using the new sids.  */
+  for (typename map_t::iterator iter = m_map.begin ();
+       iter != m_map.end ();
+       ++iter)
+    {
+      svalue_id sid = (*iter).first;
+      entry_t e = (*iter).second;
+
+      map.update (&sid);
+      map.update (&e.m_origin);
+      tmp_map.put (sid, e);
+    }
+
+  /* Clear the existing values.  */
+  m_map.empty ();
+
+  /* Copy over from intermediate map.  */
+  for (typename map_t::iterator iter = tmp_map.begin ();
+       iter != tmp_map.end ();
+       ++iter)
+    {
+      svalue_id sid = (*iter).first;
+      entry_t e = (*iter).second;
+
+      impl_set_state (sid, e.m_state, e.m_origin);
+    }
+}
+
+/* Purge any state for svalue_ids >= FIRST_UNUSED_SID.
+   If !SM::can_purge_p, then report the state as leaking,
+   using SM_IDX, CTXT, and MAP.
+   Return the number of states that were purged.  */
+
+int
+sm_state_map::on_svalue_purge (const state_machine &sm,
+			       int sm_idx,
+			       svalue_id first_unused_sid,
+			       const svalue_id_map &map,
+			       region_model_context *ctxt)
+{
+  /* FIXME: not sure how to safely remove during a traversal, so for now
+     do it in two stages.  */
+  auto_vec<svalue_id> to_remove;
+  for (typename map_t::iterator iter = m_map.begin ();
+       iter != m_map.end ();
+       ++iter)
+    {
+      svalue_id sid ((*iter).first);
+      if (sid.as_int () >= first_unused_sid.as_int ())
+	{
+	  // TODO: complain about leaks here!
+	  entry_t e = (*iter).second;
+	  //gcc_unreachable ();
+
+	  if (!sm.can_purge_p (e.m_state))
+	    {
+	      //gcc_unreachable ();
+	      ctxt->on_state_leak (sm, sm_idx, sid, map, e.m_state);
+	    }
+
+#if 0
+	  // FIXME: we need a tree, an snode, a stmt, and an sm_ctxt
+	  sm.on_leak (sm_context *sm_ctxt,
+		      const supernode *node,
+		      const gimple *stmt,
+		      tree var,
+		      e.m_state);
+#endif
+
+	  to_remove.safe_push (sid);
+	}
+    }
+
+  int i;
+  svalue_id *sid;
+  FOR_EACH_VEC_ELT (to_remove, i, sid)
+    m_map.remove (*sid);
+
+  return to_remove.length ();
+}
+
+/* Set the state of CHILD_SID to that of PARENT_SID.  */
+
+void
+sm_state_map::on_inherited_svalue (svalue_id parent_sid,
+				   svalue_id child_sid)
+{
+  state_machine::state_t state = get_state (parent_sid);
+  impl_set_state (child_sid, state, parent_sid);
+}
+
+/* Assert that this object is sane.  */
+
+void
+sm_state_map::validate (const state_machine &sm,
+			int num_svalues) const
+{
+  /* Skip this in a release build.  */
+#if !CHECKING_P
+  return;
+#endif
+
+  for (typename map_t::iterator iter = m_map.begin ();
+       iter != m_map.end ();
+       ++iter)
+    {
+      svalue_id sid = (*iter).first;
+      entry_t e = (*iter).second;
+
+      gcc_assert (sid.as_int () < num_svalues);
+      sm.validate (e.m_state);
+      gcc_assert (e.m_origin.as_int () < num_svalues);
+    }
+}
+
+////////////////////////////////////////////////////////////////////////////
+
+/* FIXME.  */
+
+class impl_region_model_context : public region_model_context, public log_user
+{
+ public:
+  impl_region_model_context (exploded_graph &eg,
+			     const exploded_node *enode_for_diag,
+
+			     /* FIXME: should we be getting the ECs from the
+				old state, rather than the new?  */
+			     const program_state *old_state,
+			     program_state *new_state,
+
+			     const gimple *stmt,
+			     stmt_finder *stmt_finder = NULL)
+  : log_user (eg.get_logger ()), m_eg (&eg),
+    m_enode_for_diag (enode_for_diag),
+    m_old_state (old_state),
+    m_new_state (new_state),
+    m_stmt (stmt),
+    m_stmt_finder (stmt_finder),
+    m_ext_state (eg.get_ext_state ())
+  {
+  }
+
+  impl_region_model_context (program_state *state,
+			     const extrinsic_state &ext_state)
+  : log_user (NULL), m_eg (NULL), m_enode_for_diag (NULL),
+    m_old_state (NULL),
+    m_new_state (state),
+    m_stmt (NULL),
+    m_stmt_finder (NULL),
+    m_ext_state (ext_state)
+  {
+  }
+
+  void warn (pending_diagnostic *d) FINAL OVERRIDE
+  {
+    LOG_FUNC (get_logger ());
+    if (m_eg)
+      m_eg->get_diagnostic_manager ().add_diagnostic
+	(m_enode_for_diag, m_enode_for_diag->get_supernode (),
+	 m_stmt, m_stmt_finder, d);
+  }
+
+  void remap_svalue_ids (const svalue_id_map &map) FINAL OVERRIDE
+  {
+    m_new_state->remap_svalue_ids (map);
+  }
+
+  /* FIXME: does this ever get called?  */
+  bool can_purge_p (svalue_id sid) FINAL OVERRIDE
+  {
+    /* Don't purge vars that have non-purgeable sm state, to avoid
+       generating false "leak" complaints.  */
+    int i;
+    sm_state_map *smap;
+    FOR_EACH_VEC_ELT (m_new_state->m_checker_states, i, smap)
+      {
+	const state_machine &sm = m_ext_state.get_sm (i);
+	if (!sm.can_purge_p (smap->get_state (sid)))
+	  return false;
+      }
+    return true;
+  }
+
+  int on_svalue_purge (svalue_id first_unused_sid,
+		       const svalue_id_map &map) FINAL OVERRIDE
+  {
+    int total = 0;
+    int sm_idx;
+    sm_state_map *smap;
+    FOR_EACH_VEC_ELT (m_new_state->m_checker_states, sm_idx, smap)
+      {
+	const state_machine &sm = m_ext_state.get_sm (sm_idx);
+	total += smap->on_svalue_purge (sm, sm_idx, first_unused_sid,
+					map, this);
+      }
+    return total;
+  }
+
+  logger *get_logger () FINAL OVERRIDE
+  {
+    return log_user::get_logger ();
+  }
+
+  void on_state_leak (const state_machine &sm,
+		      int sm_idx,
+		      svalue_id sid,
+		      const svalue_id_map &map,
+		      state_machine::state_t state) FINAL OVERRIDE;
+
+  void on_inherited_svalue (svalue_id parent_sid,
+			    svalue_id child_sid) FINAL OVERRIDE;
+
+  void on_condition (tree lhs, enum tree_code op, tree rhs) FINAL OVERRIDE;
+
+  exploded_graph *m_eg;
+  const exploded_node *m_enode_for_diag;
+  const program_state *m_old_state;
+  program_state *m_new_state;
+  const gimple *m_stmt;
+  stmt_finder *m_stmt_finder;
+  const extrinsic_state &m_ext_state;
+};
+
+////////////////////////////////////////////////////////////////////////////
+
+/* FIXME.  */
+
+class impl_sm_context : public sm_context, public log_user
+{
+public:
+  impl_sm_context (exploded_graph &eg,
+		   const state_machine &sm,
+		   const exploded_node *enode_for_diag,
+
+		   /* FIXME: should we be getting the ECs from the
+		      old state, rather than the new?  */
+		   const program_state *old_state,
+		   program_state *new_state,
+
+		   const sm_state_map *old_smap,
+		   sm_state_map *new_smap,
+
+		   stmt_finder *stmt_finder = NULL)
+  : sm_context (sm),
+    log_user (eg.get_logger ()),
+    m_eg (eg), m_enode_for_diag (enode_for_diag),
+    m_old_state (old_state), m_new_state (new_state),
+    m_old_smap (old_smap), m_new_smap (new_smap),
+    m_stmt_finder (stmt_finder)
+  {
+  }
+
+  void on_transition (const supernode *node  ATTRIBUTE_UNUSED,
+		      const gimple *stmt  ATTRIBUTE_UNUSED,
+		      tree var,
+		      state_machine::state_t from,
+		      state_machine::state_t to,
+		      tree origin) FINAL OVERRIDE
+  {
+    LOG_FUNC (get_logger ());
+    impl_region_model_context old_ctxt
+      (m_eg, m_enode_for_diag, NULL, NULL/*m_enode->get_state ()*/, NULL);
+    svalue_id var_old_sid
+      = m_old_state->m_region_model->get_rvalue (var, &old_ctxt);
+
+    impl_region_model_context new_ctxt (m_eg, m_enode_for_diag,
+					m_old_state, m_new_state, NULL);
+    svalue_id var_new_sid
+      = m_new_state->m_region_model->get_rvalue (var, &new_ctxt);
+    svalue_id origin_new_sid
+      = m_new_state->m_region_model->get_rvalue (origin, &new_ctxt);
+
+    state_machine::state_t current = m_old_smap->get_state (var_old_sid);
+    if (current == from)
+      {
+	if (get_logger ())
+	  log ("%s: state transition of %qE: %s -> %s",
+	       m_sm.get_name (),
+	       var,
+	       m_sm.get_state_name (from),
+	       m_sm.get_state_name (to));
+	m_new_smap->set_state (m_new_state->m_region_model, var_new_sid,
+			       to, origin_new_sid);
+      }
+  }
+
+  void warn_for_state (const supernode *snode, const gimple *stmt,
+		       tree var, state_machine::state_t state,
+		       pending_diagnostic *d) FINAL OVERRIDE
+  {
+    LOG_FUNC (get_logger ());
+    gcc_assert (d); // take ownership
+
+    impl_region_model_context old_ctxt
+      (m_eg, m_enode_for_diag, m_old_state, m_new_state, NULL);
+    svalue_id var_old_sid
+      = m_old_state->m_region_model->get_rvalue (var, &old_ctxt);
+    state_machine::state_t current = m_old_smap->get_state (var_old_sid);
+    if (state == current)
+      {
+	m_eg.get_diagnostic_manager ().add_diagnostic
+	  (&m_sm, m_enode_for_diag, snode, stmt, m_stmt_finder,
+	   var, state, d);
+      }
+    else
+      delete d;
+  }
+
+  /* Hook for picking more readable trees for SSA names of temporaries,
+     so that rather than e.g.
+       "double-free of '<unknown>'"
+     we can print:
+       "double-free of 'inbuf.data'".  */
+
+  tree get_readable_tree (tree expr) FINAL OVERRIDE
+  {
+    /* Only for SSA_NAMEs of temporaries; otherwise, return EXPR, as it's
+       likely to be the least surprising tree to report.  */
+    if (TREE_CODE (expr) != SSA_NAME)
+      return expr;
+    if (SSA_NAME_VAR (expr) != NULL)
+      return expr;
+
+    gcc_assert (m_new_state);
+    svalue_id sid = m_new_state->m_region_model->get_rvalue (expr, NULL);
+    /* Find trees for all regions storing the value.  */
+    auto_vec<path_var> pvs;
+    m_new_state->m_region_model->get_path_vars_for_svalue (sid, &pvs);
+    if (pvs.length () < 1)
+      return expr;
+    /* Pick the "best" such tree.  */
+    // TODO: should we also consider (and consolidate) equiv classes?
+    pvs.qsort (readability_comparator);
+    return pvs[0].m_tree;
+  }
+
+  exploded_graph &m_eg;
+  const exploded_node *m_enode_for_diag;
+  const program_state *m_old_state;
+  program_state *m_new_state;
+  const sm_state_map *m_old_smap;
+  sm_state_map *m_new_smap;
+  stmt_finder *m_stmt_finder;
+};
+
+////////////////////////////////////////////////////////////////////////////
+
+/* Subclass of stmt_finder for finding the best stmt to report the leak at,
+   given the emission path.  */
+
+class leak_stmt_finder : public stmt_finder
+{
+public:
+  leak_stmt_finder (const exploded_graph &eg, tree var)
+  : m_eg (eg), m_var (var) {}
+
+  stmt_finder *clone () const FINAL OVERRIDE
+  {
+    return new leak_stmt_finder (m_eg, m_var);
+  }
+
+  const gimple *find_stmt (const exploded_path &epath)
+    FINAL OVERRIDE
+  {
+    LOG_FUNC (m_eg.get_logger ());
+
+    if (TREE_CODE (m_var) == SSA_NAME)
+      {
+	/* Locate the final write to this SSA name in the path.  */
+	const gimple *def_stmt = SSA_NAME_DEF_STMT (m_var);
+
+	int idx_of_def_stmt;
+	bool found = epath.find_stmt_backwards (def_stmt, &idx_of_def_stmt);
+#if 0
+	gcc_assert (found);
+#else
+	if (!found)
+	  goto not_found;
+#endif
+
+	/* What was the next write to the underlying var
+	   after the SSA name was set? (if any).  */
+
+	for (unsigned idx = idx_of_def_stmt + 1;
+	     idx < epath.m_edges.length ();
+	     ++idx)
+	  {
+	    const exploded_edge *eedge = epath.m_edges[idx];
+	    if (m_eg.get_logger ())
+	      m_eg.log ("eedge[%i]: EN %i -> EN %i",
+			idx,
+			eedge->m_src->m_index,
+			eedge->m_dest->m_index);
+	    const exploded_node *dst_node = eedge->m_dest;
+	    const program_point &dst_point = dst_node->get_point ();
+	    const gimple *stmt = dst_point.get_stmt ();
+	    if (!stmt)
+	      continue;
+	    if (const gassign *assign = dyn_cast <const gassign *> (stmt))
+	      {
+		tree lhs = gimple_assign_lhs (assign);
+		if (TREE_CODE (lhs) == SSA_NAME
+		    && SSA_NAME_VAR (lhs) == SSA_NAME_VAR (m_var))
+		  return assign;
+	      }
+	  }
+      }
+
+    /* FIXME: not sure why this is needed.  */
+  not_found:
+
+    /* Look backwards for the first statement with a location.  */
+    int i;
+    const exploded_edge *eedge;
+    FOR_EACH_VEC_ELT_REVERSE (epath.m_edges, i, eedge)
+      {
+	if (m_eg.get_logger ())
+	  m_eg.log ("eedge[%i]: EN %i -> EN %i",
+		    i,
+		    eedge->m_src->m_index,
+		    eedge->m_dest->m_index);
+	const exploded_node *dst_node = eedge->m_dest;
+	const program_point &dst_point = dst_node->get_point ();
+	const gimple *stmt = dst_point.get_stmt ();
+	if (stmt)
+	  if (stmt->location != UNKNOWN_LOCATION)
+	    return stmt;
+      }
+
+    gcc_unreachable ();
+    return NULL;
+  }
+
+private:
+  const exploded_graph &m_eg;
+  tree m_var;
+};
+
+////////////////////////////////////////////////////////////////////////////
+
+/* A measurement of how good EXPR is for presenting to the user, so
+   that e.g. we can say prefer printing
+     "leak of 'tmp.m_ptr'"
+   over:
+     "leak of '<unknown>'".  */
+
+static int
+readability (const_tree expr)
+{
+  gcc_assert (expr);
+  switch (TREE_CODE (expr))
+    {
+    case COMPONENT_REF:
+    case MEM_REF:
+      /* Impose a slight readability penalty relative to that of
+	 operand 0.  */
+      return readability (TREE_OPERAND (expr, 0)) - 1;
+
+    case SSA_NAME:
+      {
+	if (tree var = SSA_NAME_VAR (expr))
+	  return readability (var);
+	/* Avoid printing '<unknown>' for SSA names for temporaries.  */
+	return -1;
+      }
+      break;
+
+    case VAR_DECL:
+      /* Arbitrarily-chosen "high readability" value.  */
+      return 256;
+
+    default:
+      return 0;
+    }
+
+  return 0;
+}
+
+/* A qsort comparator for trees to sort them into most user-readable to
+   least user-readable.  */
+
+static int
+readability_comparator (const void *p1, const void *p2)
+{
+  path_var pv1 = *(path_var const *)p1;
+  path_var pv2 = *(path_var const *)p2;
+
+  /* FIXME: should we consider stack depths?  */
+  int r1 = readability (pv1.m_tree);
+  int r2 = readability (pv2.m_tree);
+
+  return r2 - r1;
+}
+
+/* FIXME.  */
+
+void
+impl_region_model_context::on_state_leak (const state_machine &sm,
+					  int sm_idx,
+					  svalue_id sid,
+					  const svalue_id_map &map,
+					  state_machine::state_t state)
+{
+  if (!m_eg)
+    return;
+
+  /* FIXME: m_old_state also needs to be non-NULL so warn_for_state can look
+     up the state of the sid.  */
+  gcc_assert (m_old_state);
+
+  /* SID has leaked within the new state: no regions use it.
+     We need to convert it back to a tree, but since no regions use it, we
+     have to use MAP to convert it back to an svalue_id within the old state.
+     We can then look that svalue_id up to locate regions and thus tree(s)
+     that use it.  */
+
+  svalue_id old_sid = map.get_src_for_dst (sid);
+
+  auto_vec<path_var> leaked_pvs;
+  m_old_state->m_region_model->get_path_vars_for_svalue (old_sid, &leaked_pvs);
+
+  // FIXME: find "best" leaked tree
+  // TODO: should we also consider (and consolidate) equiv classes?
+  // FIXME: rewrite this comment:
+  /* Sort the leaks into most human-readable first, through
+     to least user-readable.  Given that we only emit one
+     leak per EC, this ought to ensure that we pick the most
+     user-readable description of each leaking EC.
+     This assumes that all vars in the EC have the same state.  */
+  leaked_pvs.qsort (readability_comparator);
+
+  if (leaked_pvs.length () < 1)
+    return;
+
+  tree leaked_tree = leaked_pvs[0].m_tree;
+  if (get_logger ())
+    log ("best leaked_tree: %qE", leaked_tree);
+
+  leak_stmt_finder stmt_finder (*m_eg, leaked_tree);
+  impl_sm_context sm_ctxt (*m_eg, sm, m_enode_for_diag,
+			   m_old_state, m_new_state,
+			   m_old_state->m_checker_states[sm_idx],
+			   m_new_state->m_checker_states[sm_idx],
+			   &stmt_finder);
+
+  sm.on_leak (&sm_ctxt, m_enode_for_diag->get_supernode (), m_stmt,
+	      leaked_tree, state);
+}
+
+/* FIXME.  */
+
+void
+impl_region_model_context::on_inherited_svalue (svalue_id parent_sid,
+						svalue_id child_sid)
+{
+  /* FIXME: */
+  if (!m_new_state)
+    return;
+
+  int sm_idx;
+  sm_state_map *smap;
+  FOR_EACH_VEC_ELT (m_new_state->m_checker_states, sm_idx, smap)
+    {
+      const state_machine &sm = m_ext_state.get_sm (sm_idx);
+      if (sm.inherited_state_p ())
+	smap->on_inherited_svalue (parent_sid, child_sid);
+    }
+}
+
+/* Implementation of region_model_context::on_condition vfunc.
+   Notify all state machines about the condition, which could lead to
+   state transitions.  */
+
+void
+impl_region_model_context::on_condition (tree lhs, enum tree_code op, tree rhs)
+{
+  int sm_idx;
+  sm_state_map *smap;
+  FOR_EACH_VEC_ELT (m_new_state->m_checker_states, sm_idx, smap)
+    {
+      const state_machine &sm = m_ext_state.get_sm (sm_idx);
+      impl_sm_context sm_ctxt (*m_eg, sm, m_enode_for_diag,
+			       m_old_state, m_new_state,
+			       m_old_state->m_checker_states[sm_idx],
+			       m_new_state->m_checker_states[sm_idx]);
+      sm.on_condition (&sm_ctxt,
+		       m_enode_for_diag->get_supernode (), m_stmt,
+		       lhs, op, rhs);
+    }
+}
+
+////////////////////////////////////////////////////////////////////////////
+
+/* class program_state.  */
+
+/* program_state's ctor.  */
+
+program_state::program_state (const extrinsic_state &ext_state)
+: m_region_model (new region_model ()),
+  m_checker_states (ext_state.m_checkers.length ())
+{
+  int num_states = ext_state.m_checkers.length ();
+  for (int i = 0; i < num_states; i++)
+    m_checker_states.quick_push (new sm_state_map ());
+}
+
+/* program_state's copy ctor.  */
+
+program_state::program_state (const program_state &other)
+: m_region_model (new region_model (*other.m_region_model)),
+  m_checker_states (other.m_checker_states.length ())
+{
+  int i;
+  sm_state_map *smap;
+  FOR_EACH_VEC_ELT (other.m_checker_states, i, smap)
+    m_checker_states.quick_push (smap->clone ());
+}
+
+/* program_state's assignment operator.  */
+
+program_state&
+program_state::operator= (const program_state &other)
+{
+  delete m_region_model;
+  m_region_model = new region_model (*other.m_region_model);
+
+  int i;
+  sm_state_map *smap;
+  FOR_EACH_VEC_ELT (m_checker_states, i, smap)
+    delete smap;
+  m_checker_states.truncate (0);
+  gcc_assert (m_checker_states.space (other.m_checker_states.length ()));
+
+  FOR_EACH_VEC_ELT (other.m_checker_states, i, smap)
+    m_checker_states.quick_push (smap->clone ());
+
+  return *this;
+}
+
+#if __cplusplus >= 201103
+/* FIXME.  */
+program_state::program_state (program_state &&other)
+: m_region_model (other.m_region_model),
+  m_checker_states (other.m_checker_states.length ())
+{
+  other.m_region_model = NULL;
+
+  // FIXME:
+  gcc_assert (m_checker_states.length () == 0);
+
+  int i;
+  sm_state_map *smap;
+  FOR_EACH_VEC_ELT (other.m_checker_states, i, smap)
+    m_checker_states.quick_push (smap);
+  other.m_checker_states.truncate (0);
+}
+#endif
+
+/* program_state's dtor.  */
+
+program_state::~program_state ()
+{
+  delete m_region_model;
+}
+
+/* Generate a hash value for this program_state.  */
+
+hashval_t
+program_state::hash () const
+{
+  hashval_t result = m_region_model->hash ();
+
+  int i;
+  sm_state_map *smap;
+  FOR_EACH_VEC_ELT (m_checker_states, i, smap)
+    result ^= smap->hash ();
+  return result;
+}
+
+/* Equality operator for program_state.
+   All parts of the program_state (region model, checker states) must
+   equal their counterparts in OTHER for the two program_states to be
+   considered equal.  */
+
+bool
+program_state::operator== (const program_state &other) const
+{
+  if (!(*m_region_model == *other.m_region_model))
+    return false;
+
+  int i;
+  sm_state_map *smap;
+  FOR_EACH_VEC_ELT (m_checker_states, i, smap)
+    if (!(*smap == *other.m_checker_states[i]))
+      return false;
+  return true;
+}
+
+/* Print a compact representation of this state to PP.  */
+
+void
+program_state::print (const extrinsic_state &ext_state,
+		      pretty_printer *pp) const
+{
+  pp_printf (pp, "rmodel: ");
+  m_region_model->print (pp);
+  pp_newline (pp);
+
+  int i;
+  sm_state_map *smap;
+  FOR_EACH_VEC_ELT (m_checker_states, i, smap)
+    {
+      if (!smap->is_empty_p ())
+	{
+	  pp_printf (pp, "%s: ", ext_state.get_name (i));
+	  smap->print (ext_state.get_sm (i), pp);
+	  pp_newline (pp);
+	}
+    }
+}
+
+/* Dump a multiline representation of this state to PP.  */
+
+void
+program_state::dump_to_pp (const extrinsic_state &ext_state,
+			   bool summarize,
+			   pretty_printer *pp) const
+{
+  pp_printf (pp, "rmodel: ");
+  m_region_model->dump_to_pp (pp, summarize);
+
+  int i;
+  sm_state_map *smap;
+  FOR_EACH_VEC_ELT (m_checker_states, i, smap)
+    {
+      if (!smap->is_empty_p ())
+	{
+	  pp_printf (pp, "%s: ", ext_state.get_name (i));
+	  smap->print (ext_state.get_sm (i), pp);
+	  pp_newline (pp);
+	}
+    }
+}
+
+/* Dump a multiline representation of this state to OUTF.  */
+
+void
+program_state::dump_to_file (const extrinsic_state &ext_state,
+			     bool summarize,
+			     FILE *outf) const
+{
+  pretty_printer pp;
+  pp_format_decoder (&pp) = default_tree_printer;
+  if (outf == stderr)
+    pp_show_color (&pp) = pp_show_color (global_dc->printer);
+  pp.buffer->stream = outf;
+  dump_to_pp (ext_state, summarize, &pp);
+  pp_flush (&pp);
+}
+
+/* Dump a multiline representation of this state to stderr.  */
+
+DEBUG_FUNCTION void
+program_state::dump (const extrinsic_state &ext_state,
+		     bool summarize) const
+{
+  dump_to_file (ext_state, summarize, stderr);
+}
+
+/* Determine if following edge SUCC from ENODE is valid within the graph EG
+   and update this state accordingly in-place.
+
+   Return true if the edge can be followed, or false otherwise.
+
+   Check for relevant conditionals and switch-values for conditionals
+   and switch statements, adding the relevant conditions to this state.
+   Push/pop frames for interprocedural edges and update params/returned
+   values.
+
+   This is the "state" half of exploded_node::on_edge.  */
+
+bool
+program_state::on_edge (exploded_graph &eg,
+			const exploded_node &enode,
+			const superedge *succ)
+{
+  /* Update state.  */
+  const program_point &point = enode.get_point ();
+  const gimple *last_stmt = point.m_supernode->get_last_stmt ();
+
+  /* For conditionals and switch statements, add the
+     relevant conditions (for the specific edge) to new_state;
+     skip edges for which the resulting constraints
+     are impossible.
+     This also updates frame information for call/return superedges.
+     Adding the relevant conditions for the edge could also trigger
+     sm-state transitions (e.g. transitions due to ptrs becoming known
+     to be NULL or non-NULL) */
+
+  impl_region_model_context ctxt (eg, &enode,
+				  &enode.get_state (),
+				  this,
+				  last_stmt);
+  if (!m_region_model->maybe_update_for_edge (*succ,
+					      last_stmt,
+					      &ctxt))
+    {
+      eg.log ("edge to SN: %i is impossible due to region_model constraints",
+	      succ->m_dest->m_index);
+      return false;
+    }
+
+  return true;
+}
+
+/* FIXME.
+   Generate a simpler version of THIS, discarding state that's no longer
+   relevant at POINT.
+   The idea is that we're more likely to be able to consolidate
+   multiple (point, state) into single exploded_nodes if we discard
+   irrelevant state (e.g. at the end of functions).  */
+
+program_state
+program_state::prune_for_point (exploded_graph &eg,
+				const program_point &point) const
+{
+  function *fun = point.get_function ();
+  if (!fun)
+    return *this;
+
+  /* The points in the state map don't have call strings, so flatten
+     accordingly.  */
+  program_point flat_point (point);
+  flat_point.m_call_string = call_string ();
+
+  program_state new_state (*this);
+
+  purge_stats stats;
+
+  const state_purge_map *pm = eg.get_purge_map ();
+  if (pm)
+    {
+      region_id_set purgeable_ssa_regions (new_state.m_region_model);
+      region_id frame_rid
+	= new_state.m_region_model->get_current_frame_id ();
+      frame_region *frame
+	= new_state.m_region_model->get_region <frame_region>(frame_rid);
+
+      /* FIXME: move to a member of region_model, once the state_purge_map
+	 etc decls are visible there.  */
+
+      auto_vec<tree> ssa_names_to_purge;
+      for (frame_region::map_t::iterator iter = frame->begin ();
+	   iter != frame->end ();
+	   ++iter)
+	{
+	  tree var = (*iter).first;
+	  region_id rid = (*iter).second;
+	  if (TREE_CODE (var) == SSA_NAME)
+	    {
+	      const state_purge_per_ssa_name &per_ssa
+		= pm->get_data_for_ssa_name (var);
+	      if (!per_ssa.needed_at_point_p (flat_point))
+		{
+		  region *region
+		    = new_state.m_region_model->get_region (rid);
+		  svalue_id sid = region->get_value_direct ();
+		  if (!sid.null_p ())
+		    if (!new_state.can_purge_p (eg.get_ext_state (), sid))
+		      {
+			/* (currently only state maps can keep things
+			   alive).  */
+			eg.log ("not purging RID: %i for %qE"
+				" (used by state map)",
+				rid.as_int (), var);
+			continue;
+		      }
+		  purgeable_ssa_regions.add_region (rid);
+		  ssa_names_to_purge.safe_push (var);
+		  eg.log ("purging RID: %i for %qE", rid.as_int (), var);
+		  // FIXME: we also need to remove the region from the map
+		  // note that we're mid-traversal: this is done in unbind
+		  // below
+		}
+	    }
+	}
+
+      /* Unbind the regions from the frame's map of vars-to-regions.  */
+      unsigned i;
+      tree var;
+      FOR_EACH_VEC_ELT (ssa_names_to_purge, i, var)
+	frame->unbind (var);
+
+      /* Purge the regions.  Nothing should point to them, and they
+	 should have no children, as they are for SSA names.  */
+      new_state.m_region_model->purge_regions (purgeable_ssa_regions,
+					       &stats,
+					       eg.get_logger ());
+    }
+
+  /* Purge unused svalues.  */
+  // FIXME: which enode to use, if any?
+  impl_region_model_context ctxt (eg, NULL,
+				  this,
+				  &new_state, NULL);
+  new_state.m_region_model->purge_unused_svalues (&stats, &ctxt);
+  eg.log ("num svalues purged: %i", stats.m_num_svalues);
+  eg.log ("num regions purged: %i", stats.m_num_regions);
+  eg.log ("num equiv_classes purged: %i", stats.m_num_equiv_classes);
+  eg.log ("num constraints purged: %i", stats.m_num_constraints);
+  eg.log ("num sm map items purged: %i", stats.m_num_client_items);
+
+  new_state.m_region_model->canonicalize (&ctxt);
+
+  return new_state;
+}
+
+/* Remap all svalue_ids in this state's m_checker_states according to MAP.
+   The svalues_ids in the region_model are assumed to already have been
+   remapped.  */
+
+void
+program_state::remap_svalue_ids (const svalue_id_map &map)
+{
+  int i;
+  sm_state_map *smap;
+  FOR_EACH_VEC_ELT (m_checker_states, i, smap)
+    smap->remap_svalue_ids (map);
+}
+
+/* Attempt to return a tree that represents SID, or return NULL_TREE.
+   Find the first region that stores the value (e.g. a local) and
+   generate a representative tree for it.  */
+
+tree
+program_state::get_representative_tree (svalue_id sid) const
+{
+  return m_region_model->get_representative_tree (sid);
+}
+
+/* Attempt to merge this state with OTHER, both using EXT_STATE.
+   Write the result to *OUT.
+   If the states were merged successfully, return true.  */
+
+bool
+program_state::can_merge_with_p (const program_state &other,
+				 const extrinsic_state &ext_state,
+				 program_state *out) const
+{
+  gcc_assert (out);
+
+  /* FIXME:  initially I had an early reject here if there
+     are sm-differences between the states.  However, this was
+     falsely rejecting merger opportunities for states where the
+     only difference was in svalue_id ordering.  */
+  int i;
+  sm_state_map *smap;
+#if 0
+  FOR_EACH_VEC_ELT (m_checker_states, i, smap)
+    if (!(*smap == *other.m_checker_states[i]))
+      return false;
+#endif
+
+  svalue_id_merger_mapping sid_mapping (*m_region_model,
+					*other.m_region_model);
+  if (!m_region_model->can_merge_with_p (*other.m_region_model,
+					 out->m_region_model,
+					 &sid_mapping))
+    return false;
+
+  /* Copy m_checker_states to result, remapping svalue_ids using
+     sid_mapping.  */
+  FOR_EACH_VEC_ELT (out->m_checker_states, i, smap)
+    delete smap;
+  out->m_checker_states.truncate (0);
+
+  /* Remap this and other's m_checker_states using sid_mapping.
+     Only merge states that have equality between the two end-results:
+     sm-state differences are likely to be interesting to end-users, and
+     hence are worth exploring as separate paths in the exploded graph.  */
+  FOR_EACH_VEC_ELT (m_checker_states, i, smap)
+    {
+      sm_state_map *other_smap = other.m_checker_states[i];
+      sm_state_map *remapped_a_smap
+	= smap->clone_with_remapping (sid_mapping.m_map_from_a_to_m);
+      sm_state_map *remapped_b_smap
+	= other_smap->clone_with_remapping (sid_mapping.m_map_from_b_to_m);
+      if (*remapped_a_smap == *remapped_b_smap)
+	{
+	  out->m_checker_states.safe_push (remapped_a_smap);
+	  delete remapped_b_smap;
+	}
+      else
+	{
+	  /* Don't merge if there are sm-state differences.  */
+	  delete remapped_a_smap;
+	  delete remapped_b_smap;
+	  return false;
+	}
+    }
+
+  impl_region_model_context ctxt (out, ext_state);
+  out->m_region_model->canonicalize (&ctxt);
+
+  return true;
+}
+
+/* Assert that this object is valid.  */
+
+void
+program_state::validate (const extrinsic_state &ext_state) const
+{
+  /* Skip this in a release build.  */
+#if !CHECKING_P
+  return;
+#endif
+
+  m_region_model->validate ();
+  gcc_assert (m_checker_states.length () == ext_state.get_num_checkers ());
+  int sm_idx;
+  sm_state_map *smap;
+  FOR_EACH_VEC_ELT (m_checker_states, sm_idx, smap)
+    {
+      const state_machine &sm = ext_state.get_sm (sm_idx);
+      smap->validate (sm, m_region_model->get_num_svalues ());
+    }
+}
+
+////////////////////////////////////////////////////////////////////////////
+
+/* FIXME.  */
+
+static void
+print_run (pretty_printer *pp, int start_idx, int end_idx,
+	   bool *first_run)
+{
+  if (!(*first_run))
+    pp_string (pp, ", ");
+  *first_run = false;
+  if (start_idx == end_idx)
+    pp_printf (pp, "EN: %i", start_idx);
+  else
+    pp_printf (pp, "EN: %i-%i", start_idx, end_idx);
+}
+
+/* Print the indices within ENODES to PP, collecting them as
+   runs/singletons e.g. "EN: 4-7, EN: 20-23, EN: 42".  */
+
+static void
+print_enode_indices (pretty_printer *pp,
+		     const auto_vec<exploded_node *> &enodes)
+{
+  int cur_start_idx = -1;
+  int cur_finish_idx = -1;
+  bool first_run = true;
+  unsigned i;
+  exploded_node *enode;
+  FOR_EACH_VEC_ELT (enodes, i, enode)
+    {
+      if (cur_start_idx == -1)
+	{
+	  gcc_assert (cur_finish_idx == -1);
+	  cur_start_idx = cur_finish_idx = enode->m_index;
+	}
+      else
+	{
+	  if (enode->m_index == cur_finish_idx + 1)
+	    /* Continuation of a run.  */
+	    cur_finish_idx = enode->m_index;
+	  else
+	    {
+	      /* Finish existing run, start a new one.  */
+	      gcc_assert (cur_start_idx >= 0);
+	      gcc_assert (cur_finish_idx >= 0);
+	      print_run (pp, cur_start_idx, cur_finish_idx,
+			 &first_run);
+	      cur_start_idx = cur_finish_idx = enode->m_index;
+	    }
+	}
+    }
+  /* Finish any existing run.  */
+  if (cur_start_idx >= 0)
+    {
+      gcc_assert (cur_finish_idx >= 0);
+      print_run (pp, cur_start_idx, cur_finish_idx,
+		 &first_run);
+    }
+}
+
+////////////////////////////////////////////////////////////////////////////
+
+/* For use by dump_dot, get a value for the .dot "fillcolor" attribute.
+   Colorize by sm-state, to make it easier to see how sm-state propagates
+   through the exploded_graph.  */
+
+const char *
+exploded_node::get_dot_fillcolor () const
+{
+  const program_state &state = get_state ();
+
+  /* We want to be able to easily distinguish the no-sm-state case,
+     and to be able to distinguish cases where there's a single state
+     from each other.
+
+     Sum the sm_states, and use the result to choose from a table,
+     modulo table-size, special-casing the "no sm-state" case.   */
+  int total_sm_state = 0;
+  int i;
+  sm_state_map *smap;
+  FOR_EACH_VEC_ELT (state.m_checker_states, i, smap)
+    for (sm_state_map::iterator_t iter = smap->begin ();
+	 iter != smap->end ();
+	 ++iter)
+      total_sm_state += (*iter).second.m_state;
+
+  if (total_sm_state > 0)
+    {
+      /* An arbitrarily-picked collection of light colors.  */
+      const char * const colors[]
+	= {"azure", "coral", "cornsilk", "lightblue", "yellow"};
+      const int num_colors = sizeof (colors) / sizeof (colors[0]);
+      return colors[total_sm_state % num_colors];
+    }
+  else
+    /* No sm-state.   */
+    return "lightgrey";
+}
+
+/* Implementation of dnode::dump_dot vfunc for exploded_node.  */
+
+void
+exploded_node::dump_dot (graphviz_out *gv, const dump_args_t &) const
+{
+  pretty_printer *pp = gv->get_pp ();
+
+  dump_dot_id (pp);
+  pp_printf (pp, " [shape=%s,style=filled,fillcolor=%s,label=\"",
+	     "record",
+	     get_dot_fillcolor ());
+  pp_left_brace (pp);
+  pp_write_text_to_stream (pp);
+
+  pp_printf (pp, "EN: %i", m_index);
+  pp_newline (pp);
+
+  format f (true);
+  m_ps.m_point.print (pp, f);
+  pp_newline (pp);
+
+  // FIXME
+#if 0
+  m_ps.m_state.dump_to_pp (m_ext_state, pp);
+  pp_newline (pp);
+#endif
+
+#if 1
+  m_ps.m_state.dump_to_pp (m_ext_state, true, pp);
+  pp_newline (pp);
+#endif
+
+#if 1
+  // FIXME: copied-and-pasted from program_state::dump_dot
+  {
+  int i;
+  sm_state_map *smap;
+  FOR_EACH_VEC_ELT (m_ps.m_state.m_checker_states, i, smap)
+    {
+      if (!smap->is_empty_p ())
+	{
+	  pp_printf (pp, "%s: ", m_ext_state.get_name (i));
+	  smap->print (m_ext_state.get_sm (i), pp);
+	  pp_newline (pp);
+	}
+    }
+}
+#endif
+
+  pp_write_text_as_dot_label_to_stream (pp, /*for_record=*/true);
+
+  //TODO: dump_bb_for_graph (pp, bb);
+#if 0
+  if (m_returning_call)
+    {
+      pp_string (pp, "returning call: ");
+      pp_gimple_stmt_1 (pp, m_returning_call, 0, (dump_flags_t)0);
+      if (a)
+	a->add_stmt_annotations (pp, m_returning_call);
+      pp_write_text_as_dot_label_to_stream (pp, /*for_record=*/true);
+      pp_newline (pp);
+    }
+
+  if (entry_p ())
+    {
+      pp_string (pp, "ENTRY");
+      pp_newline (pp);
+    }
+
+  if (return_p ())
+    {
+      pp_string (pp, "EXIT");
+      pp_newline (pp);
+    }
+
+  /* Phi nodes.  */
+  for (gphi_iterator gpi = const_cast<supernode *> (this)->start_phis ();
+       !gsi_end_p (gpi); gsi_next (&gpi))
+    {
+      const gimple *stmt = gsi_stmt (gpi);
+      pp_gimple_stmt_1 (pp, stmt, 0, (dump_flags_t)0);
+      pp_write_text_as_dot_label_to_stream (pp, /*for_record=*/true);
+      pp_newline (pp);
+    }
+
+  /* Statements.  */
+  int i;
+  const gimple *stmt;
+  FOR_EACH_VEC_ELT (m_stmts, i, stmt)
+    {
+      pp_gimple_stmt_1 (pp, stmt, 0, (dump_flags_t)0);
+      if (a)
+	a->add_stmt_annotations (pp, stmt);
+      pp_write_text_as_dot_label_to_stream (pp, /*for_record=*/true);
+      pp_newline (pp);
+    }
+#endif
+
+  pp_right_brace (pp);
+  pp_string (pp, "\"];\n\n");
+  pp_flush (pp);
+
+  /* Potentially visualize additional per-node data.  */
+#if 0
+  // compare with:
+  m_state->add_extra_nodes_and_edges (this, gv);
+#endif
+
+#if 0
+  if (0)
+    {
+      /* Begin a subgraph.  */
+      gv->println ("subgraph \"cluster_state_%p\" {", (const void *)this);
+      gv->indent ();
+      gv->println ("style=\"dashed\";");
+      gv->println ("color=\"green\";");
+      gv->println ("label=\"state\";");
+      gv->println ("clusterrank=\"local\";");
+
+      pretty_printer *pp = gv->m_pp;
+
+      /* Create dummy node.  */
+      gv->write_indent ();
+      pp_printf (pp,
+		 "state_%p_foo "
+		 "[shape=%s, style=\"filled\" fillcolor=\"yellow\" label=\"",
+		 (const void *)this, "record");
+      pp_left_brace (pp);
+      pp_write_text_to_stream (pp);
+      //dump_generic_node (pp, TREE_TYPE (expr), 0, (dump_flags_t)0, false);
+      pp_string (pp, " ");
+      //dump_generic_node (pp, expr, 0, (dump_flags_t)0, false);
+      pp_write_text_as_dot_label_to_stream (pp, /*for_record=*/true);
+      pp_right_brace (pp);
+      pp_string (pp, "\"];");
+      pp_flush (pp);
+      pp_newline (pp);
+
+      /* Terminate subgraph.  */
+      gv->outdent ();
+      gv->println ("}");
+    }
+#endif
+
+}
+
+/* FIXME.  */
+
+void
+exploded_node::dump_dot_id (pretty_printer *pp) const
+{
+  pp_printf (pp, "exploded_node_%i", m_index);
+}
+
+/* Dump a multiline representation of this node to PP.  */
+
+void
+exploded_node::dump_to_pp (pretty_printer *pp) const
+{
+  pp_printf (pp, "EN: %i", m_index);
+  pp_newline (pp);
+
+  format f (true);
+  m_ps.m_point.print (pp, f);
+  pp_newline (pp);
+
+  m_ps.m_state.dump_to_pp (m_ext_state, false, pp);
+  pp_newline (pp);
+}
+
+/* Dump a multiline representation of this node to FILE.  */
+
+void
+exploded_node::dump (FILE *fp) const
+{
+  pretty_printer pp;
+  pp_format_decoder (&pp) = default_tree_printer;
+  pp_show_color (&pp) = pp_show_color (global_dc->printer);
+  pp.buffer->stream = fp;
+  dump_to_pp (&pp);
+  pp_flush (&pp);
+}
+
+/* Dump a multiline representation of this node to stderr.  */
+
+DEBUG_FUNCTION void
+exploded_node::dump () const
+{
+  dump (stderr);
+}
+
+/* FIXME.  */
+// is there a pre-canned way to do this?
+
+static bool
+fndecl_has_gimple_body_p (tree fndecl)
+{
+  if (fndecl == NULL_TREE)
+    return false;
+
+  cgraph_node *n = cgraph_node::get (fndecl);
+  if (!n)
+    return false;
+
+  return n->has_gimple_body_p ();
+}
+
+/* A pending_diagnostic subclass for implementing "__analyzer_dump_path".  */
+
+class dump_path_diagnostic
+  : public pending_diagnostic_subclass<dump_path_diagnostic>
+{
+public:
+  void emit (rich_location *richloc) FINAL OVERRIDE
+  {
+    inform (richloc, "path");
+  }
+
+  const char *get_kind () const FINAL OVERRIDE { return "dump_path_diagnostic"; }
+
+  bool operator== (const dump_path_diagnostic &) const
+  {
+    return true;
+  }
+};
+
+/* Modify STATE in place, applying the effects of the stmt at this node's
+   point.
+   Return true if there were any sm-state changes.  */
+
+bool
+exploded_node::on_stmt (exploded_graph &eg,
+			const supernode *snode,
+			const gimple *stmt,
+			program_state *state) const
+{
+  /* Preserve the old state.  It is used here for looking
+     up old checker states, for determining state transitions, and
+     also within impl_region_model_context and impl_sm_context for
+     going from tree to svalue_id.  */
+  const program_state old_state (*state);
+
+  impl_region_model_context ctxt (eg, this,
+				  &old_state, state,
+				  stmt);
+
+  if (const gassign *assign = dyn_cast <const gassign *> (stmt))
+    state->m_region_model->on_assignment (assign, &ctxt);
+
+  if (const greturn *return_ = dyn_cast <const greturn *> (stmt))
+    state->m_region_model->on_return (return_, &ctxt);
+
+  if (const gcall *call = dyn_cast <const gcall *> (stmt))
+    {
+      /* Debugging/test support.  */
+      if (is_named_call_p (call, "__dump_constraints", 0))
+	{
+	  // FIXME: port to region model?
+#if 0
+	  inform (stmt->location, "__dump_constraints: %s",
+		  new_ps->to_str ().c_str ());
+#endif
+	}
+      else if (is_named_call_p (call, "__analyzer_dump", 0))
+	{
+	  /* Handle the builtin "__analyzer_dump" by dumping state
+	     to stderr.  */
+	  dump ();
+	}
+      else if (is_named_call_p (call, "__analyzer_dump_path", 0))
+	{
+	  /* Handle the builtin "__analyzer_dump_path" by queuing a
+	     diagnostic at this exploded_node.  */
+	  ctxt.warn (new dump_path_diagnostic ());
+	}
+      else if (is_named_call_p (call, "__analyzer_dump_region_model", 0))
+	{
+	  /* Handle the builtin "__analyzer_dump_region_model" by dumping
+	     the region model's state to stderr.  */
+	  state->m_region_model->dump (false);
+	}
+      else if (is_named_call_p (call, "__analyzer_eval", 1))
+	{
+	  /* Handle the builtin "__analyzer_eval" by evaluating the input
+	     and dumping as a dummy warning, so that test cases can use
+	     dg-warning to validate the result (and so unexpected warnings will
+	     lead to DejaGnu failures).  */
+	  tree t_arg = gimple_call_arg (call, 0);
+	  tristate t
+	    = state->m_region_model->eval_condition (t_arg,
+							NE_EXPR,
+							integer_zero_node,
+							&ctxt);
+	  warning_at (call->location, 0, "%s", t.as_string ());
+	}
+      else if (is_named_call_p (call, "__analyzer_break", 0))
+	{
+	  /* Handle the builtin "__analyzer_break" by triggering a
+	     breakpoint.  */
+	  /* TODO: is there a good cross-platform way to do this?  */
+	  raise (SIGINT);
+	}
+      else if (is_setjmp_call_p (stmt))
+	state->m_region_model->on_setjmp (call, this, &ctxt);
+      else if (is_longjmp_call_p (call))
+	{
+	  on_longjmp (eg, call, state, &ctxt);
+	  return true;
+	}
+      else
+	state->m_region_model->on_call (call, &ctxt);
+    }
+
+  bool any_sm_changes = false;
+  int sm_idx;
+  sm_state_map *smap;
+  FOR_EACH_VEC_ELT (old_state.m_checker_states, sm_idx, smap)
+    {
+      const state_machine &sm = m_ext_state.get_sm (sm_idx);
+      const sm_state_map *old_smap
+	= old_state.m_checker_states[sm_idx];
+      sm_state_map *new_smap = state->m_checker_states[sm_idx];
+      impl_sm_context ctxt (eg, sm, this, &old_state, state,
+			    old_smap, new_smap);
+      /* Allow the state_machine to handle the stmt.  */
+      if (!sm.on_stmt (&ctxt, snode, stmt))
+	{
+	  /* For those stmts that were not handled by the state machine.  */
+	  if (const gcall *call = dyn_cast <const gcall *> (stmt))
+	    {
+	      tree callee_fndecl = gimple_call_fndecl (call);
+	      // TODO: maybe we can be smarter about handling function pointers?
+
+	      if (!fndecl_has_gimple_body_p (callee_fndecl))
+		new_smap->purge_for_unknown_fncall (eg, sm, call, callee_fndecl,
+						    state->m_region_model);
+	    }
+	}
+      if (*old_smap != *new_smap)
+	any_sm_changes = true;
+    }
+  return any_sm_changes;
+}
+
+/* Consider the effect of following superedge SUCC from this node.
+
+   Return true if it's feasible to follow the edge, or false
+   if it's infeasible.
+
+   Examples: if it's the "true" branch within
+   a CFG and we know the conditional is false, we know it's infeasible.
+   If it's one of multiple interprocedual "return" edge, then only
+   the edge back to the most recent callsite is feasible.
+
+   Update NEXT_STATE accordingly (e.g. to record that a condition was
+   true or false, or that the NULL-ness of a pointer has been checked,
+   pushing/popping stack frames, etc).
+
+   Update NEXT_POINT accordingly (updating the call string).  */
+
+bool
+exploded_node::on_edge (exploded_graph &eg,
+			const superedge *succ,
+			program_point *next_point,
+			program_state *next_state) const
+{
+  LOG_FUNC (eg.get_logger ());
+
+  if (!next_point->on_edge (eg, succ))
+    return false;
+
+  if (!next_state->on_edge (eg, *this, succ))
+    return false;
+
+  return true;
+}
+
+/* Verify that the stack at LONGJMP_POINT is still valid, given a call
+   to "setjmp" at SETJMP_POINT - the stack frame that "setjmp" was
+   called in must still be valid.
+
+   Caveat: this merely checks the call_strings in the points; it doesn't
+   detect the case where a frame returns and is then called again.  */
+
+static bool
+valid_longjmp_stack_p (const program_point &longjmp_point,
+		       const program_point &setjmp_point)
+{
+  if (longjmp_point.m_call_string.length ()
+      < setjmp_point.m_call_string.length ())
+    return false;
+
+  /* Check that the call strings match, up to the depth of the
+     setjmp point.  */
+  for (unsigned depth = 0; depth < setjmp_point.m_call_string.length ();
+       depth++)
+    if (longjmp_point.m_call_string[depth]
+	!= setjmp_point.m_call_string[depth])
+      return false;
+
+  return true;
+}
+
+/* A pending_diagnostic subclass for complaining about bad longjmps,
+   where the enclosing function of the "setjmp" has returned (and thus
+   the stack frame no longer exists).  */
+
+class stale_jmp_buf : public pending_diagnostic_subclass<dump_path_diagnostic>
+{
+public:
+  stale_jmp_buf (const gcall *setjmp_call, const gcall *longjmp_call)
+  : m_setjmp_call (setjmp_call), m_longjmp_call (longjmp_call)
+  {}
+
+  void emit (rich_location *richloc) FINAL OVERRIDE
+  {
+    warning_at (richloc, OPT_Wanalyzer_stale_setjmp_buffer,
+		"%qs called after enclosing function of %qs has returned",
+		"longjmp", "setjmp");
+  }
+
+  const char *get_kind () const FINAL OVERRIDE
+  { return "stale_jmp_buf"; }
+
+  bool operator== (const stale_jmp_buf &other) const
+  {
+    return (m_setjmp_call == other.m_setjmp_call
+	    && m_longjmp_call == other.m_longjmp_call);
+  }
+
+private:
+  const gcall *m_setjmp_call;
+  const gcall *m_longjmp_call;
+};
+
+/* Handle LONGJMP_CALL, a call to "longjmp".
+
+   Attempt to locate where "setjmp" was called on the jmp_buf and build an
+   exploded_node and exploded_edge to it representing a rewind to that frame,
+   handling the various kinds of failure that can occur.  */
+
+void
+exploded_node::on_longjmp (exploded_graph &eg,
+			   const gcall *longjmp_call,
+			   program_state *new_state,
+			   region_model_context *ctxt) const
+{
+  tree buf_ptr = gimple_call_arg (longjmp_call, 0);
+
+  region_model *new_region_model = new_state->m_region_model;
+  region_id buf_rid = new_region_model->deref_rvalue (buf_ptr, ctxt);
+  region *buf = new_region_model->get_region (buf_rid);
+  if (!buf)
+    return;
+
+  svalue_id buf_content_sid
+    = buf->get_value (*new_region_model, false, ctxt);
+  svalue *buf_content_sval = new_region_model->get_svalue (buf_content_sid);
+  if (!buf_content_sval)
+    return;
+  setjmp_svalue *setjmp_sval = buf_content_sval->dyn_cast_setjmp_svalue ();
+  if (!setjmp_sval)
+    return;
+
+  /* Build a custom enode and eedge for rewinding from the longjmp
+     call back to the setjmp.  */
+
+  const exploded_node *enode_origin = setjmp_sval->get_exploded_node ();
+  rewind_info_t rewind_info (enode_origin);
+
+  const gcall *setjmp_call = rewind_info.get_setjmp_call ();
+  const program_point &setjmp_point = rewind_info.get_setjmp_point ();
+
+  const program_point &longjmp_point = get_point ();
+
+  /* Verify that the setjmp's call_stack hasn't been popped.  */
+  if (!valid_longjmp_stack_p (longjmp_point, setjmp_point))
+    {
+      ctxt->warn (new stale_jmp_buf (setjmp_call, longjmp_call));
+      return;
+    }
+
+  gcc_assert (longjmp_point.get_stack_depth ()
+	      >= setjmp_point.get_stack_depth ());
+
+  /* Update the state for use by the destination node.  */
+  new_region_model->on_longjmp (longjmp_call, setjmp_call,
+				setjmp_point.get_stack_depth (), ctxt);
+
+  program_point next_point
+    = program_point::after_supernode (setjmp_point.m_supernode,
+				      setjmp_point.m_call_string);
+
+  exploded_node *next = eg.get_or_create_node (next_point, *new_state);
+
+  /* Create custom exploded_edge for a longjmp.  */
+  if (next)
+    eg.add_edge (const_cast<exploded_node *> (this), next, NULL,
+		 new rewind_info_t (enode_origin));
+}
+
+// FIXME: this comment needs rewriting:
+/* Called when finding the successors of the supernode for a function exit
+   basic block.
+
+   Detect equivalence classes that are about to go out of scope, and call
+   on_leak on any that have non-default state (on the most readable
+   path_var within each equivalence class), so that the state machine can
+   potentially issue a warning.
+
+   In theory we'd do this on the return superedge, but there isn't one
+   for top-level functions.  */
+
+void
+exploded_node::detect_leaks (exploded_graph &eg) const
+{
+  LOG_FUNC_1 (eg.get_logger (), "EN: %i", m_index);
+
+  /* FIXME: with region model, should we only do this when the callstring
+     is empty?  when we're popping the stack, purge_unused_svalues ought
+     to detect leaks due to popping the frame.  */
+
+  const program_state &old_state = get_state ();
+
+  if (get_point ().get_stack_depth () > 1)
+    return;
+  gcc_assert (get_point ().get_stack_depth () == 1);
+
+  /* Work with a temporary copy of the state: pop the frame, and see
+     what leaks (via purge_unused_svalues).  */
+  program_state new_state (old_state);
+
+  gcc_assert (new_state.m_region_model);
+
+  purge_stats stats;
+  impl_region_model_context ctxt (eg, this,
+				  &old_state, &new_state, get_stmt ());
+  new_state.m_region_model->pop_frame (true, &stats, &ctxt);
+}
+
+/* Dump the successors and predecessors of this enode to OUTF.  */
+
+void
+exploded_node::dump_succs_and_preds (FILE *outf) const
+{
+  unsigned i;
+  exploded_edge *e;
+  {
+    auto_vec<exploded_node *> preds (m_preds.length ());
+    FOR_EACH_VEC_ELT (m_preds, i, e)
+      preds.quick_push (e->m_src);
+    pretty_printer pp;
+    print_enode_indices (&pp, preds);
+    fprintf (outf, "preds: %s\n",
+	     pp_formatted_text (&pp));
+  }
+  {
+    auto_vec<exploded_node *> succs (m_succs.length ());
+    FOR_EACH_VEC_ELT (m_succs, i, e)
+      succs.quick_push (e->m_dest);
+    pretty_printer pp;
+    print_enode_indices (&pp, succs);
+    fprintf (outf, "succs: %s\n",
+	     pp_formatted_text (&pp));
+  }
+}
+
+////////////////////////////////////////////////////////////////////////////
+
+/* class exploded_edge : public dedge.  */
+
+/* exploded_edge's ctor.  */
+
+exploded_edge::exploded_edge (exploded_node *src, exploded_node *dest,
+			      const superedge *sedge,
+			      rewind_info_t *rewind_info)
+: dedge (src, dest), m_sedge (sedge), m_rewind_info (rewind_info)
+{
+}
+
+/* exploded_edge's dtor.  */
+
+exploded_edge::~exploded_edge ()
+{
+  delete m_rewind_info;
+}
+
+/* Implementation of dedge::dump_dot vfunc for exploded_edge.
+   Use the label of the underlying superedge, if any.  */
+
+void
+exploded_edge::dump_dot (graphviz_out *gv, const dump_args_t &) const
+{
+  pretty_printer *pp = gv->get_pp ();
+
+  const char *style = "\"solid,bold\"";
+  const char *color = "black";
+  int weight = 10;
+  const char *constraint = "true";
+
+  if (m_sedge)
+    switch (m_sedge->m_kind)
+      {
+      default:
+	gcc_unreachable ();
+      case SUPEREDGE_CFG_EDGE:
+	break;
+      case SUPEREDGE_CALL:
+	color = "red";
+	//constraint = "false";
+	break;
+      case SUPEREDGE_RETURN:
+	color = "green";
+	//constraint = "false";
+	break;
+      case SUPEREDGE_INTRAPROCEDURAL_CALL:
+	style = "\"dotted\"";
+	break;
+      }
+  if (m_rewind_info)
+    {
+      color = "red";
+      style = "\"dotted\"";
+    }
+
+  m_src->dump_dot_id (pp);
+  pp_string (pp, " -> ");
+  m_dest->dump_dot_id (pp);
+  pp_printf (pp,
+	     (" [style=%s, color=%s, weight=%d, constraint=%s,"
+	      " headlabel=\""),
+	     style, color, weight, constraint);
+
+  if (m_sedge)
+    m_sedge->dump_label_to_pp (pp, false);
+  else if (m_rewind_info)
+    pp_string (pp, "rewind");
+
+  pp_printf (pp, "\"];\n");
+}
+
+////////////////////////////////////////////////////////////////////////////
+
+/* struct stats.  */
+
+/* FIXME.  */
+
+stats::stats (int num_supernodes)
+: m_node_reuse_count (0),
+  m_node_reuse_after_merge_count (0),
+  m_num_supernodes (num_supernodes)
+{
+  for (int i = 0; i < NUM_POINT_KINDS; i++)
+    m_num_nodes[i] = 0;
+}
+
+/* FIXME.  */
+
+void
+stats::log (logger *logger) const
+{
+  gcc_assert (logger);
+  for (int i = 0; i < NUM_POINT_KINDS; i++)
+    logger->log ("m_num_nodes[%s]: %i",
+		 point_kind_to_string (static_cast <enum point_kind> (i)),
+		 m_num_nodes[i]);
+  logger->log ("m_node_reuse_count: %i", m_node_reuse_count);
+  logger->log ("m_node_reuse_after_merge_count: %i",
+	       m_node_reuse_after_merge_count);
+}
+
+/* FIXME.  */
+
+void
+stats::dump (FILE *out) const
+{
+  for (int i = 0; i < NUM_POINT_KINDS; i++)
+    fprintf (out, "m_num_nodes[%s]: %i\n",
+	     point_kind_to_string (static_cast <enum point_kind> (i)),
+	     m_num_nodes[i]);
+  fprintf (out, "m_node_reuse_count: %i\n", m_node_reuse_count);
+  fprintf (out, "m_node_reuse_after_merge_count: %i\n",
+	   m_node_reuse_after_merge_count);
+
+  if (m_num_supernodes > 0)
+    fprintf (out, "PK_AFTER_SUPERNODE nodes per supernode: %.2f\n",
+	     (float)m_num_nodes[PK_AFTER_SUPERNODE] / (float)m_num_supernodes);
+}
+
+////////////////////////////////////////////////////////////////////////////
+
+/* class saved_diagnostic.  */
+
+/* saved_diagnostic's ctor.
+   Take ownership of D and STMT_FINDER.  */
+
+saved_diagnostic::saved_diagnostic (const state_machine *sm,
+				    const exploded_node *enode,
+				    const supernode *snode, const gimple *stmt,
+				    stmt_finder *stmt_finder,
+				    tree var, state_machine::state_t state,
+				    pending_diagnostic *d)
+: m_sm (sm), m_enode (enode), m_snode (snode), m_stmt (stmt),
+ /* stmt_finder could be on-stack; we want our own copy that can
+    outlive that.  */
+  m_stmt_finder (stmt_finder ? stmt_finder->clone () : NULL),
+  m_var (var), m_state (state),
+  m_d (d)
+{
+}
+
+/* saved_diagnostic's dtor.  */
+
+saved_diagnostic::~saved_diagnostic ()
+{
+  delete m_stmt_finder;
+  delete m_d;
+}
+
+////////////////////////////////////////////////////////////////////////////
+
+/* class diagnostic_manager.  */
+
+/* diagnostic_manager's ctor.  */
+
+diagnostic_manager::diagnostic_manager (logger *logger, int verbosity)
+: log_user (logger), m_verbosity (verbosity)
+{
+}
+
+/* FIXME.  */
+
+void
+diagnostic_manager::add_diagnostic (const state_machine *sm,
+				    const exploded_node *enode,
+				    const supernode *snode, const gimple *stmt,
+				    stmt_finder *finder,
+				    tree var, state_machine::state_t state,
+				    pending_diagnostic *d)
+{
+  LOG_FUNC (get_logger ());
+  // TODO: potentially do them directly, vs saved
+  saved_diagnostic *sd
+    = new saved_diagnostic (sm, enode, snode, stmt, finder, var, state, d);
+  m_saved_diagnostics.safe_push (sd);
+  if (get_logger ())
+    log ("adding saved diagnostic %i at SN %i: %qs",
+	 m_saved_diagnostics.length () - 1,
+	 snode->m_index, d->get_kind ());
+}
+
+/* FIXME.  */
+
+void
+diagnostic_manager::add_diagnostic (const exploded_node *enode,
+				    const supernode *snode, const gimple *stmt,
+				    stmt_finder *finder,
+				    pending_diagnostic *d)
+{
+  add_diagnostic (NULL, enode, snode, stmt, finder, NULL_TREE, 0, d);
+}
+
+/* FIXME.  */
+/* we want to find the simplest dedupe_candidate amongst those that share a
+   dedupe_key.  */
+
+class dedupe_key
+{
+public:
+  dedupe_key (const saved_diagnostic &sd,
+	      const exploded_path &epath)
+  : m_sd (sd), m_stmt (sd.m_stmt)
+  {
+    /* Support deferring the choice of stmt until after an emission path has
+     been built, using an optional stmt_finder.  */
+    if (m_stmt == NULL)
+      {
+	gcc_assert (sd.m_stmt_finder);
+	m_stmt = sd.m_stmt_finder->find_stmt (epath);
+      }
+    gcc_assert (m_stmt);
+  }
+
+  hashval_t hash () const
+  {
+    inchash::hash hstate;
+    hstate.add_ptr (m_stmt);
+    // TODO: m_sd
+    return hstate.end ();
+  }
+  bool operator== (const dedupe_key &other) const
+  {
+    return (m_sd == other.m_sd
+	    && m_stmt == other.m_stmt);
+  }
+
+  location_t get_location () const
+  {
+    return m_stmt->location;
+  }
+
+  /* A qsort comparator for use by dedupe_winners::emit_best
+     to sort them into location_t order.  */
+
+  static int
+  comparator (const void *p1, const void *p2)
+  {
+    const dedupe_key *pk1 = *(const dedupe_key * const *)p1;
+    const dedupe_key *pk2 = *(const dedupe_key * const *)p2;
+
+    location_t loc1 = pk1->get_location ();
+    location_t loc2 = pk2->get_location ();
+
+    return linemap_compare_locations (line_table, loc2, loc1);
+  }
+
+  // the pending_diagnostic
+  // the (stmt (non-NULL), var, state)
+  const saved_diagnostic &m_sd;
+  const gimple *m_stmt;
+};
+
+/* FIXME.  */
+class dedupe_candidate
+{
+public:
+  // has the exploded_path
+  dedupe_candidate  (const shortest_paths &sp,
+		     const saved_diagnostic &sd)
+  : m_epath (sp.get_shortest_path (sd.m_enode)),
+    m_num_dupes (0)
+  {
+  }
+
+  unsigned length () const { return m_epath.length (); }
+  const exploded_path &get_path () const { return m_epath; }
+
+private:
+  exploded_path m_epath;
+public:
+  int m_num_dupes;
+};
+
+/* FIXME.  */
+
+class dedupe_hash_map_traits
+{
+public:
+  typedef const dedupe_key *key_type;
+  typedef dedupe_candidate *value_type;
+  typedef dedupe_candidate *compare_type;
+
+  static inline hashval_t hash (const key_type &v)
+  {
+    return v->hash ();
+  }
+  static inline bool equal_keys (const key_type &k1, const key_type &k2)
+  {
+    return *k1 == *k2;
+  }
+  template <typename T>
+  static inline void remove (T &)
+  {
+    // TODO
+  }
+  template <typename T>
+  static inline void mark_deleted (T &entry)
+  {
+    entry.m_key = reinterpret_cast<key_type> (1);
+  }
+  template <typename T>
+  static inline void mark_empty (T &entry)
+  {
+    entry.m_key = NULL;
+  }
+  template <typename T>
+  static inline bool is_deleted (const T &entry)
+  {
+    return entry.m_key == reinterpret_cast<key_type> (1);
+  }
+  template <typename T>
+  static inline bool is_empty (const T &entry)
+  {
+    return entry.m_key == NULL;
+  }
+
+};
+
+/* A map from dedupe_key to best current dedupe_candidate.  */
+
+class dedupe_winners
+{
+public:
+  ~dedupe_winners ()
+  {
+    /* Delete all keys and candidates.  */
+    for (map_t::iterator iter = m_map.begin ();
+	 iter != m_map.end ();
+	 ++iter)
+      {
+	delete (*iter).first;
+	delete (*iter).second;
+      }
+  }
+
+  void add (logger *logger,
+	    const shortest_paths &sp,
+	    const saved_diagnostic &sd)
+  {
+    dedupe_candidate *dc = new dedupe_candidate (sp, sd);
+    /* Verify that the epath is feasible.
+       State-merging means that not every path in the epath corresponds
+       to a feasible one w.r.t. states.
+       Here we simply check each duplicate saved_diagnostic's
+       shortest_path, and reject any that aren't feasible.
+       This could introduce false negatives, as there could be longer
+       feasible paths within the egraph.  */
+    if (logger)
+      logger->log ("considering %qs at SN: %i",
+		   sd.m_d->get_kind (), sd.m_snode->m_index);
+    if (!dc->get_path ().feasible_p (logger))
+      {
+	if (logger)
+	  logger->log ("rejecting %qs at SN: %i"
+		       " due to infeasible path",
+		       sd.m_d->get_kind (), sd.m_snode->m_index);
+	delete dc;
+	return;
+      }
+    else
+      if (logger)
+	logger->log ("accepting %qs at SN: %i with feasible path",
+		     sd.m_d->get_kind (), sd.m_snode->m_index);
+
+    dedupe_key *key = new dedupe_key (sd, dc->get_path ());
+    if (dedupe_candidate **slot = m_map.get (key))
+      {
+	(*slot)->m_num_dupes++;
+
+	if (dc->length () < (*slot)->length ())
+	  {
+	    /* We've got a shorter path for the key; replace
+	       the current candidate.  */
+	    dc->m_num_dupes = (*slot)->m_num_dupes;
+	    delete *slot;
+	    *slot = dc;
+	  }
+	else
+	  /* We haven't beaten the current best candidate;
+	     drop the new candidate.  */
+	  delete dc;
+	delete key;
+      }
+    else
+      /* This is the first candidate for this key.  */
+      m_map.put (key, dc);
+  }
+
+ /* Emit the simplest diagnostic within each set.  */
+
+  void emit_best (diagnostic_manager *dm,
+		  const exploded_graph &eg,
+		  const shortest_paths &/*sp*/)
+  {
+    LOG_SCOPE (dm->get_logger ());
+
+    /* Get keys into a vec for sorting.  */
+    auto_vec<const dedupe_key *> keys (m_map.elements ());
+    for (map_t::iterator iter = m_map.begin ();
+	 iter != m_map.end ();
+	 ++iter)
+      keys.quick_push ((*iter).first);
+
+    dm->log ("# keys after de-duplication: %i", keys.length ());
+
+    /* Sort into a good emission order.  */
+    keys.qsort (dedupe_key::comparator);
+
+    /* Emit the best candidate for each key.  */
+    int i;
+    const dedupe_key *key;
+    FOR_EACH_VEC_ELT (keys, i, key)
+      {
+	dedupe_candidate **slot = m_map.get (key);
+	gcc_assert (*slot);
+	const dedupe_candidate &dc = **slot;
+
+	dm->emit_saved_diagnostic (eg, key->m_sd,
+				   dc.get_path (), key->m_stmt,
+				   dc.m_num_dupes);
+      }
+  }
+
+private:
+
+  typedef hash_map<const dedupe_key *, dedupe_candidate *,
+		   dedupe_hash_map_traits> map_t;
+  map_t m_map;
+};
+
+/* FIXME.  */
+
+void
+diagnostic_manager::emit_saved_diagnostics (const exploded_graph &eg)
+{
+  LOG_SCOPE (get_logger ());
+  auto_client_timevar tv ("emit saved diagnostics");
+  log ("# saved diagnostics: %i", m_saved_diagnostics.length ());
+
+  if (m_saved_diagnostics.length () == 0)
+    return;
+
+  /* Compute the shortest_paths once, sharing it between all diagnostics.  */
+  shortest_paths sp (eg);
+
+  // FIXME: implement a way to disable de-duplication
+  /* FIXME: add stats on de-duplication, and a way to show the
+     number of duplicates (e.g. via an option that adds a message.
+     Add a DejaGnu test for this  */
+
+  // FIXME: this isn't true:
+  /* De-duplicate the diagnostics by generating a collection of diagnostic_set,
+     picking the diagnostic with the shortest path within each set.  */
+#if 1
+  dedupe_winners best_candidates;
+
+  int i;
+  saved_diagnostic *sd;
+  FOR_EACH_VEC_ELT (m_saved_diagnostics, i, sd)
+    best_candidates.add (get_logger (), sp, *sd);
+
+  best_candidates.emit_best (this, eg, sp);
+
+#else
+  // TODO: for now, just emit them all
+
+  int i;
+  saved_diagnostic *sd;
+  FOR_EACH_VEC_ELT (m_saved_diagnostics, i, sd)
+    emit_saved_diagnostic (eg, sp, *sd);
+#endif
+}
+
+/* FIXME.  */
+
+void
+diagnostic_manager::emit_saved_diagnostic (const exploded_graph &eg,
+					   const saved_diagnostic &sd,
+					   const exploded_path &epath,
+					   const gimple *stmt,
+					   int num_dupes)
+{
+  LOG_SCOPE (get_logger ());
+  log ("sd: %qs at SN: %i", sd.m_d->get_kind (), sd.m_snode->m_index);
+  log ("num dupes: %i", num_dupes);
+
+  pretty_printer *pp = global_dc->printer->clone ();
+
+  checker_path emission_path;
+  /* Populate emission_path with a full description of EPATH.  */
+  build_emission_path (eg, epath, &emission_path);
+
+  /* Now prune it to just cover the most pertinent events.  */
+  prune_path (&emission_path, sd.m_sm, sd.m_var, sd.m_state);
+
+  /* Add a final event to the path, covering the diagnostic itself.
+     We use the final enode from the epath, which might be different from
+     the sd.m_enode, as the dedupe code doesn't care about enodes, just
+     snodes.  */
+  emission_path.add_final_event (sd.m_sm, epath.get_final_enode (), stmt,
+				 sd.m_var, sd.m_state);
+
+  emission_path.prepare_for_emission (sd.m_d);
+
+  gcc_rich_location rich_loc (stmt->location);
+  rich_loc.set_path (&emission_path);
+
+  auto_diagnostic_group d;
+  auto_cfun sentinel (sd.m_snode->m_fun);
+  sd.m_d->emit (&rich_loc);
+  if (num_dupes > 0)
+    inform_n (stmt->location, num_dupes,
+	      "%i duplicate", "%i duplicates",
+	      num_dupes);
+  delete pp;
+}
+
+/* Given a state change to DST_REP, determine a tree that gives the origin
+   of that state at STMT, using DST_STATE's region model, so that state
+   changes based on assignments can be tracked back to their origins.
+
+   For example, if we have
+
+     (S1) _1 = malloc (64);
+     (S2) EXPR = _1;
+
+   then at stmt S2 we can get the origin of EXPR's state as being _1,
+   and thus track the allocation back to S1.  */
+
+static tree
+get_any_origin (const gimple *stmt,
+		tree dst_rep,
+		const program_state &dst_state)
+{
+  if (!stmt)
+    return NULL_TREE;
+
+  if (const gassign *assign = dyn_cast <const gassign *> (stmt))
+    {
+      tree lhs = gimple_assign_lhs (assign);
+      /* Use region IDs to compare lhs with DST_REP.  */
+      if (dst_state.m_region_model->get_lvalue (lhs, NULL)
+	  == dst_state.m_region_model->get_lvalue (dst_rep, NULL))
+	{
+	  tree rhs1 = gimple_assign_rhs1 (assign);
+	  enum tree_code op = gimple_assign_rhs_code (assign);
+	  switch (op)
+	    {
+	    default:
+	      //gcc_unreachable ();  // TODO
+	      break;
+	    case COMPONENT_REF:
+	    case SSA_NAME:
+	      return rhs1;
+	    }
+	}
+    }
+  return NULL_TREE;
+}
+
+/* Emit a "path" of events to EMISSION_PATH describing the exploded path
+   EPATH within EG.  */
+
+void
+diagnostic_manager::build_emission_path (const exploded_graph &eg,
+					 const exploded_path &epath,
+					 checker_path *emission_path) const
+{
+  LOG_SCOPE (get_logger ());
+  const extrinsic_state &ext_state = eg.get_ext_state ();
+  for (unsigned i = 0; i < epath.m_edges.length (); i++)
+    {
+      const exploded_edge *eedge = epath.m_edges[i];
+      add_events_for_eedge (*eedge, ext_state, emission_path);
+    }
+}
+
+/* Subroutine of diagnostic_manager::build_emission_path.
+   Add any events for EEDGE to EMISSION_PATH.  */
+
+void
+diagnostic_manager::add_events_for_eedge (const exploded_edge &eedge,
+					  const extrinsic_state &ext_state,
+					  checker_path *emission_path) const
+{
+  const exploded_node *src_node = eedge.m_src;
+  const program_point &src_point = src_node->get_point ();
+  const int src_stack_depth = src_point.get_stack_depth ();
+  const exploded_node *dst_node = eedge.m_dest;
+  const program_point &dst_point = dst_node->get_point ();
+  const int dst_stack_depth = dst_point.get_stack_depth ();
+  if (get_logger ())
+    {
+      get_logger ()->start_log_line ();
+      pretty_printer *pp = get_logger ()->get_printer ();
+      pp_printf (pp, "EN %i -> EN %i: ",
+		 eedge.m_src->m_index,
+		 eedge.m_dest->m_index);
+      src_point.print (pp, format (false));
+      pp_string (pp, "-> ");
+      dst_point.print (pp, format (false));
+      get_logger ()->end_log_line ();
+    }
+  const program_state &src_state = src_node->get_state ();
+  const program_state &dst_state = dst_node->get_state ();
+
+  /* Add state change events for the states that have changed.
+     We add these before events for superedges, so that if we have a
+     state_change_event due to following an edge, we'll get this sequence
+     of events:
+
+      | if (!ptr)
+      |    ~
+      |    |
+      |    (1) assuming 'ptr’ is non-NULL  (state_change_event)
+      |    (2) following ‘false’ branch... (start_cfg_edge_event)
+     ...
+      | do_something (ptr);
+      | ~~~~~~~~~~~~~^~~~~
+      |              |
+      |              (3) ...to here        (end_cfg_edge_event).  */
+  gcc_assert (src_state.m_checker_states.length ()
+	      == ext_state.m_checkers.length ());
+  gcc_assert (dst_state.m_checker_states.length ()
+	      == ext_state.m_checkers.length ());
+  for (unsigned i = 0; i < ext_state.m_checkers.length (); i++)
+    {
+      const state_machine &sm = ext_state.get_sm (i);
+      const sm_state_map &src_smap = *src_state.m_checker_states[i];
+      const sm_state_map &dst_smap = *dst_state.m_checker_states[i];
+      add_state_change_events (eedge, sm, src_smap, dst_smap, emission_path);
+    }
+
+  /* Add events for rewinding from a longjmp to a setjmp.  */
+  if (eedge.m_rewind_info)
+    {
+      emission_path->add_event
+	(new rewind_from_longjmp_event
+	 (&eedge, src_point.m_supernode->get_end_location (),
+	  src_point.m_supernode->m_fun->decl,
+	  src_stack_depth));
+      emission_path->add_event
+	(new rewind_to_setjmp_event
+	 (&eedge, eedge.m_rewind_info->get_setjmp_call ()->location,
+	  dst_point.m_supernode->m_fun->decl,
+	  dst_stack_depth));
+    }
+
+  /* Add events for superedges, function entries, and for statements.  */
+  switch (dst_point.m_kind)
+    {
+    default:
+      break;
+    case PK_BEFORE_SUPERNODE:
+      if (src_point.m_kind == PK_AFTER_SUPERNODE)
+	{
+	  if (eedge.m_sedge)
+	    add_events_for_superedge (eedge, emission_path);
+	}
+      /* Add function entry events.  */
+      if (dst_point.m_supernode->entry_p ())
+	{
+	  emission_path->add_event
+	    (new function_entry_event
+	     (dst_point.m_supernode->get_start_location (),
+	      dst_point.m_supernode->m_fun->decl,
+	      dst_stack_depth));
+	}
+      break;
+    case PK_BEFORE_STMT:
+      {
+	const gimple *stmt = dst_point.get_stmt ();
+	if (is_setjmp_call_p (stmt))
+	  emission_path->add_event
+	    (new setjmp_event (stmt->location,
+			       dst_node,
+			       dst_point.m_supernode->m_fun->decl,
+			       dst_stack_depth));
+	else
+	  emission_path->add_event
+	    (new statement_event (stmt,
+				  dst_point.m_supernode->m_fun->decl,
+				  dst_stack_depth, dst_state));
+      }
+      break;
+    }
+}
+
+/* Subroutine of diagnostic_manager::add_events_for_eedge
+   where EEDGE has an underlying superedge i.e. a CFG edge,
+   or an interprocedural call/return.
+   Add any events for the superedge to EMISSION_PATH.  */
+
+void
+diagnostic_manager::add_events_for_superedge (const exploded_edge &eedge,
+					      checker_path *emission_path)
+  const
+{
+  gcc_assert (eedge.m_sedge);
+
+  const exploded_node *src_node = eedge.m_src;
+  const program_point &src_point = src_node->get_point ();
+  const exploded_node *dst_node = eedge.m_dest;
+  const program_point &dst_point = dst_node->get_point ();
+  const int src_stack_depth = src_point.get_stack_depth ();
+  const int dst_stack_depth = dst_point.get_stack_depth ();
+  const gimple *last_stmt = src_point.m_supernode->get_last_stmt ();
+
+  switch (eedge.m_sedge->m_kind)
+    {
+    case SUPEREDGE_CFG_EDGE:
+      {
+	emission_path->add_event
+	  (new start_cfg_edge_event (eedge,
+			       (last_stmt
+				? last_stmt->location
+				: UNKNOWN_LOCATION),
+			       src_point.m_supernode->m_fun->decl,
+			       src_stack_depth));
+	emission_path->add_event
+	  (new end_cfg_edge_event (eedge,
+				   dst_point.m_supernode->get_start_location (),
+				   dst_point.m_supernode->m_fun->decl,
+				   dst_stack_depth));
+      }
+      break;
+
+    case SUPEREDGE_CALL:
+      {
+	emission_path->add_event
+	  (new call_event (eedge,
+			   (last_stmt
+			    ? last_stmt->location
+			    : UNKNOWN_LOCATION),
+			   src_point.m_supernode->m_fun->decl,
+			   src_stack_depth));
+      }
+      break;
+
+    case SUPEREDGE_INTRAPROCEDURAL_CALL:
+      {
+	// FIXME:
+	emission_path->add_event
+	  (new debug_event ((last_stmt
+			     ? last_stmt->location
+			     : UNKNOWN_LOCATION),
+			    src_point.m_supernode->m_fun->decl,
+			    src_stack_depth,
+			    "call summary"));
+      }
+      break;
+
+    case SUPEREDGE_RETURN:
+      {
+	const return_superedge *return_edge
+	  = as_a <const return_superedge *> (eedge.m_sedge);
+
+	const gcall *call_stmt = return_edge->get_call_stmt ();
+	emission_path->add_event
+	  (new return_event (eedge,
+			     (call_stmt
+			      ? call_stmt->location
+			      : UNKNOWN_LOCATION),
+			     dst_point.m_supernode->m_fun->decl,
+			     dst_stack_depth));
+      }
+      break;
+    }
+}
+
+/* Subroutine of diagnostic_manager::add_events_for_edge, for handling
+   EEDGE.
+   Detect any state changes in DST_SMAP relative to SRC_SMAP for the
+   underlying state machine SM, adding them as events to *EMISSION_PATH.  */
+
+void
+diagnostic_manager::add_state_change_events (const exploded_edge &eedge,
+					     const state_machine &sm,
+					     const sm_state_map &src_smap,
+					     const sm_state_map &dst_smap,
+					     checker_path *emission_path) const
+{
+  const exploded_node *src_node = eedge.m_src;
+  const program_point &src_point = src_node->get_point ();
+  const exploded_node *dst_node = eedge.m_dest;
+  const int src_stack_depth = src_point.get_stack_depth ();
+  const program_state &src_state = src_node->get_state ();
+  const program_state &dst_state = dst_node->get_state ();
+
+  for (sm_state_map::iterator_t iter = dst_smap.begin ();
+       iter != dst_smap.end ();
+       ++iter)
+    {
+      /* FIXME: we want to compare the SM state between src state
+	 and dst state, but there's no guarantee that the IDs can
+	 be meaningfully compared.  */
+      svalue_id dst_sid = (*iter).first;
+      state_machine::state_t dst_sm_val = (*iter).second.m_state;
+
+      auto_vec<path_var> dst_pvs;
+      dst_state.m_region_model->get_path_vars_for_svalue (dst_sid,
+							  &dst_pvs);
+
+#if 1
+      unsigned j;
+      path_var *dst_pv;
+      FOR_EACH_VEC_ELT (dst_pvs, j, dst_pv)
+	{
+	  tree dst_rep = dst_pv->m_tree;
+	  gcc_assert (dst_rep);
+#else
+	  tree dst_rep = dst_state.get_representative_tree (dst_sid);
+	  if (dst_rep == NULL_TREE)
+	    continue;
+#endif
+	  if (dst_pv->m_stack_depth >= src_state.m_region_model->get_stack_depth ())
+	    continue;
+	  svalue_id src_sid
+	    = src_state.m_region_model->get_rvalue (*dst_pv, NULL);
+	  if (src_sid.null_p ())
+	    continue;
+	  state_machine::state_t src_sm_val = src_smap.get_state (src_sid);
+	  if (dst_sm_val != src_sm_val)
+	    {
+	      const gimple *stmt = src_point.get_stmt ();
+	      const supernode *supernode = src_point.m_supernode;
+	      int stack_depth = src_stack_depth;
+
+	      if (eedge.m_sedge
+		  && eedge.m_sedge->m_kind == SUPEREDGE_CFG_EDGE)
+		{
+		  supernode = src_point.m_supernode;
+		  stmt = supernode->get_last_stmt ();
+		  stack_depth = src_stack_depth;
+		}
+
+	      /* FIXME: bulletproofing for state changes at calls/returns;
+		 is there a better way? */
+	      if (!stmt)
+		continue;
+
+	      svalue_id dst_origin_sid = (*iter).second.m_origin;
+#if 0
+	      if (dst_origin_sid.null_p ())
+		{
+		  emission_path->add_event
+		    (new state_change_event (supernode,
+					     stmt,
+					     stack_depth,
+					     sm,
+					     dst_rep,
+					     src_sm_val,
+					     dst_sm_val,
+					     NULL_TREE));
+		}
+	      else
+		{
+		  auto_vec<tree> origin_trees;
+		  dst_state.m_region_model->get_trees_for_svalue
+		    (dst_origin_sid, &origin_trees);
+		  unsigned k;
+		  tree origin_rep;
+		  FOR_EACH_VEC_ELT (origin_trees, j, origin_rep)
+		    {
+		      gcc_assert (origin_rep);
+#else
+		      tree origin_rep
+			= dst_state.get_representative_tree (dst_origin_sid);
+
+		      if (origin_rep == NULL_TREE)
+			origin_rep = get_any_origin (stmt, dst_rep, dst_state);
+#endif
+		      emission_path->add_event
+			(new state_change_event (supernode,
+						 stmt,
+						 stack_depth,
+						 sm,
+						 dst_rep,
+						 src_sm_val,
+						 dst_sm_val,
+					     origin_rep,
+						 dst_state));
+#if 0
+		    }
+		}
+#endif
+	    }
+#if 1
+	}
+#endif
+    }
+}
+
+/* Prune PATH, based on the verbosity level, to the most pertinent
+   events for a diagnostic that involves VAR ending in state STATE
+   (for state machine SM).
+
+   PATH is updated in place, and the redundant checker_events are deleted.
+
+   As well as deleting events, call record_critical_state on events in
+   which state critical to the pending_diagnostic is being handled, so
+   that the event's get_desc vfunc can potentially supply a more precise
+   description of the event to the user.
+   e.g. improving
+     "calling 'foo' from 'bar'"
+   to
+     "passing possibly-NULL pointer 'ptr' to 'foo' from 'bar' as param 1"
+   when the diagnostic relates to later dereferencing 'ptr'.  */
+
+void
+diagnostic_manager::prune_path (checker_path *path,
+				const state_machine *sm,
+				tree var,
+				state_machine::state_t state) const
+{
+  LOG_FUNC (get_logger ());
+  path->maybe_log (get_logger (), "path");
+
+  /* First pass: apply verbosity level, pruning unrelated state
+     change events.
+     Iterate backwards through PATH, skipping state change events that aren't
+     VAR but update the pertinent VAR when state-copying occurs.  */
+  int idx = path->m_events.length () - 1;
+  while (idx >= 0 && idx < (signed)path->m_events.length ())
+    {
+      checker_event *base_event = path->m_events[idx];
+      if (get_logger ())
+	{
+	  if (sm)
+	    log ("considering event %i, with var: %qE, state: %qs",
+		 idx, var, sm->get_state_name (state));
+	  else
+	    log ("considering event %i", idx);
+	}
+      switch (base_event->m_kind)
+	{
+	default:
+	  gcc_unreachable ();
+
+	case EK_DEBUG:
+	  if (m_verbosity < 3)
+	    {
+	      log ("filtering event %i: debug event", idx);
+	      path->delete_event (idx);
+	    }
+	  break;
+
+	case EK_STMT:
+	  {
+	    /* If this stmt is the origin of "var", update var.  */
+	    statement_event *stmt_event = (statement_event *)base_event;
+	    tree new_var = get_any_origin (stmt_event->m_stmt, var,
+					   stmt_event->m_dst_state);
+	    if (new_var)
+	      {
+		log ("event %i: switching var of interest from %qE to %qE",
+		     idx, var, new_var);
+		var = new_var;
+	      }
+	    if (m_verbosity < 3)
+	      {
+		log ("filtering event %i: statement event", idx);
+		path->delete_event (idx);
+	      }
+	  }
+	  break;
+
+	case EK_FUNCTION_ENTRY:
+	  if (m_verbosity < 1)
+	    {
+	      log ("filtering event %i: function entry", idx);
+	      path->delete_event (idx);
+	    }
+	  break;
+
+	case EK_STATE_CHANGE:
+	  {
+	    state_change_event *state_change = (state_change_event *)base_event;
+	    if (state_change->get_lvalue (state_change->m_var)
+		== state_change->get_lvalue (var))
+	      {
+		if (state_change->m_origin)
+		  {
+		    log ("event %i: switching var of interest from %qE to %qE",
+			 idx, var, state_change->m_origin);
+		    var = state_change->m_origin;
+		  }
+		log ("event %i: switching state of interest from %qs to %qs",
+		     idx, sm->get_state_name (state_change->m_to),
+		     sm->get_state_name (state_change->m_from));
+		state = state_change->m_from;
+	      }
+	    else if (m_verbosity < 3)
+	      {
+		if (var)
+		  log ("filtering event %i:"
+		       " state change to %qE unrelated to %qE",
+		       idx, state_change->m_var, var);
+		else
+		  log ("filtering event %i: state change to %qE",
+		       idx, state_change->m_var);
+		path->delete_event (idx);
+	      }
+	  }
+	  break;
+
+	case EK_START_CFG_EDGE:
+	  {
+	    cfg_edge_event *event = (cfg_edge_event *)base_event;
+	    const cfg_superedge& cfg_superedge
+	      = event->get_cfg_superedge ();
+	    const supernode *dest = event->m_sedge->m_dest;
+	    /* Do we have an SSA_NAME defined via a phi node in
+	       the dest CFG node?  */
+	    if (var && TREE_CODE (var) == SSA_NAME)
+	      if (SSA_NAME_DEF_STMT (var)->bb == dest->m_bb)
+		{
+		  if (gphi *phi
+		      = dyn_cast <gphi *> (SSA_NAME_DEF_STMT (var)))
+		    {
+		      /* Update var based on its phi node.  */
+		      tree old_var = var;
+		      var = cfg_superedge.get_phi_arg (phi);
+		      log ("updating from %qE to %qE based on phi node",
+			   old_var, var);
+		      if (get_logger ())
+			{
+			  pretty_printer pp;
+			  pp_gimple_stmt_1 (&pp, phi, 0, (dump_flags_t)0);
+			  log ("  phi: %s", pp_formatted_text (&pp));
+			}
+		    }
+		}
+
+	    /* TODO: is this edge significant to var?
+	       See if var can be in other states in the dest, but not
+	       in other states in the src?
+	       Must have multiple sibling edges.  */
+
+	    if (event->should_filter_p (m_verbosity))
+	      {
+		log ("filtering event %i: CFG edge", idx);
+		path->delete_event (idx);
+		/* Also delete the corresponding EK_END_CFG_EDGE.  */
+		gcc_assert (path->m_events[idx]->m_kind == EK_END_CFG_EDGE);
+		path->delete_event (idx);
+	      }
+	  }
+	  break;
+
+	case EK_END_CFG_EDGE:
+	  /* These come in pairs with EK_START_CFG_EDGE events and are
+	     filtered when their start event is filtered.  */
+	  break;
+
+	case EK_CALL_EDGE:
+	  {
+	    call_event *event = (call_event *)base_event;
+	    const callgraph_superedge& cg_superedge
+	      = event->get_callgraph_superedge ();
+	    callsite_expr expr;
+	    tree caller_var
+	      = cg_superedge.map_expr_from_callee_to_caller (var, &expr);
+	    if (caller_var)
+	      {
+		log ("event %i:"
+		     " switching var of interest"
+		     " from %qE in callee to %qE in caller",
+		     idx, var, caller_var);
+		var = caller_var;
+		if (expr.param_p ())
+		  event->record_critical_state (var, state);
+	      }
+	    //#error
+	    /* potentially use this for precision-of-wording:
+	       identify passing a parm (and which parm idx)
+	       then update the superedge_event to note that it's a
+	       critical state-related edge (with the detail, maybe as an
+	       optional object).
+	       Then update the get_desc to use the detail, passing
+	       it to the pending_diagnostic to get a precise message
+	       e.g. passing possibly-NULL pointer 'ptr' to 'foo' from 'bar' as param 1
+	       or somesuch
+	    */
+	  }
+	  break;
+
+	case EK_RETURN_EDGE:
+	  // TODO: potentially update var/state based on return value,
+	  // args etc
+	  {
+	    return_event *event = (return_event *)base_event;
+	    const callgraph_superedge& cg_superedge
+	      = event->get_callgraph_superedge ();
+	    callsite_expr expr;
+	    tree callee_var
+	      = cg_superedge.map_expr_from_caller_to_callee (var, &expr);
+	    if (callee_var)
+	      {
+		if (var)
+		  log ("event %i:"
+		       " switching var of interest"
+		       " from %qE in caller to %qE in callee",
+		       idx, var, callee_var);
+		else
+		  // FIXME: how does this happen?
+		  log ("event %i:"
+		       " switching var of interest"
+		       " from NULL in caller to %qE in callee",
+		       idx, callee_var);
+		var = callee_var;
+		if (expr.return_value_p ())
+		  event->record_critical_state (var, state);
+	      }
+	  }
+	  break;
+
+	case EK_SETJMP:
+	  /* TODO: only show setjmp_events that matter i.e. those for which
+	     there is a later rewind event using them.  */
+	case EK_REWIND_FROM_LONGJMP:
+	case EK_REWIND_TO_SETJMP:
+	  break;
+
+	case EK_WARNING:
+	  /* Always show the final "warning" event in the path.  */
+	  break;
+	}
+      idx--;
+    }
+
+  /* Second pass: remove redundant interprocedural information.  For
+     example, given:
+     (1)- calling "f2" from "f1"
+     (2)--- entry to "f2"
+     (3)--- calling "f3" from "f2"
+     (4)----- entry to "f3"
+     (5)--- returning to "f2" to "f3"
+     (6)- returning to "f1" to "f2"
+     with no other intervening events, then none of these events are
+     likely to be interesting to the user.
+
+     Prune [..., call, function-entry, return, ...] triples repeatedly
+     until nothing has changed.  For the example above, this would
+     remove events (3, 4, 5), and then remove events (1, 2, 6).  */
+  bool changed = false;
+  do
+    {
+      changed = false;
+      int idx = path->m_events.length () - 3;
+      while (idx >= 0 && idx < (signed)path->m_events.length ())
+	{
+	  if (path->m_events[idx]->is_call_p ()
+	      && path->m_events[idx + 1]->is_function_entry_p ()
+	      && path->m_events[idx + 2]->is_return_p ())
+	    {
+	      if (get_logger ())
+		{
+		  label_text desc (path->m_events[idx]->get_desc (false));
+		  log ("filtering events %i-%i:"
+		       " irrelevant call/entry/return: %s",
+		       idx, idx + 2, desc.m_buffer);
+		  desc.maybe_free ();
+		}
+	      path->delete_event (idx + 2);
+	      path->delete_event (idx + 1);
+	      path->delete_event (idx);
+	      changed = true;
+	    }
+	  idx--;
+	}
+
+    }
+  while (changed);
+
+  /* Third pass.  If all we're left with is in one function, then
+     filter function entry events.  */
+  if (!path->interprocedural_p ())
+    {
+      // FIXME
+      int idx = path->m_events.length () - 1;
+      while (idx >= 0 && idx < (signed)path->m_events.length ())
+	{
+	  checker_event *base_event = path->m_events[idx];
+	  if (base_event->m_kind == EK_FUNCTION_ENTRY)
+	    {
+	      log ("filtering event %i:"
+		   " function entry for purely intraprocedural path", idx);
+	      path->delete_event (idx);
+	    }
+	  idx--;
+	}
+    }
+
+  path->maybe_log (get_logger (), "pruned");
+}
+
+////////////////////////////////////////////////////////////////////////////
+
+/* strongly_connected_components's ctor.  Tarjan's SCC algorithm.  */
+
+strongly_connected_components::
+strongly_connected_components (const supergraph &sg, logger *logger)
+: m_sg (sg), m_per_node (m_sg.num_nodes ())
+{
+  LOG_SCOPE (logger);
+  auto_client_timevar tv ("computing scc");
+
+  for (int i = 0; i < m_sg.num_nodes (); i++)
+    m_per_node.quick_push (per_node_data ());
+
+  for (int i = 0; i < m_sg.num_nodes (); i++)
+    if (m_per_node[i].m_index == -1)
+      strong_connect (i);
+
+  if (0)
+    dump ();
+}
+
+/* Dump this object to stderr.  */
+
+DEBUG_FUNCTION void
+strongly_connected_components::dump () const
+{
+  for (int i = 0; i < m_sg.num_nodes (); i++)
+    {
+      const per_node_data &v = m_per_node[i];
+      fprintf (stderr, "SN %i: index: %i lowlink: %i on_stack: %i\n",
+	       i, v.m_index, v.m_lowlink, v.m_on_stack);
+    }
+}
+
+/* Subroutine of strongly_connected_components's ctor, part of Tarjan's
+   SCC algorithm.  */
+
+void
+strongly_connected_components::strong_connect (unsigned index)
+{
+  supernode *v_snode = m_sg.get_node_by_index (index);
+
+  /* Set the depth index for v to the smallest unused index.  */
+  per_node_data *v = &m_per_node[index];
+  v->m_index = index;
+  v->m_lowlink = index;
+  m_stack.safe_push (index);
+  v->m_on_stack = true;
+  index++;
+
+  /* Consider successors of v.  */
+  unsigned i;
+  superedge *sedge;
+  FOR_EACH_VEC_ELT (v_snode->m_succs, i, sedge)
+    {
+      supernode *w_snode = sedge->m_dest;
+      per_node_data *w = &m_per_node[w_snode->m_index];
+      if (w->m_index == -1)
+	{
+	  /* Successor w has not yet been visited; recurse on it.  */
+	  strong_connect (w_snode->m_index);
+	  v->m_lowlink = MIN (v->m_lowlink, w->m_lowlink);
+	}
+      else if (w->m_on_stack)
+	{
+	  /* Successor w is in stack S and hence in the current SCC
+	     If w is not on stack, then (v, w) is a cross-edge in the DFS
+	     tree and must be ignored.  */
+	  v->m_lowlink = MIN (v->m_lowlink, w->m_index);
+	}
+    }
+
+  /* If v is a root node, pop the stack and generate an SCC.  */
+
+  if (v->m_lowlink == v->m_index)
+    {
+      /* FIXME: Start a new strongly connected component.  */
+      per_node_data *w;
+      do {
+	int idx = m_stack.pop ();
+	w = &m_per_node[idx];
+	w->m_on_stack = false;
+	/* FIXME: add w to current strongly connected component.  */
+      } while (w != v);
+      /* FIXME: output the current strongly connected component.  */
+    }
+}
+
+////////////////////////////////////////////////////////////////////////////
+
+/* worklist's ctor.  */
+
+worklist::worklist (const exploded_graph &eg, const analysis_plan &plan)
+: m_eg (eg),
+  m_scc (eg.get_supergraph (), eg.get_logger ()),
+  m_plan (plan),
+  m_queue (key_t (*this, NULL))
+{
+}
+
+/* Return the number of nodes in the worklist.  */
+
+unsigned
+worklist::length () const
+{
+  return m_queue.nodes ();
+}
+
+/* Return the next node in the worklist, removing it.  */
+
+exploded_node *
+worklist::take_next ()
+{
+  return m_queue.extract_min ();
+}
+
+/* Return the next node in the worklist without removing it.  */
+
+exploded_node *
+worklist::peek_next ()
+{
+  return m_queue.min ();
+}
+
+/* Add ENODE to the worklist.  */
+
+void
+worklist::add_node (exploded_node *enode)
+{
+  m_queue.insert (key_t (*this, enode), enode);
+}
+
+/* Comparator for implementing worklist::key_t comparison operators.
+   Return negative if KA is before KB
+   Return positive if KA is after KB
+   Return 0 if they are equal.  */
+
+int
+worklist::key_t::cmp_1 (const worklist::key_t &ka, const worklist::key_t &kb)
+{
+  const program_point &point_a = ka.m_enode->get_point ();
+  const program_point &point_b = kb.m_enode->get_point ();
+  const call_string &call_string_a = point_a.m_call_string;
+  const call_string &call_string_b = point_b.m_call_string;
+
+  /* Order empty-callstring points with different functions based on the
+     analysis_plan, so that we generate summaries before they are used.  */
+  /* FIXME: this isn't quite right, in that I'm seeing some uses before
+     creations, but it seems close.  */
+  if (flag_analyzer_call_summaries
+      && call_string_a.empty_p ()
+      && call_string_b.empty_p ()
+      && point_a.get_function () != NULL
+      && point_b.get_function () != NULL
+      && point_a.get_function () != point_b.get_function ())
+    {
+      return ka.m_worklist.m_plan.cmp_function (point_a.get_function (),
+						point_b.get_function ());
+    }
+
+  // FIXME:
+  /* First, order by SCC.  */
+  int scc_id_a = ka.get_scc_id (ka.m_enode);
+  int scc_id_b = kb.get_scc_id (kb.m_enode);
+  if (scc_id_a != scc_id_b)
+    return scc_id_a - scc_id_b;
+
+  /* If in same SCC, order by supernode index (an arbitrary but stable
+     ordering).  */
+  const supernode *snode_a = ka.m_enode->get_supernode ();
+  const supernode *snode_b = kb.m_enode->get_supernode ();
+  if (snode_a == NULL)
+    {
+      if (snode_b != NULL)
+	/* One is NULL.  */
+	return -1;
+      else
+	/* Both are NULL.  */
+	return 0;
+    }
+  if (snode_b == NULL)
+    /* One is NULL.  */
+    return 1;
+  /* Neither are NULL.  */
+  gcc_assert (snode_a && snode_b);
+  if (snode_a->m_index != snode_b->m_index)
+    return snode_a->m_index - snode_b->m_index;
+
+  gcc_assert (snode_a == snode_b);
+
+  /* Order within supernode via program point.  */
+  int within_snode_cmp = program_point::cmp_within_supernode (point_a, point_b);
+  if (within_snode_cmp)
+    return within_snode_cmp;
+
+  /* The points might vary by callstring; try sorting by callstring.  */
+  int cs_cmp = call_string::cmp (call_string_a, call_string_b);
+  if (cs_cmp)
+    return cs_cmp;
+
+  /* Otherwise, we ought to have the same program_point.  */
+  gcc_assert (point_a == point_b);
+
+  const program_state &state_a = ka.m_enode->get_state ();
+  const program_state &state_b = kb.m_enode->get_state ();
+
+  /* FIXME: sort by sm-state, so that identical sm-states are grouped
+     together in the worklist.  */
+  // for now, try the hash value (might not be deterministic)
+  for (unsigned sm_idx = 0; sm_idx < state_a.m_checker_states.length ();
+       ++sm_idx)
+    {
+      sm_state_map *smap_a = state_a.m_checker_states[sm_idx];
+      sm_state_map *smap_b = state_b.m_checker_states[sm_idx];
+
+      int sm_cmp = smap_a->hash () - smap_b->hash ();
+      if (sm_cmp)
+	return sm_cmp;
+    }
+
+  /* Otherwise, we have two enodes at the same program point but with
+     different states.  We don't have a good total ordering on states,
+     so order them by enode index, so that we have at least have a
+     stable sort.  */
+  return ka.m_enode->m_index - kb.m_enode->m_index;
+}
+
+/* Comparator for implementing worklist::key_t comparison operators.
+   Return negative if KA is before KB
+   Return positive if KA is after KB
+   Return 0 if they are equal.  */
+
+int
+worklist::key_t::cmp (const worklist::key_t &ka, const worklist::key_t &kb)
+{
+  int result = cmp_1 (ka, kb);
+
+  /* Check that the ordering is symmetric  */
+#if CHECKING_P
+  int reversed = cmp_1 (kb, ka);
+  gcc_assert (reversed == -result);
+#endif
+
+  /* We should only have 0 for equal (point, state) pairs.  */
+  gcc_assert (result != 0
+	      || (*ka.m_enode->get_ps_key ()
+		  == *kb.m_enode->get_ps_key ()));
+
+  return result;
+}
+
+////////////////////////////////////////////////////////////////////////////
+
+/* exploded_graph's ctor.  */
+
+exploded_graph::exploded_graph (const supergraph &sg, logger *logger,
+				const extrinsic_state &ext_state,
+				const state_purge_map *purge_map,
+				const analysis_plan &plan,
+				int verbosity)
+: log_user (logger), m_sg (sg),
+  m_worklist (*this, plan),
+  m_ext_state (ext_state),
+  m_purge_map (purge_map),
+  m_plan (plan),
+  m_diagnostic_manager (logger, verbosity),
+  m_global_stats (m_sg.num_nodes ()),
+  m_functionless_stats (m_sg.num_nodes ()),
+  m_PK_AFTER_SUPERNODE_per_snode (m_sg.num_nodes ())
+{
+  m_origin = get_or_create_node (program_point (NULL, NULL, 0, PK_ORIGIN,
+						call_string ()),
+				 program_state (ext_state));
+  for (int i = 0; i < m_sg.num_nodes (); i++)
+    m_PK_AFTER_SUPERNODE_per_snode.quick_push (i);
+}
+
+/* exploded_graph's dtor.  */
+
+exploded_graph::~exploded_graph ()
+{
+  for (function_stat_map_t::iterator iter = m_per_function_stats.begin ();
+       iter != m_per_function_stats.end ();
+       ++iter)
+    delete (*iter).second;
+
+  for (point_map_t::iterator iter = m_per_point_data.begin ();
+       iter != m_per_point_data.end ();
+       ++iter)
+    delete (*iter).second;
+}
+
+/* Ensure that there is an exploded_node representing an external call to
+   FUN, adding it to the worklist if creating it.
+
+   Add an edge from the origin exploded_node to the function entrypoint
+   exploded_node.
+
+   Return the exploded_node for the entrypoint to the function.  */
+
+exploded_node *
+exploded_graph::add_function_entry (function *fun)
+{
+  program_point point = program_point::from_function_entry (m_sg, fun);
+  program_state state (m_ext_state);
+  state.m_region_model->push_frame (fun, NULL);
+
+  exploded_node *enode = get_or_create_node (point, state);
+  /* We should never fail to add such a node.  */
+  gcc_assert (enode);
+  add_edge (m_origin, enode, NULL);
+  return enode;
+}
+
+/* Get or create an exploded_node for (POINT, STATE).
+   If a new node is created, it is added to the worklist.  */
+
+exploded_node *
+exploded_graph::get_or_create_node (const program_point &point,
+				    const program_state &state)
+{
+  LOG_FUNC (get_logger ());
+  if (get_logger ())
+    {
+      format f (false);
+      pretty_printer *pp = get_logger ()->get_printer ();
+      start_log_line ();
+      pp_string (pp, "point: ");
+      point.print (pp, f);
+      end_log_line ();
+      start_log_line ();
+      pp_string (pp, "state: ");
+      state.dump (m_ext_state, true);
+      end_log_line ();
+    }
+
+  auto_cfun sentinel (point.get_function ());
+
+  state.validate (get_ext_state ());
+
+  //state.dump (get_ext_state ());
+
+  /* Prune state to try to improve the chances of a cache hit,
+     avoiding generating redundant nodes.  */
+  program_state pruned_state = state.prune_for_point (*this, point);
+
+  pruned_state.validate (get_ext_state ());
+
+  //pruned_state.dump (get_ext_state ());
+
+  if (get_logger ())
+    {
+      pretty_printer *pp = get_logger ()->get_printer ();
+      start_log_line ();
+      pp_string (pp, "pruned_state: ");
+      pruned_state.dump_to_pp (m_ext_state, true, pp);
+      end_log_line ();
+      pruned_state.m_region_model->dump_to_pp (get_logger_pp (), true);
+    }
+
+  stats *per_fn_stats = get_or_create_function_stats (point.get_function ());
+
+  stats *per_cs_stats
+    = &get_or_create_per_call_string_data (point.m_call_string)->m_stats;
+
+  point_and_state ps (point, pruned_state);
+  if (exploded_node **slot = m_point_and_state_to_node.get (&ps))
+    {
+      /* An exploded_node for PS already exists.  */
+      log ("reused EN: %i", (*slot)->m_index);
+      m_global_stats.m_node_reuse_count++;
+      per_fn_stats->m_node_reuse_count++;
+      per_cs_stats->m_node_reuse_count++;
+      return *slot;
+    }
+
+  per_program_point_data *per_point_data
+    = get_or_create_per_program_point_data (point);
+
+  /* Consider merging state with another enode at this program_point.  */
+  if (flag_analyzer_state_merge)
+    {
+      exploded_node *existing_enode;
+      unsigned i;
+      FOR_EACH_VEC_ELT (per_point_data->m_enodes, i, existing_enode)
+	{
+	  log ("considering merging with existing EN: %i for point",
+	       existing_enode->m_index);
+	  gcc_assert (existing_enode->get_point () == point);
+	  const program_state &existing_state = existing_enode->get_state ();
+
+#if 0
+	  /* They shouldn't be equal, or we would have found and returned
+	     it above.  */
+	  gcc_assert (pruned_state != existing_state);
+#endif
+
+	  // FIXME: this is successively merging within the loop
+
+	  program_state merged_state (m_ext_state);
+	  if (pruned_state.can_merge_with_p (existing_state, m_ext_state,
+					     &merged_state))
+	    {
+	      log ("merging new state with that of EN: %i",
+		   existing_enode->m_index);
+
+	      /* Try again for a cache hit.  */
+	      ps.set_state (merged_state);
+	      if (exploded_node **slot = m_point_and_state_to_node.get (&ps))
+		{
+		  /* An exploded_node for PS already exists.  */
+		  log ("reused EN: %i", (*slot)->m_index);
+		  m_global_stats.m_node_reuse_after_merge_count++;
+		  per_fn_stats->m_node_reuse_after_merge_count++;
+		  per_cs_stats->m_node_reuse_after_merge_count++;
+		  return *slot;
+		}
+
+	      /* Otherwise, continue, using the merged state in "ps".  */
+	    }
+	  else
+	    log ("not merging new state with that of EN: %i",
+		 existing_enode->m_index);
+	}
+    }
+
+  /* Impose a limit on the number of enodes per program point, and
+     simply stop if we exceed it.  */
+  if ((int)per_point_data->m_enodes.length ()
+      > PARAM_VALUE (PARAM_ANALYZER_MAX_ENODES_PER_PROGRAM_POINT))
+    {
+      log ("not creating enode; too many at program point");
+      warning_at (point.get_location (), OPT_Wanalyzer_too_complex,
+		  "terminating analysis for this program point");
+      return NULL;
+    }
+
+  /* An exploded_node for "ps" doesn't already exist; create one.  */
+  exploded_node *node = new exploded_node (ps, m_ext_state, m_nodes.length ());
+  add_node (node);
+  m_point_and_state_to_node.put (node->get_ps_key (), node);
+
+  /* Update per-program_point data.  */
+  per_point_data->m_enodes.safe_push (node);
+
+  m_global_stats.m_num_nodes[node->get_point ().m_kind]++;
+  per_fn_stats->m_num_nodes[node->get_point ().m_kind]++;
+  per_cs_stats->m_num_nodes[node->get_point ().m_kind]++;
+
+  if (node->get_point ().m_kind == PK_AFTER_SUPERNODE)
+    m_PK_AFTER_SUPERNODE_per_snode[point.get_supernode ()->m_index]++;
+
+  if (get_logger ())
+    {
+      format f (false);
+      pretty_printer *pp = get_logger ()->get_printer ();
+      log ("created EN: %i", node->m_index);
+      start_log_line ();
+      pp_string (pp, "point: ");
+      point.print (pp, f);
+      end_log_line ();
+      start_log_line ();
+      pp_string (pp, "pruned_state: ");
+      pruned_state.dump_to_pp (m_ext_state, true, pp);
+      end_log_line ();
+    }
+
+  /* Add the new node to the worlist.  */
+  m_worklist.add_node (node);
+  return node;
+}
+
+/* Add an exploded_edge from SRC to DEST, recording its association
+   with SEDGE (which may be NULL), and, if non-NULL, taking ownership
+   of REWIND_INFO.  */
+
+void
+exploded_graph::add_edge (exploded_node *src, exploded_node *dest,
+			  const superedge *sedge,
+			  rewind_info_t *rewind_info)
+{
+  exploded_edge *e = new exploded_edge (src, dest, sedge, rewind_info);
+  digraph::add_edge (e);
+}
+
+/* Ensure that this graph has per-program_point-data for POINT;
+   borrow a pointer to it.  */
+
+per_program_point_data *
+exploded_graph::
+get_or_create_per_program_point_data (const program_point &point)
+{
+  if (per_program_point_data **slot = m_per_point_data.get (&point))
+    return *slot;
+
+  per_program_point_data *per_point_data = new per_program_point_data (point);
+  m_per_point_data.put (&per_point_data->m_key, per_point_data);
+  return per_point_data;
+}
+
+/* Ensure that this graph has per-call_string-data for CS;
+   borrow a pointer to it.  */
+
+per_call_string_data *
+exploded_graph::get_or_create_per_call_string_data (const call_string &cs)
+{
+  if (per_call_string_data **slot = m_per_call_string_data.get (&cs))
+    return *slot;
+
+  per_call_string_data *data = new per_call_string_data (cs, m_sg.num_nodes ());
+  m_per_call_string_data.put (&data->m_key,
+			      data);
+  return data;
+}
+
+/* Ensure that this graph has per-function-data for FUN;
+   borrow a pointer to it.  */
+
+per_function_data *
+exploded_graph::get_or_create_per_function_data (function *fun)
+{
+  if (per_function_data **slot = m_per_function_data.get (fun))
+    return *slot;
+
+  per_function_data *data = new per_function_data ();
+  m_per_function_data.put (fun, data);
+  return data;
+}
+
+/* Get this graph's per-function-data for FUN if there is any,
+   otherwise NULL.  */
+
+per_function_data *
+exploded_graph::get_per_function_data (function *fun) const
+{
+  if (per_function_data **slot
+        = const_cast <per_function_data_t &> (m_per_function_data).get (fun))
+    return *slot;
+
+  return NULL;
+}
+
+/* Return true if NODE and FUN should be traversed directly, rather than
+   called via other functions.  */
+
+static bool
+toplevel_function_p (cgraph_node *node, function *fun, logger *logger)
+{
+  /* FIXME: better logic here
+     e.g. only if more than one caller, and significantly complicated.
+     Perhaps some whole-callgraph analysis to decide if it's worth summarizing
+     an edge, and if so, we need summaries.  */
+  if (flag_analyzer_call_summaries)
+    {
+      int num_call_sites = 0;
+      for (cgraph_edge *edge = node->callers; edge; edge = edge->next_caller)
+	++num_call_sites;
+
+      /* FIXME: For now, if there's more than one in-edge, and we want
+	 call summaries, do it at the top level so that there's a chance
+	 we'll have a summary when we need one.  */
+      if (num_call_sites > 1)
+	{
+	  if (logger)
+	    logger->log ("traversing %qE (%i call sites)",
+			 fun->decl, num_call_sites);
+	  return true;
+	}
+    }
+
+  if (!TREE_PUBLIC (fun->decl))
+    {
+      if (logger)
+	logger->log ("not traversing %qE (static)", fun->decl);
+      return false;
+    }
+
+  if (logger)
+    logger->log ("traversing %qE (all checks passed)", fun->decl);
+
+  return true;
+}
+
+/* Add initial nodes to EG, with entrypoints for externally-callable
+   functions.  */
+
+void
+exploded_graph::build_initial_worklist ()
+{
+  LOG_SCOPE (get_logger ());
+
+  cgraph_node *node;
+  FOR_EACH_FUNCTION_WITH_GIMPLE_BODY (node)
+  {
+    function *fun = node->get_fun ();
+    if (!toplevel_function_p (node, fun, get_logger ()))
+      continue;
+    exploded_node *enode = add_function_entry (fun);
+    if (get_logger ())
+      log ("created EN %i for %qE entrypoint", enode->m_index, fun->decl);
+  }
+}
+
+/* The main loop of the analysis.
+   Take freshly-created exploded_nodes from the worklist, calling
+   process_node on them to explore the <point, state> graph.
+   Add edges to their successors, potentially creating new successors
+   (which are also added to the worklist).  */
+
+void
+exploded_graph::process_worklist ()
+{
+  LOG_SCOPE (get_logger ());
+  auto_client_timevar tv ("exploded_graph::process_worklist");
+
+  while (m_worklist.length () > 0)
+    {
+      exploded_node *node = m_worklist.take_next ();
+      gcc_assert (node->m_succs.length () == 0
+		  || node == m_origin);
+      // FIXME: avoid the "origin" specialcase?
+
+      log ("next to process: EN: %i", node->m_index);
+
+      /* Avoid exponential explosions of nodes by attempting to merge
+	 nodes that are at the same program point and which have
+	 sufficiently similar state.  */
+      if (flag_analyzer_state_merge && node != m_origin)
+	if (exploded_node *node_2 = m_worklist.peek_next ())
+	  {
+	    gcc_assert (node->m_succs.length () == 0);
+	    gcc_assert (node_2->m_succs.length () == 0);
+
+	    gcc_assert (node != node_2);
+
+	    log ("peek worklist: EN: %i", node_2->m_index);
+
+	    if (node->get_point () == node_2->get_point ())
+	      {
+		if (get_logger ())
+		  {
+		    format f (false);
+		    pretty_printer *pp = get_logger ()->get_printer ();
+		    start_log_line ();
+		    get_logger ()->log_partial
+		      ("got potential merge EN: %i and EN: %i at ",
+		       node->m_index, node_2->m_index);
+		    node->get_point ().print (pp, f);
+		    end_log_line ();
+		  }
+
+		const program_state &state = node->get_state ();
+		const program_state &state_2 = node_2->get_state ();
+
+		/* They shouldn't be equal, or we wouldn't have two
+		   separate nodes.  */
+		gcc_assert (state != state_2);
+
+		program_state merged_state (m_ext_state);
+		if (state.can_merge_with_p (state_2, m_ext_state,
+					    &merged_state))
+		  {
+		    log ("merging EN: %i and EN: %i",
+			 node->m_index, node_2->m_index);
+
+		    if (merged_state == state)
+		      {
+			/* Then merge node_2 into node by adding an edge.  */
+			add_edge (node_2, node, NULL);
+
+			/* Remove node_2 from the worklist.  */
+			m_worklist.take_next ();
+
+			/* Continue processing "node" below.  */
+		      }
+		    else if (merged_state == state_2)
+		      {
+			/* Then merge node into node_2, and leave node_2
+			   in the worklist, to be processed on the next
+			   iteration.  */
+			add_edge (node, node_2, NULL);
+			continue;
+		      }
+		    else
+		      {
+			/* We have a merged state that differs from
+			   both state and state_2.  */
+
+			/* Remove node_2 from the worklist.  */
+			m_worklist.take_next ();
+
+			/* Create (or get) an exploded node for the merged
+			   states, adding to the worklist.  */
+			exploded_node *merged_enode
+			  = get_or_create_node (node->get_point (),
+						merged_state);
+			if (merged_enode == NULL)
+			  continue;
+
+			log ("merged EN: %i and EN: %i into EN: %i",
+			     node->m_index, node_2->m_index,
+			     merged_enode->m_index);
+
+			/* "node" and "node_2" have both now been removed
+			   from the worklist; we should not process them.
+
+			   "merged_enode" may be a new node; if so it will be
+			   processed in a subsequent iteration.
+			   Alternatively, "merged_enode" could be an existing
+			   node; one way the latter can
+			   happen is if we end up merging a succession of
+			   similar nodes into one.  */
+
+			/* If merged_node is one of the two we were merging,
+			   add it back to the worklist to ensure it gets
+			   processed.
+
+			   Add edges from the merged nodes to it (but not a
+			   self-edge).  */
+			if (merged_enode == node)
+			  m_worklist.add_node (merged_enode);
+			else
+			  add_edge (node, merged_enode, NULL);
+
+			if (merged_enode == node_2)
+			  m_worklist.add_node (merged_enode);
+			else
+			  add_edge (node_2, merged_enode, NULL);
+
+			continue;
+		      }
+		  }
+
+		/* TODO: should we attempt more than two nodes,
+		   or just do pairs of nodes?  (and hope that we get
+		   a cascade of mergers).  */
+	      }
+	}
+
+      process_node (node);
+
+      /* Impose a hard limit on the number of exploded nodes, to ensure
+	 that the analysis terminates in the face of pathological state
+	 explosion (or bugs).
+
+	 Specifically, the limit is on the number of PK_AFTER_SUPERNODE
+	 exploded nodes, looking at supernode exit events.
+
+	 We use exit rather than entry since there can be multiple
+	 entry ENs, one per phi; the number of PK_AFTER_SUPERNODE ought
+	 to be equivalent to the number of supernodes multiplied by the
+	 number of states.  */
+      // FIXME: do per function
+      const int limit
+	= m_sg.num_nodes () * PARAM_VALUE (PARAM_ANALYZER_BB_EXPLOSION_FACTOR);
+      if (m_global_stats.m_num_nodes[PK_AFTER_SUPERNODE] > limit)
+	{
+	  log ("bailing out; too many nodes");
+	  warning_at (node->get_point ().get_location (),
+		      OPT_Wanalyzer_too_complex,
+		      "analysis bailed out early"
+		      " (%i 'after-snode' enodes; %i enodes)",
+		      m_global_stats.m_num_nodes[PK_AFTER_SUPERNODE],
+		      m_nodes.length ());
+	  return;
+	}
+    }
+}
+
+
+/* Return true if STMT must appear at the start of its exploded node, and
+   thus we can't consolidate its effects within a run of other statements.  */
+
+static bool
+stmt_requires_new_enode_p (const gimple *stmt)
+{
+  /* Stop consolidating at calls to
+     "__analyzer_dump_exploded_nodes", so they always appear at the
+     start of an exploded_node.  */
+  if (const gcall *call = dyn_cast <const gcall *> (stmt))
+    if (is_named_call_p (call, "__analyzer_dump_exploded_nodes",
+			 1))
+      return true;
+
+  /* FIXME: why *exactly* is this needed?  We need it to get
+     non-UNKNOWN_LOCATION locations for state-change events for
+     return values, so that e.g. malloc-ipa-8-unchecked.c's
+     "this call could return NULL" has a location, at:
+      |   12 |   return malloc (size);
+      |      |          ~~~~~~~~~~~~~
+      |      |          |
+      |      |          (4) this call could return NULL
+     ...which seems wrong, presumably it ought to be at the
+     call to malloc, not the return.  */
+  // FIXME: Do we really want to split after the previous statement, instead?
+  /*  We have:
+
+     bb:
+       _4 = malloc (...);
+
+     bb:
+       L0:
+       return _4;
+
+     and hence a new supernode.
+
+     In both cases, we get an enode for the after-snode with a state change
+     to "unchecked" immediately after the malloc.
+     We then have:
+     - enode for before-snode
+     - enode for before-stmt for L0
+     - (optionally) enode for before-stmt for the return _4;
+     - enode for after-snode
+
+    With a break, we have:
+
+      path[0]: EK_FUNCTION_ENTRY "entry to ‘make_boxed_int’" (depth 0, m_loc=22155405)
+      path[1]: EK_START_CFG_EDGE "" (depth 0, m_loc=0)
+      path[2]: EK_END_CFG_EDGE "...to here" (depth 0, m_loc=2147483893)
+      path[3]: EK_STMT "stmt: result_3 = wrapped_malloc (4);" (depth 0, m_loc=2147483893)
+      path[4]: EK_CALL_EDGE "calling ‘wrapped_malloc’ from ‘make_boxed_int’" (depth 0, m_loc=2147483893)
+      path[5]: EK_FUNCTION_ENTRY "entry to ‘wrapped_malloc’" (depth 1, m_loc=22110541)
+      path[6]: EK_START_CFG_EDGE "" (depth 1, m_loc=0)
+      path[7]: EK_END_CFG_EDGE "...to here" (depth 1, m_loc=2147483892)
+      path[8]: EK_STMT "stmt: _4 = malloc (size_2(D));" (depth 1, m_loc=2147483892)
+      path[9]: EK_STATE_CHANGE "state of ‘<unknown>’: ‘start’ -> ‘unchecked’ (origin: NULL)" (depth 1, m_loc=2147483892)
+      path[10]: EK_START_CFG_EDGE "" (depth 1, m_loc=2147483892)
+      path[11]: EK_END_CFG_EDGE "...to here" (depth 1, m_loc=2147483892)
+      path[12]: EK_STMT "stmt: <L0>:" (depth 1, m_loc=0)
+      path[13]: EK_STMT "stmt: return _4;" (depth 1, m_loc=2147483892)
+      path[14]: EK_STATE_CHANGE "state of ‘<return-value>’: ‘start’ -> ‘unchecked’ (origin: NULL)" (depth 1, m_loc=2147483892)
+      path[15]: EK_START_CFG_EDGE "" (depth 1, m_loc=2147483892)
+      path[16]: EK_END_CFG_EDGE "...to here" (depth 1, m_loc=22122624)
+      path[17]: EK_RETURN_EDGE "returning to ‘make_boxed_int’ from ‘wrapped_malloc’" (depth 0, m_loc=2147483893)
+      path[18]: EK_STMT "stmt: result_3->i = i_4(D);" (depth 0, m_loc=2147483894)
+and in particular:
+      path[14]: EK_STATE_CHANGE "state of ‘<return-value>’: ‘start’ -> ‘unchecked’ (origin: NULL)" (depth 1, m_loc=2147483892)
+
+Without a break, we have:
+      path[0]: EK_FUNCTION_ENTRY "entry to ‘make_boxed_int’" (depth 0, m_loc=22155405)
+      path[1]: EK_START_CFG_EDGE "" (depth 0, m_loc=0)
+      path[2]: EK_END_CFG_EDGE "...to here" (depth 0, m_loc=2147483893)
+      path[3]: EK_STMT "stmt: result_3 = wrapped_malloc (4);" (depth 0, m_loc=2147483893)
+      path[4]: EK_CALL_EDGE "calling ‘wrapped_malloc’ from ‘make_boxed_int’" (depth 0, m_loc=2147483893)
+      path[5]: EK_FUNCTION_ENTRY "entry to ‘wrapped_malloc’" (depth 1, m_loc=22110541)
+      path[6]: EK_START_CFG_EDGE "" (depth 1, m_loc=0)
+      path[7]: EK_END_CFG_EDGE "...to here" (depth 1, m_loc=2147483892)
+      path[8]: EK_STMT "stmt: _4 = malloc (size_2(D));" (depth 1, m_loc=2147483892)
+      path[9]: EK_STATE_CHANGE "state of ‘<unknown>’: ‘start’ -> ‘unchecked’ (origin: NULL)" (depth 1, m_loc=2147483892)
+      path[10]: EK_START_CFG_EDGE "" (depth 1, m_loc=2147483892)
+      path[11]: EK_END_CFG_EDGE "...to here" (depth 1, m_loc=2147483892)
+      path[12]: EK_STMT "stmt: <L0>:" (depth 1, m_loc=0)
+      path[13]: EK_STATE_CHANGE "state of ‘<return-value>’: ‘start’ -> ‘unchecked’ (origin: NULL)" (depth 1, m_loc=0)
+      path[14]: EK_START_CFG_EDGE "" (depth 1, m_loc=2147483892)
+      path[15]: EK_END_CFG_EDGE "...to here" (depth 1, m_loc=22122624)
+      path[16]: EK_RETURN_EDGE "returning to ‘make_boxed_int’ from ‘wrapped_malloc’" (depth 0, m_loc=2147483893)
+      path[17]: EK_STMT "stmt: result_3->i = i_4(D);" (depth 0, m_loc=2147483894)
+
+and note how event 13 has an unknown location:
+      path[13]: EK_STATE_CHANGE "state of ‘<return-value>’: ‘start’ -> ‘unchecked’ (origin: NULL)" (depth 1, m_loc=0)
+
+      I think what we're running into here is that state_change events can be
+      added based on region-model changes, not just sm-state changes, and we
+      want to split enodes at the points where the events would be emitted.
+  */
+#if 0
+  if (gimple_code (stmt) == GIMPLE_RETURN)
+    return true;
+#endif
+
+  return false;
+}
+
+/* The core of exploded_graph::process_worklist (the main analysis loop),
+   handling one node in the worklist.
+
+   Get successor <point, state> pairs for NODE, calling get_or_create on
+   them, and adding an exploded_edge to each successors.
+
+   Freshly-created nodes will be added to the worklist.  */
+
+void
+exploded_graph::process_node (exploded_node *node)
+{
+  LOG_FUNC_1 (get_logger (), "EN: %i", node->m_index);
+
+  const program_point &point = node->get_point ();
+
+  /* Update cfun and input_location in case of an ICE: make it easier to
+     track down which source construct we're failing to handle.  */
+  auto_cfun sentinel (node->get_function ());
+  const gimple *stmt = point.get_stmt ();
+  if (stmt)
+    input_location = stmt->location;
+
+  const program_state &state = node->get_state ();
+  if (get_logger ())
+    {
+      pretty_printer *pp = get_logger ()->get_printer ();
+      start_log_line ();
+      pp_string (pp, "point: ");
+      point.print (pp, format (false));
+      pp_string (pp, ", state: ");
+      state.dump_to_pp (m_ext_state, true, pp);
+      end_log_line ();
+    }
+
+  switch (point.m_kind)
+    {
+    default:
+      gcc_unreachable ();
+    case PK_ORIGIN:
+      /* This node exists to simplify finding the shortest path
+	 to an exploded_node.  */
+      break;
+
+    case PK_BEFORE_SUPERNODE:
+      {
+	program_state next_state (state);
+
+	if (point.m_from_edge)
+	  {
+	    impl_region_model_context ctxt (*this, node,
+					    &state, &next_state, NULL);
+	    const cfg_superedge *last_cfg_superedge
+	      = point.m_from_edge->dyn_cast_cfg_superedge ();
+	    if (last_cfg_superedge)
+	      next_state.m_region_model->update_for_phis
+		(node->get_supernode (),
+		 last_cfg_superedge,
+		 &ctxt);
+	  }
+
+	if (point.m_supernode->m_stmts.length () > 0)
+	  {
+	    program_point next_point
+	      = program_point::before_stmt (point.m_supernode, 0,
+					    point.m_call_string);
+	    exploded_node *next
+	      = get_or_create_node (next_point, next_state);
+	    if (next)
+	      add_edge (node, next, NULL);
+	  }
+	else
+	  {
+	    program_point next_point
+	      = program_point::after_supernode (point.m_supernode,
+						point.m_call_string);
+	    exploded_node *next = get_or_create_node (next_point, next_state);
+	    if (next)
+	      add_edge (node, next, NULL);
+	  }
+      }
+      break;
+    case PK_BEFORE_STMT:
+      {
+	/* Determine the effect of a run of one or more statements
+	   within one supernode, generating an edge to the program_point
+	   after the last statement that's processed.
+
+	   Stop iterating statements and thus consolidating into one enode
+	   when:
+	   - reaching the end of the statements in the supernode
+	   - if an sm-state-change occurs (so that it gets its own
+	     exploded_node)
+	   - if "-fanalyzer-fine-grained" is active
+	   - encountering certain statements must appear at the start of
+	   their enode (for which stmt_requires_new_enode_p returns true)
+
+	   Update next_state in-place, to get the result of the one
+	   or more stmts that are processed.  */
+	program_state next_state (state);
+	const supernode *snode = point.m_supernode;
+	unsigned stmt_idx;
+	for (stmt_idx = point.m_stmt_idx;
+	     stmt_idx < snode->m_stmts.length ();
+	     stmt_idx++)
+	  {
+	    const gimple *stmt = snode->m_stmts[stmt_idx];
+
+	    if (stmt_idx > point.m_stmt_idx)
+	      if (stmt_requires_new_enode_p (stmt))
+		{
+		  stmt_idx--;
+		  break;
+		}
+
+	    /* Process the stmt.  */
+	    bool any_sm_changes
+	      = node->on_stmt (*this, snode, stmt, &next_state);
+
+	    if (any_sm_changes || flag_analyzer_fine_grained)
+	      break;
+	  }
+	unsigned next_idx = stmt_idx + 1;
+	program_point next_point
+	  = (next_idx < point.m_supernode->m_stmts.length ()
+	     ? program_point::before_stmt (point.m_supernode, next_idx,
+					   point.m_call_string)
+	     : program_point::after_supernode (point.m_supernode,
+					       point.m_call_string));
+	exploded_node *next = get_or_create_node (next_point,
+						  next_state);
+	if (next)
+	  add_edge (node, next, NULL);
+      }
+      break;
+    case PK_AFTER_SUPERNODE:
+      {
+	/* If this is an EXIT BB, detect leaks, and potentially
+	   create a function summary.  */
+	if (point.m_supernode->return_p ())
+	  {
+	    node->detect_leaks (*this);
+	    if (flag_analyzer_call_summaries
+		&& point.m_call_string.empty_p ())
+	      {
+		/* TODO: create function summary
+		   There can be more than one; each corresponds to a different
+		   final enode in the function.  */
+		if (get_logger ())
+		  {
+		    pretty_printer *pp = get_logger ()->get_printer ();
+		    start_log_line ();
+		    get_logger ()->log_partial
+		      ("would create function summary for %qE; state: ",
+		       point.get_function ()->decl);
+		    state.dump_to_pp (m_ext_state, true, pp);
+		    end_log_line ();
+		  }
+		per_function_data *per_fn_data
+		  = get_or_create_per_function_data (point.get_function ());
+		per_fn_data->add_call_summary (node);
+	      }
+	  }
+	/* Traverse into successors of the supernode.  */
+	int i;
+	superedge *succ;
+	FOR_EACH_VEC_ELT (point.m_supernode->m_succs, i, succ)
+	  {
+	    log ("considering SN: %i -> SN: %i",
+		 succ->m_src->m_index, succ->m_dest->m_index);
+
+	    program_point next_point
+	      = program_point::before_supernode (succ->m_dest, succ,
+						 point.m_call_string);
+	    program_state next_state (state);
+
+	    if (!node->on_edge (*this, succ, &next_point, &next_state))
+	      {
+		log ("skipping impossible edge to SN: %i",
+		     succ->m_dest->m_index);
+		continue;
+	      }
+
+	    exploded_node *next = get_or_create_node (next_point, next_state);
+	    if (next)
+	      add_edge (node, next, succ);
+	  }
+      }
+      break;
+    }
+}
+
+/* Ensure that this graph has a stats instance for FN, return it.
+   FN can be NULL, in which case a stats instances is returned covering
+   "functionless" parts of the graph (the origin node).  */
+
+stats *
+exploded_graph::get_or_create_function_stats (function *fn)
+{
+  if (!fn)
+    return &m_functionless_stats;
+
+  if (stats **slot = m_per_function_stats.get (fn))
+    return *slot;
+  else
+    {
+      int num_supernodes = fn ? n_basic_blocks_for_fn (fn) : 0;
+      // FIXME: ^^^ not quite the num supernodes, but nearly
+      stats *new_stats = new stats (num_supernodes);
+      m_per_function_stats.put (fn, new_stats);
+      return new_stats;
+    }
+}
+
+/* Write all stats information to this graph's logger, if any.  */
+
+void
+exploded_graph::log_stats () const
+{
+  if (!get_logger ())
+    return;
+
+  LOG_SCOPE (get_logger ());
+
+  log ("m_sg.num_nodes (): %i", m_sg.num_nodes ());
+  log ("m_nodes.length (): %i", m_nodes.length ());
+  log ("m_edges.length (): %i", m_edges.length ());
+
+  log ("global stats:");
+  m_global_stats.log (get_logger ());
+
+  for (function_stat_map_t::iterator iter = m_per_function_stats.begin ();
+       iter != m_per_function_stats.end ();
+       ++iter)
+    {
+      function *fn = (*iter).first;
+      log_scope s (get_logger (), function_name (fn));
+      (*iter).second->log (get_logger ());
+    }
+}
+
+/* Dump all stats information to OUT.  */
+
+void
+exploded_graph::dump_stats (FILE *out) const
+{
+  fprintf (out, "m_sg.num_nodes (): %i\n", m_sg.num_nodes ());
+  fprintf (out, "m_nodes.length (): %i\n", m_nodes.length ());
+  fprintf (out, "m_edges.length (): %i\n", m_edges.length ());
+
+  fprintf (out, "global stats:\n");
+  m_global_stats.dump (out);
+
+  for (function_stat_map_t::iterator iter = m_per_function_stats.begin ();
+       iter != m_per_function_stats.end ();
+       ++iter)
+    {
+      function *fn = (*iter).first;
+      fprintf (out, "function: %s\n", function_name (fn));
+      (*iter).second->dump (out);
+    }
+
+  fprintf (out, "PK_AFTER_SUPERNODE per supernode:\n");
+  for (unsigned i = 0; i < m_PK_AFTER_SUPERNODE_per_snode.length (); i++)
+    fprintf (out, "  SN %i: %3i\n", i, m_PK_AFTER_SUPERNODE_per_snode[i]);
+}
+
+void
+exploded_graph::dump_states_for_supernode (FILE *out,
+					   const supernode *snode) const
+{
+  fprintf (out, "PK_AFTER_SUPERNODE nodes for SN: %i\n", snode->m_index);
+  int i;
+  exploded_node *enode;
+  int state_idx = 0;
+  FOR_EACH_VEC_ELT (m_nodes, i, enode)
+    {
+      const supernode *iter_snode = enode->get_supernode ();
+      if (enode->get_point ().m_kind == PK_AFTER_SUPERNODE
+	  && iter_snode == snode)
+	{
+	  pretty_printer pp;
+	  pp_format_decoder (&pp) = default_tree_printer;
+	  enode->get_state ().dump_to_pp (m_ext_state, true, &pp);
+	  fprintf (out, "state %i: EN: %i\n  %s\n",
+		   state_idx++, enode->m_index,
+		   pp_formatted_text (&pp));
+	}
+    }
+  fprintf (out, "#exploded_node for PK_AFTER_SUPERNODE for SN: %i = %i\n",
+	   snode->m_index, state_idx);
+}
+
+////////////////////////////////////////////////////////////////////////////
+
+/* Look for the last use of SEARCH_STMT within this path.
+   If found write the edge's index to *OUT_IDX and return true, otherwise
+   return false.  */
+
+bool
+exploded_path::find_stmt_backwards (const gimple *search_stmt,
+				    int *out_idx) const
+{
+  int i;
+  const exploded_edge *eedge;
+  FOR_EACH_VEC_ELT_REVERSE (m_edges, i, eedge)
+    {
+      const exploded_node *dst_node = eedge->m_dest;
+      const program_point &dst_point = dst_node->get_point ();
+      const gimple *stmt = dst_point.get_stmt ();
+      if (stmt == search_stmt)
+	{
+	  *out_idx = i;
+	  return true;
+	}
+    }
+  return false;
+}
+
+/* FIXME.  */
+
+exploded_node *
+exploded_path::get_final_enode () const
+{
+  gcc_assert (m_edges.length () > 0);
+  return m_edges[m_edges.length () - 1]->m_dest;
+}
+
+/* Check state along this path, returning true if it is feasible.  */
+
+bool
+exploded_path::feasible_p (logger *logger) const
+{
+  LOG_SCOPE (logger);
+
+  region_model model;
+  for (unsigned i = 0; i < m_edges.length (); i++)
+    {
+      const exploded_edge *eedge = m_edges[i];
+      if (logger)
+	logger->log ("considering edge %i: EN:%i -> EN:%i",
+		     i,
+		     eedge->m_src->m_index,
+		     eedge->m_dest->m_index);
+      const exploded_node &src_enode = *eedge->m_src;
+      const program_point &src_point = src_enode.get_point ();
+      if (logger)
+	{
+	  logger->start_log_line ();
+	  src_point.print (logger->get_printer (), format (false));
+	  logger->end_log_line ();
+	}
+
+      if (const gimple *stmt = src_point.get_stmt ())
+	{
+	  /* Update cfun and input_location in case of ICE: make it easier to
+	     track down which source construct we're failing to handle.  */
+	  auto_cfun sentinel (src_point.get_function ());
+	  input_location = stmt->location;
+
+	  if (const gassign *assign = dyn_cast <const gassign *> (stmt))
+	    model.on_assignment (assign, NULL);
+	  else if (const greturn *return_ = dyn_cast <const greturn *> (stmt))
+	    model.on_return (return_, NULL);
+	}
+
+      const superedge *sedge = eedge->m_sedge;
+      if (sedge)
+	{
+	  if (logger)
+	    logger->log ("  sedge: SN:%i -> SN:%i %s",
+			 sedge->m_src->m_index,
+			 sedge->m_dest->m_index,
+			 sedge->get_description (false));
+
+	  const gimple *last_stmt = src_point.m_supernode->get_last_stmt ();
+	  if (!model.maybe_update_for_edge (*sedge, last_stmt, NULL))
+	    {
+	      if (logger)
+		logger->log ("rejecting due to region model");
+	      return false;
+	    }
+	}
+      else
+	{
+	  /* Special-case the initial eedge from the origin node to the
+	     initial function by pushing a frame for it.  */
+	  if (i == 0)
+	    {
+	      gcc_assert (eedge->m_src->m_index == 0);
+	      gcc_assert (src_point.m_kind == PK_ORIGIN);
+	      gcc_assert (eedge->m_dest->get_point ().m_kind
+			  == PK_BEFORE_SUPERNODE);
+	      function *fun = eedge->m_dest->get_function ();
+	      gcc_assert (fun);
+	      model.push_frame (fun, NULL);
+	      if (logger)
+		logger->log ("  pushing frame for %qD", fun->decl);
+	    }
+	  else if (eedge->m_rewind_info)
+	    {
+	      /* Update state for the special-case of a rewind of a longjmp
+		 to a setjmp (which doesn't have a superedge, but does affect
+		 state).  */
+	      const gimple *last_stmt = src_point.m_supernode->get_last_stmt ();
+	      gcc_assert (last_stmt);
+	      const gcall *longjmp_call = as_a <const gcall *> (last_stmt);
+
+	      const program_point &longjmp_point = eedge->m_src->get_point ();
+	      const program_point &setjmp_point = eedge->m_dest->get_point ();
+
+	      gcc_assert (longjmp_point.get_stack_depth ()
+			  >= setjmp_point.get_stack_depth ());
+
+	      model.on_longjmp (longjmp_call,
+				eedge->m_rewind_info->get_setjmp_call (),
+				setjmp_point.get_stack_depth (), NULL);
+	    }
+	}
+
+      /* Handle phi nodes on an edge leaving a PK_BEFORE_SUPERNODE (to
+	 a PK_BEFORE_STMT, or a PK_AFTER_SUPERNODE if no stmts).
+	 This will typically not be associated with a superedge.  */
+      if (src_point.m_from_edge)
+	{
+	  const cfg_superedge *last_cfg_superedge
+	    = src_point.m_from_edge->dyn_cast_cfg_superedge ();
+	  if (last_cfg_superedge)
+	    {
+	      if (logger)
+		logger->log ("  update for phis");
+	      model.update_for_phis (src_enode.get_supernode (),
+				     last_cfg_superedge,
+				     NULL);
+	    }
+	}
+
+      if (logger)
+	{
+	  logger->log ("state after edge %i: EN:%i -> EN:%i",
+		       i,
+		       eedge->m_src->m_index,
+		       eedge->m_dest->m_index);
+	  logger->start_log_line ();
+	  model.dump_to_pp (logger->get_printer (), true);
+	  logger->end_log_line ();
+	}
+    }
+
+  // TODO
+  return true;
+}
+
+/* FIXME.  */
+
+void
+exploded_path::dump_to_pp (pretty_printer *pp) const
+{
+  for (unsigned i = 0; i < m_edges.length (); i++)
+    {
+      const exploded_edge *eedge = m_edges[i];
+      pp_printf (pp, "m_edges[%i]: EN %i -> EN %i",
+		 i,
+		 eedge->m_src->m_index,
+		 eedge->m_dest->m_index);
+      pp_newline (pp);
+    }
+}
+
+/* FIXME.  */
+
+void
+exploded_path::dump (FILE *fp) const
+{
+  pretty_printer pp;
+  pp_format_decoder (&pp) = default_tree_printer;
+  pp_show_color (&pp) = pp_show_color (global_dc->printer);
+  pp.buffer->stream = fp;
+  dump_to_pp (&pp);
+  pp_flush (&pp);
+}
+
+/* FIXME.  */
+
+DEBUG_FUNCTION void
+exploded_path::dump () const
+{
+  dump (stderr);
+}
+
+////////////////////////////////////////////////////////////////////////////
+
+/* shortest_paths's constructor.
+
+   Use Dijkstra's algorithm relative to the origin of EG to populate
+   m_dist and m_prev with enough information to be able to generate
+   exploded_path instances to give the shortest path to any node in EG
+   from the origin node.  */
+
+shortest_paths::shortest_paths (const exploded_graph &eg)
+: m_eg (eg),
+  m_dist (eg.m_nodes.length ()),
+  m_prev (eg.m_nodes.length ())
+{
+  auto_client_timevar tv ("shortest_paths");
+
+  auto_vec<int> queue (eg.m_nodes.length ());
+
+  for (unsigned i = 0; i < eg.m_nodes.length (); i++)
+    {
+      m_dist.quick_push (INT_MAX);
+      m_prev.quick_push (NULL);
+      queue.quick_push (i);
+    }
+  m_dist[eg.get_origin ()->m_index] = 0;
+
+  while (queue.length () > 0)
+    {
+      /* Get minimal distance in queue.
+	 FIXME: this is O(N^2); replace with a priority queue.  */
+      int idx_with_min_dist = -1;
+      int idx_in_queue_with_min_dist = -1;
+      int min_dist = INT_MAX;
+      for (unsigned i = 0; i < queue.length (); i++)
+	{
+	  int idx = queue[i];
+	  if (m_dist[queue[i]] < min_dist)
+	    {
+	      min_dist = m_dist[idx];
+	      idx_with_min_dist = idx;
+	      idx_in_queue_with_min_dist = i;
+	    }
+	}
+      gcc_assert (idx_with_min_dist != -1);
+      gcc_assert (idx_in_queue_with_min_dist != -1);
+
+      // FIXME: this is confusing: there are two indices here
+
+      queue.unordered_remove (idx_in_queue_with_min_dist);
+
+      exploded_node *n
+	= static_cast <exploded_node *> (m_eg.m_nodes[idx_with_min_dist]);
+
+      int i;
+      exploded_edge *succ;
+      FOR_EACH_VEC_ELT (n->m_succs, i, succ)
+	{
+	  // TODO: only for dest still in queue
+	  exploded_node *dest = succ->m_dest;
+	  int alt = m_dist[n->m_index] + 1;
+	  if (alt < m_dist[dest->m_index])
+	    {
+	      m_dist[dest->m_index] = alt;
+	      m_prev[dest->m_index] = succ;
+	    }
+	}
+   }
+}
+
+/* Gnerate an exploded_path instance giving the shortest path to the node
+   TO from the origin node.  */
+
+exploded_path
+shortest_paths::get_shortest_path (const exploded_node *to) const
+{
+  exploded_path result;
+
+  while (m_prev[to->m_index])
+    {
+      result.m_edges.safe_push (m_prev[to->m_index]);
+      to = m_prev[to->m_index]->m_src;
+    }
+
+  result.m_edges.reverse ();
+
+  return result;
+}
+
+////////////////////////////////////////////////////////////////////////////
+
+/* Print a single-line representation of this path to PP.  */
+
+void
+checker_path::dump (pretty_printer *pp) const
+{
+  pp_character (pp, '[');
+
+  checker_event *e;
+  int i;
+  FOR_EACH_VEC_ELT (m_events, i, e)
+    {
+      if (i > 0)
+	pp_string (pp, ", ");
+      label_text event_desc (e->get_desc (false));
+      pp_printf (pp, "\"%s\"", event_desc.m_buffer);
+      event_desc.maybe_free ();
+    }
+  pp_character (pp, ']');
+}
+
+////////////////////////////////////////////////////////////////////////////
+
+// FIXME:
+/* Subclass of cluster which casts the dnode instances to exploded_node.  */
+
+class exploded_cluster : public cluster<eg_traits>
+{
+};
+
+/* Cluster containing all exploded_node instances for one supernode.  */
+
+class supernode_cluster : public exploded_cluster
+{
+public:
+  supernode_cluster (const supernode *supernode) : m_supernode (supernode) {}
+
+  // TODO: dtor?
+
+  void dump_dot (graphviz_out *gv, const dump_args_t &args) const FINAL OVERRIDE
+  {
+    gv->println ("subgraph \"cluster_supernode_%p\" {",
+		 (const void *)this);
+    gv->indent ();
+    gv->println ("style=\"dashed\";");
+    gv->println ("label=\"SN: %i\";", m_supernode->m_index);
+
+    int i;
+    exploded_node *enode;
+    FOR_EACH_VEC_ELT (m_enodes, i, enode)
+      enode->dump_dot (gv, args);
+
+    /* Terminate subgraph.  */
+    gv->outdent ();
+    gv->println ("}");
+  }
+
+  void add_node (exploded_node *en) FINAL OVERRIDE
+  {
+    m_enodes.safe_push (en);
+  }
+
+private:
+  const supernode *m_supernode;
+  auto_vec <exploded_node *> m_enodes;
+};
+
+/* Cluster containing all supernode_cluster instances for one
+   (function, call_string) pair.  */
+
+class function_call_string_cluster : public exploded_cluster
+{
+public:
+  function_call_string_cluster (function *fun, call_string cs)
+  : m_fun (fun), m_cs (cs) {}
+
+  ~function_call_string_cluster ()
+  {
+    for (map_t::iterator iter = m_map.begin ();
+	 iter != m_map.end ();
+	 ++iter)
+      delete (*iter).second;
+  }
+
+  void dump_dot (graphviz_out *gv, const dump_args_t &args) const FINAL OVERRIDE
+  {
+    const char *funcname = function_name (m_fun);
+
+    gv->println ("subgraph \"cluster_function_%p\" {", (const void *)this);
+    gv->indent ();
+    gv->write_indent ();
+    gv->print ("label=\"call string: ");
+    m_cs.print (gv->get_pp ());
+    gv->print (" function: %s \";", funcname);
+    gv->print ("\n");
+
+    for (map_t::iterator iter = m_map.begin ();
+	 iter != m_map.end ();
+	 ++iter)
+      (*iter).second->dump_dot (gv, args);
+
+    /* Terminate subgraph.  */
+    gv->outdent ();
+    gv->println ("}");
+  }
+
+  void add_node (exploded_node *en) FINAL OVERRIDE
+  {
+    const supernode *supernode = en->get_supernode ();
+    gcc_assert (supernode);
+    supernode_cluster **slot = m_map.get (supernode);
+    if (slot)
+      (*slot)->add_node (en);
+    else
+      {
+	supernode_cluster *child = new supernode_cluster (supernode);
+	m_map.put (supernode, child);
+	child->add_node (en);
+      }
+  }
+
+private:
+  function *m_fun;
+  call_string m_cs;
+  typedef ordered_hash_map<const supernode *, supernode_cluster *> map_t;
+  map_t m_map;
+};
+
+/* Keys for root_cluster.  */
+
+struct function_call_string
+{
+  function_call_string (function *fun, call_string cs)
+  : m_fun (fun), m_cs (cs)
+  {
+    gcc_assert (fun);
+  }
+
+  function *m_fun;
+  call_string m_cs;
+};
+
+template <> struct default_hash_traits<function_call_string>
+: public pod_hash_traits<function_call_string>
+{
+};
+
+template <>
+inline hashval_t
+pod_hash_traits<function_call_string>::hash (value_type v)
+{
+  return pointer_hash <function>::hash (v.m_fun) ^ v.m_cs.hash ();
+}
+
+template <>
+inline bool
+pod_hash_traits<function_call_string>::equal (const value_type &existing,
+					      const value_type &candidate)
+{
+  return existing.m_fun == candidate.m_fun && existing.m_cs == candidate.m_cs;
+}
+template <>
+inline void
+pod_hash_traits<function_call_string>::mark_deleted (value_type &v)
+{
+  v.m_fun = reinterpret_cast<function *> (1);
+}
+template <>
+inline void
+pod_hash_traits<function_call_string>::mark_empty (value_type &v)
+{
+  v.m_fun = reinterpret_cast<function *> (NULL);
+}
+template <>
+inline bool
+pod_hash_traits<function_call_string>::is_deleted (value_type v)
+{
+  return v.m_fun == reinterpret_cast<function *> (1);
+}
+template <>
+inline bool
+pod_hash_traits<function_call_string>::is_empty (value_type v)
+{
+  return v.m_fun == reinterpret_cast<function *> (NULL);
+}
+
+/* Top-level cluster for printing exploded graphs, handling the functionless
+   nodes, and grouping the remaining nodes by callstring.  */
+
+class root_cluster : public exploded_cluster
+{
+public:
+  ~root_cluster ()
+  {
+    for (map_t::iterator iter = m_map.begin ();
+	 iter != m_map.end ();
+	 ++iter)
+      delete (*iter).second;
+  }
+
+  void dump_dot (graphviz_out *gv, const dump_args_t &args) const FINAL OVERRIDE
+  {
+    int i;
+    exploded_node *enode;
+    FOR_EACH_VEC_ELT (m_functionless_enodes, i, enode)
+      enode->dump_dot (gv, args);
+
+    for (map_t::iterator iter = m_map.begin ();
+	 iter != m_map.end ();
+	 ++iter)
+      (*iter).second->dump_dot (gv, args);
+  }
+
+  void add_node (exploded_node *en) FINAL OVERRIDE
+  {
+    function *fun = en->get_function ();
+    if (!fun)
+      {
+	m_functionless_enodes.safe_push (en);
+	return;
+      }
+
+    const call_string &cs = en->get_point ().m_call_string;
+    function_call_string key (fun, cs);
+    function_call_string_cluster **slot = m_map.get (key);
+    if (slot)
+      (*slot)->add_node (en);
+    else
+      {
+	function_call_string_cluster *child
+	  = new function_call_string_cluster (fun, cs);
+	m_map.put (key, child);
+	child->add_node (en);
+      }
+  }
+
+private:
+  /* This can't be an ordered_hash_map, as we can't store vec<call_string>,
+     since it's not a POD; vec<>::quick_push has:
+       *slot = obj;
+     and the slot isn't initialized, so the assignment op dies when cleaning up
+     un-inited *slot (within the truncate call).  */
+  typedef hash_map<function_call_string, function_call_string_cluster *> map_t;
+  map_t m_map;
+
+  /* This should just be the origin exploded_node.  */
+  auto_vec <exploded_node *> m_functionless_enodes;
+};
+
+////////////////////////////////////////////////////////////////////////////
+
+/* state_purge_map's ctor.  Walk all SSA names in all functions, building
+   a state_purge_per_ssa_name instance for each.  */
+
+state_purge_map::state_purge_map (const supergraph &sg,
+				  logger *logger)
+: log_user (logger), m_sg (sg)
+{
+  LOG_FUNC (logger);
+
+  auto_client_timevar tv ("state_purge_map ctor");
+
+  cgraph_node *node;
+  FOR_EACH_FUNCTION_WITH_GIMPLE_BODY (node)
+  {
+    function *fun = node->get_fun ();
+    if (logger)
+      log ("function: %s", function_name (fun));
+    tree var;
+    unsigned int i;;
+    FOR_EACH_SSA_NAME (i, var, fun)
+      m_map.put (var, new state_purge_per_ssa_name (*this, var, fun));
+  }
+}
+
+/* state_purge_map's dtor.  */
+
+state_purge_map::~state_purge_map ()
+{
+  for (iterator iter = m_map.begin (); iter != m_map.end (); ++iter)
+    delete (*iter).second;
+}
+
+/* state_purge_per_ssa_name's ctor.
+
+   Locate all uses of VAR within FUN.
+   Walk backwards from each use, marking program points, until
+   we reach the def stmt, populating m_points_needing_var.
+
+   We have to track program points rather than
+   just stmts since there could be empty basic blocks on the way.  */
+
+state_purge_per_ssa_name::state_purge_per_ssa_name (const state_purge_map &map,
+						    tree var,
+						    function *fun)
+: m_var (var)
+{
+  LOG_FUNC (map.get_logger ());
+
+  map.log ("SSA name: %qE within %qD", var, fun->decl);
+  if (map.get_logger ())
+    {
+      /* Show def stmt.  */
+      const gimple *def_stmt = SSA_NAME_DEF_STMT (var);
+      pretty_printer pp;
+      pp_gimple_stmt_1 (&pp, def_stmt, 0, (dump_flags_t)0);
+      map.log ("def stmt: %s", pp_formatted_text (&pp));
+    }
+
+  /* The worklist for the traversal.
+     We can't have a vec<program_point> here because vec<T> require T to be
+     a POD.  Hence (annoyingly) we use vec<program_point *> and dynamically
+     allocate.  */
+  auto_vec<program_point *> worklist;
+
+  /* Add all immediate uses of var to the worklist.
+     Compare with debug_immediate_uses.  */
+  imm_use_iterator iter;
+  use_operand_p use_p;
+  FOR_EACH_IMM_USE_FAST (use_p, iter, var)
+    {
+      if (USE_STMT (use_p))
+	{
+	  const gimple *use_stmt = USE_STMT (use_p);
+	  if (map.get_logger ())
+	    {
+	      pretty_printer pp;
+	      pp_gimple_stmt_1 (&pp, use_stmt, 0, (dump_flags_t)0);
+	      map.log ("used by stmt: %s", pp_formatted_text (&pp));
+	    }
+
+	  const supernode *snode
+	    = map.get_sg ().get_supernode_for_stmt (use_stmt);
+
+	  /* If it's a use within a phi node, then we care about
+	     which in-edge we came from.  */
+	  if (use_stmt->code == GIMPLE_PHI)
+	    {
+	      for (gphi_iterator gpi
+		     = const_cast<supernode *> (snode)->start_phis ();
+		   !gsi_end_p (gpi); gsi_next (&gpi))
+		{
+		  gphi *phi = gpi.phi ();
+		  if (phi == use_stmt)
+		    {
+		      /* Find arguments (and thus in-edges) which use VAR.  */
+		      for (unsigned arg_idx = 0;
+			   arg_idx < gimple_phi_num_args (phi);
+			   ++arg_idx)
+			{
+			  if (var == gimple_phi_arg (phi, arg_idx)->def)
+			    {
+			      edge in_edge = gimple_phi_arg_edge (phi, arg_idx);
+			      const superedge *in_sedge
+				= map.get_sg ().get_edge_for_cfg_edge (in_edge);
+			      program_point point
+				= program_point::before_supernode
+				(snode, in_sedge, call_string ());
+			      add_to_worklist (point, &worklist,
+					       map.get_logger ());
+			      m_points_needing_var.add (point);
+			    }
+			}
+		    }
+		}
+	    }
+	  else
+	    {
+	      program_point point = before_use_stmt (map, use_stmt);
+	      add_to_worklist (point, &worklist, map.get_logger ());
+	      m_points_needing_var.add (point);
+
+	      /* We also need to add uses for conditionals and switches,
+		 where the stmt "happens" at the after_supernode, for filtering
+		 the out-edges.  */
+	      if (use_stmt == snode->get_last_stmt ())
+		{
+		  if (map.get_logger ())
+		    map.log ("last stmt in BB");
+		  program_point point
+		    = program_point::after_supernode (snode, call_string ());
+		  add_to_worklist (point, &worklist, map.get_logger ());
+		  m_points_needing_var.add (point);
+		}
+	      else
+		if (map.get_logger ())
+		  map.log ("not last stmt in BB");
+	    }
+	}
+    }
+
+  /* Process worklist by walking backwards until we reach the def stmt.  */
+  {
+    log_scope s (map.get_logger (), "processing worklist");
+    while (worklist.length () > 0)
+      {
+	program_point *point_ptr = worklist.pop ();
+	program_point point (*point_ptr);
+	process_point (*point_ptr, &worklist, map.get_logger ());
+	delete point_ptr;
+    }
+  }
+
+  if (map.get_logger ())
+    {
+      map.log ("%qE in %qD is needed to process:", var, fun->decl);
+      for (point_set_t::iterator iter = m_points_needing_var.begin ();
+	   iter != m_points_needing_var.end ();
+	   ++iter)
+	{
+	  map.start_log_line ();
+	  map.get_logger ()->log_partial ("  point: ");
+	  (*iter).print (map.get_logger ()->get_printer (), format (false));
+	  map.end_log_line ();
+	}
+    }
+}
+
+/* Return true if the SSA name is needed at POINT.  */
+
+bool
+state_purge_per_ssa_name::needed_at_point_p (const program_point &point) const
+{
+  return const_cast <point_set_t &> (m_points_needing_var).contains (point);
+}
+
+/* Get the program_point representing immediately before USE_STMT.
+   Subroutine of ctor.  */
+
+program_point
+state_purge_per_ssa_name::before_use_stmt (const state_purge_map &map,
+					   const gimple *use_stmt)
+{
+  gcc_assert (use_stmt->code != GIMPLE_PHI); // FIXME: for now
+#if 0
+  if (stmt->code == GIMPLE_PHI)
+    {
+      const supernode *supernode = sg.get_node_for_block (stmt->bb);
+      return before_supernode (supernode, from_edge, call_string);
+    }
+  else
+#endif
+    {
+      const supernode *supernode
+	= map.get_sg ().get_supernode_for_stmt (use_stmt);
+      unsigned int stmt_idx = supernode->get_stmt_index (use_stmt);
+      return program_point::before_stmt (supernode, stmt_idx, call_string ());
+    }
+}
+
+/* Add POINT to *WORKLIST if the point has not already been seen.
+   Subroutine of ctor.  */
+
+void
+state_purge_per_ssa_name::add_to_worklist (const program_point &point,
+					   auto_vec<program_point *> *worklist,
+					   logger *logger)
+{
+  LOG_FUNC (logger);
+  if (logger)
+    {
+      logger->start_log_line ();
+      logger->log_partial ("point: '");
+      point.print (logger->get_printer (), format (false));
+      logger->log_partial ("' for worklist for %qE", m_var);
+      logger->end_log_line ();
+    }
+
+  if (m_points_needing_var.contains (point))
+    {
+      if (logger)
+	logger->log ("already seen for %qE", m_var);
+    }
+  else
+    {
+      if (logger)
+	logger->log ("not seen; adding to worklist for %qE", m_var);
+      worklist->safe_push (new program_point (point));
+    }
+}
+
+/* Process POINT, popped from WORKLIST.
+   Iterate over predecessors of POINT, adding to WORKLIST.  */
+
+void
+state_purge_per_ssa_name::process_point (const program_point &point,
+					 auto_vec<program_point *> *worklist,
+					 logger *logger)
+{
+  LOG_FUNC (logger);
+  if (logger)
+    {
+      logger->start_log_line ();
+      logger->log_partial ("considering point: '");
+      point.print (logger->get_printer (), format (false));
+      logger->log_partial ("' for %qE", m_var);
+      logger->end_log_line ();
+    }
+
+  gimple *def_stmt = SSA_NAME_DEF_STMT (m_var);
+
+  m_points_needing_var.add (point);
+
+  const supernode *snode = point.get_supernode ();
+
+  switch (point.m_kind)
+    {
+    default:
+      gcc_unreachable ();
+
+    case PK_ORIGIN:
+      break;
+
+    case PK_BEFORE_SUPERNODE:
+      {
+	for (gphi_iterator gpi
+	       = const_cast<supernode *> (snode)->start_phis ();
+	     !gsi_end_p (gpi); gsi_next (&gpi))
+	  {
+	    gphi *phi = gpi.phi ();
+	    if (phi == def_stmt)
+	      {
+		if (logger)
+		  logger->log ("def stmt within phis; terminating");
+		return;
+	      }
+	  }
+
+	/* Add given pred to worklist.  */
+	if (point.m_from_edge)
+	  {
+	    if (point.m_from_edge->m_src)
+	      add_to_worklist
+		(program_point::after_supernode (point.m_from_edge->m_src,
+						 call_string ()),
+		 worklist, logger);
+	  }
+	else
+	  gcc_assert (snode->entry_p ());
+      }
+      break;
+
+    case PK_BEFORE_STMT:
+      {
+	if (def_stmt == point.get_stmt ())
+	  {
+	    if (logger)
+	      logger->log ("def stmt; terminating");
+	    return;
+	  }
+	if (point.m_stmt_idx > 0)
+	  add_to_worklist (program_point::before_stmt (snode,
+						       point.m_stmt_idx - 1,
+						       call_string ()),
+			   worklist, logger);
+	else
+	{
+	  /* Add before_supernode to worklist.  This captures the in-edge,
+	     so we have to do it once per in-edge.  */
+	  unsigned i;
+	  superedge *pred;
+	  FOR_EACH_VEC_ELT (snode->m_preds, i, pred)
+	    add_to_worklist (program_point::before_supernode (snode,
+							      pred,
+							      call_string ()),
+			     worklist, logger);
+	}
+      }
+      break;
+
+    case PK_AFTER_SUPERNODE:
+      {
+	if (snode->m_stmts.length ())
+	  add_to_worklist
+	    (program_point::before_stmt (snode,
+					 snode->m_stmts.length () - 1,
+					 call_string ()),
+	     worklist, logger);
+	else
+	  {
+	    /* Add before_supernode to worklist.  This captures the in-edge,
+	       so we have to do it once per in-edge.  */
+	    unsigned i;
+	    superedge *pred;
+	    FOR_EACH_VEC_ELT (snode->m_preds, i, pred)
+	      add_to_worklist (program_point::before_supernode (snode,
+								pred,
+								call_string ()),
+			       worklist, logger);
+	    /* If it's the initial BB, add it, to ensure that we
+	       have "before supernode" for the initial ENTRY block, and don't
+	       erroneously purge SSA names for initial values of parameters.  */
+	    if (snode->entry_p ())
+	      {
+		add_to_worklist
+		  (program_point::before_supernode (snode,
+						    NULL,
+						    call_string ()),
+		   worklist, logger);
+	      }
+	  }
+      }
+      break;
+    }
+}
+
+/* FIXME.  */
+
+class state_purge_annotator  : public dot_annotator
+{
+public:
+  state_purge_annotator (const state_purge_map *map) : m_map (map) {}
+
+  void add_node_annotations (pretty_printer *pp ATTRIBUTE_UNUSED,
+			     const supernode &n ATTRIBUTE_UNUSED)
+    const FINAL OVERRIDE
+  {
+    if (m_map == NULL)
+      return;
+
+#if 0
+    //pp_printf (pp, "FIXME: node annotation");
+    pp_printf (pp, "annotation_for_node_%i", n.m_index);
+    pp_printf (pp, " [shape=%s,style=filled,fillcolor=%s,label=\"",
+	       "record", "lightblue");
+    pp_left_brace (pp);
+    pp_write_text_to_stream (pp);
+
+    // TODO: which in-edge?
+    program_point before_supernode
+      (program_point::before_supernode (&n, call_string ()));
+
+    for (state_purge_map::iterator iter = m_map.begin ();
+	 iter != m_map.end ();
+	 ++iter)
+      {
+	tree var = (*iter).first;
+	state_purge_per_ssa_name *per_var_data = (*iter).second;
+	if (per_var_data->needed_at_point_p (before_supernode))
+	  pp_printf (pp, "%qE needed here", var);
+	else
+	  pp_printf (pp, "%qE not needed here", var);
+	pp_newline (pp);
+      }
+
+    pp_right_brace (pp);
+
+    pp_string (pp, "\"];\n\n");
+    pp_flush (pp);
+#endif
+  }
+
+  void add_stmt_annotations (pretty_printer *pp ATTRIBUTE_UNUSED,
+			     const gimple *stmt ATTRIBUTE_UNUSED)
+    const FINAL OVERRIDE
+  {
+    if (m_map == NULL)
+      return;
+
+    if (stmt->code == GIMPLE_PHI)
+      return;
+
+    pp_newline (pp);
+
+    const supernode *supernode = m_map->get_sg ().get_supernode_for_stmt (stmt);
+    unsigned int stmt_idx = supernode->get_stmt_index (stmt);
+    program_point before_stmt
+      (program_point::before_stmt (supernode, stmt_idx, call_string ()));
+
+    auto_vec<tree> needed;
+    auto_vec<tree> not_needed;
+    for (state_purge_map::iterator iter = m_map->begin ();
+	 iter != m_map->end ();
+	 ++iter)
+      {
+	tree var = (*iter).first;
+	state_purge_per_ssa_name *per_var_data = (*iter).second;
+	if (per_var_data->needed_at_point_p (before_stmt))
+	  needed.safe_push (var);
+	else
+	  not_needed.safe_push (var);
+      }
+
+    tree var;
+    unsigned i;
+    pp_printf (pp, "needed here: {");
+    FOR_EACH_VEC_ELT (needed, i, var)
+      {
+	if (i > 0)
+	  pp_string (pp, ", ");
+PUSH_IGNORE_WFORMAT
+	pp_printf (pp, "%qE", var);
+POP_IGNORE_WFORMAT
+      }
+    pp_printf (pp, "}");
+    pp_newline (pp);
+    pp_printf (pp, "not needed here: {");
+    FOR_EACH_VEC_ELT (not_needed, i, var)
+      {
+	if (i > 0)
+	  pp_string (pp, ", ");
+PUSH_IGNORE_WFORMAT
+	pp_printf (pp, "%qE", var);
+POP_IGNORE_WFORMAT
+      }
+    pp_newline (pp);
+  }
+
+private:
+  const state_purge_map *m_map;
+};
+
+////////////////////////////////////////////////////////////////////////////
+
+/* class analysis_plan.  */
+
+/* analysis_plan's ctor.  */
+
+analysis_plan::analysis_plan (const supergraph &sg, logger *logger)
+: log_user (logger), m_sg (sg),
+  m_cgraph_node_postorder (XCNEWVEC (struct cgraph_node *,
+				     symtab->cgraph_count)),
+  m_index_by_uid (symtab->cgraph_max_uid)
+{
+  LOG_SCOPE (logger);
+  auto_client_timevar tv ("creating analysis plan");
+
+  m_num_cgraph_nodes = ipa_reverse_postorder (m_cgraph_node_postorder);
+  gcc_assert (m_num_cgraph_nodes == symtab->cgraph_count);
+  if (get_logger_file ())
+    ipa_print_order (get_logger_file (),
+		     "analysis_plan", m_cgraph_node_postorder,
+		     m_num_cgraph_nodes);
+
+  /* Populate m_index_by_uid.  */
+  for (int i = 0; i < symtab->cgraph_max_uid; i++)
+    m_index_by_uid.quick_push (-1);
+  for (int i = 0; i < m_num_cgraph_nodes; i++)
+    {
+      gcc_assert (m_cgraph_node_postorder[i]->get_uid ()
+		  < symtab->cgraph_max_uid);
+      m_index_by_uid[m_cgraph_node_postorder[i]->get_uid ()] = i;
+    }
+}
+
+/* analysis_plan's dtor.  */
+
+analysis_plan::~analysis_plan ()
+{
+  free (m_cgraph_node_postorder);
+}
+
+/* FIXME.  */
+
+int
+analysis_plan::cmp_function (function *fun_a, function *fun_b) const
+{
+  cgraph_node *node_a = cgraph_node::get (fun_a->decl);
+  cgraph_node *node_b = cgraph_node::get (fun_b->decl);
+
+  int idx_a = m_index_by_uid[node_a->get_uid ()];
+  int idx_b = m_index_by_uid[node_b->get_uid ()];
+
+  return idx_b - idx_a; // FIXME
+}
+
+/* Return true if the call EDGE should be analyzed using a call summary.
+   Return false if it should be analyzed using a full call and return.  */
+
+bool
+analysis_plan::use_summary_p (const cgraph_edge *edge) const
+{
+  /* Don't use call summaries if -fno-analyzer-call-summaries.  */
+  if (!flag_analyzer_call_summaries)
+    return false;
+
+  /* TODO: don't count callsites each time.  */
+  int num_call_sites = 0;
+  const cgraph_node *callee = edge->callee;
+  for (cgraph_edge *edge = callee->callers; edge; edge = edge->next_caller)
+    ++num_call_sites;
+
+  /* Don't use a call summary if there's only one call site.  */
+  if (num_call_sites <= 1)
+    return false;
+
+  /* Require the callee to be sufficiently complex to be worth
+     summarizing.  */
+  if ((int)m_sg.get_num_snodes (callee->get_fun ())
+      < PARAM_VALUE (PARAM_ANALYZER_MIN_SNODES_FOR_CALL_SUMMARY))
+    return false;
+
+  return true;
+}
+
+////////////////////////////////////////////////////////////////////////////
+
+#if 0
+/* FIXME.  */
+
+static int
+function_comparator (const void *pn1, const void *pn2)
+{
+  const cgraph_node *node1 = *(const cgraph_node * const *)pn1;
+  const cgraph_node *node2 = *(const cgraph_node * const *)pn2;
+
+  location_t loc1 = node1->get_fun ()->function_start_locus;
+  location_t loc2 = node2->get_fun ()->function_start_locus;
+
+  return linemap_compare_locations (line_table, loc2, loc1);
+}
+#endif
+
+/* FIXME.  */
+
+class enode_label : public range_label
+{
+ public:
+  enode_label (const extrinsic_state &ext_state,
+	       exploded_node *enode)
+  : m_ext_state (ext_state), m_enode (enode) {}
+
+  label_text get_text (unsigned) const FINAL OVERRIDE
+  {
+    pretty_printer pp;
+    pp_format_decoder (&pp) = default_tree_printer;
+    m_enode->get_state ().dump_to_pp (m_ext_state, true, &pp);
+    return make_label_text (false, "EN: %i: %s",
+			    m_enode->m_index, pp_formatted_text (&pp));
+  }
+
+private:
+  const extrinsic_state &m_ext_state;
+  exploded_node *m_enode;
+};
+
+/* Postprocessing support for dumping the exploded nodes.
+   Handle -fdump-analyzer-exploded-nodes,
+   -fdump-analyzer-exploded-nodes-2, and the
+   "__analyzer_dump_exploded_nodes" builtin.  */
+
+void
+exploded_graph::dump_exploded_nodes () const
+{
+  // TODO
+  /* Locate calls to __analyzer_dump_exploded_nodes.  */
+  // Print how many egs there are for them?
+  /* Better: log them as we go, and record the exploded nodes
+     in question.  */
+
+  /* Show every enode.  */
+
+  /* Gather them by stmt, so that we can more clearly see the
+     "hotspots" requiring numerous exploded nodes.  */
+
+  /* Alternatively, simply throw them all into one big rich_location
+     and see if the label-printing will sort it out...
+     This requires them all to be in the same source file.  */
+
+  if (flag_dump_analyzer_exploded_nodes)
+    {
+      auto_client_timevar tv ("-fdump-analyzer-exploded-nodes");
+      gcc_rich_location richloc (UNKNOWN_LOCATION);
+      unsigned i;
+      exploded_node *enode;
+      FOR_EACH_VEC_ELT (m_nodes, i, enode)
+	{
+	  //const supernode *iter_snode = enode->get_supernode ();
+	  if (const gimple *stmt = enode->get_stmt ())
+	    {
+	      if (richloc.get_loc () == UNKNOWN_LOCATION)
+		richloc.set_range (0, stmt->location, SHOW_RANGE_WITH_CARET);
+	      else
+		richloc.add_range (stmt->location,
+				   SHOW_RANGE_WITHOUT_CARET,
+				   new enode_label (m_ext_state, enode));
+	    }
+	}
+      warning_at (&richloc, 0, "%i exploded nodes", m_nodes.length ());
+
+      /* Repeat the warning without all the labels, so that message is visible
+	 (the other one may well have scrolled past the terminal limit).  */
+      warning_at (richloc.get_loc (), 0,
+		  "%i exploded nodes", m_nodes.length ());
+
+      if (m_worklist.length () > 0)
+	warning_at (richloc.get_loc (), 0,
+		    "worklist still contains %i nodes", m_worklist.length ());
+    }
+
+  /* Dump the egraph in textual form to a dump file.  */
+  if (flag_dump_analyzer_exploded_nodes_2)
+    {
+      auto_client_timevar tv ("-fdump-analyzer-exploded-nodes-2");
+      char *filename
+	= concat (dump_base_name, ".eg.txt", NULL);
+      FILE *outf = fopen (filename, "w");
+      if (!outf)
+	error_at (UNKNOWN_LOCATION, "unable to open %qs for writing", filename);
+      free (filename);
+
+      fprintf (outf, "exploded graph for %s\n", dump_base_name);
+      fprintf (outf, "  nodes: %i\n", m_nodes.length ());
+      fprintf (outf, "  edges: %i\n", m_edges.length ());
+
+      unsigned i;
+      exploded_node *enode;
+      FOR_EACH_VEC_ELT (m_nodes, i, enode)
+	{
+	  fprintf (outf, "\nEN %i:\n", enode->m_index);
+	  enode->dump_succs_and_preds (outf);
+	  pretty_printer pp;
+	  enode->get_point ().print (&pp, format (true));
+	  fprintf (outf, "%s\n", pp_formatted_text (&pp));
+	  enode->get_state ().dump_to_file (m_ext_state, false, outf);
+	}
+
+      fclose (outf);
+    }
+
+  /* Dump the egraph in textual form to multiple dump files, one per enode.  */
+  if (flag_dump_analyzer_exploded_nodes_3)
+    {
+      auto_client_timevar tv ("-fdump-analyzer-exploded-nodes-3");
+
+      unsigned i;
+      exploded_node *enode;
+      FOR_EACH_VEC_ELT (m_nodes, i, enode)
+	{
+	  char *filename
+	    = xasprintf ("%s.en-%i.txt", dump_base_name, i);
+	  FILE *outf = fopen (filename, "w");
+	  if (!outf)
+	    error_at (UNKNOWN_LOCATION, "unable to open %qs for writing", filename);
+	  free (filename);
+
+	  fprintf (outf, "EN %i:\n", enode->m_index);
+	  enode->dump_succs_and_preds (outf);
+	  pretty_printer pp;
+	  enode->get_point ().print (&pp, format (true));
+	  fprintf (outf, "%s\n", pp_formatted_text (&pp));
+	  enode->get_state ().dump_to_file (m_ext_state, false, outf);
+
+	  fclose (outf);
+	}
+    }
+
+  /* Emit a warning at any call to "__analyzer_dump_exploded_nodes",
+     giving the number of exploded nodes for "before-stmt", and their
+     IDs.  */
+
+  unsigned i;
+  exploded_node *enode;
+  hash_set<const gimple *> seen;
+  FOR_EACH_VEC_ELT (m_nodes, i, enode)
+    {
+      if (enode->get_point ().m_kind != PK_BEFORE_STMT)
+	continue;
+
+      if (const gimple *stmt = enode->get_stmt ())
+	if (const gcall *call = dyn_cast <const gcall *> (stmt))
+	  if (is_named_call_p (call, "__analyzer_dump_exploded_nodes", 1))
+	    {
+	      if (seen.contains (stmt))
+		continue;
+
+	      /* This is O(N^2).  */
+	      unsigned j;
+	      auto_vec<exploded_node *> enodes;
+	      exploded_node *other_enode;
+	      FOR_EACH_VEC_ELT (m_nodes, j, other_enode)
+		{
+		  if (other_enode->get_point ().m_kind != PK_BEFORE_STMT)
+		    continue;
+		  if (other_enode->get_stmt () == stmt)
+		    enodes.safe_push (other_enode);
+		}
+
+	      pretty_printer pp;
+	      print_enode_indices (&pp, enodes);
+
+	      warning_n (stmt->location, 0, enodes.length (),
+			 "%i exploded node: %s",
+			 "%i exploded nodes: %s",
+			 enodes.length (), pp_formatted_text (&pp));
+	      seen.add (stmt);
+
+	      /* If the argument is non-zero, then print all of the states
+		 of the various enodes.  */
+	      tree t_arg = fold (gimple_call_arg (call, 0));
+	      if (TREE_CODE (t_arg) != INTEGER_CST)
+		{
+		  error_at (call->location,
+			    "integer constant required for arg 1");
+		  return;
+		}
+	      int i_arg = TREE_INT_CST_LOW (t_arg);
+	      if (i_arg)
+		{
+		  exploded_node *other_enode;
+		  FOR_EACH_VEC_ELT (enodes, j, other_enode)
+		    {
+		      fprintf (stderr, "%i of %i: EN %i:\n",
+			       j + 1, enodes.length (), other_enode->m_index);
+		      other_enode->dump_succs_and_preds (stderr);
+		      /* Dump state.  */
+		      other_enode->get_state ().dump (m_ext_state, false);
+		    }
+		}
+	    }
+    }
+}
+
+////////////////////////////////////////////////////////////////////////////
+
+/* FIXME: currently this is just debug code for visualizing the callgraph
+   in .dot form, but it might grow into a data structure for planning
+   interprocedural analysis, call summaries, etc.  */
+
+/* FIXME.  */
+class my_callgraph_node;
+class my_callgraph_edge;
+class my_callgraph;
+class my_callgraph_cluster;
+
+/* FIXME.  */
+
+struct my_callgraph_traits
+{
+  typedef my_callgraph_node node_t;
+  typedef my_callgraph_edge edge_t;
+  typedef my_callgraph graph_t;
+  struct dump_args_t
+  {
+    dump_args_t (const exploded_graph *eg) : m_eg (eg) {}
+    const exploded_graph *m_eg;
+  };
+  typedef my_callgraph_cluster cluster_t;
+};
+
+/* Subclass of dnode representing a function within the callgraph.  */
+
+class my_callgraph_node : public dnode<my_callgraph_traits>
+{
+  friend class my_callgraph;
+
+public:
+  my_callgraph_node (function *fun, int index)
+  : m_fun (fun), m_index (index), m_num_supernodes (0), m_num_superedges (0)
+  {
+    gcc_assert (fun);
+  }
+
+  void dump_dot (graphviz_out *gv, const dump_args_t &args) const FINAL OVERRIDE
+  {
+    pretty_printer *pp = gv->get_pp ();
+
+    dump_dot_id (pp);
+    pp_printf (pp, " [shape=%s,style=filled,fillcolor=%s,label=\"",
+	       "record", "lightgrey");
+    pp_left_brace (pp);
+    pp_write_text_to_stream (pp);
+
+    pp_printf (pp, "MCG: %i: %s", m_index, function_name (m_fun));
+    pp_newline (pp);
+
+    pp_printf (pp, "supernodes: %i\n", m_num_supernodes);
+    pp_newline (pp);
+    pp_printf (pp, "superedges: %i\n", m_num_superedges);
+    pp_newline (pp);
+
+    if (args.m_eg)
+      {
+	unsigned i;
+	exploded_node *enode;
+	unsigned num_enodes = 0;
+	FOR_EACH_VEC_ELT (args.m_eg->m_nodes, i, enode)
+	  {
+	    if (enode->get_point ().get_function () == m_fun)
+	      num_enodes++;
+	  }
+	pp_printf (pp, "enodes: %i\n", num_enodes);
+	pp_newline (pp);
+
+	// TODO: also show the per-callstring breakdown
+	const exploded_graph::call_string_data_map_t *per_cs_data
+	  = args.m_eg->get_per_call_string_data ();
+	for (typename exploded_graph::call_string_data_map_t::iterator iter
+	       = per_cs_data->begin ();
+	     iter != per_cs_data->end ();
+	     ++iter)
+	  {
+	    const call_string *cs = (*iter).first;
+	    //per_call_string_data *data = (*iter).second;
+	    num_enodes = 0;
+	    FOR_EACH_VEC_ELT (args.m_eg->m_nodes, i, enode)
+	      {
+		if (enode->get_point ().get_function () == m_fun
+		    && enode->get_point ().m_call_string == *cs)
+		  num_enodes++;
+	      }
+	    if (num_enodes > 0)
+	      {
+		cs->print (pp);
+		pp_printf (pp, ": %i\n", num_enodes);
+	      }
+	  }
+
+	/* Show any summaries.  */
+	per_function_data *data = args.m_eg->get_per_function_data (m_fun);
+	if (data)
+	  {
+	    pp_newline (pp);
+	    pp_printf (pp, "summaries: %i\n", data->m_summaries.length ());
+	  }
+      }
+
+    pp_write_text_as_dot_label_to_stream (pp, /*for_record=*/true);
+    pp_right_brace (pp);
+    pp_string (pp, "\"];\n\n");
+    pp_flush (pp);
+  }
+
+  void dump_dot_id (pretty_printer *pp) const
+  {
+    pp_printf (pp, "mcg_%i", m_index);
+  }
+
+private:
+  function *m_fun;
+  int m_index;
+  int m_num_supernodes;
+  int m_num_superedges;
+};
+
+/* FIXME.  */
+
+class my_callgraph_edge : public dedge<my_callgraph_traits>
+{
+public:
+  my_callgraph_edge (my_callgraph_node *src, my_callgraph_node *dest,
+		     const call_superedge *call_sedge)
+  : dedge (src, dest),
+    m_call_sedge (call_sedge)
+  {}
+
+  void dump_dot (graphviz_out *gv, const dump_args_t &) const
+    FINAL OVERRIDE
+  {
+    pretty_printer *pp = gv->get_pp ();
+
+    const char *style = "\"solid,bold\"";
+    const char *color = "black";
+    int weight = 10;
+    const char *constraint = "true";
+
+    m_src->dump_dot_id (pp);
+    pp_string (pp, " -> ");
+    m_dest->dump_dot_id (pp);
+    pp_printf (pp,
+	       (" [style=%s, color=%s, weight=%d, constraint=%s,"
+		" headlabel=\""),
+	       style, color, weight, constraint);
+    pp_printf (pp, "\"];\n");
+  }
+
+private:
+  const call_superedge * const m_call_sedge;
+};
+
+/* FIXME.  */
+
+class my_callgraph : public digraph<my_callgraph_traits>
+{
+public:
+  my_callgraph (const supergraph &sg);
+
+  my_callgraph_node *get_mcg_node_for_function (function *fun)
+  {
+    return *m_map.get (fun);
+  }
+
+  my_callgraph_node *get_mcg_node_for_snode (supernode *snode)
+  {
+    return get_mcg_node_for_function (snode->m_fun);
+  }
+
+private:
+  const supergraph &m_sg;
+  hash_map<function *, my_callgraph_node *> m_map;
+};
+
+/* FIXME.  */
+
+class my_callgraph_cluster : public cluster<my_callgraph_traits>
+{
+};
+
+/* FIXME.  */
+
+my_callgraph::my_callgraph (const supergraph &sg)
+: m_sg (sg)
+{
+  // within cgraph_edge
+#if 0
+  cgraph_node *caller;
+  cgraph_node *callee;
+#endif
+  // callee->get_fun () gets the function * for a *graph_node
+  /* FIXME:
+     maybe do it using the supergraph; we have the information already.  */
+
+  cgraph_node *node;
+  FOR_EACH_FUNCTION_WITH_GIMPLE_BODY (node)
+    {
+      function *fun = node->get_fun ();
+      my_callgraph_node *mcg_node
+	= new my_callgraph_node (fun, m_nodes.length ());
+      m_map.put (fun, mcg_node);
+      add_node (mcg_node);
+    }
+
+  unsigned i;
+  superedge *sedge;
+  FOR_EACH_VEC_ELT (sg.m_edges, i, sedge)
+    {
+      my_callgraph_node *mcg_src = get_mcg_node_for_snode (sedge->m_src);
+      if (mcg_src->m_fun)
+	get_mcg_node_for_function (mcg_src->m_fun)->m_num_superedges++;
+      if (const call_superedge *call_sedge = sedge->dyn_cast_call_superedge ())
+	{
+	  my_callgraph_node *mcg_dest = get_mcg_node_for_snode (sedge->m_dest);
+	  my_callgraph_edge *mcg_edge
+	    = new my_callgraph_edge (mcg_src, mcg_dest, call_sedge);
+	  add_edge (mcg_edge);
+	}
+    }
+
+  supernode *snode;
+  FOR_EACH_VEC_ELT (sg.m_nodes, i, snode)
+    {
+      if (snode->m_fun)
+	get_mcg_node_for_function (snode->m_fun)->m_num_supernodes++;
+    }
+}
+
+/* FIXME.  */
+
+static void
+dump_callgraph (const supergraph &sg, const char *filename,
+		const exploded_graph *eg)
+{
+  FILE *outf = fopen (filename, "w");
+  if (!outf)
+    return;
+
+  // TODO
+  my_callgraph mcg (sg);
+  mcg.dump_dot (filename, NULL, my_callgraph_traits::dump_args_t (eg));
+
+  fclose (outf);
+}
+
+/* FIXME.  */
+
+static void
+dump_callgraph (const supergraph &sg, const exploded_graph *eg)
+{
+  auto_client_timevar tv ("writing .callgraph.dot");
+  char *filename = concat (dump_base_name, ".callgraph.dot", NULL);
+  dump_callgraph (sg, filename, eg);
+  free (filename);
+}
+
+////////////////////////////////////////////////////////////////////////////
+
+/* Run the analysis "engine".  */
+
+void
+impl_run_checkers (logger *logger)
+{
+  LOG_SCOPE (logger);
+
+  /* Create the supergraph.  */
+  supergraph sg (logger);
+
+  state_purge_map *purge_map = NULL;
+
+  if (flag_analyzer_state_purge)
+    purge_map = new state_purge_map (sg, logger);
+
+  if (flag_dump_analyzer_supergraph)
+    {
+      auto_client_timevar tv ("writing .supergraph.dot");
+      //state_purge_annotator a (purge_map);
+      char *filename = concat (dump_base_name, ".supergraph.dot", NULL);
+      //supergraph::dump_args_t args ((enum supergraph_dot_flags)0, &a);
+      supergraph::dump_args_t args ((enum supergraph_dot_flags)0, NULL);
+      sg.dump_dot (filename, args);
+      free (filename);
+    }
+
+  auto_delete_vec <state_machine> checkers;
+  make_checkers (checkers, logger);
+
+  if (logger)
+    {
+      int i;
+      state_machine *sm;
+      FOR_EACH_VEC_ELT (checkers, i, sm)
+	logger->log ("checkers[%i]: %s", i, sm->get_name ());
+    }
+
+  /* Extrinsic state shared by nodes in the graph.  */
+  const extrinsic_state ext_state (checkers);
+
+  const analysis_plan plan (sg, logger);
+
+  /* The exploded graph.  */
+  exploded_graph eg (sg, logger, ext_state, purge_map, plan,
+		     analyzer_verbosity);
+
+  /* Add entrypoints to the graph for externally-callable functions.  */
+  eg.build_initial_worklist ();
+
+  /* Now process the worklist, exploring the <point, state> graph.  */
+  eg.process_worklist ();
+
+  if (flag_dump_analyzer_exploded_graph)
+    {
+      auto_client_timevar tv ("writing .eg.dot");
+      char *filename
+	= concat (dump_base_name, ".eg.dot", NULL);
+      exploded_graph::dump_args_t args;
+      root_cluster c;
+      eg.dump_dot (filename, &c, args);
+      free (filename);
+    }
+
+  /* Now emit any saved diagnostics.  */
+  eg.get_diagnostic_manager ().emit_saved_diagnostics (eg);
+
+  eg.dump_exploded_nodes ();
+
+  eg.log_stats ();
+
+  if (flag_dump_analyzer_callgraph)
+    dump_callgraph (sg, &eg);
+
+  delete purge_map;
+}
+
+/* External entrypoint to the analysis "engine".
+   Set up any dumps, then call impl_run_checkers.  */
+
+void
+run_checkers ()
+{
+  auto_client_timevar tv ("run_checkers");
+
+  /* Handle -fdump-analyzer and -fdump-analyzer-stderr.  */
+  FILE *dump_fout = NULL;
+  /* Track if we're responsible for closing dump_fout.  */
+  bool owns_dump_fout = false;
+  if (flag_dump_analyzer_stderr)
+    dump_fout = stderr;
+  else if (flag_dump_analyzer)
+    {
+      char *dump_filename = concat (dump_base_name, ".analyzer.txt", NULL);
+      dump_fout = fopen (dump_filename, "w");
+      free (dump_filename);
+      if (dump_fout)
+	owns_dump_fout = true;
+    }
+
+  {
+    log_user the_logger (NULL);
+    if (dump_fout)
+      the_logger.set_logger (new logger (dump_fout, 0, 0,
+					 *global_dc->printer));
+    LOG_SCOPE (the_logger.get_logger ());
+
+    impl_run_checkers (the_logger.get_logger ());
+
+    /* end of lifetime of the_logger (so that dump file is closed after the
+       various dtors run).  */
+  }
+
+  if (owns_dump_fout)
+    fclose (dump_fout);
+}
+
+#if CHECKING_P
+
+namespace selftest {
+
+/* Tests for sm_state_map.  */
+
+static void
+test_sm_state_map ()
+{
+  tree x = build_global_decl ("x", integer_type_node);
+  tree y = build_global_decl ("y", integer_type_node);
+  tree z = build_global_decl ("z", integer_type_node);
+
+  /* Test setting states on svalue_id instances directly.  */
+  {
+    region_model model;
+    svalue_id sid_x = model.get_rvalue (x, NULL);
+    svalue_id sid_y = model.get_rvalue (y, NULL);
+    svalue_id sid_z = model.get_rvalue (z, NULL);
+
+    sm_state_map map;
+    ASSERT_TRUE (map.is_empty_p ());
+    ASSERT_EQ (map.get_state (sid_x), 0);
+
+    map.impl_set_state (sid_x, 42, sid_z);
+    ASSERT_EQ (map.get_state (sid_x), 42);
+    ASSERT_EQ (map.get_origin (sid_x), sid_z);
+    ASSERT_EQ (map.get_state (sid_y), 0);
+    ASSERT_FALSE (map.is_empty_p ());
+
+    map.impl_set_state (sid_y, 0, sid_z);
+    ASSERT_EQ (map.get_state (sid_y), 0);
+
+    map.impl_set_state (sid_x, 0, sid_z);
+    ASSERT_EQ (map.get_state (sid_x), 0);
+    ASSERT_TRUE (map.is_empty_p ());
+  }
+
+  /* Test setting states via equivalence classes.  */
+  {
+    region_model model;
+    svalue_id sid_x = model.get_rvalue (x, NULL);
+    svalue_id sid_y = model.get_rvalue (y, NULL);
+    svalue_id sid_z = model.get_rvalue (z, NULL);
+
+    sm_state_map map;
+    ASSERT_TRUE (map.is_empty_p ());
+    ASSERT_EQ (map.get_state (sid_x), 0);
+    ASSERT_EQ (map.get_state (sid_y), 0);
+
+    model.add_constraint (x, EQ_EXPR, y, NULL);
+
+    /* Setting x to a state should also update y, as they
+       are in the same equivalence class.  */
+    map.set_state (&model, sid_x, 5, sid_z);
+    ASSERT_EQ (map.get_state (sid_x), 5);
+    ASSERT_EQ (map.get_state (sid_y), 5);
+    ASSERT_EQ (map.get_origin (sid_x), sid_z);
+    ASSERT_EQ (map.get_origin (sid_y), sid_z);
+  }
+
+  /* Test equality and hashing.  */
+  {
+    region_model model;
+    //svalue_id sid_x = model.get_rvalue (x, NULL);
+    svalue_id sid_y = model.get_rvalue (y, NULL);
+    svalue_id sid_z = model.get_rvalue (z, NULL);
+
+    sm_state_map map0;
+    sm_state_map map1;
+    sm_state_map map2;
+
+    ASSERT_EQ (map0.hash (), map1.hash ());
+    ASSERT_EQ (map0, map1);
+
+    map1.impl_set_state (sid_y, 5, sid_z);
+    ASSERT_NE (map0.hash (), map1.hash ());
+    ASSERT_NE (map0, map1);
+
+    /* Make the same change to map2.  */
+    map2.impl_set_state (sid_y, 5, sid_z);
+    ASSERT_EQ (map1.hash (), map2.hash ());
+    ASSERT_EQ (map1, map2);
+  }
+
+  /* Test sm_state_map::remap_svalue_ids.  */
+  {
+    sm_state_map map;
+    svalue_id sid_0 = svalue_id::from_int (0);
+    svalue_id sid_1 = svalue_id::from_int (1);
+    svalue_id sid_2 = svalue_id::from_int (2);
+
+    map.impl_set_state (sid_0, 42, sid_2);
+    ASSERT_EQ (map.get_state (sid_0), 42);
+    ASSERT_EQ (map.get_origin (sid_0), sid_2);
+    ASSERT_EQ (map.get_state (sid_1), 0);
+    ASSERT_EQ (map.get_state (sid_2), 0);
+
+    /* Apply a remapping to the IDs.  */
+    svalue_id_map remapping (3);
+    remapping.put (sid_0, sid_1);
+    remapping.put (sid_1, sid_2);
+    remapping.put (sid_2, sid_0);
+    map.remap_svalue_ids (remapping);
+
+    /* Verify that the IDs have been remapped.  */
+    ASSERT_EQ (map.get_state (sid_1), 42);
+    ASSERT_EQ (map.get_origin (sid_1), sid_0);
+    ASSERT_EQ (map.get_state (sid_2), 0);
+    ASSERT_EQ (map.get_state (sid_0), 0);
+  }
+
+  // TODO: coverage for purging
+}
+
+/* Verify that program_point::operator== works as expected.  */
+
+static void
+test_program_point_equality ()
+{
+  const supernode *snode = NULL;
+
+  const call_string cs;
+
+  program_point a = program_point (snode, NULL, 0,
+				   PK_BEFORE_SUPERNODE,
+				   cs);
+
+  program_point b = program_point (snode, NULL, 0,
+				   PK_BEFORE_SUPERNODE,
+				   cs);
+  ASSERT_EQ (a, b);
+  // TODO: verify with non-empty callstrings, with different edges
+}
+
+/* Verify that program_point::cmp_within_supernode works as expected.  */
+
+static void
+test_program_point_ordering ()
+{
+  const supernode *snode = NULL;
+  const call_string call_string;
+
+  /* Populate an array with various points within the same
+     snode, in order.  */
+  auto_delete_vec<program_point> points;
+  points.safe_push (new program_point (snode, NULL, 0,
+				       PK_BEFORE_SUPERNODE,
+				       call_string));
+  points.safe_push (new program_point (snode, NULL, 0,
+				       PK_BEFORE_STMT,
+				       call_string));
+  points.safe_push (new program_point (snode, NULL, 1,
+				       PK_BEFORE_STMT,
+				       call_string));
+  points.safe_push (new program_point (snode, NULL, 0,
+				       PK_AFTER_SUPERNODE,
+				       call_string));
+  /* Check all pairs.  */
+  unsigned i;
+  program_point *point_a;
+  FOR_EACH_VEC_ELT (points, i, point_a)
+    {
+      unsigned j;
+      program_point *point_b;
+      FOR_EACH_VEC_ELT (points, j, point_b)
+	{
+	  int cmp = program_point::cmp_within_supernode (*point_a, *point_b);
+	  if (i == j)
+	    ASSERT_EQ (cmp, 0);
+	  if (i < j)
+	    ASSERT_TRUE (cmp < 0);
+	  if (i > j)
+	    ASSERT_TRUE (cmp > 0);
+	}
+    }
+}
+
+/* Verify that program_states with identical sm-state can be merged,
+   and that the merged program_state preserves the sm-state.  */
+
+static void
+test_program_state_merging ()
+{
+  /* Create a program_state for a global ptr "p" that has
+     malloc sm-state, pointing to a region on the heap.  */
+  tree p = build_global_decl ("p", ptr_type_node);
+
+  auto_delete_vec <state_machine> checkers;
+  checkers.safe_push (make_malloc_state_machine (NULL));
+  extrinsic_state ext_state (checkers);
+
+  program_state s0 (ext_state);
+  impl_region_model_context ctxt (&s0, ext_state);
+
+  region_model *model0 = s0.m_region_model;
+  region_id new_rid = model0->add_new_malloc_region ();
+  svalue_id ptr_sid
+      = model0->get_or_create_ptr_svalue (ptr_type_node, new_rid);
+  model0->set_value (model0->get_lvalue (p, &ctxt),
+		     ptr_sid);
+  sm_state_map *smap = s0.m_checker_states[0];
+  const state_machine::state_t TEST_STATE = 3;
+  smap->impl_set_state (ptr_sid, TEST_STATE, svalue_id::null ());
+  ASSERT_EQ (smap->get_state (ptr_sid), TEST_STATE);
+
+  model0->canonicalize (&ctxt);
+
+  /* Verify that canonicalization preserves sm-state.  */
+  ASSERT_EQ (smap->get_state (model0->get_rvalue (p, NULL)), TEST_STATE);
+
+  /* Make a copy of the program_state.  */
+  program_state s1 (s0);
+  ASSERT_EQ (s0, s1);
+
+  /* We have two identical states with "p" pointing to a heap region
+     with the given sm-state.
+     They ought to be mergeable, preserving the sm-state.  */
+  program_state merged (ext_state);
+  ASSERT_TRUE (s0.can_merge_with_p (s1, ext_state, &merged));
+  merged.validate (ext_state);
+
+  /* Verify that the merged state has the sm-state for "p".  */
+  region_model *merged_model = merged.m_region_model;
+  sm_state_map *merged_smap = merged.m_checker_states[0];
+  ASSERT_EQ (merged_smap->get_state (merged_model->get_rvalue (p, NULL)),
+	     TEST_STATE);
+
+  /* Try canonicalizing.  */
+  impl_region_model_context merged_ctxt (&merged, ext_state);
+  merged.m_region_model->canonicalize (&merged_ctxt);
+  merged.validate (ext_state);
+
+  /* Verify that the merged state still has the sm-state for "p".  */
+  ASSERT_EQ (merged_smap->get_state (merged_model->get_rvalue (p, NULL)),
+	     TEST_STATE);
+
+  /* After canonicalization, we ought to have equality with the inputs.  */
+  ASSERT_EQ (s0, merged);
+}
+
+/* FIXME.  */
+
+static void
+test_assignments ()
+{
+  /* Verify X = Y, then Y = Z.
+     Should propagate states accordingly.  */
+}
+
+/* Run all of the selftests within this file.  */
+
+void
+analyzer_engine_cc_tests ()
+{
+  test_sm_state_map ();
+  test_program_point_equality ();
+  test_program_point_ordering ();
+  test_program_state_merging ();
+  test_assignments ();
+}
+
+} // namespace selftest
+
+#endif /* CHECKING_P */
diff --git a/gcc/analyzer/engine.h b/gcc/analyzer/engine.h
new file mode 100644
index 0000000..b9b7e0b
--- /dev/null
+++ b/gcc/analyzer/engine.h
@@ -0,0 +1,26 @@
+/* The analysis "engine".
+   Copyright (C) 2019 Free Software Foundation, Inc.
+   Contributed by David Malcolm <dmalcolm@redhat.com>.
+
+This file is part of GCC.
+
+GCC 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, or (at your option)
+any later version.
+
+GCC 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 GCC; see the file COPYING3.  If not see
+<http://www.gnu.org/licenses/>.  */
+
+#ifndef GCC_ANALYZER_ENGINE_H
+#define GCC_ANALYZER_ENGINE_H
+
+extern void run_checkers ();
+
+#endif /* GCC_ANALYZER_ENGINE_H */
-- 
1.8.5.3

