nk 1.0.0
C++ Numerical Computing Runtime
Loading...
Searching...
No Matches
vm.hpp
1// include/vm.hpp
2#pragma once
3
4#include <nk/core/bytecode.hpp>
5#include <nk/core/callback_builtin.hpp>
6#include <nk/core/debugger.hpp>
7#include <nk/core/environment.hpp>
8#include <nk/value/value.hpp>
9
10#include <exception>
11#include <memory>
12#include <string>
13#include <unordered_map>
14#include <vector>
15
16namespace nk {
17
18class Engine;
19
20// Result of a dispatch loop iteration — Completed or Paused (for debugger)
21enum class ExecStatus : uint8_t { Completed, Paused };
22
23class VM
24{
25public:
26 explicit VM(Engine &engine);
27
28 // Legacy API: runs to completion, throws DebugStopException on pause
29 Value execute(const BytecodeChunk &chunk, const Value *args = nullptr, uint8_t nargs = 0);
30
31 // Debug-aware API: can return Paused (state preserved for resume)
32 // initialAction controls the debugger's step mode at entry:
33 // StepInto (default) — pause on the first source line
34 // Continue — run until a breakpoint is hit
35 ExecStatus startExecution(const BytecodeChunk &chunk,
36 const Value *args = nullptr,
37 uint8_t nargs = 0,
38 DebugAction initialAction = DebugAction::StepInto);
39 ExecStatus resumeExecution();
40 bool isPaused() const { return !frames_.empty(); }
41 Value takeResult() { return std::move(lastResult_); }
42
43 // Re-entrant call from C++ INTO a compiled chunk, returning all outputs.
44 // Used when a builtin/hook running mid-dispatch must run user code on the
45 // VM (function handles in cellfun/arrayfun, classdef methods/ctors invoked
46 // from C++) so the body runs on the VM rather than the TreeWalker. Saves
47 // the outer VM state, runs `chunk` as a fresh top-level frame to
48 // completion, harvests its return values, and restores the outer so the
49 // outer dispatch loop resumes seamlessly. ownerClass/isCtor set the new
50 // frame's class context; ctorSeed (constructor case) pre-binds the output
51 // variable. Breakpoints inside the callee fire; a debugger *pause* cannot
52 // suspend across the C++ boundary and surfaces as DebugStopException (same
53 // contract as the legacy execute()). See vm_callbacks_plan.md (P3).
54 std::vector<Value> callReentrant(const BytecodeChunk &chunk, Span<const Value> args,
55 size_t nargout,
56 const std::string &ownerClass = std::string(),
57 bool isCtor = false, const Value *ctorSeed = nullptr);
58
59 // Schedule a user-code function-handle callback as a pausable VM frame on
60 // behalf of a native continuation (state-machine callbacks — see
61 // callback_builtin.hpp). Resolves the handle (plain or closure cell, with
62 // captures appended) to its compiled chunk, pushes a frame, and attaches
63 // `cont` so the frame's return routes back to cont->step(). Returns false if
64 // the handle is not user code (caller should not have scheduled it). The
65 // caller (a VmContinuation::step) returns true after this so the dispatch
66 // loop runs the new frame.
67 bool pushCallbackFrame(const Value &handle, Span<const Value> args, size_t nargout,
68 std::shared_ptr<VmContinuation> cont);
69
70 struct PausedState;
71 std::unique_ptr<PausedState> savePausedState();
72 void restorePausedState(std::unique_ptr<PausedState> state);
73
74 // Point the VM at the workspace-scope function table. Single-map
75 // form is kept for tests / callers that don't run under a
76 // script scope; anything running user scripts should also pass a
77 // script-local bucket so `clear all` mid-call doesn't strand the
78 // surrounding script's local functions.
79 void setCompiledFuncs(const std::unordered_map<std::string, BytecodeChunk> *funcs)
80 {
81 compiledFuncs_ = funcs;
82 scriptLocalFuncs_ = nullptr;
83 }
84 void setCompiledFuncs(const std::unordered_map<std::string, BytecodeChunk> *funcs,
85 const std::unordered_map<std::string, BytecodeChunk> *scriptLocal)
86 {
87 compiledFuncs_ = funcs;
88 scriptLocalFuncs_ = scriptLocal;
89 }
90
91 // Unified lookup — tries the script-local bucket first, then
92 // workspace. Returns nullptr when the name isn't a compiled
93 // function (caller falls through to built-ins / external funcs).
94 const BytecodeChunk *findCompiledFunc(const std::string &name) const
95 {
96 if (scriptLocalFuncs_) {
97 auto it = scriptLocalFuncs_->find(name);
98 if (it != scriptLocalFuncs_->end())
99 return &it->second;
100 }
101 if (compiledFuncs_) {
102 auto it = compiledFuncs_->find(name);
103 if (it != compiledFuncs_->end())
104 return &it->second;
105 }
106 return nullptr;
107 }
108
109 // After execute(), get exported variables for environment sync
110 const std::vector<std::pair<std::string, Value>> &lastVarMap() const { return lastVarMap_; }
111 void clearLastVarMap() { lastVarMap_.clear(); }
112
113 // Set dynamic variables map on the current (paused) frame.
114 // The map is NOT owned by the VM — caller must keep it alive.
115 void setFrameDynVars(std::unordered_map<std::string, Value> *dv);
116
117 // Mutable view of the current (top) frame — used by DebugWorkspace to
118 // resolve register pointers by variable name.
120 {
121 const BytecodeChunk *chunk = nullptr;
122 Value *registers = nullptr;
123 bool wasReentrantEval = false;
124 };
125 FrameView currentFrameView()
126 {
127 if (frames_.empty())
128 return {};
129 auto &f = frames_.back();
130 return { f.chunk, f.R };
131 }
132
133 // Number of live frames at a pause (1 = top-level script only).
134 size_t frameCount() const { return frames_.size(); }
135
136 // Mutable view of a frame by depth from the top: 0 = current (deepest,
137 // where execution paused), increasing toward the base — the dbup/dbdown
138 // axis. Returns {} if out of range. Non-const registers so the debugger can
139 // edit a caller frame's variables, not just inspect them.
140 FrameView frameViewAt(size_t depthFromTop)
141 {
142 if (depthFromTop >= frames_.size())
143 return {};
144 auto &f = frames_[frames_.size() - 1 - depthFromTop];
145 return { f.chunk, f.R };
146 }
147
148 // Call depth: 0 at top-level, +1 per user function call
149 int callDepth() const { return std::max(0, static_cast<int>(frames_.size()) - 1); }
150
151 // The class declaring the currently-executing method/constructor frame
152 // (empty if the top frame is an ordinary function or there is no frame) —
153 // drives classdef member-access enforcement for VM-run bodies.
154 const std::string &currentMethodClass() const
155 {
156 static const std::string kEmpty;
157 return frames_.empty() ? kEmpty : frames_.back().ownerClass;
158 }
159 bool currentMethodIsCtor() const
160 {
161 return !frames_.empty() && frames_.back().isCtor;
162 }
163
164 void setMaxRecursionDepth(int d) { maxRecursion_ = d; }
165
166 // ── Frame walking for assignin / evalin / inputname ──
167 //
168 // Walk frames_ from back, treating frames_.size()-1 as "n=0" (the
169 // caller of the running builtin). Skips the script-level frame
170 // (index 0 = top-level, returns workspaceEnv there). Lazy-allocates
171 // frame.env for any user-function frame visited.
172 Environment *callerEnvAtDepth(int n);
173
174 // Variable write-through to the n-th caller's frame register if
175 // the name is in that frame's chunk.varMap. No-op otherwise. The
176 // env-side write is the caller's responsibility (engine.cpp).
177 void assignInCallerFrame(int n, const std::string &name, const Value &val);
178
179 // Scope-eval support (used by Engine::eval(src, scope) and the
180 // evalin / eval / run builtins). When non-null, the inner exec's
181 // top-level frame uses this env for currentCallEnv() instead of
182 // workspaceEnv — so imports and ctx.env-based writes go to scope.
183 // Saved/restored across re-entrant execute() boundaries via
184 // PausedState.
185 Environment *inheritedScope_ = nullptr;
186 // True while a re-entrant engine->eval()/external callback
187 // executes with the outer frames parked (savePausedState) — top-
188 // level-only errors like bare nargin must NOT fire there.
189 bool inReentrantEval_ = false;
190
191 // ── dbstop-if-error ──────────────────────────────────────
192 // When set by a debug session, an uncaught error pauses at the failing
193 // instruction (frames left intact) instead of unwinding, so the debugger
194 // can inspect state at the error point. Additionally gated on debugCtl()
195 // being live, so it never affects normal or console-eval execution.
196 // Resuming rethrows the stored exception (the error then propagates).
197 void setStopOnError(bool b) { stopOnError_ = b; }
198 bool atErrorPause() const { return atErrorPause_; }
199 const std::string &errorPauseMessage() const { return errorPauseMsg_; }
200
201 bool stopOnError_ = false;
202 bool atErrorPause_ = false;
203 std::exception_ptr pausedError_;
204 std::string errorPauseMsg_;
205
206 // Find a frame whose lazy env equals `env` (any depth) and write
207 // `val` into the register slot for `name` if name is in that
208 // frame's chunk.varMap. Used by Engine::syncVMToScope to push
209 // inner-eval results back into the outer caller's static slots.
210 void writeToFrameMatchingEnv(Environment *env, const std::string &name,
211 const Value &val);
212
213 // Caller arg names of the innermost user-function frame (the
214 // function that contains the running builtin's call site). Empty
215 // when at top-level. Used by inputname(k).
216 const std::vector<std::string> &currentFrameCallerArgNames() const
217 {
218 static const std::vector<std::string> kEmpty;
219 if (frames_.size() < 2) return kEmpty;
220 return frames_.back().callerArgNames;
221 }
222
223 // ── Scoped-eval read-side support ──
224 //
225 // Build a snapshot of caller-frame variables (statically allocated
226 // R[reg] entries plus any per-frame dynVars overlay) so that an
227 // inner scoped eval can resolve bare identifiers that exist only
228 // in the caller's registers. Empty if no matching frame.
229 std::unordered_map<std::string, Value>
230 snapshotFrameVars(Environment *frameEnv);
231
232 // Plug `dv` in as the next-pushed top-level frame's dynVars. The
233 // pointer must outlive that frame's execution. Used by
234 // Engine::eval(src, scope) to expose caller's vars to inner exec.
235 void setNextFrameDynVars(std::unordered_map<std::string, Value> *dv)
236 {
237 nextFrameDynVars_ = dv;
238 }
239
240private:
241 Engine &engine_;
242 std::unordered_map<std::string, Value> *nextFrameDynVars_ = nullptr;
243 const std::unordered_map<std::string, BytecodeChunk> *compiledFuncs_ = nullptr;
244 const std::unordered_map<std::string, BytecodeChunk> *scriptLocalFuncs_ = nullptr;
245
246 // Register stack — Value directly (16 bytes each)
247 static constexpr size_t kRegStackSize = 256 * 50;
248 std::vector<Value> regStack_;
249 size_t regStackTop_ = 0;
250 Value *R_ = nullptr;
251
252 // ── Explicit call frame stack (non-recursive VM) ────────
253 struct CallFrame
254 {
255 const BytecodeChunk *chunk = nullptr;
256 const Instruction *ip = nullptr; // saved instruction pointer
257 Value *R = nullptr; // register base for this frame
258 size_t regBase = 0; // offset in regStack_
259 uint8_t nregs = 0; // register count
260 size_t forStackBase = 0; // forStack_.size() at frame entry
261 size_t tryStackBase = 0; // tryStack_.size() at frame entry
262 uint8_t destReg = 0; // caller register for return value
263 bool isMultiReturn = false; // CALL_MULTI?
264 uint8_t outBase = 0; // multi-return: output base register
265 uint8_t nout = 0; // multi-return: output count
266 size_t nargout = 1;
267 // classdef method/ctor frames only: the declaring class (drives
268 // member-access enforcement; read on demand, never a separate stack,
269 // so it is naturally exception-safe). Empty for ordinary functions.
270 std::string ownerClass;
271 bool isCtor = false;
272
273 // Dynamic variables — fallback for variables not in static varMap.
274 // Used by debug eval to inject variables created at breakpoints.
275 std::unordered_map<std::string, Value> *dynVars = nullptr;
276
277 // Lazy per-frame env for scope-local imports / env-resident
278 // builtin state. parent = workspaceEnv. Allocated on first
279 // currentCallEnv() inside a user function; nullptr at top level.
280 std::unique_ptr<Environment> env;
281
282 // Names of the bare-identifier args at the caller's CALL site,
283 // copied from caller's chunk.callSiteArgNames at pushCallFrame.
284 // inputname(k) reads element [k-1] from this vector. Empty
285 // string for non-identifier args, empty vector for top-level
286 // and non-tracked call sites.
287 std::vector<std::string> callerArgNames;
288
289 // State-machine callbacks: when set, this frame is a callback driven by
290 // a native continuation (cellfun/arrayfun/…). On return, popCallFrame
291 // routes the result to cont->step(...) instead of a caller register, so
292 // the higher-order builtin advances without a C++ driver loop and each
293 // callback stays a pausable VM frame. Null for ordinary frames.
294 std::shared_ptr<VmContinuation> cont;
295 };
296 std::vector<CallFrame> frames_;
297 Value lastResult_;
298
299 // For-loop state
300 struct ForState
301 {
302 Value range;
303 const double *data = nullptr;
304 const void *rawData = nullptr; // for char/logical
305 ValueType rangeType = ValueType::DOUBLE;
306 size_t index = 0;
307 size_t count = 0;
308 size_t rows = 0;
309 // Lazy colon range: `for v = start:stop` and `for v = start:step:stop`
310 // skip the COLON allocation by storing scalars here. forSetVar checks
311 // lazy first; when true, range/data/rawData are unused. Only valid
312 // when rangeType == ValueType::DOUBLE and rows == 1 (scalar each iter).
313 bool lazy = false;
314 double lazyStart = 0.0;
315 double lazyStep = 1.0;
316 };
317 std::vector<ForState> forStack_;
318
319 // Try/catch handler stack
320 struct TryHandler
321 {
322 const Instruction *catchIp; // where to jump on exception
323 uint8_t exReg; // register for exception struct
324 size_t forStackSize; // forStack_ size at TRY_BEGIN (restore on catch)
325 size_t frameIndex; // index in frames_ that owns this handler
326 };
327 std::vector<TryHandler> tryStack_;
328
329public:
330 // Defined here (after private types) so callers can destroy unique_ptr<PausedState>
332 {
333 std::vector<CallFrame> frames;
334 std::vector<ForState> forStack;
335 std::vector<TryHandler> tryStack;
336 std::vector<Value> regSnapshot;
337 size_t regStackTop = 0;
338 Value lastResult;
339 // BUG #3: outer dispatch loop holds `auto &resolvedFuncs =
340 // chunkCallCache_[frame.chunk];` (a reference into the map's
341 // node memory). startExecution() on the inner re-entry clears
342 // chunkCallCache_, which destroys that node and dangles the
343 // reference. unordered_map move preserves node addresses, so
344 // saving here + restoring on exit keeps the outer's reference
345 // pointing at live memory.
346 std::unordered_map<const BytecodeChunk *,
347 std::vector<const BytecodeChunk *>>
348 chunkCallCache;
349 bool wasReentrantEval = false;
350 };
351private:
352
353 int maxRecursion_ = 200;
354
355 // Per-chunk call target cache
356 std::unordered_map<const BytecodeChunk *, std::vector<const BytecodeChunk *>> chunkCallCache_;
357
358 // Exported variables after execution
359 std::vector<std::pair<std::string, Value>> lastVarMap_;
360
361 // Multi-return buffer: RET_MULTI stores values here instead of cell-packing
362 static constexpr size_t kMaxReturns = 16;
363 Value returnBuf_[kMaxReturns];
364 uint8_t returnCount_ = 0;
365
366 // ── Core dispatch loop (non-recursive) ──────────────────
367 ExecStatus dispatchLoop();
368
369 // Frame management
370 // ctorSeed (when non-null) pre-binds the new frame's first return variable
371 // to *ctorSeed before the body runs — used to seed a classdef constructor's
372 // output variable with the default instance (MATLAB constructor semantics).
373 void pushCallFrame(const BytecodeChunk &funcChunk, const Value *args, uint8_t nargs,
374 uint8_t destReg, size_t nargout,
375 bool isMulti = false, uint8_t outBase = 0, uint8_t nout = 0,
376 const std::string &ownerClass = std::string(), bool isCtor = false,
377 const Value *ctorSeed = nullptr);
378 void popCallFrame(Value retVal);
379 // Begin driving a native continuation: runs its first step (no prior
380 // result). Returns true if it pushed a callback frame (caller must
381 // `goto enter_frame`), false if it finished synchronously (e.g. empty input
382 // — the result is already written to its destination).
383 bool startContinuation(std::shared_ptr<VmContinuation> cont);
384
385 // Returns the Environment a builtin call should see for ctx.env.
386 // Top-level (frames_.size() <= 1): workspaceEnv directly.
387 // Inside a user function: lazily-allocated frame.env, parent =
388 // workspaceEnv. Function-local imports / state live in this env and
389 // are dropped when the frame is popped.
390 Environment *currentCallEnv();
391
392 // Export top-level variables to lastVarMap_ and globalsEnv
393 void exportTopLevelVariables();
394
395 // Exception helpers for dispatch loop (frame-aware)
396 bool dispatchTryCatch(const char *msg, const char *identifier);
397 [[noreturn]] void enrichAndThrow(const std::exception &ex,
398 const Instruction *ip,
399 const BytecodeChunk &chunk);
400
401 // Helpers
402 void forSetVar(Value &varReg, const ForState &fs);
403
404 // Dispatch helpers (extracted from dispatchLoop for readability)
405 Value binarySlowPath(OpCode op, const Value &lhs, const Value &rhs);
406 Value unarySlowPath(OpCode op, const Value &operand);
407 // Object operator overloads as same-stack VM frames (P4 refinement):
408 // if an operand is an object whose class defines the operator method,
409 // push a frame for its body (pausable, no save/restore) and return true —
410 // the caller must `goto enter_frame`. Returns false for the numeric path
411 // and for object-no-overload (left to binary/unarySlowPath, which errors
412 // or falls back to callReentrant for an uncompilable body). Binary args
413 // are [lhs, rhs]; unary args are [operand].
414 bool tryBinaryOpFrame(uint8_t dst, OpCode op, uint8_t lhsReg, uint8_t rhsReg,
415 CallFrame &frame, const Instruction *ip);
416 bool tryUnaryOpFrame(uint8_t dst, OpCode op, uint8_t operandReg,
417 CallFrame &frame, const Instruction *ip);
418 // classdef subsref/subsasgn overloads as same-stack VM frames (P4
419 // refinement): if R[selfReg]/R[objReg] is an object whose class defines the
420 // overload, push a frame for its body (pausable) and return true (caller
421 // `goto enter_frame`). subsref returns one value into `dst`; subsasgn
422 // returns the modified object into `objReg` (value class) / mutates shared
423 // state (handle). `idx` is the flat subscript list; `idxAndVal` is
424 // [subscripts…, value]. Returns false → caller's existing tryObject* /
425 // builtin-indexing slow path.
426 bool tryObjectSubsrefFrame(uint8_t dst, uint8_t selfReg, Span<const Value> idx,
427 CallFrame &frame, const Instruction *ip);
428 // Same push for a receiver held by value (a temporary — e.g. a struct
429 // field or property result inside execCallIndirectTarget), which does
430 // not live in any register.
431 bool tryObjectSubsrefFrameObj(const Value &self, uint8_t dst, Span<const Value> idx,
432 CallFrame &frame, const Instruction *ip);
433 bool tryObjectSubsasgnFrame(uint8_t objReg, Span<const Value> idxAndVal,
434 CallFrame &frame, const Instruction *ip);
435 void execCallBuiltin(const Instruction &I, Value *R);
436 bool execCallIndirect(const Instruction &I, Value *R,
437 CallFrame &frame, const Instruction *ip);
438 bool execCallIndirectTarget(const Value &target, uint8_t dstReg, uint8_t argBase, uint8_t na,
439 Value *R, CallFrame &frame, const Instruction *ip);
440 bool execCallIndirectSpan(const Value &target, uint8_t dstReg,
441 const Value *AB, uint8_t na, Value *R, CallFrame &frame,
442 const Instruction *ip);
443 // Multi-output indirect (handle-variable) call: `[a,b] = h(x)`. Returns
444 // true if it pushed a VM frame (caller re-enters the dispatch loop).
445 bool execCallIndirectMulti(const Instruction &I, Value *R,
446 CallFrame &frame, const Instruction *ip);
447 void execIndirectIndex(const Instruction &I, Value *R);
448 void execIndirectIndexTarget(const Value &target, uint8_t dstReg, uint8_t argBase, uint8_t na,
449 Value *R);
450 void execIndirectIndexSpan(const Value &mv, uint8_t dstReg, const Value *AB,
451 uint8_t na, Value *R);
452 void execDisplay(const Instruction &I, Value *R, const BytecodeChunk &chunk);
453 void execWho(const Instruction &I, Value *R, const BytecodeChunk &chunk);
454 void execWhos(const Instruction &I, Value *R, const BytecodeChunk &chunk);
455
456 // ── Debugger ────────────────────────────────────────────
457 DebugController *debugCtl();
458};
459
460} // namespace nk
Core MATLAB-compatible multidimensional array and scalar value type.
Definition value.hpp:61