Skip to content

WASM: browser support for QuantEcon.py (Numba + JupyterLite) — tracking issue #925

Description

@mmcky

Note

Updated 2026-09-09. Revised after an adversarial re-check of every claim in this body against live state. Since 2026-08-31: PR #937 merged, so #926 is closed and Gap 1 is resolved; PR #947 removed the sympy dependency; #950 (WASM testbed site) was opened and linked as a sub-issue on 09-07 and is now carried in the plan; and item 1 of #931 was falsified — network calls work in the browser (@oyamad, 2026-09-06), so PR #948 needs rework. Two claims in the previous revision are corrected below (coverage is not packaged for WASM; the dependency list). Every row in the Gaps and Work plan tables now carries an evidence marker. The revision log is in the comments.

Evidence convention

The failure in #931 was a claim about browser behaviour written from first principles rather than from a run, and it reached a merge-ready PR before anyone tested it. To make that class of error visible, every claim below carries one of three markers. Add the marker whenever you add a claim, and cite the run or the file.

Marker Meaning
[browser] Observed in a real xeus-python kernel; the comment or run is linked
[source] Verified by reading the code, a patch, or a recipe in this repo or upstream
[inferred] Reasoned from platform properties, not observed. Treat as a hypothesis, and do not build a PR on it without a browser check

Background

