| // 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. | |
| #include "src/developer/debug/zxdb/expr/vm_exec.h" | |
| #include <lib/syslog/cpp/macros.h> | |
| #include <optional> | |
| #include <vector> | |
| #include "src/developer/debug/zxdb/common/ref_ptr_to.h" | |
| #include "src/developer/debug/zxdb/expr/cast.h" | |
| #include "src/developer/debug/zxdb/expr/eval_operators.h" | |
| #include "src/developer/debug/zxdb/expr/local_expr_value.h" | |
| #include "src/developer/debug/zxdb/expr/resolve_ptr_ref.h" | |
| #include "src/lib/fxl/memory/ref_counted.h" | |
| #include "src/lib/fxl/strings/string_printf.h" | |
| namespace zxdb { | |
| namespace { | |
| // Identifies how each operation can complete. | |
| enum class Completion { | |
| kSync, // Operation completed synchronously and execution can continue. | |
| kAsync, // Operation will complete asynchronously via a subsequent call to Exec(). | |
| kError, // Operation completed asynchronously and an error was reported (nothing more to do). | |
| // By convention, if code returns this value, it should have already set the error | |
| // message. | |
| }; | |
| // Sanity check for the maximum local variables alive at a given time. | |
| constexpr uint32_t kMaxLocals = 256; | |
| // Sanity check for total program instructions executed. The program will be terminated after | |
| // this many instructions. This is to prevent infinite loops from hanging the debugger. | |
| constexpr int kMaxInstructions = 65535; | |
| // This class wants to run everything sequentially until an asynchronous operation happens. It | |
| // needs to integrate with the rest of the expression system which takes EvalCallbacks that can | |
| // execute synchronously (from within the current stack frame) or asynchronously (from the message | |
| // loop in the future). | |
| // | |
| // To bridge these two models, this struct is created as a shared_ptr so the caller and the callback | |
| // (created by MakeContinueCallback()) can communicate about how the callback is issued. | |
| // | |
| // The "current" state starts off as synchronous. If the callback is executed during this time, | |
| // the callback will set the "issued" state to kSync or kError. | |
| // | |
| // When the caller completes, it calls SynchronousDone() to indicate the end of the synchronous | |
| // phase of the callback. This returns what the loop should do based on whether the callback | |
| // was already issued or not. | |
| struct CallbackInfo { | |
| // Indicates the current execution completion kind. | |
| Completion current = Completion::kSync; | |
| // Set to the "current" state or error when the callback is issued. | |
| std::optional<Completion> issued; | |
| Completion SynchronousDone() { | |
| // Any callbacks issued from here on will be "asynchronous". | |
| FX_DCHECK(current == Completion::kSync); | |
| current = Completion::kAsync; | |
| if (issued) { | |
| // Callback has been issued already. Since we just finished the synchronous phase, it should | |
| // have been marked as an error or synchronous completion. | |
| FX_DCHECK(*issued != Completion::kAsync); | |
| return *issued; | |
| } | |
| // Callback hasn't been issued yet, it must be async completion in the future. | |
| return Completion::kAsync; | |
| } | |
| }; | |
| // Saved information for the kPushBreak instruction. | |
| struct BreakInfo { | |
| size_t stack_size = 0; | |
| size_t local_stack_size = 0; | |
| uint32_t dest = VmOp::kBadJumpDest; | |
| }; | |
| // Holds the machine state for a running bytecode program. | |
| // | |
| // This is a simple stack-based machine where the various operations operate on the value stack | |
| // stored in stack_. | |
| // | |
| // A great book on this topic is "Crafting Interpreters" by Robert Nystrom. | |
| class VmExecState : public fxl::RefCountedThreadSafe<VmExecState> { | |
| public: | |
| VmExecState(const fxl::RefPtr<EvalContext>& eval_context, VmStream stream, EvalCallback cb) | |
| : eval_context_(eval_context), stream_(std::move(stream)), cb_(std::move(cb)) {} | |
| static void Exec(fxl::RefPtr<VmExecState> state); | |
| private: | |
| // Executes one operation. | |
| Completion ExecOp(const VmOp& op); | |
| Completion ExecError(const VmOp& op); | |
| Completion ExecUnary(const VmOp& op); | |
| Completion ExecBinary(const VmOp& op); | |
| Completion ExecExpandRef(const VmOp& op); | |
| Completion ExecDrop(const VmOp& op); | |
| Completion ExecDup(const VmOp& op); | |
| Completion ExecLiteral(const VmOp& op); | |
| Completion ExecJump(const VmOp& op); | |
| Completion ExecJumpIfFalse(const VmOp& op); | |
| Completion ExecGetLocal(const VmOp& op); | |
| Completion ExecSetLocal(const VmOp& op); | |
| Completion ExecPopLocals(const VmOp& op); | |
| Completion ExecPushBreak(const VmOp& op); | |
| Completion ExecPopBreak(const VmOp& op); | |
| Completion ExecBreak(const VmOp& op); | |
| Completion ExecCallback0(const VmOp& op); | |
| Completion ExecCallback1(const VmOp& op); | |
| Completion ExecCallback2(const VmOp& op); | |
| Completion ExecCallbackN(const VmOp& op); | |
| Completion ExecAsyncCallback0(const VmOp& op); | |
| Completion ExecAsyncCallback1(const VmOp& op); | |
| Completion ExecAsyncCallback2(const VmOp& op); | |
| Completion ExecAsyncCallbackN(const VmOp& op); | |
| // Pushes the given value to the stack. | |
| void Push(ExprValue v); | |
| // Pops the top stack value and puts it in *popped. Returns either kSync or kError depending on | |
| // whether the value could be popped. | |
| Completion Pop(ExprValue* popped); | |
| void ReportDone(ErrOrValue result); | |
| // Issues the callback with the given error message. | |
| // | |
| // Always returns Completion::kError (for convenience so one can do: | |
| // | |
| // return ReportError("..."); | |
| // | |
| Completion ReportError(const std::string& msg); | |
| Completion ReportError(const Err& err); | |
| // See CallbackInfo above. | |
| EvalCallback MakeContinueCallback(std::shared_ptr<CallbackInfo> cb_info); | |
| fxl::RefPtr<EvalContext> eval_context_; | |
| VmStream stream_; | |
| EvalCallback cb_; | |
| // Indicates the NEXT instruction to execute. During processing of an instruction, the current | |
| // instruction will be stream_index_ - 1. | |
| size_t stream_index_ = 0; | |
| // Counter for the total number of instructions executed. | |
| int instruction_count_ = 0; | |
| std::vector<ExprValue> stack_; | |
| // The local variable "slots" in the Op::LocalInfo refer into this array. See the comment at the | |
| // top of vm_op.h for more on how this works. | |
| std::vector<fxl::RefPtr<LocalExprValue>> locals_; | |
| // Stack used by the break instructions. See vm_op.h. | |
| std::vector<BreakInfo> breaks_; | |
| }; | |
| // static | |
| void VmExecState::Exec(fxl::RefPtr<VmExecState> state) { | |
| while (state->stream_index_ < state->stream_.size()) { | |
| const auto& op = state->stream_[state->stream_index_]; | |
| state->stream_index_++; // Advance to next instruction. | |
| switch (state->ExecOp(op)) { | |
| case Completion::kSync: | |
| continue; | |
| case Completion::kAsync: | |
| // Exec() will be called back in the future to resume. | |
| return; | |
| case Completion::kError: | |
| // Error callback should already have been issued. | |
| FX_DCHECK(!state->cb_); | |
| return; | |
| } | |
| } | |
| // Successful completion. | |
| if (state->stack_.empty()) { | |
| // Every operation should push a value on the stack, so an empty stack should only happen for | |
| // empty programs. | |
| FX_DCHECK(state->stream_.empty()); | |
| state->ReportDone(ExprValue()); | |
| } else { | |
| // Correct programs should have only one result. | |
| FX_DCHECK(state->stack_.size() == 1u) << "Got " << state->stack_.size(); | |
| state->ReportDone(state->stack_.back()); | |
| } | |
| } | |
| Completion VmExecState::ExecOp(const VmOp& op) { | |
| instruction_count_++; | |
| if (instruction_count_ > kMaxInstructions) | |
| return ReportError("Execution terminated after max instruction count reached."); | |
| switch (op.op) { | |
| // clang-format off | |
| case VmOpType::kError: return ExecError(op); | |
| case VmOpType::kUnary: return ExecUnary(op); | |
| case VmOpType::kBinary: return ExecBinary(op); | |
| case VmOpType::kExpandRef: return ExecExpandRef(op); | |
| case VmOpType::kDrop: return ExecDrop(op); | |
| case VmOpType::kDup: return ExecDup(op); | |
| case VmOpType::kLiteral: return ExecLiteral(op); | |
| case VmOpType::kJump: return ExecJump(op); | |
| case VmOpType::kJumpIfFalse: return ExecJumpIfFalse(op); | |
| case VmOpType::kGetLocal: return ExecGetLocal(op); | |
| case VmOpType::kSetLocal: return ExecSetLocal(op); | |
| case VmOpType::kPopLocals: return ExecPopLocals(op); | |
| case VmOpType::kPushBreak: return ExecPushBreak(op); | |
| case VmOpType::kPopBreak: return ExecPopBreak(op); | |
| case VmOpType::kBreak: return ExecBreak(op); | |
| case VmOpType::kCallback0: return ExecCallback0(op); | |
| case VmOpType::kCallback1: return ExecCallback1(op); | |
| case VmOpType::kCallback2: return ExecCallback2(op); | |
| case VmOpType::kCallbackN: return ExecCallbackN(op); | |
| case VmOpType::kAsyncCallback0: return ExecAsyncCallback0(op); | |
| case VmOpType::kAsyncCallback1: return ExecAsyncCallback1(op); | |
| case VmOpType::kAsyncCallback2: return ExecAsyncCallback2(op); | |
| case VmOpType::kAsyncCallbackN: return ExecAsyncCallbackN(op); | |
| // clang-format on | |
| case VmOpType::kLast: | |
| FX_DCHECK(false); // Shouldn't reach. | |
| return Completion::kSync; | |
| } | |
| } | |
| Completion VmExecState::ExecError(const VmOp& op) { | |
| // The error is optional because this instruction is used both to throw explicit errors and to | |
| // indicate an uninitialized operation. | |
| if (std::holds_alternative<Err>(op.info)) | |
| return ReportError(std::get<Err>(op.info)); | |
| return ReportError("Invalid bytecode operation."); | |
| } | |
| Completion VmExecState::ExecUnary(const VmOp& op) { | |
| ExprValue param; | |
| if (Pop(¶m) == Completion::kError) | |
| return Completion::kError; | |
| auto cb_info = std::make_shared<CallbackInfo>(); | |
| EvalUnaryOperator(eval_context_, op.token, param, MakeContinueCallback(cb_info)); | |
| return cb_info->SynchronousDone(); | |
| } | |
| Completion VmExecState::ExecBinary(const VmOp& op) { | |
| // The "left" side expression on the binary operator will be pushed on the stack first, leaving | |
| // the "right" side at the top of the stack to pop first when we execute the operator. | |
| ExprValue right_param; | |
| if (Pop(&right_param) == Completion::kError) | |
| return Completion::kError; | |
| ExprValue left_param; | |
| if (Pop(&left_param) == Completion::kError) | |
| return Completion::kError; | |
| auto cb_info = std::make_shared<CallbackInfo>(); | |
| EvalBinaryOperator(eval_context_, left_param, op.token, right_param, | |
| MakeContinueCallback(cb_info)); | |
| return cb_info->SynchronousDone(); | |
| } | |
| Completion VmExecState::ExecExpandRef(const VmOp& op) { | |
| ExprValue param; | |
| if (Pop(¶m) == Completion::kError) | |
| return Completion::kError; | |
| // This is executed a lot. It may be worth checking if it's a reference in-place and otherwise | |
| // just continuing without all of the completion callback dance. | |
| auto cb_info = std::make_shared<CallbackInfo>(); | |
| EnsureResolveReference(eval_context_, std::move(param), MakeContinueCallback(cb_info)); | |
| return cb_info->SynchronousDone(); | |
| } | |
| Completion VmExecState::ExecDrop(const VmOp& op) { | |
| ExprValue popped; | |
| return Pop(&popped); | |
| } | |
| Completion VmExecState::ExecDup(const VmOp& op) { | |
| if (stack_.empty()) | |
| return ReportError("VM stack underflow in 'dup' operation."); | |
| stack_.push_back(stack_.back()); | |
| return Completion::kSync; | |
| } | |
| Completion VmExecState::ExecLiteral(const VmOp& op) { | |
| const auto& literal_info = std::get<VmOp::LiteralInfo>(op.info); | |
| Push(literal_info.value); | |
| return Completion::kSync; | |
| } | |
| Completion VmExecState::ExecJump(const VmOp& op) { | |
| const auto& jump_info = std::get<VmOp::JumpInfo>(op.info); | |
| FX_DCHECK(jump_info.dest != VmOp::kBadJumpDest); | |
| stream_index_ = jump_info.dest; | |
| return Completion::kSync; | |
| } | |
| Completion VmExecState::ExecJumpIfFalse(const VmOp& op) { | |
| ExprValue param; | |
| if (Pop(¶m) == Completion::kError) | |
| return Completion::kError; | |
| ErrOr<bool> result = CastNumericExprValueToBool(eval_context_, param); | |
| if (result.has_error()) | |
| return ReportError(result.err()); | |
| if (!result.value()) { | |
| // Take jump. | |
| const auto& jump_info = std::get<VmOp::JumpInfo>(op.info); | |
| FX_DCHECK(jump_info.dest != VmOp::kBadJumpDest); | |
| stream_index_ = jump_info.dest; | |
| } | |
| // Otherwise just continue at next instruction. | |
| return Completion::kSync; | |
| } | |
| Completion VmExecState::ExecGetLocal(const VmOp& op) { | |
| const auto& local_info = std::get<VmOp::LocalInfo>(op.info); | |
| if (local_info.slot >= locals_.size()) { | |
| // Assume the token blamed for this code is the variable name. | |
| return ReportError(fxl::StringPrintf("Bad local variable index %u when reading '%s'.", | |
| local_info.slot, op.token.value().c_str())); | |
| } | |
| if (!locals_[local_info.slot]) { | |
| return ReportError( | |
| fxl::StringPrintf("Reading uninitialized local variable '%s'.", op.token.value().c_str())); | |
| } | |
| Push(locals_[local_info.slot]->GetValue()); | |
| return Completion::kSync; | |
| } | |
| // This is NOT a type-safe assignment. This is normally only emitted by the parser when a local | |
| // variable is created. The "=" binary operator implementation will handle updates to it and do | |
| // the expected type-checking. | |
| Completion VmExecState::ExecSetLocal(const VmOp& op) { | |
| const auto& local_info = std::get<VmOp::LocalInfo>(op.info); | |
| if (local_info.slot > kMaxLocals) | |
| return ReportError(fxl::StringPrintf("Local variable index is too large: %u", local_info.slot)); | |
| if (locals_.size() <= local_info.slot) | |
| locals_.resize(local_info.slot + 1); | |
| ExprValue new_value; | |
| if (Pop(&new_value) == Completion::kError) | |
| return Completion::kError; | |
| if (locals_[local_info.slot]) { | |
| locals_[local_info.slot]->SetValue(std::move(new_value)); | |
| } else { | |
| locals_[local_info.slot] = fxl::MakeRefCounted<LocalExprValue>(std::move(new_value)); | |
| } | |
| return Completion::kSync; | |
| } | |
| Completion VmExecState::ExecPopLocals(const VmOp& op) { | |
| const auto& local_info = std::get<VmOp::LocalInfo>(op.info); | |
| if (locals_.size() > local_info.slot) | |
| locals_.resize(local_info.slot); | |
| return Completion::kSync; | |
| } | |
| Completion VmExecState::ExecPushBreak(const VmOp& op) { | |
| const auto& jump_info = std::get<VmOp::JumpInfo>(op.info); | |
| breaks_.push_back(BreakInfo{ | |
| .stack_size = stack_.size(), .local_stack_size = locals_.size(), .dest = jump_info.dest}); | |
| return Completion::kSync; | |
| } | |
| Completion VmExecState::ExecPopBreak(const VmOp& op) { | |
| if (breaks_.empty()) | |
| return ReportError("PopBreak opcode executed outside of a loop context."); | |
| breaks_.pop_back(); | |
| return Completion::kSync; | |
| } | |
| Completion VmExecState::ExecBreak(const VmOp& op) { | |
| if (breaks_.empty()) | |
| return ReportError("'break' opcode executed outside of a loop context."); | |
| const BreakInfo& info = breaks_.back(); | |
| // The stacks should never have shrunk within the scope of the break push/pop. | |
| if (stack_.size() < info.stack_size || locals_.size() < info.local_stack_size) | |
| return ReportError("Unexpected break stack state."); | |
| // Restore the state. | |
| stack_.resize(info.stack_size); | |
| locals_.resize(info.local_stack_size); | |
| // Jump to the given destination. | |
| FX_DCHECK(info.dest != VmOp::kBadJumpDest); | |
| stream_index_ = info.dest; | |
| return Completion::kSync; | |
| } | |
| Completion VmExecState::ExecCallback0(const VmOp& op) { | |
| const auto& cb = std::get<VmOp::Callback0>(op.info); | |
| ErrOrValue result = cb(eval_context_); | |
| if (result.has_error()) | |
| return ReportError(result.err()); | |
| Push(result.value()); | |
| return Completion::kSync; | |
| } | |
| Completion VmExecState::ExecCallback1(const VmOp& op) { | |
| const auto& cb = std::get<VmOp::Callback1>(op.info); | |
| ExprValue param; | |
| if (Pop(¶m) == Completion::kError) | |
| return Completion::kError; | |
| ErrOrValue result = cb(eval_context_, std::move(param)); | |
| if (result.has_error()) | |
| return ReportError(result.err()); | |
| Push(result.value()); | |
| return Completion::kSync; | |
| } | |
| Completion VmExecState::ExecCallback2(const VmOp& op) { | |
| const auto& cb = std::get<VmOp::Callback2>(op.info); | |
| ExprValue param2; | |
| if (Pop(¶m2) == Completion::kError) | |
| return Completion::kError; | |
| ExprValue param1; | |
| if (Pop(¶m1) == Completion::kError) | |
| return Completion::kError; | |
| ErrOrValue result = cb(eval_context_, std::move(param1), std::move(param2)); | |
| if (result.has_error()) | |
| return ReportError(result.err()); | |
| Push(result.value()); | |
| return Completion::kSync; | |
| } | |
| Completion VmExecState::ExecCallbackN(const VmOp& op) { | |
| const auto& info = std::get<VmOp::CallbackNInfo>(op.info); | |
| std::vector<ExprValue> params; | |
| params.resize(info.num_params); | |
| for (int i = info.num_params - 1; i >= 0; i--) { | |
| if (Pop(¶ms[i]) == Completion::kError) | |
| return Completion::kError; | |
| } | |
| ErrOrValue result = info.cb(eval_context_, std::move(params)); | |
| if (result.has_error()) | |
| return ReportError(result.err()); | |
| Push(result.value()); | |
| return Completion::kSync; | |
| } | |
| Completion VmExecState::ExecAsyncCallback0(const VmOp& op) { | |
| const auto& cb = std::get<VmOp::AsyncCallback0>(op.info); | |
| auto cb_info = std::make_shared<CallbackInfo>(); | |
| cb(eval_context_, MakeContinueCallback(cb_info)); | |
| return cb_info->SynchronousDone(); | |
| } | |
| Completion VmExecState::ExecAsyncCallback1(const VmOp& op) { | |
| const auto& cb = std::get<VmOp::AsyncCallback1>(op.info); | |
| ExprValue param; | |
| if (Pop(¶m) == Completion::kError) | |
| return Completion::kError; | |
| auto cb_info = std::make_shared<CallbackInfo>(); | |
| cb(eval_context_, std::move(param), MakeContinueCallback(cb_info)); | |
| return cb_info->SynchronousDone(); | |
| } | |
| Completion VmExecState::ExecAsyncCallback2(const VmOp& op) { | |
| const auto& cb = std::get<VmOp::AsyncCallback2>(op.info); | |
| ExprValue param2; | |
| if (Pop(¶m2) == Completion::kError) | |
| return Completion::kError; | |
| ExprValue param1; | |
| if (Pop(¶m1) == Completion::kError) | |
| return Completion::kError; | |
| auto cb_info = std::make_shared<CallbackInfo>(); | |
| cb(eval_context_, std::move(param1), std::move(param2), MakeContinueCallback(cb_info)); | |
| return cb_info->SynchronousDone(); | |
| } | |
| Completion VmExecState::ExecAsyncCallbackN(const VmOp& op) { | |
| const auto& info = std::get<VmOp::AsyncCallbackNInfo>(op.info); | |
| std::vector<ExprValue> params; | |
| params.resize(info.num_params); | |
| for (int i = info.num_params - 1; i >= 0; i--) { | |
| if (Pop(¶ms[i]) == Completion::kError) | |
| return Completion::kError; | |
| } | |
| auto cb_info = std::make_shared<CallbackInfo>(); | |
| info.cb(eval_context_, std::move(params), MakeContinueCallback(cb_info)); | |
| return cb_info->SynchronousDone(); | |
| } | |
| void VmExecState::Push(ExprValue v) { stack_.push_back(std::move(v)); } | |
| Completion VmExecState::Pop(ExprValue* popped) { | |
| if (stack_.empty()) { | |
| // TODO report source of error. | |
| return ReportError("Stack underflow at instruction " + std::to_string(stream_index_)); | |
| } | |
| *popped = std::move(stack_.back()); | |
| stack_.pop_back(); | |
| return Completion::kSync; | |
| } | |
| void VmExecState::ReportDone(ErrOrValue result) { | |
| cb_(std::move(result)); | |
| cb_ = {}; // Clear out to catch accidental re-use. | |
| } | |
| Completion VmExecState::ReportError(const std::string& msg) { return ReportError(Err(msg)); } | |
| Completion VmExecState::ReportError(const Err& err) { | |
| ReportDone(err); | |
| return Completion::kError; | |
| } | |
| EvalCallback VmExecState::MakeContinueCallback(std::shared_ptr<CallbackInfo> cb_info) { | |
| return [state = RefPtrTo(this), cb_info = std::move(cb_info)](ErrOrValue result) { | |
| if (result.has_error()) { | |
| cb_info->issued = Completion::kError; | |
| state->ReportDone(std::move(result)); | |
| return; | |
| } | |
| // Mark this as complete. | |
| cb_info->issued = cb_info->current; | |
| state->Push(result.take_value()); | |
| if (cb_info->current == Completion::kAsync) { | |
| // Need to explicitly continue evaluation. | |
| VmExecState::Exec(std::move(state)); | |
| } | |
| // In the synchronous case, the caller will just resume. | |
| }; | |
| } | |
| } // namespace | |
| void VmExec(const fxl::RefPtr<EvalContext>& eval_context, VmStream stream, EvalCallback cb) { | |
| VmExecState::Exec( | |
| fxl::MakeRefCounted<VmExecState>(eval_context, std::move(stream), std::move(cb))); | |
| } | |
| } // namespace zxdb |