[pushed:,r17-359] analyzer: add data flow events

Message ID 20260506140844.2746885-1-dmalcolm@redhat.com
State New
Headers
Series [pushed:,r17-359] analyzer: add data flow events |

Commit Message

David Malcolm May 6, 2026, 2:08 p.m. UTC
  Analyzer warnings relating to problematic values (e.g. by division
by zero) can be difficult for the user to understand.

Consider this example
(with -fanalyzer -fno-analyzer-state-merge -fno-analyzer-state-purge):

int
get_zero (void)
{
  return 0;
}

struct foo { int x; int y; };

void
init_foo (struct foo *f, int x, int y)
{
  f->x = x;
  f->y = y;
}

int
do_divide (struct foo *f)
{
  return f->x / f->y;
}

int
test (int flag, int flag_2, int flag_3)
{
  struct foo f;
  int a = 42;
  int b = get_zero ();
  init_foo (&f, a, b);
  return do_divide (&f);
}

Before this patch, we emit:

demo.c: In function ‘do_divide’:
demo.c:24:15: warning: division by zero [-Wanalyzer-div-by-zero]
   24 |   return f->x / f->y;
      |          ~~~~~^~~~~~
  ‘test’: events 1-2
    │
    │   28 | test (int flag, int flag_2, int flag_3)
    │      | ^~~~
    │      | |
    │      | (1) entry to ‘test’
    │......
    │   34 |   return do_divide (&f);
    │      |          ~~~~~~~~~~~~~~
    │      |          |
    │      |          (2) calling ‘do_divide’ from ‘test’
    │
    └──> ‘do_divide’: events 3-4
           │
           │   22 | do_divide (struct foo *f)
           │      | ^~~~~~~~~
           │      | |
           │      | (3) entry to ‘do_divide’
           │   23 | {
           │   24 |   return f->x / f->y;
           │      |          ~~~~~~~~~~~
           │      |               |
           │      |               (4) ⚠️  division by zero
           │

which doesn't convey where the zero value came from.

This patch adds various wording and new events to tracking where the
problematic value comes from.  diagnostic_manager::annotate_exploded_path walks
backwards from the final enode, building a chain of instances of a new
state_transition class hierarchy.  These state_transition instances are
associated with checker_events where possible (e.g. at function entry),
otherwise, new state_transition_events are added for them, highlighting
e.g. where the pertinent zero value is passed as a parameter.

With the patch, we emit:

demo.c: In function ‘do_divide’:
demo.c:24:15: warning: division by zero [-Wanalyzer-div-by-zero]
   24 |   return f->x / f->y;
      |          ~~~~~^~~~~~
  ‘test’: events 1-2
    │
    │   28 | test (int flag, int flag_2, int flag_3)
    │      | ^~~~
    │      | |
    │      | (1) entry to ‘test’
    │......
    │   32 |   int b = get_zero ();
    │      |           ~~~~~~~~~~~
    │      |           |
    │      |           (2) calling ‘get_zero’ from ‘test’
    │
    └──> ‘get_zero’: events 3-4
           │
           │    7 | get_zero (void)
           │      | ^~~~~~~~
           │      | |
           │      | (3) entry to ‘get_zero’
           │    8 | {
           │    9 |   return 0;
           │      |          ~
           │      |          |
           │      |          (4) zero value originates here
           │
    <──────┘
    │
  ‘test’: events 5-6
    │
    │   32 |   int b = get_zero ();
    │      |           ^~~~~~~~~~~
    │      |           |
    │      |           (5) returning zero from (4) from ‘get_zero’ here
    │   33 |   init_foo (&f, a, b);
    │      |   ~~~~~~~~~~~~~~~~~~~
    │      |   |
    │      |   (6) passing zero from (5) from ‘test’ to ‘init_foo’ via parameter 3
    │
    └──> ‘init_foo’: events 7-8
           │
           │   15 | init_foo (struct foo *f, int x, int y)
           │      |                                 ~~~~^
           │      |                                     |
           │      |                                     (7) entry to ‘init_foo’ with zero from (5) for ‘y’
           │......
           │   18 |   f->y = y;
           │      |   ~~~~~~~~
           │      |        |
           │      |        (8) copying zero value from (7) from ‘y’ to ‘*f.y’
           │
    <──────┘
    │
  ‘test’: events 9-10
    │
    │   33 |   init_foo (&f, a, b);
    │      |   ^~~~~~~~~~~~~~~~~~~
    │      |   |
    │      |   (9) returning to ‘test’ from ‘init_foo’
    │   34 |   return do_divide (&f);
    │      |          ~~~~~~~~~~~~~~
    │      |          |
    │      |          (10) calling ‘do_divide’ from ‘test’
    │
    └──> ‘do_divide’: events 11-13
           │
           │   22 | do_divide (struct foo *f)
           │      | ^~~~~~~~~
           │      | |
           │      | (11) entry to ‘do_divide’
           │   23 | {
           │   24 |   return f->x / f->y;
           │      |          ~~~~~~~~~~~
           │      |               |  |
           │      |               |  (12) using zero value from (8) from ‘*f.y’
           │      |               (13) ⚠️  division by zero
           │

Note:
* the new "(4) zero value originates here" state transition event,
* the precision-of-wording for the return event at (5),
* the precision-of-wording for the call event at (6),
* the precision-of-wording for the function entry event at (7), and the
  underlining of the pertinent parameter
* the above call/return events no longer get optimized away, due to...
* the new "(8) copying zero value from (7) from ‘y’ to ‘*f.y’" state
  transition event
* the new "(12) using zero value from (8) from ‘*f.y’" state transition
  event

Successfully bootstrapped & regrtested on aarch64-unknown-linux-gnu
Pushed to trunk as r17-359-g9a231dbb68ab3d.

gcc/ChangeLog:
	* Makefile.in (ANALYZER_OBJS): Add analyzer/state-transition.o.
	* digraph.cc (test_path::append_edge): New.
	(test_path::reverse): New.
	* shortest-paths.h (get_shortest_path): Use append_edge and
	reverse.
	* tree-diagnostic.h
	(tree_dump_pretty_printer::~tree_dump_pretty_printer): Only flush
	when the buffer's stream is non-null.

gcc/analyzer/ChangeLog:
	* analyzer.cc (printable_expr_p): New.
	* call-info.cc: Include "analyzer/state-transition.h".
	(call_info::add_events_to_path): Add state_transition param.
	* call-info.h (call_info::add_events_to_path): Likewise.
	* callsite-expr.h: New file, based on material from supergraph.h.
	(class callsite_expr_element): New.
	* checker-event.cc: Include "analyzer/callsite-expr.h" and
	"analyzer/state-transition.h".
	(event_kind_to_string): Handle event_kind::state_transition.
	(state_transition_event::print_desc): New.
	(state_transition_event::prepare_for_emission): New.
	(function_entry_event::function_entry_event): Drop.
	(function_entry_event::print_desc): Use any m_state_trans.
	(function_entry_event::prepare_for_emission): New.
	(call_event::call_event): Add "state_trans" param and store it in
	m_state_trans.
	(call_event::print_desc): Use m_state_trans if present to call
	describe_call_with_state, adding a fallback wording for
	pending_diagnostic subclasses that don't implement it.
	(call_event::prepare_for_emission): New, storing event id into
	state_transition.
	(return_event::return_event): Add "state_trans" param and store it
	in m_state_trans.
	(return_event::print_desc): Use m_state_trans if present to call
	describe_return_of_state.
	(return_event::prepare_for_emission): New, storing event id into
	state_transition.
	* checker-event.h (enum class event_kind): Add state_transition.
	(class state_transition_event): New.
	(function_entry_event::function_entry_event): Add "state_trans"
	param and store it in m_state_trans.  Drop 2nd ctor.
	(function_entry_event::prepare_for_emission): New decl.
	(function_entry_event::get_state_transition_at_call): New.
	(function_entry_event::m_state_trans): New.
	(call_event::call_event): Add "state_trans" param.
	(call_event::prepare_for_emission): New decl.
	(call_event::get_state_transition_at_call): New.
	(call_event::m_state_trans): New.
	(return_event::return_event): Add "state_trans" param.
	(return_event::prepare_for_emission): New decl.
	(return_event::m_state_trans): New.
	* common.h: Define INCLUDE_ALGORITHM.
	(class state_transition): New forward decl
	(class state_transition_at_call): New forward decl
	(class state_transition_at_return): New forward decl
	(printable_expr_p): New decl.
	(struct diagnostic_state): New.
	(struct rewind_context): New forward decl.
	(custom_edge_info::add_events_to_path): Add "state_trans" param.
	(try_to_rewind_data_flow): New vfunc.
	* diagnostic-manager.cc (compatible_epath_p): Update for change
	from m_edges to m_elements.
	(diagnostic_manager::emit_saved_diagnostic):  Make "epath"
	non-const.  Call annotate_exploded_path on it and log before and
	after.  Update for new param to add_events_for_eedge.
	(class epath_rewind_context): New.
	(diagnostic_manager::annotate_exploded_path): New.
	(diagnostic_manager::build_emission_path): Update for change from
	m_edges to m_elements.  Pass any state transition to
	add_events_for_eedge.
	(diagnostic_manager::add_events_for_eedge): Add "state_trans"
	parameter.  Pass it when creating events, and if we don't create
	an event referencing it, make a state_transition_event for it.
	(diagnostic_manager::prune_for_sm_diagnostic): Handl
	event_kind::state_transition.
	* diagnostic-manager.h (saved_diagnostic::get_best_epath): Drop
	"const" from return value.
	(diagnostic_manager::annotate_exploded_path): New decl.
	(diagnostic_manager::add_events_for_eedge): Add "state_trans"
	param.
	* engine.cc: Include "pretty-print-markup.h".
	(leak_ploc_fixer_for_epath::fixup_for_epath): Update for change
	from m_edges to m_elements.
	(throw_custom_edge::add_events_to_path): Add state_transition
	param.
	(unwind_custom_edge::add_events_to_path): Likewise.
	(interprocedural_call::print): Use get_gcall.
	(interprocedural_call::update_model): Likewise.
	(interprocedural_call::add_events_to_path): Likewise.  Pass
	state_transition if of correct subclass.
	(interprocedural_call::try_to_rewind_data_flow): New.
	(interprocedural_call::get_gcall): New.
	(interprocedural_return::add_events_to_path): Add state_transition
	param and pass if of correct subclass.
	(interprocedural_return::try_to_rewind_data_flow): New.
	(tainted_args_function_info::add_events_to_path): Add
	state_transition param.
	(tainted_args_call_info::add_events_to_path): Likewise.
	* event-loc-info.h (event_loc_info_for_function_entry): New decl.
	* exploded-graph.h (interprocedural_call::interprocedural_call):
	Pass and store call_and_return_op rather than gcall.
	(interprocedural_call::add_events_to_path): Add state_trans param.
	(interprocedural_call::try_to_rewind_data_flow): New.
	(interprocedural_call::m_call_stmt): Replace with...
	(interprocedural_call::m_op): ...this.
	(interprocedural_return::add_events_to_path): Add state_trans
	param.
	(interprocedural_return::try_to_rewind_data_flow): New.
	(rewind_info_t::add_events_to_path): New.
	* exploded-path.cc (exploded_path::exploded_path): Drop explicit
	copy ctor.
	(diagnostic_state::dump_to_pp): New.
	(diagnostic_state::dump): New.
	(exploded_path::find_stmt_backwards): Update for change from
	m_edges to m_elements.
	(exploded_path::get_final_enode): Likewise.
	(exploded_path::feasible_p): Likewise.
	(exploded_path::dump_to_pp): Likewise.  Dump state transitions.
	(exploded_path::maybe_log): New.
	(exploded_path::reverse): New.
	* exploded-path.h: Include "analyzer/checker-event.h" and
	"analyzer/state-transition.h".
	(struct exploded_path::element_t): New.
	(exploded_path::exploded_path): Replace ctors with "= default".
	(exploded_path::length): Reimplement.
	(exploded_path::maybe_log): New decl.
	(exploded_path::append_edge): New.
	(exploded_path::reverse): New.
	(exploded_path::m_edges): Replace with...
	(exploded_path::m_elements): ...this, using std::vector rather
	than auto_vec.
	* feasible-graph.cc (feasible_graph::make_epath): Update for
	change from m_edges to m_elements.
	* infinite-recursion.cc
	(infinite_recursion_diagnostic::add_function_entry_event): Add
	"state_trans" param and pass it to base class call.
	(recursive_function_entry_event::recursive_function_entry_event):
	Pass nullptr for state transition.
	* ops.cc: Include "analyzer/callsite-expr.h" and
	"analyzer/state-transition.h".
	(event_loc_info_for_function_entry): New.
	(rewind_context::on_data_origin): New.
	(rewind_context::on_data_flow): New.
	(gassign_op::try_to_rewind_data_flow): New.
	(greturn_op::try_to_rewind_data_flow): New.
	(call_and_return_op::execute): Update for change to
	interprocedural_call.
	(call_and_return_op::try_to_rewind_data_flow): New.
	(phis_for_edge_op::try_to_rewind_data_flow): New.
	* ops.h (struct rewind_context): New.
	(operation::try_to_rewind_data_flow): New vfunc.
	(gassign_op::try_to_rewind_data_flow): New.
	(predict_op::try_to_rewind_data_flow): New.
	(greturn_op::try_to_rewind_data_flow): New.
	(call_and_return_op::try_to_rewind_data_flow): New.
	(control_flow_op::try_to_rewind_data_flow): New.
	(phis_for_edge_op::try_to_rewind_data_flow): New.
	* pending-diagnostic.cc (interesting_t::add_read_region): New.
	(interesting_t::dump_to_pp): Dump m_read_regions.
	(pending_diagnostic::add_function_entry_event): Add "state_trans"
	param and pass it to function_entry_event.  Call
	event_loc_info_for_function_entry.
	(pending_diagnostic::add_call_event): Add "state_trans" param and
	pass it to call_event.
	* pending-diagnostic.h: Include "analyzer/state-transition.h".
	(struct interesting_t): Update leading comment.
	(interesting_t::add_read_region): New.
	(interesting_t::m_read_regions): New.
	(struct origin_of_state): New.
	(call_with_state::call_with_state): Add state_trans param and use
	it to initialize m_state_trans and m_src_event_id.
	(call_with_state::m_state_trans): New field.
	(call_with_state::m_src_event_id): New field.
	(return_of_state::return_of_state): Add state_trans param and use
	it to initialize m_state_trans and m_src_event_id.
	(return_of_state::m_state_trans): New field.
	(return_of_state::m_src_event_id): New field.
	(struct copy_of_state): New.
	(struct use_of_state): New.
	(pending_diagnostic::describe_origin_of_state): New vfunc.
	(pending_diagnostic::describe_copy_of_state): New vfunc.
	(pending_diagnostic::describe_use_of_state): New vfunc.
	(pending_diagnostic::add_function_entry_event): Add "state_trans"
	param.
	(pending_diagnostic::add_call_event): Likewise.
	* poisoned-value-diagnostic.cc
	(poisoned_value_diagnostic::mark_interesting_stuff): Add call to
	add_read_region.
	* region-model.cc: Include "analyzer/state-transition.h".
	(callsite_expr::maybe_get_param_location): New.
	(callsite_expr::get_param_tree): New.
	(div_by_zero_diagnostic::div_by_zero_diagnostic): Add
	"divisor_reg" param and use it to initialize m_divisor_reg.
	(div_by_zero_diagnostic::mark_interesting_stuff): New.
	(div_by_zero_diagnostic::add_function_entry_event): New.
	(div_by_zero_diagnostic::describe_origin_of_state): New.
	(div_by_zero_diagnostic::describe_call_with_state): New.
	(div_by_zero_diagnostic::describe_return_of_state): New.
	(div_by_zero_diagnostic::describe_copy_of_state): New.
	(div_by_zero_diagnostic::describe_use_of_state): New.
	(div_by_zero_diagnostic::m_divisor_reg): New field.
	(region_model::get_gassign_result): Pass region to calls to
	make_shift_count_negative_diagnostic,
	make_shift_count_overflow_diagnostic, and
	div_by_zero_diagnostic.
	(exception_thrown_from_unrecognized_call::add_events_to_path): Add
	state_transition param.
	* region-model.h (make_shift_count_negative_diagnostic): Add
	"src_region" param.
	(make_shift_count_overflow_diagnostic): Likewise.
	* region.h: Include "analyzer/store.h" and
	"text-art/tree-widget.h".
	* setjmp-longjmp.cc (rewind_info_t::add_events_to_path): Add
	state_transition param.
	* shift-diagnostics.cc
	(shift_count_negative_diagnostic::shift_count_negative_diagnostic):
	Add src_region param and use it to initialize m_src_region.
	(shift_count_negative_diagnostic::mark_interesting_stuff): New.
	(shift_count_negative_diagnostic::m_src_region): New.
	(make_shift_count_negative_diagnostic): Add src_region param.
	(shift_count_overflow_diagnostic::shift_count_overflow_diagnostic):
	Add src_region param and use it to initialize m_src_region.
	(shift_count_overflow_diagnostic::mark_interesting_stuff): New.
	(shift_count_overflow_diagnostic::m_src_region): New field.
	(make_shift_count_overflow_diagnostic): Add src_region param.
	* sm-signal.cc (signal_delivery_edge_info_t::add_events_to_path):
	Add state_transition param.
	* state-transition.cc: New file.
	* state-transition.h: New file.
	* supergraph.h (class callsite_expr): Move to callsite-expr.h.
	* varargs.cc (va_arg_diagnostic::add_call_event): Add state_trans
	param.

gcc/testsuite/ChangeLog:
	* c-c++-common/analyzer/divide-by-zero-1.c: Add expected message
	for origin of zero value.
	* c-c++-common/analyzer/divide-by-zero-2.c: New test.
	* c-c++-common/analyzer/divide-by-zero-3.c: New test.
	* c-c++-common/analyzer/invalid-shift-1.c: Add expected messages
	about call to op3 and about pertinent param of op3.
	* c-c++-common/analyzer/invalid-shift-2.c: New test.
	* c-c++-common/analyzer/invalid-shift-3.c: New test.
	* c-c++-common/analyzer/invalid-shift-4.c: New test.
	* g++.dg/analyzer/divide-by-zero-7.C: New test.
	* gcc.dg/analyzer/divide-by-zero-4.c: New test.
	* gcc.dg/analyzer/divide-by-zero-5.c: New test.
	* gcc.dg/analyzer/divide-by-zero-6.c: New test.
	* gcc.dg/analyzer/divide-by-zero-float.c: Add expected message
	for origin of zero value.
---
 gcc/Makefile.in                               |   1 +
 gcc/analyzer/analyzer.cc                      |  10 +
 gcc/analyzer/call-info.cc                     |   8 +-
 gcc/analyzer/call-info.h                      |   3 +-
 gcc/analyzer/callsite-expr.h                  | 104 ++++++++
 gcc/analyzer/checker-event.cc                 | 242 +++++++++++++++---
 gcc/analyzer/checker-event.h                  |  66 ++++-
 gcc/analyzer/common.h                         |  52 +++-
 gcc/analyzer/diagnostic-manager.cc            | 217 +++++++++++++++-
 gcc/analyzer/diagnostic-manager.h             |   9 +-
 gcc/analyzer/engine.cc                        | 118 +++++++--
 gcc/analyzer/event-loc-info.h                 |   4 +
 gcc/analyzer/exploded-graph.h                 |  23 +-
 gcc/analyzer/exploded-path.cc                 | 135 +++++++---
 gcc/analyzer/exploded-path.h                  |  58 ++++-
 gcc/analyzer/feasible-graph.cc                |   4 +-
 gcc/analyzer/infinite-recursion.cc            |  10 +-
 gcc/analyzer/ops.cc                           | 158 +++++++++++-
 gcc/analyzer/ops.h                            |  60 +++++
 gcc/analyzer/pending-diagnostic.cc            |  36 ++-
 gcc/analyzer/pending-diagnostic.h             | 129 +++++++++-
 gcc/analyzer/poisoned-value-diagnostic.cc     |   5 +-
 gcc/analyzer/region-model.cc                  | 215 +++++++++++++++-
 gcc/analyzer/region-model.h                   |   6 +-
 gcc/analyzer/region.h                         |   3 +-
 gcc/analyzer/setjmp-longjmp.cc                |   3 +-
 gcc/analyzer/shift-diagnostics.cc             |  36 ++-
 gcc/analyzer/sm-signal.cc                     |   3 +-
 gcc/analyzer/state-transition.cc              | 184 +++++++++++++
 gcc/analyzer/state-transition.h               | 191 ++++++++++++++
 gcc/analyzer/supergraph.h                     |  34 ---
 gcc/analyzer/varargs.cc                       |   8 +-
 gcc/digraph.cc                                |   3 +
 gcc/shortest-paths.h                          |   4 +-
 .../c-c++-common/analyzer/divide-by-zero-1.c  |   2 +-
 .../c-c++-common/analyzer/divide-by-zero-2.c  |  14 +
 .../c-c++-common/analyzer/divide-by-zero-3.c  |  19 ++
 .../c-c++-common/analyzer/invalid-shift-1.c   |   6 +-
 .../c-c++-common/analyzer/invalid-shift-2.c   |  11 +
 .../c-c++-common/analyzer/invalid-shift-3.c   |  12 +
 .../c-c++-common/analyzer/invalid-shift-4.c   |  12 +
 .../g++.dg/analyzer/divide-by-zero-7.C        |  28 ++
 .../gcc.dg/analyzer/divide-by-zero-4.c        |  39 +++
 .../gcc.dg/analyzer/divide-by-zero-5.c        |  42 +++
 .../gcc.dg/analyzer/divide-by-zero-6.c        |  27 ++
 .../gcc.dg/analyzer/divide-by-zero-float.c    |   2 +-
 gcc/tree-diagnostic.h                         |   3 +-
 47 files changed, 2153 insertions(+), 206 deletions(-)
 create mode 100644 gcc/analyzer/callsite-expr.h
 create mode 100644 gcc/analyzer/state-transition.cc
 create mode 100644 gcc/analyzer/state-transition.h
 create mode 100644 gcc/testsuite/c-c++-common/analyzer/divide-by-zero-2.c
 create mode 100644 gcc/testsuite/c-c++-common/analyzer/divide-by-zero-3.c
 create mode 100644 gcc/testsuite/c-c++-common/analyzer/invalid-shift-2.c
 create mode 100644 gcc/testsuite/c-c++-common/analyzer/invalid-shift-3.c
 create mode 100644 gcc/testsuite/c-c++-common/analyzer/invalid-shift-4.c
 create mode 100644 gcc/testsuite/g++.dg/analyzer/divide-by-zero-7.C
 create mode 100644 gcc/testsuite/gcc.dg/analyzer/divide-by-zero-4.c
 create mode 100644 gcc/testsuite/gcc.dg/analyzer/divide-by-zero-5.c
 create mode 100644 gcc/testsuite/gcc.dg/analyzer/divide-by-zero-6.c
  

Patch

diff --git a/gcc/Makefile.in b/gcc/Makefile.in
index 5e656c3c25b..0ff15046f56 100644
--- a/gcc/Makefile.in
+++ b/gcc/Makefile.in
@@ -1383,6 +1383,7 @@  ANALYZER_OBJS = \
 	analyzer/sm-signal.o \
 	analyzer/sm-taint.o \
 	analyzer/state-purge.o \
+	analyzer/state-transition.o \
 	analyzer/store.o \
 	analyzer/supergraph.o \
 	analyzer/supergraph-fixup-locations.o \
diff --git a/gcc/analyzer/analyzer.cc b/gcc/analyzer/analyzer.cc
index cf9d822629d..15c0c786a06 100644
--- a/gcc/analyzer/analyzer.cc
+++ b/gcc/analyzer/analyzer.cc
@@ -29,6 +29,16 @@  along with GCC; see the file COPYING3.  If not see
 
 namespace ana {
 
+bool
+printable_expr_p (const_tree expr)
+{
+  if (TREE_CODE (expr) == SSA_NAME
+      && !SSA_NAME_VAR (expr))
+    return false;
+
+  return true;
+}
+
 /* Workaround for missing location information for some stmts,
    which ultimately should be solved by fixing the frontends
    to provide the locations (TODO).  */
diff --git a/gcc/analyzer/call-info.cc b/gcc/analyzer/call-info.cc
index 10022b43f34..b5b3fb5a53e 100644
--- a/gcc/analyzer/call-info.cc
+++ b/gcc/analyzer/call-info.cc
@@ -43,6 +43,7 @@  along with GCC; see the file COPYING3.  If not see
 #include "analyzer/exploded-graph.h"
 #include "analyzer/call-details.h"
 #include "analyzer/call-info.h"
+#include "analyzer/state-transition.h"
 
 #if ENABLE_ANALYZER
 
@@ -96,7 +97,8 @@  call_info::print (pretty_printer *pp) const
 void
 call_info::add_events_to_path (checker_path *emission_path,
 			       const exploded_edge &eedge,
-			       pending_diagnostic &) const
+			       pending_diagnostic &,
+			       const state_transition *) const
 {
   class call_event : public custom_event
   {
@@ -121,6 +123,10 @@  call_info::add_events_to_path (checker_path *emission_path,
   tree caller_fndecl = src_point.get_fndecl ();
   const int stack_depth = src_point.get_stack_depth ();
 
+  /* TODO: we don't yet make use of the state_transition (if any), as
+     doing so presumably requires a combinatorial explosion of
+     known functions vs pending diagnostics.  */
+
   emission_path->add_event
     (std::make_unique<call_event> (event_loc_info (get_call_stmt ().location,
 						   caller_fndecl,
diff --git a/gcc/analyzer/call-info.h b/gcc/analyzer/call-info.h
index 0a18e4b04f8..d25c51d0ca4 100644
--- a/gcc/analyzer/call-info.h
+++ b/gcc/analyzer/call-info.h
@@ -33,7 +33,8 @@  public:
   void print (pretty_printer *pp) const override;
   void add_events_to_path (checker_path *emission_path,
 			   const exploded_edge &eedge,
-			   pending_diagnostic &pd) const override;
+			   pending_diagnostic &pd,
+			   const state_transition *state_trans) const override;
 
   const gcall &get_call_stmt () const { return m_call_stmt; }
   tree get_fndecl () const { return m_fndecl; }
diff --git a/gcc/analyzer/callsite-expr.h b/gcc/analyzer/callsite-expr.h
new file mode 100644
index 00000000000..7c7e2d3897f
--- /dev/null
+++ b/gcc/analyzer/callsite-expr.h
@@ -0,0 +1,104 @@ 
+/* User-facing descriptions of expressions at call sites.
+   Copyright (C) 2019-2026 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_CALLSITE_EXPR_H
+#define GCC_ANALYZER_CALLSITE_EXPR_H
+
+#include "pretty-print-markup.h"
+
+namespace ana {
+
+/* An ID representing an expression at a callsite:
+   either a parameter index, or the return value (or unknown).  */
+
+class callsite_expr
+{
+ public:
+  callsite_expr () : m_val (-1) {}
+
+  static callsite_expr from_zero_based_param (int idx)
+  {
+    return callsite_expr (idx + 1);
+  }
+
+  static callsite_expr from_return_value ()
+  {
+    return callsite_expr (0);
+  }
+
+  bool param_p () const
+  {
+    return m_val > 0;
+  }
+
+  /* Get 1-based param number.  */
+  int param_num () const
+  {
+    gcc_assert (param_p ());
+    return m_val;
+  }
+
+  tree get_param_tree (tree fndecl) const;
+
+  bool
+  maybe_get_param_location (tree fndecl,
+			    location_t *out_loc) const;
+
+  bool return_value_p () const
+  {
+    return m_val == 0;
+  }
+
+ private:
+  callsite_expr (int val) : m_val (val) {}
+
+  int m_val; /* 1-based parm, 0 for return value, or -1 for "unknown".  */
+};
+
+class callsite_expr_element : public pp_element
+{
+public:
+  callsite_expr_element (callsite_expr expr) : m_expr (expr) {}
+
+  void
+  add_to_phase_2 (pp_markup::context &ctxt) final override
+  {
+
+    if (m_expr.return_value_p ())
+      pp_string (&ctxt.m_pp, "return value");
+    else if (m_expr.param_p ())
+      {
+	/* We can't call pp_printf directly on ctxt.m_pp from within
+	   formatting.  As a workaround, work with a clone of the pp.  */
+	std::unique_ptr<pretty_printer> pp (ctxt.m_pp.clone ());
+	pp_printf (pp.get (), "parameter %i", m_expr.param_num ());
+	pp_string (&ctxt.m_pp, pp_formatted_text (pp.get ()));
+      }
+    else
+      pp_string (&ctxt.m_pp, "unknown");
+  }
+
+private:
+  callsite_expr m_expr;
+};
+
+} // namespace ana
+
+#endif /* GCC_ANALYZER_CALLSITE_EXPR_H */
diff --git a/gcc/analyzer/checker-event.cc b/gcc/analyzer/checker-event.cc
index d1e9b4429f9..7e79727f5e6 100644
--- a/gcc/analyzer/checker-event.cc
+++ b/gcc/analyzer/checker-event.cc
@@ -45,6 +45,8 @@  along with GCC; see the file COPYING3.  If not see
 #include "analyzer/constraint-manager.h"
 #include "analyzer/checker-event.h"
 #include "analyzer/exploded-graph.h"
+#include "analyzer/callsite-expr.h"
+#include "analyzer/state-transition.h"
 
 #if ENABLE_ANALYZER
 
@@ -67,6 +69,8 @@  event_kind_to_string (enum event_kind ek)
       return "stmt";
     case event_kind::region_creation:
       return "region_creation";
+    case event_kind::state_transition:
+      return "state_transition";
     case event_kind::function_entry:
       return "function_entry";
     case event_kind::state_change:
@@ -301,6 +305,94 @@  statement_event::print_desc (pretty_printer &pp) const
   pp_gimple_stmt_1 (&pp, m_stmt, 0, (dump_flags_t)0);
 }
 
+/* class state_transition_event : public checker_event.  */
+
+void
+state_transition_event::print_desc (pretty_printer &pp) const
+{
+  gcc_assert (m_state_trans);
+  switch (m_state_trans->get_kind ())
+    {
+    default:
+      gcc_unreachable ();
+    case state_transition::kind::origin:
+      {
+	const state_transition_origin &state_trans
+	  = *static_cast <const state_transition_origin *> (m_state_trans);
+	if (m_pending_diagnostic)
+	  {
+	    evdesc::origin_of_state evd (state_trans.m_dst_reg_expr);
+	    if (m_pending_diagnostic->describe_origin_of_state (pp, evd))
+	      return;
+	  }
+	pp_printf (&pp, "value originates here");
+      }
+      break;
+
+    case state_transition::kind::at_call:
+    case state_transition::kind::at_return:
+      // These should be handled by call_event and return_event
+      gcc_unreachable ();
+      break;
+
+    case state_transition::kind::copy:
+      {
+	const state_transition_copy &state_trans
+	  = *static_cast <const state_transition_copy *> (m_state_trans);
+	if (m_pending_diagnostic)
+	  {
+	    evdesc::copy_of_state evd (state_trans.m_src_reg_expr,
+				       state_trans.get_src_event_id (),
+				       state_trans.m_dst_reg_expr);
+	    if (m_pending_diagnostic->describe_copy_of_state (pp, evd))
+	      return;
+	  }
+	auto event_id = state_trans.get_src_event_id ();
+	if (event_id.known_p ())
+	  pp_printf (&pp, "copying value from %@ from %qE to %qE",
+		     &event_id,
+		     state_trans.m_src_reg_expr,
+		     state_trans.m_dst_reg_expr);
+	else
+	  pp_printf (&pp, "copying value from %qE to %qE",
+		     state_trans.m_src_reg_expr,
+		     state_trans.m_dst_reg_expr);
+      }
+      break;
+    case state_transition::kind::use:
+      {
+	const state_transition_use &state_trans
+	  = *static_cast <const state_transition_use *> (m_state_trans);
+	if (m_pending_diagnostic)
+	  {
+	    evdesc::use_of_state evd (state_trans.m_src_reg_expr,
+				      state_trans.get_src_event_id ());
+	    if (m_pending_diagnostic->describe_use_of_state (pp, evd))
+	      return;
+	  }
+	auto event_id = state_trans.get_src_event_id ();
+	if (state_trans.get_src_event_id ().known_p ())
+	  pp_printf (&pp, "using value from %@ from %qE",
+		     &event_id,
+		     state_trans.m_src_reg_expr);
+	else
+	  pp_printf (&pp, "using value from %qE",
+		     state_trans.m_src_reg_expr);
+      }
+      break;
+    }
+}
+
+void
+state_transition_event::
+prepare_for_emission (checker_path *path,
+		      pending_diagnostic *pd,
+		      diagnostics::paths::event_id_t emission_id)
+{
+  checker_event::prepare_for_emission (path, pd, emission_id);
+  const_cast<state_transition *> (m_state_trans)->m_event_id = emission_id;
+}
+
 /* class region_creation_event : public checker_event.  */
 
 region_creation_event::region_creation_event (const event_loc_info &loc_info)
@@ -376,16 +468,6 @@  region_creation_event_debug::print_desc (pretty_printer &pp) const
 
 /* class function_entry_event : public checker_event.  */
 
-function_entry_event::function_entry_event (const program_point &dst_point,
-					    const program_state &state)
-: checker_event (event_kind::function_entry,
-		 event_loc_info (dst_point.get_location (),
-				 dst_point.get_fndecl (),
-				 dst_point.get_stack_depth ())),
-  m_state (state)
-{
-}
-
 /* Implementation of diagnostics::paths::event::print_desc vfunc for
    function_entry_event.
 
@@ -394,6 +476,25 @@  function_entry_event::function_entry_event (const program_point &dst_point,
 void
 function_entry_event::print_desc (pretty_printer &pp) const
 {
+  if (m_state_trans)
+    {
+      callsite_expr expr = m_state_trans->get_callsite_expr ();
+      if (tree parm = expr.get_param_tree (m_effective_fndecl))
+	{
+	  auto src_event_id = m_state_trans->get_src_event_id ();
+	  if (src_event_id.known_p ())
+	    pp_printf (&pp,
+		       "entry to %qE with problematic value from %@ for %qE",
+		       m_effective_fndecl,
+		       &src_event_id,
+		       parm);
+	  else
+	    pp_printf (&pp, "entry to %qE with problematic value for %qE",
+		       m_effective_fndecl, parm);
+	  return;
+	}
+    }
+
   pp_printf (&pp, "entry to %qE", m_effective_fndecl);
 }
 
@@ -406,6 +507,17 @@  function_entry_event::get_meaning () const
   return meaning (verb::enter, noun::function);
 }
 
+void
+function_entry_event::
+prepare_for_emission (checker_path *path,
+		      pending_diagnostic *pd,
+		      diagnostics::paths::event_id_t emission_id)
+{
+  checker_event::prepare_for_emission (path, pd, emission_id);
+  if (m_state_trans)
+    const_cast<state_transition_at_call *> (m_state_trans)->m_event_id = emission_id;
+}
+
 /* class state_change_event : public checker_event.  */
 
 /* state_change_event's ctor.  */
@@ -730,8 +842,10 @@  catch_cfg_edge_event::get_meaning () const
 /* call_event's ctor.  */
 
 call_event::call_event (const exploded_edge &eedge,
-			const event_loc_info &loc_info)
-: superedge_event (event_kind::call_, eedge, loc_info)
+			const event_loc_info &loc_info,
+			const state_transition_at_call *state_trans)
+: superedge_event (event_kind::call_, eedge, loc_info),
+  m_state_trans (state_trans)
 {
    m_src_snode = eedge.m_src->get_supernode ();
    m_dest_snode = eedge.m_dest->get_supernode ();
@@ -751,16 +865,50 @@  call_event::call_event (const exploded_edge &eedge,
 void
 call_event::print_desc (pretty_printer &pp) const
 {
-  if (m_critical_state.m_state && m_pending_diagnostic)
+  if (m_pending_diagnostic)
     {
-      gcc_assert (m_critical_state.m_var);
-      tree var = fixup_tree_for_diagnostic (m_critical_state.m_var);
-      evdesc::call_with_state evd (m_src_snode->m_fun->decl,
-				   m_dest_snode->m_fun->decl,
-				   var,
-				   m_critical_state.m_state);
-      if (m_pending_diagnostic->describe_call_with_state (pp, evd))
-	return;
+      if (m_critical_state.m_state)
+	{
+	  gcc_assert (m_critical_state.m_var);
+	  tree var = fixup_tree_for_diagnostic (m_critical_state.m_var);
+	  evdesc::call_with_state evd (m_src_snode->m_fun->decl,
+				       m_dest_snode->m_fun->decl,
+				       var,
+				       m_critical_state.m_state,
+				       m_state_trans);
+	  if (m_pending_diagnostic->describe_call_with_state (pp, evd))
+	    return;
+	}
+      else if (m_state_trans)
+	{
+	  evdesc::call_with_state evd (m_src_snode->m_fun->decl,
+				       m_dest_snode->m_fun->decl,
+				       NULL_TREE, nullptr,
+				       m_state_trans);
+	  if (m_pending_diagnostic->describe_call_with_state (pp, evd))
+	    return;
+	  callsite_expr expr = m_state_trans->get_callsite_expr ();
+	  if (expr.param_p ())
+	    {
+	      auto src_event_id = m_state_trans->get_src_event_id ();
+	      if (src_event_id.known_p ())
+		pp_printf (&pp,
+			   "passing problematic value from %@ from %qE to %qE"
+			   " via parameter %i",
+			   &src_event_id,
+			   get_caller_fndecl (),
+			   get_callee_fndecl (),
+			   expr.param_num ());
+	      else
+		pp_printf (&pp,
+			   "passing problematic value from %qE to %qE"
+			   " via parameter %i",
+			   get_caller_fndecl (),
+			   get_callee_fndecl (),
+			   expr.param_num ());
+	      return;
+	    }
+	}
     }
 
   pp_printf (&pp,
@@ -806,14 +954,26 @@  call_event::get_program_state () const
   return &m_eedge.m_src->get_state ();
 }
 
+void
+call_event::prepare_for_emission (checker_path *path,
+				  pending_diagnostic *pd,
+				  diagnostics::paths::event_id_t emission_id)
+{
+  checker_event::prepare_for_emission (path, pd, emission_id);
+  if (m_state_trans)
+    const_cast<state_transition_at_call *> (m_state_trans)->m_event_id = emission_id;
+}
+
 /* class return_event : public checker_event.  */
 
 /* return_event's ctor.  */
 
 return_event::return_event (const exploded_edge &eedge,
-			    const event_loc_info &loc_info)
+			    const event_loc_info &loc_info,
+			    const state_transition_at_return *state_trans)
 : checker_event (event_kind::return_, loc_info),
-  m_eedge (eedge)
+  m_eedge (eedge),
+  m_state_trans (state_trans)
 {
   m_src_snode = eedge.m_src->get_supernode ();
   m_dest_snode = eedge.m_dest->get_supernode ();
@@ -839,14 +999,28 @@  return_event::print_desc (pretty_printer &pp) const
       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_state && m_pending_diagnostic)
+  if (m_pending_diagnostic)
     {
-      evdesc::return_of_state evd (m_dest_snode->m_fun->decl,
-				   m_src_snode->m_fun->decl,
-				   m_critical_state.m_state);
-      if (m_pending_diagnostic->describe_return_of_state (pp, evd))
-	return;
+      if (m_critical_state.m_state)
+	{
+	  evdesc::return_of_state evd (m_dest_snode->m_fun->decl,
+				       m_src_snode->m_fun->decl,
+				       m_critical_state.m_state,
+				       nullptr);
+	  if (m_pending_diagnostic->describe_return_of_state (pp, evd))
+	    return;
+	}
+      else if (m_state_trans)
+	{
+	  evdesc::return_of_state evd (m_dest_snode->m_fun->decl,
+				       m_src_snode->m_fun->decl,
+				       nullptr,
+				       m_state_trans);
+	  if (m_pending_diagnostic->describe_return_of_state (pp, evd))
+	    return;
+	}
     }
+
   pp_printf (&pp,
 	     "returning to %qE from %qE",
 	     m_dest_snode->m_fun->decl,
@@ -876,6 +1050,16 @@  return_event::get_program_state () const
   return &m_eedge.m_dest->get_state ();
 }
 
+void
+return_event::prepare_for_emission (checker_path *path,
+				    pending_diagnostic *pd,
+				    diagnostics::paths::event_id_t emission_id)
+{
+  checker_event::prepare_for_emission (path, pd, emission_id);
+  if (m_state_trans)
+    const_cast<state_transition_at_return *> (m_state_trans)->m_event_id = emission_id;
+}
+
 /* class start_consolidated_cfg_edges_event : public checker_event.  */
 
 void
diff --git a/gcc/analyzer/checker-event.h b/gcc/analyzer/checker-event.h
index be39a89a7b1..edf81e907eb 100644
--- a/gcc/analyzer/checker-event.h
+++ b/gcc/analyzer/checker-event.h
@@ -37,6 +37,7 @@  enum class event_kind
   custom,
   stmt,
   region_creation,
+  state_transition,
   function_entry,
   state_change,
   start_cfg_edge,
@@ -68,6 +69,7 @@  extern const char *event_kind_to_string (enum event_kind ek);
        custom_event (event_kind::custom)
 	 precanned_custom_event
        statement_event (event_kind::stmt)
+       state_transition_event (event_kind::data_flow)
        region_creation_event (event_kind::region_creation)
        function_entry_event (event_kind::function_entry)
        state_change_event (event_kind::state_change)
@@ -245,6 +247,31 @@  public:
   const program_state m_dst_state;
 };
 
+/* A concrete checker_event subclass referencing a state_transition,
+   for cases where the state_transition doesn't already have its own event.  */
+
+class state_transition_event : public checker_event
+{
+public:
+  state_transition_event (const event_loc_info &loc_info,
+			  const state_transition *state_trans)
+  : checker_event (event_kind::state_transition, loc_info),
+    m_state_trans (state_trans)
+  {
+    gcc_assert (m_state_trans);
+  }
+
+  void print_desc (pretty_printer &) const final override;
+
+  void prepare_for_emission (checker_path *path,
+			     pending_diagnostic *pd,
+			     diagnostics::paths::event_id_t emission_id) final override;
+
+private:
+  // borrowed from the exploded_path
+  const state_transition *m_state_trans;
+};
+
 /* An abstract event subclass describing the creation of a region that
    is significant for a diagnostic.
 
@@ -352,15 +379,14 @@  class function_entry_event : public checker_event
 {
 public:
   function_entry_event (const event_loc_info &loc_info,
-			const program_state &state)
+			const program_state &state,
+			const state_transition_at_call *state_trans)
   : checker_event (event_kind::function_entry, loc_info),
-    m_state (state)
+    m_state (state),
+    m_state_trans (state_trans)
   {
   }
 
-  function_entry_event (const program_point &dst_point,
-			const program_state &state);
-
   void print_desc (pretty_printer &pp) const override;
   meaning get_meaning () const override;
 
@@ -372,8 +398,17 @@  public:
     return &m_state;
   }
 
+  void
+  prepare_for_emission (checker_path *path,
+			pending_diagnostic *pd,
+			diagnostics::paths::event_id_t emission_id) final override;
+
+  const state_transition_at_call *
+  get_state_transition_at_call () const { return m_state_trans; }
+
 private:
   const program_state &m_state;
+  const state_transition_at_call *m_state_trans;
 };
 
 /* Subclass of checker_event describing a state change.  */
@@ -558,7 +593,8 @@  class call_event : public superedge_event
 {
 public:
   call_event (const exploded_edge &eedge,
-	      const event_loc_info &loc_info);
+	      const event_loc_info &loc_info,
+	      const state_transition_at_call *state_trans);
 
   void print_desc (pretty_printer &pp) const override;
   meaning get_meaning () const override;
@@ -568,6 +604,11 @@  public:
   const program_state *
   get_program_state () const final override;
 
+  void
+  prepare_for_emission (checker_path *path,
+			pending_diagnostic *pd,
+			diagnostics::paths::event_id_t emission_id) final override;
+
   /* 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 print_desc can attempt to get a better description
@@ -577,6 +618,9 @@  public:
     m_critical_state = critical_state (var, state);
   }
 
+  const state_transition_at_call *
+  get_state_transition_at_call () const { return m_state_trans; }
+
 protected:
   tree get_caller_fndecl () const;
   tree get_callee_fndecl () const;
@@ -584,6 +628,7 @@  protected:
   const supernode *m_src_snode;
   const supernode *m_dest_snode;
   critical_state m_critical_state;
+  const state_transition_at_call *m_state_trans;
 };
 
 /* A concrete event subclass for an interprocedural return.  */
@@ -592,7 +637,8 @@  class return_event : public checker_event
 {
 public:
   return_event (const exploded_edge &eedge,
-		const event_loc_info &loc_info);
+		const event_loc_info &loc_info,
+		const state_transition_at_return *state_trans);
 
   void print_desc (pretty_printer &pp) const final override;
   meaning get_meaning () const override;
@@ -608,6 +654,11 @@  public:
   const program_state *
   get_program_state () const override;
 
+  void
+  prepare_for_emission (checker_path *path,
+			pending_diagnostic *pd,
+			diagnostics::paths::event_id_t emission_id) final override;
+
   /* 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 print_desc can attempt to get a better description
@@ -622,6 +673,7 @@  public:
   const supernode *m_dest_snode;
   const call_and_return_op *m_call_and_return_op;
   critical_state m_critical_state;
+  const state_transition_at_return *m_state_trans;
 };
 
 /* A concrete event subclass for the start of a consolidated run of CFG
diff --git a/gcc/analyzer/common.h b/gcc/analyzer/common.h
index cbe36dfb9c9..81db5997f2e 100644
--- a/gcc/analyzer/common.h
+++ b/gcc/analyzer/common.h
@@ -22,6 +22,7 @@  along with GCC; see the file COPYING3.  If not see
 #define GCC_ANALYZER_COMMON_H
 
 #include "config.h"
+#define INCLUDE_ALGORITHM
 #define INCLUDE_LIST
 #define INCLUDE_MAP
 #define INCLUDE_SET
@@ -139,6 +140,9 @@  class call_summary;
 class call_summary_replay;
 struct per_function_data;
 struct interesting_t;
+class state_transition;
+  class state_transition_at_call;
+  class state_transition_at_return;
 class uncertainty_t;
 
 class feasible_node;
@@ -155,6 +159,7 @@  extern void dump_tree (pretty_printer *pp, tree t);
 extern void dump_quoted_tree (pretty_printer *pp, tree t);
 extern void print_quoted_type (pretty_printer *pp, tree t);
 extern void print_expr_for_user (pretty_printer *pp, tree t);
+extern bool printable_expr_p (const_tree expr);
 extern int readability_comparator (const void *p1, const void *p2);
 extern int tree_cmp (const void *p1, const void *p2);
 extern tree fixup_tree_for_diagnostic (tree);
@@ -374,6 +379,44 @@  enum class access_direction
   write
 };
 
+/* State tracked along an execution path that's pertinent to a specific
+   diagnostic (e.g. for a divide-by-zero warning where the zero value
+   comes from).  */
+
+struct diagnostic_state
+{
+  diagnostic_state ()
+  : m_region_holding_value (nullptr)
+  {
+  }
+
+  diagnostic_state (std::string debug_desc,
+		   const region *region_holding_value)
+  : m_debug_desc (std::move (debug_desc)),
+    m_region_holding_value (region_holding_value)
+  {
+  }
+
+  void dump_to_pp (pretty_printer *) const;
+  void dump () const;
+
+  bool
+  operator== (const diagnostic_state &other) const
+  {
+    return m_region_holding_value == other.m_region_holding_value;
+  }
+  bool
+  operator!= (const diagnostic_state &other) const
+  {
+    return !(*this == other);
+  }
+
+  std::string m_debug_desc;
+  const region *m_region_holding_value;
+};
+
+struct rewind_context;
+
 /* Abstract base class for associating custom data with an
    exploded_edge, for handling non-standard edges such as
    rewinding from a longjmp, signal handlers, etc.
@@ -406,13 +449,20 @@  public:
 
   virtual void add_events_to_path (checker_path *emission_path,
 				   const exploded_edge &eedge,
-				   pending_diagnostic &pd) const = 0;
+				   pending_diagnostic &pd,
+				   const state_transition *state_trans) const = 0;
 
   virtual exploded_node *create_enode (exploded_graph &eg,
 				       const program_point &point,
 				       program_state &&state,
 				       exploded_node *enode_for_diag,
 				       region_model_context *ctxt) const;
+
+  virtual bool
+  try_to_rewind_data_flow (rewind_context &) const
+  {
+    return false;
+  }
 };
 
 /* Abstract base class for splitting state.
diff --git a/gcc/analyzer/diagnostic-manager.cc b/gcc/analyzer/diagnostic-manager.cc
index a0f149e9c52..36ce445cf8c 100644
--- a/gcc/analyzer/diagnostic-manager.cc
+++ b/gcc/analyzer/diagnostic-manager.cc
@@ -917,7 +917,7 @@  compatible_epath_p (const exploded_path *lhs_path,
       while (lhs_eedge_idx >= 0)
 	{
 	  /* Find LHS_PATH's next superedge.  */
-	  lhs_eedge = lhs_path->m_edges[lhs_eedge_idx];
+	  lhs_eedge = lhs_path->m_elements[lhs_eedge_idx].m_eedge;
 	  if (lhs_eedge->m_sedge)
 	    break;
 	  else
@@ -926,7 +926,7 @@  compatible_epath_p (const exploded_path *lhs_path,
       while (rhs_eedge_idx >= 0)
 	{
 	  /* Find RHS_PATH's next superedge.  */
-	  rhs_eedge = rhs_path->m_edges[rhs_eedge_idx];
+	  rhs_eedge = rhs_path->m_elements[rhs_eedge_idx].m_eedge;
 	  if (rhs_eedge->m_sedge)
 	    break;
 	  else
@@ -1555,13 +1555,20 @@  diagnostic_manager::emit_saved_diagnostic (const exploded_graph &eg,
        sd.get_index (), sd.m_d->get_kind (), sd.get_supernode ()->m_id);
   log ("num dupes: %i", sd.get_num_dupes ());
 
-  const exploded_path *epath = sd.get_best_epath ();
+  exploded_path *epath = sd.get_best_epath ();
   gcc_assert (epath);
 
+  epath->maybe_log (get_logger (), "best epath");
+
   /* Precompute all enodes from which the diagnostic is reachable.  */
   path_builder pb (eg, *epath, sd.get_feasibility_problem (), sd);
 
-  /* This is the diagnostics::paths::path subclass that will be built for
+  /* Annotate EPATH with information specific to the diagnostic, such
+     as pertinent data flow events.  */
+  annotate_exploded_path (pb, *epath);
+  epath->maybe_log (get_logger (), "best epath with annotations");
+
+  /* This is the diagnostics::paths::path instance that will be built for
      the diagnostic.  */
   checker_path emission_path (get_logical_location_manager (),
 			      eg.get_ext_state (),
@@ -1589,7 +1596,8 @@  diagnostic_manager::emit_saved_diagnostic (const exploded_graph &eg,
      trailing eedge stashed, add any events for it.  This is for use
      in handling longjmp, to show where a longjmp is rewinding to.  */
   if (sd.m_trailing_eedge)
-    add_events_for_eedge (pb, *sd.m_trailing_eedge, &emission_path, nullptr);
+    add_events_for_eedge (pb, *sd.m_trailing_eedge, &emission_path, nullptr,
+			  nullptr);
 
   emission_path.inject_any_inlined_call_events (get_logger ());
 
@@ -1640,6 +1648,149 @@  diagnostic_manager::get_logical_location_manager () const
   return *mgr;
 }
 
+class epath_rewind_context : public rewind_context
+{
+public:
+  epath_rewind_context (const exploded_edge &eedge,
+			logger *logger,
+			diagnostic_state input_state,
+			state_transition *&last_state_transition,
+			exploded_path::element_t &epath_element)
+  : rewind_context (eedge, logger, input_state),
+    m_last_state_transition (last_state_transition),
+    m_epath_element (epath_element)
+  {
+  }
+
+  bool
+  could_be_affected_by_write_p (tree lhs) final override
+  {
+    if (!m_input.m_region_holding_value)
+      return false;
+
+    if (TREE_CODE (lhs) == SSA_NAME)
+      if (tree decl = m_input.m_region_holding_value->maybe_get_decl ())
+	return decl == lhs;
+
+    return true;
+  }
+
+  void
+  add_state_transition (std::unique_ptr<state_transition> st) final override
+  {
+    gcc_assert (st.get ());
+    if (m_logger)
+      {
+	m_logger->start_log_line ();
+	m_logger->log_partial ("adding state transition: ");
+	st->dump_to_pp (m_logger->get_printer ());
+	m_logger->end_log_line ();
+      }
+
+    /* Chain up the state_transition instances, so that each state transition
+       has a pointer to the one that occurred before it (but was created after
+       it, since we are rewinding the epath).  */
+    if (m_last_state_transition)
+      m_last_state_transition->m_prev_state_transition = st.get ();
+    m_last_state_transition = st.get ();
+
+    m_epath_element.m_state_transition = std::move (st);
+  }
+
+private:
+  state_transition *&m_last_state_transition;
+  exploded_path::element_t &m_epath_element;
+};
+
+/* Populate the elements of EPATH with diagnostic_state and state_transition
+   information pertinent to the pending diagnostic.  */
+
+void
+diagnostic_manager::annotate_exploded_path (const path_builder &pb,
+					    exploded_path &epath) const
+{
+  auto logger = get_logger ();
+  LOG_SCOPE (logger);
+
+  // TODO: consolidate this with build_emission_path?
+  interesting_t interest;
+  pb.get_pending_diagnostic ()->mark_interesting_stuff (&interest);
+
+  gcc_assert (epath.m_elements.size () > 0);
+
+  diagnostic_state curr_state;
+  state_transition *last_state_transition = nullptr;
+
+  if (interest.m_read_regions.size () > 0)
+    curr_state = interest.m_read_regions[0];
+
+  // Walk epath backwards, propagating annotation information
+  for (int idx = epath.m_elements.size () - 1; idx >= 0; --idx)
+    {
+      exploded_path::element_t &iter_element = epath.m_elements[idx];
+      if (logger)
+	{
+	  logger->log ("edge[%i]: considering rewinding EN %i -> EN %i",
+		       idx,
+		       iter_element.m_eedge->m_src->m_index,
+		       iter_element.m_eedge->m_dest->m_index);
+	  logger->start_log_line ();
+	  logger->log_partial ("curr_state: ");
+	  curr_state.dump_to_pp (logger->get_printer ());
+	  logger->end_log_line ();
+	}
+      iter_element.m_state_at_dst = curr_state;
+      const exploded_edge *eedge = iter_element.m_eedge;
+      gcc_assert (eedge);
+
+      epath_rewind_context ctxt (*eedge, logger, curr_state,
+				 last_state_transition, iter_element);
+      if (eedge->m_custom_info)
+	{
+	  if (logger)
+	    {
+	      logger->start_log_line ();
+	      logger->log_partial ("custom_edge_info: ");
+	      eedge->m_custom_info->print (logger->get_printer ());
+	      logger->end_log_line ();
+	    }
+	  if (!eedge->m_custom_info->try_to_rewind_data_flow (ctxt))
+	    {
+	      if (logger)
+		logger->log ("could not rewind custom info");
+	      return;
+	    }
+	}
+      else if (const operation *op = eedge->maybe_get_op ())
+	{
+	  if (logger)
+	    {
+	      logger->start_log_line ();
+	      logger->log_partial ("op: ");
+	      op->print_as_edge_label (logger->get_printer (), false);
+	      logger->end_log_line ();
+	    }
+	  if (!op->try_to_rewind_data_flow (ctxt))
+	    {
+	      if (logger)
+		logger->log ("could not rewind op");
+	      return;
+	    }
+	}
+
+      iter_element.m_state_at_src = ctxt.m_output;
+      curr_state = ctxt.m_output;
+      if (logger)
+	{
+	  logger->log ("rewound");
+	  logger->start_log_line ();
+	  logger->log_partial ("curr_state: ");
+	  curr_state.dump_to_pp (logger->get_printer ());
+	  logger->end_log_line ();
+	}
+    }
+}
+
 /* Emit a "path" of events to EMISSION_PATH describing the exploded path
    EPATH within EG.  */
 
@@ -1683,10 +1834,12 @@  diagnostic_manager::build_emission_path (const path_builder &pb,
   }
 
   /* Walk EPATH, adding events as appropriate.  */
-  for (unsigned i = 0; i < epath.m_edges.length (); i++)
+  for (unsigned i = 0; i < epath.m_elements.size (); ++i)
     {
-      const exploded_edge *eedge = epath.m_edges[i];
-      add_events_for_eedge (pb, *eedge, emission_path, &interest);
+      const exploded_edge *eedge = epath.m_elements[i].m_eedge;
+      gcc_assert (eedge);
+      add_events_for_eedge (pb, *eedge, emission_path, &interest,
+			    epath.m_elements[i].m_state_transition.get ());
     }
   add_event_on_final_node (pb, epath.get_final_enode (),
 			   emission_path, &interest);
@@ -1895,7 +2048,8 @@  void
 diagnostic_manager::add_events_for_eedge (const path_builder &pb,
 					  const exploded_edge &eedge,
 					  checker_path *emission_path,
-					  interesting_t *interest) const
+					  interesting_t *interest,
+					  const state_transition *state_trans) const
 {
   const exploded_node *src_node = eedge.m_src;
   const program_point &src_point = src_node->get_point ();
@@ -1913,11 +2067,19 @@  diagnostic_manager::add_events_for_eedge (const path_builder &pb,
       src_point.print (pp, format (false));
       pp_string (pp, "-> ");
       dst_point.print (pp, format (false));
+      if (state_trans)
+	{
+	  pp_string (pp, " {");
+	  state_trans->dump_to_pp (pp);
+	  pp_string (pp, "}");
+	}
       get_logger ()->end_log_line ();
     }
   const program_state &src_state = src_node->get_state ();
   const program_state &dst_state = dst_node->get_state ();
 
+  bool created_event_for_state_trans = false;
+
   /* 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
@@ -1946,11 +2108,15 @@  diagnostic_manager::add_events_for_eedge (const path_builder &pb,
   /* Allow non-standard edges to add events, e.g. when rewinding from
      longjmp to a setjmp.  */
   if (eedge.m_custom_info)
-    eedge.m_custom_info->add_events_to_path (emission_path, eedge, *pd);
+    {
+      eedge.m_custom_info->add_events_to_path (emission_path, eedge, *pd,
+					       state_trans);
+      created_event_for_state_trans = true;
+    }
 
   /* Don't add events for insignificant edges at verbosity levels below 3.  */
   if (m_verbosity < 3)
-    if (!significant_edge_p (pb, eedge))
+    if (!significant_edge_p (pb, eedge) && !state_trans)
       return;
 
   /* Add events for operations.  */
@@ -1962,7 +2128,11 @@  diagnostic_manager::add_events_for_eedge (const path_builder &pb,
   if (dst_point.get_supernode ()->entry_p ())
     {
       pb.get_pending_diagnostic ()->add_function_entry_event
-	(eedge, emission_path);
+	(eedge, emission_path,
+	 (state_trans
+	  ? state_trans->dyn_cast_state_transition_at_call ()
+	  : nullptr));
+      created_event_for_state_trans = true;
       /* Create region_creation_events for on-stack regions within
 	 this frame.  */
       if (interest)
@@ -2028,6 +2198,14 @@  diagnostic_manager::add_events_for_eedge (const path_builder &pb,
 	}
     }
 
+  /* If we have a state transition and haven't yet created an
+     event that describes it, do so now.  */
+  if (state_trans && !created_event_for_state_trans)
+    emission_path->add_event
+      (std::make_unique<state_transition_event>
+       (eedge.m_src->get_point (),
+	state_trans));
+
   if (pb.get_feasibility_problem ()
       && &pb.get_feasibility_problem ()->m_eedge == &eedge)
     {
@@ -2241,6 +2419,21 @@  diagnostic_manager::prune_for_sm_diagnostic (checker_path *path,
 	  /* Don't filter these.  */
 	  break;
 
+	case event_kind::state_transition:
+	  /* Prune these if they have an empty description.  */
+	  {
+	    tree_dump_pretty_printer pp (nullptr);
+	    base_event->print_desc (pp);
+	    if (strlen (pp_formatted_text (&pp)) == 0)
+	      {
+		log (("filtering event %i:"
+		      " state_transition_event with empty description"),
+		     idx);
+		path->delete_event (idx);
+	      }
+	  }
+	  break;
+
 	case event_kind::function_entry:
 	  if (m_verbosity < 1)
 	    {
diff --git a/gcc/analyzer/diagnostic-manager.h b/gcc/analyzer/diagnostic-manager.h
index 58c01664965..9f01a28dafd 100644
--- a/gcc/analyzer/diagnostic-manager.h
+++ b/gcc/analyzer/diagnostic-manager.h
@@ -101,7 +101,7 @@  public:
   }
 
   bool calc_best_epath (epath_finder *pf);
-  const exploded_path *get_best_epath () const { return m_best_epath.get (); }
+  exploded_path *get_best_epath () const { return m_best_epath.get (); }
   unsigned get_epath_length () const;
 
   void add_duplicate (saved_diagnostic *other);
@@ -201,6 +201,10 @@  private:
   const diagnostics::logical_locations::manager &
   get_logical_location_manager () const;
 
+  void
+  annotate_exploded_path (const path_builder &pb,
+			  exploded_path &epath) const;
+
   void build_emission_path (const path_builder &pb,
 			    const exploded_path &epath,
 			    checker_path *emission_path) const;
@@ -213,7 +217,8 @@  private:
   void add_events_for_eedge (const path_builder &pb,
 			     const exploded_edge &eedge,
 			     checker_path *emission_path,
-			     interesting_t *interest) const;
+			     interesting_t *interest,
+			     const state_transition *state_trans) const;
 
   bool significant_edge_p (const path_builder &pb,
 			   const exploded_edge &eedge) const;
diff --git a/gcc/analyzer/engine.cc b/gcc/analyzer/engine.cc
index 3e559b47a5d..f1a9ad4b785 100644
--- a/gcc/analyzer/engine.cc
+++ b/gcc/analyzer/engine.cc
@@ -37,6 +37,7 @@  along with GCC; see the file COPYING3.  If not see
 #include "gimple-predict.h"
 #include "context.h"
 #include "channels.h"
+#include "pretty-print-markup.h"
 
 #include "text-art/dump.h"
 
@@ -308,10 +309,10 @@  public:
 	       after the SSA name was set? (if any).  */
 
 	    for (unsigned idx = idx_of_def_stmt + 1;
-		 idx < epath.m_edges.length ();
+		 idx < epath.m_elements.size ();
 		 ++idx)
 	      {
-		const exploded_edge *eedge = epath.m_edges[idx];
+		const exploded_edge *eedge = epath.m_elements[idx].m_eedge;
 		if (logger)
 		  logger->log ("eedge[%i]: EN %i -> EN %i",
 			       idx,
@@ -352,10 +353,10 @@  public:
 	  retval = gimple_return_retval (return_stmt);
 
 	log_scope sentinel (logger, "walking backward along epath");
-	int idx;
-	const exploded_edge *eedge;
-	FOR_EACH_VEC_ELT_REVERSE (epath.m_edges, idx, eedge)
+	for (int idx = epath.m_elements.size () - 1; idx >= 0; --idx)
 	  {
+	    const exploded_path::element_t &element = epath.m_elements[idx];
+	    const exploded_edge *eedge = element.m_eedge;
 	    if (logger)
 	      {
 		logger->log ("eedge[%i]: EN %i -> EN %i",
@@ -403,10 +404,10 @@  private:
   {
     LOG_SCOPE (logger);
 
-    int idx;
-    const exploded_edge *eedge;
-    FOR_EACH_VEC_ELT_REVERSE (epath.m_edges, idx, eedge)
+    for (int idx = epath.m_elements.size () - 1; idx >= 0; --idx)
       {
+	const exploded_path::element_t &element = epath.m_elements[idx];
+	const exploded_edge *eedge = element.m_eedge;
 	if (eedge->m_src->get_stack_depth ()
 	    != eedge->m_dest->get_stack_depth ())
 	  {
@@ -1319,7 +1320,8 @@  public:
 
   void add_events_to_path (checker_path *emission_path,
 			   const exploded_edge &eedge,
-			   pending_diagnostic &) const final override
+			   pending_diagnostic &,
+			   const state_transition *) const final override
   {
     const exploded_node *dst_node = eedge.m_dest;
     const program_point &dst_point = dst_node->get_point ();
@@ -1369,7 +1371,8 @@  public:
 
   void add_events_to_path (checker_path *emission_path,
 			   const exploded_edge &eedge,
-			   pending_diagnostic &) const final override
+			   pending_diagnostic &,
+			   const state_transition *) const final override
   {
     const exploded_node *src_node = eedge.m_src;
     const program_point &src_point = src_node->get_point ();
@@ -1644,7 +1647,7 @@  void
 interprocedural_call::print (pretty_printer *pp) const
 {
   pp_string (pp, "call to ");
-  pp_gimple_stmt_1 (pp, &m_call_stmt, 0, (dump_flags_t)0);
+  pp_gimple_stmt_1 (pp, &get_gcall (), 0, (dump_flags_t)0);
 }
 
 void
@@ -1667,16 +1670,70 @@  interprocedural_call::update_model (region_model *model,
 				    const exploded_edge */*eedge*/,
 				    region_model_context *ctxt) const
 {
-  model->update_for_gcall (m_call_stmt, ctxt, &m_callee_fun);
+  model->update_for_gcall (get_gcall (), ctxt, &m_callee_fun);
   return true;
 }
 
 void
 interprocedural_call::add_events_to_path (checker_path *emission_path,
 					  const exploded_edge &eedge,
-					  pending_diagnostic &pd) const
+					  pending_diagnostic &pd,
+					  const state_transition *state_trans) const
+{
+  pd.add_call_event (eedge, get_gcall (), *emission_path,
+		     (state_trans
+		      ? state_trans->dyn_cast_state_transition_at_call ()
+		      : nullptr));
+}
+
+bool
+interprocedural_call::try_to_rewind_data_flow (rewind_context &ctxt) const
+{
+  auto logger = ctxt.m_logger;
+
+  // Rewind from params to arguments
+  if (ctxt.m_input.m_region_holding_value)
+    {
+      const region_model *dst_enode_model
+	= ctxt.m_eedge.m_dest->get_state ().m_region_model;
+      tree dst_tree
+	= dst_enode_model->get_representative_tree
+	(ctxt.m_input.m_region_holding_value);
+      if (dst_tree)
+	{
+	  callsite_expr expr;
+	  tree src_tree
+	    = m_op.map_expr_from_callee_to_caller (m_callee_fun.decl,
+						   dst_tree,
+						   &expr);
+	  if (src_tree)
+	    {
+	      const region_model *src_enode_model
+		= ctxt.m_eedge.m_src->get_state ().m_region_model;
+	      ctxt.m_output.m_region_holding_value
+		= src_enode_model->get_lvalue (src_tree, nullptr);
+
+	      ctxt.add_state_transition
+		(std::make_unique<state_transition_at_call> (expr));
+
+	      if (logger)
+		{
+		  callsite_expr_element e (expr);
+		  logger->log ("updating m_region_holding_value from %qE to %qE"
+			       " (callsite_expr: %e)",
+			       dst_tree, src_tree, &e);
+		}
+	    }
+	}
+    }
+
+  return true;
+}
+
+const gcall &
+interprocedural_call::get_gcall () const
 {
-  pd.add_call_event (eedge, m_call_stmt, *emission_path);
+  return m_op.get_gcall ();
 }
 
 // class interprocedural_return : public custom_edge_info
@@ -1715,7 +1772,8 @@  interprocedural_return::update_model (region_model *model,
 void
 interprocedural_return::add_events_to_path (checker_path *emission_path,
 					    const exploded_edge &eedge,
-					    pending_diagnostic &) const
+					    pending_diagnostic &,
+					    const state_transition *state_trans) const
 {
   const program_point &dst_point = eedge.m_dest->get_point ();
   emission_path->add_event
@@ -1723,7 +1781,29 @@  interprocedural_return::add_events_to_path (checker_path *emission_path,
        (eedge,
 	event_loc_info (m_call_stmt.location,
 			dst_point.get_fndecl (),
-			dst_point.get_stack_depth ())));
+			dst_point.get_stack_depth ()),
+	(state_trans
+	 ? state_trans->dyn_cast_state_transition_at_return ()
+	 : nullptr)));
+}
+
+bool
+interprocedural_return::try_to_rewind_data_flow (rewind_context &ctxt) const
+{
+  auto logger = ctxt.m_logger;
+
+  tree lhs = gimple_call_lhs (&m_call_stmt);
+  if (!lhs)
+    return true;
+
+  const region_model *src_enode_model
+    = ctxt.m_eedge.m_src->get_state ().m_region_model;
+  tree fndecl = src_enode_model->get_current_function ()->decl;
+  tree fn_result = DECL_RESULT (fndecl);
+
+  ctxt.on_data_flow (DECL_RESULT (fndecl), lhs);
+
+  return true;
 }
 
 /* class exploded_edge : public dedge<eg_traits>.  */
@@ -2311,7 +2391,8 @@  public:
 
   void add_events_to_path (checker_path *emission_path,
 			   const exploded_edge &,
-			   pending_diagnostic &) const final override
+			   pending_diagnostic &,
+			   const state_transition *) const final override
   {
     emission_path->add_event
       (std::make_unique<tainted_args_function_custom_event>
@@ -2767,7 +2848,8 @@  public:
 
   void add_events_to_path (checker_path *emission_path,
 			   const exploded_edge &,
-			   pending_diagnostic &) const final override
+			   pending_diagnostic &,
+			   const state_transition *) const final override
   {
     /* Show the field in the struct declaration, e.g.
        "(1) field 'store' is marked with '__attribute__((tainted_args))'"  */
diff --git a/gcc/analyzer/event-loc-info.h b/gcc/analyzer/event-loc-info.h
index 24c2af10534..ace75fd9339 100644
--- a/gcc/analyzer/event-loc-info.h
+++ b/gcc/analyzer/event-loc-info.h
@@ -39,6 +39,10 @@  struct event_loc_info
   int m_depth;
 };
 
+extern event_loc_info
+event_loc_info_for_function_entry (const program_point &point,
+				   const state_transition_at_call *state_trans);
+
 } // namespace ana
 
 #endif /* GCC_ANALYZER_EVENT_LOC_INFO_H */
diff --git a/gcc/analyzer/exploded-graph.h b/gcc/analyzer/exploded-graph.h
index 0972e217baf..8fcc59a9ce0 100644
--- a/gcc/analyzer/exploded-graph.h
+++ b/gcc/analyzer/exploded-graph.h
@@ -381,9 +381,9 @@  private:
 class interprocedural_call : public custom_edge_info
 {
 public:
-  interprocedural_call (const gcall &call_stmt,
+  interprocedural_call (const call_and_return_op &op,
 			function &callee_fun)
-  : m_call_stmt (call_stmt),
+  : m_op (op),
     m_callee_fun (callee_fun)
   {}
 
@@ -402,10 +402,16 @@  public:
 
   void add_events_to_path (checker_path *emission_path,
 			   const exploded_edge &eedge,
-			   pending_diagnostic &pd) const final override;
+			   pending_diagnostic &pd,
+			   const state_transition *state_trans) const final override;
+
+  bool
+  try_to_rewind_data_flow (rewind_context &) const final override;
+
+  const gcall &get_gcall () const;
 
 private:
-  const gcall &m_call_stmt;
+  const call_and_return_op &m_op;
   function &m_callee_fun;
 };
 
@@ -434,7 +440,11 @@  public:
 
   void add_events_to_path (checker_path *emission_path,
 			   const exploded_edge &eedge,
-			   pending_diagnostic &pd) const final override;
+			   pending_diagnostic &pd,
+			   const state_transition *state_trans) const final override;
+
+  bool
+  try_to_rewind_data_flow (rewind_context &) const final override;
 
 private:
   const gcall &m_call_stmt;
@@ -463,7 +473,8 @@  public:
 
   void add_events_to_path (checker_path *emission_path,
 			   const exploded_edge &eedge,
-			   pending_diagnostic &pd) const final override;
+			   pending_diagnostic &pd,
+			   const state_transition *state_trans) const final override;
 
   program_point
   get_point_before_setjmp () const
diff --git a/gcc/analyzer/exploded-path.cc b/gcc/analyzer/exploded-path.cc
index b10761a9388..4f35de272e2 100644
--- a/gcc/analyzer/exploded-path.cc
+++ b/gcc/analyzer/exploded-path.cc
@@ -26,19 +26,30 @@  along with GCC; see the file COPYING3.  If not see
 
 namespace ana {
 
-/* class exploded_path.  */
+// struct diagnostic_state
 
-/* Copy ctor.  */
+void
+diagnostic_state::dump_to_pp (pretty_printer *pp) const
+{
+  pp_printf (pp, "%s: {", m_debug_desc.c_str ());
+  if (m_region_holding_value)
+    {
+      pp_string (pp, "region holding value: ");
+      m_region_holding_value->dump_to_pp (pp, false);
+    }
+  pp_string (pp, "}");
+}
 
-exploded_path::exploded_path (const exploded_path &other)
-: m_edges (other.m_edges.length ())
+void
+diagnostic_state::dump () const
 {
-  int i;
-  const exploded_edge *eedge;
-  FOR_EACH_VEC_ELT (other.m_edges, i, eedge)
-    m_edges.quick_push (eedge);
+  tree_dump_pretty_printer pp (stderr);
+  dump_to_pp (&pp);
+  pp_newline (&pp);
 }
 
+/* class exploded_path.  */
+
 /* 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.  */
@@ -47,31 +58,33 @@  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)
-    if (search_stmt->code == GIMPLE_PHI)
-      {
-	/* Each phis_for_edge_op instance handles multiple phi stmts
-	   at once, so we have to special-case the search for a phi stmt.  */
-	if (auto op = eedge->maybe_get_op ())
-	  if (auto phis_op = op->dyn_cast_phis_for_edge_op ())
-	    if (phis_op->defines_ssa_name_p (gimple_phi_result (search_stmt)))
+  for (int i = m_elements.size () - 1; i >= 0; --i)
+    {
+      const element_t *element = &m_elements[i];
+      const exploded_edge *eedge = element->m_eedge;
+      if (search_stmt->code == GIMPLE_PHI)
+	{
+	  /* Each phis_for_edge_op instance handles multiple phi stmts
+	     at once, so we have to special-case the search for a phi stmt.  */
+	  if (auto op = eedge->maybe_get_op ())
+	    if (auto phis_op = op->dyn_cast_phis_for_edge_op ())
+	      if (phis_op->defines_ssa_name_p (gimple_phi_result (search_stmt)))
+		{
+		  *out_idx = i;
+		  return true;
+		}
+	}
+      else
+	{
+	  /* Non-phi stmt.  */
+	  if (const gimple *stmt = eedge->maybe_get_stmt ())
+	    if (stmt == search_stmt)
 	      {
 		*out_idx = i;
 		return true;
 	      }
-      }
-    else
-      {
-	/* Non-phi stmt.  */
-	if (const gimple *stmt = eedge->maybe_get_stmt ())
-	  if (stmt == search_stmt)
-	    {
-	      *out_idx = i;
-	      return true;
-	    }
-      }
+	}
+    }
   return false;
 }
 
@@ -80,8 +93,8 @@  exploded_path::find_stmt_backwards (const gimple *search_stmt,
 exploded_node *
 exploded_path::get_final_enode () const
 {
-  gcc_assert (m_edges.length () > 0);
-  return m_edges[m_edges.length () - 1]->m_dest;
+  gcc_assert (m_elements.size () > 0);
+  return m_elements.back ().m_eedge->m_dest;
 }
 
 /* Check state along this path, returning true if it is feasible.
@@ -99,9 +112,9 @@  exploded_path::feasible_p (logger *logger,
 			   eg->get_supergraph ());
 
   /* Traverse the path, updating this state.  */
-  for (unsigned edge_idx = 0; edge_idx < m_edges.length (); edge_idx++)
+  for (unsigned edge_idx = 0; edge_idx < m_elements.size (); ++edge_idx)
     {
-      const exploded_edge *eedge = m_edges[edge_idx];
+      const exploded_edge *eedge = m_elements[edge_idx].m_eedge;
       if (logger)
 	logger->log ("considering edge %i: EN:%i -> EN:%i",
 		     edge_idx,
@@ -140,13 +153,20 @@  void
 exploded_path::dump_to_pp (pretty_printer *pp,
 			   const extrinsic_state *ext_state) const
 {
-  for (unsigned i = 0; i < m_edges.length (); i++)
+  for (unsigned i = 0; i < m_elements.size (); ++i)
     {
-      const exploded_edge *eedge = m_edges[i];
-      pp_printf (pp, "m_edges[%i]: EN %i -> EN %i",
+      const element_t &element = m_elements[i];
+      const exploded_edge *eedge = element.m_eedge;
+      pp_printf (pp, "m_elements[%i]: EN %i -> EN %i",
 		 i,
 		 eedge->m_src->m_index,
 		 eedge->m_dest->m_index);
+      if (element.m_state_transition)
+	{
+	  pp_string (pp, " {");
+	  element.m_state_transition->dump_to_pp (pp);
+	  pp_string (pp, "}");
+	}
       pp_newline (pp);
 
       if (ext_state)
@@ -188,6 +208,49 @@  exploded_path::dump_to_file (const char *filename,
   fclose (fp);
 }
 
+/* Print a multiline form of this path to LOGGER, prefixing it with DESC.  */
+
+void
+exploded_path::maybe_log (logger *logger, const char *desc) const
+{
+  if (!logger)
+    return;
+  logger->start_log_line ();
+  logger->log_partial ("%s: ", desc);
+  logger->end_log_line ();
+  for (unsigned idx = 0; idx < m_elements.size (); idx++)
+    {
+      const exploded_edge &eedge = *m_elements[idx].m_eedge;
+      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 ();
+
+      pretty_printer *pp = logger->get_printer ();
+      logger->start_log_line ();
+      pp_printf (pp, "  [%i] EN %i -> EN %i: ",
+		 idx,
+		 src_node->m_index,
+		 dst_node->m_index);
+      src_point.print (pp, format (false));
+      pp_string (pp, " -> ");
+      dst_point.print (pp, format (false));
+      if (auto state_trans = m_elements[idx].m_state_transition.get ())
+	{
+	  pp_string (pp, " {");
+	  state_trans->dump_to_pp (pp);
+	  pp_string (pp, "}");
+	}
+      logger->end_log_line ();
+    }
+}
+
+void
+exploded_path::reverse ()
+{
+  std::reverse (m_elements.begin (), m_elements.end ());
+}
+
 } // namespace ana
 
 #endif /* #if ENABLE_ANALYZER */
diff --git a/gcc/analyzer/exploded-path.h b/gcc/analyzer/exploded-path.h
index 8c5ce9766a1..9b3ae1c3cbf 100644
--- a/gcc/analyzer/exploded-path.h
+++ b/gcc/analyzer/exploded-path.h
@@ -22,6 +22,8 @@  along with GCC; see the file COPYING3.  If not see
 #define GCC_ANALYZER_EXPLODED_PATH_H
 
 #include "analyzer/exploded-graph.h"
+#include "analyzer/checker-event.h"
+#include "analyzer/state-transition.h"
 
 namespace ana {
 
@@ -30,10 +32,45 @@  namespace ana {
 class exploded_path
 {
 public:
-  exploded_path () : m_edges () {}
-  exploded_path (const exploded_path &other);
-
-  unsigned length () const { return m_edges.length (); }
+  struct element_t
+  {
+    element_t (const exploded_edge *eedge)
+    : m_eedge (eedge)
+    {
+    }
+    element_t (const element_t &other)
+    : m_eedge (other.m_eedge),
+      m_state_at_src (other.m_state_at_src),
+      m_state_at_dst (other.m_state_at_dst),
+      m_state_transition (nullptr)
+    {
+      if (other.m_state_transition)
+	m_state_transition = other.m_state_transition->clone ();
+    }
+
+    element_t (element_t &&other) = default;
+
+    element_t &operator= (const element_t &other)
+    {
+      m_eedge = other.m_eedge;
+      m_state_at_src = other.m_state_at_src;
+      m_state_at_dst = other.m_state_at_dst;
+      m_state_transition = (other.m_state_transition
+			    ? other.m_state_transition->clone ()
+			    : nullptr);
+      return *this;
+    }
+
+    const exploded_edge *m_eedge;
+    diagnostic_state m_state_at_src;
+    diagnostic_state m_state_at_dst;
+    std::unique_ptr<state_transition> m_state_transition;
+  };
+
+  exploded_path () = default;
+  exploded_path (const exploded_path &other) = default;
+
+  unsigned length () const { return m_elements.size (); }
 
   bool find_stmt_backwards (const gimple *search_stmt,
 			    int *out_idx) const;
@@ -47,10 +84,21 @@  public:
   void dump_to_file (const char *filename,
 		     const extrinsic_state &ext_state) const;
 
+  void maybe_log (logger *logger, const char *desc) const;
+
   bool feasible_p (logger *logger, std::unique_ptr<feasibility_problem> *out,
 		    engine *eng, const exploded_graph *eg) const;
 
-  auto_vec<const exploded_edge *> m_edges;
+  void
+  append_edge (const exploded_edge *edge)
+  {
+    m_elements.push_back (edge);
+  }
+
+  void
+  reverse ();
+
+  std::vector<element_t> m_elements;
 };
 
 /* Finding the shortest exploded_path within an exploded_graph.  */
diff --git a/gcc/analyzer/feasible-graph.cc b/gcc/analyzer/feasible-graph.cc
index b3097f189a1..37ba84d012c 100644
--- a/gcc/analyzer/feasible-graph.cc
+++ b/gcc/analyzer/feasible-graph.cc
@@ -187,12 +187,12 @@  feasible_graph::make_epath (feasible_node *fnode) const
       gcc_assert (fnode->m_preds.length () == 1);
       feasible_edge *pred_fedge
 	= static_cast <feasible_edge *> (fnode->m_preds[0]);
-      epath->m_edges.safe_push (pred_fedge->get_inner_edge ());
+      epath->m_elements.push_back (pred_fedge->get_inner_edge ());
       fnode = static_cast <feasible_node *> (pred_fedge->m_src);
     }
 
   /* Now reverse it.  */
-  epath->m_edges.reverse ();
+  epath->reverse ();
 
   return epath;
 }
diff --git a/gcc/analyzer/infinite-recursion.cc b/gcc/analyzer/infinite-recursion.cc
index c1dc6e49b9b..50da76c4a42 100644
--- a/gcc/analyzer/infinite-recursion.cc
+++ b/gcc/analyzer/infinite-recursion.cc
@@ -98,7 +98,8 @@  public:
 
   void
   add_function_entry_event (const exploded_edge &eedge,
-			    checker_path *emission_path) final override
+			    checker_path *emission_path,
+			    const state_transition_at_call *state_trans) final override
   {
     /* Subclass of function_entry_event for use when reporting both
        the initial and subsequent entries to the function of interest,
@@ -111,7 +112,9 @@  public:
 				      const program_state &dst_state,
 				      const infinite_recursion_diagnostic &pd,
 				      bool topmost)
-      : function_entry_event (dst_point, dst_state),
+      : function_entry_event (event_loc_info (dst_point),
+			      dst_state,
+			      nullptr),
 	m_pd (pd),
 	m_topmost (topmost)
       {
@@ -161,7 +164,8 @@  public:
 	(std::make_unique<recursive_function_entry_event>
 	 (dst_point, dst_node->get_state (), *this, true));
     else
-      pending_diagnostic::add_function_entry_event (eedge, emission_path);
+      pending_diagnostic::add_function_entry_event (eedge, emission_path,
+						    state_trans);
   }
 
   /* Customize the location where the warning_event appears, putting
diff --git a/gcc/analyzer/ops.cc b/gcc/analyzer/ops.cc
index b40e078c928..549a2e00cce 100644
--- a/gcc/analyzer/ops.cc
+++ b/gcc/analyzer/ops.cc
@@ -38,6 +38,8 @@  along with GCC; see the file COPYING3.  If not see
 #include "analyzer/call-summary.h"
 #include "analyzer/call-info.h"
 #include "analyzer/analysis-plan.h"
+#include "analyzer/callsite-expr.h"
+#include "analyzer/state-transition.h"
 
 #if ENABLE_ANALYZER
 
@@ -66,6 +68,25 @@  event_loc_info::event_loc_info (const program_point &point)
   m_depth = point.get_stack_depth ();
 }
 
+/* Make an event_loc_info suitable for a function_entry_event at POINT.
+   If STATE_TRANS is non-null, then try to extract the pertinent parameter
+   from it and use the location of that parameter, rather than that of the
+   function name.  */
+
+event_loc_info
+event_loc_info_for_function_entry (const program_point &point,
+				   const state_transition_at_call *state_trans)
+{
+  event_loc_info result (point);
+  if (state_trans)
+    {
+      callsite_expr expr = state_trans->get_callsite_expr ();
+      expr.maybe_get_param_location (point.get_fndecl (),
+				     &result.m_loc);
+    }
+  return result;
+}
+
 // struct operation_context
 
 void
@@ -190,6 +211,56 @@  private:
   } m_path_context;
 };
 
+// struct rewind_context
+
+void
+rewind_context::on_data_origin (tree dst_tree)
+{
+  gcc_assert (dst_tree);
+  const region_model *dst_enode_model
+    = m_eedge.m_dest->get_state ().m_region_model;
+  const region *dst_reg_in_dst_enode
+    = dst_enode_model->get_lvalue (dst_tree, nullptr);
+  if (m_input.m_region_holding_value == dst_reg_in_dst_enode)
+    {
+      if (m_logger)
+	m_logger->log ("data origin, into %qE", dst_tree);
+      m_output.m_region_holding_value = nullptr;
+      add_state_transition
+	(std::make_unique<state_transition_origin> (dst_tree));
+    }
+}
+
+void
+rewind_context::on_data_flow (tree src_tree, tree dst_tree)
+{
+  gcc_assert (src_tree);
+  gcc_assert (dst_tree);
+  const region_model *dst_enode_model
+    = m_eedge.m_dest->get_state ().m_region_model;
+  const region *dst_reg_in_dst_enode
+    = dst_enode_model->get_lvalue (dst_tree, nullptr);
+  if (m_input.m_region_holding_value == dst_reg_in_dst_enode)
+    {
+      if (m_logger)
+	m_logger->log ("rewinding from %qE to %qE", dst_tree, src_tree);
+      const region_model *src_enode_model
+	= m_eedge.m_src->get_state ().m_region_model;
+      const region *src_reg_in_src_enode
+	= src_enode_model->get_lvalue (src_tree, nullptr);
+      m_output.m_region_holding_value = src_reg_in_src_enode;
+
+      if (TREE_CODE (src_tree) == RESULT_DECL)
+	add_state_transition (std::make_unique<state_transition_at_return> ());
+      else if (auto state_trans
+		 = state_transition::make (m_output.m_region_holding_value,
+					   src_tree,
+					   m_input.m_region_holding_value,
+					   dst_tree))
+	add_state_transition (std::move (state_trans));
+    }
+}
+
 // class gimple_stmt_op : public operation
 
 void
@@ -663,8 +734,58 @@  gimple_stmt_op::add_any_events_for_eedge (const exploded_edge &eedge,
     }
 }
 
+// class gasm_op : public gimple_stmt_op
+
 // class gassign_op : public gimple_stmt_op
 
+bool
+gassign_op::try_to_rewind_data_flow (rewind_context &ctxt) const
+{
+  auto logger = ctxt.m_logger;
+  LOG_SCOPE (logger);
+  if (logger)
+    {
+      logger->start_log_line ();
+      pp_gimple_stmt_1 (logger->get_printer (), &get_stmt (), 0,
+			(dump_flags_t)0);
+      logger->end_log_line ();
+    }
+
+  const gassign &assign = get_gassign ();
+  tree lhs = gimple_assign_lhs (&assign);
+
+  if (!ctxt.could_be_affected_by_write_p (lhs))
+    return true;
+
+  tree rhs1 = gimple_assign_rhs1 (&assign);
+  enum tree_code op = gimple_assign_rhs_code (&assign);
+
+  switch (op)
+    {
+    default:
+      return false;
+
+    case NOP_EXPR:
+    case SSA_NAME:
+    case VAR_DECL:
+    case PARM_DECL:
+    case COMPONENT_REF:
+      ctxt.on_data_flow (rhs1, lhs);
+      break;
+
+    case INTEGER_CST:
+    case REAL_CST:
+      if (logger)
+	logger->log ("value comes from here");
+      ctxt.on_data_origin (lhs);
+      break;
+    }
+
+  return true;
+}
+
+// class predict_op : public gimple_stmt_op
+
 // class greturn_op : public gimple_stmt_op
 
 void
@@ -734,6 +855,24 @@  greturn_op::add_any_events_for_eedge (const exploded_edge &,
   // No-op.
 }
 
+
+bool
+greturn_op::try_to_rewind_data_flow (rewind_context &ctxt) const
+{
+  auto logger = ctxt.m_logger;
+  LOG_SCOPE (logger);
+
+  if (get_retval ())
+    {
+      const region_model *src_enode_model
+	= ctxt.m_eedge.m_src->get_state ().m_region_model;
+      tree fndecl = src_enode_model->get_current_function ()->decl;
+      ctxt.on_data_flow (get_retval (), DECL_RESULT (fndecl));
+    }
+
+  return true;
+}
+
 // class call_and_return_op : public gimple_stmt_op
 
 std::unique_ptr<operation>
@@ -839,7 +978,7 @@  call_and_return_op::execute (operation_context &op_ctxt) const
 	    const program_point dst_point
 	      (callee_entry_snode, *dst_call_string);
 	    auto edge_info
-	      = std::make_unique<interprocedural_call> (get_gcall (),
+	      = std::make_unique<interprocedural_call> (*this,
 							*callee_fun);
 	    edge_info->update_state (&dst_state, nullptr, &ctxt);
 	    op_ctxt.add_outcome (dst_point, dst_state, false, nullptr,
@@ -1023,6 +1162,13 @@  replay_call_summaries (operation_context &op_ctxt,
     }
 }
 
+bool
+call_and_return_op::try_to_rewind_data_flow (rewind_context &ctxt) const
+{
+  LOG_SCOPE (ctxt.m_logger);
+  return true;
+}
+
 /* A concrete call_info subclass representing a replay of a call summary.  */
 
 class call_summary_edge_info : public call_info
@@ -2350,6 +2496,16 @@  phis_for_edge_op::add_any_events_for_eedge (const exploded_edge &,
   // No-op
 }
 
+bool
+phis_for_edge_op::try_to_rewind_data_flow (rewind_context &ctxt) const
+{
+  auto logger = ctxt.m_logger;
+  LOG_SCOPE (logger);
+  for (auto iter : m_pairs)
+    ctxt.on_data_flow (iter.m_src, iter.m_dst);
+  return true;
+}
+
 // class resx_op : public gimple_stmt_op
 
 void
diff --git a/gcc/analyzer/ops.h b/gcc/analyzer/ops.h
index f1c67d4e771..f8a09c0b819 100644
--- a/gcc/analyzer/ops.h
+++ b/gcc/analyzer/ops.h
@@ -74,6 +74,36 @@  struct operation_context
   const superedge &m_sedge;
 };
 
+struct rewind_context
+{
+  rewind_context (const exploded_edge &eedge,
+		  logger *logger,
+		  diagnostic_state input_state)
+  : m_eedge (eedge),
+    m_logger (logger),
+    m_input (input_state),
+    m_output (input_state)
+  {
+  }
+
+  void
+  on_data_origin (tree dst);
+
+  void
+  on_data_flow (tree src, tree dst);
+
+  virtual bool
+  could_be_affected_by_write_p (tree lhs) = 0;
+
+  virtual void
+  add_state_transition (std::unique_ptr<state_transition>) = 0;
+
+  const exploded_edge &m_eedge;
+  logger *m_logger;
+  diagnostic_state m_input;
+  diagnostic_state m_output;
+};
+
 /* Abstract base class for an operation along a superedge.  */
 
 class operation
@@ -163,6 +193,12 @@  class operation
 
   enum kind get_kind () const { return m_kind; }
 
+  virtual bool
+  try_to_rewind_data_flow (rewind_context &) const
+  {
+    return false;
+  }
+
 protected:
   operation (enum kind kind_)
   : m_kind (kind_)
@@ -279,6 +315,9 @@  public:
   {
     return *as_a <const gassign *> (&get_stmt ());
   }
+
+  bool
+  try_to_rewind_data_flow (rewind_context &ctxt) const final override;
 };
 
 /* An operation subclass for a GIMPLE_PREDICT stmt.
@@ -300,6 +339,12 @@  public:
   {
     return std::make_unique<predict_op> (get_stmt ());
   }
+
+  bool
+  try_to_rewind_data_flow (rewind_context &) const final override
+  {
+    return true;
+  }
 };
 
 /* An operation subclass representing both:
@@ -350,6 +395,9 @@  public:
   {
     return gimple_return_retval (&get_greturn ());
   }
+
+  bool
+  try_to_rewind_data_flow (rewind_context &ctxt) const final override;
 };
 
 /* A concrete operation subclass representing the effect of a GIMPLE_CALL stmt.
@@ -428,6 +476,9 @@  public:
   const known_function *
   maybe_get_known_function (const call_details &cd) const;
 
+  bool
+  try_to_rewind_data_flow (rewind_context &ctxt) const final override;
+
 private:
   cgraph_edge *
   get_any_cgraph_edge (operation_context &op_ctxt) const;
@@ -638,6 +689,12 @@  public:
 
   const gimple &get_ctrlflow_stmt () const { return m_ctrlflow_stmt; }
 
+  bool
+  try_to_rewind_data_flow (rewind_context &) const final override
+  {
+    return true;
+  }
+
 protected:
   control_flow_op (enum kind kind_,
 		   ::edge cfg_edge,
@@ -983,6 +1040,9 @@  public:
   add_any_events_for_eedge (const exploded_edge &eedge,
 			    checker_path &out_path) const final override;
 
+  bool
+  try_to_rewind_data_flow (rewind_context &ctxt) const final override;
+
   const std::vector<pair> &get_pairs () const { return m_pairs; }
 
 private:
diff --git a/gcc/analyzer/pending-diagnostic.cc b/gcc/analyzer/pending-diagnostic.cc
index 2d90a91766f..778b5baec7a 100644
--- a/gcc/analyzer/pending-diagnostic.cc
+++ b/gcc/analyzer/pending-diagnostic.cc
@@ -58,6 +58,15 @@  interesting_t::add_region_creation (const region *reg)
   m_region_creation.safe_push (reg);
 }
 
+/* Mark the value read from REG as being interesting.  */
+
+void
+interesting_t::add_read_region (const region *reg, std::string debug_desc)
+{
+  gcc_assert (reg);
+  m_read_regions.push_back (diagnostic_state (std::move (debug_desc), reg));
+}
+
 void
 interesting_t::dump_to_pp (pretty_printer *pp, bool simple) const
 {
@@ -70,6 +79,14 @@  interesting_t::dump_to_pp (pretty_printer *pp, bool simple) const
 	pp_string (pp, ", ");
       reg->dump_to_pp (pp, simple);
     }
+  pp_string (pp, "], read regions: [");
+  for (i = 0; i < m_read_regions.size (); ++i)
+    {
+      auto &ann = m_read_regions[i];
+      if (i > 0)
+	pp_string (pp, ", ");
+      ann.dump_to_pp (pp);
+    }
   pp_string (pp, "]}");
 }
 
@@ -199,14 +216,21 @@  pending_diagnostic::fixup_location (location_t loc, bool) const
 
 void
 pending_diagnostic::add_function_entry_event (const exploded_edge &eedge,
-					      checker_path *emission_path)
+					      checker_path *emission_path,
+					      const state_transition_at_call *state_trans)
 {
   const exploded_node *dst_node = eedge.m_dest;
   const program_point &dst_point = dst_node->get_point ();
   const program_state &dst_state = dst_node->get_state ();
+
+  /* If we have STATE_TRANS with a specific param, put the event on
+     that parameter, otherwise put in on the function name.  */
+  auto loc_info {event_loc_info_for_function_entry (dst_point, state_trans)};
+
   emission_path->add_event
-    (std::make_unique<function_entry_event> (dst_point,
-					     dst_state));
+    (std::make_unique<function_entry_event> (loc_info,
+					     dst_state,
+					     state_trans));
 }
 
 /* Base implementation of pending_diagnostic::add_call_event.
@@ -215,11 +239,13 @@  pending_diagnostic::add_function_entry_event (const exploded_edge &eedge,
 void
 pending_diagnostic::add_call_event (const exploded_edge &eedge,
 				    const gcall &,
-				    checker_path &emission_path)
+				    checker_path &emission_path,
+				    const state_transition_at_call *state_trans)
 {
   emission_path.add_event
     (std::make_unique<call_event> (eedge,
-				   event_loc_info (eedge.m_src)));
+				   event_loc_info (eedge.m_src),
+				   state_trans));
 }
 
 /* Base implementation of pending_diagnostic::add_region_creation_events.
diff --git a/gcc/analyzer/pending-diagnostic.h b/gcc/analyzer/pending-diagnostic.h
index 4236acc780f..dccdb8a3f68 100644
--- a/gcc/analyzer/pending-diagnostic.h
+++ b/gcc/analyzer/pending-diagnostic.h
@@ -23,23 +23,31 @@  along with GCC; see the file COPYING3.  If not see
 
 #include "diagnostics/metadata.h"
 #include "analyzer/sm.h"
+#include "analyzer/state-transition.h"
 
 namespace ana {
 
 /* A bundle of information about things that are of interest to a
-   pending_diagnostic.
+   pending_diagnostic:
 
-   For now, merely the set of regions that are pertinent to the
+   * a set of regions that are pertinent to the
    diagnostic, so that we can notify the user about when they
-   were created.  */
+   were created.
+
+   * a set of regions that a pertinent value for the diagnostic was
+   read from, so that we can notify the user about where those values
+   came from.  */
 
 struct interesting_t
 {
   void add_region_creation (const region *reg);
 
+  void add_read_region (const region *reg, std::string debug_desc);
+
   void dump_to_pp (pretty_printer *pp, bool simple) const;
 
   auto_vec<const region *> m_region_creation;
+  std::vector<diagnostic_state> m_read_regions;
 };
 
 /* Various bundles of information used for generating more precise
@@ -74,23 +82,42 @@  struct state_change
   const state_change_event &m_event;
 };
 
+/* For use by pending_diagnostic::describe_origin_of_state.  */
+
+struct origin_of_state
+{
+  origin_of_state (tree dst_reg_expr)
+  : m_dst_reg_expr (dst_reg_expr)
+  {
+    gcc_assert (m_dst_reg_expr);
+  }
+
+  tree m_dst_reg_expr;
+};
+
 /* For use by pending_diagnostic::describe_call_with_state.  */
 
 struct call_with_state
 {
   call_with_state (tree caller_fndecl, tree callee_fndecl,
-		   tree expr, state_machine::state_t state)
+		   tree expr, state_machine::state_t state,
+		   const state_transition_at_call *state_trans)
   : m_caller_fndecl (caller_fndecl),
     m_callee_fndecl (callee_fndecl),
     m_expr (expr),
-    m_state (state)
+    m_state (state),
+    m_state_trans (state_trans)
   {
+    if (state_trans)
+      m_src_event_id = state_trans->get_src_event_id ();
   }
 
   tree m_caller_fndecl;
   tree m_callee_fndecl;
   tree m_expr;
   state_machine::state_t m_state;
+  const state_transition_at_call *m_state_trans;
+  diagnostics::paths::event_id_t m_src_event_id;
 };
 
 /* For use by pending_diagnostic::describe_return_of_state.  */
@@ -98,16 +125,58 @@  struct call_with_state
 struct return_of_state
 {
   return_of_state (tree caller_fndecl, tree callee_fndecl,
-		   state_machine::state_t state)
+		   state_machine::state_t state,
+		   const state_transition_at_return *state_trans)
   : m_caller_fndecl (caller_fndecl),
     m_callee_fndecl (callee_fndecl),
-    m_state (state)
+    m_state (state),
+    m_state_trans (state_trans)
   {
+    if (state_trans)
+      m_src_event_id = state_trans->get_src_event_id ();
   }
 
   tree m_caller_fndecl;
   tree m_callee_fndecl;
   state_machine::state_t m_state;
+  const state_transition_at_return *m_state_trans;
+  diagnostics::paths::event_id_t m_src_event_id;
+};
+
+/* For use by pending_diagnostic::describe_copy_of_state.  */
+
+struct copy_of_state
+{
+  copy_of_state (tree src_reg_expr,
+		 diagnostics::paths::event_id_t src_event_id,
+		 tree dst_reg_expr)
+  : m_src_reg_expr (src_reg_expr),
+    m_src_event_id (src_event_id),
+    m_dst_reg_expr (dst_reg_expr)
+  {
+    gcc_assert (m_src_reg_expr);
+    gcc_assert (m_dst_reg_expr);
+  }
+
+  tree m_src_reg_expr;
+  diagnostics::paths::event_id_t m_src_event_id;
+  tree m_dst_reg_expr;
+};
+
+/* For use by pending_diagnostic::describe_use_of_state.  */
+
+struct use_of_state
+{
+  use_of_state (tree src_reg_expr,
+		diagnostics::paths::event_id_t src_event_id)
+  : m_src_reg_expr (src_reg_expr),
+    m_src_event_id (src_event_id)
+  {
+    gcc_assert (m_src_reg_expr);
+  }
+
+  tree m_src_reg_expr;
+  diagnostics::paths::event_id_t m_src_event_id;
 };
 
 /* For use by pending_diagnostic::describe_final_event.  */
@@ -268,6 +337,20 @@  class pending_diagnostic
     return diagnostics::paths::event::meaning ();
   }
 
+  /* Precision-of-wording vfunc for use in describing state_transition_event
+     instances of state_transition::kind::origin.
+     Return true if a description of the event was printed to the
+     pretty-printer, or false otherwise.
+     For example, a divide-by-zero diagnostic might use:
+       "zero value originates here"
+     at the point where the zero comes from.  */
+  virtual bool describe_origin_of_state (pretty_printer &,
+					 const evdesc::origin_of_state &)
+  {
+    /* Default no-op implementation.  */
+    return false;
+  }
+
   /* Precision-of-wording vfunc for describing an interprocedural call
      carrying critial state for the diagnostic, from caller to callee.
 
@@ -299,6 +382,32 @@  class pending_diagnostic
     return false;
   }
 
+  /* Precision-of-wording vfunc for use in describing state_transition_event
+     instances of state_transition::kind::copy.
+     Return true if a description of the event was printed to the
+     pretty-printer, or false otherwise.
+     For example, a divide-by-zero diagnostic might use:
+     "copying zero value from (3) from 'x' to 'y'".  */
+  virtual bool describe_copy_of_state (pretty_printer &,
+				       const evdesc::copy_of_state &)
+  {
+    /* Default no-op implementation.  */
+    return false;
+  }
+
+  /* Precision-of-wording vfunc for use in describing state_transition_event
+     instances of state_transition::kind::use.
+     Return true if a description of the event was printed to the
+     pretty-printer, or false otherwise.
+     For example, a divide-by-zero diagnostic might use:
+     "using zero value from (7) from 'y'".  */
+  virtual bool describe_use_of_state (pretty_printer &,
+				      const evdesc::use_of_state &)
+  {
+    /* Default no-op implementation.  */
+    return false;
+  }
+
   /* Precision-of-wording vfunc for describing the final event within a
      diagnostic path.
 
@@ -322,7 +431,8 @@  class pending_diagnostic
 
   virtual void
   add_function_entry_event (const exploded_edge &eedge,
-			    checker_path *emission_path);
+			    checker_path *emission_path,
+			    const state_transition_at_call *state_trans);
 
   /* Vfunc for extending/overriding creation of the events for an
      exploded_edge, allowing for custom events to be created that are
@@ -343,7 +453,8 @@  class pending_diagnostic
      the variadic arguments.  */
   virtual void add_call_event (const exploded_edge &,
 			       const gcall &call_stmt,
-			       checker_path &emission_path);
+			       checker_path &emission_path,
+			       const state_transition_at_call *state_trans);
 
   /* Vfunc for adding any events for the creation of regions identified
      by the mark_interesting_stuff vfunc.
diff --git a/gcc/analyzer/poisoned-value-diagnostic.cc b/gcc/analyzer/poisoned-value-diagnostic.cc
index d29ceeaca63..e5d5cec7a35 100644
--- a/gcc/analyzer/poisoned-value-diagnostic.cc
+++ b/gcc/analyzer/poisoned-value-diagnostic.cc
@@ -155,7 +155,10 @@  public:
   void mark_interesting_stuff (interesting_t *interest) final override
   {
     if (m_src_region)
-      interest->add_region_creation (m_src_region);
+      {
+	interest->add_region_creation (m_src_region);
+	interest->add_read_region (m_src_region, "poisoned value");
+      }
   }
 
   /* Attempt to suppress false positives.
diff --git a/gcc/analyzer/region-model.cc b/gcc/analyzer/region-model.cc
index e4bafebbaa4..9555f72c307 100644
--- a/gcc/analyzer/region-model.cc
+++ b/gcc/analyzer/region-model.cc
@@ -69,6 +69,7 @@  along with GCC; see the file COPYING3.  If not see
 #include "analyzer/feasible-graph.h"
 #include "analyzer/record-layout.h"
 #include "analyzer/function-set.h"
+#include "analyzer/state-transition.h"
 
 #if ENABLE_ANALYZER
 
@@ -855,12 +856,57 @@  private:
   const region *m_base_reg_b;
 };
 
+/* Locate the parameter with the given index within FNDECL.
+   ARGNUM is zero based, -1 indicates the `this' argument of a method.
+   Return the location of the FNDECL itself if there are problems.  */
+
+bool
+callsite_expr::maybe_get_param_location (tree fndecl,
+					 location_t *out_loc) const
+{
+  gcc_assert (fndecl);
+
+  if (DECL_ARTIFICIAL (fndecl))
+    return false;
+
+  tree param = get_param_tree (fndecl);
+  if (!param)
+    return false;
+
+  *out_loc = DECL_SOURCE_LOCATION (param);
+  return true;
+}
+
+/* If this callsite_expr refers to a parameter, get the PARM_DECL from
+   FNDECL.
+   Return NULL_TREE on any problems.  */
+
+tree
+callsite_expr::get_param_tree (tree fndecl) const
+{
+  if (!param_p ())
+    return NULL_TREE;
+
+  int i;
+  tree param;
+
+  /* Locate param by index within DECL_ARGUMENTS (fndecl).  */
+  for (i = 1, param = DECL_ARGUMENTS (fndecl);
+       i < param_num () && param;
+       i++, param = TREE_CHAIN (param))
+    ;
+
+  return param;
+}
+
 class div_by_zero_diagnostic
 : public pending_diagnostic_subclass<div_by_zero_diagnostic>
 {
 public:
-  div_by_zero_diagnostic (const gassign *assign)
-  : m_assign (assign)
+  div_by_zero_diagnostic (const gassign *assign,
+			  const region *divisor_reg)
+  : m_assign (assign),
+    m_divisor_reg (divisor_reg)
   {}
 
   const char *get_kind () const final override
@@ -891,8 +937,142 @@  public:
     return true;
   }
 
+  void
+  mark_interesting_stuff (interesting_t *interest)
+  {
+    interest->add_read_region (m_divisor_reg, "divisor zero value");
+  }
+
+  void
+  add_function_entry_event (const exploded_edge &eedge,
+			    checker_path *emission_path,
+			    const state_transition_at_call *state_trans)
+  {
+    class custom_function_entry_event : public function_entry_event
+    {
+    public:
+      custom_function_entry_event (const event_loc_info &loc_info,
+				   const program_state &state,
+				   const state_transition_at_call *state_trans)
+      : function_entry_event (loc_info,
+			      state,
+			      state_trans)
+      {
+      }
+
+      void print_desc (pretty_printer &pp) const override
+      {
+	if (auto state_trans = get_state_transition_at_call ())
+	  {
+	    auto expr = state_trans->get_callsite_expr ();
+	    if (tree parm = expr.get_param_tree (m_effective_fndecl))
+	      {
+		auto src_event_id = state_trans->get_src_event_id ();
+		if (src_event_id.known_p ())
+		  pp_printf (&pp, "entry to %qE with zero from %@ for %qE",
+			     m_effective_fndecl,
+			     &src_event_id,
+			     parm);
+		else
+		  pp_printf (&pp, "entry to %qE with zero for %qE",
+			     m_effective_fndecl, parm);
+		return;
+	      }
+	  }
+	return function_entry_event::print_desc (pp);
+      }
+    };
+
+    const exploded_node *dst_node = eedge.m_dest;
+    const program_point &dst_point = dst_node->get_point ();
+    const program_state &dst_state = dst_node->get_state ();
+    auto loc_info {event_loc_info_for_function_entry (dst_point, state_trans)};
+    emission_path->add_event
+      (std::make_unique<custom_function_entry_event> (loc_info,
+						      dst_state,
+						      state_trans));
+  }
+
+  bool
+  describe_origin_of_state (pretty_printer &pp,
+			    const evdesc::origin_of_state &) final override
+  {
+    pp_printf (&pp, "zero value originates here");
+    return true;
+  }
+
+  bool
+  describe_call_with_state (pretty_printer &pp,
+			    const evdesc::call_with_state &evd) final override
+  {
+    if (evd.m_state_trans)
+      {
+	callsite_expr expr = evd.m_state_trans->get_callsite_expr ();
+	if (expr.param_p ())
+	  {
+	    if (evd.m_src_event_id.known_p ())
+	      pp_printf (&pp, "passing zero from %@ from %qE to %qE via parameter %i",
+			 &evd.m_src_event_id,
+			 evd.m_caller_fndecl,
+			 evd.m_callee_fndecl,
+			 expr.param_num ());
+	    else
+	      pp_printf (&pp, "passing zero from %qE to %qE via parameter %i",
+			 evd.m_caller_fndecl,
+			 evd.m_callee_fndecl,
+			 expr.param_num ());
+	    return true;
+	  }
+      }
+
+    return false;
+  }
+
+  bool
+  describe_return_of_state (pretty_printer &pp,
+			    const evdesc::return_of_state &evd) final override
+  {
+    if (evd.m_src_event_id.known_p ())
+      pp_printf (&pp, "returning zero from %@ from %qE here",
+		 &evd.m_src_event_id,
+		 evd.m_callee_fndecl);
+    else
+      pp_printf (&pp, "returning zero from %qE here",
+	       evd.m_callee_fndecl);
+    return true;
+  }
+
+  bool
+  describe_copy_of_state (pretty_printer &pp,
+			  const evdesc::copy_of_state &evd) final override
+  {
+    if (evd.m_src_event_id.known_p ())
+      pp_printf (&pp, "copying zero value from %@ from %qE to %qE",
+		 &evd.m_src_event_id,
+		 evd.m_src_reg_expr, evd.m_dst_reg_expr);
+    else
+      pp_printf (&pp, "copying zero value from %qE to %qE",
+		 evd.m_src_reg_expr, evd.m_dst_reg_expr);
+    return true;
+  }
+
+  bool
+  describe_use_of_state (pretty_printer &pp,
+			 const evdesc::use_of_state &evd) final override
+  {
+    if (evd.m_src_event_id.known_p ())
+      pp_printf (&pp, "using zero value from %@ from %qE",
+		 &evd.m_src_event_id,
+		 evd.m_src_reg_expr);
+    else
+      pp_printf (&pp, "using zero value from %qE",
+		 evd.m_src_reg_expr);
+    return true;
+  }
+
 private:
   const gassign *m_assign;
+  const region *m_divisor_reg;
 };
 
 /* Check the pointer subtraction SVAL_A - SVAL_B at ASSIGN and add
@@ -1101,15 +1281,26 @@  region_model::get_gassign_result (const gassign *assign,
 		  && INTEGRAL_TYPE_P (TREE_TYPE (rhs1)))
 		{
 		  if (tree_int_cst_sgn (rhs2_cst) < 0)
-		    ctxt->warn
-		      (make_shift_count_negative_diagnostic (assign, rhs2_cst));
+		    {
+		      const region *rhs2_reg
+			= get_lvalue (gimple_assign_rhs2 (assign), nullptr);
+		      ctxt->warn
+			(make_shift_count_negative_diagnostic (assign,
+							       rhs2_cst,
+							       rhs2_reg));
+		    }
 		  else if (compare_tree_int (rhs2_cst,
 					     TYPE_PRECISION (TREE_TYPE (rhs1)))
 			   >= 0)
-		    ctxt->warn (make_shift_count_overflow_diagnostic
-				(assign,
-				 int (TYPE_PRECISION (TREE_TYPE (rhs1))),
-				 rhs2_cst));
+		    {
+		      const region *rhs2_reg
+			= get_lvalue (gimple_assign_rhs2 (assign), nullptr);
+		      ctxt->warn (make_shift_count_overflow_diagnostic
+				  (assign,
+				   int (TYPE_PRECISION (TREE_TYPE (rhs1))),
+				   rhs2_cst,
+				   rhs2_reg));
+		    }
 		}
 	  }
 
@@ -1130,8 +1321,11 @@  region_model::get_gassign_result (const gassign *assign,
 		{
 		  if (ctxt)
 		    {
+		      const region *rhs2_reg
+			= get_lvalue (gimple_assign_rhs2 (assign), nullptr);
 		      ctxt->warn
-			(std::make_unique<div_by_zero_diagnostic> (assign));
+			(std::make_unique<div_by_zero_diagnostic> (assign,
+								   rhs2_reg));
 		      ctxt->terminate_path ();
 		    }
 		  return nullptr;
@@ -1935,7 +2129,8 @@  public:
   void
   add_events_to_path (checker_path *emission_path,
 		      const exploded_edge &eedge,
-		      pending_diagnostic &) const final override
+		      pending_diagnostic &,
+		      const state_transition *) const final override
   {
     const exploded_node *dst_node = eedge.m_dest;
     const program_point &dst_point = dst_node->get_point ();
diff --git a/gcc/analyzer/region-model.h b/gcc/analyzer/region-model.h
index 4b0160a98b0..fd947da65e8 100644
--- a/gcc/analyzer/region-model.h
+++ b/gcc/analyzer/region-model.h
@@ -1338,12 +1338,14 @@  make_poisoned_value_diagnostic (tree expr, enum poison_kind pkind,
 
 extern std::unique_ptr<pending_diagnostic>
 make_shift_count_negative_diagnostic (const gassign *assign,
-				      tree count_cst);
+				      tree count_cst,
+				      const region *src_region);
 
 extern std::unique_ptr<pending_diagnostic>
 make_shift_count_overflow_diagnostic (const gassign *assign,
 				      int operand_precision,
-				      tree count_cst);
+				      tree count_cst,
+				      const region *src_region);
 
 extern std::unique_ptr<pending_diagnostic>
 make_write_to_const_diagnostic (const region *dest_reg, tree decl);
diff --git a/gcc/analyzer/region.h b/gcc/analyzer/region.h
index 5c1d980017a..c852f7f51af 100644
--- a/gcc/analyzer/region.h
+++ b/gcc/analyzer/region.h
@@ -22,7 +22,8 @@  along with GCC; see the file COPYING3.  If not see
 #define GCC_ANALYZER_REGION_H
 
 #include "analyzer/symbol.h"
-#include "text-art/widget.h"
+#include "analyzer/store.h"
+#include "text-art/tree-widget.h"
 
 namespace ana {
 
diff --git a/gcc/analyzer/setjmp-longjmp.cc b/gcc/analyzer/setjmp-longjmp.cc
index 711dfb3e8d0..7fe0033095e 100644
--- a/gcc/analyzer/setjmp-longjmp.cc
+++ b/gcc/analyzer/setjmp-longjmp.cc
@@ -469,7 +469,8 @@  rewind_info_t::update_model (region_model *model,
 void
 rewind_info_t::add_events_to_path (checker_path *emission_path,
 				   const exploded_edge &eedge,
-				   pending_diagnostic &) const
+				   pending_diagnostic &,
+				   const state_transition *) const
 {
   const exploded_node *src_node = eedge.m_src;
   const program_point &src_point = src_node->get_point ();
diff --git a/gcc/analyzer/shift-diagnostics.cc b/gcc/analyzer/shift-diagnostics.cc
index 2bc58de248d..724c1a18d64 100644
--- a/gcc/analyzer/shift-diagnostics.cc
+++ b/gcc/analyzer/shift-diagnostics.cc
@@ -35,8 +35,9 @@  class shift_count_negative_diagnostic
 : public pending_diagnostic_subclass<shift_count_negative_diagnostic>
 {
 public:
-  shift_count_negative_diagnostic (const gassign *assign, tree count_cst)
-  : m_assign (assign), m_count_cst (count_cst)
+  shift_count_negative_diagnostic (const gassign *assign, tree count_cst,
+				   const region *src_region)
+  : m_assign (assign), m_count_cst (count_cst), m_src_region (src_region)
   {}
 
   const char *get_kind () const final override
@@ -70,15 +71,24 @@  public:
     return true;
   }
 
+  void
+  mark_interesting_stuff (interesting_t *interest)
+  {
+    interest->add_read_region (m_src_region, "shift count value");
+  }
+
 private:
   const gassign *m_assign;
   tree m_count_cst;
+  const region *m_src_region;
 };
 
 std::unique_ptr<pending_diagnostic>
-make_shift_count_negative_diagnostic (const gassign *assign, tree count_cst)
+make_shift_count_negative_diagnostic (const gassign *assign, tree count_cst,
+				      const region *src_region)
 {
-  return std::make_unique<shift_count_negative_diagnostic> (assign, count_cst);
+  return std::make_unique<shift_count_negative_diagnostic>
+    (assign, count_cst, src_region);
 }
 
 /* A subclass of pending_diagnostic for complaining about shifts
@@ -90,9 +100,11 @@  class shift_count_overflow_diagnostic
 public:
   shift_count_overflow_diagnostic (const gassign *assign,
 				   int operand_precision,
-				   tree count_cst)
+				   tree count_cst,
+				   const region *src_region)
   : m_assign (assign), m_operand_precision (operand_precision),
-    m_count_cst (count_cst)
+    m_count_cst (count_cst),
+    m_src_region (src_region)
   {}
 
   const char *get_kind () const final override
@@ -128,19 +140,27 @@  public:
     return true;
   }
 
+  void
+  mark_interesting_stuff (interesting_t *interest)
+  {
+    interest->add_read_region (m_src_region, "shift count value");
+  }
+
 private:
   const gassign *m_assign;
   int m_operand_precision;
   tree m_count_cst;
+  const region *m_src_region;
 };
 
 std::unique_ptr<pending_diagnostic>
 make_shift_count_overflow_diagnostic (const gassign *assign,
 				      int operand_precision,
-				      tree count_cst)
+				      tree count_cst,
+				      const region *src_region)
 {
   return std::make_unique<shift_count_overflow_diagnostic>
-    (assign, operand_precision, count_cst);
+    (assign, operand_precision, count_cst, src_region);
 }
 
 } // namespace ana
diff --git a/gcc/analyzer/sm-signal.cc b/gcc/analyzer/sm-signal.cc
index a348d36665f..08a0a2ad5eb 100644
--- a/gcc/analyzer/sm-signal.cc
+++ b/gcc/analyzer/sm-signal.cc
@@ -221,7 +221,8 @@  public:
 
   void add_events_to_path (checker_path *emission_path,
 			   const exploded_edge &eedge ATTRIBUTE_UNUSED,
-			   pending_diagnostic &)
+			   pending_diagnostic &,
+			   const state_transition *)
     const final override
   {
     emission_path->add_event
diff --git a/gcc/analyzer/state-transition.cc b/gcc/analyzer/state-transition.cc
new file mode 100644
index 00000000000..6c1d2837a0f
--- /dev/null
+++ b/gcc/analyzer/state-transition.cc
@@ -0,0 +1,184 @@ 
+/* Classes for tracking pertinent events that happen along
+   an execution path.
+   Copyright (C) 2026 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/>.  */
+
+#include "analyzer/common.h"
+
+#include "tree-diagnostic.h"
+
+#include "gimple-pretty-print.h"
+#include "gimple-iterator.h"
+#include "tree-cfg.h"
+#include "tree-dfa.h"
+#include "fold-const.h"
+#include "cgraph.h"
+#include "text-art/dump.h"
+#include "text-art/tree-widget.h"
+
+#include "analyzer/ops.h"
+#include "analyzer/call-details.h"
+#include "analyzer/exploded-graph.h"
+#include "analyzer/checker-path.h"
+#include "analyzer/impl-sm-context.h"
+#include "analyzer/constraint-manager.h"
+#include "analyzer/call-summary.h"
+#include "analyzer/call-info.h"
+#include "analyzer/analysis-plan.h"
+#include "analyzer/callsite-expr.h"
+#include "analyzer/state-transition.h"
+
+#if ENABLE_ANALYZER
+
+namespace ana {
+
+// class state_transition
+
+DEBUG_FUNCTION void
+state_transition::dump () const
+{
+  tree_dump_pretty_printer pp (stderr);
+  dump_to_pp (&pp);
+  pp_newline (&pp);
+}
+
+std::unique_ptr<state_transition>
+state_transition::make (const region *src_reg,
+			tree src_reg_expr,
+			const region *dst_reg,
+			tree dst_reg_expr)
+{
+  gcc_assert (src_reg != dst_reg);
+  gcc_assert (dst_reg);
+
+  if (!src_reg)
+    return std::make_unique<state_transition_origin> (dst_reg_expr);
+
+  if (src_reg->get_parent_region () ==  dst_reg->get_parent_region ())
+    if (tree src_decl =  src_reg->maybe_get_decl ())
+      if (tree dst_decl =  dst_reg->maybe_get_decl ())
+	{
+	  if (TREE_CODE (src_decl) == SSA_NAME
+	      && TREE_CODE (dst_decl) == SSA_NAME
+	      && SSA_NAME_VAR (src_decl)
+	      && SSA_NAME_VAR (src_decl) == SSA_NAME_VAR (dst_decl))
+	    {
+	      /* Avoid printing "copying value from 'y' to 'y'.  */
+	      return nullptr;
+	    }
+	}
+
+  if (printable_expr_p (src_reg_expr))
+    {
+      if (printable_expr_p (dst_reg_expr))
+	return std::make_unique<state_transition_copy> (src_reg_expr,
+							dst_reg_expr);
+      else
+	return std::make_unique<state_transition_use> (src_reg_expr);
+    }
+  else
+    return nullptr;
+}
+
+diagnostics::paths::event_id_t
+state_transition::get_src_event_id () const
+{
+  if (!m_prev_state_transition)
+    return diagnostics::paths::event_id_t ();
+  return m_prev_state_transition->m_event_id;
+}
+// class state_transition_origin : public state_transition
+
+std::unique_ptr<state_transition>
+state_transition_origin::clone () const
+{
+  return std::make_unique<state_transition_origin> (m_dst_reg_expr);
+}
+
+void
+state_transition_origin::dump_to_pp (pretty_printer *pp) const
+{
+  pp_printf (pp, "state_transition_origin (dst: %qE)",
+	     m_dst_reg_expr);
+}
+
+// class state_transition_at_call : public state_transition
+
+std::unique_ptr<state_transition>
+state_transition_at_call::clone () const
+{
+  return std::make_unique<state_transition_at_call> (m_expr);
+}
+
+void
+state_transition_at_call::dump_to_pp (pretty_printer *pp) const
+{
+  callsite_expr_element e (m_expr);
+  pp_printf (pp, "state_transition_at_call (callsite_expr: %e)", &e);
+}
+
+// class state_transition_at_return : public state_transition
+
+std::unique_ptr<state_transition>
+state_transition_at_return::clone () const
+{
+  return std::make_unique<state_transition_at_return> ();
+}
+
+void
+state_transition_at_return::dump_to_pp (pretty_printer *pp) const
+{
+  pp_printf (pp, "state_transition_at_return");
+}
+
+// class state_transition_copy : public state_transition
+
+std::unique_ptr<state_transition>
+state_transition_copy::clone () const
+{
+  return std::make_unique<state_transition_copy> (m_src_reg_expr,
+						  m_dst_reg_expr);
+}
+
+void
+state_transition_copy::dump_to_pp (pretty_printer *pp) const
+{
+  pp_printf (pp, "state_transition_copy (src: %qE, dst: %qE)",
+	     m_src_reg_expr,
+	     m_dst_reg_expr);
+}
+
+// class state_transition_use : public state_transition
+
+std::unique_ptr<state_transition>
+state_transition_use::clone () const
+{
+  return std::make_unique<state_transition_use> (m_src_reg_expr);
+}
+
+void
+state_transition_use::dump_to_pp (pretty_printer *pp) const
+{
+  pp_printf (pp, "state_transition_use (src: %qE)",
+	     m_src_reg_expr);
+}
+
+} // namespace ana
+
+#endif /* #if ENABLE_ANALYZER */
diff --git a/gcc/analyzer/state-transition.h b/gcc/analyzer/state-transition.h
new file mode 100644
index 00000000000..4615be7a839
--- /dev/null
+++ b/gcc/analyzer/state-transition.h
@@ -0,0 +1,191 @@ 
+/* Classes for tracking pertinent events that happen along
+   an execution path.
+   Copyright (C) 2026 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_STATE_TRANSITION_H
+#define GCC_ANALYZER_STATE_TRANSITION_H
+
+#include "diagnostics/event-id.h"
+#include "analyzer/callsite-expr.h"
+
+namespace ana {
+
+class state_transition
+{
+public:
+  enum class kind
+  {
+    origin,
+    at_call,
+    at_return,
+    copy,
+    use
+  };
+
+  state_transition ()
+  : m_prev_state_transition (nullptr)
+  {
+  }
+
+  virtual ~state_transition () {}
+
+  virtual std::unique_ptr<state_transition>
+  clone () const = 0;
+
+  virtual void
+  dump_to_pp (pretty_printer *pp) const = 0;
+
+  virtual enum kind get_kind () const = 0;
+
+  virtual const state_transition_at_call *
+  dyn_cast_state_transition_at_call () const { return nullptr; }
+
+  virtual const state_transition_at_return *
+  dyn_cast_state_transition_at_return () const { return nullptr; }
+
+  void dump () const;
+
+  static std::unique_ptr<state_transition>
+  make (const region *src_reg,
+	tree src_reg_expr,
+	const region *dst_reg,
+	tree dst_reg_expr);
+
+  diagnostics::paths::event_id_t
+  get_src_event_id () const;
+
+  state_transition *m_prev_state_transition;
+  diagnostics::paths::event_id_t m_event_id;
+};
+
+class state_transition_origin : public state_transition
+{
+public:
+  state_transition_origin (tree dst_reg_expr)
+  : m_dst_reg_expr (dst_reg_expr)
+  {
+  }
+
+  std::unique_ptr<state_transition>
+  clone () const final override;
+
+  void
+  dump_to_pp (pretty_printer *pp) const final override;
+
+  enum kind
+  get_kind () const final override { return kind::origin; }
+
+  tree m_dst_reg_expr;
+};
+
+class state_transition_at_call : public state_transition
+{
+public:
+  state_transition_at_call (callsite_expr expr)
+  : m_expr (expr)
+  {
+  }
+
+  std::unique_ptr<state_transition>
+  clone () const final override;
+
+  void
+  dump_to_pp (pretty_printer *pp) const final override;
+
+  enum kind
+  get_kind () const final override { return kind::at_call; }
+
+  const state_transition_at_call *
+  dyn_cast_state_transition_at_call () const final override { return this; }
+
+  callsite_expr
+  get_callsite_expr () const { return m_expr; }
+
+private:
+  callsite_expr m_expr;
+};
+
+class state_transition_at_return : public state_transition
+{
+public:
+  std::unique_ptr<state_transition>
+  clone () const final override;
+
+  void
+  dump_to_pp (pretty_printer *pp) const final override;
+
+  enum kind
+  get_kind () const final override { return kind::at_return; }
+
+  const state_transition_at_return *
+  dyn_cast_state_transition_at_return () const final override { return this; }
+};
+
+class state_transition_copy : public state_transition
+{
+public:
+  state_transition_copy (tree src_reg_expr,
+			 tree dst_reg_expr)
+  : m_src_reg_expr (src_reg_expr),
+    m_dst_reg_expr (dst_reg_expr)
+  {
+    gcc_assert (m_src_reg_expr);
+    gcc_assert (printable_expr_p (m_src_reg_expr));
+
+    gcc_assert (m_dst_reg_expr);
+    gcc_assert (printable_expr_p (m_dst_reg_expr));
+  }
+
+  std::unique_ptr<state_transition>
+  clone () const final override;
+
+  void
+  dump_to_pp (pretty_printer *pp) const final override;
+
+  enum kind
+  get_kind () const final override { return kind::copy; }
+
+  tree m_src_reg_expr;
+  tree m_dst_reg_expr;
+};
+
+class state_transition_use : public state_transition
+{
+public:
+  state_transition_use (tree src_reg_expr)
+  : m_src_reg_expr (src_reg_expr)
+  {
+  }
+
+  std::unique_ptr<state_transition>
+  clone () const final override;
+
+  void
+  dump_to_pp (pretty_printer *pp) const final override;
+
+  enum kind
+  get_kind () const final override { return kind::use; }
+
+  tree m_src_reg_expr;
+};
+
+} // namespace ana
+
+#endif /* GCC_ANALYZER_STATE_TRANSITION_H */
diff --git a/gcc/analyzer/supergraph.h b/gcc/analyzer/supergraph.h
index 5f3c9684ff0..c90dedbaff5 100644
--- a/gcc/analyzer/supergraph.h
+++ b/gcc/analyzer/supergraph.h
@@ -323,40 +323,6 @@  private:
   ::edge m_cfg_edge;
 };
 
-/* An ID representing an expression at a callsite:
-   either a parameter index, or the return value (or unknown).  */
-
-class callsite_expr
-{
- public:
-  callsite_expr () : m_val (-1) {}
-
-  static callsite_expr from_zero_based_param (int idx)
-  {
-    return callsite_expr (idx + 1);
-  }
-
-  static callsite_expr from_return_value ()
-  {
-    return callsite_expr (0);
-  }
-
-  bool param_p () const
-  {
-    return m_val > 0;
-  }
-
-  bool return_value_p () const
-  {
-    return m_val == 0;
-  }
-
- private:
-  callsite_expr (int val) : m_val (val) {}
-
-  int m_val; /* 1-based parm, 0 for return value, or -1 for "unknown".  */
-};
-
 /* Base class for adding additional content to the .dot output
    for a supergraph.  */
 
diff --git a/gcc/analyzer/varargs.cc b/gcc/analyzer/varargs.cc
index 1a1d3565d55..d2dd8934c61 100644
--- a/gcc/analyzer/varargs.cc
+++ b/gcc/analyzer/varargs.cc
@@ -775,7 +775,8 @@  public:
      adding a custom call_event subclass.  */
   void add_call_event (const exploded_edge &eedge,
 		       const gcall &call_stmt,
-		       checker_path &emission_path) override
+		       checker_path &emission_path,
+		       const state_transition_at_call *state_trans) override
   {
     /* As per call_event, but show the number of variadic arguments
        in the call.  */
@@ -785,7 +786,7 @@  public:
       va_arg_call_event (const exploded_edge &eedge,
 			 const event_loc_info &loc_info,
 			 int num_variadic_arguments)
-      : call_event (eedge, loc_info),
+      : call_event (eedge, loc_info, nullptr),
 	m_num_variadic_arguments (num_variadic_arguments)
       {
       }
@@ -819,7 +820,8 @@  public:
 	    num_variadic_arguments));
       }
     else
-      pending_diagnostic::add_call_event (eedge, call_stmt, emission_path);
+      pending_diagnostic::add_call_event (eedge, call_stmt, emission_path,
+					  state_trans);
   }
 
 protected:
