• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
2 // -*- mode: C++ -*-
3 //
4 // Copyright 2020-2022 Google LLC
5 //
6 // Licensed under the Apache License v2.0 with LLVM Exceptions (the
7 // "License"); you may not use this file except in compliance with the
8 // License.  You may obtain a copy of the License at
9 //
10 //     https://llvm.org/LICENSE.txt
11 //
12 // Unless required by applicable law or agreed to in writing, software
13 // distributed under the License is distributed on an "AS IS" BASIS,
14 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 // See the License for the specific language governing permissions and
16 // limitations under the License.
17 //
18 // Author: Giuliano Procida
19 // Author: Siddharth Nayyar
20 
21 #include "reporting.h"
22 
23 #include <array>
24 #include <cstddef>
25 #include <deque>
26 #include <optional>
27 #include <ostream>
28 #include <sstream>
29 #include <string>
30 #include <string_view>
31 #include <type_traits>
32 #include <unordered_map>
33 #include <unordered_set>
34 #include <utility>
35 #include <vector>
36 
37 #include "comparison.h"
38 #include "error.h"
39 #include "fidelity.h"
40 #include "graph.h"
41 #include "post_processing.h"
42 
43 namespace stg {
44 namespace reporting {
45 
46 struct FormatDescriptor {
47   std::string_view name;
48   OutputFormat value;
49 };
50 
51 static constexpr std::array<FormatDescriptor, 5> kFormats{{
52   {"plain", OutputFormat::PLAIN},
53   {"flat",  OutputFormat::FLAT },
54   {"small", OutputFormat::SMALL},
55   {"short", OutputFormat::SHORT},
56   {"viz",   OutputFormat::VIZ  },
57 }};
58 
ParseOutputFormat(std::string_view format)59 std::optional<OutputFormat> ParseOutputFormat(std::string_view format) {
60   for (const auto& [name, value] : kFormats) {
61     if (name == format) {
62       return {value};
63     }
64   }
65   return {};
66 }
67 
operator <<(std::ostream & os,OutputFormatUsage)68 std::ostream& operator<<(std::ostream& os, OutputFormatUsage) {
69   os << "output formats:";
70   for (const auto& [name, _] : kFormats) {
71     os << ' ' << name;
72   }
73   return os << '\n';
74 }
75 
76 namespace {
77 
GetResolvedDescription(const Graph & graph,NameCache & names,Id id)78 std::string GetResolvedDescription(
79     const Graph& graph, NameCache& names, Id id) {
80   std::ostringstream os;
81   const auto [resolved, typedefs] = ResolveTypedefs(graph, id);
82   for (const auto& td : typedefs) {
83     os << '\'' << td << "' = ";
84   }
85   os << '\'' << Describe(graph, names)(resolved) << '\''
86      << DescribeExtra(graph)(resolved);
87   return os.str();
88 }
89 
90 // Prints a comparison to the given output stream. The comparison is printed
91 // with the given indentation and prefixed with the given prefix if it is not
92 // empty.
93 //
94 // It returns true if the comparison denotes addition or removal of a node.
PrintComparison(const Reporting & reporting,const Comparison & comparison,std::ostream & os,size_t indent,const std::string & prefix)95 bool PrintComparison(const Reporting& reporting, const Comparison& comparison,
96                      std::ostream& os, size_t indent,
97                      const std::string& prefix) {
98   os << std::string(indent, ' ');
99   if (!prefix.empty()) {
100     os << prefix << ' ';
101   }
102   const auto id1 = comparison.first;
103   const auto id2 = comparison.second;
104 
105   Check(id1.has_value() || id2.has_value())
106       << "internal error: Attempt to print comparison with nothing to compare.";
107 
108   if (!id2) {
109     os << DescribeKind(reporting.graph)(*id1) << " '"
110        << Describe(reporting.graph, reporting.names)(*id1)
111        << "'"
112        << DescribeExtra(reporting.graph)(*id1)
113        << " was removed\n";
114     return true;
115   }
116   if (!id1) {
117     os << DescribeKind(reporting.graph)(*id2) << " '"
118        << Describe(reporting.graph, reporting.names)(*id2)
119        << "'"
120        << DescribeExtra(reporting.graph)(*id2)
121        << " was added\n";
122     return true;
123   }
124 
125   const auto description1 =
126       GetResolvedDescription(reporting.graph, reporting.names, *id1);
127   const auto description2 =
128       GetResolvedDescription(reporting.graph, reporting.names, *id2);
129   os << DescribeKind(reporting.graph)(*id1) << ' ';
130   if (description1 == description2) {
131     os << description1 << " changed\n";
132   } else {
133     os << "changed from " << description1 << " to " << description2 << '\n';
134   }
135   return false;
136 }
137 
138 static constexpr size_t INDENT_INCREMENT = 2;
139 
140 class Plain {
141   // unvisited (absent) -> started (false) -> finished (true)
142   using Seen = std::unordered_map<Comparison, bool, HashComparison>;
143 
144  public:
Plain(const Reporting & reporting,std::ostream & output)145   Plain(const Reporting& reporting, std::ostream& output)
146       : reporting_(reporting), output_(output) {}
147 
148   void Report(const Comparison&);
149 
150  private:
151   const Reporting& reporting_;
152   std::ostream& output_;
153   Seen seen_;
154 
155   void Print(const Comparison&, size_t, const std::string&);
156 };
157 
Print(const Comparison & comparison,size_t indent,const std::string & prefix)158 void Plain::Print(const Comparison& comparison, size_t indent,
159            const std::string& prefix) {
160   if (PrintComparison(reporting_, comparison, output_, indent, prefix)) {
161     return;
162   }
163 
164   indent += INDENT_INCREMENT;
165   const auto it = reporting_.outcomes.find(comparison);
166   Check(it != reporting_.outcomes.end())
167       << "internal error: missing comparison";
168   const auto& diff = it->second;
169 
170   const bool holds_changes = diff.holds_changes;
171   std::pair<Seen::iterator, bool> insertion;
172 
173   if (holds_changes) {
174     insertion = seen_.insert({comparison, false});
175   }
176 
177   if (holds_changes && !insertion.second) {
178     if (!insertion.first->second) {
179       output_ << std::string(indent, ' ') << "(being reported)\n";
180     } else if (!diff.details.empty()) {
181       output_ << std::string(indent, ' ') << "(already reported)\n";
182     }
183     return;
184   }
185 
186   for (const auto& detail : diff.details) {
187     if (!detail.edge_) {
188       output_ << std::string(indent, ' ') << detail.text_ << '\n';
189     } else {
190       Print(*detail.edge_, indent, detail.text_);
191     }
192   }
193 
194   if (holds_changes) {
195     insertion.first->second = true;
196   }
197 }
198 
Report(const Comparison & comparison)199 void Plain::Report(const Comparison& comparison) {
200   // unpack then print - want symbol diff forest rather than symbols diff tree
201   const auto& diff = reporting_.outcomes.at(comparison);
202   for (const auto& detail : diff.details) {
203     Print(*detail.edge_, 0, {});
204     // paragraph spacing
205     output_ << '\n';
206   }
207 }
208 
209 // Print the subtree of a diff graph starting at a given node and stopping at
210 // nodes that can themselves hold diffs, queuing such nodes for subsequent
211 // printing. Optionally, avoid printing "uninteresting" nodes - those that have
212 // no diff and no path to a diff that does not pass through a node that can hold
213 // diffs. Return whether the diff node's tree was intrinisically interesting.
214 class Flat {
215  public:
Flat(const Reporting & reporting,bool full,std::ostream & output)216   Flat(const Reporting& reporting, bool full, std::ostream& output)
217       : reporting_(reporting), full_(full), output_(output) {}
218 
219   void Report(const Comparison&);
220 
221  private:
222   const Reporting& reporting_;
223   const bool full_;
224   std::ostream& output_;
225   std::unordered_set<Comparison, HashComparison> seen_;
226   std::deque<Comparison> todo_;
227 
228   bool Print(const Comparison&, bool, std::ostream&, size_t,
229              const std::string&);
230 };
231 
Print(const Comparison & comparison,bool stop,std::ostream & os,size_t indent,const std::string & prefix)232 bool Flat::Print(const Comparison& comparison, bool stop, std::ostream& os,
233                  size_t indent, const std::string& prefix) {
234   // Nodes that represent additions or removal are always interesting and no
235   // recursion is possible.
236   if (PrintComparison(reporting_, comparison, os, indent, prefix)) {
237     return true;
238   }
239 
240   // Look up the diff (including node and edge changes).
241   const auto it = reporting_.outcomes.find(comparison);
242   Check(it != reporting_.outcomes.end())
243       << "internal error: missing comparison";
244   const auto& diff = it->second;
245 
246   // Check the stopping condition.
247   if (diff.holds_changes && stop) {
248     // If it's a new diff-holding node, queue it.
249     if (seen_.insert(comparison).second) {
250       todo_.push_back(comparison);
251     }
252     return false;
253   }
254   // The stop flag can only be false on a non-recursive call which should be for
255   // a diff-holding node.
256   if (!diff.holds_changes && !stop) {
257     Die() << "internal error: FlatPrint called on inappropriate node";
258   }
259 
260   // Indent before describing diff details.
261   indent += INDENT_INCREMENT;
262   bool interesting = diff.has_changes;
263   for (const auto& detail : diff.details) {
264     if (!detail.edge_) {
265       os << std::string(indent, ' ') << detail.text_ << '\n';
266       // Node changes may not be interesting, if we allow non-change diff
267       // details at some point. Just trust the has_changes flag.
268     } else {
269       // Edge changes are interesting if the target diff node is.
270       std::ostringstream sub_os;
271       // Set the stop flag to prevent recursion past diff-holding nodes.
272       bool sub_interesting =
273           Print(*detail.edge_, true, sub_os, indent, detail.text_);
274       // If the sub-tree was interesting, add it.
275       if (sub_interesting || full_) {
276         os << sub_os.str();
277       }
278       interesting |= sub_interesting;
279     }
280   }
281   return interesting;
282 }
283 
Report(const Comparison & comparison)284 void Flat::Report(const Comparison& comparison) {
285   // We want a symbol diff forest rather than a symbol table diff tree, so
286   // unpack the symbol table and then print the symbols specially.
287   const auto& diff = reporting_.outcomes.at(comparison);
288   for (const auto& detail : diff.details) {
289     std::ostringstream os;
290     const bool interesting = Print(*detail.edge_, true, os, 0, {});
291     if (interesting || full_) {
292       output_ << os.str() << '\n';
293     }
294   }
295   while (!todo_.empty()) {
296     auto comp = todo_.front();
297     todo_.pop_front();
298     std::ostringstream os;
299     const bool interesting = Print(comp, false, os, 0, {});
300     if (interesting || full_) {
301       output_ << os.str() << '\n';
302     }
303   }
304 }
305 
VizId(std::unordered_map<Comparison,size_t,HashComparison> & ids,const Comparison & comparison)306 size_t VizId(std::unordered_map<Comparison, size_t, HashComparison>& ids,
307              const Comparison& comparison) {
308   return ids.insert({comparison, ids.size()}).first->second;
309 }
310 
VizPrint(const Reporting & reporting,const Comparison & comparison,std::unordered_set<Comparison,HashComparison> & seen,std::unordered_map<Comparison,size_t,HashComparison> & ids,std::ostream & os)311 void VizPrint(const Reporting& reporting, const Comparison& comparison,
312               std::unordered_set<Comparison, HashComparison>& seen,
313               std::unordered_map<Comparison, size_t, HashComparison>& ids,
314               std::ostream& os) {
315   if (!seen.insert(comparison).second) {
316     return;
317   }
318 
319   const auto node = VizId(ids, comparison);
320 
321   const auto id1 = comparison.first;
322   const auto id2 = comparison.second;
323 
324   Check(id1.has_value() || id2.has_value())
325       << "internal error: Attempt to print comparison with nothing to compare.";
326 
327   if (!id2) {
328     os << "  \"" << node << "\" [color=red, label=\"" << "removed("
329        << Describe(reporting.graph, reporting.names)(*id1)
330        << DescribeExtra(reporting.graph)(*id1)
331        << ")\"]\n";
332     return;
333   }
334   if (!id1) {
335     os << "  \"" << node << "\" [color=red, label=\"" << "added("
336        << Describe(reporting.graph, reporting.names)(*id2)
337        << DescribeExtra(reporting.graph)(*id2)
338        << ")\"]\n";
339     return;
340   }
341 
342   const auto it = reporting.outcomes.find(comparison);
343   Check(it != reporting.outcomes.end()) << "internal error: missing comparison";
344   const auto& diff = it->second;
345   const char* colour = diff.has_changes ? "color=red, " : "";
346   const char* shape = diff.holds_changes ? "shape=rectangle, " : "";
347   const auto description1 =
348       GetResolvedDescription(reporting.graph, reporting.names, *id1);
349   const auto description2 =
350       GetResolvedDescription(reporting.graph, reporting.names, *id2);
351   if (description1 == description2) {
352     os << "  \"" << node << "\" [" << colour << shape << "label=\""
353        << description1 << "\"]\n";
354   } else {
355     os << "  \"" << node << "\" [" << colour << shape << "label=\""
356        << description1 << " -> " << description2 << "\"]\n";
357   }
358 
359   size_t index = 0;
360   for (const auto& detail : diff.details) {
361     if (!detail.edge_) {
362       // attribute change, create an implicit edge and node
363       os << "  \"" << node << "\" -> \"" << node << ':' << index << "\"\n"
364          << "  \"" << node << ':' << index << "\" [color=red, label=\""
365          << detail.text_ << "\"]\n";
366       ++index;
367     } else {
368       const auto& to = *detail.edge_;
369       VizPrint(reporting, to, seen, ids, os);
370       os << "  \"" << node << "\" -> \"" << VizId(ids, to) << "\" [label=\""
371          << detail.text_ << "\"]\n";
372     }
373   }
374 }
375 
ReportViz(const Reporting & reporting,const Comparison & comparison,std::ostream & output)376 void ReportViz(const Reporting& reporting, const Comparison& comparison,
377                std::ostream& output) {
378   output << "digraph \"ABI diff\" {\n";
379   std::unordered_set<Comparison, HashComparison> seen;
380   std::unordered_map<Comparison, size_t, HashComparison> ids;
381   VizPrint(reporting, comparison, seen, ids, output);
382   output << "}\n";
383 }
384 
385 template <typename T>
PrintFidelityReportBucket(T transition,const std::vector<std::string> & symbols_or_types,std::ostream & output)386 void PrintFidelityReportBucket(T transition,
387                                const std::vector<std::string>& symbols_or_types,
388                                std::ostream& output) {
389   output << symbols_or_types.size() << ' ' << transition << ":\n";
390   for (const auto& symbol_or_type : symbols_or_types) {
391     output << "  " << symbol_or_type << '\n';
392   }
393   output << '\n';
394 }
395 
396 }  // namespace
397 
Report(const Reporting & reporting,const Comparison & comparison,std::ostream & output)398 void Report(const Reporting& reporting, const Comparison& comparison,
399             std::ostream& output) {
400   switch (reporting.options.format) {
401     case OutputFormat::PLAIN: {
402       Plain(reporting, output).Report(comparison);
403       break;
404     }
405     case OutputFormat::FLAT:
406     case OutputFormat::SMALL: {
407       bool full = reporting.options.format == OutputFormat::FLAT;
408       Flat(reporting, full, output).Report(comparison);
409       break;
410     }
411     case OutputFormat::SHORT: {
412       std::stringstream report;
413       Flat(reporting, false, report).Report(comparison);
414       std::vector<std::string> report_lines;
415       std::string line;
416       while (std::getline(report, line)) {
417         report_lines.push_back(line);
418       }
419       report_lines = stg::PostProcess(report_lines,
420                                       reporting.options.max_crc_only_changes);
421       for (const auto& line : report_lines) {
422         output << line << '\n';
423       }
424       break;
425     }
426     case OutputFormat::VIZ: {
427       ReportViz(reporting, comparison, output);
428       break;
429     }
430   }
431 }
432 
FidelityDiff(const stg::FidelityDiff & diff,std::ostream & output)433 bool FidelityDiff(const stg::FidelityDiff& diff, std::ostream& output) {
434   bool diffs_reported = false;
435   auto print_bucket = [&diff, &output, &diffs_reported](auto&& from,
436                                                         auto&& to) {
437     auto transition = std::make_pair(from, to);
438     if constexpr (std::is_same_v<decltype(from), SymbolFidelity&&>) {
439       auto it = diff.symbol_transitions.find(transition);
440       if (it != diff.symbol_transitions.end()) {
441         PrintFidelityReportBucket(transition, it->second, output);
442         diffs_reported = true;
443       }
444     } else if constexpr (std::is_same_v<decltype(from), TypeFidelity&&>) {
445       auto it = diff.type_transitions.find(transition);
446       if (it != diff.type_transitions.end()) {
447         PrintFidelityReportBucket(transition, it->second, output);
448         diffs_reported = true;
449       }
450     }
451   };
452 
453   print_bucket(TypeFidelity::FULLY_DEFINED, TypeFidelity::ABSENT);
454   print_bucket(TypeFidelity::DECLARATION_ONLY, TypeFidelity::ABSENT);
455   print_bucket(TypeFidelity::FULLY_DEFINED, TypeFidelity::DECLARATION_ONLY);
456   print_bucket(SymbolFidelity::TYPED, SymbolFidelity::UNTYPED);
457   print_bucket(TypeFidelity::ABSENT, TypeFidelity::DECLARATION_ONLY);
458   print_bucket(TypeFidelity::DECLARATION_ONLY, TypeFidelity::FULLY_DEFINED);
459   print_bucket(SymbolFidelity::UNTYPED, SymbolFidelity::TYPED);
460   print_bucket(TypeFidelity::ABSENT, TypeFidelity::FULLY_DEFINED);
461   return diffs_reported;
462 }
463 
464 }  // namespace reporting
465 }  // namespace stg
466