Getting started¶
This page compiles and runs a first program, looks at what p2w produced, and shows what happens when a program uses something p2w does not support. It assumes p2w and its tools are installed.
Compile and run¶
Save this as fib.py:
p2w -r compiles it, assembles the result with wasm-tools, and runs the module under Node.js:
Step by step¶
Without -r, p2w writes WebAssembly text, to standard output or to the file given with -o:
$ uv run p2w -v fib.py -o fib.wat
Compiling fib.py...
Generated 5805 lines of WAT.
Wrote fib.wat
$ wasm-tools parse fib.wat -o fib.wasm
Most of those lines are the runtime: the builtins, the string, list and dict helpers, and the GC type definitions, written in WAT. p2w drops the runtime functions nothing in the program reaches, so fib.wasm is about 41 KB.
The module cannot run on its own. It imports its I/O from the host (env.write_char, env.write_i32, env.f64_to_string, …), and exports _start, which runs the module's top-level code, and memory. p2w -r writes a small Node.js loader that provides the imports and calls _start; a page in a browser needs a loader of its own (see JavaScript interop).
A longer program¶
Classes, __slots__, math, lambdas, f-strings, match, generators and exceptions compile as they run under CPython:
import math
class Shape:
name = "shape"
def area(self) -> float:
raise NotImplementedError
def __repr__(self) -> str:
return f"{self.name}({self.area():.2f})"
class Circle(Shape):
__slots__ = ("r",)
name = "circle"
def __init__(self, r: float) -> None:
self.r = r
def area(self) -> float:
return math.pi * self.r**2
class Square(Shape):
__slots__ = ("side",)
name = "square"
def __init__(self, side: float) -> None:
self.side = side
def area(self) -> float:
return self.side * self.side
shapes = [Circle(1.0), Square(2.0), Circle(0.5)]
print(sorted(shapes, key=lambda s: s.area()))
counts: dict[str, int] = {}
for word in "the cat sat on the mat".split():
counts[word] = counts.get(word, 0) + 1
match max(counts.items(), key=lambda kv: kv[1]):
case (word, 1):
print("all words are unique")
case (word, n):
print(f"{word!r} appears {n} times")
def evens(n):
for i in range(n):
if i % 2 == 0:
yield i
try:
print(sum(evens(10)) // 0)
except ZeroDivisionError as e:
print("error:", e)
$ uv run p2w -r shapes.py
[circle(0.79), circle(3.14), square(4.00)]
'the' appears 2 times
error: integer division or modulo by zero
The output is byte for byte what python3 shapes.py prints.
When p2w says no¶
A construct p2w cannot compile faithfully is refused at compile time, with the construct named:
$ echo 'import itertools' > uses_itertools.py
$ uv run p2w -r uses_itertools.py
p2w: error: NotImplementedError: Import not supported: itertools
Some limits can only show at run time. Integers are 31 or 64 bits wide, so a result past 64 bits raises instead of wrapping around:
The known bugs page lists every refusal and run-time limit that is known.
Making it fast¶
Annotate the hot code with WebAssembly's own number types, i32, i64 and f64, and p2w keeps those values unboxed:
from __future__ import annotations
def count_primes(limit: i32) -> i32:
count: i32 = 0
n: i32 = 2
while n < limit:
d: i32 = 2
prime: bool = True
while d * d <= n:
if n % d == 0:
prime = False
break
d += 1
if prime:
count += 1
n += 1
return count
print(count_primes(100_000))
from __future__ import annotations keeps CPython from evaluating the annotations, so the same file still runs under python3. Performance and native types explains what the compiler does with them.