blob: c8cab4e69e5614e030626eb891cf95cb6ee80f9f [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/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 = &in;
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