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