vectrojs 0.0.1 → 0.1.1

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 (63) hide show
  1. package/CHANGELOG.md +89 -0
  2. package/README.md +828 -4
  3. package/bin/vectro.mjs +82 -0
  4. package/dist/vectro.wasm +0 -0
  5. package/mcp/server.mjs +288 -0
  6. package/package.json +84 -6
  7. package/src/arrow.mjs +905 -0
  8. package/src/bitpack.mjs +135 -0
  9. package/src/browser.mjs +129 -0
  10. package/src/bytes.mjs +60 -0
  11. package/src/csv-kernel.mjs +205 -0
  12. package/src/csv.mjs +270 -0
  13. package/src/dataset.mjs +1763 -0
  14. package/src/dates.mjs +139 -0
  15. package/src/emitter.mjs +109 -0
  16. package/src/engine.mjs +881 -0
  17. package/src/files-browser.mjs +104 -0
  18. package/src/files-node.mjs +59 -0
  19. package/src/frames.mjs +191 -0
  20. package/src/fuse.mjs +512 -0
  21. package/src/interp.mjs +305 -0
  22. package/src/join.mjs +534 -0
  23. package/src/kernels.mjs +312 -0
  24. package/src/ndjson.mjs +136 -0
  25. package/src/node.mjs +120 -0
  26. package/src/pack.mjs +114 -0
  27. package/src/parallel-sort.mjs +99 -0
  28. package/src/partition-kernels.mjs +388 -0
  29. package/src/partition.mjs +395 -0
  30. package/src/plan.mjs +551 -0
  31. package/src/portable.mjs +21 -0
  32. package/src/rank-kernels.mjs +81 -0
  33. package/src/rows.mjs +91 -0
  34. package/src/serve/catalog.mjs +148 -0
  35. package/src/serve/flight.mjs +270 -0
  36. package/src/serve/formats.mjs +87 -0
  37. package/src/serve/http.mjs +182 -0
  38. package/src/serve/index.mjs +9 -0
  39. package/src/serve/odata.mjs +291 -0
  40. package/src/serve/pg.mjs +588 -0
  41. package/src/serve/pgcatalog.mjs +220 -0
  42. package/src/serve/pgtypes.mjs +199 -0
  43. package/src/sketch.mjs +75 -0
  44. package/src/sort-kernels.mjs +117 -0
  45. package/src/sort.mjs +102 -0
  46. package/src/split-worker-core.mjs +252 -0
  47. package/src/split-worker-node.mjs +8 -0
  48. package/src/split-worker.mjs +7 -0
  49. package/src/split.mjs +741 -0
  50. package/src/sql.mjs +1558 -0
  51. package/src/sqltext.mjs +132 -0
  52. package/src/static-kernels.mjs +164 -0
  53. package/src/stream-worker.mjs +553 -0
  54. package/src/table-kernels.mjs +323 -0
  55. package/src/table.mjs +2247 -0
  56. package/src/textmap.mjs +159 -0
  57. package/src/vectors.mjs +138 -0
  58. package/src/wasm.mjs +254 -0
  59. package/src/window.mjs +160 -0
  60. package/src/worker-browser.mjs +21 -0
  61. package/src/worker-loop.mjs +106 -0
  62. package/src/worker.mjs +22 -0
  63. package/src/zones.mjs +75 -0
