blob: facda809905d8b2103bcb3a7bb7c3b7d7bbf9f79 [file] [log] [blame]
// Copyright 2018 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "src/developer/debug/zxdb/expr/expr_parser.h"
#include <sstream>
#include "gtest/gtest.h"
#include "src/developer/debug/zxdb/common/string_util.h"
#include "src/developer/debug/zxdb/expr/expr_tokenizer.h"
#include "src/developer/debug/zxdb/symbols/collection.h"
namespace zxdb {
namespace {
// This name looker-upper declares anything beginning with "Namespace" is a
// namespace, anything beginning with "Template" is a template, and anything
// beginning with "Type" is a type.
FoundName TestLookupName(const ParsedIdentifier& ident,
const FindNameOptions& opts) {
const ParsedIdentifierComponent& comp = ident.components().back();
const std::string& name = comp.name();
if (opts.find_namespaces && StringBeginsWith(name, "Namespace"))
return FoundName(FoundName::kNamespace, ident.GetFullName());
if (opts.find_types && StringBeginsWith(name, "Type")) {
// Make up a random class to go with the type.
auto type = fxl::MakeRefCounted<Collection>(DwarfTag::kClassType);
type->set_assigned_name("Type");
// NOTE: This doesn't qualify the type with namespaces or classes present
// in the identifier so qualified names ("Namespace::Type") won't convert
// to strings properly. This could be added if necessary.
return FoundName(std::move(type));
}
if (opts.find_templates && StringBeginsWith(name, "Template")) {
if (comp.has_template()) {
// Assume templates with arguments are types.
return FoundName(fxl::MakeRefCounted<Collection>(DwarfTag::kClassType));
}
return FoundName(FoundName::kTemplate, ident.GetFullName());
}
return FoundName();
}
} // namespace
class ExprParserTest : public testing::Test {
public:
ExprParserTest() = default;
// Valid after Parse() is called.
ExprParser& parser() { return *parser_; }
fxl::RefPtr<ExprNode> Parse(
const char* input,
NameLookupCallback name_lookup = NameLookupCallback()) {
parser_.reset();
tokenizer_ = std::make_unique<ExprTokenizer>(input);
if (!tokenizer_->Tokenize()) {
ADD_FAILURE() << "Tokenization failure: " << input;
return nullptr;
}
parser_ =
std::make_unique<ExprParser>(tokenizer_->TakeTokens(), name_lookup);
return parser_->Parse();
}
// Does the parse and returns the string dump of the structure.
std::string GetParseString(const char* input, NameLookupCallback name_lookup =
NameLookupCallback()) {
auto root = Parse(input, name_lookup);
if (!root) {
// Expect calls to this to parse successfully.
if (parser_.get())
ADD_FAILURE() << "Parse failure: " << parser_->err().msg();
return std::string();
}
std::ostringstream out;
root->Print(out, 0);
return out.str();
}
private:
std::unique_ptr<ExprTokenizer> tokenizer_;
std::unique_ptr<ExprParser> parser_;
};
TEST_F(ExprParserTest, Empty) {
auto result = Parse("");
ASSERT_FALSE(result);
EXPECT_EQ("No input to parse.", parser().err().msg());
}
TEST_F(ExprParserTest, Identifier) {
auto result = Parse("name");
ASSERT_TRUE(result);
const IdentifierExprNode* ident = result->AsIdentifier();
ASSERT_TRUE(ident);
EXPECT_EQ("name", ident->ident().GetFullName());
}
TEST_F(ExprParserTest, Dot) {
auto result = Parse("base.member");
ASSERT_TRUE(result);
const MemberAccessExprNode* access = result->AsMemberAccess();
ASSERT_TRUE(access);
EXPECT_EQ(ExprTokenType::kDot, access->accessor().type());
EXPECT_EQ(".", access->accessor().value());
// Left side is the "base" identifier.
const IdentifierExprNode* base = access->left()->AsIdentifier();
ASSERT_TRUE(base);
EXPECT_EQ("base", base->ident().GetFullName());
// Member name.
EXPECT_EQ("member", access->member().GetFullName());
}
TEST_F(ExprParserTest, AccessorAtEnd) {
auto result = Parse("base. ");
ASSERT_FALSE(result);
EXPECT_EQ("Expected identifier for right-hand-side of \".\".",
parser().err().msg());
EXPECT_EQ(4u, parser().error_token().byte_offset());
EXPECT_EQ(".", parser().error_token().value());
}
TEST_F(ExprParserTest, BadAccessorMemberName) {
auto result = Parse("base->23");
ASSERT_FALSE(result);
EXPECT_EQ("Expected identifier for right-hand-side of \"->\".",
parser().err().msg());
// This error reports the "->" as the location, one could also imagine
// reporting the right-side token (if any) instead.
EXPECT_EQ(4u, parser().error_token().byte_offset());
EXPECT_EQ("->", parser().error_token().value());
}
TEST_F(ExprParserTest, Arrow) {
auto result = Parse("base->member");
ASSERT_TRUE(result);
const MemberAccessExprNode* access = result->AsMemberAccess();
ASSERT_TRUE(access);
EXPECT_EQ(ExprTokenType::kArrow, access->accessor().type());
EXPECT_EQ("->", access->accessor().value());
// Left side is the "base" identifier.
const IdentifierExprNode* base = access->left()->AsIdentifier();
ASSERT_TRUE(base);
EXPECT_EQ("base", base->ident().GetFullName());
// Member name.
EXPECT_EQ("member", access->member().GetFullName());
// Arrow with no name.
result = Parse("base->");
ASSERT_FALSE(result);
EXPECT_EQ("Expected identifier for right-hand-side of \"->\".",
parser().err().msg());
}
TEST_F(ExprParserTest, NestedDotArrow) {
// These should be left-associative so do the "." first, then the "->".
// When evaluating the tree, one first evaluates the left side of the
// accessor, then the right, which is why it looks backwards in the dump.
EXPECT_EQ(
"ACCESSOR(->)\n"
" ACCESSOR(.)\n"
" IDENTIFIER(\"foo\")\n"
" bar\n"
" baz\n",
GetParseString("foo.bar->baz"));
}
TEST_F(ExprParserTest, UnexpectedInput) {
auto result = Parse("foo 5");
ASSERT_FALSE(result);
EXPECT_EQ("Unexpected input, did you forget an operator?",
parser().err().msg());
EXPECT_EQ(4u, parser().error_token().byte_offset());
}
TEST_F(ExprParserTest, ArrayAccess) {
EXPECT_EQ(
"ARRAY_ACCESS\n"
" ARRAY_ACCESS\n"
" ACCESSOR(->)\n"
" ACCESSOR(.)\n"
" IDENTIFIER(\"foo\")\n"
" bar\n"
" baz\n"
" LITERAL(34)\n"
" IDENTIFIER(\"bar\")\n",
GetParseString("foo.bar->baz[34][bar]"));
// Empty array access is an error.
auto result = Parse("foo[]");
ASSERT_FALSE(result);
EXPECT_EQ("Unexpected token ']'.", parser().err().msg());
}
TEST_F(ExprParserTest, DereferenceAndAddress) {
EXPECT_EQ(
"DEREFERENCE\n"
" IDENTIFIER(\"foo\")\n",
GetParseString("*foo"));
EXPECT_EQ(
"ADDRESS_OF\n"
" IDENTIFIER(\"foo\")\n",
GetParseString("&foo"));
// "*" and "&" should be right-associative with respect to each other but
// lower precedence than -> and [].
EXPECT_EQ(
"ADDRESS_OF\n"
" DEREFERENCE\n"
" ADDRESS_OF\n"
" ARRAY_ACCESS\n"
" ACCESSOR(->)\n"
" IDENTIFIER(\"foo\")\n"
" bar\n"
" LITERAL(1)\n",
GetParseString("&*&foo->bar[1]"));
// "*" by itself is an error.
auto result = Parse("*");
ASSERT_FALSE(result);
EXPECT_EQ("Expected expression for '*'.", parser().err().msg());
EXPECT_EQ(0u, parser().error_token().byte_offset());
}
// () should override the default precedence of other operators.
TEST_F(ExprParserTest, Parens) {
EXPECT_EQ(
"ADDRESS_OF\n"
" ARRAY_ACCESS\n"
" DEREFERENCE\n"
" ACCESSOR(->)\n"
" ADDRESS_OF\n"
" IDENTIFIER(\"foo\")\n"
" bar\n"
" LITERAL(1)\n",
GetParseString("(&(*(&foo)->bar)[1])"));
}
// This test covers that the extent of number tokens are identified properly.
// Actually converting the strings to integers should be covered by the number
// parser tests.
TEST_F(ExprParserTest, IntegerLiterals) {
EXPECT_EQ("LITERAL(5)\n", GetParseString("5"));
EXPECT_EQ("LITERAL(5ull)\n", GetParseString(" 5ull"));
EXPECT_EQ("LITERAL(0xAbC)\n", GetParseString("0xAbC"));
EXPECT_EQ("LITERAL(0o551)\n", GetParseString(" 0o551 "));
EXPECT_EQ("LITERAL(0123)\n", GetParseString("0123"));
}
TEST_F(ExprParserTest, UnaryMath) {
EXPECT_EQ(
"UNARY(-)\n"
" LITERAL(5)\n",
GetParseString("-5"));
EXPECT_EQ(
"UNARY(-)\n"
" LITERAL(5)\n",
GetParseString(" - 5 "));
EXPECT_EQ(
"UNARY(-)\n"
" DEREFERENCE\n"
" IDENTIFIER(\"foo\")\n",
GetParseString("-*foo"));
// "-" by itself is an error.
auto result = Parse("-");
ASSERT_FALSE(result);
EXPECT_EQ("Expected expression for '-'.", parser().err().msg());
EXPECT_EQ(0u, parser().error_token().byte_offset());
}
TEST_F(ExprParserTest, AndOr) {
EXPECT_EQ(
"BINARY_OP(&&)\n"
" BINARY_OP(&)\n"
" ADDRESS_OF\n"
" IDENTIFIER(\"a\")\n"
" ADDRESS_OF\n"
" IDENTIFIER(\"b\")\n"
" BINARY_OP(&)\n"
" IDENTIFIER(\"c\")\n"
" IDENTIFIER(\"d\")\n",
GetParseString("& a & & b && c & d"));
EXPECT_EQ(
"BINARY_OP(||)\n"
" BINARY_OP(|)\n"
" IDENTIFIER(\"a\")\n"
" IDENTIFIER(\"b\")\n"
" BINARY_OP(|)\n"
" IDENTIFIER(\"c\")\n"
" IDENTIFIER(\"d\")\n",
GetParseString("a | b || c | d"));
EXPECT_EQ(
"BINARY_OP(||)\n"
" BINARY_OP(&&)\n"
" IDENTIFIER(\"a\")\n"
" IDENTIFIER(\"b\")\n"
" BINARY_OP(&&)\n"
" IDENTIFIER(\"c\")\n"
" IDENTIFIER(\"d\")\n",
GetParseString("a && b || c && d"));
}
TEST_F(ExprParserTest, Identifiers) {
EXPECT_EQ("IDENTIFIER(\"foo\")\n", GetParseString("foo"));
EXPECT_EQ("IDENTIFIER(::\"foo\")\n", GetParseString("::foo"));
EXPECT_EQ("IDENTIFIER(::\"foo\"; ::\"bar\")\n",
GetParseString("::foo :: bar"));
auto result = Parse("::");
ASSERT_FALSE(result);
EXPECT_EQ("Expected name after '::'.", parser().err().msg());
result = Parse(":: :: name");
ASSERT_FALSE(result);
EXPECT_EQ(
"Could not identify thing to the left of '::' as a type or namespace.",
parser().err().msg());
result = Parse("foo bar");
ASSERT_FALSE(result);
EXPECT_EQ("Unexpected identifier, did you forget an operator?",
parser().err().msg());
// It's valid to have identifiers with colons in them to access class members
// (this is how you provide an explicit base class).
/* TODO(brettw) convert an accessor to use an Identifier for the name so this
test works.
EXPECT_EQ(
"ACCESSOR(->)\n"
" IDENTIFIER(\"foo\")\n",
GetParseString("foo->Baz::bar"));
*/
}
TEST_F(ExprParserTest, FunctionCall) {
// Simple call with no args.
EXPECT_EQ("FUNCTIONCALL(\"Call\")\n", GetParseString("Call()"));
// Scoped call with namespaces and templates.
EXPECT_EQ(R"(FUNCTIONCALL("ns"; ::"Foo",<"int">; ::"GetCurrent"))"
"\n",
GetParseString("ns::Foo<int>::GetCurrent()"));
// One arg.
EXPECT_EQ(
"FUNCTIONCALL(\"Call\")\n"
" LITERAL(42)\n",
GetParseString("Call(42)"));
// Several complex args and a nested call.
EXPECT_EQ(
"FUNCTIONCALL(\"Call\")\n"
" IDENTIFIER(\"a\")\n"
" BINARY_OP(=)\n"
" IDENTIFIER(\"b\")\n"
" LITERAL(5)\n"
" FUNCTIONCALL(\"OtherCall\")\n",
GetParseString("Call(a, b = 5, OtherCall())"));
// Unmatched "(" error.
auto result = Parse("Call(a, ");
ASSERT_FALSE(result);
EXPECT_EQ("Expected ')' to match. Hit the end of input instead.",
parser().err().msg());
// Arguments not separated by commas.
result = Parse("Call(a b)");
ASSERT_FALSE(result);
EXPECT_EQ("Unexpected identifier, did you forget an operator?",
parser().err().msg());
// Empty parameter
result = Parse("Call(a, , b)");
ASSERT_FALSE(result);
EXPECT_EQ("Unexpected token ','.", parser().err().msg());
// Trailing comma.
result = Parse("Call(a,)");
ASSERT_FALSE(result);
EXPECT_EQ("Unexpected token ')'.", parser().err().msg());
// Thing on the left is not an identifier.
result = Parse("5()");
ASSERT_FALSE(result);
EXPECT_EQ("Unexpected '('.", parser().err().msg());
}
TEST_F(ExprParserTest, Templates) {
EXPECT_EQ(R"(IDENTIFIER("foo",<>))"
"\n",
GetParseString("foo<>"));
EXPECT_EQ(R"(IDENTIFIER("foo",<"Foo">))"
"\n",
GetParseString("foo<Foo>"));
EXPECT_EQ(R"(IDENTIFIER("foo",<"Foo", "5">))"
"\n",
GetParseString("foo< Foo,5 >"));
EXPECT_EQ(
R"(IDENTIFIER("std"; ::"map",<"Key", "Value", "std::less<Key>", "std::allocator<std::pair<Key const, Value>>">; ::"insert"))"
"\n",
GetParseString("std::map<Key, Value, std::less<Key>, "
"std::allocator<std::pair<Key const, Value>>>::insert"));
// Unmatched "<" error. This is generated by the parser because it's
// expecting the match for the outer level.
auto result = Parse("std::map<Key, Value");
ASSERT_FALSE(result);
EXPECT_EQ("Expected '>' to match. Hit the end of input instead.",
parser().err().msg());
// This unmatched token is generated by the template type skipper which is
// why the error message is slightly different (both are OK).
result = Parse("std::map<key[, value");
ASSERT_FALSE(result);
EXPECT_EQ("Unmatched '['.", parser().err().msg());
// Duplicate template spec.
result = Parse("Foo<Bar><Baz>");
ASSERT_FALSE(result);
EXPECT_EQ("Comparisons not supported yet.", parser().err().msg());
// Empty value.
result = Parse("Foo<1,,2>");
ASSERT_FALSE(result);
EXPECT_EQ("Expected template parameter.", parser().err().msg());
// Trailing comma.
result = Parse("Foo<Bar,>");
ASSERT_FALSE(result);
EXPECT_EQ("Expected template parameter.", parser().err().msg());
}
TEST_F(ExprParserTest, BinaryOp) {
EXPECT_EQ(
"BINARY_OP(=)\n"
" IDENTIFIER(\"a\")\n"
" LITERAL(23)\n",
GetParseString("a = 23"));
}
// Tests parsing identifier names that require lookups from the symbol system.
// See TestLookupName above for the mocked symbol rules.
TEST_F(ExprParserTest, NamesWithSymbolLookup) {
// Bare namespace is an error.
auto result = Parse("Namespace", &TestLookupName);
ASSERT_FALSE(result);
EXPECT_EQ("Expected expression after namespace name.", parser().err().msg());
// Bare template is an error.
result = Parse("Template", &TestLookupName);
ASSERT_FALSE(result);
EXPECT_EQ("Expected template args after template name.",
parser().err().msg());
// Nothing after "::"
result = Parse("Namespace::", &TestLookupName);
ASSERT_FALSE(result);
EXPECT_EQ("Expected name after '::'.", parser().err().msg());
// Can't put a template on a type that's not a template.
result = Parse("Type<int>", &TestLookupName);
ASSERT_FALSE(result);
// This error message might change with future type support because it might
// look like a comparison between a type and an int.
EXPECT_EQ("Template parameters not valid on this object type.",
parser().err().msg());
// Can't put a template on a namespace.
result = Parse("Namespace<int>", &TestLookupName);
ASSERT_FALSE(result);
EXPECT_EQ("Template parameters not valid on this object type.",
parser().err().msg());
// Good type name.
EXPECT_EQ("FUNCTIONCALL(\"Namespace\"; ::\"Template\",<\"int\">; ::\"fn\")\n",
GetParseString("Namespace::Template<int>::fn()", &TestLookupName));
}
TEST_F(ExprParserTest, TrueFalse) {
EXPECT_EQ("LITERAL(true)\n", GetParseString("true"));
EXPECT_EQ("LITERAL(false)\n", GetParseString("false"));
EXPECT_EQ(
"BINARY_OP(&&)\n"
" LITERAL(false)\n"
" LITERAL(true)\n",
GetParseString("false&&true"));
}
TEST_F(ExprParserTest, Types) {
EXPECT_EQ("TYPE(Type)\n", GetParseString("Type", &TestLookupName));
EXPECT_EQ("IDENTIFIER(\"NotType\")\n",
GetParseString("NotType", &TestLookupName));
EXPECT_EQ("TYPE(const Type)\n",
GetParseString("const Type", &TestLookupName));
EXPECT_EQ("TYPE(const Type)\n",
GetParseString("Type const", &TestLookupName));
// It would be better it this printed as "const volatile Type" but our
// heuristic for moving modifiers to the beginning isn't good enough.
EXPECT_EQ("TYPE(volatile Type const)\n",
GetParseString("const volatile Type", &TestLookupName));
// Duplicate const qualifications.
auto result = Parse("const Type const", &TestLookupName);
ASSERT_FALSE(result);
EXPECT_EQ("Duplicate 'const' type qualification.", parser().err().msg());
result = Parse("const const Type", &TestLookupName);
ASSERT_FALSE(result);
EXPECT_EQ("Duplicate 'const' type qualification.", parser().err().msg());
EXPECT_EQ("TYPE(Type*)\n", GetParseString("Type*", &TestLookupName));
EXPECT_EQ("TYPE(Type**)\n", GetParseString("Type * *", &TestLookupName));
EXPECT_EQ("TYPE(Type&&)\n", GetParseString("Type &&", &TestLookupName));
EXPECT_EQ("TYPE(Type&**)\n", GetParseString("Type&**", &TestLookupName));
EXPECT_EQ("TYPE(volatile Type* restrict* const)\n",
GetParseString("Type volatile *restrict* const", &TestLookupName));
// "const" should force us into type mode.
result = Parse("const NonType", &TestLookupName);
ASSERT_FALSE(result);
EXPECT_EQ("Expected a type name but could not find a type named 'NonType'.",
parser().err().msg());
// Try sizeof() with both a type and a non-type.
EXPECT_EQ(
"SIZEOF\n"
" TYPE(Type*)\n",
GetParseString("sizeof(Type*)", &TestLookupName));
EXPECT_EQ(
"SIZEOF\n"
" IDENTIFIER(\"foo\")\n",
GetParseString("sizeof(foo)", &TestLookupName));
}
TEST_F(ExprParserTest, C_Cast) {
EXPECT_EQ(
"CAST(C)\n"
" TYPE(Type)\n"
" IDENTIFIER(\"a\")\n",
GetParseString("(Type)(a)", &TestLookupName));
EXPECT_EQ(
"BINARY_OP(&&)\n"
" CAST(C)\n"
" TYPE(Type*)\n"
" IDENTIFIER(\"a\")\n"
" IDENTIFIER(\"b\")\n",
GetParseString("(Type*)a && b", &TestLookupName));
EXPECT_EQ(
"CAST(C)\n"
" TYPE(Type)\n"
" ACCESSOR(->)\n"
" ARRAY_ACCESS\n"
" IDENTIFIER(\"a\")\n"
" LITERAL(0)\n"
" b\n",
GetParseString("(Type)a[0]->b", &TestLookupName));
// Looks like a cast but it's not a type.
auto result = Parse("(NotType)a", &TestLookupName);
EXPECT_FALSE(result);
EXPECT_EQ("Unexpected input, did you forget an operator?",
parser().err().msg());
}
TEST_F(ExprParserTest, CppCast) {
EXPECT_EQ(
"CAST(reinterpret_cast)\n"
" TYPE(Type*)\n"
" IDENTIFIER(\"a\")\n",
GetParseString("reinterpret_cast<Type*>(a)", &TestLookupName));
EXPECT_EQ(
"CAST(static_cast)\n"
" TYPE(Type*)\n"
" IDENTIFIER(\"a\")\n",
GetParseString("static_cast<Type*>(a)", &TestLookupName));
EXPECT_EQ(
"CAST(reinterpret_cast)\n"
" TYPE(const Type&&)\n"
" BINARY_OP(&&)\n"
" IDENTIFIER(\"x\")\n"
" IDENTIFIER(\"y\")\n",
GetParseString("reinterpret_cast< const Type&& >( x && y)",
&TestLookupName));
auto result = Parse("reinterpret_cast<", &TestLookupName);
EXPECT_FALSE(result);
EXPECT_EQ("Expected type name before end of input.", parser().err().msg());
}
TEST_F(ExprParserTest, ParseIdentifier) {
Err err;
// Empty input.
ParsedIdentifier empty_ident;
err = ExprParser::ParseIdentifier("", &empty_ident);
EXPECT_TRUE(err.has_error());
EXPECT_EQ("No input to parse.", err.msg());
EXPECT_EQ("", empty_ident.GetDebugName());
// Normal word.
ParsedIdentifier word_ident;
err = ExprParser::ParseIdentifier("foo", &word_ident);
EXPECT_FALSE(err.has_error()) << err.msg();
EXPECT_EQ("\"foo\"", word_ident.GetDebugName());
// Destructor.
ParsedIdentifier destr_ident;
err = ExprParser::ParseIdentifier("Foo::~Foo", &destr_ident);
EXPECT_FALSE(err.has_error()) << err.msg();
EXPECT_EQ(R"("Foo"; ::"~Foo")", destr_ident.GetDebugName());
// Complicated identifier (copied from STL).
ParsedIdentifier complex_ident;
err = ExprParser::ParseIdentifier(
"std::unordered_map<"
"std::__2::basic_string<char>, "
"unsigned long, "
"std::__2::hash<std::__2::basic_string<char> >, "
"std::__2::equal_to<std::__2::basic_string<char> >, "
"std::__2::allocator<std::__2::pair<const std::__2::basic_string<char>, "
"unsigned long> >>",
&complex_ident);
EXPECT_FALSE(err.has_error());
EXPECT_EQ(
"\"std\"; "
"::"
"\"unordered_map\","
"<\"std::__2::basic_string<char>\", "
"\"unsigned long\", "
"\"std::__2::hash<std::__2::basic_string<char>>\", "
"\"std::__2::equal_to<std::__2::basic_string<char>>\", "
"\"std::__2::allocator<std::__2::pair<"
"const std::__2::basic_string<char>, unsigned long>>\">",
complex_ident.GetDebugName());
}
TEST_F(ExprParserTest, FromStringError) {
// Error from input.
Identifier bad_ident;
Err err = ExprParser::ParseIdentifier("Foo<Bar", &bad_ident);
EXPECT_TRUE(err.has_error());
EXPECT_EQ("Expected '>' to match. Hit the end of input instead.", err.msg());
EXPECT_EQ("", bad_ident.GetDebugName());
}
// "PLT" breakpoints are breakpoints set on ELF imports rather than DWARF
// symbols. We need to be able to parse them as an identifier even though it's
// not a valid C++ name. This can be changed in the future if we have a better
// way of identifying these.
TEST_F(ExprParserTest, PltName) {
Identifier ident;
Err err = ExprParser::ParseIdentifier("__stack_chk_fail@plt", &ident);
EXPECT_FALSE(err.has_error());
EXPECT_EQ("\"__stack_chk_fail@plt\"", ident.GetDebugName());
}
} // namespace zxdb