Skip to content

Commit 292f6c6

Browse files
committed
wasm: cast intrinsics when jit is enabled
1 parent f5e276a commit 292f6c6

4 files changed

Lines changed: 77 additions & 93 deletions

File tree

plans/wasm32-todo.md

Lines changed: 44 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,18 @@ Last updated: 2026-02-19
44

55
## Context
66

7-
In `python-blosc2` on wasm32/Pyodide, DSL kernels that use index symbols with `int(...)` casting still fail in practice for this shape:
7+
In `python-blosc2` on wasm32/Pyodide, DSL kernels that use index symbols with `int(...)` casting were failing for this shape and are now passing after integration fixes:
88

99
- DSL kernel: `return int(_i0 * _n1 + _i1)`
10-
- Typical integration symptom: `DSL kernels require miniexpr ... miniexpr compilation or execution failed`
11-
- Observed integration detail (2026-02-19): failure is raised during miniexpr setup (`NDArray._set_pref_expr`), with `NotImplementedError: Cannot compile expression ...`, and reproduces for default, `jit=False`, and `jit=True`.
12-
- Current status in python-blosc2: expected-to-fail behavior is still kept for wasm32.
10+
- Prior integration symptom: `DSL kernels require miniexpr ... miniexpr compilation or execution failed`.
11+
- Resolved integration detail (2026-02-19): python-blosc2 was mapping dtypes via `dtype.num` in `_set_pref_expr`, which is platform-dependent on wasm32/Pyodide.
12+
- Current status in python-blosc2: task-7 kernel now succeeds on wasm32 with deterministic `int64` ramp output, and wasm expected-fail behavior was removed.
1313

14-
This is likely either:
15-
- a true miniexpr wasm32 cast/runtime gap, or
16-
- an integration mismatch between python-blosc2's prefilter setup and miniexpr's supported dtype/cast combinations.
14+
The remaining miniexpr work is now runtime-JIT parity for cast intrinsics on wasm32.
1715

1816
## Goal
1917

20-
Make DSL `int(...)` casting reliable on wasm32 for ND/index-symbol expressions (or explicitly declare and document it as unsupported with precise diagnostics).
18+
Keep DSL `int(...)` casting reliable on wasm32 for ND/index-symbol expressions and complete runtime-JIT cast-intrinsic parity on wasm32.
2119

2220
## Work Items
2321

