nk 1.0.0
C++ Numerical Computing Runtime
Loading...
Searching...
No Matches
engine.hpp
1// include/engine.hpp
2#pragma once
3
4#include <nk/core/debugger.hpp>
6#include <nk/value/object.hpp>
7#include <nk/core/types.hpp>
8#include <nk/core/fusion_rule.hpp>
9#include <nk/fs/vfs.hpp>
10#include <nk/fs/fs_context.hpp>
11#include <nk/ops/rng_context.hpp>
12#include <nk/core/vm.hpp>
13
14#include <atomic>
15#include <memory>
16#include <memory_resource>
17#include <string>
18#include <unordered_map>
19#include <unordered_set>
20#include <vector>
21
22namespace nk {
23
24class TreeWalker;
25class VM;
26class Compiler;
27class DebugFacade; // grouped debugger handle (engine.debug()); defined below
28struct ClassDefDesc; // user-classdef descriptor; full type in engine.cpp
29struct CallbackBuiltin; // higher-order builtin driver; callback_builtin.hpp
30
31class Engine
32{
33public:
34 // Default ctor: uses std::pmr::get_default_resource() for all allocations.
35 Engine();
36 // Custom heap: caller-supplied memory_resource. Must outlive the Engine
37 // and every Value/DataBuffer it produces. Pass a subclass of
38 // std::pmr::memory_resource (or one of std::pmr's built-in resources).
39 explicit Engine(std::pmr::memory_resource *mr);
40 ~Engine();
41
42 Engine(const Engine &) = delete;
43 Engine &operator=(const Engine &) = delete;
44 Engine(Engine &&) = delete;
45 Engine &operator=(Engine &&) = delete;
46
47 // The memory_resource used for persistent (output-Value) allocations.
48 // Internal helpers and ScratchArena upstream-spill flow through here.
49 std::pmr::memory_resource *resource() const noexcept { return mr_; }
50
51 void registerBinaryOp(const std::string &op, BinaryOpFunc func);
52 void registerUnaryOp(const std::string &op, UnaryOpFunc func);
53
54 // Register a builtin function. Two forms:
55 // 1-arg: registers in core (flat namespace, no prefix). The full
56 // name in the registry equals `name`.
57 // 3-arg: registers under a namespace. `ns` is dot-separated
58 // (e.g. "signal.transforms"). The full name in the
59 // registry equals "ns.name". `ns=""` is equivalent to the
60 // 1-arg form.
61 // Both forms throw std::runtime_error on duplicate full names.
62 // See namespace_design.md for the rules.
63 void registerFunction(const std::string &name, ExternalFunc func);
64 void registerFunction(const std::string &ns,
65 const std::string &name,
66 ExternalFunc func);
67
68 // ── Class registry (object model — see object_model.md) ──────
69 // Register a builtin (C++-backed) class. Later, user classdef
70 // populates the same registry via an adapter. Throws on duplicate.
71 void registerClass(BuiltinClass cls);
72 // Look up a registered class by name (e.g. "containers.Map"), or
73 // nullptr. Used by constructor / method / property dispatch.
74 const BuiltinClass *findClass(const std::string &name) const;
75
76 // ── State-machine callbacks (callback_builtin.hpp, vm_callbacks_plan.md) ──
77 // Register a higher-order builtin (cellfun/arrayfun/…) that can drive its
78 // user-code callbacks as pausable VM frames. Registered alongside the
79 // ordinary synchronous external registration; the VM consults this first and
80 // falls back to the synchronous builtin when the driver declines (builtin
81 // handle / unsupported arg form). Returns the registered driver or nullptr.
82 void registerCallbackBuiltin(const std::string &name, std::shared_ptr<CallbackBuiltin> cb);
83 CallbackBuiltin *callbackBuiltin(const std::string &name) const;
84 // True if `handle` (plain handle or {handle, captures…} closure cell) names
85 // a USER function — i.e. there is a compiled body to step through — rather
86 // than a builtin.
87 bool isUserCodeHandle(const Value &handle) const;
88 // Register every top-level `function` in `src` as a PERSISTENT user
89 // function (userFuncs_ + VM compiled table) — the same path m-file loading
90 // uses, so they survive `clear` and work on both backends. Used to install
91 // `.m`-implemented builtins (e.g. fzero) whose callbacks must be pausable:
92 // the `.m` body's f-calls compile to ordinary VM frames, debuggable for
93 // free, no fiber / state machine. See vm_callbacks_plan.md.
94 // The source text is retained and replayed by reinstallBuiltinSources()
95 // after `clear all/classes/functions` — MATLAB transparently reloads
96 // cleared functions from disk; these have no disk backing, so the engine
97 // re-registers them instead (bugs/opened/runtime/clear-all-wipes-builtin-msources.md).
98 void registerBuiltinMSource(const std::string &src);
99 // Same contract for startup `classdef` sources (e.g. the inline class):
100 // parses and registers each top-level CLASSDEF_DEF via registerClassDef —
101 // the identical call the compiler makes for a classdef statement — and
102 // retains the text for the same post-clear replay.
103 void registerBuiltinClassSource(const std::string &src);
104 // Replay every retained startup registration (m-source functions +
105 // classdefs). Parser/registry work only — no VM run — so it is safe to
106 // call from inside the `clear` external while a chunk is executing.
107 void reinstallBuiltinSources();
108
109 // Register a user `classdef` (parsed CLASSDEF_DEF node) as a BuiltinClass
110 // via the adapter: generic property get/set over ObjectState.props,
111 // default-init + user constructor on construct, and method hooks that
112 // run the method bodies. Re-running a `classdef` for an already-registered
113 // class REPLACES it wholesale (see unregisterClassDef) so REPL / IDE edits
114 // take effect. `qualifiedName` (e.g. "geo.Vec") names a class loaded from a
115 // `+pkg/Name.m` file: the source node carries only the leaf (`Vec`), so the
116 // package qualification — which lives in the path — is threaded in here and
117 // becomes the registry identity (class()/isa()/registry keys all use it).
118 // Empty (the default) → use the node's own name (inline / unpackaged).
119 void registerClassDef(const ASTNode *classdef, const std::string &qualifiedName = "");
120 // `clear classes` / `clear all`: remove every USER classdef so the next
121 // reference re-loads (file classes) or errors as undefined (inline ones).
122 // Built-in classes (containers.Map, …) are not in classDefs_ and survive.
123 void clearClassDefs();
124 // Run a classdef method body (args already include `self` first) /
125 // constructor body (with `obj` seeded to the default instance) on the
126 // TreeWalker, regardless of the active backend.
127 std::vector<Value> invokeClassMethod(const UserFunction &uf, Span<const Value> args,
128 size_t nout);
129 Value invokeClassCtor(const UserFunction &ctor, const Value &seed,
130 Span<const Value> args);
131 // Compile a classdef method/constructor body into the VM's global compiled
132 // table (idempotent), returning its chunk — so the VM can run the body as
133 // a native frame. See vm_callbacks_plan.md.
134 const BytecodeChunk *ensureClassMethodChunk(const UserFunction &uf);
135 // Enforce a classdef method's declared access from the current context
136 // (no-op for a public method). Used by the VM frame-dispatch path, which
137 // bypasses the C++ method hook that would otherwise run the check.
138 void enforceMethodAccess(const std::string &className, const std::string &method);
139 // A classdef property's `get.Prop` / `set.Prop` accessor UserFunction, or
140 // nullptr when the property has none (plain stored property) / the class is
141 // not a classdef. Used by the VM FIELD_GET/FIELD_SET opcode handlers to run
142 // an accessor body as a same-stack VM frame (pausable) instead of the C++
143 // propGet/propSet hook. enforceProp{Get,Set}Access enforces the property's
144 // GetAccess/SetAccess from the current context before the accessor runs
145 // (the access half that the propGet/propSet hook would otherwise perform).
146 const UserFunction *classGetter(const std::string &className,
147 const std::string &prop) const;
148 const UserFunction *classSetter(const std::string &className,
149 const std::string &prop) const;
150 void enforcePropGetAccess(const std::string &className, const std::string &prop);
151 void enforcePropSetAccess(const std::string &className, const std::string &prop);
152 // Resolve a classdef operator overload (binary / unary) to its compiled
153 // method chunk for an in-bytecode VM frame-push. Returns nullptr — caller
154 // uses the numeric fast path or the C++ slow path (`tryObject*Op`) — when no
155 // operand is an object, the class has no such overload, the overload is a
156 // synthetic non-UserFunction (enum eq/ne), or the body can't VM-compile. On
157 // success sets ownerClassOut to the dominant object's class and enforces the
158 // operator method's access. The method's parameters ARE the operands:
159 // binary args = [lhs, rhs], unary args = [operand]. P4, vm_callbacks_plan.md.
160 const BytecodeChunk *resolveBinaryOpChunk(const std::string &op, const Value &lhs,
161 const Value &rhs, std::string &ownerClassOut);
162 const BytecodeChunk *resolveUnaryOpChunk(const std::string &op, const Value &operand,
163 std::string &ownerClassOut);
164 // Resolve a classdef `subsref`/`subsasgn` overload to its compiled method
165 // chunk for an in-bytecode VM frame-push, marshaling the call args (subsref:
166 // [self, substruct]; subsasgn: [self, substruct, value]). `idx` is the flat
167 // subscript list; for subsasgn `idxAndVal` is [subscripts…, value] (value
168 // last). Returns nullptr — caller uses the C++ slow path / builtin
169 // indexing — when the class has no such overload or the body can't compile.
170 // On success sets ownerClassOut to the object's class. P4, vm_callbacks_plan.md.
171 const BytecodeChunk *resolveSubsrefChunk(const Value &self, Span<const Value> idx,
172 std::string &ownerClassOut,
173 std::vector<Value> &argsOut);
174 const BytecodeChunk *resolveSubsasgnChunk(const Value &self, Span<const Value> idxAndVal,
175 std::string &ownerClassOut,
176 std::vector<Value> &argsOut);
177 // Superclass-qualified calls from inside a classdef body
178 // (`obj@Base(args)` / `method@Base(obj, args)`). superConstruct runs
179 // Base's constructor with `seed` as the partially-built object and
180 // returns the base-initialised object; superMethod runs Base's named
181 // method (args already include `self` first). Both throw if Base is not
182 // a registered classdef. When Base defines no constructor, superConstruct
183 // returns `seed` unchanged (a base with only default property init).
184 Value superConstruct(const std::string &base, const Value &seed,
185 Span<const Value> args);
186 std::vector<Value> superMethod(const std::string &base, const std::string &method,
187 Span<const Value> args, size_t nout);
188 // classdef member-access context (private/protected/immutable). Push the
189 // running method's/constructor's class on entry, pop on exit (RAII at the
190 // call sites). `classCtxAllows` answers "may code in the current context
191 // touch a member declared in `declClass`?": private (`privateOnly=true`)
192 // needs an exact class match; protected also admits subclasses of
193 // `declClass`. `classCtxInCtorOf` is true when the top frame is a
194 // constructor of `declClass` (used to gate `SetAccess = immutable`).
195 void pushClassCtx(std::string className, bool isCtor);
196 void popClassCtx();
197 bool classCtxAllows(const std::string &declClass, bool privateOnly) const;
198 bool classCtxInCtorOf(const std::string &declClass) const;
199 // Construct an object enforcing a non-public constructor's access against
200 // the current context (private ctor → only from the declaring class;
201 // protected → also subclasses). Used at the user-facing `ClassName(args)`
202 // call sites; internal default-fill calls `cls->construct` directly so
203 // object-array growth is never blocked. A public ctor (or a non-classdef
204 // BuiltinClass) passes straight through.
205 Value constructChecked(const BuiltinClass *cls, Span<const Value> args, CallContext &ctx);
206 // Build a default-initialised instance of a classdef (abstract-class check
207 // + every property at its declared default), WITHOUT running the user
208 // constructor. This is the "seed" the constructor body receives bound to
209 // its output variable. Throws if `className` is not a classdef.
210 Value makeDefaultInstance(const std::string &className);
211 // The classdef's user-defined constructor (the method named like the
212 // class), or nullptr when the class declares none / is not a classdef.
213 const UserFunction *classCtor(const std::string &className) const;
214 // Enforce a non-public constructor's access against the current context
215 // (the access half of constructChecked, without building the object). Used
216 // by the VM ctor-frame path before pushing the constructor frame. No-op for
217 // a public ctor or a non-classdef BuiltinClass.
218 void enforceCtorAccess(const std::string &className);
219 // MATLAB-style display text for an OBJECT value. `name` empty →
220 // bare body (disp); otherwise the `name =\n\n<body>\n` form.
221 std::string formatObjectDisplay(const std::string &name, const Value &obj) const;
222 // Emit an OBJECT value to the engine output, honouring a class-defined
223 // `display` (owns the whole output) or `disp` (owns the body; the default
224 // `name =` header is added when `name` is non-empty) method on a scalar
225 // object. Falls back to formatObjectDisplay otherwise. Shared by both
226 // backends' display paths.
227 void displayObject(const std::string &name, const Value &obj);
228 // Operator overloading: when `lhs` or `rhs` is an OBJECT, dispatch the
229 // binary operator `op` (the source token, e.g. "+", ".*", "==") to the
230 // dominant object's class `ops` entry (MATLAB names: plus/times/eq/…).
231 // The hook receives self = the dominant object and args = {lhs, rhs}.
232 // Returns true (out set) when handled; false when neither side is an
233 // object. Throws the MATLAB "Undefined operator … for type …" error
234 // when an object operand has no matching overload. Shared by both
235 // engines' binary-op slow paths.
236 bool tryObjectBinaryOp(const std::string &op, const Value &lhs, const Value &rhs,
237 Environment *env, Value &out);
238 // Unary counterpart: dispatch `op` ("-", "~", "'", ".'") to the
239 // operand object's class `ops` (uminus/not/ctranspose/transpose). The
240 // hook receives self = the operand and no args. Returns true (out set)
241 // when handled; false when the operand is not an object. Throws the
242 // MATLAB "Undefined operator" error when an object has no overload.
243 bool tryObjectUnaryOp(const std::string &op, const Value &operand,
244 Environment *env, Value &out);
245 // Value-level operator application with object-overload dispatch — the
246 // counterpart to the AST (TreeWalker) and bytecode (VM) operator paths for
247 // callers that hold Values but no AST node / bytecode operands: the codegen
248 // Value-ABI bridge (nk_codegen_rt's Dynamic tier) and feval of an operator
249 // handle. Mirrors the VM slow path exactly: tryObject*Op first (overloads),
250 // then the registered numeric op; throws "Undefined … operator" when
251 // neither applies. `op` is the source token ("+", ".*", "==", "-", "~",
252 // "'", …). Short-circuit "&&"/"||" are NOT handled here (they are control
253 // flow, lowered by the caller) — pass an eager logical op instead.
254 Value applyBinaryOp(const std::string &op, const Value &lhs, const Value &rhs);
255 Value applyUnaryOp(const std::string &op, const Value &operand);
256 // Builtin object-array slice store: dst(subscripts) = val, default-
257 // filling grown slots via the class's no-arg constructor. `perDim` holds
258 // the resolved 0-based index list per subscript — one list → linear
259 // store (shape-preserving / vector grow), several → N-D store (grid
260 // grow). `val` is a scalar object (broadcast) or matches the target
261 // count. Callers first verify dst is a valid target (empty/unset, or a
262 // same-class object array). Centralises the store boilerplate shared by
263 // TreeWalker and every VM INDEX_SET* opcode.
264 void objectStoreSlice(Value &dst, const std::vector<std::vector<size_t>> &perDim,
265 const Value &val, Environment *env);
266 // Read counterpart: if `self`'s class defines a custom subsref, dispatch
267 // it with `args` and return true (result in `out`); otherwise return
268 // false so the caller runs the builtin object-array indexing path.
269 // Centralises the subsref check + call shared by the VM index opcodes.
270 bool tryObjectSubsref(Value &self, Span<const Value> args, std::size_t nargout,
271 Value &out, Environment *env);
272
273 // ── Namespace introspection (used by resolver — Phase 6) ──────
274
275 // Top-level namespaces in registration order. `compat` shows up
276 // here once the first MATLAB-mirror library calls
277 // registerFunction("compat", …).
278 const std::vector<std::string> &namespaces() const { return namespaceOrder_; }
279
280 // Multimap from leaf name (last segment) to full name
281 // (incl. all promotions, aliases). Used by the resolver to
282 // implement `import x.*` wildcard lookups in O(1).
283 const std::unordered_multimap<std::string, std::string> &shortNameIndex() const
284 {
285 return shortNameIndex_;
286 }
287
288 // Runtime resolver: given a name (possibly short), looks it up considering
289 // the active imports in `env` (and its parent chain). Returns nullptr
290 // if not resolvable. `env` may be nullptr — then only direct (core or
291 // fully-qualified) lookup is performed.
292 //
293 // Resolution order (per namespace_design.md Section 3):
294 // 1. Direct: externalFuncs_.find(name) (core / already qualified)
295 // 2. Walk env→parent chain, for each scope's active imports:
296 // - wildcard `import a.b.*` → try "a.b.<name>"
297 // - explicit `import a.b.c` → if c == name, try "a.b.c"
298 // - alias `import a.b as x` → only resolves x.<name> (handled
299 // in dotted-name lookup, not here)
300 const ExternalFunc *findExternal(const std::string &name,
301 const Environment *env) const;
302
303 // Returns the full qualified name for a bare leaf name (e.g. "fft" →
304 // "signal.transforms.fft") — the namespace source that the bare-name
305 // resolver found. Empty when the name doesn't resolve. Used by `which`.
306 std::string bareNameSource(const std::string &name) const;
307
308 // Compile-time check: is `name` a leaf name registered anywhere in the
309 // namespace tree? Used by the compiler to decide whether an identifier
310 // is a callable. Does NOT consider imports — that's a runtime concept.
311 bool isKnownLeafName(const std::string &name) const
312 {
313 return externalFuncs_.find(name) != externalFuncs_.end()
314 || shortNameIndex_.find(name) != shortNameIndex_.end();
315 }
316
317 void setVariable(const std::string &name, Value val);
318 Value *getVariable(const std::string &name);
319
320 // Execute code — uses current backend (TreeWalker by default).
321 // When `suppressTopLevelDisplay==true`, every top-level statement
322 // in `code` is silenced (ans / lhs displays skipped). Side-effect
323 // prints inside called functions (disp, fprintf, ...) still fire.
324 // Used by the `eval` builtin when the outer caller captures the
325 // return value — matches MATLAB:
326 // r = eval('a + b'); % no inner ans display
327 // eval('a + b'); % inner ans display proceeds normally
328 Value eval(const std::string &code, bool suppressTopLevelDisplay = false);
329
330 // Reactive seam (Play B2): execute ONE pre-parsed top-level
331 // statement through the same per-statement path (rewrite +
332 // runOneChunk) the VM script-split uses. ReactiveSession is the
333 // only intended caller; the statement must outlive the call and
334 // belong to a program parsed by THIS engine's parser.
335 Value evalStatement(const ASTNode *stmt,
336 std::shared_ptr<const std::string> src);
337 // See engine.cpp: rewrites variable-headed glued -// COMMAND_CALLs
338 // into binary expressions BEFORE compiling (MATLAB-probed rule).
339 void rewriteVarHeadedCommands(ASTNode *root);
340
341 // Scoped variant: top-level imports and variable assignments inside
342 // `code` are routed to `scope` instead of workspaceEnv. Used by
343 // `eval`/`run` builtins called from inside a function, and by
344 // `evalin`. scope=nullptr or scope==&workspaceEnv() falls back to
345 // the no-scope overload (REPL-style persistence).
346 Value eval(const std::string &code, Environment *scope,
347 bool suppressTopLevelDisplay = false);
348
349 // Safe execution — never throws, returns result + error diagnostics
350 struct EvalResult {
351 Value value;
352 bool ok = true;
353 bool debugStop = false;
354 std::string errorMessage;
355 int errorLine = 0;
356 int errorCol = 0;
357 std::string errorFunc;
358 std::string errorContext; // e.g. "in call to 'sin'"
359 };
360 EvalResult evalSafe(const std::string &code);
361
362 // Backend selection
363 enum class Backend { VM, TreeWalker };
364 void setBackend(Backend b) { backend_ = b; }
365 Backend backend() const { return backend_; }
366
367 // Element-wise fusion (see fusion_rule.hpp). The standard library
368 // registers concrete rules; both backends consult them when enabled.
369 // Disabled instantly (no rebuild) via setFusion(false) / NK_FUSE=0.
370 void addFusionRule(FusionRule rule) { fusionRules_.push_back(std::move(rule)); }
371 const std::vector<FusionRule> &fusionRules() const { return fusionRules_; }
372 void setFusion(bool on) { fusionEnabled_ = on; }
373 bool fusionEnabled() const { return fusionEnabled_ && !fusionRules_.empty(); }
374 // Fusion-fire telemetry: both backends call noteFusionHit() each time a rule's
375 // execute() returns true. Lets tests PROVE a kernel actually fires (a parity
376 // test alone can't — a silent decline gives the same fused==unfused result).
377 void noteFusionHit() { ++fusionHits_; }
378 size_t fusionHits() const { return fusionHits_; }
379 void resetFusionHits() { fusionHits_ = 0; }
380
381 using OutputFunc = std::function<void(const std::string &)>;
382 void setOutputFunc(OutputFunc f);
383 OutputFunc outputFunc() const { return outputFunc_; }
384 // Injectable line source for `input()` (the setOutputFunc pattern):
385 // the provider returns ONE entered line (no trailing newline). When
386 // unset, the input builtin reads stdin — the CLI path. Tests feed
387 // canned lines; the WASM/JS shell pre-loads piped stdin through the
388 // bindings (see repl_bindings); the IDE later supplies its own
389 // transport (dev-docs/todo/ide_input_adapter.md).
390 using InputProvider = std::function<std::string()>;
391 void setInputProvider(InputProvider p) { inputProvider_ = std::move(p); }
392 const InputProvider &inputProvider() const { return inputProvider_; }
393 void setMaxRecursionDepth(int depth);
394
395 std::vector<std::string> workspaceVarNames() const;
396 std::string workspaceJSON() const;
397 void outputText(const std::string &s);
398
399 FigureManager &figureManager() { return figureManager_; }
400 const FigureManager &figureManager() const { return figureManager_; }
401
402 Environment &workspaceEnv() { return *workspaceEnv_; }
403 Environment &constantsEnv() { return *constantsEnv_; }
404 Environment &globalsEnv() { return *globalsEnv_; }
405
406 bool hasFunction(const std::string &name) const;
407 bool hasUserFunction(const std::string &name) const;
408 bool hasExternalFunction(const std::string &name) const;
409 // Compile-time probe for the compiler's dotted-rvalue decision: can
410 // `name` be CALLED with no parentheses? True when it is a registered
411 // external (full name), a short-name leaf of one, or an
412 // already-registered user function. No lazy m-file probing — the
413 // predicate must stay side-effect-free.
414 bool hasCallableName(const std::string &name) const;
415
416 // ── Public callback API for builtins ──────────────────────
417 // Invoke a function-handle Value from C++. Routes through TW so
418 // both built-in (externalFuncs_) and user/anonymous handles work
419 // regardless of the active backend. `env` defaults to
420 // workspaceEnv() when nullptr; pass the CallContext env if you
421 // want the callee to see the caller's local scope.
422 //
423 // Single-output and multi-output forms; the multi-output form
424 // returns whatever the handle produces (truncated/extended to
425 // `nout` slots).
426 Value callFunctionHandle(const Value &handle,
427 Span<const Value> args,
428 Environment *env = nullptr);
429 std::vector<Value> callFunctionHandleMulti(const Value &handle,
430 Span<const Value> args,
431 size_t nout,
432 Environment *env = nullptr);
433
434 // Unified lookup — script-scope first, then workspace-scope, then
435 // an m-file resolver pass over the configured search path
436 // (script-origin → addPath_-list). Returns nullptr when the name
437 // isn't found anywhere. May parse-and-cache an m-file as a
438 // side-effect.
439 //
440 // `env` drives the import-walk fallback for unqualified names. Pass
441 // the active scope (TW: caller's env; VM: workspaceEnv) so that
442 // `import pkg.*` in scope can resolve bare `foo()` to `pkg.foo`.
443 // Pass nullptr only for introspection paths that intentionally
444 // bypass imports (e.g. `which`, `exist`).
445 const UserFunction *lookupUserFunction(const std::string &name,
446 const Environment *env);
447
448 // Const variant — never triggers the m-file resolver. Use for
449 // pure introspection / "is this CURRENTLY known" checks.
450 const UserFunction *lookupUserFunctionLocal(const std::string &name) const;
451
452 // Clear workspace-scope user functions. Script-local functions
453 // live in a separate bucket (see beginScript/endScript) and are
454 // untouched — MATLAB treats file-scoped functions as lexically
455 // part of the script, not the workspace.
456 void clearUserFunctions();
457
458 // Adopt a UserFunction parsed/compiled by another component (the
459 // compiler emitting FUNCTION_DEF chunks, the m-file resolver, the
460 // anonymous-function machinery). When `scriptScope` is true the
461 // entry lands in the script-local bucket cleared by endScript;
462 // otherwise it lives in the workspace bucket. Single funnel for
463 // the writes that previously went straight into the private maps
464 // via friendship.
465 void adoptUserFunction(const std::string &name,
466 UserFunction uf,
467 bool scriptScope = false);
468
469 // Base-workspace global membership is the single set workspaceEnv_->globals_
470 // (see Environment::globalNames). The compiler reads it to seed each new
471 // top-level chunk's globalNames list — split-mode execution otherwise loses
472 // the declaration between statements.
473
474 // ── M-file path registry (Phase 9) ─────────────────────────────
475 // Directories searched (in order) by lookupUserFunction's m-file
476 // resolver pass. Paths are VFS-resolvable strings (e.g.
477 // "native:/scripts", "local:/work", or just a relative path).
478 void addPath(const std::string &dir);
479 void rmPath(const std::string &dir);
480 const std::vector<std::string> &path() const { return mPath_; }
481
482 // Drop all cached m-file entries — next lookup re-scans the disk.
483 // Powers MATLAB's `rehash` builtin.
484 void rehashMFiles();
485
486 // Check all cached m-files against disk/VFS and evict any that have been
487 // modified or deleted, so subsequent calls re-parse the latest source.
488 // Evicted file CLASS bases with live dependents are reloaded through the
489 // same cascade as rehashMFiles (an inline subclass must re-merge the
490 // edited base, not keep the stale snapshot).
491 void refreshStaleMFiles();
492
493 // Reload each evicted file-class base that a surviving (inline) subclass
494 // still derives from; registerClassDef's cascade re-merges the dependents.
495 void reloadEvictedClassBases_(const std::vector<std::string> &bases);
496
497 // Mark the entry/exit of a top-level script or function
498 // evaluation. While a script is active, any FUNCTION_DEF it
499 // defines compiles into the script-scope buckets instead of the
500 // workspace. Nesting is supported via internal save stacks, so
501 // recursive eval() calls don't lose their outer scope.
502 //
503 // The AST pointer must outlive the matching endScript() — the
504 // caller (eval, DebugSession) owns lifetime.
505 void beginScript(const ASTNode *ast);
506 void endScript();
507
508 // Copy the current script-scope user/compiled function buckets
509 // into the workspace-scope ones. Used by eval() at script exit
510 // so that REPL-ish multi-statement pastes (`function f()...end;
511 // f();` on one go) leave `f` callable from a later eval —
512 // preserving the engine's REPL persistence contract. Debug
513 // sessions don't call this, so file-scoped helpers in a .m
514 // script don't leak into base workspace.
515 void promoteScriptLocalsToWorkspace();
516
517 // --- Debugger API — grouped behind engine.debug() (see DebugFacade) ──
518 // The debugger accessors live off the public Engine surface (private,
519 // reached through the DebugFacade handle) to bound the class's public
520 // method count — architecture-review risk #3.
521 DebugFacade debug();
522
523private:
524 friend class DebugFacade;
525 void setDebugObserver(std::shared_ptr<DebugObserver> observer);
526 DebugObserver *debugObserver() const { return debugObserver_.get(); }
527 BreakpointManager &breakpointManager() { return breakpointManager_; }
528 const BreakpointManager &breakpointManager() const { return breakpointManager_; }
529 DebugController *debugController() { return debugController_.get(); }
530 const DebugController *debugController() const { return debugController_.get(); }
531
532 // Resume paused debug execution with the given action.
533 // Returns Paused on next breakpoint/step, Completed if execution finishes.
534 // Throws DebugStopException if Stop is requested, Error on runtime errors.
535 ExecStatus debugResume(DebugAction action);
536
537public:
538
539 // Reinstall built-in constants (pi, eps, inf, etc.) into constantsEnv.
540 // Called after clear to restore the standard environment. Also
541 // re-installs any constants the host has registered via
542 // registerConstant() so `clear all` doesn't wipe them.
543 void reinstallConstants();
544
545 // Register a host-level constant — visible to every script as if it
546 // were `pi`/`eps`/etc. Does not appear in `whos` or the debug
547 // Workspace panel, can be shadowed by `name = …` and un-shadowed by
548 // `clear name`, and survives `clear all`.
549 void registerConstant(const std::string &name, Value val);
550
551 // Is `name` a reserved name (MATLAB built-in OR host-registered
552 // constant)? Used by the compiler / VM / debug workspace to hide
553 // these names from user-workspace views and skip unnecessary
554 // runtime safety checks.
555 bool isReservedName(const std::string &name) const;
556
557 // Tic/toc timer access — used by workspace builtins
558 void setTicTimer(TimePoint tp)
559 {
560 ticBase_ = tp;
561 ticCalled_ = true;
562 }
563 TimePoint ticTimer() const { return ticBase_; }
564 bool ticWasCalled() const { return ticCalled_; }
565
566 // Returns true when executing inside a user function call (not top-level script).
567 // Used by clear() to avoid modifying workspaceEnv from within function scope.
568 bool isInsideFunctionCall() const;
569
570 // ── Frame stack introspection (for assignin / evalin / inputname) ──
571 //
572 // Walk the user-function call stack from a builtin currently being
573 // executed. n=0 = the immediate caller's scope (the function or
574 // top-level script that contains the call site). n=1 = its caller.
575 // Walking off the top of the stack returns &workspaceEnv().
576 //
577 // Backend-aware: VM walks frames_, lazy-allocating frame.env on the
578 // way; TW walks its own activeFrames_ stack maintained via RAII
579 // guards around callUserFunction.
580 Environment *callerEnv(int n = 0);
581
582 // Number of user-function frames between the running builtin and
583 // the top-level. 0 means the builtin was called directly from a
584 // top-level script (caller is workspaceEnv).
585 int callerDepth() const;
586
587 // Variable assignment in the n-th caller's scope. Writes the value
588 // such that the caller can subsequently read it back. In VM mode,
589 // also performs write-through to the caller's frame register if the
590 // name is statically allocated (chunk.varMap hit). Used by assignin.
591 void assignToCaller(int n, const std::string &name, Value val);
592
593 // Returns the source-text name of the k-th input arg as written at
594 // the call site of the currently-running user function (1-indexed,
595 // matching MATLAB). Empty string if the arg was not a bare
596 // identifier (literal, expression, etc.). Throws when invoked from
597 // outside any user-function call, or when k is out of range.
598 // Backed by per-CALL-site metadata recorded by the compiler / TW
599 // call sites.
600 std::string inputName(int k);
601
602 // ── Virtual filesystem ────────────────────────────────────
603 //
604 // Registry of named filesystems ("native", "temporary", "local", …).
605 // A native FS is pre-registered in the Engine constructor; the IDE
606 // installs "temporary" / "local" via CallbackFS hooks at startup.
607 //
608 // resolvePath() picks the right backend by this order of precedence:
609 // 1. explicit prefix in the path ("temporary:/foo", "local:/foo",
610 // "native:/foo") — wins over everything
611 // 2. env var NK_FS, if it names a registered backend
612 // 3. the current script's origin (set by the IDE before eval)
613 // 4. "native" if registered
614 // Relative paths are joined with `cwd_` when set; otherwise with
615 // NK_CWD as a host-runtime fallback. cwd_ takes precedence —
616 // once `cd` / setCwd writes to it, the env var is ignored.
617 void registerVirtualFS(std::unique_ptr<VirtualFS> fs);
618 VirtualFS *findVirtualFS(const std::string &name) const;
619
620 // Each frame carries (fsName, scriptDir):
621 // * fsName — VFS that the script came from. Used by resolvePath
622 // as the implicit FS for relative paths inside the
623 // script.
624 // * scriptDir — directory containing the script. Used by
625 // resolveMFile_ as the implicit search dir, so
626 // sibling .m files resolve without addpath.
627 // 1-arg overload pushes an empty scriptDir — kept for callers that
628 // only know the FS (IDE running an unsaved buffer, tests).
629 void pushScriptOrigin(const std::string &fsName);
630 void pushScriptOrigin(const std::string &fsName, const std::string &scriptDir);
631 void popScriptOrigin();
632 const std::string *currentScriptOrigin() const; // fsName, may be null
633 const std::string *currentScriptDir() const; // dir, may be null/empty
634
635 // Engine-owned current working directory.
636 // * `cd` / setCwd write here — canonical when non-empty.
637 // * pwd reports this (with backend-cwd fallback when empty).
638 // * resolvePath consults this first, then NK_CWD env var.
639 // The two-tier model lets hosts seed cwd via `setenv NK_CWD`
640 // without having to call setCwd, while still letting in-engine
641 // `cd` calls take precedence once they happen.
642 const std::string &cwd() const { return fsCtx_.cwd(); }
643 void setCwd(const std::string &p) { fsCtx_.setCwd(p); }
644
645 // The path-resolution result type lives on FsContext (fs/, L0); aliased
646 // here so existing callers keep writing Engine::ResolvedPath.
647 using ResolvedPath = FsContext::ResolvedPath;
648 ResolvedPath resolvePath(const std::string &userPath) const;
649
650 // Direct handle to the filesystem session (VFS registry + script-origin
651 // stack + cwd + resolver). Lets core-free toolbox code (io codecs, …)
652 // resolve paths through fs::FsContext WITHOUT depending on Engine — the
653 // Engine-free C++ API surface. Registration adapters pass engine.fsContext().
654 FsContext &fsContext() noexcept { return fsCtx_; }
655 const FsContext &fsContext() const noexcept { return fsCtx_; }
656
657 // The session RNG stream (rand/randn/randi/randperm + rng() control + every
658 // toolbox *rnd sampler draw from this). Registration adapters pass
659 // engine.rng() to the ops generators / RngContext&-taking samplers.
660 ops::RngContext &rng() noexcept { return rng_; }
661 const ops::RngContext &rng() const noexcept { return rng_; }
662
663 // ── MATLAB-style file descriptor table ────────────────────
664 //
665 // The fopen / fclose / fprintf(fid, …) state + machinery now live on
666 // FsContext (fs/, L0) so the stateful fopen-family is Engine-free; the
667 // methods below forward to fsCtx_. `OpenFile` is aliased here so existing
668 // callers keep writing `Engine::OpenFile`. fids 0/1/2 are still routed to
669 // outputText() by the fprintf builtin, not by this table.
670 using OpenFile = FsContext::OpenFile;
671 int openFile(const std::string &userPath, const std::string &mode)
672 { return fsCtx_.openFile(userPath, mode); }
673 bool closeFile(int fid) { return fsCtx_.closeFile(fid); }
674 void closeAllFiles() { fsCtx_.closeAllFiles(); }
675 OpenFile *findFile(int fid) { return fsCtx_.findFile(fid); }
676 std::vector<int> openFileIds() const { return fsCtx_.openFileIds(); }
677 const std::string &lastFopenError() const { return fsCtx_.lastFopenError(); }
678
679private:
680 std::pmr::memory_resource *mr_; // not owned; caller-supplied or get_default_resource()
681 std::unique_ptr<Environment> globalsEnv_; // MATLAB 'global' variables — shared across functions
682 std::unique_ptr<Environment> constantsEnv_; // pi, eps, inf, etc. — parent for all scopes
683 std::unique_ptr<Environment> workspaceEnv_; // top-level workspace (base workspace)
684
685 std::unordered_map<std::string, BinaryOpFunc> binaryOps_;
686 std::unordered_map<std::string, UnaryOpFunc> unaryOps_;
687 std::unordered_map<std::string, ExternalFunc> externalFuncs_;
688
689 // Object-model class registry (object_model.md). Keyed by class name.
690 std::unordered_map<std::string, BuiltinClass> classes_;
691 // Parsed user classdef descriptors (property defaults, ctor + method
692 // UserFunctions), kept for inheritance merges. Full type in engine.cpp.
693 std::unordered_map<std::string, std::shared_ptr<ClassDefDesc>> classDefs_;
694 // Cloned source AST per registered classdef, so a base-class redefinition
695 // can re-merge its (transitive) subclasses from scratch. Keyed by the
696 // class's registry name (qualified for packaged classes).
697 std::unordered_map<std::string, std::shared_ptr<const ASTNode>> classDefAst_;
698 // Re-entrancy guard for reregisterDerivedClasses: while a cascade is
699 // re-registering subclasses, their own registerClassDef calls must not each
700 // launch another (redundant, overlapping) cascade.
701 bool suppressDependentCascade_ = false;
702 // Higher-order builtins that can run callbacks as pausable VM frames
703 // (state-machine callbacks). Keyed by builtin name; consulted by the VM
704 // before the synchronous external path. Full type in callback_builtin.hpp.
705 std::unordered_map<std::string, std::shared_ptr<CallbackBuiltin>> callbackBuiltins_;
706 std::unordered_map<std::string, UserFunction> userFuncs_;
707 // Startup registration sources retained for post-clear replay
708 // (reinstallBuiltinSources): m-source function texts and classdef
709 // source texts. Cleared functions/classes reload from disk in MATLAB —
710 // these have no disk backing, so the engine re-registers them.
711 std::vector<std::string> builtinMSourceTexts_;
712 std::vector<std::string> builtinClassSourceTexts_;
713 // Parse+adopt halves of the public register entry points, factored out
714 // for replay without re-appending to the retention lists.
715 void adoptBuiltinMSource_(const std::string &src);
716 void adoptBuiltinClassSource_(const std::string &src);
717
718 // Class-execution context stack for classdef member-access enforcement
719 // (private / protected / immutable). Each frame is the class whose
720 // method or constructor body is currently running; push/pop happens in
721 // invokeClassMethod / invokeClassCtor. An access check compares the
722 // member's declaring class against the top frame. Empty == script scope
723 // (only public members reachable).
724 struct ClassCtxFrame
725 {
726 std::string className;
727 bool isCtor;
728 };
729 std::vector<ClassCtxFrame> classCtx_;
730 // Currently-executing class (TW callback guard, else VM top method frame;
731 // empty == script scope). Backs classCtxAllows / classCtxInCtorOf.
732 std::string currentClassCtx_() const;
733
734 // Auxiliary indices into externalFuncs_, populated by registerFunction.
735 // The full-name keys in externalFuncs_ are authoritative; these indices
736 // exist purely to accelerate namespace-aware lookups (Phase 6).
737 std::unordered_multimap<std::string, std::string> shortNameIndex_;
738 std::vector<std::string> namespaceOrder_;
739 std::unordered_set<std::string> namespaceSet_;
740
741 // Internal helper used by both registerFunction overloads.
742 // Throws std::runtime_error if `fullName` already registered.
743 void registerFunctionImpl_(const std::string &fullName,
744 const std::string &leafName,
745 ExternalFunc func);
746
747 // Evict a previously-registered classdef so a redefinition fully replaces
748 // it (REPL / IDE re-run of `classdef Name … end`). Removes the registry
749 // class, the descriptor, the `Name.static` / `Name.CONST` qualified
750 // externals, and the compiled method chunks. Called by registerClassDef
751 // when `Name` is already registered, instead of the old idempotent skip.
752 void unregisterClassDef(const std::string &name);
753 // Remove the bare constructor external `externalFuncs_[name]` that
754 // resolveMFile_ registers for a class loaded from `Name.m`. Guarded by
755 // mFileCache_ membership so an inline `classdef sum` can never erase the
756 // core `sum` builtin that merely shares the name.
757 void dropFileClassCtorExternal_(const std::string &name);
758 // After `base` is (re)registered, re-register every class that transitively
759 // derives from it (parent-first), so subclasses — which hold a snapshot of
760 // the base's methods/props — pick up the new definition. Guarded by
761 // suppressDependentCascade_ against re-entrant fan-out.
762 void reregisterDerivedClasses(const std::string &base);
763
764 OutputFunc outputFunc_;
765 InputProvider inputProvider_; // empty → input() reads stdin
766 FigureManager figureManager_;
767
768 // Tic/toc timer
769 TimePoint ticBase_{};
770 bool ticCalled_ = false;
771
772 // Tracks whether clear/clear all was called during VM execution
773 // so that export wipes workspaceEnv before writing back.
774 bool clearAllCalled_ = false;
775
776 // Host-registered constants — (name → value). Restored into
777 // constantsEnv_ alongside the built-ins by reinstallConstants() so
778 // `clear all` does not drop them. Name presence also feeds
779 // isReservedName() so these behave the same as `pi`/`eps`/…
780 std::unordered_map<std::string, Value> userConstants_;
781
782 // Script-lexical user functions — peer to userFuncs_ but never
783 // cleared by `clear all`. Managed by begin/endScript (which
784 // drives both this map and the compiler's matching bucket) so
785 // file-scoped functions survive a mid-script clear.
786 std::unordered_map<std::string, UserFunction> scriptLocalUserFuncs_;
787 std::vector<std::unordered_map<std::string, UserFunction>> savedScriptLocalUserFuncs_;
788
789 // ── M-file resolver state (Phase 9) ───────────────────────────
790 std::vector<std::string> mPath_;
791 struct MFileCacheEntry
792 {
793 std::string fullPath; // VFS-prefixed, the resolved location
794 int64_t mtime = 0; // 0 if backend can't supply mtime
795 std::string contentHash; // fallback when mtime is unavailable
796 std::shared_ptr<const std::string> sourceCode; // keeps AST live
797 };
798 std::unordered_map<std::string, MFileCacheEntry> mFileCache_;
799 // Helper — returns nullptr if `name`.m can't be located in any of
800 // (script-origin, mPath_-list) or fails to parse. On success caches
801 // the parsed UserFunction in userFuncs_ and the compiled chunk in
802 // the Compiler.
803 const UserFunction *resolveMFile_(const std::string &name);
804
805 // Debugger
806 std::shared_ptr<DebugObserver> debugObserver_;
807 BreakpointManager breakpointManager_;
808 std::unique_ptr<DebugController> debugController_; // created when observer is set
809
810 // Virtual filesystem session: VFS registry + script-origin stack + cwd +
811 // path resolver. Engine forwards its filesystem API to this (see the
812 // delegating wrappers in engine.cpp). Owned by value — FsContext is
813 // STL-only (fs/, L0).
814 FsContext fsCtx_;
815
816 // The session's random-number stream + rng() control. Owned by value —
817 // RngContext is an ops (L0.5) type. Default-constructed = rng('default').
818 // Each Engine has an independent, reproducible stream; registration
819 // adapters pass engine.rng() to the ops generators / toolbox samplers, so
820 // there is no process-global RNG and no mutex.
821 ops::RngContext rng_;
822
823public:
824 void markClearAll() { clearAllCalled_ = true; }
825
826private:
827 std::unique_ptr<TreeWalker> treeWalker_;
828 std::unique_ptr<Compiler> compiler_;
829
830public:
831 Compiler *compilerPtr() { return compiler_.get(); }
832
833private:
834 std::unique_ptr<VM> vm_;
835 Backend backend_ = Backend::VM;
836
837 // Element-wise fusion: registered idiom→kernel rules + on/off gate.
838 // Default reflects NK_FUSE (set in the ctor); the registry is empty
839 // until the standard library installs rules, so a bare Engine is unaffected.
840 std::vector<FusionRule> fusionRules_;
841 bool fusionEnabled_ = true;
842 size_t fusionHits_ = 0; // count of rule.execute()==true (fire telemetry)
843
844 // Sync VM's exported variables to workspaceEnv (called after execute, even on error)
845 void syncVMToWorkspace();
846
847 // Sync VM's exported variables to an arbitrary scope. Falls back
848 // to syncVMToWorkspace() when scope is null or equals workspaceEnv.
849 // VM mode also performs write-through to the scope-owning frame's
850 // register slots so subsequent register-based reads in the caller
851 // pick up the values.
852 void syncVMToScope(Environment *scope);
853
854 // Compile one AST subtree as a VM chunk, run it, sync registers to
855 // workspaceEnv. eval() calls this once per top-level statement when
856 // splitting a multi-statement script for MATLAB-parity semantics.
857 Value runOneChunk(const ASTNode *ast, std::shared_ptr<const std::string> src);
858
859 friend class TreeWalker;
860 friend class VM;
861 friend class DebugSession;
862};
863
864// Thin grouped handle over the Engine's debugger surface (architecture-review
865// risk #3): keeps these accessors off the public Engine API. Obtain via
866// engine.debug(); forwards to the (now-private) Engine debugger methods.
867class DebugFacade
868{
869public:
870 explicit DebugFacade(Engine &e) : e_(e) {}
871 void setObserver(std::shared_ptr<DebugObserver> observer) { e_.setDebugObserver(std::move(observer)); }
872 DebugObserver *observer() const { return e_.debugObserver(); }
873 BreakpointManager &breakpoints() { return e_.breakpointManager(); }
874 const BreakpointManager &breakpoints() const { return e_.breakpointManager(); }
875 DebugController *controller() { return e_.debugController(); }
876 const DebugController *controller() const { return e_.debugController(); }
877 ExecStatus resume(DebugAction action) { return e_.debugResume(action); }
878
879private:
880 Engine &e_;
881};
882
883inline DebugFacade Engine::debug() { return DebugFacade(*this); }
884
885} // namespace nk
Definition vm.hpp:24
Core MATLAB-compatible multidimensional array and scalar value type.
Definition value.hpp:61