summaryrefslogtreecommitdiffstats
path: root/js/src/devtools/rootAnalysis/t
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 09:22:09 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 09:22:09 +0000
commit43a97878ce14b72f0981164f87f2e35e14151312 (patch)
tree620249daf56c0258faa40cbdcf9cfba06de2a846 /js/src/devtools/rootAnalysis/t
parentInitial commit. (diff)
downloadfirefox-43a97878ce14b72f0981164f87f2e35e14151312.tar.xz
firefox-43a97878ce14b72f0981164f87f2e35e14151312.zip
Adding upstream version 110.0.1.upstream/110.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'js/src/devtools/rootAnalysis/t')
-rw-r--r--js/src/devtools/rootAnalysis/t/exceptions/source.cpp57
-rw-r--r--js/src/devtools/rootAnalysis/t/exceptions/test.py21
-rw-r--r--js/src/devtools/rootAnalysis/t/graph/source.cpp90
-rw-r--r--js/src/devtools/rootAnalysis/t/graph/test.py54
-rw-r--r--js/src/devtools/rootAnalysis/t/hazards/source.cpp459
-rw-r--r--js/src/devtools/rootAnalysis/t/hazards/test.py101
-rw-r--r--js/src/devtools/rootAnalysis/t/sixgill-tree/source.cpp76
-rw-r--r--js/src/devtools/rootAnalysis/t/sixgill-tree/test.py63
-rw-r--r--js/src/devtools/rootAnalysis/t/sixgill.py70
-rw-r--r--js/src/devtools/rootAnalysis/t/suppression/source.cpp72
-rw-r--r--js/src/devtools/rootAnalysis/t/suppression/test.py21
-rw-r--r--js/src/devtools/rootAnalysis/t/testlib.py230
-rw-r--r--js/src/devtools/rootAnalysis/t/virtual/source.cpp292
-rw-r--r--js/src/devtools/rootAnalysis/t/virtual/test.py91
14 files changed, 1697 insertions, 0 deletions
diff --git a/js/src/devtools/rootAnalysis/t/exceptions/source.cpp b/js/src/devtools/rootAnalysis/t/exceptions/source.cpp
new file mode 100644
index 0000000000..8d38a790a1
--- /dev/null
+++ b/js/src/devtools/rootAnalysis/t/exceptions/source.cpp
@@ -0,0 +1,57 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Simply including <exception> was enough to crash sixgill at one point.
+#include <exception>
+
+#define ANNOTATE(property) __attribute__((annotate(property)))
+
+struct Cell {
+ int f;
+} ANNOTATE("GC Thing");
+
+extern void GC() ANNOTATE("GC Call");
+
+void GC() {
+ // If the implementation is too trivial, the function body won't be emitted at
+ // all.
+ asm("");
+}
+
+class RAII_GC {
+ public:
+ RAII_GC() {}
+ ~RAII_GC() { GC(); }
+};
+
+// ~AutoSomething calls GC because of the RAII_GC field. The constructor,
+// though, should *not* GC -- unless it throws an exception. Which is not
+// possible when compiled with -fno-exceptions. This test will try it both
+// ways.
+class AutoSomething {
+ RAII_GC gc;
+
+ public:
+ AutoSomething() : gc() {
+ asm(""); // Ooh, scary, this might throw an exception
+ }
+ ~AutoSomething() { asm(""); }
+};
+
+extern Cell* getcell();
+
+extern void usevar(Cell* cell);
+
+void f() {
+ Cell* thing = getcell(); // Live range starts here
+
+ // When compiling with -fexceptions, there should be a hazard below. With
+ // -fno-exceptions, there should not be one. We will check both.
+ {
+ AutoSomething smth; // Constructor can GC only if exceptions are enabled
+ usevar(thing); // Live range ends here
+ } // In particular, 'thing' is dead at the destructor, so no hazard
+}
diff --git a/js/src/devtools/rootAnalysis/t/exceptions/test.py b/js/src/devtools/rootAnalysis/t/exceptions/test.py
new file mode 100644
index 0000000000..a40753d87a
--- /dev/null
+++ b/js/src/devtools/rootAnalysis/t/exceptions/test.py
@@ -0,0 +1,21 @@
+# flake8: noqa: F821
+
+test.compile("source.cpp", "-fno-exceptions")
+test.run_analysis_script("gcTypes")
+
+hazards = test.load_hazards()
+assert len(hazards) == 0
+
+# If we compile with exceptions, then there *should* be a hazard because
+# AutoSomething::AutoSomething might throw an exception, which would cause the
+# partially-constructed value to be torn down, which will call ~RAII_GC.
+
+test.compile("source.cpp", "-fexceptions")
+test.run_analysis_script("gcTypes")
+
+hazards = test.load_hazards()
+assert len(hazards) == 1
+hazard = hazards[0]
+assert hazard.function == "void f()"
+assert hazard.variable == "thing"
+assert "AutoSomething::AutoSomething" in hazard.GCFunction
diff --git a/js/src/devtools/rootAnalysis/t/graph/source.cpp b/js/src/devtools/rootAnalysis/t/graph/source.cpp
new file mode 100644
index 0000000000..0adff8d532
--- /dev/null
+++ b/js/src/devtools/rootAnalysis/t/graph/source.cpp
@@ -0,0 +1,90 @@
+#define ANNOTATE(property) __attribute__((annotate(property)))
+
+extern void GC() ANNOTATE("GC Call");
+
+void GC() {
+ // If the implementation is too trivial, the function body won't be emitted at
+ // all.
+ asm("");
+}
+
+extern void g(int x);
+extern void h(int x);
+
+void f(int x) {
+ if (x % 3) {
+ GC();
+ g(x);
+ }
+ h(x);
+}
+
+void g(int x) {
+ if (x % 2) f(x);
+ h(x);
+}
+
+void h(int x) {
+ if (x) {
+ f(x - 1);
+ g(x - 1);
+ }
+}
+
+void leaf() { asm(""); }
+
+void nonrecursive_root() {
+ leaf();
+ leaf();
+ GC();
+}
+
+void self_recursive(int x) {
+ if (x) self_recursive(x - 1);
+}
+
+// Set up the graph
+//
+// n1 <--> n2 n4 <--> n5
+// \ /
+// --> n3 <---------
+// \
+// ---> n6 --> n7 <---> n8 --> n9
+//
+// So recursive roots are one of (n1, n2) plus one of (n4, n5).
+extern void n1(int x);
+extern void n2(int x);
+extern void n3(int x);
+extern void n4(int x);
+extern void n5(int x);
+extern void n6(int x);
+extern void n7(int x);
+extern void n8(int x);
+extern void n9(int x);
+
+void n1(int x) { n2(x); }
+
+void n2(int x) {
+ if (x) n1(x - 1);
+ n3(x);
+}
+
+void n4(int x) { n5(x); }
+
+void n5(int x) {
+ if (x) n4(x - 1);
+ n3(x);
+}
+
+void n3(int x) { n6(x); }
+
+void n6(int x) { n7(x); }
+
+void n7(int x) { n8(x); }
+
+void n8(int x) {
+ if (x) n7(x - 1);
+ n9(x);
+}
+
+void n9(int x) { asm(""); }
diff --git a/js/src/devtools/rootAnalysis/t/graph/test.py b/js/src/devtools/rootAnalysis/t/graph/test.py
new file mode 100644
index 0000000000..f78500f200
--- /dev/null
+++ b/js/src/devtools/rootAnalysis/t/graph/test.py
@@ -0,0 +1,54 @@
+# 'test' is provided by the calling script.
+# flake8: noqa: F821
+
+test.compile("source.cpp")
+test.run_analysis_script("gcTypes")
+
+info = test.load_typeInfo()
+
+gcFunctions = test.load_gcFunctions()
+
+f = "void f(int32)"
+g = "void g(int32)"
+h = "void h(int32)"
+
+assert f in gcFunctions
+assert g in gcFunctions
+assert h in gcFunctions
+assert "void leaf()" not in gcFunctions
+assert "void nonrecursive_root()" in gcFunctions
+
+callgraph = test.load_callgraph()
+assert callgraph.calleeGraph[f][g]
+assert callgraph.calleeGraph[f][h]
+assert callgraph.calleeGraph[g][f]
+assert callgraph.calleeGraph[g][h]
+
+node = ["void n{}(int32)".format(i) for i in range(10)]
+mnode = [callgraph.unmangledToMangled.get(f) for f in node]
+for src, dst in [
+ (1, 2),
+ (2, 1),
+ (4, 5),
+ (5, 4),
+ (2, 3),
+ (5, 3),
+ (3, 6),
+ (6, 7),
+ (7, 8),
+ (8, 7),
+ (8, 9),
+]:
+ assert callgraph.calleeGraph[node[src]][node[dst]]
+
+funcInfo = test.load_funcInfo()
+rroots = set(
+ [
+ callgraph.mangledToUnmangled[f]
+ for f in funcInfo
+ if funcInfo[f].get("recursive_root")
+ ]
+)
+assert len(set([node[1], node[2]]) & rroots) == 1
+assert len(set([node[4], node[5]]) & rroots) == 1
+assert len(rroots) == 4, "rroots = {}".format(rroots) # n1, n4, f, self_recursive
diff --git a/js/src/devtools/rootAnalysis/t/hazards/source.cpp b/js/src/devtools/rootAnalysis/t/hazards/source.cpp
new file mode 100644
index 0000000000..979591ef6b
--- /dev/null
+++ b/js/src/devtools/rootAnalysis/t/hazards/source.cpp
@@ -0,0 +1,459 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include <utility>
+
+#define ANNOTATE(property) __attribute__((annotate(property)))
+
+// MarkVariableAsGCSafe is a magic function name used as an
+// explicit annotation.
+
+namespace JS {
+namespace detail {
+template <typename T>
+static void MarkVariableAsGCSafe(T&) {
+ asm("");
+}
+} // namespace detail
+} // namespace JS
+
+#define JS_HAZ_VARIABLE_IS_GC_SAFE(var) JS::detail::MarkVariableAsGCSafe(var)
+
+struct Cell {
+ int f;
+} ANNOTATE("GC Thing");
+
+template <typename T, typename U>
+struct UntypedContainer {
+ char data[sizeof(T) + sizeof(U)];
+} ANNOTATE("moz_inherit_type_annotations_from_template_args");
+
+struct RootedCell {
+ RootedCell(Cell*) {}
+} ANNOTATE("Rooted Pointer");
+
+class AutoSuppressGC_Base {
+ public:
+ AutoSuppressGC_Base() {}
+ ~AutoSuppressGC_Base() {}
+} ANNOTATE("Suppress GC");
+
+class AutoSuppressGC_Child : public AutoSuppressGC_Base {
+ public:
+ AutoSuppressGC_Child() : AutoSuppressGC_Base() {}
+};
+
+class AutoSuppressGC {
+ AutoSuppressGC_Child helpImBeingSuppressed;
+
+ public:
+ AutoSuppressGC() {}
+};
+
+extern void GC() ANNOTATE("GC Call");
+extern void invisible();
+
+void GC() {
+ // If the implementation is too trivial, the function body won't be emitted at
+ // all.
+ asm("");
+ invisible();
+}
+
+extern void usecell(Cell*);
+
+void suppressedFunction() {
+ GC(); // Calls GC, but is always called within AutoSuppressGC
+}
+
+void halfSuppressedFunction() {
+ GC(); // Calls GC, but is sometimes called within AutoSuppressGC
+}
+
+void unsuppressedFunction() {
+ GC(); // Calls GC, never within AutoSuppressGC
+}
+
+class IDL_Interface {
+ public:
+ ANNOTATE("Can run script") virtual void canScriptThis() {}
+ virtual void cannotScriptThis() {}
+ ANNOTATE("Can run script") virtual void overridden_canScriptThis() = 0;
+ virtual void overridden_cannotScriptThis() = 0;
+};
+
+class IDL_Subclass : public IDL_Interface {
+ ANNOTATE("Can run script") void overridden_canScriptThis() override {}
+ void overridden_cannotScriptThis() override {}
+};
+
+volatile static int x = 3;
+volatile static int* xp = &x;
+struct GCInDestructor {
+ ~GCInDestructor() {
+ invisible();
+ asm("");
+ *xp = 4;
+ GC();
+ }
+};
+
+template <typename T>
+void usecontainer(T* value) {
+ if (value) asm("");
+}
+
+Cell* cell() {
+ static Cell c;
+ return &c;
+}
+
+Cell* f() {
+ GCInDestructor kaboom;
+
+ Cell* cell1 = cell();
+ Cell* cell2 = cell();
+ Cell* cell3 = cell();
+ Cell* cell4 = cell();
+ {
+ AutoSuppressGC nogc;
+ suppressedFunction();
+ halfSuppressedFunction();
+ }
+ usecell(cell1);
+ halfSuppressedFunction();
+ usecell(cell2);
+ unsuppressedFunction();
+ {
+ // Old bug: it would look from the first AutoSuppressGC constructor it
+ // found to the last destructor. This statement *should* have no effect.
+ AutoSuppressGC nogc;
+ }
+ usecell(cell3);
+ Cell* cell5 = cell();
+ usecell(cell5);
+
+ {
+ // Templatized container that inherits attributes from Cell*, should
+ // report a hazard.
+ UntypedContainer<int, Cell*> container1;
+ usecontainer(&container1);
+ GC();
+ usecontainer(&container1);
+ }
+
+ {
+ // As above, but with a non-GC type.
+ UntypedContainer<int, double> container2;
+ usecontainer(&container2);
+ GC();
+ usecontainer(&container2);
+ }
+
+ // Hazard in return value due to ~GCInDestructor
+ Cell* cell6 = cell();
+ return cell6;
+}
+
+Cell* copy_and_gc(Cell* src) {
+ GC();
+ return reinterpret_cast<Cell*>(88);
+}
+
+void use(Cell* cell) {
+ static int x = 0;
+ if (cell) x++;
+}
+
+struct CellContainer {
+ Cell* cell;
+ CellContainer() { asm(""); }
+};
+
+void loopy() {
+ Cell cell;
+
+ // No hazard: haz1 is not live during call to copy_and_gc.
+ Cell* haz1;
+ for (int i = 0; i < 10; i++) {
+ haz1 = copy_and_gc(haz1);
+ }
+
+ // No hazard: haz2 is live up to just before the GC, and starting at the
+ // next statement after it, but not across the GC.
+ Cell* haz2 = &cell;
+ for (int j = 0; j < 10; j++) {
+ use(haz2);
+ GC();
+ haz2 = &cell;
+ }
+
+ // Hazard: haz3 is live from the final statement in one iteration, across
+ // the GC in the next, to the use in the 2nd statement.
+ Cell* haz3;
+ for (int k = 0; k < 10; k++) {
+ GC();
+ use(haz3);
+ haz3 = &cell;
+ }
+
+ // Hazard: haz4 is live across a GC hidden in a loop.
+ Cell* haz4 = &cell;
+ for (int i2 = 0; i2 < 10; i2++) {
+ GC();
+ }
+ use(haz4);
+
+ // Hazard: haz5 is live from within a loop across a GC.
+ Cell* haz5;
+ for (int i3 = 0; i3 < 10; i3++) {
+ haz5 = &cell;
+ }
+ GC();
+ use(haz5);
+
+ // No hazard: similar to the haz3 case, but verifying that we do not get
+ // into an infinite loop.
+ Cell* haz6;
+ for (int i4 = 0; i4 < 10; i4++) {
+ GC();
+ haz6 = &cell;
+ }
+
+ // No hazard: haz7 is constructed within the body, so it can't make a
+ // hazard across iterations. Note that this requires CellContainer to have
+ // a constructor, because otherwise the analysis doesn't see where
+ // variables are declared. (With the constructor, it knows that
+ // construction of haz7 obliterates any previous value it might have had.
+ // Not that that's possible given its scope, but the analysis doesn't get
+ // that information.)
+ for (int i5 = 0; i5 < 10; i5++) {
+ GC();
+ CellContainer haz7;
+ use(haz7.cell);
+ haz7.cell = &cell;
+ }
+
+ // Hazard: make sure we *can* see hazards across iterations involving
+ // CellContainer;
+ CellContainer haz8;
+ for (int i6 = 0; i6 < 10; i6++) {
+ GC();
+ use(haz8.cell);
+ haz8.cell = &cell;
+ }
+}
+
+namespace mozilla {
+template <typename T>
+class UniquePtr {
+ T* val;
+
+ public:
+ UniquePtr() : val(nullptr) { asm(""); }
+ UniquePtr(T* p) : val(p) {}
+ UniquePtr(UniquePtr<T>&& u) : val(u.val) { u.val = nullptr; }
+ ~UniquePtr() { use(val); }
+ T* get() { return val; }
+ void reset() { val = nullptr; }
+} ANNOTATE("moz_inherit_type_annotations_from_template_args");
+} // namespace mozilla
+
+extern void consume(mozilla::UniquePtr<Cell> uptr);
+
+void safevals() {
+ Cell cell;
+
+ // Simple hazard.
+ Cell* unsafe1 = &cell;
+ GC();
+ use(unsafe1);
+
+ // Safe because it's known to be nullptr.
+ Cell* safe2 = &cell;
+ safe2 = nullptr;
+ GC();
+ use(safe2);
+
+ // Unsafe because it may not be nullptr.
+ Cell* unsafe3 = &cell;
+ if (reinterpret_cast<long>(&cell) & 0x100) {
+ unsafe3 = nullptr;
+ }
+ GC();
+ use(unsafe3);
+
+ // Unsafe because it's not nullptr anymore.
+ Cell* unsafe3b = &cell;
+ unsafe3b = nullptr;
+ unsafe3b = &cell;
+ GC();
+ use(unsafe3b);
+
+ // Hazard involving UniquePtr.
+ {
+ mozilla::UniquePtr<Cell> unsafe4(&cell);
+ GC();
+ // Destructor uses unsafe4.
+ }
+
+ // reset() to safe value before the GC.
+ {
+ mozilla::UniquePtr<Cell> safe5(&cell);
+ safe5.reset();
+ GC();
+ }
+
+ // reset() to safe value after the GC.
+ {
+ mozilla::UniquePtr<Cell> safe6(&cell);
+ GC();
+ safe6.reset();
+ }
+
+ // reset() to safe value after the GC -- but we've already used it, so it's
+ // too late.
+ {
+ mozilla::UniquePtr<Cell> unsafe7(&cell);
+ GC();
+ use(unsafe7.get());
+ unsafe7.reset();
+ }
+
+ // initialized to safe value.
+ {
+ mozilla::UniquePtr<Cell> safe8;
+ GC();
+ }
+
+ // passed to a function that takes ownership before GC.
+ {
+ mozilla::UniquePtr<Cell> safe9(&cell);
+ consume(std::move(safe9));
+ GC();
+ }
+
+ // passed to a function that takes ownership after GC.
+ {
+ mozilla::UniquePtr<Cell> unsafe10(&cell);
+ GC();
+ consume(std::move(unsafe10));
+ }
+
+ // annotated to be safe before the GC. (This doesn't make
+ // a lot of sense here; the annotation is for when some
+ // type is known to only contain safe values, eg it is
+ // initialized as empty, or it is a union and we know
+ // that the GC pointer variants are not in use.)
+ {
+ mozilla::UniquePtr<Cell> safe11(&cell);
+ JS_HAZ_VARIABLE_IS_GC_SAFE(safe11);
+ GC();
+ }
+
+ // annotate as safe value after the GC -- since nothing else
+ // has touched the variable, that means it was already safe
+ // during the GC.
+ {
+ mozilla::UniquePtr<Cell> safe12(&cell);
+ GC();
+ JS_HAZ_VARIABLE_IS_GC_SAFE(safe12);
+ }
+
+ // annotate as safe after the GC -- but we've already used it, so it's
+ // too late.
+ {
+ mozilla::UniquePtr<Cell> unsafe13(&cell);
+ GC();
+ use(unsafe13.get());
+ JS_HAZ_VARIABLE_IS_GC_SAFE(unsafe13);
+ }
+
+ // Check JS_HAZ_CAN_RUN_SCRIPT annotation handling.
+ IDL_Subclass sub;
+ IDL_Subclass* subp = &sub;
+ IDL_Interface* base = &sub;
+ {
+ Cell* unsafe14 = &cell;
+ base->canScriptThis();
+ use(unsafe14);
+ }
+ {
+ Cell* unsafe15 = &cell;
+ subp->canScriptThis();
+ use(unsafe15);
+ }
+ {
+ // Almost the same as the last one, except call using the actual object, not
+ // a pointer. The type is known, so there is no danger of the actual type
+ // being a subclass that has overridden the method with an implementation
+ // that calls script.
+ Cell* safe16 = &cell;
+ sub.canScriptThis();
+ use(safe16);
+ }
+ {
+ Cell* safe17 = &cell;
+ base->cannotScriptThis();
+ use(safe17);
+ }
+ {
+ Cell* safe18 = &cell;
+ subp->cannotScriptThis();
+ use(safe18);
+ }
+}
+
+// Make sure `this` is live at the beginning of a function.
+class Subcell : public Cell {
+ int method() {
+ GC();
+ return f; // this->f
+ }
+};
+
+template <typename T>
+struct RefPtr {
+ ~RefPtr() { GC(); }
+ void forget() {}
+};
+
+Cell* refptr_test1() {
+ static Cell cell;
+ RefPtr<float> v1;
+ Cell* ref_unsafe1 = &cell;
+ return ref_unsafe1;
+}
+
+Cell* refptr_test2() {
+ static Cell cell;
+ RefPtr<float> v2;
+ Cell* ref_safe2 = &cell;
+ v2.forget();
+ return ref_safe2;
+}
+
+Cell* refptr_test3() {
+ static Cell cell;
+ RefPtr<float> v3;
+ Cell* ref_unsafe3 = &cell;
+ if (x) {
+ v3.forget();
+ }
+ return ref_unsafe3;
+}
+
+Cell* refptr_test4() {
+ static Cell cell;
+ RefPtr<int> r;
+ return &cell; // hazard in return value
+}
+
+Cell* refptr_test5() {
+ static Cell cell;
+ RefPtr<int> r;
+ return nullptr; // returning immobile value, so no hazard
+}
diff --git a/js/src/devtools/rootAnalysis/t/hazards/test.py b/js/src/devtools/rootAnalysis/t/hazards/test.py
new file mode 100644
index 0000000000..75b8811c0f
--- /dev/null
+++ b/js/src/devtools/rootAnalysis/t/hazards/test.py
@@ -0,0 +1,101 @@
+# flake8: noqa: F821
+
+from collections import defaultdict
+
+test.compile("source.cpp")
+test.run_analysis_script("gcTypes")
+
+# gcFunctions should be the inverse, but we get to rely on unmangled names here.
+gcFunctions = test.load_gcFunctions()
+assert "void GC()" in gcFunctions
+assert "void suppressedFunction()" not in gcFunctions
+assert "void halfSuppressedFunction()" in gcFunctions
+assert "void unsuppressedFunction()" in gcFunctions
+assert "int32 Subcell::method()" in gcFunctions
+assert "Cell* f()" in gcFunctions
+
+hazards = test.load_hazards()
+hazmap = {haz.variable: haz for haz in hazards}
+assert "cell1" not in hazmap
+assert "cell2" in hazmap
+assert "cell3" in hazmap
+assert "cell4" not in hazmap
+assert "cell5" not in hazmap
+assert "cell6" not in hazmap
+assert "<returnvalue>" in hazmap
+assert "this" in hazmap
+
+# All hazards should be in f(), loopy(), safevals(), method(), and refptr_test{1,3,4}()
+assert hazmap["cell2"].function == "Cell* f()"
+haz_functions = set(haz.function for haz in hazards)
+print(haz_functions)
+assert len(haz_functions) == 7
+
+# Check that the correct GC call is reported for each hazard. (cell3 has a
+# hazard from two different GC calls; it doesn't really matter which is
+# reported.)
+assert hazmap["cell2"].GCFunction == "void halfSuppressedFunction()"
+assert hazmap["cell3"].GCFunction in (
+ "void halfSuppressedFunction()",
+ "void unsuppressedFunction()",
+)
+returnval_hazards = set(
+ haz.function for haz in hazards if haz.variable == "<returnvalue>"
+)
+assert returnval_hazards == set(
+ [
+ "Cell* f()",
+ "Cell* refptr_test1()",
+ "Cell* refptr_test3()",
+ "Cell* refptr_test4()",
+ ]
+)
+
+assert "container1" in hazmap
+assert "container2" not in hazmap
+
+# Type names are handy to have in the report.
+assert hazmap["cell2"].type == "Cell*"
+assert hazmap["<returnvalue>"].type == "Cell*"
+assert hazmap["this"].type == "Subcell*"
+
+# loopy hazards. See comments in source.
+assert "haz1" not in hazmap
+assert "haz2" not in hazmap
+assert "haz3" in hazmap
+assert "haz4" in hazmap
+assert "haz5" in hazmap
+assert "haz6" not in hazmap
+assert "haz7" not in hazmap
+assert "haz8" in hazmap
+
+# safevals hazards. See comments in source.
+assert "unsafe1" in hazmap
+assert "safe2" not in hazmap
+assert "unsafe3" in hazmap
+assert "unsafe3b" in hazmap
+assert "unsafe4" in hazmap
+assert "safe5" not in hazmap
+assert "safe6" not in hazmap
+assert "unsafe7" in hazmap
+assert "safe8" not in hazmap
+assert "safe9" not in hazmap
+assert "safe10" not in hazmap
+assert "safe11" not in hazmap
+assert "safe12" not in hazmap
+assert "unsafe13" in hazmap
+assert "unsafe14" in hazmap
+assert "unsafe15" in hazmap
+assert "safe16" not in hazmap
+assert "safe17" not in hazmap
+assert "safe18" not in hazmap
+
+# method hazard.
+
+byfunc = defaultdict(lambda: defaultdict(dict))
+for haz in hazards:
+ byfunc[haz.function][haz.variable] = haz
+
+methhaz = byfunc["int32 Subcell::method()"]
+assert "this" in methhaz
+assert methhaz["this"].type == "Subcell*"
diff --git a/js/src/devtools/rootAnalysis/t/sixgill-tree/source.cpp b/js/src/devtools/rootAnalysis/t/sixgill-tree/source.cpp
new file mode 100644
index 0000000000..149d77b03a
--- /dev/null
+++ b/js/src/devtools/rootAnalysis/t/sixgill-tree/source.cpp
@@ -0,0 +1,76 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#define ANNOTATE(property) __attribute__((annotate(property)))
+
+namespace js {
+namespace gc {
+struct Cell {
+ int f;
+} ANNOTATE("GC Thing");
+} // namespace gc
+} // namespace js
+
+struct Bogon {};
+
+struct JustACell : public js::gc::Cell {
+ bool iHaveNoDataMembers() { return true; }
+};
+
+struct JSObject : public js::gc::Cell, public Bogon {
+ int g;
+};
+
+struct SpecialObject : public JSObject {
+ int z;
+};
+
+struct ErrorResult {
+ bool hasObj;
+ JSObject* obj;
+ void trace() {}
+} ANNOTATE("Suppressed GC Pointer");
+
+struct OkContainer {
+ ErrorResult res;
+ bool happy;
+};
+
+struct UnrootedPointer {
+ JSObject* obj;
+};
+
+template <typename T>
+class Rooted {
+ T data;
+} ANNOTATE("Rooted Pointer");
+
+extern void js_GC() ANNOTATE("GC Call") ANNOTATE("Slow");
+
+void js_GC() {}
+
+void root_arg(JSObject* obj, JSObject* random) {
+ // Use all these types so they get included in the output.
+ SpecialObject so;
+ UnrootedPointer up;
+ Bogon b;
+ OkContainer okc;
+ Rooted<JSObject*> ro;
+ Rooted<SpecialObject*> rso;
+
+ obj = random;
+
+ JSObject* other1 = obj;
+ js_GC();
+
+ float MARKER1 = 0;
+ JSObject* other2 = obj;
+ other1->f = 1;
+ other2->f = -1;
+
+ unsigned int u1 = 1;
+ unsigned int u2 = -1;
+}
diff --git a/js/src/devtools/rootAnalysis/t/sixgill-tree/test.py b/js/src/devtools/rootAnalysis/t/sixgill-tree/test.py
new file mode 100644
index 0000000000..5e99fff908
--- /dev/null
+++ b/js/src/devtools/rootAnalysis/t/sixgill-tree/test.py
@@ -0,0 +1,63 @@
+# flake8: noqa: F821
+import re
+
+test.compile("source.cpp")
+test.computeGCTypes()
+body = test.process_body(test.load_db_entry("src_body", re.compile(r"root_arg"))[0])
+
+# Rendering positive and negative integers
+marker1 = body.assignment_line("MARKER1")
+equal(body.edge_from_line(marker1 + 2)["Exp"][1]["String"], "1")
+equal(body.edge_from_line(marker1 + 3)["Exp"][1]["String"], "-1")
+
+equal(body.edge_from_point(body.assignment_point("u1"))["Exp"][1]["String"], "1")
+equal(
+ body.edge_from_point(body.assignment_point("u2"))["Exp"][1]["String"], "4294967295"
+)
+
+assert "obj" in body["Variables"]
+assert "random" in body["Variables"]
+assert "other1" in body["Variables"]
+assert "other2" in body["Variables"]
+
+# Test function annotations
+js_GC = test.process_body(test.load_db_entry("src_body", re.compile(r"js_GC"))[0])
+annotations = js_GC["Variables"]["void js_GC()"]["Annotation"]
+assert annotations
+found_call_annotate = False
+for annotation in annotations:
+ (annType, value) = annotation["Name"]
+ if annType == "annotate" and value == "GC Call":
+ found_call_annotate = True
+assert found_call_annotate
+
+# Test type annotations
+
+# js::gc::Cell first
+cell = test.load_db_entry("src_comp", "js::gc::Cell")[0]
+assert cell["Kind"] == "Struct"
+annotations = cell["Annotation"]
+assert len(annotations) == 1
+(tag, value) = annotations[0]["Name"]
+assert tag == "annotate"
+assert value == "GC Thing"
+
+# Check JSObject inheritance.
+JSObject = test.load_db_entry("src_comp", "JSObject")[0]
+bases = [b["Base"] for b in JSObject["CSUBaseClass"]]
+assert "js::gc::Cell" in bases
+assert "Bogon" in bases
+assert len(bases) == 2
+
+# Check type analysis
+gctypes = test.load_gcTypes()
+assert "js::gc::Cell" in gctypes["GCThings"]
+assert "JustACell" in gctypes["GCThings"]
+assert "JSObject" in gctypes["GCThings"]
+assert "SpecialObject" in gctypes["GCThings"]
+assert "UnrootedPointer" in gctypes["GCPointers"]
+assert "Bogon" not in gctypes["GCThings"]
+assert "Bogon" not in gctypes["GCPointers"]
+assert "ErrorResult" not in gctypes["GCPointers"]
+assert "OkContainer" not in gctypes["GCPointers"]
+assert "class Rooted<JSObject*>" not in gctypes["GCPointers"]
diff --git a/js/src/devtools/rootAnalysis/t/sixgill.py b/js/src/devtools/rootAnalysis/t/sixgill.py
new file mode 100644
index 0000000000..0b8c2c7073
--- /dev/null
+++ b/js/src/devtools/rootAnalysis/t/sixgill.py
@@ -0,0 +1,70 @@
+#!/usr/bin/env python
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+from collections import defaultdict
+
+# Simplified version of the body info.
+
+
+class Body(dict):
+ def __init__(self, body):
+ self["BlockIdKind"] = body["BlockId"]["Kind"]
+ if "Variable" in body["BlockId"]:
+ self["BlockName"] = body["BlockId"]["Variable"]["Name"][0].split("$")[-1]
+ loc = body["Location"]
+ self["LineRange"] = (loc[0]["Line"], loc[1]["Line"])
+ self["Filename"] = loc[0]["CacheString"]
+ self["Edges"] = body.get("PEdge", [])
+ self["Points"] = {
+ i: p["Location"]["Line"] for i, p in enumerate(body["PPoint"], 1)
+ }
+ self["Index"] = body["Index"]
+ self["Variables"] = {
+ x["Variable"]["Name"][0].split("$")[-1]: x["Type"]
+ for x in body["DefineVariable"]
+ }
+
+ # Indexes
+ self["Line2Points"] = defaultdict(list)
+ for point, line in self["Points"].items():
+ self["Line2Points"][line].append(point)
+ self["SrcPoint2Edges"] = defaultdict(list)
+ for edge in self["Edges"]:
+ src, dst = edge["Index"]
+ self["SrcPoint2Edges"][src].append(edge)
+ self["Line2Edges"] = defaultdict(list)
+ for (src, edges) in self["SrcPoint2Edges"].items():
+ line = self["Points"][src]
+ self["Line2Edges"][line].extend(edges)
+
+ def edges_from_line(self, line):
+ return self["Line2Edges"][line]
+
+ def edge_from_line(self, line):
+ edges = self.edges_from_line(line)
+ assert len(edges) == 1
+ return edges[0]
+
+ def edges_from_point(self, point):
+ return self["SrcPoint2Edges"][point]
+
+ def edge_from_point(self, point):
+ edges = self.edges_from_point(point)
+ assert len(edges) == 1
+ return edges[0]
+
+ def assignment_point(self, varname):
+ for edge in self["Edges"]:
+ if edge["Kind"] != "Assign":
+ continue
+ dst = edge["Exp"][0]
+ if dst["Kind"] != "Var":
+ continue
+ if dst["Variable"]["Name"][0] == varname:
+ return edge["Index"][0]
+ raise Exception("assignment to variable %s not found" % varname)
+
+ def assignment_line(self, varname):
+ return self["Points"][self.assignment_point(varname)]
diff --git a/js/src/devtools/rootAnalysis/t/suppression/source.cpp b/js/src/devtools/rootAnalysis/t/suppression/source.cpp
new file mode 100644
index 0000000000..56e458bdaa
--- /dev/null
+++ b/js/src/devtools/rootAnalysis/t/suppression/source.cpp
@@ -0,0 +1,72 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#define ANNOTATE(property) __attribute__((annotate(property)))
+
+struct Cell {
+ int f;
+} ANNOTATE("GC Thing");
+
+class AutoSuppressGC_Base {
+ public:
+ AutoSuppressGC_Base() {}
+ ~AutoSuppressGC_Base() {}
+} ANNOTATE("Suppress GC");
+
+class AutoSuppressGC_Child : public AutoSuppressGC_Base {
+ public:
+ AutoSuppressGC_Child() : AutoSuppressGC_Base() {}
+};
+
+class AutoSuppressGC {
+ AutoSuppressGC_Child helpImBeingSuppressed;
+
+ public:
+ AutoSuppressGC() {}
+};
+
+extern void GC() ANNOTATE("GC Call");
+
+void GC() {
+ // If the implementation is too trivial, the function body won't be emitted at
+ // all.
+ asm("");
+}
+
+extern void foo(Cell*);
+
+void suppressedFunction() {
+ GC(); // Calls GC, but is always called within AutoSuppressGC
+}
+
+void halfSuppressedFunction() {
+ GC(); // Calls GC, but is sometimes called within AutoSuppressGC
+}
+
+void unsuppressedFunction() {
+ GC(); // Calls GC, never within AutoSuppressGC
+}
+
+void f() {
+ Cell* cell1 = nullptr;
+ Cell* cell2 = nullptr;
+ Cell* cell3 = nullptr;
+ {
+ AutoSuppressGC nogc;
+ suppressedFunction();
+ halfSuppressedFunction();
+ }
+ foo(cell1);
+ halfSuppressedFunction();
+ foo(cell2);
+ unsuppressedFunction();
+ {
+ // Old bug: it would look from the first AutoSuppressGC constructor it
+ // found to the last destructor. This statement *should* have no effect.
+ AutoSuppressGC nogc;
+ }
+ foo(cell3);
+}
diff --git a/js/src/devtools/rootAnalysis/t/suppression/test.py b/js/src/devtools/rootAnalysis/t/suppression/test.py
new file mode 100644
index 0000000000..118ae422ab
--- /dev/null
+++ b/js/src/devtools/rootAnalysis/t/suppression/test.py
@@ -0,0 +1,21 @@
+# flake8: noqa: F821
+test.compile("source.cpp")
+test.run_analysis_script("gcTypes", upto="gcFunctions")
+
+# The suppressions file uses mangled names.
+info = test.load_funcInfo()
+suppressed = [f for f, v in info.items() if v.get("limits", 0) | 1]
+
+# Only one of these is fully suppressed (ie, *always* called within the scope
+# of an AutoSuppressGC).
+assert len(list(filter(lambda f: "suppressedFunction" in f, suppressed))) == 1
+assert len(list(filter(lambda f: "halfSuppressedFunction" in f, suppressed))) == 0
+assert len(list(filter(lambda f: "unsuppressedFunction" in f, suppressed))) == 0
+
+# gcFunctions should be the inverse, but we get to rely on unmangled names here.
+gcFunctions = test.load_gcFunctions()
+assert "void GC()" in gcFunctions
+assert "void suppressedFunction()" not in gcFunctions
+assert "void halfSuppressedFunction()" in gcFunctions
+assert "void unsuppressedFunction()" in gcFunctions
+assert "void f()" in gcFunctions
diff --git a/js/src/devtools/rootAnalysis/t/testlib.py b/js/src/devtools/rootAnalysis/t/testlib.py
new file mode 100644
index 0000000000..a29e640ba4
--- /dev/null
+++ b/js/src/devtools/rootAnalysis/t/testlib.py
@@ -0,0 +1,230 @@
+import json
+import os
+import re
+import subprocess
+import sys
+from collections import defaultdict, namedtuple
+
+from sixgill import Body
+
+scriptdir = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
+
+HazardSummary = namedtuple(
+ "HazardSummary", ["function", "variable", "type", "GCFunction", "location"]
+)
+
+Callgraph = namedtuple(
+ "Callgraph",
+ [
+ "functionNames",
+ "nameToId",
+ "mangledToUnmangled",
+ "unmangledToMangled",
+ "calleesOf",
+ "callersOf",
+ "tags",
+ "calleeGraph",
+ "callerGraph",
+ ],
+)
+
+
+def equal(got, expected):
+ if got != expected:
+ print("Got '%s', expected '%s'" % (got, expected))
+
+
+def extract_unmangled(func):
+ return func.split("$")[-1]
+
+
+class Test(object):
+ def __init__(self, indir, outdir, cfg, verbose=0):
+ self.indir = indir
+ self.outdir = outdir
+ self.cfg = cfg
+ self.verbose = verbose
+
+ def infile(self, path):
+ return os.path.join(self.indir, path)
+
+ def binpath(self, prog):
+ return os.path.join(self.cfg.sixgill_bin, prog)
+
+ def compile(self, source, options=""):
+ env = os.environ
+ env["CCACHE_DISABLE"] = "1"
+ cmd = "{CXX} -c {source} -O3 -std=c++11 -fplugin={sixgill} -fplugin-arg-xgill-mangle=1 {options}".format( # NOQA: E501
+ source=self.infile(source),
+ CXX=self.cfg.cxx,
+ sixgill=self.cfg.sixgill_plugin,
+ options=options,
+ )
+ if self.cfg.verbose:
+ print("Running %s" % cmd)
+ subprocess.check_call(["sh", "-c", cmd])
+
+ def load_db_entry(self, dbname, pattern):
+ """Look up an entry from an XDB database file, 'pattern' may be an exact
+ matching string, or an re pattern object matching a single entry."""
+
+ if hasattr(pattern, "match"):
+ output = subprocess.check_output(
+ [self.binpath("xdbkeys"), dbname + ".xdb"], universal_newlines=True
+ )
+ matches = list(filter(lambda _: re.search(pattern, _), output.splitlines()))
+ if len(matches) == 0:
+ raise Exception("entry not found")
+ if len(matches) > 1:
+ raise Exception("multiple entries found")
+ pattern = matches[0]
+
+ output = subprocess.check_output(
+ [self.binpath("xdbfind"), "-json", dbname + ".xdb", pattern],
+ universal_newlines=True,
+ )
+ return json.loads(output)
+
+ def run_analysis_script(self, startPhase, upto=None):
+ open("defaults.py", "w").write(
+ """\
+analysis_scriptdir = '{scriptdir}'
+sixgill_bin = '{bindir}'
+""".format(
+ scriptdir=scriptdir, bindir=self.cfg.sixgill_bin
+ )
+ )
+ cmd = [
+ sys.executable,
+ os.path.join(scriptdir, "analyze.py"),
+ "-v" if self.verbose else "-q",
+ ]
+ cmd += ["--first", startPhase]
+ if upto:
+ cmd += ["--last", upto]
+ cmd.append("--source=%s" % self.indir)
+ cmd.append("--js=%s" % self.cfg.js)
+ if self.cfg.verbose:
+ cmd.append("--verbose")
+ print("Running " + " ".join(cmd))
+ subprocess.check_call(cmd)
+
+ def computeGCTypes(self):
+ self.run_analysis_script("gcTypes", upto="gcTypes")
+
+ def computeHazards(self):
+ self.run_analysis_script("gcTypes")
+
+ def load_text_file(self, filename, extract=lambda l: l):
+ fullpath = os.path.join(self.outdir, filename)
+ values = (extract(line.strip()) for line in open(fullpath, "r"))
+ return list(filter(lambda _: _ is not None, values))
+
+ def load_json_file(self, filename, reviver=None):
+ fullpath = os.path.join(self.outdir, filename)
+ with open(fullpath) as fh:
+ return json.load(fh, object_hook=reviver)
+
+ def load_gcTypes(self):
+ def grab_type(line):
+ m = re.match(r"^(GC\w+): (.*)", line)
+ if m:
+ return (m.group(1) + "s", m.group(2))
+ return None
+
+ gctypes = defaultdict(list)
+ for collection, typename in self.load_text_file(
+ "gcTypes.txt", extract=grab_type
+ ):
+ gctypes[collection].append(typename)
+ return gctypes
+
+ def load_typeInfo(self, filename="typeInfo.txt"):
+ return self.load_json_file(filename)
+
+ def load_funcInfo(self, filename="limitedFunctions.lst"):
+ return self.load_json_file(filename)
+
+ def load_gcFunctions(self):
+ return self.load_text_file("gcFunctions.lst", extract=extract_unmangled)
+
+ def load_callgraph(self):
+ data = Callgraph(
+ functionNames=["dummy"],
+ nameToId={},
+ mangledToUnmangled={},
+ unmangledToMangled={},
+ calleesOf=defaultdict(list),
+ callersOf=defaultdict(list),
+ tags=defaultdict(set),
+ calleeGraph=defaultdict(dict),
+ callerGraph=defaultdict(dict),
+ )
+
+ def lookup(id):
+ mangled = data.functionNames[int(id)]
+ return data.mangledToUnmangled.get(mangled, mangled)
+
+ def add_call(caller, callee, limit):
+ data.calleesOf[caller].append(callee)
+ data.callersOf[callee].append(caller)
+ data.calleeGraph[caller][callee] = True
+ data.callerGraph[callee][caller] = True
+
+ def process(line):
+ if line.startswith("#"):
+ name = line.split(" ", 1)[1]
+ data.nameToId[name] = len(data.functionNames)
+ data.functionNames.append(name)
+ return
+
+ if line.startswith("="):
+ m = re.match(r"^= (\d+) (.*)", line)
+ mangled = data.functionNames[int(m.group(1))]
+ unmangled = m.group(2)
+ data.nameToId[unmangled] = id
+ data.mangledToUnmangled[mangled] = unmangled
+ data.unmangledToMangled[unmangled] = mangled
+ return
+
+ limit = 0
+ m = re.match(r"^\w (?:/(\d+))? ", line)
+ if m:
+ limit = int(m[1])
+
+ tokens = line.split(" ")
+ if tokens[0] in ("D", "R"):
+ _, caller, callee = tokens
+ add_call(lookup(caller), lookup(callee), limit)
+ elif tokens[0] == "T":
+ data.tags[tokens[1]].add(line.split(" ", 2)[2])
+ elif tokens[0] in ("F", "V"):
+ pass
+
+ elif tokens[0] == "I":
+ m = re.match(r"^I (\d+) VARIABLE ([^\,]*)", line)
+ pass
+
+ self.load_text_file("callgraph.txt", extract=process)
+ return data
+
+ def load_hazards(self):
+ def grab_hazard(line):
+ m = re.match(
+ r"Function '(.*?)' has unrooted '(.*?)' of type '(.*?)' live across GC call '(.*?)' at (.*)", # NOQA: E501
+ line,
+ )
+ if m:
+ info = list(m.groups())
+ info[0] = info[0].split("$")[-1]
+ info[3] = info[3].split("$")[-1]
+ return HazardSummary(*info)
+ return None
+
+ return self.load_text_file("rootingHazards.txt", extract=grab_hazard)
+
+ def process_body(self, body):
+ return Body(body)
+
+ def process_bodies(self, bodies):
+ return [self.process_body(b) for b in bodies]
diff --git a/js/src/devtools/rootAnalysis/t/virtual/source.cpp b/js/src/devtools/rootAnalysis/t/virtual/source.cpp
new file mode 100644
index 0000000000..83633a3436
--- /dev/null
+++ b/js/src/devtools/rootAnalysis/t/virtual/source.cpp
@@ -0,0 +1,292 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#define ANNOTATE(property) __attribute__((annotate(property)))
+
+extern void GC() ANNOTATE("GC Call");
+
+void GC() {
+ // If the implementation is too trivial, the function body won't be emitted at
+ // all.
+ asm("");
+}
+
+// Special-cased function -- code that can run JS has an artificial edge to
+// js::RunScript.
+namespace js {
+void RunScript() { GC(); }
+} // namespace js
+
+struct Cell {
+ int f;
+} ANNOTATE("GC Thing");
+
+extern void foo();
+
+void bar() { GC(); }
+
+typedef void (*func_t)();
+
+class Base {
+ public:
+ int ANNOTATE("field annotation") dummy;
+ virtual void someGC() ANNOTATE("Base pure virtual method") = 0;
+ virtual void someGC(int) ANNOTATE("overloaded Base pure virtual method") = 0;
+ virtual void sibGC() = 0;
+ virtual void onBase() { bar(); }
+ func_t functionField;
+
+ // For now, this is just to verify that the plugin doesn't crash. The
+ // analysis code does not yet look at this annotation or output it anywhere
+ // (though it *is* being recorded.)
+ static float testAnnotations() ANNOTATE("static func");
+
+ // Similar, though sixgill currently completely ignores parameter annotations.
+ static double testParamAnnotations(Cell& ANNOTATE("param annotation")
+ ANNOTATE("second param annot") cell)
+ ANNOTATE("static func") ANNOTATE("second func");
+};
+
+float Base::testAnnotations() {
+ asm("");
+ return 1.1;
+}
+
+double Base::testParamAnnotations(Cell& cell) {
+ asm("");
+ return 1.2;
+}
+
+class Super : public Base {
+ public:
+ virtual void ANNOTATE("Super pure virtual") noneGC() = 0;
+ virtual void allGC() = 0;
+ virtual void onSuper() { asm(""); }
+ void nonVirtualFunc() { asm(""); }
+};
+
+class Sub1 : public Super {
+ public:
+ void noneGC() override { foo(); }
+ void someGC() override ANNOTATE("Sub1 override") ANNOTATE("second attr") {
+ foo();
+ }
+ void someGC(int) override ANNOTATE("Sub1 override for int overload") {
+ foo();
+ }
+ void allGC() override {
+ foo();
+ bar();
+ }
+ void sibGC() override { foo(); }
+ void onBase() override { foo(); }
+} ANNOTATE("CSU1") ANNOTATE("CSU2");
+
+class Sub2 : public Super {
+ public:
+ void noneGC() override { foo(); }
+ void someGC() override {
+ foo();
+ bar();
+ }
+ void someGC(int) override {
+ foo();
+ bar();
+ }
+ void allGC() override {
+ foo();
+ bar();
+ }
+ void sibGC() override { foo(); }
+};
+
+class Sibling : public Base {
+ public:
+ virtual void noneGC() { foo(); }
+ void someGC() override {
+ foo();
+ bar();
+ }
+ void someGC(int) override {
+ foo();
+ bar();
+ }
+ virtual void allGC() {
+ foo();
+ bar();
+ }
+ void sibGC() override { bar(); }
+};
+
+class AutoSuppressGC {
+ public:
+ AutoSuppressGC() {}
+ ~AutoSuppressGC() {}
+} ANNOTATE("Suppress GC");
+
+void use(Cell*) { asm(""); }
+
+class nsISupports {
+ public:
+ virtual ANNOTATE("Can run script") void danger() { asm(""); }
+
+ virtual ~nsISupports() = 0;
+};
+
+class nsIPrincipal : public nsISupports {
+ public:
+ ~nsIPrincipal() override{};
+};
+
+struct JSPrincipals {
+ int debugToken;
+ JSPrincipals() = default;
+ virtual ~JSPrincipals() { GC(); }
+};
+
+class nsJSPrincipals : public nsIPrincipal, public JSPrincipals {
+ public:
+ void Release() { delete this; }
+};
+
+class SafePrincipals : public nsIPrincipal {
+ public:
+ ~SafePrincipals() { foo(); }
+};
+
+void f() {
+ Sub1 s1;
+ Sub2 s2;
+
+ static Cell cell;
+ {
+ Cell* c1 = &cell;
+ s1.noneGC();
+ use(c1);
+ }
+ {
+ Cell* c2 = &cell;
+ s2.someGC();
+ use(c2);
+ }
+ {
+ Cell* c3 = &cell;
+ s1.allGC();
+ use(c3);
+ }
+ {
+ Cell* c4 = &cell;
+ s2.noneGC();
+ use(c4);
+ }
+ {
+ Cell* c5 = &cell;
+ s2.someGC();
+ use(c5);
+ }
+ {
+ Cell* c6 = &cell;
+ s2.allGC();
+ use(c6);
+ }
+
+ Super* super = &s2;
+ {
+ Cell* c7 = &cell;
+ super->noneGC();
+ use(c7);
+ }
+ {
+ Cell* c8 = &cell;
+ super->someGC();
+ use(c8);
+ }
+ {
+ Cell* c9 = &cell;
+ super->allGC();
+ use(c9);
+ }
+
+ {
+ Cell* c10 = &cell;
+ s1.functionField();
+ use(c10);
+ }
+ {
+ Cell* c11 = &cell;
+ super->functionField();
+ use(c11);
+ }
+ {
+ Cell* c12 = &cell;
+ super->sibGC();
+ use(c12);
+ }
+
+ Base* base = &s2;
+ {
+ Cell* c13 = &cell;
+ base->sibGC();
+ use(c13);
+ }
+
+ nsJSPrincipals pals;
+ {
+ Cell* c14 = &cell;
+ nsISupports* p = &pals;
+ p->danger();
+ use(c14);
+ }
+
+ // Base defines, Sub1 overrides, static Super can call either.
+ {
+ Cell* c15 = &cell;
+ super->onBase();
+ use(c15);
+ }
+
+ {
+ Cell* c16 = &cell;
+ s2.someGC(7);
+ use(c16);
+ }
+
+ {
+ Cell* c17 = &cell;
+ super->someGC(7);
+ use(c17);
+ }
+
+ {
+ nsJSPrincipals* princ = new nsJSPrincipals();
+ Cell* c18 = &cell;
+ delete princ; // Can GC
+ use(c18);
+ }
+
+ {
+ nsJSPrincipals* princ = new nsJSPrincipals();
+ nsISupports* supp = static_cast<nsISupports*>(princ);
+ Cell* c19 = &cell;
+ delete supp; // Can GC
+ use(c19);
+ }
+
+ {
+ auto* safe = new SafePrincipals();
+ Cell* c20 = &cell;
+ delete safe; // Cannot GC
+ use(c20);
+ }
+
+ {
+ auto* safe = new SafePrincipals();
+ nsISupports* supp = static_cast<nsISupports*>(safe);
+ Cell* c21 = &cell;
+ delete supp; // Compiler thinks destructor can GC.
+ use(c21);
+ }
+}
diff --git a/js/src/devtools/rootAnalysis/t/virtual/test.py b/js/src/devtools/rootAnalysis/t/virtual/test.py
new file mode 100644
index 0000000000..e8474ae28b
--- /dev/null
+++ b/js/src/devtools/rootAnalysis/t/virtual/test.py
@@ -0,0 +1,91 @@
+# 'test' is provided by the calling script.
+# flake8: noqa: F821
+
+test.compile("source.cpp")
+test.run_analysis_script("gcTypes")
+
+info = test.load_typeInfo()
+
+assert "Sub1" in info["OtherCSUTags"]
+assert ["CSU1", "CSU2"] == sorted(info["OtherCSUTags"]["Sub1"])
+assert "Base" in info["OtherFieldTags"]
+assert "someGC" in info["OtherFieldTags"]["Base"]
+assert "Sub1" in info["OtherFieldTags"]
+assert "someGC" in info["OtherFieldTags"]["Sub1"]
+
+# For now, fields with the same name (eg overloaded virtual methods) just
+# accumulate attributes.
+assert ["Sub1 override", "Sub1 override for int overload", "second attr"] == sorted(
+ info["OtherFieldTags"]["Sub1"]["someGC"]
+)
+
+gcFunctions = test.load_gcFunctions()
+
+assert "void Sub1::noneGC()" not in gcFunctions
+assert "void Sub1::someGC()" not in gcFunctions
+assert "void Sub1::someGC(int32)" not in gcFunctions
+assert "void Sub1::allGC()" in gcFunctions
+assert "void Sub2::noneGC()" not in gcFunctions
+assert "void Sub2::someGC()" in gcFunctions
+assert "void Sub2::someGC(int32)" in gcFunctions
+assert "void Sub2::allGC()" in gcFunctions
+
+callgraph = test.load_callgraph()
+
+assert callgraph.calleeGraph["void f()"]["Super.noneGC:0"]
+assert callgraph.calleeGraph["Super.noneGC:0"]["Sub1.noneGC:0"]
+assert callgraph.calleeGraph["Super.noneGC:0"]["Sub2.noneGC:0"]
+assert callgraph.calleeGraph["Sub1.noneGC:0"]["void Sub1::noneGC()"]
+assert callgraph.calleeGraph["Sub2.noneGC:0"]["void Sub2::noneGC()"]
+assert "void Sibling::noneGC()" not in callgraph.calleeGraph["Super.noneGC:0"]
+assert callgraph.calleeGraph["Super.onBase:0"]["Sub1.onBase:0"]
+assert callgraph.calleeGraph["Sub1.onBase:0"]["void Sub1::onBase()"]
+assert callgraph.calleeGraph["Super.onBase:0"]["void Base::onBase()"]
+assert "void Sibling::onBase()" not in callgraph.calleeGraph["Super.onBase:0"]
+
+hazards = test.load_hazards()
+hazmap = {haz.variable: haz for haz in hazards}
+
+assert "c1" not in hazmap
+assert "c2" in hazmap
+assert "c3" in hazmap
+assert "c4" not in hazmap
+assert "c5" in hazmap
+assert "c6" in hazmap
+assert "c7" not in hazmap
+assert "c8" in hazmap
+assert "c9" in hazmap
+assert "c10" in hazmap
+assert "c11" in hazmap
+
+# Virtual resolution should take the static type into account: the only method
+# implementations considered should be those of descendants, even if the
+# virtual method is inherited and not overridden in the static class. (Base
+# defines sibGC() as pure virtual, Super inherits it without overriding,
+# Sibling and Sub2 both implement it.)
+
+# Call Base.sibGC on a Super pointer: can only call Sub2.sibGC(), which does not GC.
+# In particular, PEdgeCallInstance.Exp.Field.FieldCSU.Type = {Kind: "CSU", Name="Super"}
+assert "c12" not in hazmap
+# Call Base.sibGC on a Base pointer; can call Sibling.sibGC(), which GCs.
+assert "c13" in hazmap
+
+# Call nsISupports.danger() which is annotated to be overridable and hence can GC.
+assert "c14" in hazmap
+
+# someGC(int) overload
+assert "c16" in hazmap
+assert "c17" in hazmap
+
+# Super.onBase() could call the GC'ing Base::onBase().
+assert "c15" in hazmap
+
+# virtual ~nsJSPrincipals calls ~JSPrincipals calls GC.
+assert "c18" in hazmap
+assert "c19" in hazmap
+
+# ~SafePrincipals does not GC.
+assert "c20" not in hazmap
+
+# ...but when cast to a nsISupports*, the compiler can't tell that it won't.
+assert "c21" in hazmap