| // 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); | |
| } | |
| TEST_F(ExprTest, RustEnumMemberAccess) { | |
| auto eval_context = fxl::MakeRefCounted<MockEvalContext>(); | |
| eval_context->set_language(ExprLanguage::kRust); | |
| auto enum_type = MakeTestRustEnum(); | |
| // Point{x:12, y:13}: two 32-bit values following the 32-bit discriminant of 1. | |
| ExprValue point_value(enum_type, {1, 0, 0, 0, 12, 0, 0, 0, 13, 0, 0, 0}); | |
| eval_context->AddVariable("e", point_value); | |
| auto result = Eval("e.x", eval_context); | |
| ASSERT_TRUE(result.ok()) << result.err().msg(); | |
| int64_t value = 0; | |
| ASSERT_TRUE(result.value().PromoteTo64(&value).ok()); | |
| EXPECT_EQ(value, 12); | |
| // Scalar(123): a single 32-bit value following the 32-bit discriminant of 0. | |
| ExprValue scalar_value(enum_type, {0, 0, 0, 0, 123, 0, 0, 0, 0, 0, 0, 0}); | |
| eval_context->AddVariable("e", scalar_value); | |
| result = Eval("e.0", eval_context); | |
| ASSERT_TRUE(result.ok()) << result.err().msg(); | |
| ASSERT_TRUE(result.value().PromoteTo64(&value).ok()); | |
| EXPECT_EQ(value, 123); | |
| } | |
| } // namespace zxdb |