diff --git a/gcc/digraph.cc b/gcc/digraph.cc
index 724d541e68f..fc015b8330b 100644
--- a/gcc/digraph.cc
+++ b/gcc/digraph.cc
@@ -96,6 +96,9 @@  struct test_cluster : public cluster<test_graph_traits>
 
 struct test_path
 {
+  void append_edge (const test_edge *edge) { m_edges.safe_push (edge); }
+  void reverse () { m_edges.reverse (); }
+
   auto_vec<const test_edge *> m_edges;
 };
 
diff --git a/gcc/shortest-paths.h b/gcc/shortest-paths.h
index dd0d4d2a193..5351b7e3737 100644
--- a/gcc/shortest-paths.h
+++ b/gcc/shortest-paths.h
@@ -187,7 +187,7 @@  get_shortest_path (const node_t *other_node) const
 
   while (m_best_edge[other_node->m_index])
     {
-      result.m_edges.safe_push (m_best_edge[other_node->m_index]);
+      result.append_edge (m_best_edge[other_node->m_index]);
       if (m_sense == SPS_FROM_GIVEN_ORIGIN)
 	other_node = m_best_edge[other_node->m_index]->m_src;
       else
@@ -195,7 +195,7 @@  get_shortest_path (const node_t *other_node) const
     }
 
   if (m_sense == SPS_FROM_GIVEN_ORIGIN)