On 11 August 2026 QuantStack announced Numba in the Browser (Anutosh Bhat): a genuine Numba JIT running entirely in the browser. Python functions are compiled through llvmlite to WebAssembly, linked in-process with LLD, and loaded as Emscripten side modules into a JupyterLite kernel — no server anywhere. Numba is now published on emscripten-forge (numba 0.67.0 build 1 + 10 emscripten patches; llvmlite 0.49.0 + 4 patches), and the announcement showcases the economics stack built on top of it (EconForge's interpolation.py and Dolo.py, PyMC/PyTensor). QuantEcon.py is the obvious next citizen of that ecosystem, and its authors explicitly invite the wider Numba ecosystem to validate their packages.

Concretely, "browser support" for QuantEcon.py means: works in JupyterLite with the xeus-python kernel against the emscripten-forge + conda-forge channels. This is not the classic Pyodide kernel — Numba remains unavailable there (see numba/numba#3284, pyodide/pyodide-recipes#192).

Where we stand (verified 2026-09-09)

Fact Value Evidence
main 0c7c816; five commits since the last revision (#923, #937, #831, #949, #947) [source] git log origin/main, 2026-09-09
Sub-issues 11 linked, 1 closed (#926 via PR #937). #950 has been a linked sub-issue since 2026-09-07 but was absent from this body and from the work plan until this revision [source] GitHub, 2026-09-09
Open PRs #943 (#930, CLEAN, unreviewed since 2026-08-22); #948 (#931, CLEAN, needs rework — its premise is falsified); #952 (#929, CLEAN, premise unverified in browser) [source] GitHub, 2026-09-09
Closed PRs #937 merged 2026-09-09 (closed #926); #938 merged; #935, #942 closed unmerged [source] GitHub, 2026-09-09
Runtime dependencies Four: numba, numpy, requests, scipy. sympy removed by PR #947 (2026-09-09) [source] pyproject.toml @ 0c7c816
Branches in this repo wasm-930-dynamic-gufuncs (7c512ed, oyamad, 2026-08-23; still no PR) [source] git ls-remote, 2026-09-09
emscripten-forge numba 0.67.0 build 1, 10 patches; recipe unchanged since 9a2db20 (2026-08-18) [source] recipe.yaml, 2026-09-09
emscripten-forge/recipes#6309 (#944) open; no maintainer reply since oyamad's repro on 2026-08-20 [source] GitHub, 2026-09-09
Jitted-generator hang (#927) root cause confirmed by symbol probe and a monkeypatch that makes the generator return; still no upstream report filed [browser] #927 thread, 2026-08-19
numba/numba#10128 open; the candidate fix at numba/numba#10545 is open and unmerged [source] GitHub, 2026-09-09
Browser runs from this repo's CI still none. All browser evidence comes from the public numba-ecosystem demo, run by hand [source] #928 results table
#928 evidence table 5 of 14 rows have browser evidence; 9 remain "not yet run" [source] #928, 2026-09-09

The starting position is strong. QuantEcon.py is pure Python (noarch on conda-forge, universal wheel), every runtime dependency is already resolvable for the browser, and all ~90 jitted callables are nopython-mode with lazy specialisation — exactly the model the WASM engine implements. import quantecon already works in the xeus-python kernel, _numba_linalg_solve / np.linalg.solve inside @njit return correct results there, and the requests / urllib network paths work too [browser].

The library's widespread cache=True discipline should become an asset under JupyterLite's persistent cross-session compilation cache (patch 0006), but until emscripten-forge/recipes#6309 is resolved it is a liability: a cache=True function compiled in a session where a cache=True callee was restored from a previous session crashes with RuntimeError: no compiled object yet (#944). This affects every cached call chain (simplex_grid, the pivoting-based solvers including lemke_howson), not just comb_jit.

The constraints of the in-browser Numba build, read from the ten emscripten-forge patches and the recipe's tests — all [source]:

Gaps

# Gap Evidence Severity
1 gini_coefficient uses @njit(parallel=True) + prange Resolved. PR #937 merged 2026-09-09 replaced it with the O(n log n) sorted closed form; #926 is closed. The browser behaviour of the new implementation has not been observed [source] Closed
2 support_enumeration and vertex_enumeration hang because jitted generator functions hang on the emscripten-forge build: numba requests the symbol numba_make_generator but patch 0002 exports only Numba_make_generator. Confirmed by a ctypes symbol probe and by a PythonAPI._get_function redirect that makes the generator return [0, 1, 2]. vertex_enumeration is expected to fail by the same mechanism but has not been observed [browser] for support_enumeration and the root cause; [inferred] for vertex_enumeration Blocked on upstream
3 import quantecon eagerly compiles four guvectorize ufuncs (_probvec_parallel, _probvec_cpu, _sample_without_replacement, _ints_arr_to_bits) plus one eagerly-signed @jit, comb_jit, carrying types.intp(types.intp, types.intp). ufunc caching is disabled on Emscripten (patch 0007) so the four gufuncs recompile every session. Native cost measured 2026-09-09: 1.85 s cold vs 0.65 s warm, so ~1.2 s is eager compilation. Browser cost still unmeasured; test_import_time in the smoke suite is the instrument [source] inventory; native timing measured; browser unmeasured UX / performance
4 32-bit intp on wasm32: the comb_jit / simplex_grid overflow guard trips at 2³¹−1 instead of 2⁶³−1. Correct by design (the value is an array size); simplex_index is pure Python and cannot wrap, and the tests are already intp-keyed. Only the docstrings need to state the platform-dependent boundary (#929) [inferred] — one line in a kernel (np.iinfo(np.intp).max) settles it, and PR #952 should not merge before that Documentation
5 A cache=True function compiled in a session where a cache=True callee was restored from the persistent cache fails with RuntimeError: no compiled object yet (patch 0006, emscripten-forge/recipes#6309). Deterministic for simplex_grid via num_compositions_jit → comb_jit; also exposes k_array_rank_jit → comb_jit and the _pivoting / _lex_min_ratio_test callers (lemke_howson, linprog_simplex, lcp_lemke). Only reproduces on a warm cache [browser] #944 Blocked on upstream
6 New. quantecon/util/array.py runs with objmode(): inside @jit(nopython=True) to emit a DeprecationWarning from the public searchsorted helper. objmode lifts back into the CPython interpreter from compiled code and requires the GIL; none of the ten emscripten patches mentions it. Previously filed under "what does not need to change", which was too generous — the "no object-mode @jit" claim is true of the decorator but skips the context manager [inferred] — row 14 of #928's table, "not yet run" Unknown until run
7 Withdrawn. Network utilities were listed as failing in the browser because WASM has no sockets. This is false. GAMReader.from_url and requests.get both work: urllib3 has shipped native Emscripten support since 2.2.0 and enables it automatically when sys.platform == "emscripten". See #931 and PR #948 [browser] @oyamad, 2026-09-06 Not a gap

Work plan

Phase Issue Work item Status (2026-09-09)
0 — Prove it #928 JupyterLite proof-of-concept deployment + browser smoke suite Config, ci/wasm/smoke_test.py and the Playwright harness on main (PR #938). Results table has 5 of 14 rows filled, all from the public demo. Nothing has run in the browser from this repo
0 #950 Priority raised 2026-09-09 — this is now the lead item of the campaign. WASM testbed site under the QuantEcon org: a JupyterLite site on Pages whose environment can include locally patched packages, with the smoke notebooks preloaded Not started. Two jobs, and the second is why it moved up: it replaces "paste cells into someone else's demo" as our evidence source, and it is the bench on which we design and test the patches we contribute upstream. A C-level fix cannot be validated by monkeypatching a live kernel — it needs a rebuilt package running in a browser, which is exactly what #950 provides
1 — Code fixes #926 Replace the O(n²) parallel loop in gini_coefficient with the O(n log n) sorted closed form Done. PR #937 merged 2026-09-09; #926 closed. Note the smoke suite's test_gini_coefficient still carries a strict=True xfail for Emscripten that is now wrong on two counts — see #933
1 #927 Upstream the jitted-generator hang with a minimal repro; decide whether support_enumeration / vertex_enumeration need Emscripten-gated non-generator paths Blocked on upstream. The draft report in the #927 thread is still unfiled after 19 days, and anutosh491's offer to take it is still unanswered. The repro has not been re-run on 0.67.0 build 1
1 #929 Document the platform-dependent intp overflow boundary (docs only) PR #952 open and CLEAN. Hold for a one-line browser check of np.iinfo(np.intp).max before merging — the claim is very likely right, which is exactly why confirming it is cheap
1 #930 Reduce import-time eager Numba compilation PR #943 CLEAN, unreviewed since 2026-08-22. oyamad's alternative on wasm-930-dynamic-gufuncs still has no PR. Decision needed on which implementation goes forward. Native timing now recorded (Gap 3); the browser gate is still open
1 #931 Document browser limitations; raise helpful platform-specific errors Item 1 falsified — see Gap 7. PR #948 implements guards that would remove working functionality and must be cut back to the probvec docstring note (item 2), which is correct and should land. Item 3 (demote requests) is dependency hygiene with no WASM rationale and is entangled with the separate question of deprecating util/notebooks.py in 0.12.0 — move it out of this tracker. Item 4 (docs page) stands
1 #944 cache=True caller linking a cache-restored callee fails with "no compiled object yet" Blocked on emscripten-forge/recipes#6309, no maintainer response in 20 days. Decision stands: wait for upstream, no library-side change
2 — Distribution #932 Contribute a quantecon recipe to emscripten-forge No recipe yet; quantecon already installs from conda-forge noarch. Justify or drop: the package is pure Python with no build step, so state what the recipe buys (solve speed, pinned coverage, visibility) before spending effort on it
3 — CI #933 Add a WebAssembly/JupyterLite job to CI Native job on main (PR #938); browser runner PR not started. Tasks 1–6 in the body, plus two new ones: the gini strict=True xfail will hard-fail the first browser run, and coverage is not packaged for WASM
4 — Ecosystem #934 Demo notebook, lectures without a kernel server, WASM SIMD exploration After the intermediate release and #932. The JAX-on-emscripten funding conversation with QuantStack is tracked separately — see "Explicitly not doing"

Sequencing. #950 leads. Two arguments put it at the front. The first is defensive: reasoning is currently cheaper than testing, which is what produced #931 — five issues that depend on browser evidence were written while the harness sat unbuilt, two became PRs, and one rested on a false premise. The second is the stronger one. Two of this campaign's five gaps are blocked on upstream fixes we have already diagnosed but cannot build or test (#927, #944). Being able to compile a patched numba and exercise it in a real kernel turns us from a reporter of upstream bugs into a contributor of upstream fixes — with browser evidence attached to the PR. That is a different relationship with emscripten-forge, and it is worth more than any single item further down this plan. #933 remains complementary rather than an alternative: it gives regression CI against the stock stack, while #950 gives the interactive, deep-linkable kernel and the patched-build bench. Both before more documentation written from inference.

Front of the plan (next session):

  1. Build WASM: Host a WASM testbed site under the QuantEcon org #950. The lead item. It converts the 9 "not yet run" rows into evidence, removes the dependency on a third-party demo, and — the reason it outranks everything else — gives us a bench on which to design and test upstream patches. The first candidate is ready: WASM: support_enumeration and vertex_enumeration hang in the browser — root cause is jitted generators (numba.np.linalg._LAPACK verified working) #927's generator alias. A source audit on 2026-09-09 confirms the fix is a single alias and that it is complete — _dynfunc.c_helpers contains exactly one entry, make_generator, and numba/core/base.py registers every helper from both helper dicts as "numba_" + key, so Numba_make_generator is the only helper whose registered name differs in case from its C symbol (_helperlib's 158 are already lowercase in C; NRT's 25 register as NRT_<name> and match). Once WASM: Host a WASM testbed site under the QuantEcon org #950 exists that patch can be built, run, and sent upstream with evidence rather than offered as a diagnosis.
  2. Rework PR WASM: raise OSError for network calls on Emscripten (#931) #948 to the probvec docstring only, and correct item 1 of WASM: document browser limitations and raise helpful platform-specific errors #931 so the next person is not sent down the same path.
  3. Fix the gini strict=True xfail in ci/wasm/smoke_test.py before WASM: add a WebAssembly/JupyterLite smoke-test job to CI #933's runner executes, or the first correct browser run reports red.
  4. File the jitted-generator report upstream (WASM: support_enumeration and vertex_enumeration hang in the browser — root cause is jitted generators (numba.np.linalg._LAPACK verified working) #927) — take anutosh491's offer or file from the draft; re-run the repro on 0.67.0 build 1 first.
  5. Decide the WASM: reduce import-time eager Numba compilation #930 implementation — PR WASM: defer eager Numba gufunc/jit compilation to first use (#930) #943 as rebased, or a PR from wasm-930-dynamic-gufuncs. Both satisfy the Race condition in GUFunc caching can lead to segmentation faults numba/numba#10128 constraint. Then either record the browser import timing or waive the gate explicitly.

Explicitly not doing

What does not need to change

No C extensions, no build step, no subprocess/multiprocessing/threading anywhere in the runtime package [source], no @jitclass, no generated_jit, no threading-layer APIs. All four runtime dependencies resolve in the browser ecosystem today. One correction to the previous revision: the test tooling is not entirely packaged for WASM — pytest and pandas are emscripten-forge recipes, but coverage is not, and conda-forge's coverage is not noarch because it builds a C tracer per platform [source]. #933 should plan for the pure-Python tracer or accept no coverage from the browser job.

Sources

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions