ADR 009. v0.5 roadmap: usable WASM for web devs + selective Python parity + size-then-perf¶
- Status: Implemented in v0.5. Remaining open work fed into ADR 010.
- Decider: project maintainers
- Supersedes: ADR 001 §6 (v0.4 success criteria); ADR 001 §7 (open questions for v0.5+); ADR 005 (optimization-landscape PROTO; this ADR commits to specific arcs from its §3 categories)
- Branches from: ADR 007 (wasm fat loss, In Progress) + ADR 008 (golden-programs, drives the priority signal)
- Consequences: sets four priority lanes for v0.5's arc; commits to size before perf; defers the major MicroPython-only embedded ports (settrace, larger error reporting); makes the integration-suite-on-both-arms harness the primary regression detector going forward.
1. Context¶
The v0.4 roadmap (ADR 001) made WASM the primary distribution target and committed to feature completeness via Themes A (async/await), B (CI + releases), C (web demos), D (cross-platform), and F (per-feature size flags). Themes A/B/D/F shipped; Theme C is ongoing. The C→Zig strangler that motivated the original phasing is largely done: corpus/nano/basics/ reports 538/541 native and make wasm-check reports green at the 468/541 baseline, the runtime works end-to-end, and golden-programs/works/ holds 595 of 823 in-scope real programs (72.3%). The recent session arc closed a long tail of "is_full-gated bug-for-bug compat" defects (LONGLONG on wasm32, float wiring, bytearray ↔ bytes interop, range slicing, defaultdict, six "easy fill" stdlib additions, wasm float-print signature mismatch, …). Net +162 programs landed in works/ in one session.
The shape of the work has shifted. We are no longer porting; we are improving a working runtime. The questions are different:
- Is the WASM build actually pleasant for a web developer to use? (Not yet: float used to silently trap, eval/exec still hits the longjmp wall, the cold-start
.wasmis 286 KB and ADR 007's 228 KB goal hasn't shipped.) - Are the remaining MicroPython parity gaps small and closable? (Yes: three corpus tests, plus the 27 known-fails on wasm that come from MPZ-needing arithmetic.)
- Are we ready to start cherry-picking Python 3.x features beyond MicroPython? (Selectively, yes: this session showed
range.start,dict.fromkeys,str.title/.center(fill),pow(b,e,m),defaultdict, modular__getitem__all land in well-under-an-hour each.) - Is size or performance the binding constraint? (Size first: 286 KB → 228 KB is the documented gap. Perf is green-band by ADR 004's benchmark; size is the visible loading cost.)
This ADR commits to a four-lane plan that answers each. The four lanes are sequenced by priority, but progress in any can proceed independently.
2. Goals, in priority order¶
- Primary: Usable WASM port for web developers. A web dev who downloads the binary or visits a demo should be able to write idiomatic Python (with floats, bytes/bytearray fluency, dict literals, comprehensions, basic stdlib like
heapq/bisect/random) without surprise crashes, see clean error messages when something goes wrong, and have the page load in <1s on a cold cache. - Secondary: MicroPython parity for embedded. Close the 3 remaining
make checkfailures and reach the long-standing 541/541 byte-exact baseline. Keep the strangler discipline intact (no commit drops below baseline; themakeaggregate gate stays green). - Tertiary: Selective Python 3.15 features beyond the MicroPython baseline. Pick features that help our use cases (web demos, algorithms, data-science workflows). Skip the rest (full
dataclasses, the entiretypingmodule, the GIL story). Includes shipping more pure-Python stdlib via the frozen-tiny mechanism (ADR 003) and porting/mimicking small C-side modules where the cost is low. - Cross-cutting: Size, then performance. ADR 007 Phase 1 (the wasm-opt + per-feature audit) lands first; if there's headroom afterward, attack benchmark hot paths. Performance includes startup time: a 4 MB Debug
.wasmparses on the browser in ~30 ms cold-cache, but a 360 KB optimized.wasmparses in ~5 ms; the size-first sequencing reflects this.
Out of scope for the lanes: full async/await wiring under nano (ADR 002 Path B deferral stands), GIL/threading semantics (_thread stays a stub per ADR 003 §D6), CPython C-API extension loading, native-code-generation for hot Python.
3. Lane A. Usable WASM for web devs (primary)¶
The signal for "usable" is concrete: a web dev shouldn't have to learn nanopython-specific idioms to write a working demo. The remaining friction sits in three buckets.
A1. Close the eval/exec longjmp wall¶
50 programs sit in golden-programs/works-native-only/ because they use eval(), exec(), or compile() and those paths still go through vendor C's nlr_jump, which the wasm ABI cannot unwind. ADR 005 §3.1 noted this; ADR 007 §3 §1.2 acknowledged it as the "bucket-1 architectural blocker" left by the polled-sentinel arc.
Two paths:
- A1.a Port the eval/exec C code to PyError convention (the polled-sentinel pattern). Best-fit if the surface is small: probably ~200-400 LOC across
builtinevex.zigplus the runtime entry. Closes the bucket without depending on emscripten tooling. - A1.b Wire emscripten-sjlj as a build-mode alternative. Lets vendor C use longjmp on wasm via a vendored shim. Larger surface change but unblocks any future vendor C addition that also uses NORETURN raises.
Default position: try A1.a first, treat the longjmp surface as a small + specific port. A1.b is the fallback if A1.a balloons.
Success criterion: golden-programs/works-native-only/ drops below 10. The Python-program-level integration suite gains an eval-or-exec case that runs on wasm.
A2. Close the cold-load size gap (size-first part of Lane D)¶
ADR 007 §4 Phase 1 (P1-P6). The wasm-opt prototype lives on slim-wasm at -14.3% / -5% gzipped; it has not yet landed on main. Ship it. The four "easy" techniques (1 through 4 in ADR 007 §1.3) targeting ≤228 KB raw.
Success criterion: ADR 007 moves from "In Progress" to "Closed" (its Phase 1 acceptance criteria).
A3. Wider feature surface: ship the next round of stdlib + finish the FLOAT-arm details¶
The "easy fills" from this session unlocked 137 programs. The same approach has more headroom:
- A3.a. Frozen stdlib expansion.
re, more ofitertools, morecollections,argparse,string,enum. Each is a vendormicropython-libmodule or a small ~50-LOC pure Python. - A3.b. Multiple inheritance. 10 programs in
needs-stdlib/need it. Real Zig work onobjtype.zig's class-lookup path, but it's well-bounded. - A3.c. MPZ on wasm32. 27 programs hit
OverflowError: long int not supported in this buildbecause we use LONGLONG, capping at 2^63. MPZ would unlock those plus a futureproofing margin. The cost is ~40-80 KB of binary; tension with A2's size goal. Defer until A2 lands and we measure the budget.
Success criterion: golden-programs/works/ reaches 650+ (from 595 today).
A4. Polish the dev experience¶
make wasmalready produces a runnable binary (commit571b1d6); keep it that way. Same withtests/integration.pycovering both arms (commit5444662).- Wire the "what works" view: ship
docs/COMPATIBILITY.mdauto-generated fromgolden-programs/, linked from the README. ADR 008 §3 §Phase 2 already committed to this; just ship it. - Browser smoke (
tools/browser-smoke.py) gains a case per demo so a wasm regression visible-in-UI also surfaces. - Improve startup time only after A2 lands; at the lower size the cold parse time may already be acceptable.
4. Lane B. MicroPython parity for embedded (secondary)¶
The 3 remaining corpus failures are documented in CLAUDE.md's "Known native failures" table. Closing them is the path to 541/541: the long-standing strangler invariant.
- B1.
builtin_help. Wire vendorpy/builtinhelp.cinto the nano carveout. Estimated 1-2 sessions; small. - B2.
gc1. Auditsrc/memory/gc.zigagainst vendorpy/gc.cfor the specific allocations the test counts. Small; needs careful attention togc.mem_alloc()precision. - B3.
string_strip. Some Unicode whitespace handling difference. Tiny; not yet investigated, suspect a single bit-test instr_strip.
Success criterion: make verify exits 0. The 541/541 invariant from CLAUDE.md is reached. (At that point we may want a corpus/nano-known-fails.txt parallel to the wasm-side discipline, empty file initially, so future regressions surface atomically.)
The 27 long-int / 19 small-int / 50 SKIP-on-feature programs in golden-programs/needs-stdlib/ belong to Lane C and A3 rather than Lane B.
5. Lane C. Python 3.x features beyond MicroPython (tertiary)¶
The principle is selective. Pick features that help the priority use cases:
- C1. More dict-subclass dispatch. The
__getitem__workaround incollections.py(commit0b3cee0) is fine, but the proper__missing__protocol dispatch on subscr would compose better. Probably needs runtime changes to pass the outer subclass instance through to the subscr slot. - C2. More iterator support. Right now
reversed()andzip()work; generators work; comprehensions work. Investigate the lazy-generator-method idiomsitertoolsand theyield fromstory for nano (currently gated on full). - C3. Slice with step != 1 for strings.
"abc"[::-1]is a CPython idiom absent in vendor MicroPython and absent here; common enough that the question "is this the next investment?" deserves a numerical answer fromgolden-programs/. - C4. Better error messages.
NameError/AttributeErrornow name the offending identifier (commit5b050a3).TypeErrorin arithmetic could mention which side is wrong.SyntaxErroralready names the line.
Out of scope for Lane C: full dataclasses, full typing, walrus operator (:=), match statements, async generators, cm.contextmanager decorator semantics beyond what nanopython-lib/ already provides.
Success criterion: 5-10 features ship via small, individually-justified commits. Each adds a regression-pin case to tests/integration.py.
6. Lane D. Size, then performance (cross-cutting)¶
Reaffirms ADR 005 §6 (size-primary recommendation) and ADR 007 §2 (Phase 1 commitment).
- D1. Ship ADR 007 Phase 1.
wasm-opt -Oz(already prototyped at -14.3%) lands on main; export strip + WASI cull; the size budget reaches ≤228 KB raw. - D2. Measure startup time end-to-end. A small harness (timer.html or similar) measures cold-cache parse + instantiate + first-print. Goal: <1s cold on a mid-range laptop on a typical web demo.
- D3. Bench-driven perf work, only if D1+D2 produce headroom. Re-run ADR 004's benchmark against MicroPython unix-standard. If we slip materially, attack the hottest opcode (likely
MP_BC_BINARY_OPorMP_BC_LOAD_FAST).
Out of scope for Lane D: LLVM-side optimizations (we're at Zig's ReleaseSmall ceiling); custom calling conventions; ahead-of-time compilation.
Success criterion: web/nanopython.wasm ≤228 KB raw; cold-cache demo load <1s (define "the bar" via tools/browser-smoke.py extended with a --time flag).
7. Sequencing¶
Phases are independent within a lane; lanes can run in parallel by different developers.
Phase α (immediate; this is the next session's worth of work)¶
- Close B1, B2, B3 to hit 541/541 (Lane B is the smallest scope and most well-understood).
- Land ADR 007 P1 (wasm-opt integration): Lane D D1 first half.
- Ship the auto-generated
docs/COMPATIBILITY.mdfromgolden-programs/: Lane A A4.
Phase β (next-after; assumes α green)¶
- Lane A A1 (eval/exec PyError port).
- Lane A A3.a (next round of frozen stdlib).
- Lane D D1 second half (export strip, WASI cull).
- Lane D D2 (cold-load timing harness).
Phase γ (longer arc; pre-requires α + β)¶
- Lane A A3.b (multiple inheritance).
- Lane A A3.c (MPZ on wasm32); gated on D1's size delivery.
- Lane C selective features.
8. Out of scope for v0.5¶
- The MicroPython
async/awaitarc under nano (ADR 002 Path B was the deliberate deferral; still deferred). - A
requirements/pip-style packaging story. Thenanopython-lib/+ frozen-tiny mechanism (ADR 003) is the shipping path for now. - Native binary distribution as a standalone product. Native is a development companion only.
- Any GIL / threading semantics.
_threadstays a stub. - A web framework. The browser demo + ci-build-monitor extension are the demonstrations; users build their own apps.
- Coverage of every CPython 3.15 stdlib module.
re,argparse, basicstringandenumare in scope; the network stack,subprocess,multiprocessing, etc. are out. - C-extension loading. The scope stays at "Python interpreter" rather than "Python platform."
9. Consequences¶
Wins:
- Clear priorities for the next 3-6 months. Each lane has a measurable success criterion.
- The strangler discipline (CLAUDE.md "The 3 known native failures") is now explicit and visible at the top of the doc.
- Size has primacy over performance; matches the user-visible "the demo loads fast" cost model.
- Phase α is concrete enough to start tomorrow.
Costs:
- The 50
works-native-only/programs fromeval/execremain visibly broken on wasm until A1 lands. That state predates this ADR. - MPZ deferral means
print(2**100)keeps failing on wasm until A3.c. The wasm-known-fails entry documents the deliberate trade. - We commit to skipping the long tail of CPython 3.15 stdlib modules. Some users will land on
import socketorimport asyncioand getImportError. That trade is deliberate.
10. See also¶
- ADR 001: v0.4 roadmap (superseded by this for §6/§7).
- ADR 005: optimization landscape (this ADR commits to its size-primary recommendation).
- ADR 007: WASM fat loss (Phase 1 sits inside Lane D D1).
- ADR 008: golden-programs (drives the prioritization signal; the
works/823ratio is the headline metric). notes/03-nano-python-cheatsheet.md: the user-facing constraints doc.CLAUDE.md: the "test harness in plain terms" + "3 known native failures" + "surface problems explicitly" section.