From: David Malcolm Date: Mon, 23 Jun 2025 15:06:43 +0000 (-0400) Subject: diagnostics: add state diagrams to analyzer experimental-html output [PR116792] X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=2334d30cd8feac28831ffef857cd09753a3ca3d3;p=thirdparty%2Fgcc.git diagnostics: add state diagrams to analyzer experimental-html output [PR116792] This patch adds various support for debugging diagnostic paths and events, intended initially for myself to help with debugging -fanalyzer. It adds the optional ability for a diagnostic_event to supply a description of the predicted state of the program at that point along the diagnostic_path. To isolate the diagnostic subsystem from the analyzer, this representation is currently an xml::document with custom elements. The XML representation is similar to the analyzer's internal state but can be easier to read - for example, rather than storing the contents of memory via byte offsets, it uses fields for structs and element indexes for arrays, recursively. These states are handled by the HTML and SARIF diagnostic sinks. The SARIF sink simply embeds the XML as a string in a property bag of the threadFlowLocation object (SARIF v2.1.0 section 3.38). For HTML output, the "experimental-html" sink gains a new "show-state-diagrams=yes" option i.e.: -fdiagnostics-add-output=experimental-html:show-state-diagrams=yes which converts the state XML into SVG diagrams visualizing the state of memory at each event, inspired by the "ddd" debugger. These can be seen by pressing 'j' and 'k' to single-step forward and backward through events, making it *much* easier to debug -fanalyzer. An example of output can be seen here: https://dmalcolm.fedorapeople.org/gcc/2025-06-23/state-diagram-1.c.html showing an issue in a singly-linked list; there are various other examples in the parent directory. Generating the SVG diagrams requires an invocation of "dot" per event, so it noticeable slows down diagnostic emission, hence the opt-in command-line flag. However, I'm already finding bugs in -fanalyzer with this that I hadn't seen before. Given that the UI is rather clunky and there is lots of room for improvement to the visualizations, for now this feature is marked as being for GCC developers, not end-users. The patch also adds a dot::ast_node class hierarachy to make it easy to create GraphViz dot files with the correct escaping, and adds a C++ wrapper around pex adding some syntactic sugar for invoking subprocesses. gcc/ChangeLog: PR other/116792 * Makefile.in (ANALYZER_OBJS): Add analyzer/ana-state-to-diagnostic-state.o. (OBJS): Move graphviz.o to... (OBJS-libcommon): ...here. Add diagnostic-state-to-dot.o and pex.o. * diagnostic-format-html.cc: Include "diagnostic-state.h" and "graphviz.h". (html_generation_options::html_generation_options): Initialize the new flags. (HTML_SCRIPT): Add function "get_any_state_diagram". Use it when changing current focus id to update the visibility of the pertinent diagram, if any. (print_pre_source): New. (html_builder::maybe_make_state_diagram): New. (html_path_label_writer::html_path_label_writer): Add "path" param. Initialize m_path and m_curr_event_id. (html_path_label_writer::begin_label): Store current event id. (html_path_label_writer::end_label): Attempt to make a state diagram and add it if successful. (html_path_label_writer::get_element_id): New. (html_path_label_writer::m_path): New field. (html_path_label_writer::m_curr_event_id): New field. (html_builder::make_element_for_diagnostic): Pass path to label writer. * diagnostic-format-html.h (html_generation_options::m_show_state_diagrams): New field. (html_generation_options::m_show_state_diagram_xml): New field. (html_generation_options::m_show_state_diagram_dot_src): New field. * diagnostic-format-sarif.cc: Include "xml.h". (populate_thread_flow_location_object): If requested, attempt to generate xml state and add it to the proeprty bag as "gcc/diagnostic_event/xml_state" in xml source form. (sarif_generation_options::sarif_generation_options): Initialize m_xml_state. * diagnostic-format-sarif.h (sarif_generation_options::m_xml_state): New field. * diagnostic-path.cc: Define INCLUDE_MAP. Include "xml.h". (diagnostic_event::maybe_make_xml_state): New. * diagnostic-path.h (class xml::document): New forward decl. (diagnostic_event::maybe_make_xml_state): New vfunc decl. * diagnostic-state-to-dot.cc: New file. * diagnostic-state.h: New file. * digraph.cc: Define INCLUDE_STRING and INCLUDE_VECTOR. * doc/analyzer.texi: Document state diagrams in html output. (__analyzer_dump_dot): New. (__analyzer_dump_xml): New. * doc/invoke.texi (sarif): Add "xml-state" key. (experimental-html): Add keys "show-state-diagrams", "show-state-diagrams-dot-src" and "show-state-diagrams-xml". * graphviz.cc: Define INCLUDE_MAP, INCLUDE_STRING, and INCLUDE_VECTOR. Include "xml.h", "xml-printer.h", "pex.h" and "selftest.h". (graphviz_out::graphviz_out): Extract... (dot::writer::writer): ...this. (graphviz_out::write_indent): Convert to... (dot::writer::write_indent): ...this. (graphviz_out::print): Use get_pp. (graphviz_out::println): Likewise. (graphviz_out::begin_tr): Likewise. (graphviz_out::end_tr): Likewise. (graphviz_out::begin_td): Likewise. (graphviz_out::end_td): Likewise. (graphviz_out::begin_trtd): Likewise. (graphviz_out::end_tdtr): Likewise. (dot::ast_node::dump): New. (dot::id::id): New. (dot::id::print): New. (dot::id::is_identifier_p): New. (dot::kv_pair::print): New. (dot::attr_list::print): New. (dot::stmt_list::print): New. (dot::stmt_list::add_edge): New. (dot::stmt_list::add_attr): New. (dot::graph::print): New. (dot::stmt_with_attr_list::set_label): New. (dot::node_stmt::print): New. (dot::attr_stmt::print): New. (dot::kv_stmt::print): New. (dot::node_id::print): New. (dot::port::print): New. (dot::edge_stmt::print): New. (dot::subgraph::print): New. (dot::make_svg_document_buffer_from_graph): New. (dot::make_svg_from_graph): New. (selftest:test_ids): New. (selftest:test_trivial_graph): New. (selftest:test_layout_example): New. (selftest:graphviz_cc_tests): New. * graphviz.h (xml::node): New forward decl. (class graphviz_out): Split out into... (class dot::writer): ...this new class (struct dot::ast_node): New. (struct dot::id): New. (struct dot::kv_pair): New. (struct dot::attr_list): New. (struct dot::stmt_list): New. (struct dot::graph): New. (struct dot::stmt): New. (struct dot::stmt_with_attr_list): New. (struct dot::node_stmt): New. (struct dot::attr_stmt): New. (struct dot::kv_stmt): New. (enum class dot::compass_pt): New. (struct dot::port): New. (struct dot::node_id): New. (struct dot::edge_stmt): New. (struct dot::subgraph): New. (dot::make_svg_from_graph): New. * opts-diagnostic.cc (sarif_scheme_handler::make_sink): Add "xml-state" flag. (html_scheme_handler::make_sink): Add flags "show-state-diagrams", "show-state-diagram-dot-src", and "show-state-diagram-xml". * pex.cc: New file. * pex.h: New file. * selftest-run-tests.cc (selftest::run_tests): Call graphviz_cc_tests. * selftest.h (selftest::graphviz_cc_tests): New decl. * xml.cc (xml::node_with_children::add_comment): New. (xml::node_with_children::find_child_element): New. (xml::element::get_attr): New. (xml::comment::write_as_xml): New. (selftest::test_printer): Add coverage of find_child_element and get_attr. (selftest::test_comment): New. (selftest::xml_cc_tests): Call test_comment. * xml.h: New forward decls. (xml::node::dyn_cast_text): Use nullptr. (xml::node::dyn_cast_element): New vfunc. (xml::node_with_children::add_comment): New decl. (xml::node_with_children::find_child_element): New decl. (xml::element::dyn_cast_element): New vfunc impl. (xml::element::get_attr): New decl. (struct xml::comment): New xml::node subclass. gcc/analyzer/ChangeLog: PR other/116792 * ana-state-to-diagnostic-state.cc: New file. * ana-state-to-diagnostic-state.h: New file. * checker-event.cc: Include "xml.h". (checker_event::checker_event): Initialize m_path. (checker_event::prepare_for_emission): Store the path pointer into m_path. (checker_event::maybe_make_xml_state): New. (function_entry_event::function_entry_event): Add "state" param and use it to initialize m_state. (superedge_event::get_program_state): New. (call_event::get_program_state): New. (warning_event::get_program_state): New. * checker-event.h (checker_event::get_program_state): New vfunc. (checker_event::maybe_make_xml_state): New decl. (checker_event::m_path): New field. (statement_event::get_program_state): New vfunc impl. (function_entry_event::function_entry_event): Add "state" param. (function_entry_event::get_program_state): New vfunc impl. (function_entry_event::m_state): New field. (state_change_event::get_program_state): New vfunc impl. (superedge_event::get_program_state): New vfunc decl. (warning_event::warning_event): Add "program_state_" param and copy it. (warning_event::get_program_state): New vfunc decl. (warning_event::m_program_state): New field. * checker-path.h (checker_path::checker_path): Add ext_state param. (checker_path::get_ext_state): New accessor. (checker_path::m_ext_state): New field. * common.h: Define INCLUDE_MAP and INCLUDE_STRING. * diagnostic-manager.cc (saved_diagnostic::operator==): Don't deduplicate dump_path_diagnostic instances. (diagnostic_manager::emit_saved_diagnostic): Pass ext_state to checker_path ctor. * engine.cc: (impl_region_model_context::on_state_leak): Pass old and new state to state_machine::on_leak. (exploded_node::on_stmt_pre): Implement __analyzer_dump_xml and __analyzer_dump_dot. * exploded-graph.h (impl_region_model_context::get_state): New. * infinite-recursion.cc (recursive_function_entry_event::recursive_function_entry_event): Add "dst_state" param and pass to function_entry_event ctor. (infinite_recursion_diagnostic::add_function_entry_event): Pass state to event ctor. * kf-analyzer.cc: Include "analyzer/program-state.h" (dump_path_diagnostic::dump_path_diagnostic): Add "state" param. (dump_path_diagnostic::get_final_state): New. (dump_path_diagnostic::m_state): New field. (kf_analyzer_dump_path::impl_call_pre): Pass state to warning. * pending-diagnostic.cc (pending_diagnostic::add_function_entry_event): Pass state to function_entry_event. (pending_diagnostic::add_final_event): Likewise to warning_event. * pending-diagnostic.h (pending_diagnostic::get_final_state): New vfunc decl. * program-state.cc: Include "diagnostic-state.h", "graphviz.h" and "analyzer/ana-state-to-diagnostic-state.h". (program_state::dump_dot): New. * program-state.h: Include "text-art/tree-widget.h" and "analyzer/store.h". (class xml::document): New forward decl. (make_xml): New. (dump_xml_to_pp): New. (dump_xml_to_file): New. (dump_xml): New. (dump_dot): New. * record-layout.cc (record_layout::record_layout): Make param const_tree. * record-layout.h (item::item): Likewise. (item::m_field): Likewise. (record_layout::record_layout): Likewise. (record_layout::begin): New. (record_layout::end): New. * region-model.cc (exposure_through_uninit_copy::complain_about_fully_uninit_item): Use const_tree. (exposure_through_uninit_copy::complain_about_partially_uninit_item): Likewise. * region-model.h (region_model_context::get_state): New vfunc. (noop_region_model_context::get_state): New. (region_model_context_decorator::get_state): New. * sm-fd.cc (fd_leak::fd_leak): Add "final_state" param and capture it if present. (fd_leak::get_final_state): New. (fd_leak::m_final_state): New. (fd_state_machine::on_open): Pass nullptr for new "final_state" param. (fd_state_machine::on_creat): Likewise. (fd_state_machine::on_socket): Likewise. (fd_state_machine::on_accept): Likewise. (fd_state_machine::on_leak): Add state params and pass new state as final state to fd_leak ctor. * sm-file.cc: Include "analyzer/program-state.h". (file_leak::file_leak): Add "final_state" param and capture it if present. (file_leak::get_final_state): New. (file_leak::m_final_state): New. (fileptr_state_machine::on_leak): Add state params and pass new state as final state to fd_leak ctor. * sm-malloc.cc: Include "analyzer/ana-state-to-diagnostic-state.h". (malloc_leak::malloc_leak): Add "final_state" param and use it. (malloc_leak::get_final_state): New vfunc impl. (malloc_leak::m_final_state): New field. (malloc_state_machine::on_leak): Add state params; capture final state. (malloc_state_machine::add_state_to_xml): New. * sm.cc (state_machine::on_leak): Add "old_state" and "new_state" params. Use nullptr. (state_machine::add_state_to_xml): New. (state_machine::add_global_state_to_xml): New. * sm.h (class xml_state): New forward decl. (state_machine::on_leak): Add state params. (state_machine::add_state_to_xml): New vfunc decl. (state_machine::add_global_state_to_xml): New vfunc decl. * store.h (bit_range::operator<): New. * varargs.cc (va_list_leak::va_list_leak): Add final_state param and capture it if non-null. (va_list_leak::get_final_state): New. (va_list_leak::m_final_state): New. (va_list_state_machine::on_leak): Add state params and pass final state to va_list_leak ctor. gcc/testsuite/ChangeLog: PR other/116792 * g++.dg/analyzer/state-diagram.C: New test. * gcc.dg/analyzer/analyzer-decls.h (__analyzer_dump_dot): New decl. (__analyzer_dump_xml): New decl. * gcc.dg/analyzer/state-diagram-1-sarif.py: New test script. * gcc.dg/analyzer/state-diagram-1.c: New test. * gcc.dg/analyzer/state-diagram-2.c: New test. * gcc.dg/analyzer/state-diagram-3.c: New test. * gcc.dg/analyzer/state-diagram-4.c: New test. * gcc.dg/analyzer/state-diagram-5-html.py: New test script. * gcc.dg/analyzer/state-diagram-5-sarif.py: New test script. * gcc.dg/analyzer/state-diagram-5.c: New test. * gcc.dg/plugin/analyzer_cpython_plugin.cc: Define INCLUDE_STRING. * gcc.dg/plugin/analyzer_gil_plugin.cc: Likewise. * gcc.dg/plugin/analyzer_kernel_plugin.cc: Likewise. * gcc.dg/plugin/analyzer_known_fns_plugin.cc: Likewise. * lib/htmltest.py (ns): Add SVG namespace. * lib/sarif.py (get_result_by_index): New. (get_xml_state): New. Signed-off-by: David Malcolm --- diff --git a/gcc/Makefile.in b/gcc/Makefile.in index 56fa5ac25ff..f106e833425 100644 --- a/gcc/Makefile.in +++ b/gcc/Makefile.in @@ -1325,6 +1325,7 @@ C_COMMON_OBJS = c-family/c-common.o c-family/c-cppbuiltin.o c-family/c-dump.o \ # Analyzer object files ANALYZER_OBJS = \ analyzer/access-diagram.o \ + analyzer/ana-state-to-diagnostic-state.o \ analyzer/analysis-plan.o \ analyzer/analyzer.o \ analyzer/analyzer-language.o \ @@ -1537,7 +1538,6 @@ OBJS = \ godump.o \ graph.o \ graphds.o \ - graphviz.o \ graphite.o \ graphite-isl-ast-to-gimple.o \ graphite-dependences.o \ @@ -1861,7 +1861,9 @@ OBJS-libcommon = diagnostic-spec.o diagnostic.o diagnostic-color.o \ diagnostic-path.o \ diagnostic-path-output.o \ diagnostic-show-locus.o \ + diagnostic-state-to-dot.o \ edit-context.o \ + graphviz.o pex.o \ pretty-print.o intl.o \ json.o json-parsing.o \ xml.o \ diff --git a/gcc/analyzer/ana-state-to-diagnostic-state.cc b/gcc/analyzer/ana-state-to-diagnostic-state.cc new file mode 100644 index 00000000000..b85a2f1b242 --- /dev/null +++ b/gcc/analyzer/ana-state-to-diagnostic-state.cc @@ -0,0 +1,786 @@ +/* Converting ana::program_state to XML state documents. + Copyright (C) 2025 Free Software Foundation, Inc. + Contributed by David Malcolm . + +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 +. */ + +#define INCLUDE_ALGORITHM +#define INCLUDE_MAP +#define INCLUDE_SET +#include "analyzer/common.h" + +#include "xml.h" +#include "xml-printer.h" + +#include "analyzer/region-model.h" +#include "analyzer/program-state.h" +#include "analyzer/record-layout.h" +#include "analyzer/ana-state-to-diagnostic-state.h" + +#if ENABLE_ANALYZER + +#if __GNUC__ >= 10 +#pragma GCC diagnostic ignored "-Wformat" +#endif + +namespace ana { + +static void +set_wi_attr (xml::element &e, + const char *attr_name, + const wide_int_ref &w, + signop sgn) +{ + pretty_printer pp; + pp_wide_int (&pp, w, sgn); + e.set_attr (attr_name, pp_formatted_text (&pp)); +} + +static void +set_type_attr (xml::element &e, const_tree type) +{ + gcc_assert (type); + pretty_printer pp; + pp_format_decoder (&pp) = default_tree_printer; + pp_printf (&pp, "%T", type); + e.set_attr ("type", pp_formatted_text (&pp)); +} + +static void +set_bits_attr (xml::element &e, + bit_range bits) +{ + pretty_printer pp; + bits.dump_to_pp (&pp); + e.set_attr ("bits", pp_formatted_text (&pp)); +} + +static void +set_region_id_attr (xml::element &e, + const region ®) +{ + e.set_attr ("region_id", std::to_string (reg.get_id ())); +} + +// class xml_state : public xml::document + +xml_state::xml_state (const program_state &state, + const extrinsic_state &ext_state) +: xml::document (), + m_state (state), + m_ext_state (ext_state), + m_mgr (*ext_state.get_engine ()->get_model_manager ()), + m_root (nullptr) +{ + auto root = std::make_unique ("state-diagram", false); + m_root = root.get (); + add_child (std::move (root)); + + /* Find pointers to heap-allocated regions, and record their types, + so that we have a user-friendly way of showing the memory + (by field, rather than by byte offset). */ + for (auto cluster_iter : *state.m_region_model->get_store ()) + for (auto binding_iter : *cluster_iter.second) + { + const svalue *svalue = binding_iter.second; + if (const region *reg = svalue->maybe_get_region ()) + if (svalue->get_type () && !reg->get_type ()) + { + tree pointed_to_type = TREE_TYPE (svalue->get_type ()); + if (!VOID_TYPE_P (pointed_to_type)) + m_types_for_untyped_regions[reg] = pointed_to_type; + } + } + + /* TODO: look for vtable pointers at the top of dynamically-allocated + regions and use that type as a fallback. */ + + /* Find regions of interest. + Create elements per region, and build into hierarchy. + Add edges for pointers. */ + + /* Create stack, from top to bottom. */ + for (int i = state.m_region_model->get_stack_depth () - 1; i >= 0; --i) + { + const frame_region *reg = state.m_region_model->get_frame_at_index (i); + get_or_create_element (*reg); + } + + /* Create bound memory. */ + for (auto iter : *state.m_region_model->get_store ()) + { + const bool create_all = false; // "true" for verbose, for debugging + create_elements_for_binding_cluster (*iter.second, create_all); + } + + /* TODO: Constraints. */ + + /* Annotate with information from state machines. */ + { + int i; + sm_state_map *smap; + FOR_EACH_VEC_ELT (state.m_checker_states, i, smap) + { + auto &sm = ext_state.get_sm (i); + for (const auto &iter : *smap) + sm.add_state_to_xml (*this, *iter.first, iter.second.m_state); + if (auto s = smap->get_global_state ()) + sm.add_global_state_to_xml (*this, s); + } + } +} + +xml::element & +xml_state::get_or_create_element (const region ®) +{ + auto existing = m_region_to_element_map.find (®); + if (existing != m_region_to_element_map.end ()) + return *existing->second; + + auto &e = create_and_add_element (reg); + m_region_to_element_map[®] = &e; + return e; +} + +xml::element& +xml_state::create_and_add_element (const region ®) +{ + auto e = create_element (reg); + xml::element &result = *e; + if (auto parent_reg = reg.get_parent_region ()) + { + auto parent_element = &get_or_create_element (*parent_reg); + parent_element->add_child (std::move (e)); + } + else + m_root->add_child (std::move (e)); + return result; +} + +std::unique_ptr +xml_state::make_memory_space_element (const char *label) +{ + auto e = std::make_unique ("memory-space", false); + e->set_attr ("label", label); + return e; +} + +std::unique_ptr +xml_state::create_element (const region ®) +{ + std::unique_ptr e; + switch (reg.get_kind ()) + { + default: + gcc_unreachable (); + + case RK_FRAME: + { + e = std::make_unique ("stack-frame", false); + const frame_region &frame_reg + = static_cast (reg); + { + pretty_printer pp; + pp_format_decoder (&pp) = default_tree_printer; + pp_printf (&pp, "%E", frame_reg.get_fndecl ()); + e->set_attr ("function", pp_formatted_text (&pp)); + } + } + break; + case RK_GLOBALS: + e = make_memory_space_element ("Globals"); + break; + case RK_CODE: + e = make_memory_space_element ("Code"); + break; + case RK_FUNCTION: + e = std::make_unique ("function", false); + // TODO + break; + case RK_LABEL: + e = std::make_unique ("label", false); + // TODO + break; + case RK_STACK: + e = std::make_unique ("stack", false); + break; + case RK_HEAP: + e = make_memory_space_element ("Heap"); + break; + case RK_THREAD_LOCAL: + e = make_memory_space_element ("Thread-local"); + break; + case RK_ROOT: + e = std::make_unique ("memory-regions", false); + break; + case RK_SYMBOLIC: + e = std::make_unique ("symbolic-region", false); + // TODO + break; + case RK_DECL: + { + e = std::make_unique ("variable", false); + const decl_region &decl_reg + = static_cast (reg); + { + pretty_printer pp; + pp_format_decoder (&pp) = default_tree_printer; + pp_printf (&pp, "%E", decl_reg.get_decl ()); + e->set_attr ("name", pp_formatted_text (&pp)); + } + set_type_attr (*e, TREE_TYPE (decl_reg.get_decl ())); + } + break; + case RK_FIELD: + e = std::make_unique ("field", false); + break; + case RK_ELEMENT: + e = std::make_unique ("element", false); + break; + case RK_OFFSET: + e = std::make_unique ("offset-region", false); + // TODO + break; + case RK_SIZED: + e = std::make_unique ("sized-region", false); + // TODO + break; + case RK_CAST: + e = std::make_unique ("cast-region", false); + // TODO + break; + case RK_HEAP_ALLOCATED: + e = std::make_unique ("heap-buffer", false); + set_attr_for_dynamic_extents (reg, *e); + break; + case RK_ALLOCA: + e = std::make_unique ("alloca-buffer", false); + set_attr_for_dynamic_extents (reg, *e); + break; + case RK_STRING: + e = std::make_unique ("string-region", false); + // TODO + break; + case RK_BIT_RANGE: + e = std::make_unique ("RK_BIT_RANGE", false); // TODO + break; + case RK_VAR_ARG: + e = std::make_unique ("RK_VAR_ARG", false); // TODO + break; + case RK_ERRNO: + e = std::make_unique ("errno", false); + break; + case RK_PRIVATE: + e = std::make_unique ("RK_PRIVATE", false); // TODO + break; + case RK_UNKNOWN: + e = std::make_unique ("RK_UNKNOWN", false); // TODO + break; + } + gcc_assert (e); + + set_region_id_attr (*e, reg); + + if (reg.get_base_region () == ®) + if (!reg.get_type ()) + { + auto search + = m_types_for_untyped_regions.find (®); + if (search != m_types_for_untyped_regions.end ()) + { + tree type_to_use = search->second; + set_type_attr (*e, type_to_use); + } + } + + return e; +} + +void +xml_state::create_elements_for_binding_cluster (const binding_cluster &cluster, + bool create_all) +{ + /* TODO: + - symbolic bindings + - get current svalue, so as to get "zeros" and "uninitialized". */ + + concrete_bindings_t conc_bindings; + for (auto iter : cluster) + { + const binding_key *key = iter.first; + const svalue *svalue = iter.second; + if (auto conc_key = key->dyn_cast_concrete_binding ()) + conc_bindings[conc_key->get_bit_range ()] = svalue; + if (const region *reg = svalue->maybe_get_region ()) + get_or_create_element (*reg); + } + + auto &e = get_or_create_element (*cluster.get_base_region ()); + + e.add_child (create_element_for_conc_bindings (conc_bindings)); + + const region *typed_reg = cluster.get_base_region (); + if (!typed_reg->get_type ()) + { + auto search + = m_types_for_untyped_regions.find (cluster.get_base_region ()); + if (search != m_types_for_untyped_regions.end ()) + { + tree type_to_use = search->second; + typed_reg = m_mgr.get_cast_region (typed_reg, type_to_use); + } + } + + if (typed_reg->get_type ()) + populate_element_for_typed_region (e, + *typed_reg, + conc_bindings, + create_all); + else + { + // TODO + } +} + +std::unique_ptr +xml_state::create_element_for_conc_bindings (const concrete_bindings_t &conc_bindings) +{ + auto e = std::make_unique ("concrete-bindings", false); + for (auto iter : conc_bindings) + { + const bit_range bits = iter.first; + const svalue *sval = iter.second; + auto binding_element + = std::make_unique ("binding", false); + set_bits_attr (*binding_element, bits); + { + pretty_printer pp; + pp_format_decoder (&pp) = default_tree_printer; + sval->dump_to_pp (&pp, true); + binding_element->set_attr ("value", pp_formatted_text (&pp)); + if (auto svalue_element = create_element_for_svalue (sval)) + binding_element->add_child (std::move (svalue_element)); + } + e->add_child (std::move (binding_element)); + } + return e; +} + +// Try to get the bit_range of REG within its base region +bool +xml_state::get_bit_range_within_base_region (const region ®, + bit_range &out) +{ + region_offset start_offset = reg.get_offset (&m_mgr); + if (!start_offset.concrete_p ()) + return false; + region_offset next_offset = reg.get_next_offset (&m_mgr); + if (!next_offset.concrete_p ()) + return false; + out = bit_range (start_offset.get_bit_offset (), + next_offset.get_bit_offset () + - start_offset.get_bit_offset ()); + return true; +} + +void +xml_state::populate_element_for_typed_region (xml::element &e, + const region ®, + const concrete_bindings_t &conc_bindings, + bool create_all) +{ + const_tree reg_type = reg.get_type (); + gcc_assert (reg_type); + set_type_attr (e, reg_type); + + bit_range bits (0, 0); + if (get_bit_range_within_base_region (reg, bits)) + { + set_bits_attr (e, bits); + + auto search = conc_bindings.find (bits); + if (search != conc_bindings.end ()) + { + const svalue *bound_sval = search->second; + if (auto svalue_element = create_element_for_svalue (bound_sval)) + { + xml::printer xp (e); + xp.push_tag ("value-of-region"); + xp.append (std::move (svalue_element)); + } + } + } + + switch (TREE_CODE (reg_type)) + { + default: + break; + + case ARRAY_TYPE: + { + tree domain = TYPE_DOMAIN (reg_type); + if (!domain) + return; + const_tree max_idx = TYPE_MAX_VALUE (domain); + if (!max_idx) + return; + if (TREE_CODE (max_idx) != INTEGER_CST) + return; + const_tree min_idx = TYPE_MIN_VALUE (domain); + if (TREE_CODE (min_idx) != INTEGER_CST) + return; + for (offset_int idx = wi::to_offset (min_idx); + idx <= wi::to_offset (max_idx); + ++idx) + { + const_tree element_type = TREE_TYPE (reg_type); + const svalue *sval_index + = m_mgr.get_or_create_int_cst (domain, idx); + const region *child_reg + = m_mgr.get_element_region (®, + const_cast (element_type), + sval_index); + if (show_child_element_for_child_region_p (*child_reg, + conc_bindings, + create_all)) + { + // Here "element" is in the xml sense + auto child_element + = std::make_unique ("element", false); + set_wi_attr (*child_element, "index", idx, UNSIGNED); + set_region_id_attr (*child_element, *child_reg); + // Recurse: + gcc_assert (element_type); + populate_element_for_typed_region (*child_element, + *child_reg, + conc_bindings, + create_all); + e.add_child (std::move (child_element)); + } + } + } + break; + + case RECORD_TYPE: + { + const record_layout layout (reg_type); + for (auto item : layout) + { + if (item.m_is_padding) + { + const bit_range bits (0, item.m_bit_range.m_size_in_bits); + const region *child_reg + = m_mgr.get_bit_range (®, NULL_TREE, bits); + if (show_child_element_for_child_region_p (*child_reg, + conc_bindings, + create_all)) + { + auto child_element + = std::make_unique ("padding", false); + set_wi_attr (*child_element, "num_bits", + item.m_bit_range.m_size_in_bits, SIGNED); + e.add_child (std::move (child_element)); + } + } + else + { + const region *child_reg + = m_mgr.get_field_region (®, + const_cast (item.m_field)); + if (show_child_element_for_child_region_p (*child_reg, + conc_bindings, + create_all)) + { + auto child_element + = std::make_unique ("field", false); + { + pretty_printer pp; + pp_format_decoder (&pp) = default_tree_printer; + pp_printf (&pp, "%D", item.m_field); + child_element->set_attr ("name", + pp_formatted_text (&pp)); + } + set_region_id_attr (*child_element, *child_reg); + // Recurse: + populate_element_for_typed_region (*child_element, + *child_reg, + conc_bindings, + create_all); + e.add_child (std::move (child_element)); + } + } + } + } + break; + } +} + +void +xml_state::set_attr_for_dynamic_extents (const region ®, xml::element &e) +{ + const svalue *sval = m_state.m_region_model->get_dynamic_extents (®); + if (sval) + { + pretty_printer pp; + pp_format_decoder (&pp) = default_tree_printer; + if (auto cst = sval->maybe_get_constant ()) + pp_wide_int (&pp, wi::to_wide (cst), UNSIGNED); + else + sval->dump_to_pp (&pp, true); + e.set_attr ("dynamic-extents", pp_formatted_text (&pp)); + } +} + +bool +xml_state:: +show_child_element_for_child_region_p (const region ®, + const concrete_bindings_t &conc_bindings, + bool create_all) +{ + if (create_all) + return true; + bit_range reg_bits (0, 0); + if (!get_bit_range_within_base_region (reg, reg_bits)) + return true; + + /* Is any of "bits" bound? + TODO: ideally there would be a more efficient way to do this, using + spatial relationships. */ + for (auto iter : conc_bindings) + { + const bit_range bound_bits = iter.first; + if (bound_bits.intersects_p (reg_bits)) + return true; + } + return false; +} + +std::unique_ptr +xml_state::create_element_for_svalue (const svalue *sval) +{ + if (!sval) + return nullptr; + + std::unique_ptr result; + switch (sval->get_kind ()) + { + default: + gcc_unreachable (); + case SK_REGION: + { + const region_svalue *region_sval = (const region_svalue *)sval; + result + = std::make_unique ("pointer-to-region", false); + set_region_id_attr (*result, *region_sval->get_pointee ()); + } + break; + case SK_CONSTANT: + { + const constant_svalue *constant_sval = (const constant_svalue *)sval; + result = std::make_unique ("constant", false); + pretty_printer pp; + pp_format_decoder (&pp) = default_tree_printer; + pp_printf (&pp, "%E", constant_sval->get_constant ()); + result->set_attr ("value", pp_formatted_text (&pp)); + } + break; + case SK_UNKNOWN: + result = std::make_unique ("unknown", false); + break; + case SK_POISONED: + { + const poisoned_svalue *poisoned_sval = (const poisoned_svalue *)sval; + switch (poisoned_sval->get_poison_kind ()) + { + default: + gcc_unreachable (); + case poison_kind::uninit: + result = std::make_unique ("uninitialized", false); + break; + case poison_kind::freed: + result = std::make_unique ("freed", false); + break; + case poison_kind::deleted: + result = std::make_unique ("deleted", false); + break; + case poison_kind::popped_stack: + result = std::make_unique ("popped-stack", false); + break; + } + } + break; + case SK_SETJMP: + { + //const setjmp_svalue *setjmp_sval = (const setjmp_svalue *)sval; + result = std::make_unique ("setjmp-buffer", false); + // TODO + } + break; + case SK_INITIAL: + { + const initial_svalue *initial_sval = (const initial_svalue *)sval; + result = std::make_unique ("initial-value-of", false); + set_region_id_attr (*result, *initial_sval->get_region ()); + } + break; + case SK_UNARYOP: + { + const unaryop_svalue *unaryop_sval = (const unaryop_svalue *)sval; + result = std::make_unique ("unary-op", false); + result->set_attr ("op", get_tree_code_name (unaryop_sval->get_op ())); + result->add_child + (create_element_for_svalue (unaryop_sval->get_arg ())); + } + break; + case SK_BINOP: + { + const binop_svalue *binop_sval = (const binop_svalue *)sval; + result = std::make_unique ("binary-op", false); + result->set_attr ("op", get_tree_code_name (binop_sval->get_op ())); + result->add_child (create_element_for_svalue (binop_sval->get_arg0 ())); + result->add_child (create_element_for_svalue (binop_sval->get_arg1 ())); + } + break; + case SK_SUB: + { + //const sub_svalue *sub_sval = (const sub_svalue *)sval; + result = std::make_unique ("subregion-value", false); + // TODO + } + break; + case SK_REPEATED: + { + const repeated_svalue *repeated_sval = (const repeated_svalue *)sval; + result = std::make_unique ("repeated-value", false); + result->add_child + (create_element_for_svalue (repeated_sval->get_outer_size ())); + result->add_child + (create_element_for_svalue (repeated_sval->get_inner_svalue ())); + } + break; + case SK_BITS_WITHIN: + { + const bits_within_svalue *bits_within_sval + = (const bits_within_svalue *)sval; + result = std::make_unique ("bits-within", false); + set_bits_attr (*result, bits_within_sval->get_bits ()); + result->add_child + (create_element_for_svalue (bits_within_sval->get_inner_svalue ())); + } + break; + case SK_UNMERGEABLE: + { + const unmergeable_svalue *unmergeable_sval + = (const unmergeable_svalue *)sval; + result = std::make_unique ("unmergeable", false); + result->add_child + (create_element_for_svalue (unmergeable_sval->get_arg ())); + } + break; + case SK_PLACEHOLDER: + { + const placeholder_svalue *placeholder_sval + = (const placeholder_svalue *)sval; + result = std::make_unique ("placeholder", false); + result->set_attr ("name", placeholder_sval->get_name ()); + } + break; + case SK_WIDENING: + { + //const widening_svalue *widening_sval = (const widening_svalue *)sval; + result = std::make_unique ("iterating-value", false); + // TODO + } + break; + case SK_COMPOUND: + { + //const compound_svalue *compound_sval = (const compound_svalue *)sval; + result = std::make_unique ("compound-value", false); + // TODO + } + break; + case SK_CONJURED: + { + //const conjured_svalue *conjured_sval = (const conjured_svalue *)sval; + result = std::make_unique ("conjured-value", false); + // TODO + } + break; + case SK_ASM_OUTPUT: + { + /* const asm_output_svalue *asm_output_sval + = (const asm_output_svalue *)sval; */ + result = std::make_unique ("asm-output", false); + // TODO + } + break; + case SK_CONST_FN_RESULT: + { + /* const const_fn_result_svalue *const_fn_result_sval + = (const const_fn_result_svalue *)sval; */ + result = std::make_unique ("const-fn-result", false); + // TODO + } + } + + if (result) + { + if (sval->get_type ()) + set_type_attr (*result, sval->get_type ()); + + pretty_printer pp; + pp_format_decoder (&pp) = default_tree_printer; + sval->dump_to_pp (&pp, true); + result->set_attr ("dump-text", pp_formatted_text (&pp)); + } + + return result; +} + +std::unique_ptr +program_state::make_xml (const extrinsic_state &ext_state) const +{ + return std::make_unique (*this, ext_state); +} + +void +program_state::dump_xml_to_pp (const extrinsic_state &ext_state, + pretty_printer *pp) const +{ + auto doc = make_xml (ext_state); + doc->write_as_xml (pp, 0, true); +} + +void +program_state::dump_xml_to_file (const extrinsic_state &ext_state, + FILE *outf) const +{ + pretty_printer pp; + pp.set_output_stream (outf); + dump_xml_to_pp (ext_state, &pp); + pp_flush (&pp); +} + +void +program_state::dump_xml (const extrinsic_state &ext_state) const +{ + dump_xml_to_file (ext_state, stderr); +} + +} // namespace ana + +#endif /* #if ENABLE_ANALYZER */ diff --git a/gcc/analyzer/ana-state-to-diagnostic-state.h b/gcc/analyzer/ana-state-to-diagnostic-state.h new file mode 100644 index 00000000000..bd6aa464191 --- /dev/null +++ b/gcc/analyzer/ana-state-to-diagnostic-state.h @@ -0,0 +1,89 @@ +/* XML documents for dumping state in an easier-to-read form. + Copyright (C) 2025 Free Software Foundation, Inc. + Contributed by David Malcolm . + +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 +. */ + +#ifndef GCC_ANALYZER_ANA_STATE_TO_XML_STATE_H +#define GCC_ANALYZER_ANA_STATE_TO_XML_STATE_H + +#include "xml.h" + +namespace ana { + +class xml_state : public xml::document +{ +public: + xml_state (const program_state &state, + const extrinsic_state &ext_state); + + xml::element & + get_or_create_element (const region ®); + +private: + xml::element& + create_and_add_element (const region ®); + + static std::unique_ptr + make_memory_space_element (const char *label); + + std::unique_ptr + create_element (const region ®); + + /* Spatially sorted concrete bindings. */ + typedef std::map concrete_bindings_t; + + void + create_elements_for_binding_cluster (const binding_cluster &cluster, + bool create_all); + + std::unique_ptr + create_element_for_conc_bindings (const concrete_bindings_t &conc_bindings); + + // Try to get the bit_range of REG within its base region + bool + get_bit_range_within_base_region (const region ®, + bit_range &out); + + void + populate_element_for_typed_region (xml::element &e, + const region ®, + const concrete_bindings_t &conc_bindings, + bool create_all); + + void + set_attr_for_dynamic_extents (const region ®, xml::element &e); + + bool + show_child_element_for_child_region_p (const region ®, + const concrete_bindings_t &conc_bindings, + bool create_all); + + std::unique_ptr + create_element_for_svalue (const svalue *sval); + + const program_state &m_state; + const extrinsic_state &m_ext_state; + region_model_manager &m_mgr; + xml::element *m_root; + std::map m_region_to_element_map; + std::map m_types_for_untyped_regions; +}; + +} // namespace ana + +#endif /* GCC_ANALYZER_ANA_STATE_TO_XML_STATE_H */ diff --git a/gcc/analyzer/checker-event.cc b/gcc/analyzer/checker-event.cc index e041778e8d1..04b66bf85d6 100644 --- a/gcc/analyzer/checker-event.cc +++ b/gcc/analyzer/checker-event.cc @@ -28,6 +28,7 @@ along with GCC; see the file COPYING3. If not see #include "inlining-iterator.h" #include "tree-logical-location.h" #include "diagnostic-format-sarif.h" +#include "xml.h" #include "analyzer/analyzer-logging.h" #include "analyzer/sm.h" @@ -106,7 +107,8 @@ event_kind_to_string (enum event_kind ek) checker_event::checker_event (enum event_kind kind, const event_loc_info &loc_info) -: m_kind (kind), m_loc (loc_info.m_loc), +: m_path (nullptr), + m_kind (kind), m_loc (loc_info.m_loc), m_original_fndecl (loc_info.m_fndecl), m_effective_fndecl (loc_info.m_fndecl), m_original_depth (loc_info.m_depth), @@ -211,10 +213,11 @@ checker_event::debug () const pertinent data within the sm-state). */ void -checker_event::prepare_for_emission (checker_path *, +checker_event::prepare_for_emission (checker_path *path, pending_diagnostic *pd, diagnostic_event_id_t emission_id) { + m_path = path; m_pending_diagnostic = pd; m_emission_id = emission_id; @@ -222,6 +225,29 @@ checker_event::prepare_for_emission (checker_path *, print_desc (*pp.get ()); } +std::unique_ptr +checker_event::maybe_make_xml_state (bool debug) const +{ + const program_state *state = get_program_state (); + if (!state) + return nullptr; + + gcc_assert (m_path); + const extrinsic_state &ext_state = m_path->get_ext_state (); + + auto result = state->make_xml (ext_state); + + if (debug) + { + pretty_printer pp; + text_art::theme *theme = global_dc->get_diagram_theme (); + text_art::dump_to_pp (*state, theme, &pp); + result->add_comment (pp_formatted_text (&pp)); + } + + return result; +} + /* class debug_event : public checker_event. */ /* Implementation of diagnostic_event::print_desc vfunc for @@ -344,12 +370,14 @@ 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) +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_supernode ()->get_start_location (), dst_point.get_fndecl (), - dst_point.get_stack_depth ())) + dst_point.get_stack_depth ())), + m_state (state) { } @@ -557,6 +585,12 @@ superedge_event::should_filter_p (int verbosity) const return false; } +const program_state * +superedge_event::get_program_state () const +{ + return &m_eedge.m_dest->get_state (); +} + /* superedge_event's ctor. */ superedge_event::superedge_event (enum event_kind kind, @@ -869,6 +903,14 @@ call_event::get_callee_fndecl () const return m_dest_snode->m_fun->decl; } +const program_state * +call_event::get_program_state () const +{ + /* Use the state at the source (at the caller), + rather than the one at the dest, which has a frame for the callee. */ + return &m_eedge.m_src->get_state (); +} + /* class return_event : public superedge_event. */ /* return_event's ctor. */ @@ -1213,6 +1255,15 @@ warning_event::get_meaning () const return meaning (VERB_danger, NOUN_unknown); } +const program_state * +warning_event::get_program_state () const +{ + if (m_program_state) + return m_program_state.get (); + else + return &m_enode->get_state (); +} + } // namespace ana #endif /* #if ENABLE_ANALYZER */ diff --git a/gcc/analyzer/checker-event.h b/gcc/analyzer/checker-event.h index e29173a439d..7c44f1e83c8 100644 --- a/gcc/analyzer/checker-event.h +++ b/gcc/analyzer/checker-event.h @@ -128,6 +128,12 @@ public: virtual bool is_function_entry_p () const { return false; } virtual bool is_return_p () const { return false; } + virtual const program_state * + get_program_state () const { return nullptr; } + + std::unique_ptr + maybe_make_xml_state (bool debug) const final override; + /* For use with %@. */ const diagnostic_event_id_t *get_id_ptr () const { @@ -144,6 +150,7 @@ protected: const event_loc_info &loc_info); private: + const checker_path *m_path; const enum event_kind m_kind; protected: location_t m_loc; @@ -225,6 +232,12 @@ public: void print_desc (pretty_printer &) const final override; + const program_state * + get_program_state () const final override + { + return &m_dst_state; + } + const gimple * const m_stmt; const program_state m_dst_state; }; @@ -335,17 +348,29 @@ private: class function_entry_event : public checker_event { public: - function_entry_event (const event_loc_info &loc_info) - : checker_event (event_kind::function_entry, loc_info) + function_entry_event (const event_loc_info &loc_info, + const program_state &state) + : checker_event (event_kind::function_entry, loc_info), + m_state (state) { } - function_entry_event (const program_point &dst_point); + 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; bool is_function_entry_p () const final override { return true; } + + const program_state * + get_program_state () const final override + { + return &m_state; + } + +private: + const program_state &m_state; }; /* Subclass of checker_event describing a state change. */ @@ -366,6 +391,12 @@ public: void print_desc (pretty_printer &pp) const final override; meaning get_meaning () const override; + const program_state * + get_program_state () const final override + { + return &m_dst_state; + } + const function *get_dest_function () const { return m_dst_state.get_current_function (); @@ -408,6 +439,9 @@ public: bool should_filter_p (int verbosity) const; + const program_state * + get_program_state () const override; + protected: superedge_event (enum event_kind kind, const exploded_edge &eedge, const event_loc_info &loc_info); @@ -518,6 +552,9 @@ public: bool is_call_p () const final override; + const program_state * + get_program_state () const final override; + protected: tree get_caller_fndecl () const; tree get_callee_fndecl () const; @@ -792,16 +829,22 @@ public: warning_event (const event_loc_info &loc_info, const exploded_node *enode, const state_machine *sm, - tree var, state_machine::state_t state) + tree var, state_machine::state_t state, + const program_state *program_state_ = nullptr) : checker_event (event_kind::warning, loc_info), m_enode (enode), m_sm (sm), m_var (var), m_state (state) { + if (program_state_) + m_program_state = std::make_unique (*program_state_); } void print_desc (pretty_printer &pp) const final override; meaning get_meaning () const override; + const program_state * + get_program_state () const final override; + const exploded_node *get_exploded_node () const { return m_enode; } private: @@ -809,6 +852,9 @@ private: const state_machine *m_sm; tree m_var; state_machine::state_t m_state; + /* Optional copy of program state, for when this is different from + m_enode's state: */ + std::unique_ptr m_program_state; }; } // namespace ana diff --git a/gcc/analyzer/checker-path.h b/gcc/analyzer/checker-path.h index 80c975c6a74..3c174bf8dd1 100644 --- a/gcc/analyzer/checker-path.h +++ b/gcc/analyzer/checker-path.h @@ -32,8 +32,10 @@ class checker_path : public diagnostic_path { public: checker_path (const logical_location_manager &logical_loc_mgr, + const extrinsic_state &ext_state, logger *logger) : diagnostic_path (logical_loc_mgr), + m_ext_state (ext_state), m_thread ("main"), m_logger (logger) {} @@ -59,6 +61,8 @@ public: return m_thread; } + const extrinsic_state &get_ext_state () const { return m_ext_state; } + checker_event *get_checker_event (int idx) { return m_events[idx]; @@ -140,6 +144,8 @@ public: private: DISABLE_COPY_AND_ASSIGN(checker_path); + const extrinsic_state &m_ext_state; + simple_diagnostic_thread m_thread; /* The events that have occurred along this path. */ diff --git a/gcc/analyzer/common.h b/gcc/analyzer/common.h index cb030043d8a..c35cdb25d30 100644 --- a/gcc/analyzer/common.h +++ b/gcc/analyzer/common.h @@ -22,6 +22,8 @@ along with GCC; see the file COPYING3. If not see #define GCC_ANALYZER_COMMON_H #include "config.h" +#define INCLUDE_MAP +#define INCLUDE_STRING #define INCLUDE_VECTOR #include "system.h" #include "coretypes.h" diff --git a/gcc/analyzer/diagnostic-manager.cc b/gcc/analyzer/diagnostic-manager.cc index 6867dd267e6..d3cf993fa42 100644 --- a/gcc/analyzer/diagnostic-manager.cc +++ b/gcc/analyzer/diagnostic-manager.cc @@ -687,6 +687,11 @@ saved_diagnostic::operator== (const saved_diagnostic &other) const for (unsigned i = 0; i < m_notes.length (); i++) if (!m_notes[i]->equal_p (*other.m_notes[i])) return false; + + // Don't deduplicate dump_path_diagnostic instances + if (!strcmp (m_d->get_kind (), "dump_path_diagnostic")) + return this == &other; + return (m_sm == other.m_sm /* We don't compare m_enode. */ && m_snode == other.m_snode @@ -1581,6 +1586,7 @@ diagnostic_manager::emit_saved_diagnostic (const exploded_graph &eg, /* This is the diagnostic_path subclass that will be built for the diagnostic. */ checker_path emission_path (get_logical_location_manager (), + eg.get_ext_state (), get_logger ()); /* Populate emission_path with a full description of EPATH. */ diff --git a/gcc/analyzer/engine.cc b/gcc/analyzer/engine.cc index c3e4800f70a..5eb9048d5ed 100644 --- a/gcc/analyzer/engine.cc +++ b/gcc/analyzer/engine.cc @@ -925,7 +925,9 @@ impl_region_model_context::on_state_leak (const state_machine &sm, } tree leaked_tree_for_diag = fixup_tree_for_diagnostic (leaked_tree); - std::unique_ptr pd = sm.on_leak (leaked_tree_for_diag); + std::unique_ptr pd = sm.on_leak (leaked_tree_for_diag, + m_old_state, + m_new_state); if (pd) { pending_location ploc (m_enode_for_diag, @@ -1578,6 +1580,16 @@ exploded_node::on_stmt_pre (exploded_graph &eg, state->dump (eg.get_ext_state (), true); return; } + else if (is_special_named_call_p (call, "__analyzer_dump_xml", 0)) + { + state->dump_xml (eg.get_ext_state ()); + return; + } + else if (is_special_named_call_p (call, "__analyzer_dump_dot", 0)) + { + state->dump_dot (eg.get_ext_state ()); + return; + } else if (is_special_named_call_p (call, "__analyzer_dump_state", 2)) { state->impl_call_analyzer_dump_state (call, eg.get_ext_state (), diff --git a/gcc/analyzer/exploded-graph.h b/gcc/analyzer/exploded-graph.h index 23e344d87e4..ca0b7878595 100644 --- a/gcc/analyzer/exploded-graph.h +++ b/gcc/analyzer/exploded-graph.h @@ -112,6 +112,8 @@ class impl_region_model_context : public region_model_context const gimple *get_stmt () const override { return m_stmt; } const exploded_graph *get_eg () const override { return m_eg; } + const program_state *get_state () const override { return m_new_state; } + void maybe_did_work () override; bool checking_for_infinite_loop_p () const override { return false; } void on_unusable_in_infinite_loop () override {} diff --git a/gcc/analyzer/infinite-recursion.cc b/gcc/analyzer/infinite-recursion.cc index 064111769b0..bf5c19490ef 100644 --- a/gcc/analyzer/infinite-recursion.cc +++ b/gcc/analyzer/infinite-recursion.cc @@ -108,9 +108,10 @@ public: { public: recursive_function_entry_event (const program_point &dst_point, + const program_state &dst_state, const infinite_recursion_diagnostic &pd, bool topmost) - : function_entry_event (dst_point), + : function_entry_event (dst_point, dst_state), m_pd (pd), m_topmost (topmost) { @@ -148,15 +149,17 @@ public: { gcc_assert (m_prev_entry_event == NULL); std::unique_ptr prev_entry_event - = std::make_unique (dst_point, - *this, false); + = std::make_unique + (dst_point, + dst_node->get_state (), + *this, false); m_prev_entry_event = prev_entry_event.get (); emission_path->add_event (std::move (prev_entry_event)); } else if (eedge.m_dest == m_new_entry_enode) emission_path->add_event (std::make_unique - (dst_point, *this, true)); + (dst_point, dst_node->get_state (), *this, true)); else pending_diagnostic::add_function_entry_event (eedge, emission_path); } diff --git a/gcc/analyzer/kf-analyzer.cc b/gcc/analyzer/kf-analyzer.cc index 3e671e5edad..13476defcaf 100644 --- a/gcc/analyzer/kf-analyzer.cc +++ b/gcc/analyzer/kf-analyzer.cc @@ -28,6 +28,7 @@ along with GCC; see the file COPYING3. If not see #include "analyzer/region-model.h" #include "analyzer/pending-diagnostic.h" #include "analyzer/call-details.h" +#include "analyzer/program-state.h" #if ENABLE_ANALYZER @@ -260,6 +261,11 @@ class dump_path_diagnostic : public pending_diagnostic_subclass { public: + dump_path_diagnostic (const program_state &state) + : m_state (state) + { + } + int get_controlling_option () const final override { return 0; @@ -280,6 +286,15 @@ public: { return true; } + + const program_state * + get_final_state () const final override + { + return &m_state; + } + +private: + program_state m_state; }; /* Handle calls to "__analyzer_dump_path" by queuing a diagnostic at this @@ -297,7 +312,8 @@ public: region_model_context *ctxt = cd.get_ctxt (); if (!ctxt) return; - ctxt->warn (std::make_unique ()); + if (const program_state *state = ctxt->get_state ()) + ctxt->warn (std::make_unique (*state)); } }; diff --git a/gcc/analyzer/pending-diagnostic.cc b/gcc/analyzer/pending-diagnostic.cc index 70dc8154d62..b727392bb96 100644 --- a/gcc/analyzer/pending-diagnostic.cc +++ b/gcc/analyzer/pending-diagnostic.cc @@ -185,7 +185,10 @@ pending_diagnostic::add_function_entry_event (const exploded_edge &eedge, { const exploded_node *dst_node = eedge.m_dest; const program_point &dst_point = dst_node->get_point (); - emission_path->add_event (std::make_unique (dst_point)); + const program_state &dst_state = dst_node->get_state (); + emission_path->add_event + (std::make_unique (dst_point, + dst_state)); } /* Base implementation of pending_diagnostic::add_call_event. @@ -241,7 +244,8 @@ pending_diagnostic::add_final_event (const state_machine *sm, (std::make_unique (loc_info, enode, - sm, var, state)); + sm, var, state, + get_final_state ())); } } // namespace ana diff --git a/gcc/analyzer/pending-diagnostic.h b/gcc/analyzer/pending-diagnostic.h index 6a0289e705b..469513c726b 100644 --- a/gcc/analyzer/pending-diagnostic.h +++ b/gcc/analyzer/pending-diagnostic.h @@ -364,6 +364,12 @@ class pending_diagnostic tree var, state_machine::state_t state, checker_path *emission_path); + virtual const program_state * + get_final_state () const + { + return nullptr; + } + /* Vfunc for determining that this pending_diagnostic supercedes OTHER, and that OTHER should therefore not be emitted. They have already been tested for being at the same stmt. */ diff --git a/gcc/analyzer/program-state.cc b/gcc/analyzer/program-state.cc index 21f78e5b5c3..0d9199f61f2 100644 --- a/gcc/analyzer/program-state.cc +++ b/gcc/analyzer/program-state.cc @@ -28,6 +28,8 @@ along with GCC; see the file COPYING3. If not see #include "cgraph.h" #include "digraph.h" #include "diagnostic-event-id.h" +#include "diagnostic-state.h" +#include "graphviz.h" #include "text-art/tree-widget.h" #include "text-art/dump.h" @@ -48,6 +50,7 @@ along with GCC; see the file COPYING3. If not see #include "analyzer/state-purge.h" #include "analyzer/call-summary.h" #include "analyzer/analyzer-selftests.h" +#include "analyzer/ana-state-to-diagnostic-state.h" #if ENABLE_ANALYZER @@ -1224,6 +1227,18 @@ program_state::make_dump_widget (const text_art::dump_widget_info &dwi) const return state_widget; } +void +program_state::dump_dot (const extrinsic_state &ext_state) const +{ + auto doc = make_xml (ext_state); + auto graph = make_dot_graph_from_xml_state (*doc); + + pretty_printer pp; + dot::writer w (pp); + graph->print (w); + pp_flush (&pp); +} + /* Update this program_state to reflect a top-level call to FUN. The params will have initial_svalues. */ diff --git a/gcc/analyzer/program-state.h b/gcc/analyzer/program-state.h index 269ffde9ee6..d8bd9206937 100644 --- a/gcc/analyzer/program-state.h +++ b/gcc/analyzer/program-state.h @@ -22,6 +22,11 @@ along with GCC; see the file COPYING3. If not see #define GCC_ANALYZER_PROGRAM_STATE_H #include "text-art/widget.h" +#include "text-art/tree-widget.h" + +#include "analyzer/store.h" + +namespace xml { class document; } namespace ana { @@ -243,6 +248,12 @@ public: void dump (const extrinsic_state &ext_state, bool simple) const; void dump () const; + std::unique_ptr make_xml (const extrinsic_state &ext_state) const; + void dump_xml_to_pp (const extrinsic_state &ext_state, pretty_printer *pp) const; + void dump_xml_to_file (const extrinsic_state &ext_state, FILE *outf) const; + void dump_xml (const extrinsic_state &ext_state) const; + void dump_dot (const extrinsic_state &ext_state) const; + std::unique_ptr to_json (const extrinsic_state &ext_state) const; diff --git a/gcc/analyzer/record-layout.cc b/gcc/analyzer/record-layout.cc index aaf8ccd6f96..7edc81cede0 100644 --- a/gcc/analyzer/record-layout.cc +++ b/gcc/analyzer/record-layout.cc @@ -30,7 +30,7 @@ namespace ana { /* class record_layout. */ -record_layout::record_layout (tree record_type) +record_layout::record_layout (const_tree record_type) { gcc_assert (TREE_CODE (record_type) == RECORD_TYPE); diff --git a/gcc/analyzer/record-layout.h b/gcc/analyzer/record-layout.h index c1c41890dcd..8649d1d601c 100644 --- a/gcc/analyzer/record-layout.h +++ b/gcc/analyzer/record-layout.h @@ -36,7 +36,7 @@ public: { public: item (const bit_range &br, - tree field, + const_tree field, bool is_padding) : m_bit_range (br), m_field (field), @@ -69,17 +69,20 @@ public: } bit_range m_bit_range; - tree m_field; + const_tree m_field; bool m_is_padding; }; - record_layout (tree record_type); + record_layout (const_tree record_type); void dump_to_pp (pretty_printer *pp) const; DEBUG_FUNCTION void dump () const; const record_layout::item *get_item_at (bit_offset_t offset) const; + auto begin () const { return m_items.begin (); } + auto end () const { return m_items.end (); } + private: void maybe_pad_to (bit_offset_t next_offset); diff --git a/gcc/analyzer/region-model.cc b/gcc/analyzer/region-model.cc index 1ee882c98ea..d1c7e8cd53e 100644 --- a/gcc/analyzer/region-model.cc +++ b/gcc/analyzer/region-model.cc @@ -7798,7 +7798,7 @@ private: static void complain_about_fully_uninit_item (const record_layout::item &item) { - tree field = item.m_field; + const_tree field = item.m_field; bit_size_t num_bits = item.m_bit_range.m_size_in_bits; if (item.m_is_padding) { @@ -7859,7 +7859,7 @@ private: static void complain_about_partially_uninit_item (const record_layout::item &item) { - tree field = item.m_field; + const_tree field = item.m_field; if (item.m_is_padding) inform (DECL_SOURCE_LOCATION (field), "padding after field %qD is partially uninitialized", diff --git a/gcc/analyzer/region-model.h b/gcc/analyzer/region-model.h index 2c7f73795e2..781a585f7bc 100644 --- a/gcc/analyzer/region-model.h +++ b/gcc/analyzer/region-model.h @@ -928,6 +928,8 @@ class region_model_context virtual const exploded_graph *get_eg () const = 0; + virtual const program_state *get_state () const = 0; + /* Hooks for detecting infinite loops. */ virtual void maybe_did_work () = 0; virtual bool checking_for_infinite_loop_p () const = 0; @@ -989,6 +991,8 @@ public: const gimple *get_stmt () const override { return NULL; } const exploded_graph *get_eg () const override { return NULL; } + const program_state *get_state () const override { return nullptr; } + void maybe_did_work () override {} bool checking_for_infinite_loop_p () const override { return false; } void on_unusable_in_infinite_loop () override {} @@ -1167,6 +1171,14 @@ class region_model_context_decorator : public region_model_context return nullptr; } + const program_state *get_state () const override + { + if (m_inner) + return m_inner->get_state (); + else + return nullptr; + } + void maybe_did_work () override { if (m_inner) diff --git a/gcc/analyzer/sm-fd.cc b/gcc/analyzer/sm-fd.cc index cee8d2d7670..09c311594ba 100644 --- a/gcc/analyzer/sm-fd.cc +++ b/gcc/analyzer/sm-fd.cc @@ -116,7 +116,11 @@ public: const svalue *rhs) const final override; bool can_purge_p (state_t s) const final override; - std::unique_ptr on_leak (tree var) const final override; + + std::unique_ptr + on_leak (tree var, + const program_state *old_state, + const program_state *new_state) const final override; bool is_unchecked_fd_p (state_t s) const; bool is_valid_fd_p (state_t s) const; @@ -477,7 +481,14 @@ protected: class fd_leak : public fd_diagnostic { public: - fd_leak (const fd_state_machine &sm, tree arg) : fd_diagnostic (sm, arg) {} + fd_leak (const fd_state_machine &sm, tree arg, + const program_state *final_state) + : fd_diagnostic (sm, arg), + m_final_state () + { + if (final_state) + m_final_state = std::make_unique (*final_state); + } const char * get_kind () const final override @@ -543,8 +554,15 @@ public: return true; } + const program_state * + get_final_state () const final override + { + return m_final_state.get (); + } + private: diagnostic_event_id_t m_open_event; + std::unique_ptr m_final_state; }; class fd_access_mode_mismatch : public fd_param_diagnostic @@ -1528,7 +1546,7 @@ fd_state_machine::on_open (sm_context &sm_ctxt, const supernode *node, else { sm_ctxt.warn (node, stmt, NULL_TREE, - std::make_unique (*this, NULL_TREE)); + std::make_unique (*this, NULL_TREE, nullptr)); } } @@ -1541,7 +1559,7 @@ fd_state_machine::on_creat (sm_context &sm_ctxt, const supernode *node, sm_ctxt.on_transition (node, stmt, lhs, m_start, m_unchecked_write_only); else sm_ctxt.warn (node, stmt, NULL_TREE, - std::make_unique (*this, NULL_TREE)); + std::make_unique (*this, NULL_TREE, nullptr)); } void @@ -1792,7 +1810,7 @@ fd_state_machine::on_socket (const call_details &cd, } else sm_ctxt.warn (node, &call, NULL_TREE, - std::make_unique (*this, NULL_TREE)); + std::make_unique (*this, NULL_TREE, nullptr)); } else { @@ -2185,7 +2203,7 @@ fd_state_machine::on_accept (const call_details &cd, } else sm_ctxt.warn (node, &call, NULL_TREE, - std::make_unique (*this, NULL_TREE)); + std::make_unique (*this, NULL_TREE, nullptr)); } else { @@ -2321,9 +2339,11 @@ fd_state_machine::can_purge_p (state_t s) const } std::unique_ptr -fd_state_machine::on_leak (tree var) const +fd_state_machine::on_leak (tree var, + const program_state *, + const program_state *new_state) const { - return std::make_unique (*this, var); + return std::make_unique (*this, var, new_state); } } // namespace diff --git a/gcc/analyzer/sm-file.cc b/gcc/analyzer/sm-file.cc index d7dbe2fe7b6..61e24248c08 100644 --- a/gcc/analyzer/sm-file.cc +++ b/gcc/analyzer/sm-file.cc @@ -30,6 +30,7 @@ along with GCC; see the file COPYING3. If not see #include "analyzer/analyzer-selftests.h" #include "analyzer/call-string.h" #include "analyzer/program-point.h" +#include "analyzer/program-state.h" #include "analyzer/store.h" #include "analyzer/region-model.h" #include "analyzer/call-details.h" @@ -72,7 +73,11 @@ public: const svalue *rhs) const final override; bool can_purge_p (state_t s) const final override; - std::unique_ptr on_leak (tree var) const final override; + + std::unique_ptr + on_leak (tree var, + const program_state *old_state, + const program_state *new_state) const final override; /* State for a FILE * returned from fopen that hasn't been checked for NULL. @@ -226,9 +231,14 @@ private: class file_leak : public file_diagnostic { public: - file_leak (const fileptr_state_machine &sm, tree arg) - : file_diagnostic (sm, arg) - {} + file_leak (const fileptr_state_machine &sm, tree arg, + const program_state *final_state) + : file_diagnostic (sm, arg), + m_final_state () + { + if (final_state) + m_final_state = std::make_unique (*final_state); + } const char *get_kind () const final override { return "file_leak"; } @@ -286,8 +296,15 @@ public: return true; } + const program_state * + get_final_state () const final override + { + return m_final_state.get (); + } + private: diagnostic_event_id_t m_fopen_event; + std::unique_ptr m_final_state; }; /* fileptr_state_machine's ctor. */ @@ -492,9 +509,11 @@ fileptr_state_machine::can_purge_p (state_t s) const state 'unchecked' and 'nonnull'. */ std::unique_ptr -fileptr_state_machine::on_leak (tree var) const +fileptr_state_machine::on_leak (tree var, + const program_state *, + const program_state *new_state) const { - return std::make_unique (*this, var); + return std::make_unique (*this, var, new_state); } } // anonymous namespace diff --git a/gcc/analyzer/sm-malloc.cc b/gcc/analyzer/sm-malloc.cc index 333dfea47d3..ee60b963b2b 100644 --- a/gcc/analyzer/sm-malloc.cc +++ b/gcc/analyzer/sm-malloc.cc @@ -23,6 +23,7 @@ along with GCC; see the file COPYING3. If not see #include "diagnostic-event-id.h" #include "stringpool.h" #include "attribs.h" +#include "xml-printer.h" #include "analyzer/analyzer-logging.h" #include "analyzer/sm.h" @@ -37,6 +38,7 @@ along with GCC; see the file COPYING3. If not see #include "analyzer/checker-event.h" #include "analyzer/exploded-graph.h" #include "analyzer/inlining-iterator.h" +#include "analyzer/ana-state-to-diagnostic-state.h" #if ENABLE_ANALYZER @@ -403,7 +405,11 @@ public: const frame_region *) const final override; bool can_purge_p (state_t s) const final override; - std::unique_ptr on_leak (tree var) const final override; + + std::unique_ptr + on_leak (tree var, + const program_state *old_state, + const program_state *new_state) const final override; bool reset_when_passed_to_unknown_fn_p (state_t s, bool is_mutable) const final override; @@ -429,6 +435,11 @@ public: const svalue *new_ptr_sval, const extrinsic_state &ext_state) const; + void + add_state_to_xml (xml_state &out_xml, + const svalue &sval, + state_machine::state_t state) const final override; + standard_deallocator_set m_free; standard_deallocator_set m_scalar_delete; standard_deallocator_set m_vector_delete; @@ -1414,8 +1425,14 @@ private: class malloc_leak : public malloc_diagnostic { public: - malloc_leak (const malloc_state_machine &sm, tree arg) - : malloc_diagnostic (sm, arg) {} + malloc_leak (const malloc_state_machine &sm, tree arg, + const program_state *final_state) + : malloc_diagnostic (sm, arg), + m_final_state () + { + if (final_state) + m_final_state = std::make_unique (*final_state); + } const char *get_kind () const final override { return "malloc_leak"; } @@ -1475,8 +1492,15 @@ public: return true; } + const program_state * + get_final_state () const final override + { + return m_final_state.get (); + } + private: diagnostic_event_id_t m_alloc_event; + std::unique_ptr m_final_state; }; class free_of_non_heap : public malloc_diagnostic @@ -2589,9 +2613,11 @@ malloc_state_machine::can_purge_p (state_t s) const 'nonnull'). */ std::unique_ptr -malloc_state_machine::on_leak (tree var) const +malloc_state_machine::on_leak (tree var, + const program_state *, + const program_state *new_state) const { - return std::make_unique (*this, var); + return std::make_unique (*this, var, new_state); } /* Implementation of state_machine::reset_when_passed_to_unknown_fn_p vfunc @@ -2700,6 +2726,30 @@ malloc_state_machine::transition_ptr_sval_non_null (region_model *model, smap->set_state (model, new_ptr_sval, m_free.m_nonnull, NULL, ext_state); } +void +malloc_state_machine::add_state_to_xml (xml_state &out_xml, + const svalue &sval, + state_machine::state_t state) const +{ + if (const region *reg = sval.maybe_get_region ()) + { + auto ®_element = out_xml.get_or_create_element (*reg); + auto alloc_state = as_a_allocation_state (state); + gcc_assert (alloc_state); + + reg_element.set_attr ("dynamic-alloc-state", state->get_name ()); + if (alloc_state->m_deallocators) + { + pretty_printer pp; + alloc_state->m_deallocators->dump_to_pp (&pp); + reg_element.set_attr ("expected-deallocators", pp_formatted_text (&pp)); + } + if (alloc_state->m_deallocator) + reg_element.set_attr ("deallocator", + alloc_state->m_deallocator->m_name); + } +} + } // anonymous namespace /* Internal interface to this file. */ diff --git a/gcc/analyzer/sm.cc b/gcc/analyzer/sm.cc index 0abbdd69adf..54bd92c31ab 100644 --- a/gcc/analyzer/sm.cc +++ b/gcc/analyzer/sm.cc @@ -116,9 +116,11 @@ state_machine::get_state_by_name (const char *name) const /* Base implementation of state_machine::on_leak. */ std::unique_ptr -state_machine::on_leak (tree var ATTRIBUTE_UNUSED) const +state_machine::on_leak (tree var ATTRIBUTE_UNUSED, + const program_state *old_state ATTRIBUTE_UNUSED, + const program_state *new_state ATTRIBUTE_UNUSED) const { - return NULL; + return nullptr; } /* Dump a multiline representation of this state machine to PP. */ @@ -158,6 +160,21 @@ state_machine::to_json () const return sm_obj; } +void +state_machine::add_state_to_xml (xml_state &out_xml, + const svalue &sval, + state_machine::state_t state) const +{ + // no-op +} + +void +state_machine::add_global_state_to_xml (xml_state &out_xml, + state_machine::state_t state) const +{ + // no-op +} + /* class sm_context. */ const region_model * diff --git a/gcc/analyzer/sm.h b/gcc/analyzer/sm.h index a932765131b..f366be13217 100644 --- a/gcc/analyzer/sm.h +++ b/gcc/analyzer/sm.h @@ -28,6 +28,7 @@ namespace ana { class state_machine; class sm_context; class pending_diagnostic; +class xml_state; extern bool any_pointer_p (tree expr); extern bool any_pointer_p (const svalue *sval); @@ -136,7 +137,9 @@ public: /* Called when VAR leaks (and !can_purge_p). */ virtual std::unique_ptr - on_leak (tree var ATTRIBUTE_UNUSED) const; + on_leak (tree var ATTRIBUTE_UNUSED, + const program_state *old_state, + const program_state *new_state) const; /* Return true if S should be reset to "start" for values passed (or reachable from) calls to unknown functions. IS_MUTABLE is true for pointers as @@ -184,6 +187,15 @@ public: state_t get_start_state () const { return m_start; } + virtual void + add_state_to_xml (xml_state &out_xml, + const svalue &sval, + state_machine::state_t state) const; + + virtual void + add_global_state_to_xml (xml_state &out_xml, + state_machine::state_t state) const; + protected: state_t add_state (const char *name); state_t add_custom_state (state *s) diff --git a/gcc/analyzer/store.h b/gcc/analyzer/store.h index 171324c13d3..c85539435f2 100644 --- a/gcc/analyzer/store.h +++ b/gcc/analyzer/store.h @@ -299,6 +299,16 @@ struct bit_range bool as_byte_range (byte_range *out) const; + bool + operator< (const bit_range &other) const + { + if (m_start_bit_offset < other.m_start_bit_offset) + return true; + if (m_start_bit_offset > other.m_start_bit_offset) + return false; + return (m_size_in_bits < other.m_size_in_bits); + } + bit_offset_t m_start_bit_offset; bit_size_t m_size_in_bits; }; diff --git a/gcc/analyzer/varargs.cc b/gcc/analyzer/varargs.cc index 6ea0d29d131..3b8f86364be 100644 --- a/gcc/analyzer/varargs.cc +++ b/gcc/analyzer/varargs.cc @@ -205,7 +205,11 @@ public: { return s != m_started; } - std::unique_ptr on_leak (tree var) const final override; + + std::unique_ptr + on_leak (tree var, + const program_state *old_state, + const program_state *new_state) const final override; /* State for a va_list that is the result of a va_start or va_copy. */ state_t m_started; @@ -460,10 +464,14 @@ class va_list_leak : public va_list_sm_diagnostic { public: va_list_leak (const va_list_state_machine &sm, - const svalue *ap_sval, tree ap_tree) + const svalue *ap_sval, tree ap_tree, + const program_state *final_state) : va_list_sm_diagnostic (sm, ap_sval, ap_tree), - m_start_event_fnname (NULL) + m_start_event_fnname (NULL), + m_final_state () { + if (final_state) + m_final_state = std::make_unique (*final_state); } int get_controlling_option () const final override @@ -524,9 +532,16 @@ public: return true; } + const program_state * + get_final_state () const final override + { + return m_final_state.get (); + } + private: diagnostic_event_id_t m_start_event; const char *m_start_event_fnname; + std::unique_ptr m_final_state; }; /* Update state machine for a "va_start" call. */ @@ -633,9 +648,11 @@ va_list_state_machine::on_va_end (sm_context &sm_ctxt, (for complaining about leaks of values in state 'started'). */ std::unique_ptr -va_list_state_machine::on_leak (tree var) const +va_list_state_machine::on_leak (tree var, + const program_state *, + const program_state *new_state) const { - return std::make_unique (*this, nullptr, var); + return std::make_unique (*this, nullptr, var, new_state); } } // anonymous namespace diff --git a/gcc/diagnostic-format-html.cc b/gcc/diagnostic-format-html.cc index 5668b50a91a..b1b0895d031 100644 --- a/gcc/diagnostic-format-html.cc +++ b/gcc/diagnostic-format-html.cc @@ -41,6 +41,8 @@ along with GCC; see the file COPYING3. If not see #include "intl.h" #include "xml.h" #include "xml-printer.h" +#include "diagnostic-state.h" +#include "graphviz.h" #include "json.h" #include "selftest-xml.h" @@ -48,7 +50,10 @@ along with GCC; see the file COPYING3. If not see html_generation_options::html_generation_options () : m_css (true), - m_javascript (true) + m_javascript (true), + m_show_state_diagrams (false), + m_show_state_diagram_xml (false), + m_show_state_diagram_dot_src (false) { } @@ -141,6 +146,9 @@ public: m_ui_focus_ids.append_string (focus_id.c_str ()); } + std::unique_ptr + maybe_make_state_diagram (const diagnostic_event &event); + private: void add_stylesheet (std::string url); @@ -310,14 +318,27 @@ const char * const HTML_SCRIPT " const element_id = focus_ids[focus_idx];\n" " return document.getElementById(element_id);\n" " }\n" + " function get_any_state_diagram (focus_idx)\n" + " {\n" + " const element_id = focus_ids[focus_idx];\n" + " return document.getElementById(element_id + \"-state-diagram\");\n" + " }\n" " function unhighlight_current_focus_idx ()\n" " {\n" " get_focus_span (current_focus_idx).classList.remove ('selected');\n" + " state_diagram = get_any_state_diagram (current_focus_idx);\n" + " if (state_diagram) {\n" + " state_diagram.style.visibility = \"hidden\";\n" + " }\n" " }\n" " function highlight_current_focus_idx ()\n" " {\n" " const el = get_focus_span (current_focus_idx);\n" " el.classList.add ('selected');\n" + " state_diagram = get_any_state_diagram (current_focus_idx);\n" + " if (state_diagram) {\n" + " state_diagram.style.visibility = \"visible\";\n" + " }\n" " // Center the element on the screen\n" " const top_y = el.getBoundingClientRect ().top + window.pageYOffset;\n" " const middle = top_y - (window.innerHeight / 2);\n" @@ -563,6 +584,58 @@ html_builder::pop_nesting_level () m_cur_nesting_levels.pop_back (); } +static void +print_pre_source (xml::printer &xp, const char *text) +{ + xp.push_tag_with_class ("pre", "source", true); + xp.add_text (text); + xp.pop_tag ("pre"); +} + +std::unique_ptr +html_builder::maybe_make_state_diagram (const diagnostic_event &event) +{ + if (!m_html_gen_opts.m_show_state_diagrams) + return nullptr; + + /* Get XML state document; if we're going to print it later, also request + the debug version. */ + auto xml_state + = event.maybe_make_xml_state (m_html_gen_opts.m_show_state_diagram_xml); + if (!xml_state) + return nullptr; + + // Convert it to .dot AST + auto graph = make_dot_graph_from_xml_state (*xml_state); + gcc_assert (graph); + + auto wrapper = std::make_unique ("div", false); + xml::printer xp (*wrapper); + + if (m_html_gen_opts.m_show_state_diagram_xml) + { + // For debugging, show the XML src inline: + pretty_printer pp; + xml_state->write_as_xml (&pp, 0, true); + print_pre_source (xp, pp_formatted_text (&pp)); + } + + if (m_html_gen_opts.m_show_state_diagram_dot_src) + { + // For debugging, show the dot src inline: + pretty_printer pp; + dot::writer w (pp); + graph->print (w); + print_pre_source (xp, pp_formatted_text (&pp)); + } + + // Turn the .dot into SVG and splice into place + auto svg = dot::make_svg_from_graph (*graph); + xp.append (std::move (svg)); + + return wrapper; +} + /* Custom subclass of html_label_writer. Wrap labels within a element, supplying them with event IDs. Add the IDs to the list of focus IDs. */ @@ -572,34 +645,59 @@ class html_path_label_writer : public html_label_writer public: html_path_label_writer (xml::printer &xp, html_builder &builder, + const diagnostic_path &path, const std::string &event_id_prefix) : m_xp (xp), m_html_builder (builder), + m_path (path), m_event_id_prefix (event_id_prefix), - m_next_event_idx (0) + m_next_event_idx (0), + m_curr_event_id () { } void begin_label () final override { + m_curr_event_id = m_next_event_idx++; m_xp.push_tag_with_class ("span", "event", true); - pretty_printer pp; - pp_printf (&pp, "%s%i", - m_event_id_prefix.c_str (), m_next_event_idx++); - m_xp.set_attr ("id", pp_formatted_text (&pp)); - m_html_builder.add_focus_id (pp_formatted_text (&pp)); + m_xp.set_attr ("id", get_element_id ()); + m_html_builder.add_focus_id (get_element_id ()); } void end_label () final override { + const diagnostic_event &event + = m_path.get_event (m_curr_event_id.zero_based ()); + if (auto state_doc = m_html_builder.maybe_make_state_diagram (event)) + { + m_xp.push_tag_with_class ("div", "state-diagram", false); + m_xp.set_attr ("id", get_element_id () + "-state-diagram"); + m_xp.set_attr ("style", + ("position: absolute;" + " z-index: 1;" + " visibility: hidden;")); + m_xp.append (std::move (state_doc)); + m_xp.pop_tag ("div"); + } + m_xp.pop_tag ("span"); // from begin_label } private: + std::string + get_element_id () const + { + gcc_assert (m_curr_event_id.known_p ()); + return (m_event_id_prefix + + std::to_string (m_curr_event_id.zero_based ())); + } + xml::printer &m_xp; html_builder &m_html_builder; + const diagnostic_path &m_path; const std::string &m_event_id_prefix; int m_next_event_idx; + diagnostic_event_id_t m_curr_event_id; }; /* See https://pf3.patternfly.org/v3/pattern-library/widgets/#alerts */ @@ -1013,8 +1111,9 @@ html_builder::make_element_for_diagnostic (const diagnostic_info &diagnostic, xp.pop_tag ("label"); std::string event_id_prefix (diag_id + "-event-"); - html_path_label_writer event_label_writer (xp, *this, + html_path_label_writer event_label_writer (xp, *this, *path, event_id_prefix); + diagnostic_source_print_policy dspp (m_context); print_path_as_html (xp, *path, m_context, &event_label_writer, dspp); diff --git a/gcc/diagnostic-format-html.h b/gcc/diagnostic-format-html.h index 9ca43f253f0..09a97e0ccac 100644 --- a/gcc/diagnostic-format-html.h +++ b/gcc/diagnostic-format-html.h @@ -30,6 +30,17 @@ struct html_generation_options bool m_css; bool m_javascript; + + // Debugging options: + + // If true, attempt to show state diagrams at events + bool m_show_state_diagrams; + + // If true, show the XML form of the state with such diagrams + bool m_show_state_diagram_xml; + + // If true, show the .dot source used for the diagram + bool m_show_state_diagram_dot_src; }; extern diagnostic_output_file diff --git a/gcc/diagnostic-format-sarif.cc b/gcc/diagnostic-format-sarif.cc index 853a8333b3c..d4ebe5a31ed 100644 --- a/gcc/diagnostic-format-sarif.cc +++ b/gcc/diagnostic-format-sarif.cc @@ -50,6 +50,7 @@ along with GCC; see the file COPYING3. If not see #include "pretty-print-urlifier.h" #include "demangle.h" #include "backtrace.h" +#include "xml.h" /* A json::array where the values are "unique" as per SARIF v2.1.0 section 3.7.3 ("Array properties with unique values"). */ @@ -2959,6 +2960,20 @@ populate_thread_flow_location_object (sarif_result &result, via a property bag. */ ev.maybe_add_sarif_properties (*this, tfl_obj); + if (get_opts ().m_xml_state) + if (auto xml_state = ev.maybe_make_xml_state (true)) + { + sarif_property_bag &props = tfl_obj.get_or_create_properties (); + + pretty_printer pp; + xml_state->write_as_xml (&pp, 0, true); + +#define PROPERTY_PREFIX "gcc/diagnostic_event/" + props.set_string (PROPERTY_PREFIX "xml_state", + pp_formatted_text (&pp)); +#undef PROPERTY_PREFIX + } + /* "location" property (SARIF v2.1.0 section 3.38.3). */ tfl_obj.set ("location", @@ -4101,7 +4116,8 @@ make_sarif_sink (diagnostic_context &context, // struct sarif_generation_options sarif_generation_options::sarif_generation_options () -: m_version (sarif_version::v2_1_0) +: m_version (sarif_version::v2_1_0), + m_xml_state (false) { } diff --git a/gcc/diagnostic-format-sarif.h b/gcc/diagnostic-format-sarif.h index 763dc25f39e..c3ae330d3e4 100644 --- a/gcc/diagnostic-format-sarif.h +++ b/gcc/diagnostic-format-sarif.h @@ -101,6 +101,7 @@ struct sarif_generation_options sarif_generation_options (); enum sarif_version m_version; + bool m_xml_state; }; extern std::unique_ptr diff --git a/gcc/diagnostic-path.cc b/gcc/diagnostic-path.cc index 7a9c051c534..0c617edeaa5 100644 --- a/gcc/diagnostic-path.cc +++ b/gcc/diagnostic-path.cc @@ -20,12 +20,14 @@ along with GCC; see the file COPYING3. If not see #include "config.h" #define INCLUDE_ALGORITHM +#define INCLUDE_MAP #define INCLUDE_STRING #define INCLUDE_VECTOR #include "system.h" #include "coretypes.h" #include "diagnostic.h" #include "diagnostic-path.h" +#include "xml.h" /* Disable warnings about missing quoting in GCC diagnostics for the print calls below. */ @@ -154,6 +156,15 @@ diagnostic_event::get_desc (pretty_printer &ref_pp) const return label_text::take (xstrdup (pp_formatted_text (pp.get ()))); } +// Base implementation of diagnostic_event::maybe_make_xml_state + +std::unique_ptr +diagnostic_event::maybe_make_xml_state (bool) const +{ + // Don't attempt to make a state document: + return nullptr; +} + /* class diagnostic_path. */ /* Subroutine of diagnostic_path::interprocedural_p. diff --git a/gcc/diagnostic-path.h b/gcc/diagnostic-path.h index 4419139413b..a3987a015a3 100644 --- a/gcc/diagnostic-path.h +++ b/gcc/diagnostic-path.h @@ -25,6 +25,8 @@ along with GCC; see the file COPYING3. If not see #include "diagnostic-event-id.h" #include "logical-location.h" +namespace xml { class document; } + class sarif_builder; class sarif_object; @@ -170,6 +172,11 @@ class diagnostic_event { } + /* Hook for capturing state at this event, potentially for visualizing + in HTML output. */ + virtual std::unique_ptr + maybe_make_xml_state (bool debug) const; + label_text get_desc (pretty_printer &ref_pp) const; }; diff --git a/gcc/diagnostic-state-to-dot.cc b/gcc/diagnostic-state-to-dot.cc new file mode 100644 index 00000000000..b6d7ec5a082 --- /dev/null +++ b/gcc/diagnostic-state-to-dot.cc @@ -0,0 +1,537 @@ +/* Creating GraphViz .dot files from XML state documents. + Copyright (C) 2025 Free Software Foundation, Inc. + Contributed by David Malcolm . + +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 +. */ + +#define INCLUDE_ALGORITHM +#define INCLUDE_MAP +#define INCLUDE_SET +#define INCLUDE_STRING +#define INCLUDE_VECTOR +#include "config.h" +#include "system.h" +#include "coretypes.h" + +#include "xml.h" +#include "xml-printer.h" +#include "graphviz.h" + +static int +get_depth (const xml::element &e) +{ + int deepest_child = 0; + for (auto &iter : e.m_children) + if (xml::element *child_element = iter->dyn_cast_element ()) + deepest_child = std::max (deepest_child, + get_depth (*child_element)); + return deepest_child + 1; +} + +enum class dynalloc_state +{ + unknown, + nonnull, + unchecked, + freed +}; + +static const char * +get_color_for_dynalloc_state (enum dynalloc_state dynalloc_state) +{ + switch (dynalloc_state) + { + default: + gcc_unreachable (); + break; + case dynalloc_state::unknown: + case dynalloc_state::nonnull: + return nullptr; + + case dynalloc_state::unchecked: + return "#ec7a08"; // pf-orange-400 + + case dynalloc_state::freed: + return "#cc0000"; // pf-red-100 + } +} + +static void +set_color_for_dynalloc_state (dot::attr_list &attrs, + enum dynalloc_state dynalloc_state) +{ + if (const char *color = get_color_for_dynalloc_state (dynalloc_state)) + attrs.add (dot::id ("color"), dot::id (color)); +} + +static enum dynalloc_state +get_dynalloc_state (const xml::element &input_element) +{ + const char *dyn_alloc_state = input_element.get_attr ("dynamic-alloc-state"); + if (!dyn_alloc_state) + return dynalloc_state::unknown; + + if (dyn_alloc_state == std::string ("unchecked")) + return dynalloc_state::unchecked; + + if (dyn_alloc_state == std::string ("nonnull")) + return dynalloc_state::nonnull; + + if (dyn_alloc_state == std::string ("freed")) + return dynalloc_state::freed; + + return dynalloc_state::unknown; +} + +class state_diagram : public dot::graph +{ +public: + state_diagram (const xml::document &input_state_doc) + : m_next_id (0), + m_show_tags (false) + { + // "node [shape=plaintext]\n" + { + auto attr_stmt + = std::make_unique (dot::attr_stmt::kind::node); + attr_stmt->m_attrs.add (dot::id ("shape"), dot::id ("plaintext")); + add_stmt (std::move (attr_stmt)); + } + + /* Recurse down the XML state diagram, creating subgraphs + and then eventually creating nodes, and recursively + creating XML tables, adding ports for the endpoints of edges, + and recording edges we'll want to create (into m_pending_edges). */ + xml::element *input_elmt_state_diagram + = input_state_doc.find_child_element ("state-diagram"); + gcc_assert (input_elmt_state_diagram); + xml::element *input_elmt_mem_regions + = input_elmt_state_diagram->find_child_element ("memory-regions"); + if (!input_elmt_mem_regions) + return; + auto root_cluster + = std::make_unique (dot::id ("cluster_memory_regions")); + for (auto &iter : input_elmt_mem_regions->m_children) + on_input_xml_node (*root_cluster, *iter); + add_stmt (std::move (root_cluster)); + + /* We should now have ports for edge endpoints for all region ids. + Use them now to create edges. */ + for (auto &pe : m_pending_edges) + { + auto search = m_region_id_to_dst_node_id.find (pe.m_dst_region_id); + if (search != m_region_id_to_dst_node_id.end ()) + { + auto &dst_node_id = search->second; + auto e = std::make_unique (pe.m_src_node_id, + dst_node_id); + + auto dynalloc_state = m_region_id_to_dynalloc_state.find (pe.m_dst_region_id); + if (dynalloc_state != m_region_id_to_dynalloc_state.end ()) + set_color_for_dynalloc_state (e->m_attrs, + dynalloc_state->second); + + add_stmt (std::move (e)); + } + } + } + +private: + struct pending_edge + { + dot::node_id m_src_node_id; + std::string m_dst_region_id; + }; + + dot::id + get_id_for_region (const char *region_id) + { + gcc_assert (region_id); + return std::string ("cluster_region_") + region_id; + } + + dot::id + make_id (bool cluster = false) + { + if (cluster) + return std::string ("cluster_") + std::to_string (m_next_id++); + else + return std::string ("id_") + std::to_string (m_next_id++); + } + + bool + starts_node_p (const xml::element &e) + { + if (e.m_kind == "stack" + || e.m_kind == "heap-buffer" + || e.m_kind == "variable") // e.g. within globals + return true; + return false; + } + + void + on_input_xml_node (dot::subgraph &parent_subgraph, + xml::node &input_node) + { + xml::element *input_element = input_node.dyn_cast_element (); + if (!input_element) + return; + + dot::id sg_id = make_id (true); + + if (starts_node_p (*input_element)) + { + // Create node with table + xml::element table ("table", false); + xml::printer xp (table); + xp.set_attr ("border", "0"); + xp.set_attr ("cellborder", "1"); + xp.set_attr ("cellspacing", "0"); + + const int max_depth = get_depth (*input_element); + const int num_columns = max_depth + 2; + + dot::id id_of_node = make_id (); + on_xml_node (id_of_node, xp, *input_element, + max_depth, 0, num_columns); + + auto node = std::make_unique (std::move (id_of_node)); + node->m_attrs.add (dot::id ("shape"), + dot::id ("plaintext")); + + // xml must be done by now + + node->m_attrs.add (dot::id ("label"), + dot::id (table)); + + parent_subgraph.m_stmt_list.add_stmt (std::move (node)); + } + else + { + auto child_subgraph = std::make_unique (std::move (sg_id)); + + if (const char *label = input_element->get_attr ("label")) + child_subgraph->add_attr (dot::id ("label"), dot::id (label)); + + // recurse: + for (auto &iter : input_element->m_children) + on_input_xml_node (*child_subgraph, *iter); + parent_subgraph.m_stmt_list.add_stmt (std::move (child_subgraph)); + } + } + + enum class style { h1, h2 }; + + void + add_title_tr (const dot::id &id_of_node, + xml::printer &xp, + int num_columns, + const xml::element &input_element, + std::string heading, + enum style style, + enum dynalloc_state dynalloc_state) + { + xp.push_tag ("tr", true); + xp.push_tag ("td", false); + xp.set_attr ("colspan", std::to_string (num_columns)); + xp.set_attr ("cellpadding", "5"); + + const char *bgcolor; + const char *color; + if (const char *c = get_color_for_dynalloc_state (dynalloc_state)) + { + bgcolor = c; + color = "white"; + } + else + switch (style) + { + default: + gcc_unreachable (); + case style::h1: + // from diagnostic-format-html.cc: HTML_STYLE .linenum + bgcolor = "#0088ce"; + color = "white"; + break; + case style::h2: + // from diagnostic-format-html.cc: HTML_STYLE .events-hdr + bgcolor = "#393f44"; // pf-black-800 + color = "white"; + break; + } + + xp.set_attr ("bgcolor", bgcolor); + xp.push_tag ("font", false); + xp.set_attr ("color", color); + if (heading == "") + heading = " "; + xp.add_text (std::move (heading)); + xp.pop_tag ("font"); + + maybe_add_dst_port (id_of_node, xp, input_element); + + xp.pop_tag ("td"); + xp.pop_tag ("tr"); + } + + /* Recursively add to XP for INPUT_NODE and its descendents. */ + void + on_xml_node (const dot::id &id_of_node, + xml::printer &xp, + xml::node &input_node, + int max_depth, + int depth, + int num_columns) + { + bool recurse = true; + + xml::element *input_element = input_node.dyn_cast_element (); + if (!input_element) + return; + + if (input_element->m_kind == "concrete-bindings") + return; + if (input_element->m_kind == "padding") + return; + + if (input_element->m_kind == "stack") + { + add_title_tr (id_of_node, xp, num_columns, *input_element, "Stack", + style::h1, dynalloc_state::unknown); + } + else if (input_element->m_kind == "stack-frame") + { + if (const char *function = input_element->get_attr ("function")) + add_title_tr (id_of_node, xp, num_columns, *input_element, + std::string ("Frame: ") + function, + style::h2, dynalloc_state::unknown); + } + else if (input_element->m_kind == "heap-buffer") + { + const char *extents = input_element->get_attr ("dynamic-extents"); + enum dynalloc_state dynalloc_state = get_dynalloc_state (*input_element); + if (auto region_id = input_element->get_attr ("region_id")) + m_region_id_to_dynalloc_state[region_id] = dynalloc_state; + const char *type = input_element->get_attr ("type"); + pretty_printer pp; + switch (dynalloc_state) + { + default: + gcc_unreachable (); + + case dynalloc_state::unknown: + case dynalloc_state::nonnull: + if (type) + { + if (extents) + pp_printf (&pp, "%s (%s byte allocation)", + type, extents); + else + pp_printf (&pp, "%s", type); + } + else + { + if (extents) + pp_printf (&pp, "%s byte allocation", + extents); + } + break; + + case dynalloc_state::unchecked: + if (type) + { + if (extents) + pp_printf (&pp, "%s (unchecked %s byte allocation)", + type, extents); + } + else + { + if (extents) + pp_printf (&pp, "Unchecked %s byte allocation", + extents); + } + break; + + case dynalloc_state::freed: + // TODO: show deallocator + // TODO: show deallocation event + pp_printf (&pp, "Freed buffer"); + break; + } + add_title_tr (id_of_node, xp, num_columns, *input_element, + pp_formatted_text (&pp), + style::h2, + dynalloc_state); + } + else + { + xp.push_tag ("tr", true); + if (depth > 0) + { + /* Indent, by create a spanning "depth" columns. */ + xp.push_tag ("td", false); + xp.set_attr ("colspan", std::to_string (depth)); + xp.add_text (" "); // graphviz doesn't like + xp.pop_tag ("td"); + } + if (m_show_tags) + { + // Debug: show XML tag + xp.push_tag ("td", false); + xp.add_text ("<"); + xp.add_text (input_element->m_kind); + xp.add_text (">"); + xp.pop_tag ("td"); + } + if (input_element->m_kind == "variable") + { + const char *name = input_element->get_attr ("name"); + gcc_assert (name); + xp.push_tag ("td", false); + maybe_add_dst_port (id_of_node, xp, *input_element); + push_src_text (xp); + xp.add_text (name); + pop_src_text (xp); + xp.pop_tag ("td"); + } + else if (input_element->m_kind == "element") + { + const char *index = input_element->get_attr ("index"); + gcc_assert (index); + xp.push_tag ("td", false); + maybe_add_dst_port (id_of_node, xp, *input_element); + push_src_text (xp); + xp.add_text ("["); + xp.add_text (index); + xp.add_text ("]"); + pop_src_text (xp); + xp.pop_tag ("td"); + } + else if (input_element->m_kind == "field") + { + const char *name = input_element->get_attr ("name"); + gcc_assert (name); + xp.push_tag ("td", false); + maybe_add_dst_port (id_of_node, xp, *input_element); + push_src_text (xp); + xp.add_text ("."); + xp.add_text (name); + pop_src_text (xp); + xp.pop_tag ("td"); + } + if (const char *type = input_element->get_attr ("type")) + { + xp.push_tag ("td", false); + if (max_depth > depth) + xp.set_attr ("colspan", std::to_string (max_depth - depth)); + xp.set_attr ("align", "right"); + push_src_text (xp); + xp.add_text (type); + pop_src_text (xp); + xp.pop_tag ("td"); + } + if (auto value = input_element->find_child_element ("value-of-region")) + { + xp.push_tag ("td", false); + for (auto &iter : value->m_children) + if (auto child_element = iter->dyn_cast_element ()) + print_value (id_of_node, xp, *child_element); + xp.pop_tag ("td"); + recurse = false; + } + xp.pop_tag ("tr"); + } + + if (recurse) + for (auto &iter : input_element->m_children) + on_xml_node (id_of_node, xp, *iter, max_depth, depth + 1, num_columns); + } + + void + push_src_text (xml::printer &xp) + { + xp.push_tag ("font"); + xp.set_attr ("color", "blue"); + } + + void + pop_src_text (xml::printer &xp) + { + xp.pop_tag ("font"); + } + + void + print_value (const dot::id &id_of_node, + xml::printer &xp, + xml::element &input_element) + { + if (input_element.m_kind == "pointer-to-region") + if (const char *dst_region_id = input_element.get_attr ("region_id")) + { + dot::id src_port_id = make_id (); + xp.set_attr ("port", src_port_id.m_str); + m_pending_edges.push_back + ({dot::node_id (id_of_node, + dot::port (src_port_id, + dot::compass_pt::e)), + dst_region_id}); + } + + if (input_element.m_kind == "uninitialized") + { + xp.add_text ("(uninitialized)"); + return; + } + + if (auto dump_text = input_element.get_attr ("dump-text")) + xp.add_text (dump_text); + } + + /* If INPUT_ELEMENT has a "region_id", add a port to XP for possible + incoming edges to use. */ + + void + maybe_add_dst_port (const dot::id &id_of_node, + xml::printer &xp, + const xml::element &input_element) + { + if (const char *region_id = input_element.get_attr ("region_id")) + { + dot::id dst_id = make_id (); + dot::node_id node_id (id_of_node, + dot::port (dst_id/*, + dot::compass_pt::w*/)); + xp.set_attr ("port", dst_id.m_str); + m_region_id_to_dst_node_id.emplace (std::string (region_id), + std::move (node_id)); + } + } + + +private: + int m_next_id; + std::vector m_pending_edges; + std::map m_region_id_to_dst_node_id; + std::map m_region_id_to_dynalloc_state; + bool m_show_tags; +}; + +std::unique_ptr +make_dot_graph_from_xml_state (const xml::document &xml_state) +{ + return std::make_unique (xml_state); +} diff --git a/gcc/diagnostic-state.h b/gcc/diagnostic-state.h new file mode 100644 index 00000000000..68c3e00266e --- /dev/null +++ b/gcc/diagnostic-state.h @@ -0,0 +1,37 @@ +/* Capturing changing state in diagnostic paths. + Copyright (C) 2025 Free Software Foundation, Inc. + Contributed by David Malcolm . + +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 +. */ + +#ifndef GCC_DIAGNOSTIC_STATE_H +#define GCC_DIAGNOSTIC_STATE_H + +/* We want to be able to express changing program states in diagnostic paths, + so that we can emit this in HTML and SARIF output, and to keep this + separate from implementation details of -fanalyzer. + + For now, we use xml::document as the type in the diagnostic subsystem + for (optionally) tracking the state at a diagnostic_event. */ + +namespace xml { class document; } +namespace dot { class graph; } + +extern std::unique_ptr +make_dot_graph_from_xml_state (const xml::document &xml_state); + +#endif /* GCC_DIAGNOSTIC_STATE_H */ diff --git a/gcc/digraph.cc b/gcc/digraph.cc index 10e77056dee..196b4b1438e 100644 --- a/gcc/digraph.cc +++ b/gcc/digraph.cc @@ -18,6 +18,8 @@ You should have received a copy of the GNU General Public License along with GCC; see the file COPYING3. If not see . */ +#define INCLUDE_STRING +#define INCLUDE_VECTOR #include "config.h" #include "system.h" #include "coretypes.h" diff --git a/gcc/doc/analyzer.texi b/gcc/doc/analyzer.texi index 755db7cde53..4dd14c8895c 100644 --- a/gcc/doc/analyzer.texi +++ b/gcc/doc/analyzer.texi @@ -619,6 +619,25 @@ python-xdot) @item @code{-fdump-analyzer-exploded-nodes-2} which dumps a @file{SRC.eg.txt} file containing the full @code{exploded_graph}. +@item @code{-fdiagnostics-add-output=experimental-html:show-state-diagrams=yes} +which writes out the diagnostics in HTML form, and generates SVG state +diagrams visualizing the state of memory at each event (inspired by the +"ddd" debugger). These can be seen by pressing 'j' and 'k' to single-step +forward and backward through events. Note that these SVG diagrams are +created from an intermediate XML representation generated from +@code{program_state} objects. The XML representation can be easier to +read - for example, rather than storing the contents of memory via byte +offsets, it uses fields for structs and element indexes for arrays, +recursively. However it is a different representation, and thus bugs could +be hidden by this transformation. Generating the SVG diagrams requires +an invocation of "dot" per event, so it noticeably slows down diagnostic +emission, hence the opt-in command-line flag. The XML and ``dot'' +representations can be seen by @code{__analyzer_dump_xml} and +@code{__analyzer_dump_dot} below (writing them to stderr), or by adding +@code{show-state-diagrams-xml=yes} and +@code{show-state-diagrams-dot-src=yes} to the html sink, which shows +them within the generated HTML next to the generated SVG. + @end itemize Assuming that you have the @@ -688,6 +707,15 @@ extern void __analyzer_dump_capacity (const void *ptr); will emit a warning describing the capacity of the base region of the region pointed to by the 1st argument. +@item __analyzer_dump_dot +@smallexample +__analyzer_dump_dot (); +@end smallexample + +will dump GraphViz .dot source to stderr reaches the call in its +traversal of the source. This .dot source implements a diagram +describing the analyzer’s state. + @item __analyzer_dump_escaped @smallexample extern void __analyzer_dump_escaped (void); @@ -764,6 +792,14 @@ will emit a warning describing the state of the 2nd argument a name matching the 1st argument (which must be a string literal). This is for use when debugging, and may be of use in DejaGnu tests. +@item __analyzer_dump_xml +@smallexample +__analyzer_dump_xml (); +@end smallexample + +will dump the copious information about the analyzer's state each time it +reaches the call in its traversal of the source. + @item __analyzer_eval @smallexample __analyzer_eval (expr); diff --git a/gcc/doc/invoke.texi b/gcc/doc/invoke.texi index 91b0a201e1b..693bd57691e 100644 --- a/gcc/doc/invoke.texi +++ b/gcc/doc/invoke.texi @@ -6131,6 +6131,18 @@ in this release. @end table +There is also this key intended for use by GCC developers, rather than +end-users, and subject to change or removal without notice: + +@table @gcctabopt + +@item xml-state=@r{[}yes@r{|}no@r{]} +This is a debugging feature and defaults to @code{no}. +If @code{xml-state=yes}, then attempt to capture detailed state information +from @option{-fanalyzer} in the generated SARIF. + +@end table + @item experimental-html Emit diagnostics to a file in HTML format. This scheme is experimental, and may go away in future GCC releases. The keys and details of the output @@ -6154,6 +6166,35 @@ for viewing results. Defaults to yes. @end table +There are also these keys intended for use by GCC developers, rather than +end-users, and subject to change or removal without notice: + +@table @gcctabopt + +@item show-state-diagrams=@r{[}yes@r{|}no@r{]} +This is a debugging feature and defaults to @code{no}. +If @code{show-state-diagrams=yes}, then attempt to use @command{dot} to +generate SVG diagrams in the generated HTML, visualizing the state at each +event in a diagnostic path. +These are visible by pressing ``j'' and ``k'' to single-step forward and +backward through events. Enabling this option will slow down +HTML generation. + +@item show-state-diagrams-dot-src=@r{[}yes@r{|}no@r{]} +This is a debugging feature and defaults to @code{no}. +If @code{show-state-diagrams-dot-src=yes} +then if @code{show-state-diagrams=yes}, +the generated state diagrams will also show the .dot source input to +GraphViz used for the diagram. + +@item show-state-diagrams-xml=@r{[}yes@r{|}no@r{]} +This is a debugging feature and defaults to @code{no}. +If @code{show-state-diagrams-xml=yes} +then if @code{show-state-diagrams=yes}, the generated state diagrams will +also show an XML representation of the state. + +@end table + @end table For example, diff --git a/gcc/graphviz.cc b/gcc/graphviz.cc index 65bb6cbcd76..f8cedc023b8 100644 --- a/gcc/graphviz.cc +++ b/gcc/graphviz.cc @@ -18,19 +18,39 @@ You should have received a copy of the GNU General Public License along with GCC; see the file COPYING3. If not see . */ +#define INCLUDE_MAP +#define INCLUDE_STRING +#define INCLUDE_VECTOR #include "config.h" #include "system.h" #include "coretypes.h" #include "graphviz.h" +#include "xml.h" +#include "xml-printer.h" +#include "pex.h" +#include "selftest.h" -/* graphviz_out's ctor, wrapping PP. */ - -graphviz_out::graphviz_out (pretty_printer *pp) +dot::writer::writer (pretty_printer &pp) : m_pp (pp), m_indent (0) { } +/* Print the current indent to the underlying pp. */ + +void +dot::writer::write_indent () +{ + for (int i = 0; i < m_indent * 4; ++i) + pp_space (get_pp ()); +} + +graphviz_out::graphviz_out (pretty_printer *pp) +: writer (*pp) +{ + gcc_assert (pp); +} + /* Formatted print of FMT. */ void @@ -40,8 +60,8 @@ graphviz_out::print (const char *fmt, ...) va_start (ap, fmt); text_info text (fmt, &ap, errno); - pp_format (m_pp, &text); - pp_output_formatted_text (m_pp); + pp_format (get_pp (), &text); + pp_output_formatted_text (get_pp ()); va_end (ap); } @@ -57,20 +77,11 @@ graphviz_out::println (const char *fmt, ...) va_start (ap, fmt); text_info text (fmt, &ap, errno); - pp_format (m_pp, &text); - pp_output_formatted_text (m_pp); + pp_format (get_pp (), &text); + pp_output_formatted_text (get_pp ()); va_end (ap); - pp_newline (m_pp); -} - -/* Print the current indent to the underlying pp. */ - -void -graphviz_out::write_indent () -{ - for (int i = 0; i < m_indent * 2; ++i) - pp_space (m_pp); + pp_newline (get_pp ()); } /* Write the start of an HTML-like row via , writing to the stream @@ -79,8 +90,8 @@ graphviz_out::write_indent () void graphviz_out::begin_tr () { - pp_string (m_pp, ""); - pp_write_text_to_stream (m_pp); + pp_string (get_pp (), ""); + pp_write_text_to_stream (get_pp ()); } /* Write the end of an HTML-like row via , writing to the stream @@ -89,8 +100,8 @@ graphviz_out::begin_tr () void graphviz_out::end_tr () { - pp_string (m_pp, ""); - pp_write_text_to_stream (m_pp); + pp_string (get_pp (), ""); + pp_write_text_to_stream (get_pp ()); } /* Write the start of an HTML-like , writing to the stream @@ -99,8 +110,8 @@ graphviz_out::end_tr () void graphviz_out::begin_td () { - pp_string (m_pp, ""); - pp_write_text_to_stream (m_pp); + pp_string (get_pp (), ""); + pp_write_text_to_stream (get_pp ()); } /* Write the end of an HTML-like , writing to the stream @@ -109,8 +120,8 @@ graphviz_out::begin_td () void graphviz_out::end_td () { - pp_string (m_pp, ""); - pp_write_text_to_stream (m_pp); + pp_string (get_pp (), ""); + pp_write_text_to_stream (get_pp ()); } /* Write the start of an HTML-like row via , writing to the stream @@ -119,8 +130,8 @@ graphviz_out::end_td () void graphviz_out::begin_trtd () { - pp_string (m_pp, ""); - pp_write_text_to_stream (m_pp); + pp_string (get_pp (), ""); + pp_write_text_to_stream (get_pp ()); } /* Write the end of an HTML-like row via , writing to the stream @@ -129,6 +140,628 @@ graphviz_out::begin_trtd () void graphviz_out::end_tdtr () { - pp_string (m_pp, ""); - pp_write_text_to_stream (m_pp); + pp_string (get_pp (), ""); + pp_write_text_to_stream (get_pp ()); +} + +namespace dot { + +// Definitions + +// struct ast_node + +void +ast_node::dump () const +{ + pretty_printer pp; + pp.set_output_stream (stderr); + writer w (pp); + print (w); + pp_newline (&pp); + pp_flush (&pp); +} + +// struct id + +id::id (std::string str) +: m_str (std::move (str)), + m_kind (is_identifier_p (m_str.c_str ()) + ? kind::identifier + : kind::quoted) +{ +} + +id::id (const xml::node &n) +: m_kind (kind::html) +{ + pretty_printer pp; + n.write_as_xml (&pp, 0, true); + m_str = pp_formatted_text (&pp); +} + +void +id::print (writer &w) const +{ + switch (m_kind) + { + default: + gcc_unreachable (); + + case kind::identifier: + w.write_string (m_str.c_str ()); + break; + + case kind::quoted: + w.write_character ('"'); + for (auto ch : m_str) + if (ch == '"') + w.write_string ("\\\""); + else + w.write_character (ch); + w.write_character ('"'); + break; + + case kind::html: + w.write_character ('<'); + w.write_string (m_str.c_str ()); + w.write_character ('>'); + break; + + } +} + +bool +id::is_identifier_p (const char *str) +{ + const char initial_ch = *str; + if (initial_ch != '_' && !ISALPHA (initial_ch)) + return false; + for (const char *iter = str + 1; *iter; ++iter) + { + const char iter_ch = *iter; + if (iter_ch != '_' && !ISALNUM (iter_ch)) + return false; + } + return true; +} + +// struct kv_pair + +void +kv_pair::print (writer &w) const +{ + m_key.print (w); + w.write_character ('='); + m_value.print (w); +} + +// struct attr_list + +void +attr_list::print (writer &w) const +{ + if (m_kvs.empty ()) + return; + w.write_string (" ["); + for (auto iter = m_kvs.begin (); iter != m_kvs.end (); ++iter) + { + if (iter != m_kvs.begin ()) + w.write_string ("; "); + iter->print (w); + } + w.write_string ("]"); +} + +// struct stmt_list + +void +stmt_list::print (writer &w) const +{ + for (auto &stmt : m_stmts) + { + w.write_indent (); + stmt->print (w); + w.write_string (";"); + w.write_newline (); + } +} + +void +stmt_list::add_edge (node_id src_id, node_id dst_id) +{ + m_stmts.push_back + (std::make_unique + (std::move (src_id), std::move (dst_id))); +} + +void +stmt_list::add_attr (id key, id value) +{ + add_stmt + (std::make_unique (kv_pair (std::move (key), + std::move (value)))); +} + +// struct graph + +void +graph::print (writer &w) const +{ + w.write_indent (); + w.write_string ("digraph "); + if (m_id) + { + m_id->print (w); + w.write_character (' '); + } + w.write_string ("{"); + w.write_newline (); + + w.indent (); + m_stmt_list.print (w); + w.outdent (); + + w.write_indent (); + w.write_string ("}"); + w.write_newline (); +} + +// struct stmt_with_attr_list : public stmt + +void +stmt_with_attr_list::set_label (dot::id value) +{ + m_attrs.add (dot::id ("label"), std::move (value)); +} + +// struct node_stmt : public stmt_with_attr_list + +void +node_stmt::print (writer &w) const +{ + m_id.print (w); + m_attrs.print (w); +} + +// struct attr_stmt : public stmt_with_attr_list + +void +attr_stmt::print (writer &w) const +{ + switch (m_kind) + { + default: + gcc_unreachable (); + case kind::graph: + w.write_string ("graph"); + break; + case kind::node: + w.write_string ("node"); + break; + case kind::edge: + w.write_string ("edge"); + break; + } + m_attrs.print (w); +} + +// struct kv_stmt : public stmt + +void +kv_stmt::print (writer &w) const +{ + m_kv.print (w); +} + +// struct node_id + +void +node_id::print (writer &w) const +{ + m_id.print (w); + if (m_port) + m_port->print (w); +} + +// struct port + +void +port::print (writer &w) const +{ + if (m_id) + { + w.write_character (':'); + m_id->print (w); + } + if (m_compass_pt) + { + w.write_character (':'); + switch (*m_compass_pt) + { + default: + gcc_unreachable (); + case compass_pt::n: + w.write_string ("n"); + break; + case compass_pt::ne: + w.write_string ("ne"); + break; + case compass_pt::e: + w.write_string ("e"); + break; + case compass_pt::se: + w.write_string ("se"); + break; + case compass_pt::s: + w.write_string ("s"); + break; + case compass_pt::sw: + w.write_string ("sw"); + break; + case compass_pt::w: + w.write_string ("w"); + break; + case compass_pt::nw: + w.write_string ("nw"); + break; + case compass_pt::c: + w.write_string ("c"); + break; + } + } +} + +// struct edge_stmt : public stmt_with_attr_list + +void +edge_stmt::print (writer &w) const +{ + for (auto iter = m_node_ids.begin (); iter != m_node_ids.end (); ++iter) + { + if (iter != m_node_ids.begin ()) + w.write_string (" -> "); + iter->print (w); + } + m_attrs.print (w); +} + +// struct subgraph : public stmt + +void +subgraph::print (writer &w) const +{ + w.write_newline (); + w.write_indent (); + w.write_string ("subgraph "); + m_id.print (w); + w.write_string (" {"); + w.write_newline (); + + w.indent (); + m_stmt_list.print (w); + w.outdent (); + w.write_newline (); + + w.write_indent (); + w.write_string ("}"); // newline and semicolon added by stmt_list +} + +/* Convert G to graphviz source, attempt to invoke "dot -Tsvg" on it + as a subprocess, and get the SVG source from stdout, or nullptr + if there was a problem. */ + +static std::unique_ptr +make_svg_document_buffer_from_graph (const graph &g) +{ + /* Ideally there would be a way of doing this without + invoking dot as a subprocess. */ + + std::vector args; + args.push_back ("dot"); + args.push_back ("-Tsvg"); + + pex p (0, "dot", nullptr); + + { + auto pipe_stdin = p.input_file (true, nullptr); + gcc_assert (pipe_stdin.m_file); + pretty_printer pp; + pp.set_output_stream (pipe_stdin.m_file); + writer w (pp); + g.print (w); + pp_flush (&pp); + } + + int err = 0; + const char * errmsg + = p.run (PEX_SEARCH, + "dot", args, nullptr, nullptr, &err); + auto pipe_stdout = p.read_output (); + auto content = pipe_stdout.read_all (); + + if (errmsg) + return nullptr; + if (err) + return nullptr; + + std::string result; + result.reserve (content->size () + 1); + for (auto &iter : *content) + result.push_back (iter); + return std::make_unique (std::move (result)); +} + +/* Convert G to graphviz source, attempt to invoke "dot -Tsvg" on it + as a subprocess, and get the SVG source from stdout, and extract + the "svg" subtree as an xml::raw node. + + Note that this + (a) invokes "dot" as a subprocess + (b) assumes that we trust the output from "dot". + + Return nullptr if there was a problem. */ + +std::unique_ptr +make_svg_from_graph (const graph &g) +{ + auto svg_src = make_svg_document_buffer_from_graph (g); + if (!svg_src) + return nullptr; + + /* Skip past the XML header to the parts we care about. */ + auto pos = svg_src->find (""); + if (indent) + pp_newline (pp); +} + // struct raw : public node void @@ -363,6 +406,15 @@ test_printer () " \n" " \n" "\n"); + + xml::element *foo = top.find_child_element ("foo"); + ASSERT_TRUE (foo); + ASSERT_EQ (top.find_child_element ("not-foo"), nullptr); + xml::element *bar = foo->find_child_element ("bar"); + ASSERT_TRUE (bar); + ASSERT_STREQ (bar->get_attr ("size"), "3"); + ASSERT_STREQ (bar->get_attr ("color"), "red"); + ASSERT_EQ (bar->get_attr ("airspeed-velocity"), nullptr); } // Verify that element attributes preserve insertion order. @@ -393,6 +445,19 @@ test_attribute_ordering () "\n"); } +static void +test_comment () +{ + xml::document doc; + doc.add_comment ("hello"); + doc.add_comment ("world"); + ASSERT_XML_PRINT_EQ + (doc, + "\n" + "\n" + "\n"); + +} /* Run all of the selftests within this file. */ void @@ -401,6 +466,7 @@ xml_cc_tests () test_no_dtd (); test_printer (); test_attribute_ordering (); + test_comment (); } } // namespace selftest diff --git a/gcc/xml.h b/gcc/xml.h index 952cfa4376b..15591290c7d 100644 --- a/gcc/xml.h +++ b/gcc/xml.h @@ -30,6 +30,8 @@ struct node; struct document; struct element; struct doctypedecl; + struct comment; + struct raw; struct node { @@ -38,7 +40,11 @@ struct node int depth, bool indent) const = 0; virtual text *dyn_cast_text () { - return 0; + return nullptr; + } + virtual element *dyn_cast_element () + { + return nullptr; } void dump (FILE *out) const; void DEBUG_FUNCTION dump () const { dump (stderr); } @@ -66,6 +72,9 @@ struct node_with_children : public node void add_child (std::unique_ptr node); void add_text (std::string str); void add_text_from_pp (pretty_printer &pp); + void add_comment (std::string str); + + element *find_child_element (std::string kind) const; std::vector> m_children; }; @@ -90,10 +99,16 @@ struct element : public node_with_children m_preserve_whitespace (preserve_whitespace) {} + element *dyn_cast_element () final override + { + return this; + } + void write_as_xml (pretty_printer *pp, int depth, bool indent) const final override; void set_attr (const char *name, std::string value); + const char *get_attr (const char *name) const; std::string m_kind; bool m_preserve_whitespace; @@ -101,6 +116,21 @@ struct element : public node_with_children std::vector m_key_insertion_order; }; +/* An XML comment. */ + +struct comment : public node +{ + comment (std::string text) + : m_text (std::move (text)) + { + } + + void write_as_xml (pretty_printer *pp, + int depth, bool indent) const final override; + + std::string m_text; +}; + /* A fragment of raw XML source, to be spliced in directly. Use sparingly. */