@@ -122,54 +120,62 @@ Make DSL `int(...)` casting reliable on wasm32 for ND/index-symbol expressions (
122120
### Updated status after this follow-up
123121

124122
- Phase 1 compile/setup behavior in miniexpr is now exercised by dedicated contract tests and has explicit diagnostics.
125-
- The remaining Phase 1 integration gap is surfacing those diagnostics in python-blosc2 exceptions (`_set_pref_expr`) so failures are actionable instead of generic.
123+
- The former Phase 1 integration gap in python-blosc2 is now closed (diagnostics are surfaced and wasm-safe dtype mapping is in place).
126124
- Full wasm runtime-JIT support for cast intrinsics remains Phase 2 follow-up work.
127125

128-
## Remaining Tasks (Task-7 Unblock First)
126+
## Session Update (2026-02-19, Phase 2 takeover)
129127

130-
### Phase 1: unblock python-blosc2 task-7 compile path
128+
### Completed in this session
129+
130+
- Root-caused the wasm invalid-module failure for cast-heavy kernels by reproducing without the temporary skip gate:
131+
- `WebAssembly.Module(): ... i32.trunc_f32_u ... expected type f32, found ... f64`.
132+
- Implemented wasm-safe cast lowering for runtime-JIT kernels in the wasm patch step:
133+
- `src/miniexpr.c` now rewrites JIT C cast macros to helper-call form in patched wasm source:
134+
- `ME_DSL_CAST_INT(x)` -> `me_wasm32_cast_int((double)(x))`
135+
- `ME_DSL_CAST_FLOAT(x)` -> `me_wasm32_cast_float((double)(x))`
136+
- `ME_DSL_CAST_BOOL(x)` -> `me_wasm32_cast_bool((double)(x))`
137+
- Added explicit wasm import helpers in both wasm JIT JS import builders:
138+
- `src/me_jit_glue.js` (side-module path used in wasm tests)
139+
- `src/miniexpr.c` EM_JS instantiate helper (main-module path)
140+
- Removed the temporary wasm cast-intrinsic runtime-JIT skip gate in `src/miniexpr.c`.
141+
- Flipped wasm runtime-cache cast behavior contract to require JIT kernel creation:
142+
- `tests/test_dsl_jit_runtime_cache.c` test 8c now expects `me_expr_has_jit_kernel(expr) == true`.
143+
144+
### Validation in this session
145+
146+
- wasm targeted tests:
147+
- `ctest --test-dir build-wasm32-fast --output-on-failure -R "test_nd|test_dsl_syntax|test_dsl_jit_runtime_cache|test_dsl_jit_side_module"` (pass)
148+
- native targeted regression check:
149+
- `ctest --test-dir build --output-on-failure -R test_dsl_jit_runtime_cache` (pass)
150+
151+
## Remaining Tasks
131152

132-
1. Reproduce the exact python-blosc2 call contract inside miniexpr tests.
133-
- Add a wasm test that uses function-form DSL source with reserved index symbols and `int(...)` return cast:
134-
- `def kernel(x): return int(_i0 * _n1 + _i1)`
135-
- output dtype `ME_INT64`
136-
- evaluate with runtime-JIT default, disabled, and enabled settings.
137-
- Ensure this test exercises the same compile/setup entrypoint used by `NDArray._set_pref_expr` integration.
153+
### Phase 1: task-7 unblock status (completed)
138154

139-
2. Diagnose compile-time rejection and close the gap.
140-
- Identify why miniexpr accepts equivalent interpreter tests but rejects this integration expression form.
141-
- Audit parser/lowering for function-form DSL + reserved symbols + `int(...)` cast combination.
142-
- Fix support if intended, or return a precise unsupported reason if not.
155+
Completed on 2026-02-19 (cross-repo validation):
143156

144-
3. Strengthen diagnostics and contract tests for compile/setup failures.
145-
- Replace generic compile failure surfaces with actionable reason strings.
146-
- Add tests that assert the reason for intentionally unsupported combos.
157+
- miniexpr contract-parity tests pass for the exact task-7 expression shape on native and wasm.
158+
- miniexpr diagnostics are exposed through `me_get_last_error_message()`.
159+
- python-blosc2 integration now uses wasm-safe dtype mapping for miniexpr setup and task-7 passes on wasm.
147160

148-
4. Re-validate python-blosc2 task-7 immediately after phase-1 changes.
149-
- Rebuild python-blosc2 against updated miniexpr.
150-
- Re-run `tests/ndarray/test_dsl_kernels.py::test_dsl_kernel_index_symbols_int_cast_matches_expected_ramp`.
151-
- Remove expected-fail behavior only after this passes with deterministic output.
161+
No additional Phase 1 actions are pending in miniexpr for task-7 unblock.
152162

153163
### Phase 2: full wasm runtime-JIT cast support (follow-up)
154164

155165
1. Root-cause the wasm backend invalid-module failure for cast-heavy kernels.
156-
- Reproduce with a minimal runtime-JIT kernel that uses cast intrinsics (e.g. `float(int(x)) + bool(x)`).
157-
- Inspect generated C and emitted wasm around conversion ops; isolate exact tinycc wasm32 lowering pattern that triggers invalid wasm.
166+
- Completed in this session (reproduced concrete truncation opcode/type mismatch in wasm module compile).
158167

159168
2. Implement wasm-safe cast lowering for runtime-JIT kernels.
160-
- Introduce lowering that avoids problematic wasm32 conversion instruction sequences for:
169+
- Completed in this session via wasm-patched cast helper-call lowering for:
161170
- `int(...)`
162171
- `float(...)`
163172
- `bool(...)`
164-
- Prefer explicit helper calls or safe expansion patterns known to compile/instantiate correctly in wasm side-module mode.
165173

166174
3. Remove the temporary wasm cast-intrinsic runtime-JIT skip gate.
167-
- Delete the detection/skip path once runtime JIT works for cast intrinsics.
168-
- Keep precise diagnostics for genuinely unsupported combinations (if any remain).
175+
- Completed in this session.
169176

170177
4. Flip wasm tests from fallback-expectation to JIT-expectation.
171-
- Update wasm runtime-cache cast test to require `me_expr_has_jit_kernel(expr) == true` for cast kernels.
172-
- Keep interpreter/JIT numeric parity checks.
178+
- Completed in this session (`test_dsl_jit_runtime_cache` test 8c).
173179

174180
5. Expand wasm runtime-cache coverage back to full parity where feasible.
175181
- Reconcile runtime-cache test assumptions that are host-file-cache specific vs wasm side-module behavior.
@@ -200,5 +206,5 @@ ctest --test-dir build-wasm32 --output-on-failure -R "test_nd|test_dsl_syntax|te
200206

201207
## Exit Criteria
202208

203-
- `int(_i0 * _n1 + _i1)` works on wasm32 in miniexpr and in python-blosc2 integration, with deterministic behavior and tests.
204-
- OR behavior is explicitly unsupported, documented, and enforced by a precise, tested error path.
209+
- Task-7 unblock criterion is met: `int(_i0 * _n1 + _i1)` works on wasm32 in miniexpr and python-blosc2 integration, with deterministic behavior and tests.
210+
- Remaining completion criterion for this plan: runtime-JIT cast intrinsics on wasm32 achieve parity (remove fallback gate and require JIT-kernel expectations in wasm tests).

src/me_jit_glue.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,15 @@
302302
me_jit_exp10: meJitExp10, me_jit_sinpi: meJitSinpi, me_jit_cospi: meJitCospi,
303303
me_jit_logaddexp: meJitLogaddexp, me_jit_where: meJitWhere
304304
};
305+
env.me_wasm32_cast_int = function(x) {
306+
return x < 0 ? Math.ceil(x) : Math.floor(x);
307+
};
308+
env.me_wasm32_cast_float = function(x) {
309+
return x;
310+
};
311+
env.me_wasm32_cast_bool = function(x) {
312+
return x !== 0 ? 1 : 0;
313+
};
305314
/* Prefer host wasm bridge symbols; keep JS fallbacks for robustness. */
306315
env.me_jit_exp10 = bindBridge("me_jit_exp10", env.me_jit_exp10);
307316
env.me_jit_sinpi = bindBridge("me_jit_sinpi", env.me_jit_sinpi);

