nk 1.0.0
C++ Numerical Computing Runtime
Loading...
Searching...
No Matches
core/include/nk/core/compiler.hpp
1// include/compiler.hpp
2#pragma once
3
4#include <nk/core/ast.hpp>
5#include <nk/core/bytecode.hpp>
6#include <bitset>
7#include <cstdint>
8#include <unordered_set>
9
10#include <stdexcept>
11#include <string>
12#include <unordered_map>
13#include <vector>
14
15namespace nk {
16
17class Engine;
18struct UserFunction; // defined in types.hpp; used by-reference here
19
20// Base for "chunk exceeds a bytecode operand-space limit" errors. The engine
21// catches this type to run the script on the TreeWalker instead (the
22// documented dual-engine fallback contract) unless a debug observer needs the
23// VM. Derives from runtime_error so existing broad `catch (std::exception&)`
24// TW-fallbacks (class methods) keep working.
25struct CompilerLimitError : std::runtime_error {
26 using std::runtime_error::runtime_error;
27};
28
29// Thrown when a single chunk needs more than the 255 register slots the
30// bytecode operand byte can address. Distinct type (not a bare runtime_error)
31// so callers can tell "function too large for the VM" apart from other compile
32// failures and surface it instead of silently dropping VM compilation.
34 using CompilerLimitError::CompilerLimitError;
35};
36
37// Thrown when the constant/string pool index or a jump offset would exceed
38// the int16 operand field and silently wrap (>= 32768 pool entries / > 32767
39// instruction span). Same fallback contract as RegisterExhaustionError.
41 using CompilerLimitError::CompilerLimitError;
42};
43
44class Compiler
45{
46 friend class IndexContextGuard;
47
48public:
49 explicit Compiler(Engine &engine);
50
51 BytecodeChunk compile(const ASTNode *ast,
52 std::shared_ptr<const std::string> sourceCode = nullptr);
53
54 // Compile a function definition into a BytecodeChunk
55 BytecodeChunk compileFunction(const ASTNode *funcDef,
56 std::shared_ptr<const std::string> sourceCode = nullptr);
57
58 // Register a FUNCTION_DEF node in the currently-active compiled
59 // function table (+ engine.{script,user}Funcs_) without running
60 // it. Used by Engine::beginScript to pre-compile a script's
61 // local functions into the script-scope bucket, and by the
62 // split-mode driver so forward references resolve.
63 void registerFunction(const ASTNode *funcDef);
64
65 // Same as above but binds the function under an explicit name
66 // instead of funcDef->strValue. resolveMFile_ uses this for
67 // package-qualified m-files (`+pkg/foo.m` is bound under
68 // "pkg.foo", not the leaf "foo", so two packages with the same
69 // leaf don't collide in compiledFuncs_).
70 void registerFunctionAs(const std::string &qualifiedName,
71 const ASTNode *funcDef);
72
73 // Compile a classdef method / constructor body (reconstructed from its
74 // UserFunction) into the GLOBAL compiled-function table under `uf.name`
75 // ("Class>member"), returning the chunk. Idempotent. Unlike
76 // registerFunctionAs this never lands in the script-local bucket —
77 // classdef bodies persist with the class across evals. Lets the VM run
78 // classdef method bodies as native frames (see vm_callbacks_plan.md).
79 const BytecodeChunk *ensureClassMethodCompiled(const UserFunction &uf);
80
81 // Workspace-scope compiled functions. Populated by `function` at
82 // the REPL or by anonymous-function allocation. Cleared by
83 // `clear all` / `clear functions` (see Engine::clearUserFunctions).
84 const std::unordered_map<std::string, BytecodeChunk> &compiledFuncs() const
85 {
86 return compiledFuncs_;
87 }
88 void clearCompiledFuncs() { compiledFuncs_.clear(); }
89 // Drop a single compiled chunk by name (used by rehash to invalidate just
90 // the reloaded m-file, not every compiled function).
91 void eraseCompiledFunc(const std::string &name) { compiledFuncs_.erase(name); }
92 // Drop every compiled classdef-method chunk for `className` — keys of the
93 // form "ClassName>method" — plus any "uncompilable" marks, so redefining
94 // the class recompiles its bodies from scratch instead of reusing the
95 // stale cached chunks. See Engine::unregisterClassDef.
96 void eraseCompiledFuncsForClass(const std::string &className);
97
98 // Script-scope compiled functions. Populated by beginScriptScope
99 // via compileFunctionDef routing. NEVER cleared by
100 // clearCompiledFuncs — that's the whole point of keeping this
101 // bucket separate. Its lifetime is bounded by beginScriptScope /
102 // endScriptScope pairs.
103 const std::unordered_map<std::string, BytecodeChunk> &scriptLocalCompiledFuncs() const
104 {
105 return scriptLocalCompiledFuncs_;
106 }
107
108 // Unified lookup used by dispatch sites outside the compiler —
109 // tries script-scope first, then workspace-scope. Returns
110 // nullptr if unknown.
111 const BytecodeChunk *findCompiled(const std::string &name) const;
112
113 // Enter/leave a script-lexical scope. While inside, compiled
114 // FUNCTION_DEFs land in scriptLocalCompiledFuncs_ rather than
115 // the workspace map. Nestable — each pair push/pop an isolated
116 // bucket, so a recursive eval's inner script can't leak into
117 // the outer one.
118 void beginScriptScope();
119 void endScriptScope();
120 bool inScriptScope() const { return scriptDepth_ > 0; }
121
122 // Peer of Engine::promoteScriptLocalsToWorkspace: copy the
123 // script-scope compiled chunks into the workspace bucket so
124 // their VM dispatches keep resolving after the scope ends.
125 void promoteScriptLocalsToWorkspace();
126
127 // Debug: dump bytecode
128 static std::string disassemble(const BytecodeChunk &chunk);
129
130private:
131 Engine &engine_;
132
133 // Current chunk being compiled
134 BytecodeChunk chunk_;
135
136 // Register allocation: variable name → register index
137 std::unordered_map<std::string, uint8_t> varRegisters_;
138 // nextReg_ is `int` (not uint8_t) so we can detect overflow (>256)
139 // before it silently wraps. Bytecode reg fields are uint8_t — so we
140 // must throw cleanly when a chunk's allocation hits that hard limit
141 // rather than corrupt low/variable slots. See peakReg_ for the value
142 // stored in BytecodeChunk::numRegisters.
143 int nextReg_ = 0;
144 // High-water mark for nextReg_ across the chunk. Used at end of
145 // compilation to size the runtime register file. Distinct from
146 // nextReg_, which may shrink when compileBlock releases temps at
147 // statement boundaries (see end of compileBlock).
148 int peakReg_ = 0;
149 // Highest variable register reserved so far (exclusive). Slots
150 // [0, maxVarReg_) are pinned to variables and never reclaimable;
151 // [maxVarReg_, nextReg_) are transient temps that compileBlock
152 // releases between statements.
153 int maxVarReg_ = 0;
154 int anonCounter_ = 0;
155 bool isTopLevel_ = false;
156 // True while compiling a fusion idiom's fallback path (the normal compile),
157 // so the nested compileCall doesn't re-attempt fusion (infinite recursion).
158 bool inFusionFallback_ = false;
159 uint8_t nargoutContext_ = 1; // expected number of outputs (0=statement, 1=expression)
160
161 // Index context for END_VAL compilation
162 uint8_t indexContextArr_ = 0;
163 uint8_t indexContextDim_ = 0;
164 uint8_t indexContextNdims_ = 1;
165
166 // Current source location (updated before compiling each node)
167 SourceLoc currentLoc_{};
168
169 // Compiled function table (persists across compile() calls) —
170 // workspace-scope (cleared by `clear all`/`clear functions`).
171 std::unordered_map<std::string, BytecodeChunk> compiledFuncs_;
172
173 // classdef method bodies that the VM compiler can't yet compile (e.g. a
174 // super-call before that lands in the VM) — cached so dispatch falls back
175 // to the TW hook without re-attempting the compile every call.
176 std::unordered_set<std::string> uncompilableClassMethods_;
177
178 // Script-lexical compiled functions. Separated from workspace-
179 // scope so `clear all` mid-script can't wipe them — MATLAB
180 // treats local functions as part of the script's code, not its
181 // workspace. Managed by begin/endScriptScope; nesting via the
182 // save stack.
183 std::unordered_map<std::string, BytecodeChunk> scriptLocalCompiledFuncs_;
184 std::vector<std::unordered_map<std::string, BytecodeChunk>> savedScriptLocalCompiledFuncs_;
185 int scriptDepth_ = 0;
186
187 // ── Variable register access API ────────────────────────────
188 //
189 // Every assignment-like compile site MUST call `varRegWrite(name)` so
190 // BytecodeChunk::assignedVars is populated — the debug workspace uses
191 // it to distinguish "user assigned" from "just read" (MATLAB's
192 // whos-parity for shadowed built-ins). Reads and pure re-lookups use
193 // the other two methods.
194 //
195 // varRegWrite(name) — allocates + marks as assigned. Use for every
196 // AST node that writes to `name`.
197 // varRegRead(name) — allocates + emits ASSERT_DEF. Use for checked
198 // reads at the AST level.
199 // varRegLookup(name) — allocates without marking or asserting. Use
200 // for internal plumbing: re-fetching a register
201 // for an already-allocated variable, pre-loading
202 // pseudo-vars (nargin/nargout), workspace-env
203 // imports, etc. NEVER use at a user-assignment
204 // site — that would defeat the whole point.
205 //
206 // `markAssigned(name)` is a rare helper for write sites that bypass
207 // varRegWrite (e.g. move-elimination in compileAssign that writes via
208 // `varRegisters_[name] = src` directly).
209 uint8_t varRegWrite(const std::string &name);
210 uint8_t varRegRead(const std::string &name);
211 // preloadReserved: when first allocating a register for a reserved name
212 // (pi/eps/i/j/…), emit a LOAD_CONST of the engine's value. Correct for a
213 // *read* of the constant; must be false when allocating for a parameter
214 // or assignment target named like a constant (e.g. a param `i`), whose
215 // value comes from the call/assignment — otherwise the pre-load clobbers
216 // the argument with the imaginary unit.
217 uint8_t varRegLookup(const std::string &name, bool preloadReserved = true);
218 void markAssigned(const std::string &name) { chunk_.assignedVars.insert(name); }
219 // Pre-import global variables before compiling AST
220 void preImportGlobals(const ASTNode *ast);
221 void collectAllIdentifiers(const ASTNode *node, std::unordered_set<std::string> &out);
222 // Scan AST for names that get ASSIGNED somewhere — preImportGlobals
223 // pre-allocates a low slot for each so vars cluster at the bottom
224 // and don't fragment the slot range under temp pressure.
225 void collectAssignedNames(const ASTNode *node, std::vector<std::string> &out);
226 // Cluster every variable assigned in `body` at a low, contiguous register
227 // slot BEFORE compiling the body. Without this, a local's slot is whatever
228 // temp happened to be free at its first assignment (high when the RHS is a
229 // deep expression) — fragmenting the slot range, creeping maxVarReg_ (the
230 // statement-boundary temp-release floor) upward, and exhausting the 256-slot
231 // chunk on functions that don't need that many slots. Run for BOTH the
232 // top-level script (via preImportGlobals) and every user function
233 // (compileFunction) so they allocate identically.
234 void preallocateAssignedVars(const ASTNode *body);
235 // Allocate a temporary register
236 uint8_t tempReg();
237
238 // Emit instructions
239 void emit(Instruction instr);
240 void emitABC(OpCode op, uint8_t a, uint8_t b, uint8_t c);
241 void emitAB(OpCode op, uint8_t a, uint8_t b);
242 void emitAD(OpCode op, uint8_t a, int16_t d);
243 void emitA(OpCode op, uint8_t a);
244 void emitD(OpCode op, int16_t d);
245 void emitNone(OpCode op);
246
247 // Current instruction index (for patching jumps)
248 size_t currentPos() const;
249 // Patch a jump instruction's offset at given position. Takes int32_t so
250 // the range check happens here, not after a caller-side truncating cast.
251 void patchJump(size_t instrPos, int32_t offset);
252
253 // Add constant to pool, return index (dedups doubles / strings)
254 int16_t addConstant(double value);
255 int16_t addStringConstant(const std::string &s);
256 // The only sanctioned way to grow a pool: bounds-checks the int16 index
257 // space and throws ChunkOverflowError at the ceiling instead of wrapping.
258 int16_t appendConstant(Value v);
259 int16_t appendString(const std::string &s);
260
261 // Reserve n contiguous temp registers (nextReg_ .. nextReg_+n); returns
262 // the first. The block-return idiom — tempReg() n times with the result
263 // discarded — hides the intent from both readers and static analysis.
264 uint8_t reserveRegs(size_t n);
265
266 // Compile AST nodes — return register holding result
267 // compileNode is the single-value entry: it compiles the node, then (when the node
268 // is a brace cell-index that may yield a comma-separated list) emits COLLAPSE on the
269 // result register, so operands / conditions / index values / assignment RHS never
270 // see a CSL. Splice contexts (call args, [..], {..}, multi-assign) call
271 // compileNodeExpand instead, which leaves a CSL for the consumer to flatten.
272 // Mirrors the TreeWalker execNode / execNodeExpand split. See csl_first_class.md.
273 uint8_t compileNode(const ASTNode *node);
274 uint8_t compileNodeExpand(const ASTNode *node);
275 uint8_t compileBlock(const ASTNode *node);
276 uint8_t compileNumber(const ASTNode *node);
277 uint8_t compileString(const ASTNode *node);
278 uint8_t compileBool(const ASTNode *node);
279 uint8_t compileIdentifier(const ASTNode *node);
280 uint8_t compileAssign(const ASTNode *node);
281 uint8_t compileMultiAssign(const ASTNode *node);
282 uint8_t compileBinaryOp(const ASTNode *node);
283 uint8_t compileUnaryOp(const ASTNode *node);
284 uint8_t compileExprStmt(const ASTNode *node);
285
286 // Phase 2: control flow
287 uint8_t compileIf(const ASTNode *node);
288 uint8_t compileSwitch(const ASTNode *node);
289 uint8_t compileTryCatch(const ASTNode *node);
290 uint8_t compileGlobalPersistent(const ASTNode *node);
291 uint8_t compileFieldAccess(const ASTNode *node);
292 uint8_t compileFieldAssign(const ASTNode *node);
293 uint8_t compileCellLiteral(const ASTNode *node);
294 uint8_t compileCellIndex(const ASTNode *node);
295 uint8_t compileCellAssign(const ASTNode *node);
296
297 // Lvalue-store cores shared by single-target ASSIGN and multi-output
298 // MULTI_ASSIGN. When `valReg >= 0` the value to store is taken from
299 // that register (multi-assign output); otherwise `rhsNode` is
300 // compiled in-place exactly as the single-assign path always did.
301 uint8_t compileFieldStore(const ASTNode *lhs, const ASTNode *rhsNode,
302 int inValReg, bool suppress);
303 uint8_t compileCellStore(const ASTNode *lhs, const ASTNode *rhsNode,
304 int inValReg, bool suppress);
305 uint8_t compileIndexStore(const ASTNode *lhs, const ASTNode *rhsNode,
306 int inValReg, bool suppress);
307 // Dispatch an output register into any lvalue target (multi-assign).
308 void compileStoreLValue(const ASTNode *target, uint8_t valReg, bool suppress);
309
310 // True when an lvalue mixes accessor kinds or is rooted at a non-
311 // identifier object beyond what the single-level fast compilers
312 // handle (`s.x(2)`, `d(i).a.b`, `c{i}(2)`, `c{i}.f`, …). Such lvalues
313 // are compiled by compileLValueStore via a get/set write-back chain.
314 bool needsGeneralLValuePath(const ASTNode *lhs) const;
315 // General compound-lvalue store: flatten the accessor chain, load
316 // intermediate containers (get-or-create), apply the final write, and
317 // write each modified container back up to the root variable. Sources
318 // the value from `inValReg` when ≥ 0, else compiles `rhsNode`.
319 uint8_t compileLValueStore(const ASTNode *lhs, const ASTNode *rhsNode,
320 int inValReg, bool suppress);
321 // Superclass call inside a classdef body: `callNode` is the enclosing
322 // CALL, `superRef` its SUPERCLASS_REF head. Emits CALL_SUPER_CTOR
323 // (`obj@Base(args)`, lhs a local) or CALL_SUPER_METHOD
324 // (`method@Base(obj,…)`). explicitOutBase ≥ 0 routes outputs to that
325 // register block (multi-assign); otherwise a temp is allocated. Returns
326 // the first output register.
327 uint8_t compileSuperCall(const ASTNode *callNode, const ASTNode *superRef,
328 int explicitOutBase, uint8_t nout);
329 uint8_t compileAnonFunc(const ASTNode *node);
330 void collectFreeVars(const ASTNode *node,
331 const std::vector<std::string> &params,
332 std::vector<std::string> &freeVars);
333 uint8_t compileWhile(const ASTNode *node);
334 uint8_t compileBreak(const ASTNode *node);
335 uint8_t compileContinue(const ASTNode *node);
336
337 // Phase 3: for-loop, colon, arrays
338 uint8_t compileFor(const ASTNode *node);
339 uint8_t compileColonExpr(const ASTNode *node);
340 uint8_t compileMatrixLiteral(const ASTNode *node);
341 uint8_t compileIndexExpr(const ASTNode *node);
342 uint8_t compileIndexAssign(const ASTNode *node);
343
344 // Phase 4+5: function calls
345 uint8_t compileCall(const ASTNode *node);
346 // Element-wise fusion: if `node` matches a registered idiom (and we're not
347 // already compiling a fallback, and there's register headroom), emit
348 // FUSE_EWISE + fallback and return the result register; else return -1 so
349 // the caller proceeds with the normal compile. Shared by every node type
350 // that can root a fused idiom (CALL for max/min clamp, BINARY_OP for affine
351 // & friends), so adding a binary-rooted idiom needs no new core hook.
352 int tryCompileFused(const ASTNode *node);
353 // Emit FUSE_EWISE for the matched idiom + the normally compiled idiom as the
354 // runtime fallback (patched dst + skip). `ruleIdx` indexes
355 // engine.fusionRules(); `operands` are the rule's operand sub-exprs.
356 uint8_t compileFused(const ASTNode *node, size_t ruleIdx,
357 const std::vector<const ASTNode *> &operands);
358 uint8_t compileCommandCall(const ASTNode *node);
359 // Record per-CALL-site arg names into chunk_.callSiteArgNames so
360 // inputname(k) inside the callee can introspect the call site.
361 void recordCallArgNames(const ASTNode *callNode, size_t callInstrIdx);
362 uint8_t compileFunctionDef(const ASTNode *node);
363 uint8_t compileReturn(const ASTNode *node);
364 // Emit a RET_VARARGOUT for the current chunk (last return is `varargout`):
365 // gather the fixed leading outputs, then the dynamic varargout cell.
366 void emitVarargoutReturn();
367 uint8_t compileDeleteAssign(const ASTNode *node);
368 static int8_t resolveBuiltinId(const std::string &name, size_t nargs);
369
370 // Constant register cache: avoid redundant LOAD_CONST in loops
371 // Maps constant pool index → register that already holds the value
372 std::unordered_map<int16_t, uint8_t> constRegCache_;
373
374 // Scalar register tracking: registers known to hold double scalars
375 // Enables emission of type-check-free _SS opcodes
376 std::bitset<256> scalarRegs_;
377
378 // Peephole optimization pass
379 void peepholeOptimize();
380
381 // Break/continue loop patching
382 struct LoopContext
383 {
384 std::vector<size_t> breakPatches;
385 std::vector<size_t> continuePatches;
386 bool isForLoop = false; // true for for-loops (need forStack_ pop on break)
387 };
388 std::vector<LoopContext> loopStack_;
389};
390
391} // namespace nk
Core MATLAB-compatible multidimensional array and scalar value type.
Definition value.hpp:61