TypeGraph: Introduce TypeGraphParser to simplify unit testing

TypeGraphParser parses a textual type graph, as emitted by Printer.

It also doubles as a way of ensuring that Printer displays all
information about a type graph, to aid with debugging.

Convert Flattener unit tests over to this new framework as a first step.
This commit is contained in:
Alastair Robertson 2023-07-11 09:01:13 -07:00 committed by Alastair Robertson
parent bd948152b7
commit 30cd23fa53
7 changed files with 781 additions and 391 deletions

View File

@ -55,21 +55,27 @@ class TypeGraph {
template <typename T>
Primitive& makeType(Primitive::Kind kind);
template <typename T, typename... Args>
T& makeType(NodeId id, Args&&... args) {
static_assert(std::is_same_v<T, Class> || std::is_same_v<T, Container> ||
std::is_same_v<T, Array> || std::is_same_v<T, Typedef> ||
std::is_same_v<T, Pointer>,
"Unnecessary node ID provided");
auto type_unique_ptr = std::make_unique<T>(id, std::forward<Args>(args)...);
auto type_raw_ptr = type_unique_ptr.get();
types_.push_back(std::move(type_unique_ptr));
return *type_raw_ptr;
}
template <typename T, typename... Args>
T& makeType(Args&&... args) {
static_assert(!std::is_same<T, Primitive>::value,
static_assert(!std::is_same_v<T, Primitive>,
"Primitive singleton override should be used");
if constexpr (std::is_same<T, Class>::value ||
std::is_same<T, Container>::value ||
std::is_same<T, Array>::value ||
std::is_same<T, Typedef>::value ||
std::is_same<T, Pointer>::value) {
if constexpr (std::is_same_v<T, Class> || std::is_same_v<T, Container> ||
std::is_same_v<T, Array> || std::is_same_v<T, Typedef> ||
std::is_same_v<T, Pointer>) {
// Node ID required
auto type_unique_ptr =
std::make_unique<T>(next_id_++, std::forward<Args>(args)...);
auto type_raw_ptr = type_unique_ptr.get();
types_.push_back(std::move(type_unique_ptr));
return *type_raw_ptr;
return makeType<T>(next_id_++, std::forward<Args>(args)...);
} else {
// No Node ID
auto type_unique_ptr = std::make_unique<T>(std::forward<Args>(args)...);

View File

@ -62,6 +62,7 @@ add_executable(test_type_graph
test_topo_sorter.cpp
test_type_identifier.cpp
type_graph_utils.cpp
TypeGraphParser.cpp
)
add_dependencies(test_type_graph integration_test_target)
target_compile_definitions(test_type_graph PRIVATE

422
test/TypeGraphParser.cpp Normal file
View File

@ -0,0 +1,422 @@
#include "TypeGraphParser.h"
#include <string>
#include "oi/type_graph/TypeGraph.h"
namespace {
/*
* Sets line to the contents of the current line.
* Advances input to the beginning of the next line.
*/
bool getline(std::string_view& input, std::string_view& line) {
auto nextline = input.find('\n');
if (nextline == input.npos) {
return false;
}
line = input.substr(0, nextline);
input = input.substr(nextline + 1);
return true;
}
Primitive::Kind getKind(std::string_view kindStr) {
if (kindStr == "int8_t")
return Primitive::Kind::Int8;
if (kindStr == "int16_t")
return Primitive::Kind::Int16;
if (kindStr == "int32_t")
return Primitive::Kind::Int32;
if (kindStr == "int64_t")
return Primitive::Kind::Int64;
if (kindStr == "uint8_t")
return Primitive::Kind::UInt8;
if (kindStr == "uint16_t")
return Primitive::Kind::UInt16;
if (kindStr == "uint32_t")
return Primitive::Kind::UInt32;
if (kindStr == "uint64_t")
return Primitive::Kind::UInt64;
if (kindStr == "float")
return Primitive::Kind::Float32;
if (kindStr == "double")
return Primitive::Kind::Float64;
if (kindStr == "long double")
return Primitive::Kind::Float128;
if (kindStr == "bool")
return Primitive::Kind::Bool;
if (kindStr == "uintptr_t")
return Primitive::Kind::UIntPtr;
if (kindStr == "void")
return Primitive::Kind::Void;
throw std::runtime_error("Invalid Primitive::Kind: " + std::string{kindStr});
}
ContainerInfo getContainerInfo(std::string_view name) {
if (name == "std::vector") {
ContainerInfo info{"std::vector", SEQ_TYPE, "vector"};
info.stubTemplateParams = {1};
return info;
}
if (name == "std::map") {
ContainerInfo info{"std::map", STD_MAP_TYPE, "utility"};
info.stubTemplateParams = {2, 3};
return info;
}
if (name == "std::pair") {
ContainerInfo info{"std::pair", SEQ_TYPE, "utility"};
return info;
}
if (name == "std::allocator") {
ContainerInfo info{"std::allocator", DUMMY_TYPE, "memory"};
return info;
}
throw std::runtime_error("Unsupported container: " + std::string{name});
}
Qualifier getQualifier(std::string_view line) {
if (line == "const") {
return Qualifier::Const;
}
throw std::runtime_error("Unsupported qualifier: " + std::string{line});
}
size_t stripIndent(std::string_view& line) {
auto indent = line.find_first_not_of(" ");
line.remove_prefix(indent);
return indent;
}
bool tryRemovePrefix(std::string_view& line, std::string_view prefix) {
if (!line.starts_with(prefix))
return false;
line.remove_prefix(prefix.size());
return true;
}
void removePrefix(std::string_view& line, std::string_view prefix) {
if (!tryRemovePrefix(line, prefix))
throw std::runtime_error("Unexpected line prefix. Expected '" +
std::string{prefix} + "'. Got '" +
std::string{line} + "'.");
}
std::optional<uint64_t> tryParseIntAttribute(std::string_view line,
std::string_view marker) {
auto attrStartPos = line.find(marker);
if (attrStartPos == line.npos)
return {};
auto valStartPos = attrStartPos + marker.size();
auto valEndPos = line.find_first_not_of("0123456789", valStartPos);
auto valStr = line.substr(valStartPos, valEndPos);
uint64_t val = std::stoi(std::string{valStr}); // Makes a string copy :'(
return val;
}
uint64_t parseIntAttribute(std::string_view line,
std::string_view type,
std::string_view marker) {
auto val = tryParseIntAttribute(line, marker);
if (!val)
throw std::runtime_error(std::string{type} + " must have an attribute: '" +
std::string{marker} + "'. Got: '" +
std::string{line} + "'");
return *val;
}
std::optional<std::string_view> tryParseStringValue(std::string_view& input,
std::string_view marker,
size_t rootIndent) {
std::string_view modifiedInput = input;
std::string_view line;
getline(modifiedInput, line);
size_t indent = stripIndent(line);
if (indent != rootIndent)
return {};
if (!tryRemovePrefix(line, marker))
return {};
auto val = line;
// Update input to point to after the value we have read
input = modifiedInput;
return val;
}
NodeId getId(std::string_view str, size_t* idLen = nullptr) {
if (idLen)
*idLen = 0;
if (str[0] != '[')
return -1;
auto closeBracket = str.find(']');
if (closeBracket == str.npos)
return -1;
auto idStr = str.substr(1, closeBracket);
NodeId id = std::stoi(std::string{idStr}); // Makes a string copy :'(
if (idLen)
*idLen = closeBracket + 2; // +2 for the trailing "] "
return id;
}
} // namespace
void TypeGraphParser::parse(std::string_view input) {
size_t rootIndent = input.find_first_not_of("[]0123456789 ");
while (!input.empty()) {
Type& type = parseType(input, rootIndent);
typeGraph_.addRoot(type);
}
}
Type& TypeGraphParser::parseType(std::string_view& input, size_t rootIndent) {
std::string_view line;
getline(input, line);
size_t idLen = 0;
NodeId id = getId(line, &idLen);
line.remove_prefix(idLen);
size_t indent = stripIndent(line) + idLen;
if (indent != rootIndent)
throw std::runtime_error("Unexpected indent for line: " +
std::string{line});
auto nodeEndPos = line.find_first_of(": \n");
auto nodeTypeName = line.substr(0, nodeEndPos);
Type* type = nullptr;
if (NodeId refId = getId(nodeTypeName); refId != -1) {
auto it = nodesById_.find(refId);
if (it == nodesById_.end())
throw std::runtime_error("Node ID referenced before definition: " +
std::to_string(refId));
type = &it->second.get();
} else if (nodeTypeName == "Class" || nodeTypeName == "Struct" ||
nodeTypeName == "Union") {
// Format: "Class: MyClass (size: 12)"
Class::Kind kind;
if (nodeTypeName == "Class")
kind = Class::Kind::Class;
else if (nodeTypeName == "Struct")
kind = Class::Kind::Struct;
else if (nodeTypeName == "Union")
kind = Class::Kind::Union;
else
abort();
// Extract name
auto nameStartPos = line.find(' ') + 1;
auto nameEndPos = line.find('(', nameStartPos + 1);
auto name = line.substr(nameStartPos, nameEndPos - nameStartPos - 1);
auto size = parseIntAttribute(line, nodeTypeName, "size: ");
auto align = tryParseIntAttribute(line, "align: ");
Class& c = typeGraph_.makeType<Class>(id, kind, std::string{name}, size);
if (align)
c.setAlign(*align);
nodesById_.insert({id, c});
parseParams(c, input, indent + 2);
parseParents(c, input, indent + 2);
parseMembers(c, input, indent + 2);
parseFunctions(c, input, indent + 2);
parseChildren(c, input, indent + 2);
type = &c;
} else if (nodeTypeName == "Container") {
// Format: "Container: std::vector (size: 24)
// Extract container type name
auto nameStartPos = line.find(' ') + 1;
auto nameEndPos = line.find('(', nameStartPos + 1);
auto name = line.substr(nameStartPos, nameEndPos - nameStartPos - 1);
auto info = getContainerInfo(name);
auto size = parseIntAttribute(line, nodeTypeName, "size: ");
Container& c = typeGraph_.makeType<Container>(id, info, size);
nodesById_.insert({id, c});
parseParams(c, input, indent + 2);
type = &c;
} else if (nodeTypeName == "Primitive") {
// Format: "Primitive: int32_t"
removePrefix(line, "Primitive: ");
auto kind = getKind(line);
type = &typeGraph_.makeType<Primitive>(kind);
} else if (nodeTypeName == "Enum") {
// Format: "Enum: MyEnum (size: 4)"
removePrefix(line, "Enum: ");
auto nameEndPos = line.find(' ');
auto name = line.substr(0, nameEndPos);
auto size = parseIntAttribute(line, nodeTypeName, "size: ");
type = &typeGraph_.makeType<Enum>(std::string{name}, size);
} else if (nodeTypeName == "Array") {
// Format: "Array: (length: 5)
auto len = parseIntAttribute(line, nodeTypeName, "length: ");
auto& elementType = parseType(input, indent + 2);
type = &typeGraph_.makeType<Array>(id, elementType, len);
} else if (nodeTypeName == "Typedef") {
// Format: "Typedef: myTypedef"
removePrefix(line, "Typedef: ");
auto name = line;
auto& underlyingType = parseType(input, indent + 2);
type = &typeGraph_.makeType<Typedef>(id, std::string{name}, underlyingType);
nodesById_.insert({id, *type});
} else if (nodeTypeName == "Pointer") {
// Format: "Pointer"
auto& pointeeType = parseType(input, indent + 2);
type = &typeGraph_.makeType<Pointer>(id, pointeeType);
nodesById_.insert({id, *type});
} else {
throw std::runtime_error("Unsupported node type: " +
std::string{nodeTypeName});
}
return *type;
}
template <typename T>
void TypeGraphParser::parseParams(T& c,
std::string_view& input,
size_t rootIndent) {
std::string_view origInput = input;
for (std::string_view line; getline(input, line); origInput = input) {
size_t indent = stripIndent(line);
if (indent != rootIndent)
break;
// Format: "Param"
if (!tryRemovePrefix(line, "Param"))
break;
if (auto value = tryParseStringValue(input, "Value: ", rootIndent + 2);
value) {
c.templateParams.emplace_back(std::string{*value});
} else {
Type& type = parseType(input, rootIndent + 2);
TemplateParam param{type};
if (auto qualStr =
tryParseStringValue(input, "Qualifiers: ", rootIndent + 2);
qualStr) {
Qualifier qual = getQualifier(*qualStr);
param.qualifiers[qual] = true;
}
c.templateParams.push_back(param);
}
}
// No more params for us - put back the line we just read
input = origInput;
}
void TypeGraphParser::parseParents(Class& c,
std::string_view& input,
size_t rootIndent) {
std::string_view origInput = input;
for (std::string_view line; getline(input, line); origInput = input) {
size_t indent = stripIndent(line);
if (indent != rootIndent)
break;
// Format: "Parent (offset: 0)"
if (!tryRemovePrefix(line, "Parent "))
break;
auto offset = parseIntAttribute(line, "Parent", "offset: ");
Type& type = parseType(input, rootIndent + 2);
c.parents.emplace_back(type, offset * 8);
}
// No more parents for us - put back the line we just read
input = origInput;
}
void TypeGraphParser::parseMembers(Class& c,
std::string_view& input,
size_t rootIndent) {
std::string_view origInput = input;
for (std::string_view line; getline(input, line); origInput = input) {
size_t indent = stripIndent(line);
if (indent != rootIndent)
break;
// Format: "Member: memberName (offset: 0)"
if (!tryRemovePrefix(line, "Member: "))
break;
auto nameEndPos = line.find(' ');
auto name = line.substr(0, nameEndPos);
auto offset = parseIntAttribute(line, "Member", "offset: ");
auto align = tryParseIntAttribute(line, "align: ");
Type& type = parseType(input, rootIndent + 2);
Member member{type, std::string{name}, offset * 8};
if (align)
member.align = *align;
c.members.push_back(member);
}
// No more members for us - put back the line we just read
input = origInput;
}
void TypeGraphParser::parseFunctions(Class& c,
std::string_view& input,
size_t rootIndent) {
std::string_view origInput = input;
for (std::string_view line; getline(input, line); origInput = input) {
size_t indent = stripIndent(line);
if (indent != rootIndent)
break;
// Format: "Function: funcName"
if (!tryRemovePrefix(line, "Function: "))
break;
auto name = line;
Function func{std::string{name}};
c.functions.push_back(func);
}
// No more functions for us - put back the line we just read
input = origInput;
}
void TypeGraphParser::parseChildren(Class& c,
std::string_view& input,
size_t rootIndent) {
std::string_view origInput = input;
for (std::string_view line; getline(input, line); origInput = input) {
size_t indent = stripIndent(line);
if (indent != rootIndent)
break;
// Format: "Child"
if (!tryRemovePrefix(line, "Child"))
break;
Type& type = parseType(input, rootIndent + 2);
auto* childClass = dynamic_cast<Class*>(&type);
if (!childClass)
throw std::runtime_error("Invalid type for child");
c.children.push_back(*childClass);
}
// No more children for us - put back the line we just read
input = origInput;
}

37
test/TypeGraphParser.h Normal file
View File

@ -0,0 +1,37 @@
#pragma once
#include <functional>
#include <unordered_map>
#include "oi/type_graph/Types.h"
using namespace type_graph;
namespace type_graph {
class TypeGraph;
} // namespace type_graph
/*
* TypeGraphParser
*
* Parses a textual type graph, as emitted by Printer.
*/
class TypeGraphParser {
public:
TypeGraphParser(TypeGraph& typeGraph) : typeGraph_(typeGraph) {
}
void parse(std::string_view input);
private:
TypeGraph& typeGraph_;
std::unordered_map<NodeId, std::reference_wrapper<Type>> nodesById_;
Type& parseType(std::string_view& input, size_t rootIndent);
template <typename T>
void parseParams(T& c, std::string_view& input, size_t rootIndent);
void parseParents(Class& c, std::string_view& input, size_t rootIndent);
void parseMembers(Class& c, std::string_view& input, size_t rootIndent);
void parseFunctions(Class& c, std::string_view& input, size_t rootIndent);
void parseChildren(Class& c, std::string_view& input, size_t rootIndent);
};

View File

@ -7,6 +7,7 @@
using namespace type_graph;
TEST(FlattenerTest, NoParents) {
// No change
// Original and flattened:
// struct MyStruct { int n0; };
// class MyClass {
@ -14,18 +15,7 @@ TEST(FlattenerTest, NoParents) {
// MyEnum e;
// MyStruct mystruct;
// };
auto myint = Primitive{Primitive::Kind::Int32};
auto myenum = Enum{"MyEnum", 4};
auto mystruct = Class{1, Class::Kind::Struct, "MyStruct", 4};
auto myclass = Class{0, Class::Kind::Class, "MyClass", 12};
mystruct.members.push_back(Member{myint, "n0", 0});
myclass.members.push_back(Member{myint, "n", 0});
myclass.members.push_back(Member{myenum, "e", 4 * 8});
myclass.members.push_back(Member{mystruct, "mystruct", 8 * 8});
test(Flattener::createPass(), {myclass}, R"(
testNoChange(Flattener::createPass(), R"(
[0] Class: MyClass (size: 12)
Member: n (offset: 0)
Primitive: int32_t
@ -49,18 +39,18 @@ TEST(FlattenerTest, OnlyParents) {
// int b;
// int c;
// };
auto myint = Primitive{Primitive::Kind::Int32};
auto classA = Class{0, Class::Kind::Class, "ClassA", 8};
auto classB = Class{1, Class::Kind::Class, "ClassB", 4};
auto classC = Class{2, Class::Kind::Class, "ClassC", 4};
classC.members.push_back(Member{myint, "c", 0});
classB.members.push_back(Member{myint, "b", 0});
classA.parents.push_back(Parent{classB, 0});
classA.parents.push_back(Parent{classC, 4 * 8});
test(Flattener::createPass(), {classA}, R"(
test(Flattener::createPass(), R"(
[0] Class: ClassA (size: 8)
Parent (offset: 0)
[1] Class: ClassB (size: 4)
Member: b (offset: 0)
Primitive: int32_t
Parent (offset: 4)
[2] Class: ClassC (size: 4)
Member: c (offset: 0)
Primitive: int32_t
)",
R"(
[0] Class: ClassA (size: 8)
Member: b (offset: 0)
Primitive: int32_t
@ -81,19 +71,21 @@ TEST(FlattenerTest, ParentsFirst) {
// int c;
// int a;
// };
auto myint = Primitive{Primitive::Kind::Int32};
auto classA = Class{0, Class::Kind::Class, "ClassA", 12};
auto classB = Class{1, Class::Kind::Class, "ClassB", 4};
auto classC = Class{2, Class::Kind::Class, "ClassC", 4};
classC.members.push_back(Member{myint, "c", 0});
classB.members.push_back(Member{myint, "b", 0});
classA.parents.push_back(Parent{classB, 0});
classA.parents.push_back(Parent{classC, 4 * 8});
classA.members.push_back(Member{myint, "a", 8 * 8});
test(Flattener::createPass(), {classA}, R"(
test(Flattener::createPass(), R"(
[0] Class: ClassA (size: 12)
Parent (offset: 0)
[1] Class: ClassB (size: 4)
Member: b (offset: 0)
Primitive: int32_t
Parent (offset: 4)
[2] Class: ClassC (size: 4)
Member: c (offset: 0)
Primitive: int32_t
Member: a (offset: 8)
Primitive: int32_t
)",
R"(
[0] Class: ClassA (size: 12)
Member: b (offset: 0)
Primitive: int32_t
@ -116,20 +108,21 @@ TEST(FlattenerTest, MembersFirst) {
// int b;
// int c;
// };
auto myint = Primitive{Primitive::Kind::Int32};
auto classA = Class{0, Class::Kind::Class, "ClassA", 12};
auto classB = Class{1, Class::Kind::Class, "ClassB", 4};
auto classC = Class{2, Class::Kind::Class, "ClassC", 4};
classC.members.push_back(Member{myint, "c", 0});
classB.members.push_back(Member{myint, "b", 0});
classA.members.push_back(Member{myint, "a", 0});
classA.parents.push_back(Parent{classB, 4 * 8});
classA.parents.push_back(Parent{classC, 8 * 8});
test(Flattener::createPass(), {classA}, R"(
test(Flattener::createPass(), R"(
[0] Class: ClassA (size: 12)
Parent (offset: 4)
[1] Class: ClassB (size: 4)
Member: b (offset: 0)
Primitive: int32_t
Parent (offset: 8)
[2] Class: ClassC (size: 4)
Member: c (offset: 0)
Primitive: int32_t
Member: a (offset: 0)
Primitive: int32_t
)",
R"(
[0] Class: ClassA (size: 12)
Member: a (offset: 0)
Primitive: int32_t
@ -153,21 +146,23 @@ TEST(FlattenerTest, MixedMembersAndParents) {
// int a2;
// int c;
// };
auto myint = Primitive{Primitive::Kind::Int32};
auto classA = Class{0, Class::Kind::Class, "ClassA", 16};
auto classB = Class{1, Class::Kind::Class, "ClassB", 4};
auto classC = Class{2, Class::Kind::Class, "ClassC", 4};
classC.members.push_back(Member{myint, "c", 0});
classB.members.push_back(Member{myint, "b", 0});
classA.parents.push_back(Parent{classB, 0});
classA.members.push_back(Member{myint, "a1", 4 * 8});
classA.members.push_back(Member{myint, "a2", 8 * 8});
classA.parents.push_back(Parent{classC, 12 * 8});
test(Flattener::createPass(), {classA}, R"(
test(Flattener::createPass(), R"(
[0] Class: ClassA (size: 16)
Parent (offset: 0)
[1] Class: ClassB (size: 4)
Member: b (offset: 0)
Primitive: int32_t
Parent (offset: 12)
[2] Class: ClassC (size: 4)
Member: c (offset: 0)
Primitive: int32_t
Member: a1 (offset: 4)
Primitive: int32_t
Member: a2 (offset: 8)
Primitive: int32_t
)",
R"(
[0] Class: ClassA (size: 16)
Member: b (offset: 0)
Primitive: int32_t
@ -192,19 +187,21 @@ TEST(FlattenerTest, EmptyParent) {
// int a1;
// int a2;
// };
auto myint = Primitive{Primitive::Kind::Int32};
auto classA = Class{0, Class::Kind::Class, "ClassA", 12};
auto classB = Class{1, Class::Kind::Class, "ClassB", 0};
auto classC = Class{2, Class::Kind::Class, "ClassC", 4};
classC.members.push_back(Member{myint, "c", 0});
classA.members.push_back(Member{myint, "a1", 4 * 8});
classA.members.push_back(Member{myint, "a2", 8 * 8});
classA.parents.push_back(Parent{classB, 0});
classA.parents.push_back(Parent{classC, 0});
test(Flattener::createPass(), {classA}, R"(
test(Flattener::createPass(), R"(
[0] Class: ClassA (size: 12)
Parent (offset: 0)
[1] Class: ClassB (size: 0)
Parent (offset: 0)
[2] Class: ClassC (size: 4)
Member: c (offset: 0)
Primitive: int32_t
Member: a1 (offset: 4)
Primitive: int32_t
Member: a2 (offset: 8)
Primitive: int32_t
)",
R"(
[0] Class: ClassA (size: 12)
Member: c (offset: 0)
Primitive: int32_t
@ -229,24 +226,25 @@ TEST(FlattenerTest, TwoDeep) {
// int c;
// int a;
// };
auto myint = Primitive{Primitive::Kind::Int32};
auto classA = Class{0, Class::Kind::Class, "ClassA", 16};
auto classB = Class{1, Class::Kind::Class, "ClassB", 8};
auto classC = Class{2, Class::Kind::Class, "ClassC", 4};
auto classD = Class{3, Class::Kind::Class, "ClassD", 4};
classD.members.push_back(Member{myint, "d", 0});
classC.members.push_back(Member{myint, "c", 0});
classB.parents.push_back(Parent{classD, 0});
classB.members.push_back(Member{myint, "b", 4 * 8});
classA.parents.push_back(Parent{classB, 0});
classA.parents.push_back(Parent{classC, 8 * 8});
classA.members.push_back(Member{myint, "a", 12 * 8});
test(Flattener::createPass(), {classA}, R"(
test(Flattener::createPass(), R"(
[0] Class: ClassA (size: 16)
Parent (offset: 0)
[1] Class: ClassB (size: 8)
Parent (offset: 0)
[2] Class: ClassD (size: 4)
Member: d (offset: 0)
Primitive: int32_t
Member: b (offset: 4)
Primitive: int32_t
Parent (offset: 8)
[3] Class: ClassC (size: 4)
Member: c (offset: 0)
Primitive: int32_t
Member: a (offset: 12)
Primitive: int32_t
)",
R"(
[0] Class: ClassA (size: 16)
Member: d (offset: 0)
Primitive: int32_t
@ -272,21 +270,23 @@ TEST(FlattenerTest, DiamondInheritance) {
// int c1;
// int a;
// };
auto myint = Primitive{Primitive::Kind::Int32};
auto classA = Class{0, Class::Kind::Class, "ClassA", 16};
auto classB = Class{1, Class::Kind::Class, "ClassB", 8};
auto classC = Class{2, Class::Kind::Class, "ClassC", 4};
classC.members.push_back(Member{myint, "c", 0});
classB.parents.push_back(Parent{classC, 0});
classB.members.push_back(Member{myint, "b", 4 * 8});
classA.parents.push_back(Parent{classB, 0});
classA.parents.push_back(Parent{classC, 8 * 8});
classA.members.push_back(Member{myint, "a", 12 * 8});
test(Flattener::createPass(), {classA}, R"(
test(Flattener::createPass(), R"(
[0] Class: ClassA (size: 16)
Parent (offset: 0)
[1] Class: ClassB (size: 8)
Parent (offset: 0)
[2] Class: ClassC (size: 4)
Member: c (offset: 0)
Primitive: int32_t
Member: b (offset: 4)
Primitive: int32_t
Parent (offset: 8)
[2]
Member: a (offset: 12)
Primitive: int32_t
)",
R"(
[0] Class: ClassA (size: 16)
Member: c (offset: 0)
Primitive: int32_t
@ -308,20 +308,21 @@ TEST(FlattenerTest, Member) {
// Flattened:
// class B { int c; int b; };
// Class A { int a; B b; };
auto myint = Primitive{Primitive::Kind::Int32};
auto classA = Class{0, Class::Kind::Class, "ClassA", 12};
auto classB = Class{1, Class::Kind::Class, "ClassB", 8};
auto classC = Class{2, Class::Kind::Class, "ClassC", 4};
classC.members.push_back(Member{myint, "c", 0});
classB.parents.push_back(Parent{classC, 0});
classB.members.push_back(Member{myint, "b", 4 * 8});
classA.members.push_back(Member{myint, "a", 0});
classA.members.push_back(Member{classB, "b", 4 * 8});
test(Flattener::createPass(), {classA}, R"(
test(Flattener::createPass(), R"(
[0] Class: ClassA (size: 12)
Member: a (offset: 0)
Primitive: int32_t
Member: b (offset: 4)
[1] Class: ClassB (size: 8)
Parent (offset: 0)
[2] Class: ClassC (size: 4)
Member: c (offset: 0)
Primitive: int32_t
Member: b (offset: 4)
Primitive: int32_t
)",
R"(
[0] Class: ClassA (size: 12)
Member: a (offset: 0)
Primitive: int32_t
@ -343,20 +344,21 @@ TEST(FlattenerTest, MemberOfParent) {
// Flattened:
// class C { int c; };
// class A { int b; C c; int a; };
auto myint = Primitive{Primitive::Kind::Int32};
auto classA = Class{0, Class::Kind::Class, "ClassA", 12};
auto classB = Class{1, Class::Kind::Class, "ClassB", 8};
auto classC = Class{2, Class::Kind::Class, "ClassC", 4};
classC.members.push_back(Member{myint, "c", 0});
classB.members.push_back(Member{myint, "b", 0});
classB.members.push_back(Member{classC, "c", 4 * 8});
classA.parents.push_back(Parent{classB, 0});
classA.members.push_back(Member{myint, "a", 8 * 8});
test(Flattener::createPass(), {classA}, R"(
test(Flattener::createPass(), R"(
[0] Class: ClassA (size: 12)
Parent (offset: 0)
[1] Class: ClassB (size: 8)
Member: b (offset: 0)
Primitive: int32_t
Member: c (offset: 4)
[2] Class: ClassC (size: 4)
Member: c (offset: 0)
Primitive: int32_t
Member: a (offset: 8)
Primitive: int32_t
)",
R"(
[0] Class: ClassA (size: 12)
Member: b (offset: 0)
Primitive: int32_t
@ -378,20 +380,21 @@ TEST(FlattenerTest, ContainerParam) {
// Flattened:
// class A { int b; int a; };
// std::vector<A, int>
auto myint = Primitive{Primitive::Kind::Int32};
auto classA = Class{1, Class::Kind::Class, "ClassA", 8};
auto classB = Class{2, Class::Kind::Class, "ClassB", 4};
auto container = getVector();
classB.members.push_back(Member{myint, "b", 0});
classA.parents.push_back(Parent{classB, 0});
classA.members.push_back(Member{myint, "a", 4 * 8});
container.templateParams.push_back(TemplateParam{classA});
container.templateParams.push_back(TemplateParam{myint});
test(Flattener::createPass(), {container}, R"(
test(Flattener::createPass(), R"(
[0] Container: std::vector (size: 24)
Param
[1] Class: ClassA (size: 8)
Parent (offset: 0)
[2] Class: ClassB (size: 4)
Member: b (offset: 0)
Primitive: int32_t
Member: a (offset: 4)
Primitive: int32_t
Param
Primitive: int32_t
)",
R"(
[0] Container: std::vector (size: 24)
Param
[1] Class: ClassA (size: 8)
@ -409,18 +412,18 @@ TEST(FlattenerTest, Array) {
// class B { int b; };
// class A : B { int a; };
// A[5]
auto myint = Primitive{Primitive::Kind::Int32};
auto classB = Class{2, Class::Kind::Class, "ClassB", 4};
classB.members.push_back(Member{myint, "b", 0});
auto classA = Class{1, Class::Kind::Class, "ClassA", 8};
classA.parents.push_back(Parent{classB, 0});
classA.members.push_back(Member{myint, "a", 4 * 8});
auto arrayA = Array{0, classA, 5};
test(Flattener::createPass(), {arrayA}, R"(
test(Flattener::createPass(), R"(
[0] Array: (length: 5)
[1] Class: ClassA (size: 8)
Parent (offset: 0)
[2] Class: ClassB (size: 4)
Member: b (offset: 0)
Primitive: int32_t
Member: a (offset: 4)
Primitive: int32_t
)",
R"(
[0] Array: (length: 5)
[1] Class: ClassA (size: 8)
Member: b (offset: 0)
@ -435,17 +438,18 @@ TEST(FlattenerTest, Typedef) {
// class B { int b; };
// class A : B { int a; };
// using aliasA = A;
auto myint = Primitive{Primitive::Kind::Int32};
auto classB = Class{2, Class::Kind::Class, "ClassB", 4};
classB.members.push_back(Member{myint, "b", 0});
auto classA = Class{1, Class::Kind::Class, "ClassA", 8};
classA.parents.push_back(Parent{classB, 0});
classA.members.push_back(Member{myint, "a", 4 * 8});
auto aliasA = Typedef{0, "aliasA", classA};
test(Flattener::createPass(), {aliasA}, R"(
test(Flattener::createPass(), R"(
[0] Typedef: aliasA
[1] Class: ClassA (size: 8)
Parent (offset: 0)
[2] Class: ClassB (size: 4)
Member: b (offset: 0)
Primitive: int32_t
Member: a (offset: 4)
Primitive: int32_t
)",
R"(
[0] Typedef: aliasA
[1] Class: ClassA (size: 8)
Member: b (offset: 0)
@ -460,17 +464,18 @@ TEST(FlattenerTest, TypedefParent) {
// class B { int b; };
// using aliasB = B;
// class A : aliasB { int a; };
auto myint = Primitive{Primitive::Kind::Int32};
auto classB = Class{1, Class::Kind::Class, "ClassB", 4};
classB.members.push_back(Member{myint, "b", 0});
auto aliasB = Typedef{2, "aliasB", classB};
auto classA = Class{0, Class::Kind::Class, "ClassA", 8};
classA.parents.push_back(Parent{aliasB, 0});
classA.members.push_back(Member{myint, "a", 4 * 8});
test(Flattener::createPass(), {classA}, R"(
test(Flattener::createPass(), R"(
[0] Class: ClassA (size: 8)
Parent (offset: 0)
[1] Typedef: aliasB
[2] Class: ClassB (size: 4)
Member: b (offset: 0)
Primitive: int32_t
Member: a (offset: 4)
Primitive: int32_t
)",
R"(
[0] Class: ClassA (size: 8)
Member: b (offset: 0)
Primitive: int32_t
@ -483,7 +488,7 @@ TEST(FlattenerTest, Pointer) {
// Original:
// class B { int b; };
// class A : B { int a; };
// class C { A a; };
// class C { A* a; };
auto myint = Primitive{Primitive::Kind::Int32};
auto classB = Class{3, Class::Kind::Class, "ClassB", 4};
@ -497,7 +502,19 @@ TEST(FlattenerTest, Pointer) {
auto classC = Class{0, Class::Kind::Class, "ClassC", 8};
classC.members.push_back(Member{ptrA, "a", 0});
test(Flattener::createPass(), {classC}, R"(
test(Flattener::createPass(), R"(
[0] Class: ClassC (size: 8)
Member: a (offset: 0)
[1] Pointer
[2] Class: ClassA (size: 8)
Parent (offset: 0)
[3] Class: ClassB (size: 4)
Member: b (offset: 0)
Primitive: int32_t
Member: a (offset: 4)
Primitive: int32_t
)",
R"(
[0] Class: ClassC (size: 8)
Member: a (offset: 0)
[1] Pointer
@ -511,7 +528,7 @@ TEST(FlattenerTest, Pointer) {
TEST(FlattenerTest, PointerCycle) {
// Original:
// class B { A a };
// class B { A* a };
// class A { B b; };
//
// Flattened:
@ -522,7 +539,16 @@ TEST(FlattenerTest, PointerCycle) {
classA.members.push_back(Member{classB, "b", 0});
classB.members.push_back(Member{ptrA, "a", 0});
test(Flattener::createPass(), {classA, classB}, R"(
test(Flattener::createPass(), R"(
[0] Class: ClassA (size: 69)
Member: b (offset: 0)
[1] Class: ClassB (size: 69)
Member: a (offset: 0)
[2] Pointer
[0]
[1]
)",
R"(
[0] Class: ClassA (size: 69)
Member: b (offset: 0)
[1] Class: ClassB (size: 69)
@ -538,23 +564,8 @@ TEST(FlattenerTest, Alignment) {
// class alignas(16) C { int c; };
// class B { alignas(8) int b; };
// class A : B, C { int a; };
auto myint = Primitive{Primitive::Kind::Int32};
auto classA = Class{0, Class::Kind::Class, "ClassA", 12};
auto classB = Class{1, Class::Kind::Class, "ClassB", 4};
auto classC = Class{2, Class::Kind::Class, "ClassC", 4};
classC.setAlign(16);
classC.members.push_back(Member{myint, "c", 0});
Member memberB{myint, "b", 0};
memberB.align = 8;
classB.members.push_back(memberB);
classA.parents.push_back(Parent{classB, 0});
classA.parents.push_back(Parent{classC, 4 * 8});
classA.members.push_back(Member{myint, "a", 8 * 8});
test(Flattener::createPass(), {classA}, R"(
test(Flattener::createPass(), R"(
[0] Class: ClassA (size: 12)
Parent (offset: 0)
[1] Class: ClassB (size: 4)
@ -594,7 +605,17 @@ TEST(FlattenerTest, Functions) {
classB.functions.push_back(Function{"funcB"});
classC.functions.push_back(Function{"funcC"});
test(Flattener::createPass(), {classA}, R"(
test(Flattener::createPass(), R"(
[0] Class: ClassA (size: 0)
Parent (offset: 0)
[1] Class: ClassB (size: 0)
Function: funcB
Parent (offset: 0)
[2] Class: ClassC (size: 0)
Function: funcC
Function: funcA
)",
R"(
[0] Class: ClassA (size: 0)
Function: funcA
Function: funcB
@ -607,21 +628,23 @@ TEST(FlattenerTest, Children) {
// class C { int c; };
// class B { int b; };
// class A : B, C { };
auto myint = Primitive{Primitive::Kind::Int32};
auto classA = Class{1, Class::Kind::Class, "ClassA", 8};
auto classB = Class{0, Class::Kind::Class, "ClassB", 4};
auto classC = Class{2, Class::Kind::Class, "ClassC", 4};
classC.members.push_back(Member{myint, "c", 0});
classB.members.push_back(Member{myint, "b", 0});
classA.parents.push_back(Parent{classB, 0});
classA.parents.push_back(Parent{classC, 4 * 8});
classB.children.push_back(classA);
classC.children.push_back(classA);
test(Flattener::createPass(), {classB}, R"(
test(Flattener::createPass(), R"(
[0] Class: ClassB (size: 4)
Member: b (offset: 0)
Primitive: int32_t
Child
[1] Class: ClassA (size: 8)
Parent (offset: 0)
[0]
Parent (offset: 4)
[2] Class: ClassC (size: 4)
Member: c (offset: 0)
Primitive: int32_t
Child
[1]
)",
R"(
[0] Class: ClassB (size: 4)
Member: b (offset: 0)
Primitive: int32_t
@ -640,28 +663,31 @@ TEST(FlattenerTest, ChildrenTwoDeep) {
// class C { int c; };
// class B : D { int b; };
// class A : B, C { int a; };
auto myint = Primitive{Primitive::Kind::Int32};
auto classA = Class{2, Class::Kind::Class, "ClassA", 16};
auto classB = Class{1, Class::Kind::Class, "ClassB", 8};
auto classC = Class{3, Class::Kind::Class, "ClassC", 4};
auto classD = Class{0, Class::Kind::Class, "ClassD", 4};
classD.members.push_back(Member{myint, "d", 0});
classC.members.push_back(Member{myint, "c", 0});
classB.parents.push_back(Parent{classD, 0});
classB.members.push_back(Member{myint, "b", 4 * 8});
classA.parents.push_back(Parent{classB, 0});
classA.parents.push_back(Parent{classC, 8 * 8});
classA.members.push_back(Member{myint, "a", 12 * 8});
classD.children.push_back(classB);
classB.children.push_back(classA);
classC.children.push_back(classA);
test(Flattener::createPass(), {classD}, R"(
test(Flattener::createPass(), R"(
[0] Class: ClassD (size: 4)
Member: d (offset: 0)
Primitive: int32_t
Child
[1] Class: ClassB (size: 8)
Parent (offset: 0)
[0]
Member: b (offset: 4)
Primitive: int32_t
Child
[2] Class: ClassA (size: 16)
Parent (offset: 0)
[1]
Parent (offset: 8)
[3] Class: ClassC (size: 4)
Member: c (offset: 0)
Primitive: int32_t
Child
[2]
Member: a (offset: 12)
Primitive: int32_t
)",
R"(
[0] Class: ClassD (size: 4)
Member: d (offset: 0)
Primitive: int32_t
@ -687,16 +713,7 @@ TEST(FlattenerTest, ChildrenTwoDeep) {
}
TEST(FlattenerTest, ParentContainer) {
auto myint = Primitive{Primitive::Kind::Int32};
auto vector = getVector();
vector.templateParams.push_back(TemplateParam{myint});
auto classA = Class{0, Class::Kind::Class, "ClassA", 32};
classA.parents.push_back(Parent{vector, 0});
classA.members.push_back(Member{myint, "a", 24 * 8});
test(Flattener::createPass(), {classA}, R"(
test(Flattener::createPass(), R"(
[0] Class: ClassA (size: 32)
Parent (offset: 0)
[1] Container: std::vector (size: 24)
@ -717,16 +734,7 @@ TEST(FlattenerTest, ParentContainer) {
}
TEST(FlattenerTest, ParentTwoContainers) {
auto myint = Primitive{Primitive::Kind::Int32};
auto vector = getVector();
vector.templateParams.push_back(TemplateParam{myint});
auto classA = Class{0, Class::Kind::Class, "ClassA", 48};
classA.parents.push_back(Parent{vector, 0});
classA.parents.push_back(Parent{vector, 24 * 8});
test(Flattener::createPass(), {classA}, R"(
test(Flattener::createPass(), R"(
[0] Class: ClassA (size: 48)
Parent (offset: 0)
[1] Container: std::vector (size: 24)
@ -747,19 +755,7 @@ TEST(FlattenerTest, ParentTwoContainers) {
}
TEST(FlattenerTest, ParentClassAndContainer) {
auto myint = Primitive{Primitive::Kind::Int32};
auto vector = getVector();
vector.templateParams.push_back(TemplateParam{myint});
auto classB = Class{1, Class::Kind::Class, "ClassB", 4};
classB.members.push_back(Member{myint, "b", 0});
auto classA = Class{0, Class::Kind::Class, "ClassA", 32};
classA.parents.push_back(Parent{classB, 0});
classA.parents.push_back(Parent{vector, 8 * 8});
test(Flattener::createPass(), {classA}, R"(
test(Flattener::createPass(), R"(
[0] Class: ClassA (size: 32)
Parent (offset: 0)
[1] Class: ClassB (size: 4)
@ -782,33 +778,7 @@ TEST(FlattenerTest, ParentClassAndContainer) {
}
TEST(FlattenerTest, AllocatorParamInParent) {
ContainerInfo mapInfo{"std::map", STD_MAP_TYPE, "utility"};
mapInfo.stubTemplateParams = {2, 3};
Primitive myint{Primitive::Kind::Int32};
auto pair = getPair(3);
pair.templateParams.push_back(TemplateParam{myint, {Qualifier::Const}});
pair.templateParams.push_back(TemplateParam{myint});
Class myallocBase{2, Class::Kind::Struct,
"MyAllocBase<std::pair<const int, int>>", 1};
myallocBase.templateParams.push_back(TemplateParam{pair});
myallocBase.functions.push_back(Function{"allocate"});
myallocBase.functions.push_back(Function{"deallocate"});
Class myalloc{1, Class::Kind::Struct, "MyAlloc<std::pair<const int, int>>",
1};
myalloc.parents.push_back(Parent{myallocBase, 0});
myalloc.functions.push_back(Function{"allocate"});
myalloc.functions.push_back(Function{"deallocate"});
Container map{0, mapInfo, 24};
map.templateParams.push_back(TemplateParam{myint});
map.templateParams.push_back(TemplateParam{myint});
map.templateParams.push_back(TemplateParam{myalloc});
test(Flattener::createPass(), {map}, R"(
test(Flattener::createPass(), R"(
[0] Container: std::map (size: 24)
Param
Primitive: int32_t
@ -853,26 +823,7 @@ TEST(FlattenerTest, AllocatorParamInParent) {
}
TEST(FlattenerTest, AllocatorUnfixableNoParent) {
Primitive myint{Primitive::Kind::Int32};
Class myalloc{1, Class::Kind::Struct, "MyAlloc", 1};
myalloc.functions.push_back(Function{"allocate"});
myalloc.functions.push_back(Function{"deallocate"});
auto vector = getVector();
vector.templateParams.push_back(TemplateParam{myint});
vector.templateParams.push_back(TemplateParam{myalloc});
test(Flattener::createPass(), {vector}, R"(
[0] Container: std::vector (size: 24)
Param
Primitive: int32_t
Param
[1] Struct: MyAlloc (size: 1)
Function: allocate
Function: deallocate
)",
R"(
testNoChange(Flattener::createPass(), R"(
[0] Container: std::vector (size: 24)
Param
Primitive: int32_t
@ -885,26 +836,7 @@ TEST(FlattenerTest, AllocatorUnfixableNoParent) {
TEST(FlattenerTest, AllocatorUnfixableParentNotClass) {
// This could be supported if need-be, we just don't do it yet
Primitive myint{Primitive::Kind::Int32};
auto pair = getPair(3);
pair.templateParams.push_back(TemplateParam{myint, {Qualifier::Const}});
pair.templateParams.push_back(TemplateParam{myint});
ContainerInfo stdAllocatorInfo{"std::allocator", DUMMY_TYPE, "memory"};
Container stdAllocator{2, stdAllocatorInfo, 1};
stdAllocator.templateParams.push_back(TemplateParam{pair});
Class myalloc{1, Class::Kind::Struct, "MyAlloc", 1};
myalloc.parents.push_back(Parent{stdAllocator, 0});
myalloc.functions.push_back(Function{"allocate"});
myalloc.functions.push_back(Function{"deallocate"});
auto vector = getVector();
vector.templateParams.push_back(TemplateParam{myint});
vector.templateParams.push_back(TemplateParam{myalloc});
test(Flattener::createPass(), {vector}, R"(
test(Flattener::createPass(), R"(
[0] Container: std::vector (size: 24)
Param
Primitive: int32_t
@ -943,22 +875,7 @@ TEST(FlattenerTest, AllocatorUnfixableParentNotClass) {
}
TEST(FlattenerTest, AllocatorUnfixableParentNoParams) {
Primitive myint{Primitive::Kind::Int32};
Class myallocBase{2, Class::Kind::Struct, "MyAllocBase", 1};
myallocBase.functions.push_back(Function{"allocate"});
myallocBase.functions.push_back(Function{"deallocate"});
Class myalloc{1, Class::Kind::Struct, "MyAlloc", 1};
myalloc.parents.push_back(Parent{myallocBase, 0});
myalloc.functions.push_back(Function{"allocate"});
myalloc.functions.push_back(Function{"deallocate"});
auto vector = getVector();
vector.templateParams.push_back(TemplateParam{myint});
vector.templateParams.push_back(TemplateParam{myalloc});
test(Flattener::createPass(), {vector}, R"(
test(Flattener::createPass(), R"(
[0] Container: std::vector (size: 24)
Param
Primitive: int32_t
@ -985,27 +902,7 @@ TEST(FlattenerTest, AllocatorUnfixableParentNoParams) {
}
TEST(FlattenerTest, AllocatorUnfixableParentParamIsValue) {
ContainerInfo mapInfo{"std::map", STD_MAP_TYPE, "utility"};
mapInfo.stubTemplateParams = {2, 3};
Primitive myint{Primitive::Kind::Int32};
Class myallocBase{2, Class::Kind::Struct, "MyAllocBase", 1};
myallocBase.templateParams.push_back(TemplateParam{"123"});
myallocBase.functions.push_back(Function{"allocate"});
myallocBase.functions.push_back(Function{"deallocate"});
Class myalloc{1, Class::Kind::Struct, "MyAlloc", 1};
myalloc.parents.push_back(Parent{myallocBase, 0});
myalloc.functions.push_back(Function{"allocate"});
myalloc.functions.push_back(Function{"deallocate"});
Container map{0, mapInfo, 24};
map.templateParams.push_back(TemplateParam{myint});
map.templateParams.push_back(TemplateParam{myint});
map.templateParams.push_back(TemplateParam{myalloc});
test(Flattener::createPass(), {map}, R"(
test(Flattener::createPass(), R"(
[0] Container: std::map (size: 24)
Param
Primitive: int32_t
@ -1038,16 +935,7 @@ TEST(FlattenerTest, AllocatorUnfixableParentParamIsValue) {
}
TEST(FlattenerTest, ClassParam) {
auto myint = Primitive{Primitive::Kind::Int32};
auto mychild = Class{1, Class::Kind::Class, "MyChild", 4};
auto myparent = Class{2, Class::Kind::Class, "MyParent", 4};
myparent.members.push_back(Member{myint, "a", 0});
mychild.parents.push_back(Parent{myparent, 0});
auto myclass = Class{0, Class::Kind::Class, "MyClass", 4};
myclass.templateParams.push_back(TemplateParam{mychild});
test(Flattener::createPass(), {myclass}, R"(
test(Flattener::createPass(), R"(
[0] Class: MyClass (size: 4)
Param
[1] Class: MyChild (size: 4)

View File

@ -6,6 +6,7 @@
#include "oi/type_graph/PassManager.h"
#include "oi/type_graph/Printer.h"
#include "oi/type_graph/TypeGraph.h"
#include "test/TypeGraphParser.h"
using type_graph::Container;
using type_graph::NodeId;
@ -26,10 +27,39 @@ void check(const std::vector<ref<Type>>& types,
printer.print(type);
}
expected.remove_prefix(1); // Remove initial '\n'
if (expected[0] == '\n')
expected.remove_prefix(1); // Remove initial '\n'
ASSERT_EQ(expected, out.str()) << "Test failure " << comment;
}
void test(type_graph::Pass pass,
std::string_view input,
std::string_view expectedAfter) {
input.remove_prefix(1); // Remove initial '\n'
TypeGraph typeGraph;
TypeGraphParser parser{typeGraph};
parser.parse(input);
// Validate input formatting
check(typeGraph.rootTypes(), input, " parsing input graph");
// Run pass and check results
test(pass, typeGraph.rootTypes(), expectedAfter);
}
void testNoChange(type_graph::Pass pass, std::string_view input) {
input.remove_prefix(1); // Remove initial '\n'
TypeGraph typeGraph;
TypeGraphParser parser{typeGraph};
parser.parse(input);
// Validate input formatting
check(typeGraph.rootTypes(), input, " parsing input graph");
// Run pass and check results
test(pass, typeGraph.rootTypes(), input);
}
void test(type_graph::Pass pass,
std::vector<ref<Type>> rootTypes,
std::string_view expectedBefore,

View File

@ -14,6 +14,12 @@ void check(const std::vector<std::reference_wrapper<type_graph::Type>>& types,
std::string_view expected,
std::string_view comment);
void test(type_graph::Pass pass,
std::string_view input,
std::string_view expectedAfter);
void testNoChange(type_graph::Pass pass, std::string_view input);
void test(type_graph::Pass pass,
std::vector<std::reference_wrapper<type_graph::Type>> rootTypes,
std::string_view expectedBefore,