-    result.m_edges.reverse ();
+    result.reverse ();
 
   return result;
 }
diff --git a/gcc/testsuite/c-c++-common/analyzer/divide-by-zero-1.c b/gcc/testsuite/c-c++-common/analyzer/divide-by-zero-1.c
index 0d0b8e01157..ff4f788f8d7 100644
--- a/gcc/testsuite/c-c++-common/analyzer/divide-by-zero-1.c
+++ b/gcc/testsuite/c-c++-common/analyzer/divide-by-zero-1.c
@@ -3,7 +3,7 @@ 
 static int __attribute__((noipa))
 return_zero (void)
 {
-  return 0;
+  return 0; /* { dg-message "value originates here" } */
 }
 
 void
diff --git a/gcc/testsuite/c-c++-common/analyzer/divide-by-zero-2.c b/gcc/testsuite/c-c++-common/analyzer/divide-by-zero-2.c
new file mode 100644
index 00000000000..75a25a669a6
--- /dev/null
+++ b/gcc/testsuite/c-c++-common/analyzer/divide-by-zero-2.c
@@ -0,0 +1,14 @@ 
+/* { dg-additional-options "-fno-analyzer-state-merge" } */
+
+extern int
+get_value (void);
+
+int
+test (int flag)
+{
+  int x = 42;
+  int y = 0; /* { dg-message "value originates here" } */
+  if (flag)
+    y = get_value ();
+  return x / y; /* { dg-warning "division by zero" } */
+}
diff --git a/gcc/testsuite/c-c++-common/analyzer/divide-by-zero-3.c b/gcc/testsuite/c-c++-common/analyzer/divide-by-zero-3.c
new file mode 100644
index 00000000000..ecbef931111
--- /dev/null
+++ b/gcc/testsuite/c-c++-common/analyzer/divide-by-zero-3.c
@@ -0,0 +1,19 @@ 
+/* { dg-additional-options "-fno-analyzer-state-merge" } */
+
+extern int
+get_value (void);
+
+int
+test (int flag, int flag_2, int flag_3)
+{
+  int x = 42; /* { dg-bogus "value originates here" } */
+  int y = 10; /* { dg-bogus "value originates here" } */
+  int z = 0; /* { dg-message "value originates here" } */
+  if (flag)
+    y = get_value ();
+  if (flag_2)
+    z = get_value ();
+  if (flag_3)
+    y = z;
+  return x / y; /* { dg-warning "division by zero" } */
+}
diff --git a/gcc/testsuite/c-c++-common/analyzer/invalid-shift-1.c b/gcc/testsuite/c-c++-common/analyzer/invalid-shift-1.c
index 08e52728748..f8a3ed67383 100644
--- a/gcc/testsuite/c-c++-common/analyzer/invalid-shift-1.c
+++ b/gcc/testsuite/c-c++-common/analyzer/invalid-shift-1.c
@@ -27,8 +27,10 @@  f2 (void)
   f1 (_dl_hwcaps_subdirs_build_bitmask (33, 31));
 }
 
