Running
What a program looks like
One complete working program, and a plain list of the shapes that do not compile. Read it in a minute and you will know whether your code is in scope.
Compiles and runs
Classes holding collections of instances, inheritance with virtual dispatch, methods calling methods, string and dict work, comprehensions, f-strings, exceptions as objects, generators, closures, printing and file I/O through WASI, and imports across your own files.
Refused, loudly
Everything not implemented is a compile error naming the construct, its line, the enclosing function, and a workaround. A refusal is the design, not a bug.
Answers differently
Two documented cases: int is 32 bits and wraps instead of growing, and a NaN inside a collection does not equal itself. Everywhere else the rule is Python's answer or a loud failure.
A whole program
This is the code of examples/shopping_cart.py from the repository, as it is apart from its module docstring. Nothing in it was chosen to show off a compiler feature, and its results are asserted against what CPython answers for the same source, under three WebAssembly runtimes.
class Item:
def __init__(self, name: str, price: float, qty: int):
self.name = name
self.price = price
self.qty = qty
def subtotal(self) -> float:
return self.price * self.qty
class Cart:
def __init__(self):
self.items = []
self.count = 0
def add(self, name: str, price: float, qty: int):
item = Item(name, price, qty)
self.items.append(item)
self.count = self.count + 1
def total(self) -> float:
out: float = 0.0
for it in self.items:
out = out + it.subtotal()
return out
def units(self) -> int:
n: int = 0
for it in self.items:
n = n + it.qty
return n
def discount_rate(total: float) -> float:
if total >= 100.0:
return 0.10
if total >= 50.0:
return 0.05
return 0.0
def checkout() -> float:
cart = Cart()
cart.add("widget", 9.99, 3)
cart.add("gadget", 24.50, 2)
cart.add("doohickey", 5.00, 4)
subtotal = cart.total()
rate = discount_rate(subtotal)
return subtotal - subtotal * rate
def item_count() -> int:
cart = Cart()
cart.add("a", 1.0, 2)
cart.add("b", 2.0, 3)
return cart.count * 100 + cart.units()
def empty_cart_total() -> float:
cart = Cart()
return cart.total()
Compiled, every function above is an export a host can call: checkout() answers 94.0215, item_count() answers 205, and empty_cart_total() answers 0.0, as CPython does. What you need to know to write the same:
- Annotations
- Parameter and return annotations pick the WebAssembly value types. A collection passed into or out of a function needs the parameterised form,
List[str]rather than a barelist: the parameterised form is what gives the compiler an element type. Calls may pass arguments by keyword, and functions and__init__collect extras with*args: Tand**kwargs: T. - Entry points
- Every module-level
defis exported. There is no top-level run step, so module level holds definitions only; put the code that runs in a function, or underif __name__ == "__main__":, which makes the module a WASI command thatwasmtime runruns likepython3. - Imports
import modandfrom mod import fresolve sibling.pyfiles (andpkg/mod.pypackages) from disk and link them into the one output module, each compiled once. No PyPI packages: beyond your own modules, the supported subset is standard-library only.- Side effects
print(),input(),sys.argv, andopen()go through WASI, and a module imports only the WASI functions it uses, so the program above imports nothing at all. See Running compiled modules.
What it costs
Median of nine release-build compilations on an Apple M1 Pro, via just benchmark. Wall-clock numbers from one machine, so read them for orders of magnitude, not percentages.
A whole program compiles in a few milliseconds into a module of one to a few tens of kilobytes, with no interpreter bundled in: the module is the program, plus the runtime helpers it calls (float formatting, string case mapping, and the like), each built only when a program uses it. Optimization is where the time goes. Binaryen costs 4 to 14 times the rest of the pipeline and takes 6 to 54% off the module, so it is worth running for something you ship and worth skipping in a fast edit loop, which is what CompilerOptions { optimize: false, ..CompilerOptions::default() } is for. The first three rows are end-to-end programs from the repository; nested_collections is the feature example with the largest module, as a second data point at a different shape.
The rule the compiler holds itself to: a construct either produces Python's answer or fails loudly. It never quietly produces a different one.
There are two documented exceptions. Integer width is the first: int is a 32-bit two's-complement value, so 1000000 * 1000000 wraps to -727379968 where CPython answers 1000000000000. If your arithmetic can exceed 32 bits, your program is not in the supported subset. The second is a NaN inside a collection, which does not equal itself (see the list below). Both are explained under Numbers.
Shapes that do not compile
Tagged by what happens when you try. refused is a compile error you will see immediately; traps stops the module at run time instead of answering; differs is a case where the answer is not Python's; workaround means there is a supported way to write the same thing.
-
Module-level statements other than definitions
refusedworkaroundA loop,
if,try, bare call, augmented assignment, tuple unpacking, or write through a subscript or attribute at module level. A WebAssembly module has no top-level run step, so the statement would be compiled away.X = 1,X = helper(),X = ClassName(),def,class, and imports (atry/except ImportErrorguarded import included) do work, and a definition that is not a plain constant is evaluated once, at instantiation, in source order, so every function shares the same list or instance. Move the rest into a function, or underif __name__ == "__main__":. -
refusedasync def,await,async for,async withThere is no event loop in the output.
-
Starred parameters on methods,
refused**unpacking in a call, positional-only parametersFunctions and
__init__take*args: T(a tuple ofT),**kwargs: T(adict[str, T]), and keyword-only parameters,f(*xs)passes a list or tuple on as*args, and keyword arguments work in every call. Other methods take neither starred nor keyword-only parameters, a call cannot unpack a dict with**, and/is refused. Every call to one of the module's own functions is checked against its signature, so a surplus or missing argument is a compile-timeTypeError. -
refusedmatch,del name,except*,typealiases,from x import *Not implemented. Each is reported by name with its location, and so is
delof an attribute or a stepped slice.global,nonlocal, nesteddef,assert, conditional expressions, anddelof a list index, list slice, or dict key do work. -
Metaclass shapes beyond
refused__new__,__init__, and__call__Multiple inheritance works, along CPython's C3 method resolution order with cooperative
super(), and so do metaclasses: a metaclass's__new__and__init__run once for each class it makes, specialized at compile time, and its__call__replaces instantiation. The classnamespaceargument,__prepare__, creating a class by calling the metaclass, and class keywords other thanmetaclass=are refused. -
An override that changes the signature
refusedworkaroundEvery implementation of an overridden method is reached through one indirect call, which names a single WebAssembly signature, so
def m(self)in the base anddef m(self, k: int)in the subclass is refused. Give the override the same parameters and return type. -
Indexing, measuring, or calling a method on a parameter nothing types
refusedworkaroundA lambda's parameters, and a nested function's unannotated ones, take their types from the calls in their scope when those agree, and a
sorted()key's from the elements it is given, sosorted(pairs, key=lambda kv: kv[1])works. A parameter nothing types (a lambda only passed on to an unannotated parameter, or called with an int in one place and a str in another) holds an untyped word, sokv[1]orlen(w)on it is refused rather than answering from that word. Use a nameddef, whose parameters can be annotated. -
A decorator the compiler cannot apply
refusedworkaroundThe method kinds,
@dataclass,@abstractmethod,@functools.total_ordering,@functools.singledispatchwith@f.register, the caching decorators (accepted, no effect), and decorators the program writes itself all work on module functions: a user decorator is applied at compile time and specialized for each function it wraps, factories and stacks included. A user decorator on a method or a class, a wrapper that uses*argsother than to pass it on,@cached_property,@singledispatchmethod, and a decorator name the program never defines are refused, and so are calls tofunctools.partial,reduce, andcmp_to_key. Write the wrapper's work inline, or use@property. -
Comparing collections whose element types differ or are unknown
refusedworkaroundTuples and lists compare by value, and a tuple works as a set member or a dict key. Comparing two with different element types (
(1, 2) == (1.0, 2), True in CPython) or with element types the compiler cannot see is refused rather than read at the wrong width, and so are equality between dicts and a list, dict, or set used as a set member or dict key (unhashable in CPython). Annotate the collections (List[Tuple[int, int]]) so both sides are known. -
Mixing types in one collection
refusedworkaroundEvery slot in a collection is read at one width, so a literal mixing types no one type holds (
[1, "a"],[1, True]) is refused; use a class or a tuple for a record. An int written into a float collection is widened for you (xs.append(3)on aList[float]). The other direction is refused, because storing2.5where the slot is read as an integer would truncate it. Make the elements one type. -
Reading a field through an unannotated parameter
refusedworkarounddef total(cart): return cart.counthas no type forcart, so the read is refused with a hint rather than answering 0. Annotate the parameter (cart: Cart). The rich comparison methods are the exception: an unannotatedotherin__eq__,__lt__, and the rest is typed as the class. -
A string, float, collection, or instance through an untyped place
refusedworkaroundAn unannotated parameter or return, and the elements of a bare
list,set, ordictparameter or return, hold one word read back as an int. Ints, bools,None, and functions pass through; anything else is refused, since a string would lose its length and a float its width. Annotate the parameter, or writeList[str]andDict[str, int]. A bare annotation on a variable is fine: its elements take the type of the value assigned. -
refusedworkaroundx is Noneon an untyped value, anddict.get(key)without a defaultNoneis the word 0, which an untyped value can also be, so the test cannot be answered there. AnOptional[int],Optional[bool], orOptional[float]is boxed, sois None,==, and truthiness on one give CPython's answer; arithmetic between two optional numbers is refused.dict.get(key)with no default is refused where the values are ints, floats, or strings, which have no distinct word for theNonea missing key answers; pass a default. -
refusedstr.formator%on a format string built at run timeThe whole format specification mini-language works in f-strings,
format(),.format(), and%, digit for digit with CPython..format()and%need a literal format string, and a*width or precision in%, or a precision on%d,%x, or%o, is refused. A spec CPython rejects is a compile error carrying CPython's message when it is a literal, and traps when it is built at run time. -
traps**with a fractional or negative exponentPowers are computed by repeated multiplication.
2.0 ** 0.5traps, because it needs exp/log this runtime does not carry, and a negative integer exponent traps because Python's answer is a float an int cannot hold.float()andint()of a string with non-ASCII digits or spaces trap too, where CPython would accept them. -
Two modules defining the same function name
refusedworkaroundMerged modules share one namespace, so
utils.formatnext toreport.formatis a compile error naming both files. Rename one. -
Object lifetime
differsThe allocator has no
free: every instance lives until the module is torn down, and__del__is never invoked. A long-running program that allocates in a loop will grow its memory, and a generator abandoned mid-iteration does not run itsfinallyblocks untilclose()is called. -
A NaN inside a collection
differsWith
x = float("nan"),(x,) == (x,)and[x] == [x]are False here, where CPython answers True: floats are unboxed values, so there is no object identity for CPython's same-object shortcut to find.x == xis False in both. -
Tracebacks
differsNo traceback is kept. An exception a
__main__block does not catch writes only the last line of CPython's report (ValueError: boom) to standard error and exits with status 1. -
Third-party packages, and most of the standard library
refusedNo PyPI. Standard-library support is small so far: the module constants (
math.pi,os.sep, ...),sys.stdin,sys.stdout,sys.stderr,sys.argv, andsys.exit(),re.subandre.escapeon constant arguments, and thefunctoolsdecorators. Every other function insys,os,os.path,math,random,json,re,datetime,logging,collections,itertools, andfunctoolsis refused at compile time, and each is tracked on the modules board.
Where to look next
The repository's examples/ directory holds the programs this page is drawn from, each asserted against CPython in the test suite.