blob: 6cdb216cd87c1ca12b51a0264950cc913f7d282e [file] [log] [blame]
// Copyright 2020 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.h"
#include <gtest/gtest.h>
#include "src/developer/debug/zxdb/common/test_with_loop.h"
#include "src/developer/debug/zxdb/expr/builtin_types.h"
#include "src/developer/debug/zxdb/expr/expr_parser.h"
#include "src/developer/debug/zxdb/expr/expr_tokenizer.h"
#include "src/developer/debug/zxdb/expr/mock_eval_context.h"
#include "src/developer/debug/zxdb/expr/vm_op.h"
#include "src/developer/debug/zxdb/expr/vm_stream.h"
#include "src/developer/debug/zxdb/symbols/modified_type.h"
#include "src/developer/debug/zxdb/symbols/type_test_support.h"
namespace zxdb {
namespace {
class ExprTest : public TestWithLoop {
public:
ErrOrValue Eval(const std::string& code, const fxl::RefPtr<EvalContext>& context) {
ErrOrValue result(Err("Uncalled"));
bool called = false;
EvalExpression(code, context, true, [&](ErrOrValue in_result) {
result = in_result;
called = true;
debug::MessageLoop::Current()->QuitNow();
});
if (!called)
loop().RunUntilNoTasks();
EXPECT_TRUE(called);
return result;
}
};
} // namespace
TEST_F(ExprTest, ValueToAddressAndSize) {
auto eval_context = fxl::MakeRefCounted<MockEvalContext>();
// Ints are OK but have no size.
uint64_t address = 0;
std::optional<uint32_t> size;
ASSERT_TRUE(ValueToAddressAndSize(eval_context, ExprValue(23), &address, &size).ok());
EXPECT_EQ(23u, address);
EXPECT_EQ(std::nullopt, size);
// Structure.
auto uint64_type = MakeUint64Type();
auto collection =
MakeCollectionType(DwarfTag::kStructureType, "Foo", {{"a", uint64_type}, {"b", uint64_type}});
std::vector<uint8_t> collection_data;
collection_data.resize(collection->byte_size());
// Currently evaluating a structure is expected to fail.
// TODO(bug 44074) support non-pointer values and take their address implicitly.
address = 0;
size = std::nullopt;
Err err = ValueToAddressAndSize(
eval_context, ExprValue(collection, collection_data, ExprValueSource(0x12345678)), &address,
&size);
ASSERT_TRUE(err.has_error());
EXPECT_EQ("Can't convert 'Foo' to an address.", err.msg());
EXPECT_EQ(0u, address);
EXPECT_EQ(std::nullopt, size);
// Pointer to a collection.
auto collection_ptr = fxl::MakeRefCounted<ModifiedType>(DwarfTag::kPointerType, collection);
std::vector<uint8_t> ptr_data{8, 7, 6, 5, 4, 3, 2, 1};
address = 0;
size = std::nullopt;
err = ValueToAddressAndSize(eval_context, ExprValue(collection_ptr, ptr_data), &address, &size);
ASSERT_TRUE(err.ok());
EXPECT_EQ(0x0102030405060708u, address);
ASSERT_TRUE(size);
EXPECT_EQ(collection->byte_size(), *size);
}
TEST_F(ExprTest, CConditions) {
auto eval_context = fxl::MakeRefCounted<MockEvalContext>();
// If true condition executed.
auto result = Eval("if (5 > 0) { 6; } else { 7; }", eval_context);
EXPECT_TRUE(result.ok());
int64_t result_value = 0;
ASSERT_TRUE(result.value().PromoteTo64(&result_value).ok());
EXPECT_EQ(6, result_value);
// Else condition executed.
result = Eval("if (5 < 0) { 6; } else { 7; }", eval_context);
EXPECT_TRUE(result.ok());
result_value = 0;
ASSERT_TRUE(result.value().PromoteTo64(&result_value).ok());
EXPECT_EQ(7, result_value);
// Cascading if/else, execute the middle condition.
result = Eval("if (5 < 0) { 6; } else if (0 < 5) { 99; } else { 7; }", eval_context);
EXPECT_TRUE(result.ok());
result_value = 0;
ASSERT_TRUE(result.value().PromoteTo64(&result_value).ok());
EXPECT_EQ(99, result_value);
}
TEST_F(ExprTest, RustConditions) {
auto eval_context = fxl::MakeRefCounted<MockEvalContext>();
eval_context->set_language(ExprLanguage::kRust);
// If true condition executed.
auto result = Eval("if 5 > 0 { 6 } else { 7 }", eval_context);
EXPECT_TRUE(result.ok());
int64_t result_value = 0;
ASSERT_TRUE(result.value().PromoteTo64(&result_value).ok());
EXPECT_EQ(6, result_value);
// Else condition executed.
result = Eval("if 5 < 0 { 6 } else { 7 }", eval_context);
EXPECT_TRUE(result.ok());
result_value = 0;
ASSERT_TRUE(result.value().PromoteTo64(&result_value).ok());
EXPECT_EQ(7, result_value);
// Cascading if/else, execute the middle condition.
result = Eval("if 5 < 0 { 6 } else if 0 < 5 { 99 } else { 7 }", eval_context);
EXPECT_TRUE(result.ok());
result_value = 0;
ASSERT_TRUE(result.value().PromoteTo64(&result_value).ok());
EXPECT_EQ(99, result_value);
}
// Tests short-circuiting behavior of the || and && operators.
//
// This test takes advantage of our lazy evaluation where we don't do name lookups until the code
// actually executes. We can therefore tell if the condition was executed by whether it encountered
// a name lookup error or not.
TEST_F(ExprTest, LogicalOrShortCircuit) {
auto eval_context = fxl::MakeRefCounted<MockEvalContext>();
ExprValue true_value(true);
ExprValue false_value(false);
auto result = Eval("1 || nonexistant", eval_context);
EXPECT_TRUE(result.ok());
EXPECT_EQ(true_value, result.value());
result = Eval("0 || nonexistant", eval_context);
EXPECT_TRUE(result.has_error());
EXPECT_EQ("MockEvalContext::GetNamedValue 'nonexistant' not found.", result.err().msg());
result = Eval("0 || 1", eval_context);
EXPECT_TRUE(result.ok());
EXPECT_EQ(true_value, result.value());
result = Eval("0 || 0", eval_context);
EXPECT_TRUE(result.ok());
EXPECT_EQ(false_value, result.value());
// Check that condition in a real "if" statement.
result = Eval("if (1 || nonexistant) { 5; } else { 6; }", eval_context);
EXPECT_TRUE(result.ok());
int64_t result_value = 0;
ASSERT_TRUE(result.value().PromoteTo64(&result_value).ok());
EXPECT_EQ(5, result_value);
}
// See "LogicalOrShortCircuit" above.
TEST_F(ExprTest, LogicalAndShortCircuit) {
auto eval_context = fxl::MakeRefCounted<MockEvalContext>();
ExprValue true_value(true);
ExprValue false_value(false);
auto result = Eval("0 && nonexistant", eval_context);
EXPECT_TRUE(result.ok());
EXPECT_EQ(false_value, result.value());
result = Eval("1 && nonexistant", eval_context);
EXPECT_TRUE(result.has_error());
EXPECT_EQ("MockEvalContext::GetNamedValue 'nonexistant' not found.", result.err().msg());
result = Eval("1 && 99", eval_context);
EXPECT_TRUE(result.ok());
EXPECT_EQ(true_value, result.value());
result = Eval("1 && 0", eval_context);
EXPECT_TRUE(result.ok());
EXPECT_EQ(false_value, result.value());
// Check that condition in a real "if" statement.
result = Eval("if (0 && nonexistant) { 5; } else { 6; }", eval_context);
EXPECT_TRUE(result.ok());
int64_t result_value = 0;
ASSERT_TRUE(result.value().PromoteTo64(&result_value).ok());
EXPECT_EQ(6, result_value);
}
TEST_F(ExprTest, CLocalVars) {
const char kCode[] = R"(
{
int source = 45;
auto sum(source - 3);
sum = sum * 2;
sum; // The result of the program (since everything is an expression).
}
)";
bool called = false;
EvalExpression(kCode, fxl::MakeRefCounted<MockEvalContext>(), false, [&](ErrOrValue result) {
called = true;
ASSERT_TRUE(result.ok()) << result.err().msg();
// (45 - 3) * 2 = 84
// The expression system likes to promote internally to C-style int64 to avoid overflows.
EXPECT_EQ(result.value(),
ExprValue(static_cast<int64_t>(84), GetBuiltinType(ExprLanguage::kC, "int64_t")));
});
EXPECT_TRUE(called);
}
TEST_F(ExprTest, RustLocalVars) {
const char kCode[] = R"(
{
let source:i32;
source = 45;
let sum = source - 3;
sum = sum * 2;
sum; // The result of the program (since everything is an expression).
}
)";
auto eval_context = fxl::MakeRefCounted<MockEvalContext>();
eval_context->set_language(ExprLanguage::kRust);
bool called = false;
EvalExpression(kCode, eval_context, false, [&](ErrOrValue result) {
called = true;
ASSERT_TRUE(result.ok()) << result.err().msg();
// (45 - 3) * 2 = 84
// The expression system likes to promote to int64 to avoid overflows (in contrast to C).
EXPECT_EQ(result.value(),
ExprValue(static_cast<int64_t>(84), GetBuiltinType(ExprLanguage::kC, "int64_t")));
});
EXPECT_TRUE(called);
}
TEST_F(ExprTest, CForLoop) {
const char kCode[] = R"(
{
int sum = 0;
for (int i = 0; i < 10; i = i + 1) {
sum = sum + i;
}
sum; // The result of the program (since everything is an expression).
}
)";
bool called = false;
EvalExpression(kCode, fxl::MakeRefCounted<MockEvalContext>(), false, [&](ErrOrValue result) {
called = true;
ASSERT_TRUE(result.ok()) << result.err().msg();
// 0+1+2+3+4+5+6+7+8+9 = 45
EXPECT_EQ(result.value(), ExprValue(45, GetBuiltinType(ExprLanguage::kC, "int")));
});
EXPECT_TRUE(called);
// Check the bytecode. This should be relatively stable.
ExprTokenizer tokenizer(kCode, ExprLanguage::kC);
ASSERT_TRUE(tokenizer.Tokenize());
ExprParser parser(tokenizer.TakeTokens(), tokenizer.language(),
fxl::MakeRefCounted<MockEvalContext>());
auto node = parser.ParseStandaloneExpression();
ASSERT_TRUE(node);
VmStream stream;
node->EmitBytecode(stream);
// clang-format off
EXPECT_EQ(
// "int sum = 0"
"0: Literal(int(0))\n" // Literal for "sum" initialization.
"1: AsyncCallback1()\n" // Cast to "int" (strictly unnecessary here).
"2: Dup()\n" // Make a copy to save as the local.
"3: SetLocal(0)\n" // Save the 0 to local var slot 0 (the "sum" variable").
"4: Drop()\n" // Discard the result of the declaration.
// Set up break destination.
"5: PushBreak(34)\n" // "break" ops jump to the given address with the stack restored.
// "int i = 0" (same as the above except for "i" in slot 1).
"6: Literal(int(0))\n"
"7: AsyncCallback1()\n"
"8: Dup()\n"
"9: SetLocal(1)\n"
"10: Drop()\n"
// "i < 10"
"11: GetLocal(1)\n" // Get "i".
"12: ExpandRef()\n" // Make sure "i" isn't a reference (derefs the addr to its value).
"13: Literal(int(10))\n" // "10"
"14: Binary(<)\n"
"15: JumpIfFalse(33)\n" // End of loop is the given address.
// "sum = sum + i"
"16: GetLocal(0)\n" // "sum" (for the left-side of the assignment).
"17: ExpandRef()\n"
"18: GetLocal(0)\n" // "sum" (for adding to "i").
"19: ExpandRef()\n"
"20: GetLocal(1)\n" // "i"
"21: Binary(+)\n"
"22: Binary(=)\n"
"23: Drop()\n" // Discard the result of the assignment expression.
// "i = i + 1"
"24: GetLocal(1)\n" // "i" (for left side of assignment).
"25: ExpandRef()\n"
"26: GetLocal(1)\n" // "i" (for adding to 1).
"27: ExpandRef()\n"
"28: Literal(int(1))\n" // "1"
"29: Binary(+)\n"
"30: Binary(=)\n"
"31: Drop()\n" // Discard the result of the increment expression.
// Loop back to the precondition on the given line.
"32: Jump(11)\n"
// Loop end cleanup.
"33: PopLocals(1)\n" // Discard the "i" local variable, now only one ("sum") in scope.
"34: PopBreak()\n" // Restore previous break destination.
"35: Literal({null ExprValue})\n" // Result of loop expression (nothing).
"36: Drop()\n" // Discard the result of the loop expression.
// "sum"
"37: GetLocal(0)\n"
// Clean up outer block state.
"38: PopLocals(0)\n", // Discard "sum" variable.
VmStreamToString(stream));
// clang-format on
// Try a loop with a break statement.
const char kCodeWithBreak[] = R"(
{
int sum = 0;
for (int i = 0; i < 10; i = i + 1) {
sum = sum + i;
if (i == 3)
break;
}
sum; // The result of the program (since everything is an expression).
}
)";
called = false;
EvalExpression(kCodeWithBreak, fxl::MakeRefCounted<MockEvalContext>(), false,
[&](ErrOrValue result) {
called = true;
ASSERT_TRUE(result.ok()) << result.err().msg();
// 0+1+2+3 = 6
EXPECT_EQ(result.value(), ExprValue(6, GetBuiltinType(ExprLanguage::kC, "int")));
});
EXPECT_TRUE(called);
}
TEST_F(ExprTest, RustWhileLoop) {
// This program computes the next power of 2 greater than 3000.
const char kCode[] = R"(
{
let sum: i32 = 1;
while sum < 3000 {
sum = sum * 2
}
sum
}
)";
auto eval_context = fxl::MakeRefCounted<MockEvalContext>();
eval_context->set_language(ExprLanguage::kRust);
bool called = false;
EvalExpression(kCode, eval_context, false, [&](ErrOrValue result) {
called = true;
ASSERT_TRUE(result.ok()) << result.err().msg();
EXPECT_EQ(result.value(), ExprValue(4096, GetBuiltinType(ExprLanguage::kRust, "i32")));
});
EXPECT_TRUE(called);
}
TEST_F(ExprTest, RustIfLet) {
auto int32_type = GetBuiltinType(ExprLanguage::kRust, "i32");
auto enum_type = MakeRustEnum(
"Option", {{"None", {}}, {"Some", {int32_type}}, {"Many", {int32_type, int32_type}}});
ASSERT_EQ(16u, enum_type->byte_size());
const char kEnumName[] = "my_enum"; // Injected variable with Rust enum type.
const char kCode[] = R"(
let result: i32 = 0;
if let Some(a) = my_enum {
result = a;
} else if let None = my_enum {
result = 9999;
} else if let Many(x, y) = my_enum {
result = x * 100 + y;
}
result
)";
ExprValue enum_none_value(
enum_type,
{0, 0, 0, 0, 0, 0, 0, 0, // Discriminant in the first 8 bytes, 0 = None.
0, 0, 0, 0, 0, 0, 0, 0}); // Padding for the 2x 32 bit value only used for "Some".
// Inject the enum value as a variable in the context.
auto eval_context = fxl::MakeRefCounted<MockEvalContext>();
eval_context->set_language(ExprLanguage::kRust);
eval_context->AddVariable(kEnumName, enum_none_value);
bool called = false;
EvalExpression(kCode, eval_context, false, [&](ErrOrValue result) {
called = true;
ASSERT_TRUE(result.ok()) << result.err().msg();
EXPECT_EQ(result.value(), ExprValue(9999, int32_type));
});
EXPECT_TRUE(called);
// Now try with a "some" enum.
ExprValue enum_some_value(
enum_type, {1, 0, 0, 0, 0, 0, 0, 0, // Discriminant in the first 8 bytes, 1 = Some.
42, 0, 0, 0, 0, 0, 0, 0}); // Value = 42 + padding.
eval_context->AddVariable(kEnumName, enum_some_value);
called = false;
EvalExpression(kCode, eval_context, false, [&](ErrOrValue result) {
called = true;
ASSERT_TRUE(result.ok()) << result.err().msg();
EXPECT_EQ(result.value(), ExprValue(42, int32_type));
});
EXPECT_TRUE(called);
// Try with a "many" enum.
ExprValue enum_many_value(
enum_type, {2, 0, 0, 0, 0, 0, 0, 0, // Discriminant in the first 8 bytes, 2 = Many.
76, 0, 0, 0, 19, 0, 0, 0}); // Value = (76, 19)
eval_context->AddVariable(kEnumName, enum_many_value);
called = false;
EvalExpression(kCode, eval_context, false, [&](ErrOrValue result) {
called = true;
ASSERT_TRUE(result.ok()) << result.err().msg();
EXPECT_EQ(result.value(), ExprValue(7619, int32_type));
});
EXPECT_TRUE(called);
// Mismatched tuple member count (using the "Many" tuple value set in previous test).
called = false;
EvalExpression("if let Many(a) = my_enum {}", eval_context, false, [&](ErrOrValue result) {
called = true;
ASSERT_TRUE(result.has_error());
EXPECT_EQ("Tuple pattern contains 1 members but the matched tuple has 2.", result.err().msg());
});
EXPECT_TRUE(called);
}
TEST_F(ExprTest, BuiltinFunctionCall) {
const char kCode[] = "1 + MyFunction(2, 3 * 4)";
auto eval_context = fxl::MakeRefCounted<MockEvalContext>();
ParsedIdentifier ident(ParsedIdentifierComponent("MyFunction"));
eval_context->AddBuiltinFunction(
ident, [](const fxl::RefPtr<EvalContext>& eval_context, const std::vector<ExprValue>& params,
EvalCallback cb) {
// Validate we got the expected parameters,
ASSERT_EQ(2u, params.size());
int64_t value = 0;
ASSERT_TRUE(params[0].PromoteTo64(&value).ok());
EXPECT_EQ(2, value);
ASSERT_TRUE(params[1].PromoteTo64(&value).ok());
EXPECT_EQ(12, value);
// This is the return value.
cb(ExprValue(999));
});
bool called = false;
EvalExpression(kCode, eval_context, false, [&](ErrOrValue result) {
called = true;
ASSERT_TRUE(result.ok()) << result.err().msg();
int64_t value = 0;
ASSERT_TRUE(result.value().PromoteTo64(&value).ok());
EXPECT_EQ(1000, value);
});
EXPECT_TRUE(called);
}
TEST_F(ExprTest, VoidPointerArithmetic) {
const char kCode[] = "rip - 0x20";
auto eval_context = fxl::MakeRefCounted<MockEvalContext>();
eval_context->set_language(ExprLanguage::kC);
eval_context->data_provider()->set_ip(0x1000);
eval_context->data_provider()->set_arch(debug::Arch::kX64);
bool called = false;
EvalExpression(kCode, eval_context, false, [&](ErrOrValue result) {
called = true;
ASSERT_TRUE(result.ok()) << result.err().msg();
uint64_t value = 0;
ASSERT_TRUE(result.value().PromoteTo64(&value).ok());
EXPECT_EQ(0xfe0ull, value);
});
EXPECT_TRUE(called);
}
} // namespace zxdb