Skip to content

Commit 2308af3

Browse files
committed
Add new JS coverage plan
1 parent e24d183 commit 2308af3

1 file changed

Lines changed: 143 additions & 0 deletions

File tree

plans/dsl-js-coverage.md

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
# DSL → JS transpiler: coverage gaps & future work
2+
3+
Status of the `blosc2.dsl_js` transpiler (the `jit_backend="js"` path) versus the
4+
miniexpr + WASM-JIT backend. Everything listed below as *unsupported* currently rides on
5+
**miniexpr + jit-wasm** instead of the JS bridge.
6+
7+
## Implemented
8+
9+
- **P1 — Index / shape symbols** (`_i0`/`_n0`/`_flat_idx`, ...). The transpiler emits them
10+
as trailing kernel params and the runtime driver reconstructs per-block global coordinates
11+
from `(off, gshape, cshape)`; see `_module_with_index` in `src/blosc2/dsl_js.py`. The
12+
whole-array shape is threaded `chunked_eval → _maybe_js_backend → _as_js_udf → js_kernel`.
13+
Requires ≥1 array operand (zero-input DSL kernels stay on miniexpr) and a known output
14+
shape; without a shape such kernels fall back. Covered by `tests/ndarray/test_dsl_js.py`
15+
(`test_index_*`) and `tests/ndarray/test_wasm_dsl_jit.py::test_wasm_dsl_index_symbols_via_js`.
16+
- **P2 (input side) — Integer inputs with a floating output.** The JS bridge already
17+
float64-converts every operand, which is exactly miniexpr's promotion of integer inputs for
18+
a float result (so values above 2**53 lose precision identically). `_js_dtypes_ok`
19+
(`src/blosc2/lazyexpr.py`) now admits integer inputs when the output dtype is floating.
20+
Integer/complex *output* still goes to miniexpr — see the remaining P2 work below.
21+
22+
## Performance characteristics (and where the residual cost is)
23+
24+
Measured with `bench/js-transpiler/dsl-js-node.mjs` (Pyodide, ms/frame). JS beats miniexpr's
25+
TinyCC JIT (`tcc`) on **compute-heavy** kernels and lands at parity / slightly behind on
26+
**compute-light, vectorizable** ones:
27+
28+
```
29+
kernel js/tcc
30+
newton 2.80x (heavy: loop + complex arithmetic)
31+
deepar 2.78x
32+
idxgrad 2.00x (P1 index symbols)
33+
deep 1.30x
34+
trans 0.99x
35+
intmix 0.87x (P2 int inputs; light, vectorizable)
36+
poly 0.86x (light, vectorizable)
37+
```
38+
39+
Two cost components matter, and only the second remains:
40+
41+
- **Per-evaluation transpile + `js.eval` (amortized away).** Each `lazyudf` evaluation used to
42+
re-parse the kernel AST and re-`eval` the JS module, while miniexpr caches its compiled
43+
program by source. Now memoized: `_TRANSPILE_CACHE` (by kernel source) and `_RUN_CACHE` (the
44+
V8-compiled `__run`, by module string) in `src/blosc2/dsl_js.py`. This lifted every ratio
45+
(e.g. newton 2.20→2.80x, poly 0.77→0.86x) and is a real win for repeated / animation-loop use.
46+
47+
- **Per-block marshaling (the residual).** The bridge copies each block across the Python↔JS
48+
boundary: in via `ascontiguousarray(float64) → tobytes → Float64Array`, out via
49+
`to_bytes → np.frombuffer`. miniexpr's prefilter computes **in place** with zero copies. For
50+
light kernels (~2 ms compute) these two copies are a meaningful fraction with no compute to
51+
hide them behind, so JS sits at parity or just behind `tcc` there. For compute-bound kernels
52+
(the reason the JS backend exists) it is negligible.
53+
54+
**Future lever — zero-copy block I/O.** Replace the `tobytes`/`frombuffer` copies with a
55+
`HEAPF64` view onto WASM linear memory so operands/output alias the block buffers (the
56+
"ponytail" note in `js_kernel`). This would mostly close the gap on marshaling-bound (light)
57+
kernels but needs care around WASM-heap lifetime/alignment, and does nothing for compute-bound
58+
kernels — so build it only if a real marshaling-bound workload appears.
59+
60+
## How routing works today
61+
62+
Under WebAssembly with `jit_backend` unset (and `jit != False`, no `strict_miniexpr`),
63+
blosc2 *prefers* JS for float DSL kernels and **silently falls back to miniexpr+jit-wasm**
64+
for anything it can't transpile — see `_maybe_js_backend` (`src/blosc2/lazyexpr.py:1475`):
65+
66+
```python
67+
try:
68+
bridge = _as_js_udf(expression) # transpiles; raises on any unsupported construct
69+
except Exception:
70+
return expression, jit, jit_backend # fall back to miniexpr, no regression
71+
```
72+
73+
With an **explicit** `jit_backend="js"`, the same gaps instead **raise** rather than fall
74+
back (the user asked for JS specifically, so we don't second-guess them).
75+
76+
The JS backend today covers *float64/float32 element-wise scalar kernels* using arithmetic,
77+
`where`, comparisons, `if/elif/else`, `range` loops, and whitelisted math functions.
78+
79+
## Remaining P2 — Integer *output*
80+
81+
`_js_dtypes_ok` still sends any non-floating *output* dtype to miniexpr, because the JS
82+
bridge computes in **float64** and can't reproduce integer semantics for the result:
83+
84+
- **Integer division / modulo / truncation**: `//`, `%`, `int(...)` must match C/miniexpr
85+
integer rules, not float `Math.floor`/`pymod`.
86+
- **Overflow / wraparound**: miniexpr wraps at the integer width; float64 doesn't.
87+
- **int64 range**: float64 can't represent int64 above 2**53 exactly.
88+
89+
Options, in rough order of effort:
90+
- **int32 and smaller output**: representable exactly in float64; could be allowed for kernels
91+
that provably stay within ±2^53 with integer-valued ops and an explicit safe-range / no-
92+
overflow contract. Still needs integer-correct `//`/`%`/`int()` codegen.
93+
- **int64 output**: requires BigInt or a typed-array split-word scheme — significantly more
94+
work and likely slower; probably not worth it until a real workload needs it.
95+
96+
## Other unsupported constructs (lower priority)
97+
98+
All of these raise `_DSLToJSError` in the transpiler → fall back (or raise under explicit
99+
`jit_backend="js"`).
100+
101+
**Reductions** — any `reduce_args` (`sum`, `prod`, …) → miniexpr. Explicit
102+
`jit_backend="js"` raises `'jit_backend="js" does not support reductions'`. A JS reduction
103+
path would need a fundamentally different driver (accumulate, not map).
104+
105+
**Statements** — only `Assign, AugAssign, Return, Expr, If, For(range), While, Break,
106+
Continue` are emitted (`_stmt`, `src/blosc2/dsl_js.py:151`). Not supported:
107+
- Tuple / multiple / subscript assignment targets — only a single `Name` target is handled
108+
(`node.targets[0].id`). `a, b = ...`, `a = b = ...`, `arr[i] = ...` all fail.
109+
- `with`, nested `def`, `try`, etc.
110+
111+
**Expressions** — only `Name, Constant, UnaryOp, BinOp, BoolOp, Compare, Call` (`_expr`).
112+
Not supported:
113+
- Python ternary `a if cond else b` (`ast.IfExp`) — must be written as `where(cond, a, b)`.
114+
- Chained comparisons `a < b < c` — only `ops[0]`/`comparators[0]` are read.
115+
- Subscript / indexing, attribute access (except `np.`/`numpy.`/`math.` call targets),
116+
tuples, lists, dicts, comprehensions, slices.
117+
118+
**Calls** — only `where`, `int`, `float`, `bool`, and the `_MATH` whitelist (`sin, cos, exp,
119+
log, sqrt, pow, floor, abs, min/max, …`, see `src/blosc2/dsl_js.py:27`). Any other call name,
120+
or a call through a non-`np`/`numpy`/`math` target → fall back.
121+
122+
**For-loops** — only `for v in range(...)`. Iterating over arrays/other iterables is
123+
unsupported.
124+
125+
## Environment gate (by design)
126+
127+
Browser/Pyodide only. `_as_js_udf` raises `RuntimeError` off-WASM (`js_kernel` imports
128+
Pyodide's `js` at run time). On native/CI, DSL kernels always go to miniexpr+jit.
129+
130+
## Known semantic ceilings (supported, but lossy)
131+
132+
These transpile but with caveats worth tracking, since miniexpr may differ:
133+
- 64-bit integer bitwise ops degrade to int32 (JS number semantics).
134+
- `%` uses a Python-sign helper (`pymod`); large-magnitude float edge cases may differ.
135+
- `range()` with a non-literal step assumes a positive step (loop-direction guess).
136+
- float64/float32 are the target; exotic dtypes untested.
137+
138+
## See also
139+
140+
- `plans/dsl-js.md` — original design, perf numbers, and the "Deferred" / "Known ceilings"
141+
notes this document expands on.
142+
- `src/blosc2/dsl_js.py` — the transpiler.
143+
- `src/blosc2/lazyexpr.py``_maybe_js_backend`, `_js_dtypes_ok`, `_as_js_udf` (routing).

0 commit comments

Comments
 (0)