vectrojs 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/LICENSE +21 -0
  3. package/README.md +822 -2
  4. package/bin/vectro.mjs +82 -0
  5. package/dist/vectro.wasm +0 -0
  6. package/mcp/server.mjs +288 -0
  7. package/package.json +91 -4
  8. package/src/arrow.mjs +905 -0
  9. package/src/bitpack.mjs +135 -0
  10. package/src/browser.mjs +129 -0
  11. package/src/bytes.mjs +60 -0
  12. package/src/csv-kernel.mjs +205 -0
  13. package/src/csv.mjs +270 -0
  14. package/src/dataset.mjs +1763 -0
  15. package/src/dates.mjs +139 -0
  16. package/src/emitter.mjs +109 -0
  17. package/src/engine.mjs +881 -0
  18. package/src/files-browser.mjs +104 -0
  19. package/src/files-node.mjs +59 -0
  20. package/src/frames.mjs +191 -0
  21. package/src/fuse.mjs +512 -0
  22. package/src/interp.mjs +305 -0
  23. package/src/join.mjs +534 -0
  24. package/src/kernels.mjs +312 -0
  25. package/src/ndjson.mjs +136 -0
  26. package/src/node.mjs +120 -0
  27. package/src/pack.mjs +114 -0
  28. package/src/parallel-sort.mjs +99 -0
  29. package/src/partition-kernels.mjs +388 -0
  30. package/src/partition.mjs +395 -0
  31. package/src/plan.mjs +551 -0
  32. package/src/portable.mjs +21 -0
  33. package/src/rank-kernels.mjs +81 -0
  34. package/src/rows.mjs +91 -0
  35. package/src/serve/catalog.mjs +148 -0
  36. package/src/serve/flight.mjs +270 -0
  37. package/src/serve/formats.mjs +87 -0
  38. package/src/serve/http.mjs +182 -0
  39. package/src/serve/index.mjs +9 -0
  40. package/src/serve/odata.mjs +291 -0
  41. package/src/serve/pg.mjs +588 -0
  42. package/src/serve/pgcatalog.mjs +220 -0
  43. package/src/serve/pgtypes.mjs +199 -0
  44. package/src/sketch.mjs +75 -0
  45. package/src/sort-kernels.mjs +117 -0
  46. package/src/sort.mjs +102 -0
  47. package/src/split-worker-core.mjs +252 -0
  48. package/src/split-worker-node.mjs +8 -0
  49. package/src/split-worker.mjs +7 -0
  50. package/src/split.mjs +741 -0
  51. package/src/sql.mjs +1558 -0
  52. package/src/sqltext.mjs +132 -0
  53. package/src/static-kernels.mjs +164 -0
  54. package/src/stream-worker.mjs +553 -0
  55. package/src/table-kernels.mjs +323 -0
  56. package/src/table.mjs +2206 -0
  57. package/src/textmap.mjs +159 -0
  58. package/src/vectors.mjs +138 -0
  59. package/src/wasm.mjs +254 -0
  60. package/src/window.mjs +160 -0
  61. package/src/worker-browser.mjs +21 -0
  62. package/src/worker-loop.mjs +106 -0
  63. package/src/worker.mjs +22 -0
  64. package/src/zones.mjs +75 -0