package/src/fuse.mjs ADDED
@@ -0,0 +1,512 @@
1
+ // Kernel fusion: compile an expression like `sum(a * 2 + 1)` or
2
+ // `[sum(a), min(a), count(a > 500 && b < 3)]` into ONE WebAssembly SIMD
3
+ // function that reads each array once, instead of one pass per operation.
4
+
5
+ import { I32, F64, V128, TY, Fn, loops, buildModule, get, set, simd, vload, vstore, memarg } from './wasm.mjs';
6
+
7
+ // ---- parsing -----------------------------------------------------------------
8
+
9
+ const TOKEN = /\s*(?:(\d+\.?\d*(?:[eE][+-]?\d+)?|\.\d+(?:[eE][+-]?\d+)?)|([A-Za-z_]\w*)|(&&|\|\||==|!=|<=|>=|[-+*/()<>,?:!])|'([^']*)'|"([^"]*)")/y;
10
+
11
+ function tokenize(src) {
12
+ const out = [];
13
+ TOKEN.lastIndex = 0;
14
+ while (TOKEN.lastIndex < src.length && /\S/.test(src.slice(TOKEN.lastIndex))) {
15
+ const pos = TOKEN.lastIndex;
16
+ const m = TOKEN.exec(src);
17
+ if (!m) throw new SyntaxError(`vectro: unexpected "${src.slice(pos).trim()[0]}" in "${src}"`);
18
+ if (m[1] !== undefined) out.push({ t: 'num', v: Number(m[1]) });
19
+ else if (m[2] !== undefined) out.push({ t: 'id', v: m[2] });
20
+ else if (m[3] !== undefined) out.push({ t: 'op', v: m[3] });
21
+ else out.push({ t: 'str', v: m[4] ?? m[5] });
22
+ }
23
+ return out;
24
+ }
25
+
26
+ // Same precedence as JavaScript: relational binds tighter than equality.
27
+ const BIN_PREC = { '||': 2, '&&': 3, '==': 4, '!=': 4, '<': 5, '>': 5, '<=': 5, '>=': 5, '+': 6, '-': 6, '*': 7, '/': 7 };
28
+ const UNARY_PREC = 8;
29
+
30
+ export function parse(src) {
31
+ const toks = tokenize(src);
32
+ let i = 0;
33
+ const err = (msg) => new SyntaxError(`vectro: ${msg} in "${src}"`);
34
+ const peek = () => toks[i];
35
+ const expect = (v) => { const t = toks[i++]; if (!t || t.v !== v) throw err(`expected "${v}"`); };
36
+
37
+ function primary() {
38
+ const t = toks[i++];
39
+ if (!t) throw err('unexpected end');
40
+ if (t.t === 'num') return { t: 'num', v: t.v };
41
+ if (t.t === 'str') return { t: 'str', v: t.v };
42
+ if (t.t === 'id') {
43
+ if (peek()?.v !== '(') {
44
+ if (t.v === 'Infinity') return { t: 'num', v: Infinity };
45
+ if (t.v === 'NaN') return { t: 'num', v: NaN };
46
+ return { t: 'var', name: t.v };
47
+ }
48
+ i++;
49
+ const args = [];
50
+ if (peek()?.v !== ')') {
51
+ args.push(expr(0));
52
+ while (peek()?.v === ',') { i++; args.push(expr(0)); }
53
+ }
54
+ expect(')');
55
+ return { t: 'call', fn: t.v, args };
56
+ }
57
+ if (t.v === '(') { const e = expr(0); expect(')'); return e; }
58
+ if (t.v === '-' || t.v === '!') return { t: 'un', op: t.v, x: expr(UNARY_PREC) };
59
+ throw err(`unexpected "${t.v}"`);
60
+ }
61
+
62
+ function expr(minPrec) {
63
+ let left = primary();
64
+ for (;;) {
65
+ const t = peek();
66
+ if (!t || t.t !== 'op') break;
67
+ if (t.v === '?' && minPrec <= 1) {
68
+ i++;
69
+ const x = expr(1);
70
+ expect(':');
71
+ left = { t: 'cond', c: left, x, y: expr(1) };
72
+ continue;
73
+ }
74
+ const p = BIN_PREC[t.v];
75
+ if (!p || p <= minPrec) break;
76
+ i++;
77
+ left = { t: 'bin', op: t.v, l: left, r: expr(p) };
78
+ }
79
+ return left;
80
+ }
81
+
82
+ const ast = expr(0);
83
+ if (i < toks.length) throw err(`unexpected "${toks[i].v}"`);
84
+ return ast;
85
+ }
86
+
87
+ // ---- analysis ----------------------------------------------------------------
88
+
89
+ const REDUCTIONS = new Set(['sum', 'count', 'mean', 'min', 'max']);
90
+ const UNARY_FN = new Set(['abs', 'sqrt', 'floor', 'ceil']);
91
+ const BINARY_FN = new Set(['min', 'max']);
92
+ const CMP = { '==': 'eq', '!=': 'ne', '<': 'lt', '>': 'gt', '<=': 'le', '>=': 'ge' };
93
+ const ARITH = { '+': 'add', '-': 'sub', '*': 'mul', '/': 'div' };
94
+
95
+ const isReduction = (n) => n.t === 'call' && REDUCTIONS.has(n.fn) && n.args.length === 1;
96
+ // Masks are true/false values (comparisons and logic); everything else is a number.
97
+ export const isMask = (n) => (n.t === 'bin' && (CMP[n.op] || n.op === '&&' || n.op === '||')) || (n.t === 'un' && n.op === '!');
98
+
99
+ // Text as the tokenizer reads it back exactly (no escapes inside quotes).
100
+ export function quoteText(v) {
101
+ if (!v.includes("'")) return `'${v}'`;
102
+ if (!v.includes('"')) return `"${v}"`;
103
+ throw new SyntaxError(`vectro: text with both ' and " can't be used in an expression: ${JSON.stringify(v)}`);
104
+ }
105
+
106
+ // Turn an AST back into source (fully parenthesized), e.g. after rewriting it.
107
+ export function toSource(n) {
108
+ switch (n.t) {
109
+ case 'num': return Number.isNaN(n.v) ? 'NaN' : n.v === Infinity ? 'Infinity' : n.v === -Infinity ? '(-Infinity)' : n.v < 0 ? `(${n.v})` : String(n.v);
110
+ case 'str': return quoteText(n.v);
111
+ case 'var': return n.name;
112
+ case 'un': return `${n.op}(${toSource(n.x)})`;
113
+ case 'bin': return `(${toSource(n.l)} ${n.op} ${toSource(n.r)})`;
114
+ case 'call': return `${n.fn}(${n.args.map(toSource).join(', ')})`;
115
+ case 'cond': return `(${toSource(n.c)} ? ${toSource(n.x)} : ${toSource(n.y)})`;
116
+ }
117
+ throw new Error(`bad node ${n.t}`);
118
+ }
119
+
120
+ export function walk(n, f) {
121
+ f(n);
122
+ if (n.t === 'bin') { walk(n.l, f); walk(n.r, f); }
123
+ else if (n.t === 'un') walk(n.x, f);
124
+ else if (n.t === 'call') n.args.forEach((a) => walk(a, f));
125
+ else if (n.t === 'cond') { walk(n.c, f); walk(n.x, f); walk(n.y, f); }
126
+ }
127
+
128
+ function checkElementwise(n, src) {
129
+ walk(n, (m) => {
130
+ if (m.t === 'str') throw new SyntaxError(`vectro: text like ${JSON.stringify(m.v)} can only be compared (== / !=) with a text column, in "${src}"`);
131
+ if (m.t !== 'call') return;
132
+ if (isReduction(m)) throw new SyntaxError(`vectro: ${m.fn}() must be the outermost call in "${src}"`);
133
+ const ok = (UNARY_FN.has(m.fn) && m.args.length === 1) || (BINARY_FN.has(m.fn) && m.args.length === 2);
134
+ if (!ok) throw new SyntaxError(`vectro: unknown function ${m.fn}() with ${m.args.length} argument(s) in "${src}"`);
135
+ });
136
+ }
137
+
138
+ // Parse a spec into either one elementwise map or a list of reductions.
139
+ // spec: string | string[] | { name: string }
140
+ export function analyze(spec) {
141
+ const entries = typeof spec === 'string' ? [[null, spec]] : Array.isArray(spec) ? spec.map((s, i) => [i, s]) : Object.entries(spec);
142
+ const items = entries.map(([key, src]) => {
143
+ const ast = parse(src);
144
+ if (isReduction(ast)) {
145
+ let arg = ast.args[0];
146
+ checkElementwise(arg, src);
147
+ arg = fold(arg);
148
+ if (ast.fn === 'count' && !isMask(arg)) arg = { t: 'bin', op: '!=', l: arg, r: { t: 'num', v: 0 } };
149
+ return { key, src, reduce: ast.fn, ast: arg, raw: ast.args[0] };
150
+ }
151
+ checkElementwise(ast, src);
152
+ return { key, src, reduce: null, ast: fold(ast), raw: ast };
153
+ });
154
+ const maps = items.filter((it) => !it.reduce);
155
+ if (maps.length && items.length > 1) throw new SyntaxError('vectro: an elementwise expression must be compiled on its own (one output array)');
156
+ if (items.filter((it) => it.reduce).length > 8) throw new RangeError('vectro: at most 8 reductions per kernel');
157
+
158
+ const vars = [];
159
+ for (const it of items) walk(it.raw, (n) => { if (n.t === 'var' && !vars.includes(n.name)) vars.push(n.name); }); // (before folding: the inputs stay the same)
160
+ return { shape: typeof spec === 'string' ? 'single' : Array.isArray(spec) ? 'array' : 'object', items, vars, isMap: maps.length === 1 };
161
+ }
162
+
163
+ // Constant parts computed now: 2 * 3 -> 6, floor(2.5) -> 2, (1 ? x : y) -> x.
164
+ // Same results as the kernels (IEEE doubles; f32 kernels round literals).
165
+ const FOLD_BIN = { '+': (a, b) => a + b, '-': (a, b) => a - b, '*': (a, b) => a * b, '/': (a, b) => a / b };
166
+ const FOLD_FN = { abs: Math.abs, sqrt: Math.sqrt, floor: Math.floor, ceil: Math.ceil, min: Math.min, max: Math.max };
167
+ export function fold(n) {
168
+ const isNum = (x) => x.t === 'num';
169
+ switch (n.t) {
170
+ case 'un': {
171
+ const x = fold(n.x);
172
+ if (isNum(x)) return { t: 'num', v: n.op === '-' ? -x.v : +(x.v === 0) };
173
+ return { ...n, x };
174
+ }
175
+ case 'bin': {
176
+ const l = fold(n.l), r = fold(n.r);
177
+ if (isNum(l) && isNum(r) && FOLD_BIN[n.op]) return { t: 'num', v: FOLD_BIN[n.op](l.v, r.v) };
178
+ return { ...n, l, r };
179
+ }
180
+ case 'call': {
181
+ const args = n.args.map(fold);
182
+ if (FOLD_FN[n.fn] && args.every(isNum)) return { t: 'num', v: FOLD_FN[n.fn](...args.map((a) => a.v)) };
183
+ return { ...n, args };
184
+ }
185
+ case 'cond': {
186
+ const c = fold(n.c), x = fold(n.x), y = fold(n.y);
187
+ if (isNum(c)) return c.v !== 0 ? x : y; // (as in the kernels: NaN != 0, so NaN counts as true)
188
+ return { ...n, c, x, y };
189
+ }
190
+ }
191
+ return n;
192
+ }
193
+
194
+ // ---- code generation ---------------------------------------------------------
195
+
196
+ export const VEC = {
197
+ f64: { add: 0xf0, sub: 0xf1, mul: 0xf2, div: 0xf3, min: 0xf4, max: 0xf5, abs: 0xec, neg: 0xed, sqrt: 0xef, ceil: 0x74, floor: 0x75, eq: 0x47, ne: 0x48, lt: 0x49, gt: 0x4a, le: 0x4b, ge: 0x4c },
198
+ f32: { add: 0xe4, sub: 0xe5, mul: 0xe6, div: 0xe7, min: 0xe8, max: 0xe9, abs: 0xe0, neg: 0xe1, sqrt: 0xe3, ceil: 0x67, floor: 0x68, eq: 0x41, ne: 0x42, lt: 0x43, gt: 0x44, le: 0x45, ge: 0x46 },
199
+ };
200
+ export const SCA = {
201
+ f64: { add: 0xa0, sub: 0xa1, mul: 0xa2, div: 0xa3, min: 0xa4, max: 0xa5, abs: 0x99, neg: 0x9a, sqrt: 0x9f, ceil: 0x9b, floor: 0x9c, eq: 0x61, ne: 0x62, lt: 0x63, gt: 0x64, le: 0x65, ge: 0x66, fromBool: 0xb8, fromF64: [] },
202
+ f32: { add: 0x92, sub: 0x93, mul: 0x94, div: 0x95, min: 0x96, max: 0x97, abs: 0x8b, neg: 0x8c, sqrt: 0x91, ceil: 0x8d, floor: 0x8e, eq: 0x5b, ne: 0x5c, lt: 0x5d, gt: 0x5e, le: 0x5f, ge: 0x60, fromBool: 0xb3, fromF64: [0xb6] },
203
+ };
204
+ const V128_NOT = 0x4d, V128_AND = 0x4e, V128_OR = 0x50, BITSELECT = 0x52;
205
+ const F64X2_PROMOTE_LOW = 0x5f, F64X2_ADD = 0xf0, F64X2_MIN = 0xf4, F64X2_MAX = 0xf5;
206
+ const I32_AND = 0x71, I32_OR = 0x72, I32_EQZ = 0x45, SELECT = 0x1b, F64_PROMOTE_F32 = 0xbb;
207
+ const F64_OPS = { add: 0xa0, min: 0xa4, max: 0xa5 };
208
+ const HIGH_HALF = [8, 9, 10, 11, 12, 13, 14, 15, 0, 1, 2, 3, 4, 5, 6, 7]; // i8x16.shuffle lanes
209
+
210
+ // Emits code for one AST in vector mode (v128, k = unroll slot) or scalar mode.
211
+ // Repeated subexpressions: after prepare(asts), a value subtree that occurs
212
+ // more than once in the kernel is computed once per row (per vector) into a
213
+ // local and read back after that; call reset() at the start of each loop body.
214
+ export class Emitter {
215
+ constructor(f, t, env) {
216
+ this.f = f;
217
+ this.t = t;
218
+ this.ty = TY[t];
219
+ this.env = env;
220
+ this.repeated = new Set();
221
+ this.memo = new Map();
222
+ }
223
+
224
+ prepare(asts) {
225
+ const seen = new Map();
226
+ const visit = (n) => {
227
+ if (n.t === 'num' || n.t === 'var' || n.t === 'str') return;
228
+ const key = toSource(n);
229
+ seen.set(key, (seen.get(key) ?? 0) + 1);
230
+ if (seen.get(key) > 1) return; // (its parts are counted once)
231
+ if (n.t === 'bin') { visit(n.l); visit(n.r); } else if (n.t === 'un') visit(n.x);
232
+ else if (n.t === 'call') n.args.forEach(visit); else if (n.t === 'cond') { visit(n.c); visit(n.x); visit(n.y); }
233
+ };
234
+ for (const a of asts) if (a) visit(a);
235
+ for (const [key, c] of seen) if (c > 1) this.repeated.add(key);
236
+ this.keys = new Map();
237
+ }
238
+
239
+ reset() { this.memo.clear(); }
240
+
241
+ // emit(node) once, then from a local (key: subtree + unroll slot or 'sca')
242
+ #reuse(n, slot, type, emit) {
243
+ if (!this.repeated.size || n.t === 'num' || n.t === 'var') return emit();
244
+ let key = this.keys.get(n);
245
+ if (key === undefined) { key = toSource(n); this.keys.set(n, key); }
246
+ if (!this.repeated.has(key)) return emit();
247
+ const m = `${key}|${slot}`;
248
+ const local = this.memo.get(m);
249
+ if (local !== undefined) return this.f.emit(get(local));
250
+ emit();
251
+ this.locals ??= new Map();
252
+ let l = this.locals.get(m);
253
+ if (l === undefined) { l = this.f.local(type); this.locals.set(m, l); }
254
+ this.f.emit([0x22, ...get(l).slice(1)]); // local.tee
255
+ this.memo.set(m, l);
256
+ }
257
+
258
+ vec(n, k) { return this.#reuse(n, k, V128, () => this.#vec(n, k)); }
259
+ sca(n) { return this.#reuse(n, 'sca', this.ty.val, () => this.#sca(n)); }
260
+
261
+ #vec(n, k) {
262
+ const { f, env } = this, V = VEC[this.t];
263
+ switch (n.t) {
264
+ case 'num': return f.emit(get(env.constVec(n.v)));
265
+ case 'var': return f.emit(get(env.arrays.has(n.name) ? env.arrays.get(n.name).vec[k] : env.scalars.get(n.name).vec));
266
+ case 'un':
267
+ if (n.op === '-') { this.vec(n.x, k); return f.emit(simd(V.neg)); }
268
+ break;
269
+ case 'bin':
270
+ if (ARITH[n.op]) { this.vec(n.l, k); this.vec(n.r, k); return f.emit(simd(V[ARITH[n.op]])); }
271
+ break;
272
+ case 'call':
273
+ n.args.forEach((a) => this.vec(a, k));
274
+ return f.emit(simd(V[n.fn]));
275
+ case 'cond':
276
+ this.vec(n.x, k); this.vec(n.y, k); this.vecMask(n.c, k);
277
+ return f.emit(simd(BITSELECT));
278
+ }
279
+ // a mask used as a number: all-ones lanes AND 1.0 = 1.0, zero lanes stay 0.0
280
+ this.vecMask(n, k);
281
+ f.emit(get(env.constVec(1)), simd(V128_AND));
282
+ }
283
+
284
+ vecMask(n, k) {
285
+ const { f, env } = this, V = VEC[this.t];
286
+ if (n.t === 'bin' && CMP[n.op]) { this.vec(n.l, k); this.vec(n.r, k); return f.emit(simd(V[CMP[n.op]])); }
287
+ if (n.t === 'bin' && (n.op === '&&' || n.op === '||')) { this.vecMask(n.l, k); this.vecMask(n.r, k); return f.emit(simd(n.op === '&&' ? V128_AND : V128_OR)); }
288
+ if (n.t === 'un' && n.op === '!') { this.vecMask(n.x, k); return f.emit(simd(V128_NOT)); }
289
+ this.vec(n, k); // a number used as a condition: true when != 0
290
+ f.emit(get(env.constVec(0)), simd(V.ne));
291
+ }
292
+
293
+ #sca(n) {
294
+ const { f, env, ty } = this, S = SCA[this.t];
295
+ switch (n.t) {
296
+ case 'num': return f.emit(ty.cnst(n.v));
297
+ case 'var': return f.emit(get(env.arrays.has(n.name) ? env.arrays.get(n.name).sca : env.scalars.get(n.name).sca));
298
+ case 'un':
299
+ if (n.op === '-') { this.sca(n.x); return f.emit([S.neg]); }
300
+ break;
301
+ case 'bin':
302
+ if (ARITH[n.op]) { this.sca(n.l); this.sca(n.r); return f.emit([S[ARITH[n.op]]]); }
303
+ break;
304
+ case 'call':
305
+ n.args.forEach((a) => this.sca(a));
306
+ return f.emit([S[n.fn]]);
307
+ case 'cond':
308
+ this.sca(n.x); this.sca(n.y); this.scaMask(n.c);
309
+ return f.emit([SELECT]);
310
+ }
311
+ this.scaMask(n);
312
+ f.emit([S.fromBool]);
313
+ }
314
+
315
+ scaMask(n) {
316
+ const { f, ty } = this, S = SCA[this.t];
317
+ if (n.t === 'bin' && CMP[n.op]) { this.sca(n.l); this.sca(n.r); return f.emit([S[CMP[n.op]]]); }
318
+ if (n.t === 'bin' && (n.op === '&&' || n.op === '||')) { this.scaMask(n.l); this.scaMask(n.r); return f.emit([n.op === '&&' ? I32_AND : I32_OR]); }
319
+ if (n.t === 'un' && n.op === '!') { this.scaMask(n.x); return f.emit([I32_EQZ]); }
320
+ this.sca(n);
321
+ f.emit(ty.cnst(0), [S.ne]);
322
+ }
323
+ }
324
+
325
+ // Packed columns (see pack.mjs): load the codes of one vector's rows, widen
326
+ // them to 32-bit lanes, convert to floats, scale, and turn the missing code
327
+ // into NaN. off = byte offset of the vector's first row.
328
+ const PACKED_VEC = {
329
+ f64: { u8: (off) => [...simd(0x02, ...memarg(0, off)), ...simd(0xa9), ...simd(0xff)], // load8x8_u, extend_low_u, convert_low_u
330
+ u16: (off) => [...simd(0x5c, ...memarg(0, off)), ...simd(0xa9), ...simd(0xff)], // load32_zero
331
+ i32: (off) => [...simd(0x5d, ...memarg(0, off)), ...simd(0xfe)] }, // load64_zero, convert_low_s
332
+ f32: { u8: (off) => [...simd(0x5c, ...memarg(0, off)), ...simd(0x89), ...simd(0xa9), ...simd(0xfb)],
333
+ u16: (off) => [...simd(0x04, ...memarg(0, off)), ...simd(0xfb)], // load16x4_u
334
+ i32: (off) => [...simd(0x00, ...memarg(0, off)), ...simd(0xfa)] },
335
+ };
336
+ const PACKED_SCA = {
337
+ f64: { u8: [0x2d, 0, 0, 0xb8], u16: [0x2f, 1, 0, 0xb8], i32: [0x28, 2, 0, 0xb7] },
338
+ f32: { u8: [0x2d, 0, 0, 0xb3], u16: [0x2f, 1, 0, 0xb3], i32: [0x28, 2, 0, 0xb2] },
339
+ };
340
+ const PACKED_NAN = { u8: 255, u16: 65535, i32: -2147483648 };
341
+ export const PACKED_BYTES = { u8: 1, u16: 2, i32: 4 };
342
+
343
+ // One row of a packed column, decoded, on the stack (scalar kernels). raw and
344
+ // tmp: locals of the element type, used when the column has missing values.
345
+ export function packedScalar(f, t, enc, ptr, raw, tmp) {
346
+ const ty = TY[t], S = SCA[t];
347
+ const { kind, div, mul, base, nan } = enc;
348
+ f.emit(get(ptr), PACKED_SCA[t][kind]);
349
+ if (nan) f.emit([0x22, ...get(raw).slice(1)]); // local.tee
350
+ if (div) f.emit(ty.cnst(div), [S.div]);
351
+ else {
352
+ if (mul !== 1) f.emit(ty.cnst(mul), [S.mul]);
353
+ if (base !== 0) f.emit(ty.cnst(base), [S.add]);
354
+ }
355
+ if (nan) f.emit(set(tmp), ty.cnst(NaN), get(tmp), get(raw), ty.cnst(PACKED_NAN[kind]), [S.eq], [SELECT]);
356
+ }
357
+
358
+ // Build the kernel for one element type ('f64' | 'f32') and one binding of
359
+ // names to arrays/scalars. Signature: (array ptrs..., [out ptr], n, scalars...).
360
+ // encs[i]: the packing of array i (null: plain floats).
361
+ export function generate(analysis, t, arrayNames, scalarNames, shared = true, encs = []) {
362
+ const ty = TY[t];
363
+ const { items, isMap } = analysis;
364
+ const nPtr = arrayNames.length + (isMap ? 1 : 0);
365
+ const params = [...Array(nPtr).fill(I32), I32, ...Array(scalarNames.length).fill(F64)];
366
+ const reductions = isMap ? [] : items;
367
+ // Vectors per loop iteration. More gives the CPU independent additions to
368
+ // overlap, but every reduction needs U accumulators and ARM has only 32
369
+ // vector registers, so kernels with many reductions use fewer.
370
+ const U = isMap || reductions.length > 2 ? 2 : 4;
371
+ const f = new Fn('k', params, reductions.map(() => F64));
372
+ const ptrs = Array.from({ length: nPtr }, (_, i) => i);
373
+ const n = nPtr;
374
+
375
+ // Values reused across the loop live in locals: constants, scalars, loaded vectors.
376
+ const consts = new Map();
377
+ const env = {
378
+ arrays: new Map(arrayNames.map((name, i) => [name, { ptr: i, vec: [], sca: 0, enc: encs[i] ?? null }])),
379
+ scalars: new Map(),
380
+ constVec: (v) => {
381
+ if (!consts.has(v)) {
382
+ const l = f.local(V128);
383
+ f.emit(ty.cnst(v), ty.vsplat, set(l));
384
+ consts.set(v, l);
385
+ }
386
+ return consts.get(v);
387
+ },
388
+ };
389
+ for (const a of env.arrays.values()) {
390
+ for (let k = 0; k < U; k++) a.vec.push(f.local(V128));
391
+ a.sca = f.local(ty.val);
392
+ }
393
+ scalarNames.forEach((name, i) => {
394
+ const param = nPtr + 1 + i;
395
+ const vec = f.local(V128), sca = f.local(ty.val);
396
+ f.emit(get(param), SCA[t].fromF64, set(sca), get(sca), ty.vsplat, set(vec));
397
+ env.scalars.set(name, { vec, sca });
398
+ });
399
+ // Constants are created on first use, which must happen before the loop.
400
+ const literals = [0, 1];
401
+ for (const it of items) walk(it.ast, (m) => { if (m.t === 'num') literals.push(m.v); });
402
+ for (const a of env.arrays.values()) {
403
+ if (!a.enc) continue;
404
+ literals.push(a.enc.div || a.enc.mul, a.enc.base);
405
+ if (a.enc.nan) literals.push(NaN, PACKED_NAN[a.enc.kind]);
406
+ }
407
+ literals.forEach((v) => env.constVec(v));
408
+ const V = VEC[t], S = SCA[t];
409
+ const conv = f.local(V128), convS = f.local(ty.val);
410
+ // one vector of a packed column -> decoded values on the stack
411
+ const loadPacked = (a, k) => {
412
+ const { kind, div, mul, base, nan } = a.enc;
413
+ f.emit(get(a.ptr), PACKED_VEC[t][kind](k * ty.lanes * PACKED_BYTES[kind]));
414
+ if (nan) f.emit([0x22, ...get(conv).slice(1)]); // local.tee: keep the raw value for the missing test
415
+ if (div) f.emit(get(env.constVec(div)), simd(V.div));
416
+ else {
417
+ if (mul !== 1) f.emit(get(env.constVec(mul)), simd(V.mul));
418
+ if (base !== 0) f.emit(get(env.constVec(base)), simd(V.add));
419
+ }
420
+ if (nan) { // bitselect(NaN, value, raw == missing code)
421
+ f.emit(set(tmpDec), get(env.constVec(NaN)), get(tmpDec), get(conv), get(env.constVec(PACKED_NAN[kind])), simd(V.eq), simd(BITSELECT));
422
+ }
423
+ };
424
+ const loadPackedSca = (a) => {
425
+ const { kind, div, mul, base, nan } = a.enc;
426
+ f.emit(get(a.ptr), PACKED_SCA[t][kind]);
427
+ if (nan) f.emit([0x22, ...get(convS).slice(1)]);
428
+ if (div) f.emit(ty.cnst(div), [S.div]);
429
+ else {
430
+ if (mul !== 1) f.emit(ty.cnst(mul), [S.mul]);
431
+ if (base !== 0) f.emit(ty.cnst(base), [S.add]);
432
+ }
433
+ if (nan) f.emit(set(tmpDecS), ty.cnst(NaN), get(tmpDecS), get(convS), ty.cnst(PACKED_NAN[kind]), [S.eq], [SELECT]);
434
+ };
435
+ const tmpDec = f.local(V128), tmpDecS = f.local(ty.val);
436
+
437
+ const em = new Emitter(f, t, env);
438
+ em.prepare(items.map((it) => it.ast));
439
+ const tmp = f.local(V128);
440
+
441
+ // Accumulators. float64: f64x2 per slot. float32: sums widen to float64
442
+ // (two f64x2 per slot, no precision loss); min/max stay f32x4.
443
+ const accs = reductions.map((r) => {
444
+ const op = r.reduce === 'min' ? 'min' : r.reduce === 'max' ? 'max' : 'add';
445
+ const identity = op === 'min' ? Infinity : op === 'max' ? -Infinity : 0;
446
+ const wide = t === 'f32' && op === 'add';
447
+ const slots = Array.from({ length: wide ? 2 * U : U }, () => f.local(V128));
448
+ for (const s of slots) {
449
+ if (wide || t === 'f64') f.emit(TY.f64.cnst(identity), TY.f64.vsplat, set(s));
450
+ else f.emit(ty.cnst(identity), ty.vsplat, set(s));
451
+ }
452
+ return { r, op, wide, slots, s: f.local(F64) };
453
+ });
454
+
455
+ const out = ptrs[nPtr - 1];
456
+ loops(f, ty, {
457
+ ptrs, n, U,
458
+ strides: ptrs.map((i) => (encs[i] ? PACKED_BYTES[encs[i].kind] : ty.bytes)),
459
+ body: () => {
460
+ em.reset();
461
+ for (let k = 0; k < U; k++) {
462
+ for (const a of env.arrays.values()) {
463
+ if (a.enc) { loadPacked(a, k); f.emit(set(a.vec[k])); } else f.emit(get(a.ptr), vload(16 * k), set(a.vec[k]));
464
+ }
465
+ if (isMap) { f.emit(get(out)); em.vec(items[0].ast, k); f.emit(vstore(16 * k)); continue; }
466
+ for (const acc of accs) {
467
+ if (acc.wide) {
468
+ const [lo, hi] = [acc.slots[2 * k], acc.slots[2 * k + 1]];
469
+ em.vec(acc.r.ast, k);
470
+ f.emit(set(tmp));
471
+ f.emit(get(lo), get(tmp), simd(F64X2_PROMOTE_LOW), simd(F64X2_ADD), set(lo));
472
+ f.emit(get(hi), get(tmp), get(tmp), simd(0x0d, ...HIGH_HALF), simd(F64X2_PROMOTE_LOW), simd(F64X2_ADD), set(hi));
473
+ } else {
474
+ f.emit(get(acc.slots[k]));
475
+ em.vec(acc.r.ast, k);
476
+ f.emit(simd(VEC[t][acc.op]));
477
+ f.emit(set(acc.slots[k]));
478
+ }
479
+ }
480
+ }
481
+ },
482
+ between: () => {
483
+ for (const acc of accs) {
484
+ const f64Lanes = acc.wide || t === 'f64';
485
+ const vop = f64Lanes ? { add: F64X2_ADD, min: F64X2_MIN, max: F64X2_MAX }[acc.op] : VEC[t][acc.op];
486
+ f.emit(get(acc.slots[0]));
487
+ for (let j = 1; j < acc.slots.length; j++) f.emit(get(acc.slots[j]), simd(vop));
488
+ f.emit(set(acc.slots[0]));
489
+ const lanes = f64Lanes ? 2 : 4;
490
+ const lane = (l) => (f64Lanes ? simd(0x21, l) : [...simd(0x1f, l), F64_PROMOTE_F32]);
491
+ f.emit(get(acc.slots[0]), lane(0));
492
+ for (let l = 1; l < lanes; l++) f.emit(get(acc.slots[0]), lane(l), [F64_OPS[acc.op]]);
493
+ f.emit(set(acc.s));
494
+ }
495
+ },
496
+ tail: () => {
497
+ em.reset();
498
+ for (const a of env.arrays.values()) {
499
+ if (a.enc) { loadPackedSca(a); f.emit(set(a.sca)); } else f.emit(get(a.ptr), ty.load, set(a.sca));
500
+ }
501
+ if (isMap) { f.emit(get(out)); em.sca(items[0].ast); f.emit(ty.store); return; }
502
+ for (const acc of accs) {
503
+ f.emit(get(acc.s));
504
+ em.sca(acc.r.ast);
505
+ if (t === 'f32') f.emit([F64_PROMOTE_F32]);
506
+ f.emit([F64_OPS[acc.op]], set(acc.s));
507
+ }
508
+ },
509
+ });
510
+ for (const acc of accs) f.emit(get(acc.s));
511
+ return buildModule([f], shared);
512
+ }