nk
1.0.0
C++ Numerical Computing Runtime
Toggle main menu visibility
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
12
namespace
nk {
13
14
enum 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
//
279
struct
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
356
static_assert
(
sizeof
(
Instruction
) == 8,
"Instruction must be 8 bytes"
);
357
358
// ============================================================
359
// Source location for debugging / error reporting
360
// ============================================================
361
struct
SourceLoc
362
{
363
uint16_t line = 0;
364
uint16_t col = 0;
365
};
366
367
// ============================================================
368
// BytecodeChunk: compiled function or script
369
// ============================================================
370
struct
BytecodeChunk
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
nk::BytecodeChunk
Definition
bytecode.hpp:371
nk::Instruction
Definition
bytecode.hpp:280
nk::SourceLoc
Definition
bytecode.hpp:362
src
core
include
nk
core
bytecode.hpp
Generated by
1.17.0