nk 1.0.0
C++ Numerical Computing Runtime
Loading...
Searching...
No Matches
bytecode.hpp
1// include/bytecode.hpp
2#pragma once
3
4#include <nk/value/value.hpp>
5#include <cstdint>
6#include <memory>
7#include <string>
8#include <unordered_map>
9#include <unordered_set>
10#include <vector>
11
12namespace nk {
13
14enum class OpCode : uint8_t {
15 // ── Data movement ────────────────────────────────────────
16 LOAD_CONST, // dst, constIdx R[dst] = constants[constIdx]
17 LOAD_EMPTY, // dst R[dst] = empty
18 LOAD_STRING, // dst, strIdx R[dst] = strings[strIdx]
19 MOVE, // dst, src R[dst] = R[src]
20 // Collapse a comma-separated list reaching a single-value context: R[dst] =
21 // collapseCsl(R[src]) (CSL of 1 -> the elem; 0/>1 -> error; non-CSL passes through).
22 // Inserted by the compiler dataflow pass everywhere a CSL-tainted register feeds a
23 // single-value sink. See dev-docs/memory/csl_first_class.md.
24 COLLAPSE, // dst, src R[dst] = collapseCsl(R[src])
25 LOAD_END, // dst, arrReg, dim R[dst] = size(R[arrReg], dim)
26 COLON_ALL, // dst R[dst] = <colon-all marker>
27 LOAD_NARGIN, // [reserved] compiler injects nargin via varMap instead
28 LOAD_NARGOUT, // [reserved] compiler injects nargout via varMap instead
29
30 // ── Arithmetic ───────────────────────────────────────────
31 ADD, // dst, a, b R[dst] = R[a] + R[b]
32 SUB, // dst, a, b R[dst] = R[a] - R[b]
33 MUL, // dst, a, b R[dst] = R[a] * R[b]
34 RDIV, // dst, a, b R[dst] = R[a] / R[b]
35 LDIV, // dst, a, b R[dst] = R[a] \ R[b]
36 POW, // dst, a, b R[dst] = R[a] ^ R[b]
37 NEG, // dst, src R[dst] = -R[src]
38 UPLUS, // dst, src R[dst] = +R[src]
39
40 // ── Element-wise arithmetic ──────────────────────────────
41 EMUL, // dst, a, b R[dst] = R[a] .* R[b]
42 ERDIV, // dst, a, b R[dst] = R[a] ./ R[b]
43 ELDIV, // dst, a, b R[dst] = R[a] .\ R[b]
44 EPOW, // dst, a, b R[dst] = R[a] .^ R[b]
45
46 // ── Comparison ───────────────────────────────────────────
47 EQ, // dst, a, b R[dst] = R[a] == R[b]
48 NE, // dst, a, b R[dst] = R[a] ~= R[b]
49 LT, // dst, a, b R[dst] = R[a] < R[b]
50 GT, // dst, a, b R[dst] = R[a] > R[b]
51 LE, // dst, a, b R[dst] = R[a] <= R[b]
52 GE, // dst, a, b R[dst] = R[a] >= R[b]
53
54 // ── Logical ──────────────────────────────────────────────
55 AND, // dst, a, b R[dst] = R[a] & R[b]
56 OR, // dst, a, b R[dst] = R[a] | R[b]
57 NOT, // dst, src R[dst] = ~R[src]
58 AND_SC, // [reserved] && short-circuit — compiler uses JMP_FALSE chain instead
59 OR_SC, // [reserved] || short-circuit — compiler uses JMP_TRUE chain instead
60
61 // ── Control flow ─────────────────────────────────────────
62 JMP, // offset(int16) unconditional jump
63 JMP_TRUE, // reg, offset(int16) jump if R[reg] != 0
64 JMP_FALSE, // reg, offset(int16) jump if R[reg] == 0
65
66 // ── For-loop ─────────────────────────────────────────────
67 FOR_INIT, // var, range, endOffset setup iterator from R[range]
68 FOR_INIT_RANGE, // var, start, stop, endOffset, step fused `for v = a:b` /
69 // `for v = a:s:b` — skips
70 // materialising the colon
71 // range value (avoids the
72 // 8N-byte allocation for
73 // `for i = 1:N` loops).
74 // e=0xFF → implicit step=1.
75 FOR_NEXT, // var, backOffset advance iterator, jump back or fall through
76
77 // ── Function calls ───────────────────────────────────────
78 CALL, // dst, argBase, nargs, funcIdx, e=nargout R[dst] = func(R[base..base+nargs-1])
79 // First-class call with possible CSL args: a=dst, b=argBase, c=nargs, d=funcIdx,
80 // e=nargout. Flattens any CSL arg register into a runtime arg vector, then runs the
81 // full CALL target dispatch (user fn / ctor / method / m-file / callback / external).
82 // Emitted only when an arg could be a CSL, so the hot CALL is untouched.
83 CALL_FLATTEN, // dst, argBase, nargs, funcIdx, e=nargout
84 // Multi-output sibling of CALL_FLATTEN: a=outBase, b=argBase, c=nargs, d=funcIdx,
85 // e=nout. Flattens any CSL arg, then runs the CALL_MULTI dispatch. ([a,b]=f(x,c{idx}))
86 CALL_FLATTEN_MULTI,
87 CALL_MULTI, // dstBase, funcIdx, argBase, nargs, e=nout
88 CALL_BUILTIN, // dst, builtinId, base, nargs inline builtin (mod, sin, etc.)
89 CALL_INDIRECT, // dst, fhReg, base, nargs R[dst] = R[fhReg](R[base..base+nargs-1])
90 // [a,b,…] = R[fhReg](R[base..]): multi-output indirect (handle-variable)
91 // call. a=outBase, b=fhReg, c=argBase, d=nargs, e=nout. Mirrors
92 // CALL_INDIRECT's handle resolution + CALL_MULTI's nout frame-push so a
93 // stored function handle can return several outputs (e.g. fmincon nonlcon
94 // `[c, ceq] = nonlcon(x)`).
95 CALL_INDIRECT_MULTI,
96 // f(c{:}) with a VARIABLE callee (handle or indexable value):
97 // a=outBase, b=fhReg, c=argBase, d=nargs, e=nout. Same as
98 // CALL_INDIRECT(_MULTI) but any CSL argument is flattened into the
99 // runtime arg list first (handle-callee brace-splat:
100 // bugs/closed/lang/handle-call-csl-splat).
101 CALL_INDIRECT_FLATTEN,
102 // Fused element-wise idiom (VM fusion). a=dst, b=operandBase, c=nOps,
103 // d=ruleIdx (into engine.fusionRules()), e=skip. Gather R[base..base+nOps)
104 // → rule.execute; on success R[dst]=result and skip the `e` following
105 // fallback instructions; on decline fall through to them — the normally
106 // compiled idiom, i.e. exact per-op semantics.
107 FUSE_EWISE,
108
109 // ── Array indexing ───────────────────────────────────────
110 INDEX_GET, // dst, arr, idx R[dst] = R[arr](R[idx]) 1D
111 INDEX_GET_2D, // dst, arr, row, col R[dst] = R[arr](R[row], R[col]) 2D
112 INDEX_GET_ND, // dst, arr, base, ndims R[dst] = R[arr](R[base]..R[base+ndims-1])
113 INDEX_SET, // arr, idx, val R[arr](R[idx]) = R[val] 1D
114 INDEX_SET_2D, // arr, row, col, val R[arr](R[row], R[col]) = R[val] 2D
115 INDEX_SET_ND, // arr, base, ndims, val R[arr](R[base]..R[base+ndims-1]) = R[val]
116 INDEX_DELETE, // arr, idx R[arr](R[idx]) = [] 1D
117 INDEX_DELETE_2D, // arr, row, col R[arr](R[row], R[col]) = [] 2D
118 INDEX_DELETE_ND, // arr, base, ndims R[arr](R[base]..R[base+ndims-1]) = []
119
120 // ── Struct field access ──────────────────────────────────
121 FIELD_GET, // dst, obj, nameIdx R[dst] = R[obj].fields[nameIdx]
122 FIELD_GET_OR_CREATE, // dst, obj, nameIdx like FIELD_GET but auto-creates struct/field
123 FIELD_SET, // obj, nameIdx, val R[obj].fields[nameIdx] = R[val]
124 FIELD_GET_DYN, // dst, obj, nameReg R[dst] = R[obj].(R[nameReg])
125 FIELD_GET_OR_CREATE_DYN, // dst, obj, nameReg like FIELD_GET_DYN but auto-creates
126 FIELD_SET_DYN, // obj, nameReg, val R[obj].(R[nameReg]) = R[val]
127 // Struct-array element field write: R[obj](R[idx]).field = R[val].
128 // Auto-grows when idx exceeds current numel; creates a 1×0 struct
129 // array if obj is unset / not a struct.
130 STRUCT_ELEM_FIELD_SET, // a=obj, b=idxReg, c=valReg, d=nameIdx
131 // obj.name(args): dotted call. a=dst, b=objReg, c=argBase, d=nameIdx,
132 // e=nargs. If R[objReg] is an OBJECT whose class has method `name`,
133 // dispatch it (self + args); otherwise fall back to FIELD_GET + the
134 // CALL_INDIRECT machinery (struct-field func handle / index). Object
135 // model — see object_model.md §3.
136 CALL_METHOD,
137 // [a,b] = obj.name(args): dotted multi-output method call. a=outBase,
138 // b=objReg, c=argBase, d=nameIdx, e=(nargs<<4)|nout (each nibble ≤15 —
139 // methods with >15 args/outputs are not supported via this path).
140 // Dispatches the class method with nout result slots written to
141 // R[outBase..outBase+nout). Object model — see object_model.md §3.
142 CALL_METHOD_MULTI,
143 // classdef superclass calls inside a method/ctor body (compiled into the
144 // method chunk so the body runs on the VM). Both delegate to
145 // Engine::superConstruct / superMethod.
146 // CALL_SUPER_CTOR: `obj = obj@Base(args)` — a=dst, b=objReg (seed),
147 // c=argBase, d=baseNameIdx, e=nargs.
148 // CALL_SUPER_METHOD: `[outs] = method@Base(obj, args)` — a=outBase,
149 // b=argBase (args incl. obj at [0]), c=nargs,
150 // d=idx of "Base>method", e=nout.
151 CALL_SUPER_CTOR,
152 CALL_SUPER_METHOD,
153 // Struct-array element get/set as a whole scalar struct, used by the
154 // general compound-lvalue store chain (`d(i).a.b = …`, `d(i,j,k).…`).
155 // Subscripts live in R[base..base+nargs-1] (column-major, any rank).
156 // GET auto-grows the array and returns a 1×1 struct (empty if the
157 // slot was vacant); SET writes the scalar struct back into the
158 // element. Coerce an unset/empty receiver to a struct array.
159 STRUCT_ELEM_GET_OR_CREATE, // a=dst, b=obj, c=base, e=nargs
160 STRUCT_ELEM_SET, // a=obj, b=base, c=nargs, e=valReg
161
162 // ── Cell array access ────────────────────────────────────
163 CELL_GET, // dst, cell, idx R[dst] = R[cell]{R[idx]} 1D
164 // Like CELL_GET but for the compound-lvalue store chain: coerces an
165 // unset/empty receiver to a cell and auto-grows to fit the subscripts
166 // in R[base..base+nargs-1] (any rank), returning the content slot.
167 CELL_GET_OR_CREATE, // a=dst, b=cell, c=base, e=nargs
168 CELL_SET, // cell, idx, val R[cell]{R[idx]} = R[val] 1D
169 CELL_GET_2D, // dst, cell, row, col R[dst] = R[cell]{R[row], R[col]}
170 CELL_SET_2D, // cell, row, col, val R[cell]{R[row], R[col]} = R[val]
171 CELL_GET_MULTI,// outBase, cell, idx, nout R[outBase..+nout] = R[cell]{R[idx]}
172 // 2-D cell CSL multi-assign [a,b]=c{r,cols}: a=outBase, b=cell, c=rowSubReg,
173 // d=colSubReg, e=nout. resolveIndices per dim + sub2ind, column-major; assigns
174 // up to nout of the selected contents to R[outBase..].
175 CELL_GET_MULTI_2D,
176 CELL_GET_ND, // dst, cell, base, ndims R[dst] = R[cell]{R[base]..R[base+ndims-1]}
177 CELL_SET_ND, // cell, base, ndims, val R[cell]{R[base]..R[base+ndims-1]} = R[val]
178 // First-class cell-literal builder: a=cellAcc (in/out, coerced to 1x0 if unset),
179 // b=elem. If R[elem] is a comma-separated list, append ALL its items (column-major);
180 // otherwise append R[elem] as ONE element. Driven by the runtime VALUE (isCsl), not
181 // the compile-time subscript shape, so {c{idx}} with a variable idx flattens too.
182 CELL_APPEND_FLATTEN, // cellAcc, elem
183
184 // ── Transpose ────────────────────────────────────────────
185 CTRANSPOSE, // dst, src R[dst] = R[src]' (conjugate)
186 TRANSPOSE, // dst, src R[dst] = R[src].' (non-conjugate)
187
188 // ── Literals / construction ──────────────────────────────
189 COLON, // dst, start, stop R[dst] = R[start]:R[stop]
190 COLON3, // dst, start, step, stop R[dst] = R[start]:R[step]:R[stop]
191 HORZCAT, // dst, base, count R[dst] = [R[base], ..., R[base+count-1]]
192 HORZCAT_APPEND,// dst, val R[dst] = [R[dst], R[val]] amortised O(1)
193 // Comma-separated-list expansion: R[dst] = [R[dst], structArr(0).fname, ..., structArr(N-1).fname]
194 HORZCAT_APPEND_CSL, // a=dst, b=structArrReg, d=nameIdx
195 // when dst is a row vector / empty and val is a real
196 // scalar; falls back to a 2-elem horzcat otherwise
197 // First-class concat builder: a=dst (in/out), b=elem. If R[elem] is a CSL,
198 // horzcat-append all its items; else append the one value. Driven by the runtime
199 // value, so [a, c{idx}, b] flattens for any subscript. See csl_first_class.md.
200 HORZCAT_APPEND_FLATTEN, // a=dst, b=elem
201 VERTCAT, // dst, base, count R[dst] = [R[base]; ...; R[base+count-1]]
202 // dst = [dst; val] — the vertical twin of HORZCAT_APPEND: scalar
203 // fast path grows a {n,1} column in place (appendScalarCol);
204 // anything else falls back to the generic two-element vertcat.
205 // The accumulator operand of large column literals
206 // (bugs/closed/core/register-exhaustion-no-fallback).
207 VERTCAT_APPEND, // a=dst, b=val
208 MATRIX_BUILD, // [reserved] compiler uses HORZCAT/VERTCAT instead
209 CELL_LITERAL, // dst, base, count {R[base]..R[base+count-1]}
210
211 // ── Display ──────────────────────────────────────────────
212 DISPLAY, // reg, nameIdx print "name = value"
213
214 // ── Return / flow signals ────────────────────────────────
215 RET, // reg return R[reg]
216 RET_MULTI, // base, count return R[base..base+count-1]
217 RET_EMPTY, // return empty
218 // varargout return: a=fixedBase, b=numFixed, c=varargoutReg. Returns the
219 // numFixed leading fixed outputs (R[fixedBase..]) followed by the elements
220 // of the varargout cell R[varargoutReg] — a DYNAMIC return count
221 // (numFixed + numel(cell)), so a `function varargout = f(...)` can return
222 // however many values the caller's nargout asks for.
223 RET_VARARGOUT,
224 BREAK, // [reserved] compiler uses JMP + NOP(flag) instead
225 CONTINUE, // [reserved] compiler uses JMP + NOP(flag) instead
226
227 // ── Error handling ───────────────────────────────────────
228 TRY_BEGIN, // catchOffset, exReg setup try, on catch: R[exReg] = exception
229 TRY_END, // cleanup try block
230 THROW, // reg error(R[reg])
231
232 // ── Scope ────────────────────────────────────────────────
233 GLOBAL_DECL, // [reserved] compiler writes to chunk.globalNames instead
234 PERSISTENT_DECL, // [reserved] compiler writes to chunk.globalNames instead
235 CLOSURE_MAKE, // [reserved] compiler uses cell-packing {funcHandle, captures} instead
236 CLEAR_VAR, // reg R[reg] = empty (clear variable)
237 CLEAR_DYN, // nameReg lookup name in varMap, clear register
238 EXIST_VAR, // dst, nameReg, filterReg check name in varMap/functions → 0/1/5
239 // filterReg=0: no filter; else R[filterReg] = 'var'/'builtin'
240 WHO, // base, count list variables (count=0: all, else R[base..base+count-1])
241 WHOS, // base, count list variables with details (same)
242
243 // ── Utility ──────────────────────────────────────────────
244 NOP, // no-op (patching, alignment)
245 ASSERT_DEF, // reg, nameIdx throw if R[reg] is unset (undefined variable)
246
247 // ── Scalar-specialized (compiler guarantees double-scalar operands) ──
248 ADD_SS, // dst, a, b R[dst] = R[a].scalar + R[b].scalar (no type check)
249 SUB_SS, // dst, a, b
250 MUL_SS, // dst, a, b
251 RDIV_SS, // dst, a, b
252 POW_SS, // dst, a, b (includes integer-exponent fast paths)
253 NEG_S, // dst, src R[dst] = -R[src].scalar
254
255 // ── Fused multiply-add/sub (loop opt #2): R[a] = R[b] ± R[c]*R[e] ──
256 // Collapses sum-of-products (MAC) chains — filters, polynomials, dot
257 // products — into one opcode. Fast path is all-scalar (two-step double:
258 // prod = c*e; a = b ± prod, so bit-identical to the unfused MUL+ADD/SUB).
259 // Non-scalar operands fall back to the matching product then sum/diff.
260 MULADD, // a=dst, b=acc, c, e R[a] = R[b] + R[c]*R[e] (* matmul fallback)
261 MULSUB, // a=dst, b=acc, c, e R[a] = R[b] - R[c]*R[e]
262 EMULADD, // a=dst, b=acc, c, e R[a] = R[b] + R[c].*R[e]
263 EMULSUB, // a=dst, b=acc, c, e R[a] = R[b] - R[c].*R[e]
264
265 // ── End marker ───────────────────────────────────────────
266 HALT, // stop execution
267};
268
269// ============================================================
270// Instruction: fixed 8-byte encoding
271// ============================================================
272//
273// Byte: 0 1 2 3 4 5 6 7
274// [opcode] [ a ] [ b ] [ c ] [ d (int16) ] [ e ]
275//
276// a, b, c, e — register indices (0-255) or small immediates
277// d — signed 16-bit: jump offset, constant/string/func index
278//
279struct Instruction
280{
281 OpCode op;
282 uint8_t a; // dst / reg
283 uint8_t b; // src1 / base
284 uint8_t c; // src2 / nargs
285 int16_t d; // offset / constIdx / funcIdx / nameIdx
286 uint8_t e; // 5th operand (nout for CALL_MULTI, etc.)
287
288 Instruction()
289 : op(OpCode::HALT)
290 , a(0)
291 , b(0)
292 , c(0)
293 , d(0)
294 , e(0)
295 {}
296
297 static Instruction make_abcde(OpCode op, uint8_t a, uint8_t b, uint8_t c, int16_t d, uint8_t e)
298 {
299 Instruction i;
300 i.op = op;
301 i.a = a;
302 i.b = b;
303 i.c = c;
304 i.d = d;
305 i.e = e;
306 return i;
307 }
308 static Instruction make_abc(OpCode op, uint8_t a, uint8_t b, uint8_t c)
309 {
310 Instruction i;
311 i.op = op;
312 i.a = a;
313 i.b = b;
314 i.c = c;
315 return i;
316 }
317 static Instruction make_abd(OpCode op, uint8_t a, uint8_t b, int16_t d)
318 {
319 Instruction i;
320 i.op = op;
321 i.a = a;
322 i.b = b;
323 i.d = d;
324 return i;
325 }
326 static Instruction make_ad(OpCode op, uint8_t a, int16_t d)
327 {
328 Instruction i;
329 i.op = op;
330 i.a = a;
331 i.d = d;
332 return i;
333 }
334 static Instruction make_a(OpCode op, uint8_t a)
335 {
336 Instruction i;
337 i.op = op;
338 i.a = a;
339 return i;
340 }
341 static Instruction make_d(OpCode op, int16_t d)
342 {
343 Instruction i;
344 i.op = op;
345 i.d = d;
346 return i;
347 }
348 static Instruction make_none(OpCode op)
349 {
350 Instruction i;
351 i.op = op;
352 return i;
353 }
354};
355
356static_assert(sizeof(Instruction) == 8, "Instruction must be 8 bytes");
357
358// ============================================================
359// Source location for debugging / error reporting
360// ============================================================
362{
363 uint16_t line = 0;
364 uint16_t col = 0;
365};
366
367// ============================================================
368// BytecodeChunk: compiled function or script
369// ============================================================
371{
372 std::vector<Instruction> code;
373
374 // Constant pools
375 std::vector<Value> constants; // numeric/complex constants
376 std::vector<std::string> strings; // string constants + field/variable/function names
377
378 // Metadata
379 std::string name; // function name or "<script>"
380 uint8_t numRegisters = 0; // total registers needed
381 uint8_t numParams = 0;
382 uint8_t numReturns = 0;
383 std::vector<std::string> paramNames;
384 std::vector<std::string> returnNames;
385
386 // Closure support: indices of captured variables from parent scope
387 std::vector<uint8_t> capturedRegisters;
388
389 // Variable name → register mapping (for exporting to environment after execution)
390 std::vector<std::pair<std::string, uint8_t>> varMap;
391
392 // Names of variables the compiler emits a write for (STORE, param intro,
393 // for-loop var, catch var, global/persistent decl, etc). Reads alone do
394 // not populate this set. Used by DebugWorkspace::names() to decide if a
395 // built-in (pi, eps, …) should be visible — MATLAB only lists built-ins
396 // in `whos` once they've been shadowed by an assignment.
397 std::unordered_set<std::string> assignedVars;
398
399 // Global variable names this chunk routes through globalsEnv_: this chunk's
400 // own `global X` declarations PLUS the base workspace's inherited globals
401 // (seeded at compile so `gv = 5` without a re-declaration still writes the
402 // global). Used by the import/export paths.
403 std::vector<std::string> globalNames;
404
405 // Just this chunk's OWN top-level `global X` declarations (a subset of
406 // globalNames, excluding the inherited seed). Engine::runOneChunk uses it to
407 // record base-workspace membership and seed a freshly declared global as [].
408 // Excluding the inherited seed is what keeps `clear global g` from being
409 // resurrected by a later chunk that merely inherited g.
410 std::vector<std::string> ownGlobalDecls;
411
412 // Per-CALL-site arg names — populated by the compiler for each
413 // CALL / CALL_MULTI it emits. Key = index into `code` of the CALL
414 // instruction. Value = list of arg names; entry i is the name of
415 // the i-th arg if it was a bare identifier at the call site,
416 // empty string otherwise. Used by `inputname(k)` to introspect
417 // the caller's call site. Sparse on purpose — only populated for
418 // calls that have at least one bare-identifier arg.
419 std::unordered_map<uint32_t, std::vector<std::string>> callSiteArgNames;
420
421 // Source mapping (parallel to code, same size — one entry per instruction)
422 std::vector<SourceLoc> sourceMap;
423
424 // Original source text (shared across chunks compiled from the same source)
425 std::shared_ptr<const std::string> sourceCode;
426};
427
428} // namespace nk