package/src/interp.mjs ADDED
@@ -0,0 +1,305 @@
1
+ // Vectorized interpreter: runs expressions, group-by and filtered copies with
2
+ // only the precompiled kernels in static-kernels.mjs, for places that forbid
3
+ // generating code at runtime (Cloudflare Workers, Vercel Edge).
4
+ //
5
+ // An expression becomes a list of vector operations over registers. Rows are
6
+ // processed in blocks of CHUNK: every operation runs over the whole block with
7
+ // SIMD before the next one starts, so the cost of interpreting is paid once
8
+ // per block instead of once per row. Registers for intermediate results live
9
+ // in a scratch area small enough to stay in the CPU cache.
10
+
11
+ import { walk, isMask } from './fuse.mjs';
12
+
13
+ export const CHUNK = 4096;
14
+ const ARITH = { '+': 'add', '-': 'sub', '*': 'mul', '/': 'div' };
15
+ // Operations on constants are worked out once, while compiling (constant
16
+ // folding), with the same results the vector kernels would give.
17
+ const FOLD = {
18
+ add: (a, b) => a + b, sub: (a, b) => a - b, mul: (a, b) => a * b, div: (a, b) => a / b,
19
+ min: Math.min, max: Math.max, neg: (a) => -a, abs: Math.abs, sqrt: Math.sqrt, floor: Math.floor, ceil: Math.ceil,
20
+ eq: (a, b) => +(a === b), ne: (a, b) => +(a !== b), lt: (a, b) => +(a < b), gt: (a, b) => +(a > b),
21
+ le: (a, b) => +(a <= b), ge: (a, b) => +(a >= b), sel: (m, a, b) => (m !== 0 ? a : b),
22
+ };
23
+ const CMP = { '==': 'eq', '!=': 'ne', '<': 'lt', '>': 'gt', '<=': 'le', '>=': 'ge' };
24
+
25
+ // Registers: { k: 'col', name } a slice of a column (no copy) · { k: 'const', i }
26
+ // a block filled with one value · { k: 'tmp', i } an intermediate result.
27
+ class Program {
28
+ constructor(t) {
29
+ this.t = t;
30
+ this.ops = []; // { fn, dst, args }
31
+ this.nTmp = 0;
32
+ this.consts = []; // { value } or { param }
33
+ this.constIndex = new Map();
34
+ this.memo = new Map(); // value numbering: the same op on the same inputs is computed once
35
+ }
36
+
37
+ #const(key, entry) {
38
+ if (!this.constIndex.has(key)) { this.constIndex.set(key, this.consts.length); this.consts.push(entry); }
39
+ return { k: 'const', i: this.constIndex.get(key) };
40
+ }
41
+ num(v) { return this.#const(`n:${Object.is(v, -0) ? '-0' : v}`, { value: v }); }
42
+ #op(name, args) {
43
+ if (args.every((r) => r.k === 'const' && 'value' in this.consts[r.i])) {
44
+ const R = this.t === 'f32' ? Math.fround : (x) => x; // constants are stored at the column type
45
+ return this.num(R(FOLD[name](...args.map((r) => R(this.consts[r.i].value)))));
46
+ }
47
+ const key = `${name}(${args.map((r) => (r.k === 'col' ? `c:${r.name}` : `${r.k[0]}${r.i}`)).join(',')})`;
48
+ const seen = this.memo.get(key);
49
+ if (seen) return seen;
50
+ const dst = { k: 'tmp', i: this.nTmp++ };
51
+ this.ops.push({ fn: `v${name}_${this.t}`, dst, args });
52
+ this.memo.set(key, dst);
53
+ return dst;
54
+ }
55
+
56
+ // true/false as 1/0: masks already are; numbers count as true when != 0
57
+ truth(n, isColumn) {
58
+ const r = this.emit(n, isColumn);
59
+ return isMask(n) ? r : this.#op('ne', [r, this.num(0)]);
60
+ }
61
+
62
+ emit(n, isColumn) {
63
+ switch (n.t) {
64
+ case 'num': return this.num(n.v);
65
+ case 'var': return isColumn(n.name) ? { k: 'col', name: n.name } : this.#const(`p:${n.name}`, { param: n.name });
66
+ case 'un':
67
+ return n.op === '-' ? this.#op('neg', [this.emit(n.x, isColumn)]) : this.#op('eq', [this.emit(n.x, isColumn), this.num(0)]);
68
+ case 'bin':
69
+ if (ARITH[n.op]) return this.#op(ARITH[n.op], [this.emit(n.l, isColumn), this.emit(n.r, isColumn)]);
70
+ if (CMP[n.op]) return this.#op(CMP[n.op], [this.emit(n.l, isColumn), this.emit(n.r, isColumn)]);
71
+ return this.#op(n.op === '&&' ? 'mul' : 'max', [this.truth(n.l, isColumn), this.truth(n.r, isColumn)]);
72
+ case 'call':
73
+ return this.#op(n.fn, n.args.map((a) => this.emit(a, isColumn)));
74
+ case 'cond':
75
+ return this.#op('sel', [this.emit(n.c, isColumn), this.emit(n.x, isColumn), this.emit(n.y, isColumn)]);
76
+ }
77
+ throw new Error(`vectro: can't interpret node ${n.t}`);
78
+ }
79
+ }
80
+
81
+ // Runs a compiled program block by block. `cols` maps names to arrays in
82
+ // engine memory; `params` holds scalar values.
83
+ class Runner {
84
+ static bytes(program) {
85
+ const bpe = program.t === 'f32' ? 4 : 8;
86
+ return CHUNK * Math.max(1, program.nTmp + program.consts.length) * bpe;
87
+ }
88
+
89
+ // `base`: where this program's registers go (default: the scratch area).
90
+ constructor(engine, program, cols, params, n, base = null) {
91
+ this.engine = engine;
92
+ this.p = program;
93
+ this.n = n;
94
+ this.Type = program.t === 'f32' ? Float32Array : Float64Array;
95
+ this.bpe = this.Type.BYTES_PER_ELEMENT;
96
+ // Get scratch first: if it ever has to grow it moves array views.
97
+ this.base = base ?? engine.interpreterScratch(Runner.bytes(program));
98
+ this.tmpBase = this.base + program.consts.length * CHUNK * this.bpe;
99
+ const mem = engine.memory.buffer;
100
+ program.consts.forEach((c, i) => {
101
+ const v = 'param' in c ? params[c.param] : c.value;
102
+ new this.Type(mem, this.base + i * CHUNK * this.bpe, CHUNK).fill(v);
103
+ });
104
+ this.colPtr = new Map();
105
+ for (const [name, arr] of Object.entries(cols)) {
106
+ if (ArrayBuffer.isView(arr)) this.colPtr.set(name, engine.live(arr).byteOffset);
107
+ }
108
+ this.W = engine.kernels;
109
+ }
110
+
111
+ ptr(r, start) {
112
+ if (r.k === 'col') return this.colPtr.get(r.name) + start * this.bpe;
113
+ if (r.k === 'const') return this.base + r.i * CHUNK * this.bpe;
114
+ if (r.k === 'out') return r.ptr + start * this.bpe;
115
+ return this.tmpBase + r.i * CHUNK * this.bpe;
116
+ }
117
+
118
+ // Run every op for the block [start, start + len). `redirect` sends one
119
+ // tmp register straight to an output array instead of scratch.
120
+ block(start, len, redirect) {
121
+ const W = this.W;
122
+ for (const op of this.p.ops) {
123
+ const dst = redirect && op.dst === redirect.reg ? redirect.out : op.dst;
124
+ const d = this.ptr(dst, start);
125
+ const a = op.args;
126
+ if (a.length === 1) W[op.fn](d, this.ptr(a[0], start), len);
127
+ else if (a.length === 2) W[op.fn](d, this.ptr(a[0], start), this.ptr(a[1], start), len);
128
+ else W[op.fn](d, this.ptr(a[0], start), this.ptr(a[1], start), this.ptr(a[2], start), len);
129
+ }
130
+ }
131
+
132
+ done() {}
133
+ }
134
+
135
+ const isColumnOf = (cols) => (name) => ArrayBuffer.isView(cols[name]);
136
+
137
+ // Reductions: items = [{ reduce: 'sum'|'count'|'mean'|'min'|'max', ast }].
138
+ // Returns one value per item (mean = sum / n).
139
+ export function interpretReduce(engine, t, items, cols, params, n) {
140
+ const p = new Program(t);
141
+ const isColumn = isColumnOf(cols);
142
+ const regs = items.map((it) => p.emit(it.ast, isColumn));
143
+ const run = new Runner(engine, p, cols, params, n);
144
+ const W = engine.kernels;
145
+ const sum = t === 'f32' ? W.vsumw_f32 : W.sum_f64;
146
+ const acc = items.map((it) => (it.reduce === 'min' ? Infinity : it.reduce === 'max' ? -Infinity : 0));
147
+ try {
148
+ for (let start = 0; start < n; start += CHUNK) {
149
+ const len = Math.min(CHUNK, n - start);
150
+ run.block(start, len);
151
+ items.forEach((it, j) => {
152
+ const ptr = run.ptr(regs[j], start);
153
+ if (it.reduce === 'min') acc[j] = Math.min(acc[j], W[`min_${t}`](ptr, len));
154
+ else if (it.reduce === 'max') acc[j] = Math.max(acc[j], W[`max_${t}`](ptr, len));
155
+ else acc[j] += sum(ptr, len);
156
+ });
157
+ }
158
+ } finally {
159
+ run.done();
160
+ }
161
+ return items.map((it, j) => (it.reduce === 'mean' ? acc[j] / n : acc[j]));
162
+ }
163
+
164
+ // Reductions under a filter. Per block the filter is evaluated first; then
165
+ // - few rows match (< 25%): the matching rows of the columns the reductions
166
+ // need are packed together (compact kernel) and reduced on their own
167
+ // ("filter pushdown"), so the math only touches matching rows;
168
+ // - many rows match: packing would cost more than it saves, so every row is
169
+ // computed and non-matching ones are replaced by the reduction's identity
170
+ // (sum(p ? e : 0), min(p ? e : Infinity), ...).
171
+ // items: [{ reduce, ast }]; ast null = count of matching rows.
172
+ export function interpretFilteredReduce(engine, t, pred, items, cols, params, n) {
173
+ const isColumn = isColumnOf(cols);
174
+ const A = new Program(t); // the filter
175
+ const mask = A.truth(pred, isColumn);
176
+ const B = new Program(t); // each value, over packed rows
177
+ const packedRegs = items.map((it) => (it.ast ? B.emit(it.ast, isColumn) : null));
178
+ // each value with the filter applied, over all rows; it reads the mask A
179
+ // already computed through a pseudo-column (MASK) instead of redoing it
180
+ const D = new Program(t);
181
+ const MASK = '\0mask';
182
+ const identity = { min: Infinity, max: -Infinity };
183
+ const denseRegs = items.map((it) => (it.ast
184
+ ? D.emit({ t: 'cond', c: { t: 'var', name: MASK }, x: it.ast, y: { t: 'num', v: identity[it.reduce] ?? 0 } }, (nm) => nm === MASK || isColumn(nm))
185
+ : null));
186
+ const used = [...new Set(items.flatMap((it) => (it.ast ? varsOf(it.ast) : [])))].filter(isColumn);
187
+
188
+ const bpe = t === 'f32' ? 4 : 8;
189
+ const [aBytes, bBytes, dBytes] = [Runner.bytes(A), Runner.bytes(B), Runner.bytes(D)];
190
+ const base = engine.interpreterScratch(aBytes + bBytes + dBytes + Math.max(1, used.length) * CHUNK * bpe);
191
+ const runA = new Runner(engine, A, cols, params, n, base);
192
+ const runB = new Runner(engine, B, {}, params, n, base + aBytes);
193
+ const runD = new Runner(engine, D, cols, params, n, base + aBytes + bBytes);
194
+ const packed = base + aBytes + bBytes + dBytes;
195
+ runB.colPtr = new Map(used.map((nm, k) => [nm, packed + k * CHUNK * bpe])); // packed rows start at 0
196
+ const src = used.map((nm) => runA.colPtr.get(nm));
197
+
198
+ const W = engine.kernels;
199
+ const sum = t === 'f32' ? W.vsumw_f32 : W.sum_f64;
200
+ const acc = items.map((it) => identity[it.reduce] ?? 0);
201
+ const reduceInto = (j, it, ptr, len) => {
202
+ if (it.reduce === 'min') acc[j] = Math.min(acc[j], W[`min_${t}`](ptr, len));
203
+ else if (it.reduce === 'max') acc[j] = Math.max(acc[j], W[`max_${t}`](ptr, len));
204
+ else acc[j] += sum(ptr, len);
205
+ };
206
+ let matched = 0;
207
+ for (let start = 0; start < n; start += CHUNK) {
208
+ const len = Math.min(CHUNK, n - start);
209
+ runA.block(start, len);
210
+ const m0 = runA.ptr(mask, start);
211
+ const m = sum(m0, len); // the mask is 1 or 0 per row
212
+ matched += m;
213
+ if (m === 0) continue;
214
+ if (m < len / 4) {
215
+ used.forEach((_, k) => W[`compact_${t}`](m0, src[k] + start * bpe, packed + k * CHUNK * bpe, len));
216
+ runB.block(0, m);
217
+ items.forEach((it, j) => { if (it.ast) reduceInto(j, it, runB.ptr(packedRegs[j], 0), m); });
218
+ } else {
219
+ runD.colPtr.set(MASK, m0 - start * bpe); // the mask block, addressed like a column
220
+ runD.block(start, len);
221
+ items.forEach((it, j) => { if (it.ast) reduceInto(j, it, runD.ptr(denseRegs[j], start), len); });
222
+ }
223
+ }
224
+ return items.map((it, j) => (!it.ast ? matched : it.reduce === 'mean' ? acc[j] / matched : acc[j]));
225
+ }
226
+
227
+ // Elementwise: out[i] = ast(row i).
228
+ export function interpretMap(engine, t, ast, cols, params, out, n) {
229
+ const p = new Program(t);
230
+ let reg = p.emit(ast, isColumnOf(cols));
231
+ if (reg.k !== 'tmp') reg = p.emit({ t: 'bin', op: '*', l: ast, r: { t: 'num', v: 1 } }, isColumnOf(cols)); // a copy: x * 1 keeps -0 and NaN
232
+ if (reg.k === 'const') { // a constant (folded): every row gets it
233
+ const c = p.consts[reg.i];
234
+ engine.live(out).fill('param' in c ? params[c.param] : c.value, 0, n);
235
+ return out;
236
+ }
237
+ const run = new Runner(engine, p, cols, params, n);
238
+ const outRef = { k: 'out', ptr: engine.live(out).byteOffset };
239
+ try {
240
+ for (let start = 0; start < n; start += CHUNK) run.block(start, Math.min(CHUNK, n - start), { reg, out: outRef });
241
+ } finally {
242
+ run.done();
243
+ }
244
+ return out;
245
+ }
246
+
247
+ // Group-by: for each row passing `pred`, acc[j*K + key] OP= item j's value.
248
+ export function interpretGroup(engine, t, items, pred, cols, params, key, acc, K, n) {
249
+ const p = new Program(t);
250
+ const isColumn = isColumnOf(cols);
251
+ const regs = items.map((it) => p.emit(it.ast, isColumn));
252
+ const mask = pred ? p.truth(pred, isColumn) : null;
253
+ const run = new Runner(engine, p, cols, params, n);
254
+ const W = engine.kernels;
255
+ const keyPtr = engine.live(key).byteOffset, accPtr = engine.live(acc).byteOffset;
256
+ const bpe = run.bpe;
257
+ try {
258
+ for (let start = 0; start < n; start += CHUNK) {
259
+ const len = Math.min(CHUNK, n - start);
260
+ run.block(start, len);
261
+ items.forEach((it, j) => {
262
+ const op = it.reduce === 'min' ? 'min' : it.reduce === 'max' ? 'max' : 'add';
263
+ const accJ = accPtr + j * K * 8;
264
+ if (mask) W[`g${op}m_${t}`](keyPtr + start * bpe, run.ptr(regs[j], start), run.ptr(mask, start), accJ, len);
265
+ else W[`g${op}_${t}`](keyPtr + start * bpe, run.ptr(regs[j], start), accJ, len);
266
+ });
267
+ }
268
+ } finally {
269
+ run.done();
270
+ }
271
+ }
272
+
273
+ // Filtered copy: returns the matching rows of each column in `names`.
274
+ export function interpretCompact(engine, t, pred, cols, params, names, n) {
275
+ const Type = t === 'f32' ? Float32Array : Float64Array;
276
+ const isColumn = isColumnOf(cols);
277
+ const count = interpretReduce(engine, t, [{ reduce: 'count', ast: isMask(pred) ? pred : { t: 'bin', op: '!=', l: pred, r: { t: 'num', v: 0 } } }], cols, params, n)[0];
278
+ const dst = names.map(() => engine.alloc(Type, count));
279
+ const p = new Program(t);
280
+ const mask = p.truth(pred, isColumn);
281
+ const run = new Runner(engine, p, cols, params, n);
282
+ const W = engine.kernels, bpe = run.bpe;
283
+ const dstPtr = dst.map((d) => engine.live(d).byteOffset);
284
+ const srcPtr = names.map((nm) => engine.live(cols[nm]).byteOffset);
285
+ let pos = 0;
286
+ try {
287
+ for (let start = 0; start < n; start += CHUNK) {
288
+ const len = Math.min(CHUNK, n - start);
289
+ run.block(start, len);
290
+ let wrote = 0;
291
+ names.forEach((_, k) => { wrote = W[`compact_${t}`](run.ptr(mask, start), srcPtr[k] + start * bpe, dstPtr[k] + pos * bpe, len); });
292
+ pos += wrote;
293
+ }
294
+ } finally {
295
+ run.done();
296
+ }
297
+ return dst.map((d) => engine.live(d)); // scratch allocation may have moved memory
298
+ }
299
+
300
+ // Every column an expression refers to.
301
+ export function varsOf(ast) {
302
+ const names = [];
303
+ walk(ast, (m) => { if (m.t === 'var' && !names.includes(m.name)) names.push(m.name); });
304
+ return names;
305
+ }