src/miniexpr.c

Lines changed: 19 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -6306,6 +6306,15 @@ EM_JS(int, me_wasm_jit_instantiate,
63066306
me_jit_exp10: meJitExp10, me_jit_sinpi: meJitSinpi, me_jit_cospi: meJitCospi,
63076307
me_jit_logaddexp: meJitLogaddexp, me_jit_where: meJitWhere
63086308
};
6309+
env.me_wasm32_cast_int = function(x) {
6310+
return x < 0 ? Math.ceil(x) : Math.floor(x);
6311+
};
6312+
env.me_wasm32_cast_float = function(x) {
6313+
return x;
6314+
};
6315+
env.me_wasm32_cast_bool = function(x) {
6316+
return x !== 0 ? 1 : 0;
6317+
};
63096318
/* Prefer host wasm bridge symbols; keep JS fallbacks for robustness. */
63106319
env.me_jit_exp10 = bindBridge("me_jit_exp10", env.me_jit_exp10);
63116320
env.me_jit_sinpi = bindBridge("me_jit_sinpi", env.me_jit_sinpi);
@@ -6448,6 +6457,15 @@ static char *dsl_wasm32_patch_source(const char *src) {
64486457
(TCC wasm32 can't mix comparison types in ||) */
64496458
typedef struct { const char *old; const char *rep; } Repl;
64506459
Repl repls[] = {
6460+
{ "#define ME_DSL_CAST_INT(x) ((int64_t)(x))",
6461+
"extern int me_wasm32_cast_int(double);\n"
6462+
"#define ME_DSL_CAST_INT(x) (me_wasm32_cast_int((double)(x)))" },
6463+
{ "#define ME_DSL_CAST_FLOAT(x) ((double)(x))",
6464+
"extern double me_wasm32_cast_float(double);\n"
6465+
"#define ME_DSL_CAST_FLOAT(x) (me_wasm32_cast_float((double)(x)))" },
6466+
{ "#define ME_DSL_CAST_BOOL(x) ((x) != 0)",
6467+
"extern int me_wasm32_cast_bool(double);\n"
6468+
"#define ME_DSL_CAST_BOOL(x) (me_wasm32_cast_bool((double)(x)))" },
64516469
{ "int64_t ", "int " },
64526470
{ "(int64_t)", "(int)" },
64536471
{ "if (!output || nitems < 0) {\n"
@@ -6462,7 +6480,7 @@ static char *dsl_wasm32_patch_source(const char *src) {
64626480
};
64636481
size_t nrepls = sizeof(repls) / sizeof(repls[0]);
64646482
size_t src_len = strlen(src);
6465-
size_t alloc = src_len + 512;
6483+
size_t alloc = src_len + 2048;
64666484
char *patched = (char *)malloc(alloc);
64676485
if (!patched) return NULL;
64686486
const char *p = src;
@@ -6517,48 +6535,6 @@ static bool dsl_wasm32_source_calls_symbol(const char *src, const char *name) {
65176535
return false;
65186536
}
65196537

6520-
static bool dsl_wasm32_source_uses_cast_intrinsics(const char *src) {
6521-
if (!src) {
6522-
return false;
6523-
}
6524-
const char *tok_int = "ME_DSL_CAST_INT(";
6525-
const char *tok_float = "ME_DSL_CAST_FLOAT(";
6526-
const char *tok_bool = "ME_DSL_CAST_BOOL(";
6527-
size_t tok_int_len = strlen(tok_int);
6528-
size_t tok_float_len = strlen(tok_float);
6529-
size_t tok_bool_len = strlen(tok_bool);
6530-
const char *line = src;
6531-
while (*line) {
6532-
const char *line_end = strchr(line, '\n');
6533-
if (!line_end) {
6534-
line_end = line + strlen(line);
6535-
}
6536-
const char *p = line;
6537-
while (p < line_end && isspace((unsigned char)*p)) {
6538-
p++;
6539-
}
6540-
if (p < line_end && *p != '#') {
6541-
for (const char *q = p; q < line_end; q++) {
6542-
size_t remain = (size_t)(line_end - q);
6543-
if (remain >= tok_int_len && memcmp(q, tok_int, tok_int_len) == 0) {
6544-
return true;
6545-
}
6546-
if (remain >= tok_float_len && memcmp(q, tok_float, tok_float_len) == 0) {
6547-
return true;
6548-
}
6549-
if (remain >= tok_bool_len && memcmp(q, tok_bool, tok_bool_len) == 0) {
6550-
return true;
6551-
}
6552-
}
6553-
}
6554-
if (*line_end == '\0') {
6555-
break;
6556-
}
6557-
line = line_end + 1;
6558-
}
6559-
return false;
6560-
}
6561-
65626538
typedef struct {
65636539
const char *name;
65646540
const void *addr;
@@ -6653,13 +6629,6 @@ static bool dsl_jit_compile_wasm32(me_dsl_compiled_program *program) {
66536629
return false;
66546630
}
66556631
#endif
6656-
if (dsl_wasm32_source_uses_cast_intrinsics(program->jit_c_source)) {
6657-
snprintf(program->jit_c_error, sizeof(program->jit_c_error), "%s",
6658-
"wasm32 runtime JIT does not yet support DSL cast intrinsics");
6659-
dsl_tracef("jit runtime skip: %s", program->jit_c_error);
6660-
return false;
6661-
}
6662-
66636632
/* Patch int64_t → int for nitems (wasm32 backend limitation). */
66646633
char *patched_src = dsl_wasm32_patch_source(program->jit_c_source);
66656634
if (!patched_src) return false;

tests/test_dsl_jit_runtime_cache.c

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1072,11 +1072,11 @@ static int test_cast_interpreter_jit_parity_compilers(void) {
10721072
return 0;
10731073
}
10741074

1075-
static int test_wasm_cast_intrinsics_jit_skip(void) {
1075+
static int test_wasm_cast_intrinsics_jit_enabled(void) {
10761076
#if !defined(__EMSCRIPTEN__)
10771077
return 0;
10781078
#else
1079-
printf("\n=== DSL JIT Runtime Cache Test 8c: wasm cast intrinsics runtime-JIT fallback ===\n");
1079+
printf("\n=== DSL JIT Runtime Cache Test 8c: wasm cast intrinsics runtime-JIT enabled ===\n");
10801080

10811081
int rc = 1;
10821082
char *saved_jit = dup_env_value("ME_DSL_JIT");
@@ -1103,8 +1103,8 @@ static int test_wasm_cast_intrinsics_jit_skip(void) {
11031103
goto cleanup;
11041104
}
11051105

1106-
if (me_expr_has_jit_kernel(expr)) {
1107-
printf(" FAILED: expected wasm runtime JIT fallback for cast intrinsics\n");
1106+
if (!me_expr_has_jit_kernel(expr)) {
1107+
printf(" FAILED: expected wasm runtime JIT kernel for cast intrinsics\n");
11081108
me_free(expr);
11091109
goto cleanup;
11101110
}
@@ -1472,7 +1472,7 @@ int main(void) {
14721472
fail |= test_jit_disable_env_guardrail();
14731473
fail |= test_default_tcc_skips_cc_backend();
14741474
fail |= test_cast_interpreter_jit_parity_compilers();
1475-
fail |= test_wasm_cast_intrinsics_jit_skip();
1475+
fail |= test_wasm_cast_intrinsics_jit_enabled();
14761476
fail |= test_missing_return_skips_runtime_jit();
14771477
#else
14781478
fail |= test_negative_cache_skips_immediate_retry();

0 commit comments

Comments
 (0)