-static int __attribute__((noinline)) op3 (int op, int c) { return op << c; } /* { dg-message "shift by negative count \\('-1'\\)" } */
-int test_3 (void) { return op3 (1, -1); }
+static int __attribute__((noinline)) op3 (int op, int c) { return op << c; } /* { dg-message "55: entry to 'op3' with problematic value for 'c'" } */
+/* { dg-message "shift by negative count \\('-1'\\)" "" { target *-*-* } .-1 } */
+
+int test_3 (void) { return op3 (1, -1); } /* { dg-message "passing problematic value from 'test_3' to 'op3' via parameter 2" } */
 
 static int __attribute__((noinline)) op4 (int op, int c) { return op << c; }
 int test_4 (void) { return op4 (1, 0); }
diff --git a/gcc/testsuite/c-c++-common/analyzer/invalid-shift-2.c b/gcc/testsuite/c-c++-common/analyzer/invalid-shift-2.c
new file mode 100644
index 00000000000..19f8c4da407
--- /dev/null
+++ b/gcc/testsuite/c-c++-common/analyzer/invalid-shift-2.c
@@ -0,0 +1,11 @@ 
+unsigned char
+do_shift (unsigned char val, int bits) /* { dg-message "34: entry to 'do_shift' with problematic value for 'bits'" } */
+{
+  return val << bits; /* { dg-warning "Wanalyzer-shift-count-overflow" } */
+}
+
+int
+test (unsigned char ch)
+{
+  return do_shift (ch, 1000); /* { dg-message "passing problematic value from 'test' to 'do_shift' via parameter 2" } */
+}
diff --git a/gcc/testsuite/c-c++-common/analyzer/invalid-shift-3.c b/gcc/testsuite/c-c++-common/analyzer/invalid-shift-3.c
new file mode 100644
index 00000000000..b0435a8b087
--- /dev/null
+++ b/gcc/testsuite/c-c++-common/analyzer/invalid-shift-3.c
@@ -0,0 +1,12 @@ 
+unsigned char
+do_shift (unsigned char val, int bits) /* { dg-message "34: entry to 'do_shift' with problematic value from \\\(2\\\) for 'bits'" } */
+{
+  return val << bits; /* { dg-warning "Wanalyzer-shift-count-overflow" } */
+}
+
+int
+test (unsigned char ch)
+{
+  int bits = 1000; /* { dg-message "\\\(2\\\) value originates here" } */
+  return do_shift (ch, bits); /* { dg-message "\\\(3\\\) passing problematic value from \\\(2\\\) from 'test' to 'do_shift' via parameter 2" } */
+}
diff --git a/gcc/testsuite/c-c++-common/analyzer/invalid-shift-4.c b/gcc/testsuite/c-c++-common/analyzer/invalid-shift-4.c
new file mode 100644
index 00000000000..3dcfedbe95d
--- /dev/null
+++ b/gcc/testsuite/c-c++-common/analyzer/invalid-shift-4.c
@@ -0,0 +1,12 @@ 
+unsigned char
+do_shift (unsigned char val, int bits) /* { dg-message "34: entry to 'do_shift' with problematic value from \\\(2\\\) for 'bits'" } */
+{
+  return val << bits; /* { dg-warning "Wanalyzer-shift-count-negative" } */
+}
+
+int
+test (unsigned char ch)
+{
+  int bits = -1; /* { dg-message "\\\(2\\\) value originates here" } */
+  return do_shift (ch, bits); /* { dg-message "\\\(3\\\) passing problematic value from \\\(2\\\) from 'test' to 'do_shift' via parameter 2" } */
+}
diff --git a/gcc/testsuite/g++.dg/analyzer/divide-by-zero-7.C b/gcc/testsuite/g++.dg/analyzer/divide-by-zero-7.C
new file mode 100644
index 00000000000..3196a4f5731
--- /dev/null
+++ b/gcc/testsuite/g++.dg/analyzer/divide-by-zero-7.C
@@ -0,0 +1,28 @@ 
+/* { dg-additional-options "-fno-analyzer-state-merge" } */
+
+// TODO: we shouldn't need this:
+/* { dg-additional-options "-fno-analyzer-state-purge" } */
+
+struct foo
+{
+  foo (int x_, int y_)
+  : x (x_), y (y_) // TODO: should show event here
+  {
+  }
+
+  int divide () const
+  {
+    return x / y; /* { dg-message "using zero value from '\\*this\\.foo::y'" } */
+    /* { dg-warning "division by zero" "" { target *-*-* } .-1 } */
+  }
+
+  int x;
+  int y;
+};
+
+int
+test ()
+{
+  foo f (5, 0); // TODO: should show "origin of zero" event here
+  return f.divide ();
+}
diff --git a/gcc/testsuite/gcc.dg/analyzer/divide-by-zero-4.c b/gcc/testsuite/gcc.dg/analyzer/divide-by-zero-4.c
new file mode 100644
index 00000000000..b685ba1d91b
--- /dev/null
+++ b/gcc/testsuite/gcc.dg/analyzer/divide-by-zero-4.c
@@ -0,0 +1,39 @@ 
+/* { dg-additional-options "-fno-analyzer-state-merge" } */
+
+/* TODO: we shouldn't need this:  */
+/* { dg-additional-options "-fno-analyzer-state-purge" } */
+
+int
+get_zero (void)
+{
+  return 0; /* { dg-message "\\\(6\\\) zero value originates here" } */
+}
+
+struct foo { int x; int y; };
+
+void
+init_foo (struct foo *f, int x, int y) /* { dg-message "\\\(9\\\) entry to 'init_foo' with zero from \\\(7\\\) for 'y'" } */
+{
+  f->x = x;
+  f->y = y; /* { dg-message "\\\(10\\\) copying zero value from \\\(9\\\) from 'y' to '\\*f\\.y'" } */
+}
+
+int
+do_divide (struct foo *f)
+{
+  return f->x / f->y; /* { dg-message "using zero value from \\\(10\\\) from '\\*f\\.y'" } */
+  /* { dg-warning "division by zero" "" { target *-*-* } .-1 } */
+}
+
+int
+test (int flag, int flag_2, int flag_3)
+{
+  struct foo f;
+  int a = 42;
+  int b = 10;
+  if (flag)
+    b = get_zero ();
+    /* { dg-message "\\\(7\\\) returning zero from \\\(6\\\) from 'get_zero' here" "" { target *-*-* } .-1 } */
+  init_foo (&f, a, b); /* { dg-message "passing zero from \\\(7\\\) from 'test' to 'init_foo' via parameter 3" } */
+  return do_divide (&f);
+}
diff --git a/gcc/testsuite/gcc.dg/analyzer/divide-by-zero-5.c b/gcc/testsuite/gcc.dg/analyzer/divide-by-zero-5.c
new file mode 100644
index 00000000000..96c9d01700d
--- /dev/null
+++ b/gcc/testsuite/gcc.dg/analyzer/divide-by-zero-5.c
@@ -0,0 +1,42 @@ 
+/* { dg-additional-options "-fno-analyzer-state-merge" } */
+
+/* TODO: we shouldn't need this:  */
+/* { dg-additional-options "-fno-analyzer-state-purge" } */
+
+int
+maybe_get_zero (int flag)
+{
+  if (flag)
+    return 0; /* { dg-message "zero value originates here" } */
+  else
+    return 42;
+}
+
+struct foo { int x; int y; };
+
+void
+init_foo (struct foo *f, int x, int y) /* { dg-message "\\\(11\\\) entry to 'init_foo' with zero from \\\(9\\\) for 'y'" } */
+{
+  f->x = x;
+  f->y = y; /* { dg-message "\\\(12\\\) copying zero value from \\\(11\\\) from 'y' to '\\*f\\.y'" } */
+}
+
+int
+do_divide (struct foo *f)
+{
+  return f->x / f->y; /* { dg-message "using zero value from \\\(12\\\) from '\\*f\\.y'" } */
+  /* { dg-warning "division by zero" "" { target *-*-* } .-1 } */
+}
+
+int
+test (int flag, int flag_2, int flag_3)
+{
+  struct foo f;
+  int a = 42;
+  int b = 10;
+  if (flag)
+    b = maybe_get_zero (flag_2); /* { dg-bogus "value of 'b' unchanged here" } */
+    /* { dg-message "returning zero from \\\(8\\\) from 'maybe_get_zero' here" "" { target *-*-* } .-1 } */
+  init_foo (&f, a, b); /* { dg-message "passing zero from \\\(9\\\) from 'test' to 'init_foo' via parameter 3" } */
+  return do_divide (&f);
+}
diff --git a/gcc/testsuite/gcc.dg/analyzer/divide-by-zero-6.c b/gcc/testsuite/gcc.dg/analyzer/divide-by-zero-6.c
new file mode 100644
index 00000000000..5fd8539f109
--- /dev/null
+++ b/gcc/testsuite/gcc.dg/analyzer/divide-by-zero-6.c
@@ -0,0 +1,27 @@ 
+/* { dg-additional-options "-fno-analyzer-state-merge" } */
+
+/* TODO: we shouldn't need this:  */
+/* { dg-additional-options "-fno-analyzer-state-purge" } */
+
+struct foo { int x; int y; };
+
+void
+init_foo (struct foo *f)
+{
+  __builtin_memset (f, 0, sizeof (f));
+}
+
+int
+do_divide (struct foo *f)
+{
+  return f->x / f->y; /* { dg-message "using zero value from '\\*f\\.y'" } */
+  /* { dg-warning "division by zero" "" { target *-*-* } .-1 } */
+}
+
+int
+test (int flag, int flag_2, int flag_3)
+{
+  struct foo f;
+  init_foo (&f);
+  return do_divide (&f);
+}
diff --git a/gcc/testsuite/gcc.dg/analyzer/divide-by-zero-float.c b/gcc/testsuite/gcc.dg/analyzer/divide-by-zero-float.c
index 3aaee568bc1..940c5725169 100644
--- a/gcc/testsuite/gcc.dg/analyzer/divide-by-zero-float.c
+++ b/gcc/testsuite/gcc.dg/analyzer/divide-by-zero-float.c
@@ -7,7 +7,7 @@  test_1 ()
 static float __attribute__((noinline))
 get_zero ()
 {
-  return 0.f;
+  return 0.f; /* { dg-message "value originates here" } */
 }
 
 float
diff --git a/gcc/tree-diagnostic.h b/gcc/tree-diagnostic.h
index d844e752c4b..1556c1de595 100644
--- a/gcc/tree-diagnostic.h
+++ b/gcc/tree-diagnostic.h
@@ -71,7 +71,8 @@  public:
   }
   ~tree_dump_pretty_printer ()
   {
-    pp_flush (this);
+    if (pp_buffer (this)->m_stream)
+      pp_flush (this);
   }
 };