blob: 5d15615bbfd8661557cee29b07b0cd81cedb97d2 [file] [log] [blame]
// Copyright 2022 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.
#ifndef SRC_DEVELOPER_DEBUG_ZXDB_EXPR_VM_OP_H_
#define SRC_DEVELOPER_DEBUG_ZXDB_EXPR_VM_OP_H_
#include <variant>
#include <vector>
#include "src/developer/debug/zxdb/common/err.h"
#include "src/developer/debug/zxdb/expr/eval_context.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/parsed_identifier.h"
#include "src/developer/debug/zxdb/expr/vm_op_type.h"
namespace zxdb {
// Holds a bytecode operation type (VmOpType) and any parameters associated with it.
//
// Most bytecode machines would encode any parameters like strings constants or a binary operator
// type compactly, either in the byte string, or using a small representation of a reference to
// some other stream.
//
// Our programs are so small compared to the type of system we expect to run on we do not care
// about space. Instead, this implementation prefers a safe, simple approach. As part of this,
// each VM operation is a bytecode operation combined with a relatively large variant holding any
// ancillary data required by the operation. For many operations, this is very wasteful, but means
// we can avoid doing error-prone reading of variable data from the instruction stream and the
// decode logic becomes trivial.
//
// See vm_exec.cc for execution details.
//
// Local variables
// ---------------
//
// In most "real" stack-based bycode machines, the local variables would be stored on the stack
// and referenced by index from the "stack pointer" (the position of the stack and the entrypoint
// of the current function). It is simple and efficient.
//
// But it requires very careful tracking of where variables can be declared such that the block
// that contains them can pop its local variables when the block exits: a local variable declaration
// must never be conditionally executed since then the containing block wouldn't know whether to
// clean it up. Any mistakes will corrupt the VM stack and are difficult to debug.
//
// Our parser supports multiple languages and plays a bit fast-and-loose with where declarations can
// appear. This could be fixed but since we also care much more about debugability and simplicity
// of the parser than of performance, we have dedicated storage for local variables.
//
// As local variables are parsed, we assign each one an index based on the parser depth just like
// the stack-based approach. But these refer into a separate array specific to local variables.
// This adds a copy for each variable declaration and some extra memory allocations for the separate
// array but neither of these matter to us. The local variables created inside a scope are cleared
// by the kPopLocals command which will be emitted at the end of a block. The block knows the size
// to shrink to because it knows how many local variables are in scope at the entry to the block.
// But this approach doesn't care if a local variable declaration was skipped (that slot will just
// be unused).
//
// Example:
//
// { // The parser remembers the # of locals in scope at the opening of the block.
// // This info is saved on the block parse node for the last step.
//
// int i = 23; // The parser adds info about "i" and saves its index as the local variable
// // slot. This slot is used in the op kSetLocal.
//
// i = 19; // The parser looks up the variable index and uses it in a kGetLocal op.
//
// } // At the exit of a block, the parser emits kPopLocals to set the # locals back
// // the size it was at the opening of the block.
//
// Variable assignment
// -------------------
//
// Our expression language doesn't have lvalues. The assignment operator (binary operator "=")
// always fully evaluates the thing on the left-hand-side. This simplifies things by providing only
// one code path for all evaluating (rather than having a code path to compute where the thing would
// be without computing its value).
//
// All ExprValues have an ExprValueSource which tracks where they came from originally. This
// information is used to update the variable when modified. For program values this is normally a
// memory address or a register. For debugger-local variables, we keep a refptr to the source of
// the value. The LocalExprValue object is a refcounted container for local variables that can
// have such references to them.
//
// Break
// -----
// The "break" keyword (e.g. for a loop) is weird and difficult. You need to execute the cleanup
// code for every construct between the break and the loop, but not any of the remaining code in
// them.
//
// Our break is implemented somewhat like an exception. At the top of a loop, the loop emits the
// "PushBreak" opcode (like the "try" instruction of an exception handler). This sets the
// destination stream address to jump to and saves the stack and local variable stack. The "break"
// opcode jumps to the nearest "set break" destination (like a thrown exception), and pops the stack
// and local variable stack size back to the values they were at the top of the loop (like exception
// unwinding code).
//
// The bottom of the loop executes a "PopBreak" command which releases the registration of that
// loop.
struct VmOp {
// Constant to default-initialize a jump destination to that's wrong.
static constexpr uint32_t kBadJumpDest = static_cast<uint32_t>(-1);
// Callback types.
using Callback0 = fit::function<ErrOrValue(const fxl::RefPtr<EvalContext>& eval_context)>;
using Callback1 =
fit::function<ErrOrValue(const fxl::RefPtr<EvalContext>& eval_context, ExprValue param)>;
using Callback2 = fit::function<ErrOrValue(const fxl::RefPtr<EvalContext>& eval_context,
ExprValue param1, ExprValue param2)>;
using CallbackN = fit::function<ErrOrValue(const fxl::RefPtr<EvalContext>& eval_context,
std::vector<ExprValue> params)>;
using AsyncCallback0 =
fit::function<void(const fxl::RefPtr<EvalContext>& eval_context, EvalCallback cb)>;
using AsyncCallback1 = fit::function<void(const fxl::RefPtr<EvalContext>& eval_context,
ExprValue param, EvalCallback cb)>;
using AsyncCallback2 = fit::function<void(const fxl::RefPtr<EvalContext>& eval_context,
ExprValue first, ExprValue second, EvalCallback cb)>;
using AsyncCallbackN = fit::function<void(const fxl::RefPtr<EvalContext>& eval_context,
std::vector<ExprValue> params, EvalCallback cb)>;
// Constructor helper functions.
static VmOp MakeError(Err err, ExprToken token = ExprToken()) {
return VmOp{.op = VmOpType::kError, .token = std::move(token), .info = err};
}
static VmOp MakeUnary(ExprToken token) {
return VmOp{.op = VmOpType::kUnary, .token = std::move(token)};
}
static VmOp MakeBinary(ExprToken token) {
return VmOp{.op = VmOpType::kBinary, .token = std::move(token)};
}
static VmOp MakeExpandRef(ExprToken token = ExprToken()) {
return VmOp{.op = VmOpType::kExpandRef, .token = std::move(token)};
}
static VmOp MakeDrop(ExprToken token = ExprToken()) {
return VmOp{.op = VmOpType::kDrop, .token = std::move(token)};
}
static VmOp MakeDup(ExprToken token = ExprToken()) {
return VmOp{.op = VmOpType::kDup, .token = std::move(token)};
}
static VmOp MakeLiteral(ExprValue value, ExprToken token = ExprToken()) {
return VmOp{.op = VmOpType::kLiteral,
.token = std::move(token),
.info = LiteralInfo{.value = std::move(value)}};
}
static VmOp MakeJump(uint32_t dest = kBadJumpDest, ExprToken token = ExprToken()) {
return VmOp{.op = VmOpType::kJump, .token = std::move(token), .info = JumpInfo{.dest = dest}};
}
static VmOp MakeJumpIfFalse(uint32_t dest = kBadJumpDest, ExprToken token = ExprToken()) {
return VmOp{
.op = VmOpType::kJumpIfFalse, .token = std::move(token), .info = JumpInfo{.dest = dest}};
}
static VmOp MakeGetLocal(uint32_t slot, ExprToken token = ExprToken()) {
return VmOp{
.op = VmOpType::kGetLocal, .token = std::move(token), .info = LocalInfo{.slot = slot}};
}
static VmOp MakeSetLocal(uint32_t slot, ExprToken token = ExprToken()) {
return VmOp{
.op = VmOpType::kSetLocal, .token = std::move(token), .info = LocalInfo{.slot = slot}};
}
static VmOp MakePopLocals(uint32_t entry_count, ExprToken token = ExprToken()) {
return VmOp{.op = VmOpType::kPopLocals,
.token = std::move(token),
.info = LocalInfo{.slot = entry_count}};
}
static VmOp MakePushBreak(uint32_t dest = kBadJumpDest, ExprToken token = ExprToken()) {
return VmOp{
.op = VmOpType::kPushBreak, .token = std::move(token), .info = JumpInfo{.dest = dest}};
}
static VmOp MakePopBreak(ExprToken token = ExprToken()) {
return VmOp{.op = VmOpType::kPopBreak, .token = std::move(token)};
}
static VmOp MakeBreak(ExprToken token = ExprToken()) {
return VmOp{.op = VmOpType::kBreak, .token = std::move(token)};
}
static VmOp MakeCallback0(Callback0 cb, ExprToken token = ExprToken()) {
return VmOp{.op = VmOpType::kCallback0, .token = std::move(token), .info = std::move(cb)};
}
static VmOp MakeCallback1(Callback1 cb, ExprToken token = ExprToken()) {
return VmOp{.op = VmOpType::kCallback1, .token = std::move(token), .info = std::move(cb)};
}
static VmOp MakeCallback2(Callback2 cb, ExprToken token = ExprToken()) {
return VmOp{.op = VmOpType::kCallback2, .token = std::move(token), .info = std::move(cb)};
}
static VmOp MakeCallbackN(int num_params, CallbackN cb, ExprToken token = ExprToken()) {
return VmOp{.op = VmOpType::kCallbackN,
.token = std::move(token),
.info = CallbackNInfo{.num_params = num_params, .cb = std::move(cb)}};
}
static VmOp MakeAsyncCallback0(AsyncCallback0 cb, ExprToken token = ExprToken()) {
return VmOp{.op = VmOpType::kAsyncCallback0, .token = std::move(token), .info = std::move(cb)};
}
static VmOp MakeAsyncCallback1(AsyncCallback1 cb, ExprToken token = ExprToken()) {
return VmOp{.op = VmOpType::kAsyncCallback1, .token = std::move(token), .info = std::move(cb)};
}
static VmOp MakeAsyncCallback2(AsyncCallback2 cb, ExprToken token = ExprToken()) {
return VmOp{.op = VmOpType::kAsyncCallback2, .token = std::move(token), .info = std::move(cb)};
}
static VmOp MakeAsyncCallbackN(int num_params, AsyncCallbackN cb, ExprToken token = ExprToken()) {
return VmOp{.op = VmOpType::kAsyncCallbackN,
.token = std::move(token),
.info = AsyncCallbackNInfo{.num_params = num_params, .cb = std::move(cb)}};
}
// For operators with no extra info.
struct NoInfo {};
// For kLiteral.
struct LiteralInfo {
ExprValue value; // Literal value to push on the stack.
};
// For kJump, kJumpIfFalse, and kPushBreak.
struct JumpInfo {
uint32_t dest = kBadJumpDest; // Index of the destination of a jump operator.
};
// For kGetLocal, kSetLocal, and kPopLocals.
struct LocalInfo {
// Indicates the index into the local variable array of the local variable to be get/set for
// kGetLocal and kSetLocal.
//
// Indicates the final size of the variable array for kPopLocals.
//
// See "Local Variables" at the top of this file for an overview.
uint32_t slot = static_cast<uint32_t>(-1);
};
// The other callback types use the callback "using" statement above in the variant directly, but
// the "N" versions need a place to store the number of arguments alongside it.
struct CallbackNInfo {
int num_params;
CallbackN cb;
};
struct AsyncCallbackNInfo {
int num_params;
AsyncCallbackN cb;
};
using VariantType = std::variant<Err, NoInfo, JumpInfo, LiteralInfo, LocalInfo, Callback0,
Callback1, Callback2, CallbackNInfo, AsyncCallback0,
AsyncCallback1, AsyncCallback2, AsyncCallbackNInfo>;
// Sets the destination of the this operator's jump destination to the given value. This will
// assert if this operation is not a jump.
//
// This is used commonly because the destination of a jump is often unknown until additional code
// is filled in.
void SetJumpDest(uint32_t dest);
// Operation.
VmOpType op = VmOpType::kError;
// The token that generated this operation. For binary and unary operations, this is the operator
// itself. For things like loops, it will be the token indicating the loop ("while" for example).
// Errors will be blamed on this token.
ExprToken token;
VariantType info;
};
} // namespace zxdb
#endif // SRC_DEVELOPER_DEBUG_ZXDB_EXPR_VM_OP_H_