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