@nadrif/thread 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.
@@ -0,0 +1,240 @@
1
+ /**
2
+ * @file Pure utility functions for GPU compute — type helpers, auto-boxing,
3
+ * JS→WGSL transpilation, and format detection.
4
+ *
5
+ * These functions have zero coupling to GPU state and can be used
6
+ * independently of the {@link GPUCompute} class.
7
+ *
8
+ * @module gpu/helpers
9
+ */
10
+
11
+ import { GPUComputeError } from '../error.js';
12
+
13
+ // ---------------------------------------------------------------------------
14
+ // Output type helpers
15
+ // ---------------------------------------------------------------------------
16
+
17
+ /** Map of human-readable names to TypedArray constructors. */
18
+ export const OUTPUT_TYPES = {
19
+ f32: Float32Array,
20
+ i32: Int32Array,
21
+ u32: Uint32Array,
22
+ f16: Float32Array, // JS has no Float16Array; use Float32Array
23
+ f64: Float64Array,
24
+ };
25
+
26
+ /**
27
+ * Resolve a type name or constructor to a TypedArray constructor.
28
+ *
29
+ * @param {string|typeof TypedArray} type
30
+ * @returns {typeof TypedArray}
31
+ */
32
+ export function resolveType(type) {
33
+ if (typeof type === 'function') return type;
34
+ const resolved = OUTPUT_TYPES[type];
35
+ if (!resolved) throw new TypeError(`Unknown output type: "${type}"`);
36
+ return resolved;
37
+ }
38
+
39
+ // ---------------------------------------------------------------------------
40
+ // Auto-boxing helpers
41
+ // ---------------------------------------------------------------------------
42
+
43
+ /**
44
+ * Auto-box a value into a TypedArray.
45
+ *
46
+ * - `number` → `Float32Array([n])`
47
+ * - `number[]` → `Float32Array(arr)`
48
+ * - `TypedArray` → returned as-is
49
+ *
50
+ * @param {*} val
51
+ * @returns {TypedArray}
52
+ */
53
+ export function box(val) {
54
+ if (val instanceof Float32Array || val instanceof Int32Array || val instanceof Uint32Array) return val;
55
+ if (typeof val === 'number') return new Float32Array([val]);
56
+ if (Array.isArray(val)) return new Float32Array(val);
57
+ return val;
58
+ }
59
+
60
+ /**
61
+ * Box all values in a plain object.
62
+ *
63
+ * @param {Object<string, *>} obj
64
+ * @returns {Object<string, TypedArray>}
65
+ */
66
+ export function boxAll(obj) {
67
+ if (!obj) return {};
68
+ const out = {};
69
+ for (const [k, v] of Object.entries(obj)) out[k] = box(v);
70
+ return out;
71
+ }
72
+
73
+ // ---------------------------------------------------------------------------
74
+ // Format detection
75
+ // ---------------------------------------------------------------------------
76
+
77
+ /**
78
+ * Detect whether the second arg to `run()` is the old format
79
+ * `{ inputs, uniforms, outputs, ... }` or the new flat format `inputs`.
80
+ *
81
+ * @param {*} arg
82
+ * @returns {boolean}
83
+ */
84
+ export function isOldRunFormat(arg) {
85
+ return arg != null && typeof arg === 'object' && !ArrayBuffer.isView(arg) &&
86
+ ('inputs' in arg || 'uniforms' in arg || 'outputBuffers' in arg || 'outputs' in arg);
87
+ }
88
+
89
+ // ---------------------------------------------------------------------------
90
+ // JS → WGSL transpilation
91
+ // ---------------------------------------------------------------------------
92
+
93
+ /**
94
+ * Transpile a WGSL expression string (JS-flavored) to valid WGSL.
95
+ *
96
+ * Handles: ternary→select, ===→==, &&→&, ||→|, **→^,
97
+ * Math.*→WGSL equivalents, integer→float literals.
98
+ *
99
+ * @param {string} expr - JS-flavored expression
100
+ * @returns {string} Valid WGSL expression
101
+ */
102
+ export function transpileExpression(expr) {
103
+ const MATH_MAP = {
104
+ 'Math.sqrt': 'sqrt', 'Math.abs': 'abs', 'Math.sin': 'sin',
105
+ 'Math.cos': 'cos', 'Math.tan': 'tan', 'Math.asin': 'asin',
106
+ 'Math.acos': 'acos', 'Math.atan': 'atan', 'Math.atan2': 'atan2',
107
+ 'Math.exp': 'exp', 'Math.log': 'log', 'Math.log2': 'log2',
108
+ 'Math.log10': 'log10', 'Math.pow': 'pow', 'Math.max': 'max',
109
+ 'Math.min': 'min', 'Math.floor': 'floor', 'Math.ceil': 'ceil',
110
+ 'Math.round': 'round', 'Math.trunc': 'trunc', 'Math.sign': 'sign',
111
+ 'Math.PI': '3.14159265', 'Math.E': '2.71828182',
112
+ 'Math.tanh': 'tanh', 'Math.sinh': 'sinh', 'Math.cosh': 'cosh',
113
+ 'Math.cbrt': 'cbrt', 'Math.expm1': '(exp($1) - 1.0)',
114
+ 'Math.log1p': 'log(1.0 + $1)',
115
+ 'Math.hypot': 'sqrt($1 * $1 + $2 * $2)',
116
+ 'Math.random': 'fract(f32(hash(u32(i))))',
117
+ };
118
+
119
+ let wgsl = expr;
120
+
121
+ // Replace ternary: `cond ? a : b` → `select(b, a, cond)`
122
+ wgsl = wgsl.replace(
123
+ /(.+?)\s*\?\s*(.+?)\s*:\s*(.+)/g,
124
+ (_, cond, a, b) => `select(${b.trim()}, ${a.trim()}, ${cond.trim()})`,
125
+ );
126
+
127
+ // Replace JS comparison operators
128
+ wgsl = wgsl.replace(/===/g, '==');
129
+ wgsl = wgsl.replace(/!==/g, '!=');
130
+
131
+ // Replace JS logical operators
132
+ wgsl = wgsl.replace(/&&/g, '&');
133
+ wgsl = wgsl.replace(/\|\|/g, '|');
134
+ wgsl = wgsl.replace(/!(?=[a-zA-Z_])/g, '!');
135
+
136
+ // Replace JS operators
137
+ wgsl = wgsl.replace(/\*\*/g, '^');
138
+
139
+ // Replace JS math calls with WGSL equivalents (longest first)
140
+ const sortedKeys = Object.keys(MATH_MAP).sort((a, b) => b.length - a.length);
141
+ for (const jsName of sortedKeys) {
142
+ const wgslName = MATH_MAP[jsName];
143
+ if (wgslName.includes('$')) {
144
+ wgsl = wgsl.replace(
145
+ new RegExp(jsName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '\\(([^,)]+),\\s*([^)]+)\\)', 'g'),
146
+ wgslName.replace('$1', '$1').replace('$2', '$2'),
147
+ );
148
+ wgsl = wgsl.replace(
149
+ new RegExp(jsName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '\\(([^)]+)\\)', 'g'),
150
+ wgslName.replace('$1', '$1').replace(/, *\$2.*/, ''),
151
+ );
152
+ } else {
153
+ wgsl = wgsl.split(jsName).join(wgslName);
154
+ }
155
+ }
156
+
157
+ // Replace bare integer literals in arithmetic with floats
158
+ wgsl = wgsl.replace(/(?<=[^a-zA-Z_0-9.\[])(\d+)(?=[^a-zA-Z_0-9.\]$)])/g, '$1.0');
159
+ wgsl = wgsl.replace(/true\.0/g, 'true').replace(/false\.0/g, 'false');
160
+ wgsl = wgsl.replace(/(\d+\.\d+)\.0/g, '$1');
161
+
162
+ return wgsl;
163
+ }
164
+
165
+ /**
166
+ * Transpile a `define(name, fn)` function body to a WGSL assignment expression.
167
+ *
168
+ * Converts `data.value` → `inputName[i]`, `data.index`/`data.i` → `f32(i)`,
169
+ * then runs the result through {@link transpileExpression}.
170
+ *
171
+ * @param {Function} fn - The JS function `(data, { uniform }) => expression`
172
+ * @param {string} inputName - The storage buffer name (first param name)
173
+ * @returns {string} WGSL body like `result[i] = data[i] * alpha + ...;`
174
+ */
175
+ export function transpileDefineBody(fn, inputName) {
176
+ const src = fn.toString();
177
+
178
+ // Extract body from arrow function
179
+ let body;
180
+ const arrowMatch = src.match(/=>\s*\{?\s*(?:return\s+)?(.+?);?\s*\}?\s*$/);
181
+ if (arrowMatch) {
182
+ body = arrowMatch[1].trim();
183
+ } else {
184
+ throw new GPUComputeError(`Cannot parse define function: ${src}`);
185
+ }
186
+
187
+ // Replace data proxy references with WGSL equivalents
188
+ // data.value → inputName[i]
189
+ body = body.replace(/data\.value/g, `${inputName}[i]`);
190
+ // data.index → f32(i)
191
+ body = body.replace(/data\.index/g, 'f32(i)');
192
+ // data.i → f32(i) (but not data.index which was already handled)
193
+ body = body.replace(/data\.i\b(?!\w)/g, 'f32(i)');
194
+
195
+ // Run through expression transpiler for math, ternary, etc.
196
+ return transpileExpression(body);
197
+ }
198
+
199
+ /**
200
+ * Convert a JS function body to a WGSL expression.
201
+ *
202
+ * Handles common patterns:
203
+ * `x => Math.sqrt(x)` → `sqrt(x)`
204
+ * `x => x * 2 + 1` → `x * 2.0 + 1.0`
205
+ * `x => Math.max(0, x)` → `max(0.0, x)`
206
+ * `(a, b) => a + b` → `a + b`
207
+ * `x => x > 0 ? x : 0` → select(0.0, x, x > 0.0)
208
+ * `x => x === 0 ? 1 : 0` → select(0.0, 1.0, x == 0.0)
209
+ * `x => !x ? 1 : 0` → select(0.0, 1.0, !x)
210
+ *
211
+ * @param {Function} fn
212
+ * @returns {string} WGSL expression
213
+ */
214
+ export function jsToWgsl(fn) {
215
+ const src = fn.toString();
216
+
217
+ // Extract the body — works for `x => expr` and `(x) => { return expr; }`
218
+ let body;
219
+ const arrowMatch = src.match(/=>\s*\{?\s*(?:return\s+)?(.+?);?\s*\}?\s*$/);
220
+ if (arrowMatch) {
221
+ body = arrowMatch[1].trim();
222
+ } else {
223
+ throw new GPUComputeError(`Cannot parse function body: ${src}`);
224
+ }
225
+
226
+ return transpileExpression(body);
227
+ }
228
+
229
+ /**
230
+ * Extract parameter names from a JS function's signature.
231
+ *
232
+ * @param {Function} fn
233
+ * @returns {string[]}
234
+ */
235
+ export function getParamNames(fn) {
236
+ const src = fn.toString();
237
+ const match = src.match(/\(([^)]*)\)/);
238
+ if (!match) return ['x'];
239
+ return match[1].split(',').map(s => s.trim()).filter(Boolean);
240
+ }
@@ -0,0 +1,284 @@
1
+ /**
2
+ * @file Framework-agnostic hooks for GPU compute.
3
+ *
4
+ * Provides lifecycle-bound hooks that mirror the thread/pool hooks
5
+ * but are specialised for {@link GPUCompute} instances.
6
+ *
7
+ * The framework (Preact, React, etc.) is resolved at load time via the
8
+ * thread config system.
9
+ *
10
+ * @module gpu/hooks
11
+ */
12
+
13
+ import { getHooks } from '../config/index.js';
14
+ import { GPUCompute } from './gpu.js';
15
+
16
+ // Resolve framework hooks at module load time (top-level await)
17
+ let useRef, useState, useEffect, useCallback, useMemo;
18
+ try {
19
+ ({ useRef, useState, useEffect, useCallback, useMemo } = await getHooks());
20
+ } catch {
21
+ const _stub = () => { throw new Error('[thread] No framework installed. Install preact or react to use GPU hooks.'); };
22
+ useRef = _stub; useState = _stub; useEffect = _stub; useCallback = _stub; useMemo = _stub;
23
+ }
24
+
25
+ // ---------------------------------------------------------------------------
26
+ // useGPU
27
+ // ---------------------------------------------------------------------------
28
+
29
+ /**
30
+ * Create and manage a {@link GPUCompute} instance bound to a component's
31
+ * lifecycle.
32
+ *
33
+ * The GPU device is lazily initialised on the first `run()` call.
34
+ * The instance is automatically destroyed on unmount.
35
+ *
36
+ * @param {import('../types.js').GPUComputeOptions & { destroy?: boolean }} [options={}]
37
+ * GPU options plus `destroy: false` to skip automatic cleanup.
38
+ * @returns {{
39
+ * gpu: GPUCompute,
40
+ * run: (...args: any[]) => Promise<any>,
41
+ * pipe: (name?: string, count?: number) => import('./chains.js').DataPipelineChain,
42
+ * define: (name: string, fn: Function) => void,
43
+ * result: any,
44
+ * loading: boolean,
45
+ * error: string|null,
46
+ * status: string,
47
+ * metrics: import('../types.js').MetricsSnapshot,
48
+ * }}
49
+ *
50
+ * @example
51
+ * ```jsx
52
+ * function GPUDemo({ prices }) {
53
+ * const { run, result, loading, error, status } = useGPU();
54
+ *
55
+ * const handleEMA = () => {
56
+ * run('ema', new Float32Array(prices), { alpha: 0.3 });
57
+ * };
58
+ *
59
+ * return (
60
+ * <div>
61
+ * <p>GPU: {status}</p>
62
+ * <button onClick={handleEMA} disabled={loading}>
63
+ * {loading ? 'Computing...' : 'Run EMA'}
64
+ * </button>
65
+ * {error && <p className="error">{error}</p>}
66
+ * {result && <pre>{JSON.stringify(Array.from(result))}</pre>}
67
+ * </div>
68
+ * );
69
+ * }
70
+ * ```
71
+ */
72
+ export function useGPU(options = {}) {
73
+ const { destroy = true, ...gpuOpts } = options;
74
+
75
+ const gpuRef = useRef(null);
76
+ const [result, setResult] = useState(null);
77
+ const [error, setError] = useState(null);
78
+ const [loading, setLoading] = useState(false);
79
+ const [status, setStatus] = useState('idle');
80
+ const [metricsSnapshot, setMetricsSnapshot] = useState({});
81
+
82
+ // Create instance once
83
+ if (gpuRef.current === null) {
84
+ gpuRef.current = new GPUCompute(gpuOpts);
85
+ }
86
+ const gpu = gpuRef.current;
87
+
88
+ // Cleanup on unmount
89
+ useEffect(() => {
90
+ return () => {
91
+ if (gpu && destroy) {
92
+ gpu.destroy();
93
+ }
94
+ };
95
+ }, [gpu, destroy]);
96
+
97
+ // Poll metrics & status
98
+ useEffect(() => {
99
+ if (!gpu) return;
100
+ const interval = setInterval(() => {
101
+ setStatus(gpu.status);
102
+ setMetricsSnapshot(gpu.metrics);
103
+ }, 500);
104
+ return () => clearInterval(interval);
105
+ }, [gpu]);
106
+
107
+ // Stable run function
108
+ const run = useCallback(
109
+ async (...args) => {
110
+ setLoading(true);
111
+ setError(null);
112
+ setStatus(gpu.status);
113
+ try {
114
+ const res = await gpu.run(...args);
115
+ setResult(res);
116
+ setLoading(false);
117
+ setStatus(gpu.status);
118
+ setMetricsSnapshot(gpu.metrics);
119
+ return res;
120
+ } catch (err) {
121
+ const msg = err.message || String(err);
122
+ setError(msg);
123
+ setLoading(false);
124
+ setStatus(gpu.status);
125
+ throw err;
126
+ }
127
+ },
128
+ [gpu],
129
+ );
130
+
131
+ // Stable pipe
132
+ const pipe = useCallback(
133
+ (name, count) => gpu.pipe(name, count),
134
+ [gpu],
135
+ );
136
+
137
+ // Stable define
138
+ const define = useCallback(
139
+ (name, fn) => gpu.define(name, fn),
140
+ [gpu],
141
+ );
142
+
143
+ return { gpu, run, pipe, define, result, loading, error, status, metrics: metricsSnapshot };
144
+ }
145
+
146
+ // ---------------------------------------------------------------------------
147
+ // useGPURun
148
+ // ---------------------------------------------------------------------------
149
+
150
+ /**
151
+ * Get a stable `run` wrapper around an existing {@link GPUCompute}
152
+ * instance, with reactive loading / result / error state.
153
+ *
154
+ * Use this when you manage the GPU instance yourself (e.g. in a store
155
+ * or module-level variable).
156
+ *
157
+ * @param {GPUCompute} gpu - Existing GPU instance.
158
+ * @returns {{
159
+ * run: (...args: any[]) => Promise<any>,
160
+ * result: any,
161
+ * loading: boolean,
162
+ * error: string|null,
163
+ * }}
164
+ *
165
+ * @example
166
+ * ```jsx
167
+ * // Shared GPU across components
168
+ * const sharedGPU = new GPUCompute();
169
+ *
170
+ * function ComponentA() {
171
+ * const { run, result, loading } = useGPURun(sharedGPU);
172
+ * return <button onClick={() => run('add', data1, data2)}>A</button>;
173
+ * }
174
+ * ```
175
+ */
176
+ export function useGPURun(gpu) {
177
+ const [result, setResult] = useState(null);
178
+ const [error, setError] = useState(null);
179
+ const [loading, setLoading] = useState(false);
180
+
181
+ const run = useCallback(
182
+ async (...args) => {
183
+ if (!gpu) {
184
+ const msg = 'No GPU instance';
185
+ setError(msg);
186
+ throw new Error(msg);
187
+ }
188
+ setLoading(true);
189
+ setError(null);
190
+ try {
191
+ const res = await gpu.run(...args);
192
+ setResult(res);
193
+ setLoading(false);
194
+ return res;
195
+ } catch (err) {
196
+ const msg = err.message || String(err);
197
+ setError(msg);
198
+ setLoading(false);
199
+ throw err;
200
+ }
201
+ },
202
+ [gpu],
203
+ );
204
+
205
+ return useMemo(() => ({ run, result, loading, error }), [run, result, loading, error]);
206
+ }
207
+
208
+ // ---------------------------------------------------------------------------
209
+ // useGPUMetrics
210
+ // ---------------------------------------------------------------------------
211
+
212
+ /**
213
+ * Subscribe to a {@link GPUCompute} instance's metrics and re-render
214
+ * on updates.
215
+ *
216
+ * @param {GPUCompute} [gpu] - GPU instance. If `null`/`undefined`,
217
+ * returns a zeroed snapshot.
218
+ * @param {number} [intervalMs=500] - Polling interval in ms.
219
+ * @returns {import('../types.js').MetricsSnapshot}
220
+ *
221
+ * @example
222
+ * ```jsx
223
+ * function GPUDashboard({ gpu }) {
224
+ * const metrics = useGPUMetrics(gpu, 250);
225
+ *
226
+ * return (
227
+ * <div>
228
+ * <span>{metrics.count} dispatches</span>
229
+ * <span>{metrics.avg?.toFixed(1)}ms avg</span>
230
+ * </div>
231
+ * );
232
+ * }
233
+ * ```
234
+ */
235
+ export function useGPUMetrics(gpu, intervalMs = 500) {
236
+ const [snapshot, setSnapshot] = useState(
237
+ gpu ? gpu.metrics : { count: 0, errors: 0, avg: 0, min: 0, max: 0, throughput: 0, errorRate: 0 },
238
+ );
239
+
240
+ useEffect(() => {
241
+ if (!gpu) return;
242
+ const interval = setInterval(() => {
243
+ setSnapshot(gpu.metrics);
244
+ }, intervalMs);
245
+ return () => clearInterval(interval);
246
+ }, [gpu, intervalMs]);
247
+
248
+ return snapshot;
249
+ }
250
+
251
+ // ---------------------------------------------------------------------------
252
+ // useGPUStatus
253
+ // ---------------------------------------------------------------------------
254
+
255
+ /**
256
+ * Reactive status string for a {@link GPUCompute} instance.
257
+ *
258
+ * Returns one of: `'idle'`, `'running'`, `'error'`, `'unavailable'`.
259
+ *
260
+ * @param {GPUCompute} [gpu] - GPU instance.
261
+ * @param {number} [intervalMs=500] - Polling interval in ms.
262
+ * @returns {string}
263
+ *
264
+ * @example
265
+ * ```jsx
266
+ * function StatusBadge({ gpu }) {
267
+ * const status = useGPUStatus(gpu);
268
+ * return <span className={`badge badge-${status}`}>{status}</span>;
269
+ * }
270
+ * ```
271
+ */
272
+ export function useGPUStatus(gpu, intervalMs = 500) {
273
+ const [status, setStatus] = useState(gpu ? gpu.status : 'idle');
274
+
275
+ useEffect(() => {
276
+ if (!gpu) return;
277
+ const interval = setInterval(() => {
278
+ setStatus(gpu.status);
279
+ }, intervalMs);
280
+ return () => clearInterval(interval);
281
+ }, [gpu, intervalMs]);
282
+
283
+ return status;
284
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * @file GPU compute barrel export.
3
+ *
4
+ * Re-exports every public symbol from the GPU sub-modules so
5
+ * consumers can do `import { GPUCompute, ... } from '../gpu/index.js'`.
6
+ *
7
+ * @module gpu
8
+ */
9
+
10
+ export { GPUCompute, createGPUCompute, createGPUWithFallback, outputSpec, uniform } from './gpu.js';
11
+ export { PipelineChain, DataPipelineChain } from './chains.js';
12
+ export { buildShader, BUILT_IN_OPS, BUILT_IN_OP_NAMES, SPECIAL_OPS } from './shaders.js';
13
+ export { runSpecial, runMatmul, buildMatmulShader, runReduce, getReduceBody,
14
+ runHistogram, runArgMaxMin, runScan } from './special.js';
15
+ export { useGPU, useGPURun, useGPUMetrics, useGPUStatus } from './hooks.js';
16
+ export { createGPUBinder, createGPUSignalBinder, createGPUStoreBinder } from './adapters.js';