| // 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/eval_operators.h" |
| |
| #include <lib/syslog/cpp/macros.h> |
| |
| #include <type_traits> |
| |
| #include "src/developer/debug/shared/register_info.h" |
| #include "src/developer/debug/zxdb/common/err.h" |
| #include "src/developer/debug/zxdb/expr/bitfield.h" |
| #include "src/developer/debug/zxdb/expr/cast.h" |
| #include "src/developer/debug/zxdb/expr/eval_context.h" |
| #include "src/developer/debug/zxdb/expr/expr_node.h" |
| #include "src/developer/debug/zxdb/expr/expr_token.h" |
| #include "src/developer/debug/zxdb/expr/expr_value.h" |
| #include "src/developer/debug/zxdb/expr/resolve_array.h" |
| #include "src/developer/debug/zxdb/expr/resolve_ptr_ref.h" |
| #include "src/developer/debug/zxdb/symbols/base_type.h" |
| #include "src/developer/debug/zxdb/symbols/modified_type.h" |
| #include "src/developer/debug/zxdb/symbols/symbol_data_provider.h" |
| |
| // About math handling |
| // ------------------- |
| // |
| // C++ applies "integer promotion" to doing arithmetic operations. This is a set of rules for |
| // promoting the parameters to larger types. See: |
| // https://en.cppreference.com/w/cpp/language/operator_arithmetic#Conversions |
| // |
| // When evaluating expressions in a debugger, the user expects more calculator-like behavior and |
| // cares less about specific types and truncation rules. As an example, in C++ multiplying two |
| // integers will yield an integer type that may overflow. But in a debugger expression truncating |
| // an overflowing value is extremely undesirable. |
| // |
| // As a result we upcast all integer operations to 64-bit. This is in contrast to C++ which often |
| // prefers "int" which are often 32 bits. |
| // |
| // We still more-or-less follow the signed/unsigned rules since sometimes those behaviors are |
| // important to the result being computed. Effectively, this means using the larger of the two types |
| // if the type sizes differ, and converting to unsigned if the sizes but sign-edness of the types |
| // differ. |
| |
| namespace zxdb { |
| |
| namespace { |
| |
| using debug::RegisterCategory; |
| using debug::RegisterID; |
| using debug::RegisterInfo; |
| |
| // Backend for register assignment that takes the known current value of the destination register |
| // as well as the new value (possibly in a subrange) and updates the value. This is updated |
| // according to the used bits and shift amount. |
| void AssignRegisterWithExistingValue(const fxl::RefPtr<EvalContext>& context, |
| const ExprValueSource& dest, |
| std::vector<uint8_t> existing_data, const RegisterInfo& info, |
| const ExprValue& source, |
| SymbolDataProvider::WriteCallback cb) { |
| // Here we want to support vector registers so can't always bring the result into a numeric |
| // variable. These large values are always multiples of bytes (not random bit ranges within |
| // bytes). Sometimes bitfields with arbitrary ranges can be brought into registers, but this will |
| // always be normal smaller ones that can be used with numbers. |
| // |
| // These computations assume little-endian. |
| if (dest.bit_shift() % 8 == 0 && dest.bit_size() % 8 == 0) { |
| // Easy case of everything being byte-aligned. This can handle all vector registers. |
| |
| // We expect all non-canonical registers to be byte-aligned inside their canonical one. |
| FX_DCHECK(info.bits % 8 == 0); |
| FX_DCHECK(info.shift % 8 == 0); |
| |
| // In little-endian, the byte shift (from the low bit) just measures from the [0] byte. |
| // |
| // Do these computations in signed numbers because weird symbol data could give |
| // data.size() - offset => negative number. |
| int byte_shift = static_cast<int>((dest.bit_shift() + info.shift) / 8); |
| int byte_length = std::min(static_cast<int>(dest.bit_size()), info.bits) / 8; |
| |
| // Clamp the range to within the buffer in case anything is corrupted. |
| byte_length = std::min(byte_length, std::max(0, static_cast<int>(existing_data.size()) - |
| byte_shift - byte_length)); |
| |
| if (byte_length > 0) { |
| memcpy(&existing_data[byte_shift], &source.data().bytes()[0], byte_length); |
| context->GetDataProvider()->WriteRegister(info.canonical_id, std::move(existing_data), |
| std::move(cb)); |
| } else { |
| // Nothing to write, the symbol shifts seem messed up. |
| cb(Err("Could not write register data of %d bytes at offset %d bytes.", byte_length, |
| byte_shift)); |
| } |
| } else if (existing_data.size() < sizeof(uint128_t) && source.data().size() < sizeof(uint128_t)) { |
| // Have non-byte-sized shifts, the source is probably a bitfield. This assumes little-endian. |
| uint128_t existing_value = 0; |
| memcpy(&existing_value, existing_data.data(), existing_data.size()); |
| |
| uint128_t write_value = 0; |
| memcpy(&write_value, &source.data().bytes()[0], source.data().size()); |
| |
| // This ExprValueSource takes into account any non-canonical register shifts on top of what |
| // may already be there. |
| ExprValueSource new_dest(info.canonical_id, |
| std::max(dest.bit_size(), static_cast<uint32_t>(info.bits)), |
| dest.bit_shift() + info.shift); |
| |
| uint128_t new_value = new_dest.SetBits(existing_value, write_value); |
| memcpy(existing_data.data(), &new_value, existing_data.size()); |
| |
| context->GetDataProvider()->WriteRegister(info.canonical_id, std::move(existing_data), |
| std::move(cb)); |
| } else { |
| cb(Err("Can't write bitfield of size %zu to register of size %zu.", source.data().size(), |
| existing_data.size())); |
| } |
| } |
| |
| void DoRegisterAssignment(const fxl::RefPtr<EvalContext>& context, const ExprValueSource& dest, |
| const ExprValue& source, EvalCallback cb) { |
| const RegisterInfo* info = debug::InfoForRegister(dest.register_id()); |
| if (!info) |
| return cb(Err("Assignment to invalid register %u.", dest.register_id())); |
| |
| // Transforms a register write callback (Err only) to a EvalCallback (ErrOr<ExprValue>). |
| SymbolDataProvider::WriteCallback write_cb = [source, |
| cb = std::move(cb)](const Err& err) mutable { |
| if (err.has_error()) |
| cb(err); |
| else |
| cb(source); |
| }; |
| |
| if (info->canonical_id == dest.register_id() && !dest.is_bitfield()) { |
| // Normal register write with no masking or shifting. |
| context->GetDataProvider()->WriteRegister(dest.register_id(), source.data().bytes(), |
| std::move(write_cb)); |
| } else { |
| // This write requires some masking and shifting, and therefore needs the current register |
| // value. |
| context->GetDataProvider()->GetRegisterAsync( |
| info->canonical_id, [context, source, dest, info = *info, write_cb = std::move(write_cb)]( |
| const Err& err, std::vector<uint8_t> data) mutable { |
| if (err.has_error()) { |
| write_cb(err); |
| } else { |
| AssignRegisterWithExistingValue(context, dest, std::move(data), info, source, |
| std::move(write_cb)); |
| } |
| }); |
| } |
| } |
| |
| void DoMemoryAssignment(const fxl::RefPtr<EvalContext>& context, const ExprValueSource& dest, |
| const ExprValue& source, EvalCallback cb) { |
| // Update the memory with the new data. The result of the expression is the coerced value. |
| auto write_callback = [source, cb = std::move(cb)](const Err& err) mutable { |
| if (err.has_error()) |
| cb(err); |
| else |
| cb(source); |
| }; |
| if (dest.is_bitfield()) { |
| WriteBitfieldToMemory(context, dest, source.data().bytes(), std::move(write_callback)); |
| } else { |
| // Normal case for non-bitfields. |
| context->GetDataProvider()->WriteMemory(dest.address(), source.data().bytes(), |
| std::move(write_callback)); |
| } |
| } |
| |
| void DoAssignment(const fxl::RefPtr<EvalContext>& context, const ExprValue& left_value, |
| const ExprValue& right_value, EvalCallback cb) { |
| if (left_value.data().size() == 0) |
| return cb(Err("Can't assign 0-size value.")); |
| |
| // Note: the calling code will have evaluated the value of the left node. Often this isn't |
| // strictly necessary: we only need the "source", but optimizing in that way would complicate |
| // things. |
| const ExprValueSource& dest = left_value.source(); |
| if (dest.type() == ExprValueSource::Type::kTemporary) |
| return cb(Err("Can't assign to a temporary.")); |
| if (dest.type() == ExprValueSource::Type::kConstant) |
| return cb(Err("Can't assign to a constant.")); |
| if (dest.type() == ExprValueSource::Type::kComposite) { |
| // TODO(bug 39630) implement composite variable locations. |
| return cb(Err("Can't assign to a composite variable location (see bug 39630).")); |
| } |
| |
| // The coerced value will be the result. It should have the "source" of the left-hand-side since |
| // the location being assigned to doesn't change. |
| CastExprValue(context, CastType::kImplicit, right_value, left_value.type_ref(), ExprValueSource(), |
| [context, dest, cb = std::move(cb)](ErrOrValue coerced) mutable { |
| if (coerced.has_error()) |
| return cb(coerced); |
| |
| if (dest.type() == ExprValueSource::Type::kRegister) { |
| DoRegisterAssignment(context, dest, coerced.value(), std::move(cb)); |
| } else { |
| DoMemoryAssignment(context, dest, coerced.value(), std::move(cb)); |
| } |
| }); |
| } |
| |
| // This is used as the return type for comparison operations. |
| fxl::RefPtr<BaseType> MakeBoolType() { |
| return fxl::MakeRefCounted<BaseType>(BaseType::kBaseTypeBoolean, 1, "bool"); |
| } |
| |
| bool IsComparison(ExprTokenType op) { |
| return op == ExprTokenType::kEquality || op == ExprTokenType::kInequality || |
| op == ExprTokenType::kLessEqual || op == ExprTokenType::kGreaterEqual || |
| op == ExprTokenType::kSpaceship || op == ExprTokenType::kLess || |
| op == ExprTokenType::kGreater; |
| } |
| |
| // The "math realm" is the type of operation being done, since operators in these different spaces |
| // have very different behaviors. |
| enum class MathRealm { kSigned, kUnsigned, kFloat, kPointer }; |
| |
| bool IsIntegerRealm(MathRealm realm) { |
| return realm == MathRealm::kSigned || realm == MathRealm::kUnsigned; |
| } |
| |
| // Computes how math should be done on the given type. The type should be concrete. |
| Err GetRealm(const Type* type, MathRealm* realm) { |
| // Check for pointers. |
| if (const ModifiedType* mod = type->As<ModifiedType>()) { |
| if (mod->tag() == DwarfTag::kPointerType) { |
| *realm = MathRealm::kPointer; |
| return Err(); |
| } |
| } else if (const BaseType* base = type->As<BaseType>()) { |
| // Everything else should be a base type. |
| switch (base->base_type()) { |
| case BaseType::kBaseTypeNone: |
| break; // Error, fall through to bottom of function. |
| |
| case BaseType::kBaseTypeAddress: |
| *realm = MathRealm::kPointer; |
| return Err(); |
| |
| case BaseType::kBaseTypeFloat: |
| *realm = MathRealm::kFloat; |
| return Err(); |
| } |
| |
| if (BaseType::IsSigned(base->base_type())) |
| *realm = MathRealm::kSigned; |
| else |
| *realm = MathRealm::kUnsigned; |
| return Err(); |
| } |
| |
| return Err("Invalid non-numeric type '%s' for operator.", type->GetFullName().c_str()); |
| } |
| |
| // Collects the computed information for one parameter for passing around more conveniently. |
| struct OpValue { |
| const ExprValue* value = nullptr; |
| fxl::RefPtr<Type> concrete_type; // Extracted from value.type(). |
| MathRealm realm = MathRealm::kUnsigned; |
| }; |
| |
| Err FillOpValue(EvalContext* context, const ExprValue& in, OpValue* out) { |
| out->value = ∈ |
| |
| out->concrete_type = context->GetConcreteType(in.type()); |
| if (!out->concrete_type) |
| return Err("No type information"); |
| if (out->concrete_type->byte_size() == 0 || in.data().empty()) |
| return Err("Empty type size for operator."); |
| |
| return GetRealm(out->concrete_type.get(), &out->realm); |
| } |
| |
| // Given a binary operation of the two parameters, computes the realm that the operation should be |
| // done in, and computes which of the types is larger. This larger type does not take into account |
| // integral promotion described at the top of this file, it will always be one of the two inputs. |
| Err GetOpRealm(const fxl::RefPtr<EvalContext>& context, const OpValue& left, const ExprToken& op, |
| const OpValue& right, MathRealm* op_realm, fxl::RefPtr<Type>* larger_type) { |
| // Pointer always takes precedence. Pointer comparisons use the unsigned realm since they just do |
| // integer comparisons of the pointer value. Everything else falls into the pointer realm. |
| bool is_comparison = IsComparison(op.type()); |
| if (left.realm == MathRealm::kPointer) { |
| *op_realm = is_comparison ? MathRealm::kUnsigned : MathRealm::kPointer; |
| *larger_type = left.concrete_type; |
| return Err(); |
| } |
| if (right.realm == MathRealm::kPointer) { |
| *op_realm = is_comparison ? MathRealm::kUnsigned : MathRealm::kPointer; |
| *larger_type = right.concrete_type; |
| return Err(); |
| } |
| |
| // Floating-point is next. |
| if (left.realm == MathRealm::kFloat && right.realm == MathRealm::kFloat) { |
| // Both float: pick the biggest one (defaulting to the left on a tie). |
| *op_realm = MathRealm::kFloat; |
| if (right.concrete_type->byte_size() > left.concrete_type->byte_size()) |
| *larger_type = right.concrete_type; |
| else |
| *larger_type = left.concrete_type; |
| return Err(); |
| } |
| if (left.realm == MathRealm::kFloat) { |
| *op_realm = MathRealm::kFloat; |
| *larger_type = left.concrete_type; |
| return Err(); |
| } |
| if (right.realm == MathRealm::kFloat) { |
| *op_realm = MathRealm::kFloat; |
| *larger_type = right.concrete_type; |
| return Err(); |
| } |
| |
| // Integer math. Pick the larger one if the sizes are different. |
| if (left.concrete_type->byte_size() > right.concrete_type->byte_size()) { |
| *op_realm = left.realm; |
| *larger_type = left.concrete_type; |
| return Err(); |
| } |
| if (right.concrete_type->byte_size() > left.concrete_type->byte_size()) { |
| *op_realm = right.realm; |
| *larger_type = right.concrete_type; |
| return Err(); |
| } |
| |
| // Same size and both are integers, pick the unsigned one if they disagree. |
| if (left.realm != right.realm) { |
| if (left.realm == MathRealm::kUnsigned) { |
| *op_realm = left.realm; |
| *larger_type = left.concrete_type; |
| } else { |
| *op_realm = right.realm; |
| *larger_type = right.concrete_type; |
| } |
| return Err(); |
| } |
| |
| // Pick the left one if everything else agrees. |
| *op_realm = left.realm; |
| *larger_type = left.concrete_type; |
| return Err(); |
| } |
| |
| // Applies the given operator to two integers. The type T can be either uint64_t for unsigned, or |
| // int64_t for signed operation. |
| // |
| // The flag "check_for_zero_right" will issue a divide-by-zero error if the right-hand-side is zero. |
| // Error checking could be generalized more in the "op" callback, but this is currently the only |
| // error case and it keeps all of the op implementations simpler to do it this way. |
| template <typename T, typename ResultT> |
| ErrOrValue DoIntBinaryOp(const OpValue& left, const OpValue& right, bool check_for_zero_right, |
| ResultT (*op)(T, T), fxl::RefPtr<Type> result_type) { |
| T left_val; |
| if (Err err = left.value->PromoteTo64(&left_val); err.has_error()) |
| return err; |
| |
| T right_val; |
| if (Err err = right.value->PromoteTo64(&right_val); err.has_error()) |
| return err; |
| if (check_for_zero_right) { |
| if (right_val == 0) |
| return Err("Division by 0."); |
| } |
| |
| ResultT result_val = op(left_val, right_val); |
| |
| // Never expect to generate larger output than our internal result. |
| FX_DCHECK(result_type->byte_size() <= sizeof(ResultT)); |
| |
| // Convert to a base type of the correct size. |
| std::vector<uint8_t> result_data; |
| result_data.resize(result_type->byte_size()); |
| memcpy(result_data.data(), &result_val, result_type->byte_size()); |
| |
| return ExprValue(std::move(result_type), std::move(result_data)); |
| } |
| |
| // Converts the given value to a double if possible when |
| Err OpValueToDouble(const fxl::RefPtr<EvalContext>& context, const OpValue& in, double* out) { |
| if (in.realm == MathRealm::kFloat) |
| return in.value->PromoteToDouble(out); // Already floating-point. |
| |
| // Needs casting to a float. |
| auto double_type = fxl::MakeRefCounted<BaseType>(BaseType::kBaseTypeFloat, 8, "double"); |
| ErrOrValue casted = CastNumericExprValue(context, *in.value, std::move(double_type)); |
| if (casted.has_error()) |
| return casted.err(); |
| |
| return casted.value().PromoteToDouble(out); |
| } |
| |
| // Applies the given operator to two values that should be done in floating-point. The templated |
| // result type should be either a double (for math) or bool (for comparison). In the boolean case, |
| // the result_typee may be null since this will be the autmoatically created one. |
| template <typename ResultT> |
| ErrOrValue DoFloatBinaryOp(const fxl::RefPtr<EvalContext>& context, const OpValue& left, |
| const OpValue& right, ResultT (*op)(double, double), |
| fxl::RefPtr<Type> result_type) { |
| // The inputs could be various types like signed or unsigned integers or even bools. Use the |
| // casting infrastructure to convert these when necessary. |
| double left_double = 0.0; |
| if (Err err = OpValueToDouble(context, left, &left_double); err.has_error()) |
| return err; |
| double right_double = 0.0; |
| if (Err err = OpValueToDouble(context, right, &right_double); err.has_error()) |
| return err; |
| |
| // The actual operation. |
| ResultT result_val = op(left_double, right_double); |
| |
| // Convert to raw bytes. |
| std::vector<uint8_t> result_data; |
| if (std::is_same<ResultT, bool>::value) // Result wants a boolean. |
| return ExprValue(result_val); |
| if (result_type->byte_size() == sizeof(double)) // Result wants a double. |
| return ExprValue(result_val, std::move(result_type)); |
| if (result_type->byte_size() == sizeof(float)) // Convert down to 32-bit float. |
| return ExprValue(static_cast<float>(result_val), std::move(result_type)); |
| |
| // No other floating-point sizes are supported. |
| return Err("Invalid floating point operation."); |
| } |
| |
| // Returns a language-appropriate 64-bit signed or unsigned (according to the realm) type. The |
| // language is taken from the given language reference type. |
| fxl::RefPtr<Type> Make64BitIntegerType(MathRealm realm, fxl::RefPtr<Type> lang_reference) { |
| bool is_rust = lang_reference->GetLanguage() == DwarfLang::kRust; |
| |
| if (realm == MathRealm::kSigned) { |
| if (is_rust) |
| return fxl::MakeRefCounted<BaseType>(BaseType::kBaseTypeSigned, 8, "i64"); |
| return fxl::MakeRefCounted<BaseType>(BaseType::kBaseTypeSigned, 8, "int64_t"); |
| } else if (realm == MathRealm::kUnsigned) { |
| if (is_rust) |
| return fxl::MakeRefCounted<BaseType>(BaseType::kBaseTypeUnsigned, 8, "u64"); |
| return fxl::MakeRefCounted<BaseType>(BaseType::kBaseTypeUnsigned, 8, "uint64_t"); |
| } |
| return fxl::RefPtr<Type>(); |
| } |
| |
| // Computes a possibly-new larger type for the given math realm. This is so we can avoid overflow |
| // when using expressions in "calculator" mode regardless of the input type. |
| fxl::RefPtr<Type> ExpandTypeTo64(MathRealm realm, fxl::RefPtr<Type> input) { |
| if (input->byte_size() >= 8) |
| return input; // 64-bit input is large enough, don't mess with it. |
| |
| // Smaller ints get a synthesized type. |
| if (realm == MathRealm::kSigned || realm == MathRealm::kUnsigned) |
| return Make64BitIntegerType(realm, input); |
| |
| // No change necessary. Don't change floats or pointers. |
| return input; |
| } |
| |
| // Returns the byte size of the type pointed to by the given type. If anything fails or if the size |
| // is 0, returns an error. |
| Err GetPointedToByteSize(const fxl::RefPtr<EvalContext>& context, const Type* type, |
| uint32_t* size) { |
| *size = 0; |
| |
| fxl::RefPtr<Type> pointed_to; |
| if (Err err = GetPointedToType(context, type, &pointed_to); err.has_error()) |
| return err; |
| |
| // Need to make concrete to get the size. |
| pointed_to = context->GetConcreteType(pointed_to.get()); |
| *size = pointed_to->byte_size(); |
| if (*size == 0) |
| return Err("Can't do pointer arithmetic on a type of size 0."); |
| return Err(); |
| } |
| |
| ErrOrValue DoPointerOperation(const fxl::RefPtr<EvalContext>& context, const OpValue& left_value, |
| const ExprToken& op, const OpValue& right_value) { |
| // Adding or subtracting a pointer and an integer or and integer to a pointer advances the pointer |
| // by the size of the pointed-to type. |
| const OpValue* int_value = nullptr; |
| const OpValue* ptr_value = nullptr; |
| if (left_value.realm == MathRealm::kPointer && IsIntegerRealm(right_value.realm)) { |
| // pointer <op> int: Addition and subtraction are supported. |
| if (op.type() != ExprTokenType::kMinus && op.type() != ExprTokenType::kPlus) |
| return Err("Unsupported operator '%s' for pointer.", op.value().c_str()); |
| |
| ptr_value = &left_value; |
| int_value = &right_value; |
| } else if (IsIntegerRealm(left_value.realm) && right_value.realm == MathRealm::kPointer) { |
| // int <op> pointer: Only addition is supported. |
| if (op.type() != ExprTokenType::kPlus) |
| return Err("Unsupported operator '%s' for pointer.", op.value().c_str()); |
| |
| int_value = &left_value; |
| ptr_value = &right_value; |
| } |
| if (int_value && ptr_value) { |
| uint32_t pointed_to_size = 0; |
| if (Err err = GetPointedToByteSize(context, ptr_value->concrete_type.get(), &pointed_to_size); |
| err.has_error()) |
| return err; |
| |
| uint64_t ptr_number = 0; |
| if (Err err = ptr_value->value->PromoteTo64(&ptr_number); err.has_error()) |
| return err; |
| |
| int64_t int_number = 0; |
| if (Err err = int_value->value->PromoteTo64(&int_number); err.has_error()) |
| return err; |
| |
| uint64_t result_number; |
| if (op.type() == ExprTokenType::kMinus) { |
| // For minus everything was checked above so we know this is <pointer> - <number>. |
| result_number = ptr_number - pointed_to_size * int_number; |
| } else { |
| // Everything else should be addition. |
| result_number = ptr_number + pointed_to_size * int_number; |
| } |
| |
| // Convert to the result. Use the type from the pointer on the value to keep things like C-V |
| // qualifiers from the original. |
| return ExprValue(result_number, ptr_value->value->type_ref()); |
| } |
| |
| // The only other pointer operation to support is subtraction. |
| if (op.type() != ExprTokenType::kMinus) |
| return Err("Unsupported operator '%s' for pointer.", op.value().c_str()); |
| |
| // For subtraction, both pointers need to be the same type. |
| if (left_value.concrete_type->GetFullName() != right_value.concrete_type->GetFullName()) { |
| return Err("Can't subtract pointers of different types '%s' and '%s'.", |
| left_value.concrete_type->GetFullName().c_str(), |
| right_value.concrete_type->GetFullName().c_str()); |
| } |
| |
| // Validate the pointed-to type sizes. |
| uint32_t left_pointed_to_size = 0; |
| if (Err err = |
| GetPointedToByteSize(context, left_value.concrete_type.get(), &left_pointed_to_size); |
| err.has_error()) |
| return err; |
| uint32_t right_pointed_to_size = 0; |
| if (Err err = |
| GetPointedToByteSize(context, right_value.concrete_type.get(), &right_pointed_to_size); |
| err.has_error()) |
| return err; |
| if (left_pointed_to_size != right_pointed_to_size) { |
| return Err("Can't subtract pointers of different sizes %" PRIu32 " and %" PRIu32 ".", |
| left_pointed_to_size, right_pointed_to_size); |
| } |
| |
| // Do the operation in signed so that subtraction makes sense (ptrdiff_t is signed). |
| int64_t left_number = 0; |
| if (Err err = left_value.value->PromoteTo64(&left_number); err.has_error()) |
| return err; |
| int64_t right_number = 0; |
| if (Err err = right_value.value->PromoteTo64(&right_number); err.has_error()) |
| return err; |
| |
| return ExprValue(static_cast<uint64_t>((left_number - right_number) / left_pointed_to_size), |
| Make64BitIntegerType(MathRealm::kSigned, left_value.concrete_type)); |
| } |
| |
| ErrOrValue DoLogicalBinaryOp(const fxl::RefPtr<EvalContext>& context, const OpValue& left_value, |
| const ExprToken& op, const OpValue& right_value) { |
| // In general the left will have already been converted to a bool and checks to implement |
| // short-ciruiting for these operators. But reevaluate anyway which is useful for tests. |
| ErrOrValue left_as_bool = CastNumericExprValue(context, *left_value.value, MakeBoolType()); |
| if (left_as_bool.has_error()) |
| return left_as_bool; |
| |
| ErrOrValue right_as_bool = CastNumericExprValue(context, *right_value.value, MakeBoolType()); |
| if (right_as_bool.has_error()) |
| return right_as_bool; |
| |
| if (op.type() == ExprTokenType::kDoubleAnd) { |
| return ExprValue(left_as_bool.value().GetAs<uint8_t>() && |
| right_as_bool.value().GetAs<uint8_t>()); |
| } |
| if (op.type() == ExprTokenType::kLogicalOr) { |
| return ExprValue(left_as_bool.value().GetAs<uint8_t>() || |
| right_as_bool.value().GetAs<uint8_t>()); |
| } |
| return Err("Internal error."); |
| } |
| |
| // This is called for an expression like "x@20" which means "interpret x as an array of length 20". |
| // This syntax matches GDB. |
| void DoSetArrayLength(const fxl::RefPtr<EvalContext>& context, const ExprValue& array_value, |
| const ExprValue& length_value, EvalCallback cb) { |
| OpValue length_op_value; |
| if (Err err = FillOpValue(context.get(), length_value, &length_op_value); err.has_error()) |
| return cb(err); |
| if (!IsIntegerRealm(length_op_value.realm)) |
| return cb(Err("Value on right of '@' must be an integer.")); |
| |
| int64_t new_length = 0; |
| if (Err err = length_value.PromoteTo64(&new_length); err.has_error()) |
| return cb(err); |
| if (new_length < 0) |
| return cb(Err("Can not resize an array to a negative size.")); |
| |
| CoerceArraySize(context, array_value, new_length, std::move(cb)); |
| } |
| |
| } // namespace |
| |
| void EvalBinaryOperator(const fxl::RefPtr<EvalContext>& context, const ExprValue& left_value, |
| const ExprToken& op, const ExprValue& right_value, EvalCallback cb) { |
| // Handle array resize specially (this can handle 0-length arrays on the left so must be done |
| // before FillOpValue() which errors on that case. |
| if (op.type() == ExprTokenType::kAt) { |
| // This is not a standard C operator, we use it to declare array sizes like GDB. This will |
| // complete asynchronously since it may involve a memory fetch. |
| return DoSetArrayLength(context, left_value, right_value, std::move(cb)); |
| } |
| |
| if (!left_value.type() || !right_value.type()) |
| return cb(Err("No type information.")); |
| if (!left_value.data().all_valid() || !right_value.data().all_valid()) |
| return cb(Err::OptimizedOut()); |
| |
| // Handle assignement specially. |
| if (op.type() == ExprTokenType::kEquals) |
| return DoAssignment(std::move(context), left_value, right_value, std::move(cb)); |
| |
| // Left info. |
| OpValue left_op_value; |
| if (Err err = FillOpValue(context.get(), left_value, &left_op_value); err.has_error()) |
| return cb(err); |
| |
| // Right info. |
| OpValue right_op_value; |
| if (Err err = FillOpValue(context.get(), right_value, &right_op_value); err.has_error()) |
| return cb(err); |
| |
| // Operation info. |
| MathRealm realm; |
| fxl::RefPtr<Type> larger_type; |
| if (Err err = GetOpRealm(context, left_op_value, op, right_op_value, &realm, &larger_type); |
| err.has_error()) |
| return cb(err); |
| |
| // Special-case pointer operations since they work differently. |
| if (realm == MathRealm::kPointer) |
| return cb(DoPointerOperation(context, left_op_value, op, right_op_value)); |
| |
| // Implements the type expansion described at the top of this file. |
| larger_type = ExpandTypeTo64(realm, larger_type); |
| |
| // Implements support for a given operator that only works for integer types. |
| #define IMPLEMENT_INTEGER_BINARY_OP(c_op, is_divide) \ |
| switch (realm) { \ |
| case MathRealm::kSigned: \ |
| result = DoIntBinaryOp<int64_t, int64_t>( \ |
| left_op_value, right_op_value, is_divide, \ |
| [](int64_t left, int64_t right) { return left c_op right; }, larger_type); \ |
| break; \ |
| case MathRealm::kUnsigned: \ |
| result = DoIntBinaryOp<uint64_t, uint64_t>( \ |
| left_op_value, right_op_value, is_divide, \ |
| [](uint64_t left, uint64_t right) { return left c_op right; }, larger_type); \ |
| break; \ |
| case MathRealm::kFloat: \ |
| result = Err("Operator '%s' not defined for floating point.", op.value().c_str()); \ |
| break; \ |
| case MathRealm::kPointer: \ |
| FX_NOTREACHED(); \ |
| break; \ |
| } |
| |
| // Implements support for a given operator that only works for integer or floating point types. |
| // Pointers should have been handled specially above. |
| #define IMPLEMENT_BINARY_OP(c_op, is_divide) \ |
| switch (realm) { \ |
| case MathRealm::kSigned: \ |
| result = DoIntBinaryOp<int64_t, int64_t>( \ |
| left_op_value, right_op_value, is_divide, \ |
| [](int64_t left, int64_t right) { return left c_op right; }, larger_type); \ |
| break; \ |
| case MathRealm::kUnsigned: \ |
| result = DoIntBinaryOp<uint64_t, uint64_t>( \ |
| left_op_value, right_op_value, is_divide, \ |
| [](uint64_t left, uint64_t right) { return left c_op right; }, larger_type); \ |
| break; \ |
| case MathRealm::kFloat: \ |
| result = DoFloatBinaryOp<double>( \ |
| context, left_op_value, right_op_value, \ |
| [](double left, double right) { return left c_op right; }, larger_type); \ |
| break; \ |
| case MathRealm::kPointer: \ |
| FX_NOTREACHED(); \ |
| break; \ |
| } |
| |
| // Implements support for a given comparison operator. |
| #define IMPLEMENT_COMPARISON_BINARY_OP(c_op) \ |
| switch (realm) { \ |
| case MathRealm::kSigned: \ |
| result = DoIntBinaryOp<int64_t, bool>( \ |
| left_op_value, right_op_value, false, \ |
| [](int64_t left, int64_t right) { return left c_op right; }, MakeBoolType()); \ |
| break; \ |
| case MathRealm::kUnsigned: \ |
| result = DoIntBinaryOp<uint64_t, bool>( \ |
| left_op_value, right_op_value, false, \ |
| [](uint64_t left, uint64_t right) { return left c_op right; }, MakeBoolType()); \ |
| break; \ |
| case MathRealm::kFloat: \ |
| result = DoFloatBinaryOp<bool>( \ |
| context, left_op_value, right_op_value, \ |
| [](double left, double right) { return left c_op right; }, nullptr); \ |
| break; \ |
| case MathRealm::kPointer: \ |
| FX_NOTREACHED(); \ |
| break; \ |
| } |
| |
| ErrOrValue result((ExprValue())); |
| switch (op.type()) { |
| case ExprTokenType::kPlus: |
| IMPLEMENT_BINARY_OP(+, false); |
| break; |
| case ExprTokenType::kMinus: |
| IMPLEMENT_BINARY_OP(-, false); |
| break; |
| case ExprTokenType::kSlash: |
| IMPLEMENT_BINARY_OP(/, true); |
| break; |
| case ExprTokenType::kStar: |
| IMPLEMENT_BINARY_OP(*, false); |
| break; |
| case ExprTokenType::kPercent: |
| IMPLEMENT_INTEGER_BINARY_OP(%, true); |
| break; |
| case ExprTokenType::kAmpersand: |
| IMPLEMENT_INTEGER_BINARY_OP(&, false); |
| break; |
| case ExprTokenType::kBitwiseOr: |
| IMPLEMENT_INTEGER_BINARY_OP(|, false); |
| break; |
| case ExprTokenType::kCaret: |
| IMPLEMENT_INTEGER_BINARY_OP(^, false); |
| break; |
| case ExprTokenType::kShiftLeft: |
| IMPLEMENT_INTEGER_BINARY_OP(<<, false); |
| break; |
| case ExprTokenType::kShiftRight: |
| IMPLEMENT_INTEGER_BINARY_OP(>>, false); |
| break; |
| |
| case ExprTokenType::kEquality: |
| IMPLEMENT_COMPARISON_BINARY_OP(==); |
| break; |
| case ExprTokenType::kInequality: |
| IMPLEMENT_COMPARISON_BINARY_OP(!=); |
| break; |
| case ExprTokenType::kLessEqual: |
| IMPLEMENT_COMPARISON_BINARY_OP(<=); |
| break; |
| case ExprTokenType::kGreaterEqual: |
| IMPLEMENT_COMPARISON_BINARY_OP(>=); |
| break; |
| case ExprTokenType::kLess: |
| IMPLEMENT_COMPARISON_BINARY_OP(<); |
| break; |
| case ExprTokenType::kGreater: |
| IMPLEMENT_COMPARISON_BINARY_OP(>); |
| break; |
| |
| case ExprTokenType::kSpaceship: |
| // The three-way comparison isn't useful in a debugger, and isn't really implementable anyway |
| // because it returns some kind of special std constant that we would rather not count on. |
| result = Err("Sorry, no UFOs allowed here."); |
| break; |
| |
| case ExprTokenType::kDoubleAnd: |
| case ExprTokenType::kLogicalOr: |
| result = DoLogicalBinaryOp(context, left_op_value, op, right_op_value); |
| break; |
| |
| case ExprTokenType::kMinusMinus: |
| case ExprTokenType::kPlusPlus: |
| case ExprTokenType::kPlusEquals: |
| case ExprTokenType::kMinusEquals: |
| case ExprTokenType::kStarEquals: |
| case ExprTokenType::kSlashEquals: |
| case ExprTokenType::kPercentEquals: |
| case ExprTokenType::kCaretEquals: |
| case ExprTokenType::kAndEquals: |
| case ExprTokenType::kOrEquals: |
| case ExprTokenType::kShiftLeftEquals: |
| case ExprTokenType::kShiftRightEquals: |
| result = Err("In-place update operators (++, --, +=, >>=, etc.) aren't supported."); |
| break; |
| |
| default: |
| result = Err("Unsupported binary operator '%s', sorry!", op.value().c_str()); |
| break; |
| } |
| |
| cb(std::move(result)); |
| } |
| |
| void EvalBinaryOperator(const fxl::RefPtr<EvalContext>& context, const fxl::RefPtr<ExprNode>& left, |
| const ExprToken& op, const fxl::RefPtr<ExprNode>& right, EvalCallback cb) { |
| left->Eval(context, [context, op, right, cb = std::move(cb)](ErrOrValue left_value) mutable { |
| if (left_value.has_error()) |
| return cb(left_value); |
| |
| if (op.type() == ExprTokenType::kLogicalOr || op.type() == ExprTokenType::kDoubleAnd) { |
| // Short-circuit for || and &&. |
| ErrOrValue left_as_bool = CastNumericExprValue(context, left_value.value(), MakeBoolType()); |
| if (left_as_bool.has_error()) |
| return cb(left_as_bool.err()); |
| |
| if (left_as_bool.value().GetAs<uint8_t>()) { |
| if (op.type() == ExprTokenType::kLogicalOr) |
| return cb(left_as_bool); // Computation complete, skip evaluating the right side. |
| |
| // Fall through to evaluating the right side given the left already casted to a bool. |
| left_value = left_as_bool.value(); |
| } else { |
| if (op.type() == ExprTokenType::kDoubleAnd) |
| return cb(left_as_bool); // Computation complete, skip evaluating the right side. |
| |
| // Fall through to evaluating the right side given the left already casted to a bool. |
| left_value = left_as_bool.value(); |
| } |
| } |
| |
| right->Eval(context, [context, left_value = left_value.take_value(), op, |
| cb = std::move(cb)](ErrOrValue right_value) mutable { |
| if (right_value.has_error()) |
| cb(right_value); |
| else |
| EvalBinaryOperator(std::move(context), left_value, op, right_value.value(), std::move(cb)); |
| }); |
| }); |
| } |
| |
| // UBSan complains about the overflow of -INT32_MAX but our tests cover that. |
| [[clang::no_sanitize("signed-integer-overflow")]] void EvalUnaryOperator( |
| const fxl::RefPtr<EvalContext>& context, const ExprToken& op_token, const ExprValue& value, |
| EvalCallback cb) { |
| if (!value.type()) |
| return cb(Err("No type information.")); |
| if (!value.data().all_valid()) |
| return cb(Err::OptimizedOut()); |
| |
| OpValue op_value; |
| if (Err err = FillOpValue(context.get(), value, &op_value); err.has_error()) |
| return cb(err); |
| |
| // Implements a unary operator that applies C rules for the 4 different sized types. The types |
| // are passed in so that this can work with both signed and unsigned input. |
| // |
| // C has a bunch of rules (see "integer promotion" at the top of this file). |
| // |
| // This logic implicitly takes advantage of the C rules but the type names produced will be the |
| // sized C++ stdint.h types rather than what C would use (int/unsigned, etc.) or whatever the |
| // current language would produce (e.g. u32 on Rust). Since these are temporaries, the type |
| // names usually aren't very important so the simplicity of this approach is preferrable. |
| #define IMPLEMENT_UNARY_INTEGER_OP(c_op, type8, type16, type32, type64) \ |
| switch (value.data().size()) { \ |
| case sizeof(type8): \ |
| result = ExprValue(c_op value.GetAs<type8>()); \ |
| break; \ |
| case sizeof(type16): \ |
| result = ExprValue(c_op value.GetAs<type16>()); \ |
| break; \ |
| case sizeof(type32): \ |
| result = ExprValue(c_op value.GetAs<type32>()); \ |
| break; \ |
| case sizeof(type64): \ |
| result = ExprValue(c_op value.GetAs<type64>()); \ |
| break; \ |
| default: \ |
| result = Err("Unsupported size for unary operator '%s'.", op_token.value().c_str()); \ |
| break; \ |
| } |
| |
| #define IMPLEMENT_UNARY_FLOAT_OP(c_op) \ |
| switch (value.data().size()) { \ |
| case sizeof(float): \ |
| result = ExprValue(c_op value.GetAs<float>()); \ |
| break; \ |
| case sizeof(double): \ |
| result = ExprValue(c_op value.GetAs<double>()); \ |
| break; \ |
| default: \ |
| result = Err("Unsupported size for unary operator '%s'.", op_token.value().c_str()); \ |
| break; \ |
| } |
| |
| ErrOrValue result((ExprValue())); |
| switch (op_token.type()) { |
| // - |
| case ExprTokenType::kMinus: |
| switch (op_value.realm) { |
| case MathRealm::kSigned: |
| IMPLEMENT_UNARY_INTEGER_OP(-, int8_t, int16_t, int32_t, int64_t); |
| break; |
| case MathRealm::kUnsigned: |
| IMPLEMENT_UNARY_INTEGER_OP(-, uint8_t, uint16_t, uint32_t, uint64_t); |
| break; |
| case MathRealm::kFloat: |
| IMPLEMENT_UNARY_FLOAT_OP(-); |
| break; |
| default: |
| result = |
| Err("Invalid type '%s' for unary operator '-'.", value.type()->GetFullName().c_str()); |
| break; |
| } |
| break; |
| |
| // ! |
| case ExprTokenType::kBang: |
| switch (op_value.realm) { |
| case MathRealm::kSigned: |
| IMPLEMENT_UNARY_INTEGER_OP(!, int8_t, int16_t, int32_t, int64_t); |
| break; |
| case MathRealm::kPointer: // ! can treat a pointer like an unsigned int. |
| case MathRealm::kUnsigned: |
| IMPLEMENT_UNARY_INTEGER_OP(!, uint8_t, uint16_t, uint32_t, uint64_t); |
| break; |
| case MathRealm::kFloat: |
| IMPLEMENT_UNARY_FLOAT_OP(!); |
| break; |
| } |
| break; |
| |
| // ~ |
| case ExprTokenType::kTilde: |
| switch (op_value.realm) { |
| case MathRealm::kSigned: |
| IMPLEMENT_UNARY_INTEGER_OP(~, int8_t, int16_t, int32_t, int64_t); |
| break; |
| case MathRealm::kUnsigned: |
| IMPLEMENT_UNARY_INTEGER_OP(~, uint8_t, uint16_t, uint32_t, uint64_t); |
| break; |
| default: |
| result = |
| Err("Invalid type '%s' for unary operator '~'.", value.type()->GetFullName().c_str()); |
| break; |
| } |
| break; |
| |
| default: |
| result = Err("Invalid unary operator '%s'.", op_token.value().c_str()); |
| break; |
| } |
| cb(result); |
| } |
| |
| } // namespace zxdb |