Project
Changelog
Every release of waspy, and what is merged on main since the last one. Rendered from CHANGELOG.md in the repository.
On this page
Upcoming
Added
- Conditional expressions,
a if cond else b, chained and nested anywhere an expression goes (returns, arguments, f-string placeholders, comprehensions, lambdas, module-level definitions). Only the arm the condition picks is evaluated, and the condition is tested for truth asiftests it, so an empty list or string is false. Both arms must read as one type: two classes share their nearest common base, a class andNonemake anOptional, an empty collection takes the other arm's element type, and an int widens only where a float is expected. Arms that would read differently, such as1 if c else 2.5(CPython keeps the int) orTrue if c else 0, are refused examples/conditional_expressions.py, asserted against CPythonassert:assert test, msgraisesAssertionErrorwhen the test is false, evaluating the message only then. It was refusedglobalin functions and methods: assigning a name the function declaresglobalrebinds the module definition for every reader, augmented assignment included (a list extends in place). A plain constant that some function rebinds becomes a real global, and a definition computed from it keeps its import-time value. A rebinding must keep the definition's type, and a global bound by a loop, unpacking, orwithis refuseddelof an item:del xs[i],del xs[a:b](clamped as slicing is), anddel d[k], through names, fields, and several targets at once. A missing index raisesIndexErrorand a missing keyKeyError.delof a tuple or str item is refused as CPython'sTypeError;delof a name, an attribute, or a stepped slice is still refused- Keyword-only parameters (
def f(x, *, factor=2)) on functions and__init__, passed by name or taken from their default; passing one positionally is a compile-timeTypeError. On other methods, whose calls do not take keywords, they are refused - Nested functions:
definside a function or method, typed by its own annotations, so it takes and returns floats and strings at their real types. It closes over the enclosing variables by reference (a float or a string included), calls itself and its siblings, takes keywords and defaults, and nests to any depth. Called by name it is a direct call; returned, stored, or passed on it is a closure value, called at its signature wherever that is known (a variable, a parameter annotatedCallable[[float], float]) and as an all-int function through an untyped place, which refuses a closure taking or returning anything else. Adefinside anif, loop,try, orwithblock, one inside a generator, a nested generator, a computed default, and a call that runs before thedefare refused nonlocal: a nested function rebinds the enclosing function's variable for every reader, augmented assignment included. A name no enclosing function binds is a compile-timeSyntaxError, as in CPython, and a captured variable keeps one type*argsand**kwargson functions, nested functions, and__init__.*args: Tcollects the surplus positional arguments as a tuple ofT(each converted on its way in, so an int widens to a float) that iterates, indexes, slices, compares, unpacks, hashes, and answerslen,in,count,index, andisinstance(args, tuple)as CPython's does;**kwargs: Tcollects the surplus keywords as adict[str, T].f(*xs)passes a list or tuple on as*args(as a copy), so a function can forward its own. An unannotated*argsholds ints, bools, and None only. Star parameters on other methods,**unpacking in a call, and*unpacking into a function without*argsare refused- The rest of the dict API:
update,pop(with or without a default;KeyErrorwithout one),popitem(the last entry,KeyErrorwhen empty),setdefault,copy,clear,dict.fromkeys(iterable[, value]), andd |= otherin place.d[k] = vand the methods that insert share one search-and-grow sequence - The rest of the set API:
update,intersection_update,difference_update,symmetric_difference_updateand the in-place|= &= -= ^=,copy,pop(KeyErrorwhen empty),clear, andset(iterable)of a set, list, tuple, range, or dict.union,intersection,difference, the update forms, and the subset tests take a list or tuple as well as a set, and several arguments where CPython does min()andmax()over an iterable (#118): a list, tuple, or range, withkey=anddefault=, andkey=on the several-argument form too. Ties keep the first element, as in CPython, and an empty iterable without a default raisesValueError. A set, dict, or string argument is refused for now- A module function,
len, orabsnamed as a value, sosorted(words, key=len),max(xs, key=abs), andapply(double, 3)work. They were refused as undefined names - Lambda parameters typed from their calls (#115): a parameter with no annotation, of a lambda or a nested function, takes the type the calls in its scope pass it when they agree, and a
sorted()key's takes its elements' type. So(lambda kv: kv[1])((1, 5))answers 5,sorted(words, key=lambda w: len(w))sorts by length, and a key over floats or returning one works.name = lambda ...is compiled as the nested function it is equivalent to, so it is called directly and takes keywords and defaults. A parameter whose calls disagree stays untyped - Closures capture floats and strings: a captured variable is kept at its own type in its cell, so
k = 1.5; g = lambda x: x * kanswers 3.0 forg(2)and a captured string keeps its length. Capturing a float was refused Tuple[int, ...]annotations: a tuple of any length whose members share a type, the type*argshas. Slicing a tuple whose members share a type gives one (it was refused, its result having no type)Callable[[int, str], bool]annotations, giving a parameter, return, or variable that holds a function its signature.Callable[..., R]is refused, since a call through it cannot be made at a known signaturestr()andrepr()of every type: a float by CPython's shortest round-trip repr (correctly rounded digits, exponent notation outside 1e-4 to 1e16), bools and None by name, strings and bytes quoted and escaped as CPython quotes them, ranges, nested lists, tuples ((1,)included), dicts, and sets in their iteration order, and instances through__str__or__repr__(dispatched on the instance's own class). f-string placeholders render the same way. The defaultreprof an instance, which includes an address in CPython, is refused- The format specification mini-language, shared by f-strings,
str.format,format(), and%: fill, alignment, sign,z,#,0, width,,and_grouping, precision, and the typesb c d e E f F g G n o s x X %, rendered digit for digit as CPython renders them (floats by a port ofPyOS_double_to_stringover exactly rounded digits). A spec may hold replacement fields (f"{x:{w}.{p}f}"). A literal spec CPython rejects is a compile error with CPython's message format(value[, spec]), andascii(),!r,!s, and!ain f-strings andstr.formatstr.formatfields: named ({name}, from keyword arguments), attribute and index lookups ({0.x},{1[2]},{d[key]}), conversions, and specs, numbered automatically or by position as CPython numbers them. An argument that is computed has to be used once each in the order given, since CPython evaluates each exactly once%formatting of runtime values: every conversion (%s %r %a %d %i %u %x %X %o %c %e %E %f %F %g %G %%), the-+ 0#flags, width, and precision, a tuple operand (a literal, or a tuple variable), and%(key)sover a dict.%dtruncates a float as CPython does;%xof a float, a*width, and a precision on an integer conversion are refusedfloat()of a string, correctly rounded: CPython's grammar (surrounding whitespace, a sign,inf/infinity/nanin any case,_between digits, an exponent), parsed into the float CPython produces for every input, halfway cases and subnormals included (Gay'sbigcomp: an approximation corrected against the exact decimal expansion of the midpoints). A string CPython rejects raises a catchableValueError; a literal one is a compile error- Unicode text: case conversion, the
is*predicates (withisdecimalandisnumericadded),strip(), andsplit()follow CPython over the whole of Unicode, from tablesscripts/gen_unicode_tables.pygenerates from CPython itself, so final sigma, titlecase digraphs, andßtoSScome out as CPython's. A program carries only the tables the methods it calls read - A tuple may mix ints and floats,
(1, 2.5), each member read at its own width;in,count, andindexcompare them by value - The host interface is WASI preview 1. A program imports from
wasi_snapshot_preview1only what it uses (fd_write,fd_read,args_sizes_get,args_get,proc_exit), so a program with no side effects still imports nothing, and the module runs unchanged underwasmtime,node:wasi, or a browser shim print()writes to standard output, withsep,end,file=sys.stderr, andflush, every argument rendered bystr()after all of them are evaluated, as CPython evaluates a call first. It wrote nothinginput([prompt]), raisingEOFErrorat the end of input;sys.stdin(readline,read,readlines, andfor line in sys.stdin), read as UTF-8 withUnicodeDecodeErroron bytes that are not;sys.stdoutandsys.stderr(write, which answers the characters written, andflush); andsys.argv, built once from the host's arguments- A program with a
__main__block exports_start, so it runs as a WASI command (wasmtime run program.wasm args); an exception the block does not catch exits with status 1 docs/src/playground/wasi.js, a small WASI preview 1 host for the browser and Node, used by the playground (whose Run tab now shows what a call prints) and byjust verify-runtime- Exception objects.
raise E(args)raises an instance andexcept E as ebinds it:str(e),repr(e)(naming the instance's own class),e.args, user exception classes with__init__, fields, and__str__,super().__init__(...), andraise X from Ywith__cause__. Every exception is an instance of a class: CPython's builtin hierarchy is part of each module that can raise, so user exceptions derive from it like any base. The runtime's own faults raise objects carrying CPython's messages,IndexError('list index out of range'),KeyError(key),ValueError("invalid literal for int() with base 10: 'x'"),ZeroDivisionError('integer division or modulo by zero'), and the rest.except ... as eused to bind a type code that could only be re-raised - Subclass matching:
except Basecatches a subclass, along user classes and the builtin hierarchy (LookupErroroverIndexErrorandKeyError,ArithmeticErroroverZeroDivisionError,RuntimeErroroverNotImplementedError, ...). Only the named class matched, andexcept Exceptioncaught everything,SystemExitincluded - An exception a
__main__block does not catch writes the last line of CPython's report to standard error (ValueError: boom, or the class name alone) and exits with status 1 sys.exit([status])raises a catchableSystemExit; one nothing catches exits with its status: an int, 0 for none or None, and 1 with the argument printed to standard error for anything else.raise SystemExit(...)does the same- File I/O goes through WASI:
open()resolves its path against the host's preopened directories withpath_open, and reads, writes, and closes throughfd_read/fd_write/fd_close, so a program with files runs underwasmtime run --dir .ornode:wasilike any other, and every side effect has one interface. A file that cannot be opened raises CPython'sFileNotFoundError([Errno 2] No such file or directory: 'x'),PermissionError,IsADirectoryError, ...; it answered -1. Thewaspy_hostimports remain behind the new, deprecatedCompilerOptions::waspy_host_file_io, until 1.0 - None is distinct from 0 for optional numbers. An
Optional[int],Optional[bool], orOptional[float]is boxed (None is the null pointer, a number a cell holding it), sox is None,==, truthiness, andstr()give CPython's answers, arithmetic and an ordering on None raise itsTypeError, anddict.get(key)on a dict of numbers answers None for a missing key. All were refused; anOptional[float]failed validation - Metaclasses (#8):
class M(type)withmetaclass=M, inherited by subclasses.M.__new__andM.__init__run once as each class is created (in the module's initialization, where the class statement stands), the attributes they give the class are its class variables,M.__call__replaces instantiation, and M's other methods and class variables are reached through the class. A metaclass conflict is CPython'sTypeError; the namespace argument,__prepare__, and calling the metaclass to make a class are refused. Class keywords were refused outright - Classes are values:
type(x),x.__class__, a class named as a value,type[A]annotations, identity comparison,__name__, the class'srepr, and its class variables - Class variables are rebound through their class (
Counter.count += 1from any function); this was refused, and a constant one was inlined at every read - A class variable read through an instance reads the class's, or the subclass's own: it was laid out as an instance field nothing set, so
self.labeltrapped andself.countread 0 yieldinsidetry,except,finally, andwith: a generator resumes inside the block it suspended in, so a later exception reaches its handler, afinallyruns on every way out (falling off, an exception,return,break,continue), and awithcalls__exit__.close()raisesGeneratorExitat the suspendedyield, so handlers andfinallyblocks see it, and a generator that yields instead raises CPython'sRuntimeError: generator ignored GeneratorExit. All three were refused or skipped- Generator methods: a method containing
yieldmakes a generator holding its instance. One that another class of its hierarchy also defines is refused zip()with a generator after its first argument, pulled one value per round in CPython's order. It was refused- A loop or unpacking target read after a
yieldkeeps its value; it was refused - A generator yielding strings yields strings. It was typed as yielding ints, so a consumer read each string's address
- A
rangeloop leaves its variable at the last value. It counted in the variable itself, so afterfor i in range(3)it held 3 (CPython: 2), an empty range overwrote it, and assigning to it in the body changed the iteration - After a
zip()loop cut short, the targets hold the last whole tuple; the first target already held the extra value - A
forloop over a value that is not iterable is refused. It counted the value down as a length, sofor x in 5ran five times. Iterating a generator object whose class is not known where it is iterated is refused too; it was walked as a list - A field first assigned inside a
tryin a method is laid out; it was missed - User-written decorators on module functions, applied at compile time: a decorator
def deco(fn)returning a nested wrapper orfnitself, a decorator factory (@repeat(3)), and stacks of them. A wrapper forwardingfn(*args, **kwargs)is specialized with the decorated function's parameters (so keyword arguments at the call still bind), state the decorator keeps and the wrapper updates throughnonlocalis one per decorated function, statements the decorator runs (registering the function, say) run once when the module is initialized, andfn.__name__and@functools.wrapswork. A wrapper whose answer would depend on how each call is written (len(args)) is refused. They were refused outright - A return of the wrong kind of value is refused. A function declared
-> intreturning astr(or a list, or an instance) handed its caller the value's address as the int, and-> strreturning a number read the number as a string. CPython returns the value whatever the annotation says, so this is now a compile error asking for the annotation to be corrected - Multiple inheritance with CPython's C3 method resolution order: a class takes the methods, properties, class variables, and fields of every base, a method another class of its MRO defines too dispatches on the instance's own class (so a mixin's
self.describe()reaches a sibling's),super()follows the MRO of the instance's class (cooperative__init__chains through a diamond run each class once), andisinstance,issubclass, andexceptmatch every base. An inconsistent order is refused as CPython'sTypeError. It was refused outright __exit__receives the exception leaving awithblock and suppresses it by answering true; an exception__exit__raises itself replaces it. It received(None, None, None)on every path and its answer was ignoredexamples/word_frequency_cli.py, an end-to-end program run as a WASI command:just verify-runtimegives it the input and argument of its fixture under Node andwasmtime, optimized and unoptimized, and requires the standard output and error CPython wrote, byte for bytemathon runtime values:sqrt,sin,cos,tan,asin,acos,atan,atan2,sinh,cosh,tanh,exp,log(with a base),log10,log2,pow,fabs,copysign,fmod,remainder,degrees,radians,hypot(any number of coordinates),floor,ceil,trunc,factorial,gcd(any number of arguments),isnan,isinf, andisfinite, read through the module or imported by name. Each raises what CPython 3.13 raises:ValueError("math domain error"),OverflowError("math range error"), the float-to-int errors offloor, andfactorial's. The transcendental functions are a port of musl's libm (what CPython on WASI links), checked bit for bit against thelibmcrate over every branch, so a result equals CPython's on WASI; CPython on macOS or glibc can round some in the last bit differently,tanandexp(1)among them.hypotandremainderare CPython's own algorithmsrandom, bit for bit as CPython draws it: CPython's MT19937, seeded byinit_by_arrayfrom an int, a bool, or a float (its hash), or from host entropy forseed()and an unseeded generator, andrandom,getrandbits,randrangeandrandint(rejection sampling, CPython's checks and messages),uniform,choice(a list, tuple, or str),shuffle,sample(both of CPython's selection strategies),gauss(with its cached second value),normalvariate, andexpovariate, with their keyword forms. A thousand draws of each, for several seeds, are CPython's.seed()of a str or bytes, which CPython seeds from a SHA-512 digest, is refusedsys.platform("wasi", as CPython on WASI reports),sys.version("3.13.0 (waspy x.y.z)"), andsys.path(an empty list, since imports are linked at compile time)oson the host's values, through WASI:os.environ(a dict of the host's environment, built once) andos.getenv(),os.urandom(), andos.getcwd()andos.getpid(), which WASI has no notion of: they answer"/"and 42, as wasi-libc, and so CPython on WASI, doesos.path:join,basename,dirname,split,splitext,normpath,abspath, andisabs, computed asposixpathcomputes them, andexists,isfile, andisdir, asking the host throughpath_filestat_getwith the path resolved against the preopens asopen()resolves it.from os.path import joinworks**on floats is CPython'sfloat_powover libm'spow: a fractional exponent works, the special values give CPython's answers, zero to a negative power raisesZeroDivisionError, and a result too large raisesOverflowError(34, 'Result too large'). A negative base to a fractional power, which CPython answers with a complex number, traps- A program imports
random_get,environ_get, andpath_filestat_getonly when it usesrandom, the environment, oros.path's queries;docs/src/playground/wasi.jsand the tests' host implement them functools:reduce(with or without an initial value,TypeErroron an empty iterable without one),partial(a function and arguments bound first, called like the function),partialmethodin a class body,cmp_to_key(a key class whose__lt__calls the comparison),@cached_property(computed on the first read, kept on the instance, and settable), and@singledispatchmethodwith@method.register(dispatched on the static type of the first argument afterself)itertools, every function:count,cycle,repeat,accumulate,batched,chainandchain.from_iterable,compress,dropwhile,takewhile,filterfalse,starmap,islice,pairwise,teeandzip_longest(over sequences),groupby(whose groups end when the groupby moves on, the last one included, as CPython's do),product,permutations,combinations, andcombinations_with_replacement, each a generator synthesized for its call, in CPython's order, with CPython's argument checks- Iterators are consumed lazily: a comprehension over a generator pulls one value at a time, so the generator's side effects interleave with the element's as in CPython, and
list(),tuple(),sum(),sorted(),min(),max(),any(),all(), andset()take a generator or an iterator class. A generator method's-> Iterator[T]gives what it yields collections:namedtuple("P", "x y")at module level andclass P(NamedTuple), whose instances are their fields as a tuple (indexing, slicing, unpacking, iteration,in, comparison with a tuple, hashing as a set member or dict key,+) with_replace,_asdict,_make, and_fields, the untyped fields ofnamedtuple()taking their types from the calls of the class; andCounter,defaultdict,OrderedDict,deque, andChainMap, each a class specialized by its element types (Counter[str]), from an annotation, the constructor's arguments, or how the variable is used afterwards (c[w] += 1keys a Counter byw's type). Each holds a builtin dict or list and reads as it wherever CPython reads it as one (iteration,in,len,dict(), its dict methods, and a parameter declareddict), with its own behaviour on top: Counter's missing count of 0,most_common,elements,total,update/subtract, and multiset arithmetic and comparisons; defaultdict's factory (a type, a class, a function, a lambda, a nested defaultdict or Counter); OrderedDict'smove_to_endandpopitem(last=False)and order-sensitive equality; deque's two ends,maxlen,rotate, and indexing; ChainMap's lookups through its maps; and each one's repr and errors, as CPython'sjson:dumpsanddumpwrite CPython's text, withindent,separators,sort_keys,ensure_ascii(its\uXXXXescapes and surrogate pairs), andallow_nan, for ints, floats, bools, None, strings, lists, tuples, dicts (CPython's rendering of int, float, bool, and None keys), and thecollectionsclasses that are dicts;loadsandloadparse into the type the value is declared to have, raising CPython'sJSONDecodeError(shown asjson.decoder.JSONDecodeError, aValueError) with its message, line, column, and character for a document that is not JSON, and aTypeErrorfor one the declared type cannot hold- Special methods on user classes: binary operators call
__add__and the rest (the left operand's, then the right one's reflected method, a subclass's reflection first), unary-,+, and~,incalls__contains__,c[k]andc[k] = vcall__getitem__and__setitem__,a += bcalls__iadd__(or__add__),len()calls__len__(with CPython'sValueErrorfor a negative answer), truth tests call__bool__or__len__,abs(),int(),float(), andround()call theirs, and calling an instance calls__call__. They were refused - Dicts compare by their entries, whatever their order (
==and!=), and an empty literal compares as the other side's type (d == {}). Comparing two dicts was refused reversed()where it is walked: aforloop, a comprehension, andlist(),sorted(),sum(),str.join()and the other builtins that iterate their argument, over a list, tuple, string, range, or dict (its keys)datetime:timedelta,date,time,datetime, andtimezone, ported from CPython's_pydatetimewith the C implementation's messages and reprs. Every field is checked on construction;timedeltanormalizes ints and floats as CPython does; the classes support their arithmetic, comparisons,str/repr,isoformat,ctime,strftime(the C locale's directives plus%f,%z,%:z,%Z), f-string specs,replace,toordinal/fromordinal,weekday/isoweekday/isocalendar/fromisocalendar,fromisoformat(every form CPython 3.13 reads),datetime.strptime(matched as CPython's regular expression matches, backtracking included),combine,date()/time()/timetz(),timestamp/fromtimestamp/utcfromtimestamp,now/today/utcnowfrom the host's clock (WASIclock_time_get),utcoffset/tzname/dst, andastimezone. Aware values compare and subtract by their offsets, and mixing naive and aware raises CPython'sTypeError.isinstance(dt, date)is True. The classes print as<class 'datetime.date'>logging, ported from CPython'slogging/__init__.py: the logger hierarchy, the level methods and the module's functions (which configure the root logger first),basicConfig(),disable(),lastResort,Handler,StreamHandler,FileHandler,NullHandler,Formatterin all three styles (each format compiled where it is written, with CPython's validation errors),Filter, andLogRecord, all subclassable. Arguments are evaluated at the call and the message formatted only when the level is enabled. Records carry their call's line and function name, and the time from the host's clockreat run time: patterns parsed as CPython 3.13 parses them (a bad one is reported when compiling, withre.error's message and position) and matched by a port ofsre's backtracking semantics over a pattern's compiled program: greedy, lazy, and possessive repeats withsre's zero-width rule, atomic groups, lookarounds, back references, conditional groups, the Unicode and ASCII classes, and IGNORECASE with CPython's case folding.compile,search,match,fullmatch,findall,finditer,split,sub/subn(template or function),escape,purge, the flags, andPatternandMatchobjects. Each pattern is compiled once, asre.compilecaches.re.error(re.PatternError) is raised for a bad replacement template, and can be caughtord()andchr(), with CPython's TypeError and ValueError. They were refused as undefined names__name__:"__main__"in the entry file and the module's name in an imported one, sologging.getLogger(__name__)names the logger as CPython does. It was an undefined namedict(x): a copy of a dict (or of the dict acollectionsclass holds), a dict of a list of pairs, or of keyword arguments. It was refusedprint(),str(),repr(),format(), f-strings, and%of an instance whose__str__or__repr__can raise, the exception propagating from the call. They were refuseddict.get(key, [])and other empty-literal defaults, which take the dict's value type
Changed
float ** floatno longer multiplies repeatedly. It trapped on a fractional exponent and rounded an integral one differently from CPython'spow, so1.1 ** 10.0could differ in its last bit- A refusal of a standard-library function names the call and nothing else: refusals said the function was "planned for 0.20.0", a release that will not exist
- Sets are CPython's hash table: the same 64-bit hashes for ints, bools, floats, and tuples, the same table sizes, probe sequence, growth and tombstone rules, and the same order of work in each operation, down to the frozenset CPython's compiler folds a constant display into. So iterating a set, printing one, and
pop()visit the members in CPython's order. A set of strings has no reproducible order in CPython (its string hash is keyed per process); here its order is fixed and unspecified - Collection faults raise instead of trapping:
list.index(v)of a missing value raisesValueError,set.remove(v)of a missing member raisesKeyError, andlist.pop()from an empty list orlist.pop(i)out of range raisesIndexError, each catchable and propagating out of calls like any other exception dict.get(key)with no default is refused unless the dict holds instances. A missing key answersNonein CPython, which an int, float, or str value has no distinct word for here, so it answered 0 andd.get(k) == 0held for an absent key.dict.get(key, default)is unchanged, and a default of a type the values do not share is refusedlist.extend()of a range, a set, a string, or a tuple mixing types is refused. It dropped the argument and extended by nothing; a list, or a tuple whose members share a type, extends as before- A literal whose elements no one type holds is refused:
[1, "a"],{"name": "bob", "age": 30}, and[1, True]. The collection took the first element's type and read the others back as it, sostr([1, "a"][1])answered the string's address andstr(d["age"])answered"".[[], [1]]and lists of tuples of one shape still unify - Walking a tuple whose members have different types (a loop, a comprehension,
sorted,sum,min,max) is refused; the walk read every member as one type just board-checkalso fails when a card's status disagrees with its features: a card marked done with an open feature, a card marked todo with one started, or a card in progress whose features are all done or all todo- The website is rebuilt with Eleventy and Tailwind CSS, in the same warm theme: self-hosted fonts, a documentation section at
/docs/with a sidebar, "On this page", and previous/next links, the playground's editor on CodeMirror 6 (bundled, no CDN), and the development board's data indocs/src/_data/modules.json, whichjust board-checkreads. The changelog page rendersCHANGELOG.mdat build time./programs/and/changelog/moved under/docs/and redirect there.just docsserves the site with live reload andjust docs-buildbuilds it - The README is installation, a quick start, and links. The supported Python subset, numbers, limitations, the library API and options, and running compiled modules (the WASI host interface, file I/O, preserved comments) move to the website's documentation. Its options example builds
CompilerOptionsover the default; the README's old struct literal naming each field no longer compiled
Fixed
- The
if __name__ == "__main__":block runs as written._startused to callmain()and nothing else, so the block's other statements never ran (aprintbefore or aftermain()was silently skipped), and a block with nomainto call trapped at start-up, the generated entry point being amainthat called itself. The block's body is now the entry point: it runs top to bottom as module code, so assigning a module definition there rebinds it for every function, a log record made there names<module>as its function, and an exception it does not catch exits with status 1. An imported module's block does not run. A block that reaches a name defined after it (CPython'sNameError), one that rebinds a module function or class,returnin the block, and anelseon an imported module's guard are refused - An empty
Optional[str]was true:if x:tested the pointer. It is true only when the string is not empty - A function that could raise on an optional string or instance was not checked after its call, so the caller read its result as a string and faulted. Reading an optional string or instance and using it marks a function as one that can raise
- An
Optional[str](a group that may not have matched) works as CPython's value does: its methods (AttributeErroron None),+,len(),int(), andfloat()(TypeErroron None),==with any string, and indexing an optional instance (m[1]afterif m:) x[k]on an instance whose class defines__getitem__by the key's typeopen(path, mode)with the mode in a variable opened the file for reading whatever the mode was. A mode that is not a literal is refused- An
Optional[str]compared or converted as its pointer:x == "ab"answered False forx = "ab", andy: str = xafterif x is None: returnlost the text. Equality compares the text (None is unequal to every string), and the conversion makes the string, trapping on None super().__init__()without an argument that has a default produced an invalid module, and keyword arguments tosuper()calls were refused. They are placed against the method the call reachesinfer_value_typeread a module-level variable inside a function as untyped, so a method called on it (log.info(...)forlog = logging.getLogger(...)) was refused- A method's variants by argument type were only chosen when the program used a
collectionsclass - A refusal found both while scanning a function and while compiling it is reported once
- Typing a long chain of
+took time exponential in its length: each operator asked the special-method rewrite for its operands' types, and then typed them again, so a 30-part string concatenation took minutes to compile. A binary operator only consults the rewrite when an operand is an instance - A constructor's float parameter given a computed int produced an invalid module:
A(-5)fordef __init__(self, x: float)passed the i32. Constructor arguments are converted as a function's are, which also refuses a float passed to an int parameter - A class variable built by a call that leaves out defaulted arguments produced an invalid module, and keyword arguments in one were not placed:
class A: d = B(1)passed one argument whereB.__init__takes three. Class variables' calls get their defaults and keywords as every other call does format(x, spec)andf"{x:spec}"of an instance whose class defines__format__call it; they trapped at run time, or were refused for a spec the builtin types reject- A class variable read through its class (
A.origin.x,A.zero + d) is typed as the variable is, so methods and operators on it work; they were refused as calls on an unknown value - Keyword arguments written out of parameter order were refused when one was a negative number (
f(b=-1, a=2)), which counted as computed - An int argument picks a method's float variant when it has no int one (
td / 3with__truediv__defined for timedelta and float) - A method called without its defaulted arguments produced an invalid module:
obj.m()fordef m(self, n=5)never filled the default in, so the module failed validation. A constant default is given at the call now, and a default that is not a constant (which CPython evaluates once, when the method is defined) is refused - A generator walking a dict or a set by a suspending loop read
d[0],d[1], ...:for k in d: yield kindexed the dict by position. It walks the keys (and a set's members) now - Calling a method a class does not have produced an invalid module, and calling one through the class (
A.nothing()) answered 0. Both are refused as CPython'sAttributeError - An instance passed where a list, dict, string, or number is expected was read as one:
len(d)of an instance passed to adictparameter answered one of its fields. It is refused, except acollectionsclass, which passes as the dict it holds sorted()andmax()/min()withkey=lambdaover a list of instances held in a variable were refused; the lambda's parameter is typed from the elements, and a key reading a field or calling a method of one is typed from itsorted()of instances ordered them by address; it orders them by their__lt__, and an exception the key or__lt__raises propagates- A comprehension over a tuple of floats answered
[0, 0], a comprehension unpacking a list of tuples bound its targets to the tuple's address, and a variable assigned a comprehension took untyped elements; each element and target now has its type - A runtime string counted UTF-8 bytes:
len()of"aßc"built at run time was 4,s[1]was half of theß,find()andindex()answered byte offsets, a slice could cut a character in two,ljust/rjust/centerpadded to a byte width (and trapped on a multi-byte fill), andfor ch in ssplit a character. Each now counts code points, as CPython does; a string's length in code points and the last position looked up are cached per string, so an indexed loop stays linear - A comprehension over a string trapped: it read the text as a list header. It walks the string's characters now, and so do
sorted(s),min(s), andmax(s) - A string method folded on a literal followed Rust's rules where they differ from CPython's:
"hello-world".title()was"Hello-world",capitalize()uppercased its first character where CPython titlecases it,strip()kept CPython's whitespace beyond Rust's (\x1cto\x1f),"123".isupper()was True,"aßc".find("c")was a byte offset, and"abc".index("z")answered 0 where CPython raisesValueError - A runtime case method trapped on any character outside ASCII, and a predicate failed it:
"é".isalpha()was False.strip()andsplit()treated only ASCII whitespace as whitespace incompared a list's slots at the needle's type:"a" in [1]read the int as a string and trapped,1 in [1.0]read the float's low word, and"a" in (1, "a")trapped. A list is now searched at its element type (an int among floats compares as a float, two distinct types are never equal), and a tuple position by positiontuple.index(v)of a missing value raisesValueError, aslist.indexdoes; it trapped- A comprehension over a dict walked its entries as a list's slots, keys and values interleaved:
[k for k in {5: 3, 7: 2}]was[5, 3], a str key read as an int, andsum(d),sorted(d),min(d), andmax(d)did the same. They walk the dict's keys now, as CPython does - A handler's
asname is its own variable, typed as the class the handler names, soexcept NotFound as ereadsNotFound's fields even when another handler in the function bindseto aValueError raise X from YdroppedYwithout a word; it is the exception's__cause__nowexceptnaming a class that is not an exception, or a name that is no class, is a compile error, as it is aTypeErrororNameErrorin CPython; it matched nothing- A function declared
-> Nonethat raised outside atryfailed validation: the exception's return pushed no value where the function answers one word - A
@dataclass's generated__repr__quoted a str field without escaping it (P(name='it's')), and was not generated at all for a float, collection, or instance field. It renders each field withrepr()now, as CPython's does - Iterating a set answered garbage: a
forloop over a set walked its hash table as if it were a list, sofor x in {1, 9, 17}bound arbitrary words, and a comprehension over a set built nothing. Both reported success. A set is now walked member by member, in CPython's order, byfor, comprehensions,sum,sorted,min, andmax - Indexing or slicing a value whose type is not known is refused.
def g(kv): return kv[1]called with 5 answered 0, where CPython raisesTypeError, and slicing one answered the value itself - A collection whose element type differs from the place it is stored is refused. A
List[str]passed to aList[int]parameter, astrappended to aList[int], and astrstored as aDict[str, int]value were accepted, and reading the element back answered the string's offset as an int where CPython raisesTypeError - An int passed to a
floatparameter is widened; the module failed validation, blamed on code generation - Positional-only parameters (
def f(a, /, b)) are refused by name. The parameters before the/were dropped, so every call reported the wrong argument count - Calling a closure that another closure captures (
g = lambda x: x + 1thenh = lambda y: g(y) * 2) compiles and answers CPython's value; the call through the captured variable was refused as a call to an unknown function - A call through a closure value checks for a pending exception, so an exception the closure raises reaches the caller's
try dict.get()keeps the dict and the key while the default is evaluated, sod.get(xs.index(2), -1)finds the key rather than answering the default, and a float or str value is read at its own width (a float value failed validation)- A call to one of the module's functions is checked against its signature: a surplus positional argument is a compile-time
TypeError(f(1, 2, 3)on a two-parameterfranf(1, 2)and reported success), and so is a missing one (it produced an invalid module) - A list method's argument no longer overwrites the list it is called on.
xs.index(ys.index(1))searchedys,xs.pop(ys.pop())poppedystwice,xs.count(ys.index(1))counted inys, andxs.extend([...])with a call inside the literal trapped, all answering something CPython does not xs.extend((a, b))extends by the tuple's members; it extended by nothing- An exception raised inside a comprehension, or in the right operand of
and/or, reaches the enclosingtry. Inside atry, a comprehension whose element, filter, or inner iterable raised (anIndexError, aKeyError, a call that raises) looped forever, and a raising right operand ofand/orfailed validation
0.18.0
Three more end-to-end programs, written as ordinary Python and compiled unchanged, found what follows. Most of it was silent: the compiler reported success and the program answered something CPython does not. The sixth program found no silent defect, which is what the correctness rule is now re-signed on.
Added
examples/expression_evaluator.py, the fourth end-to-end program: a tokenizer, a recursive-descent parser, and an AST class hierarchy withevaluate()andshow()overridden per node type. Asserted against CPython input by input, and part ofjust verify-runtimeunder Node andwasmtimeint()parses a string or bytes value the way CPython does: surrounding whitespace, one sign, digits with single underscores between them. Anything else raises a catchableValueError. A byte at or above 0x80 traps rather than raising, since CPython accepts every Unicode decimal digitfor ch in siterates a string, binding each character as a one-character stringa or b or canda and b and cwith any number of operands. Only two used to be acceptedexamples/grid_algorithms.pyandexamples/order_ledger.py, the fifth and sixth end-to-end programs (a Game of Life step applied to its own output with a BFS maze solver over tuple coordinates, and an order ledger with dataclasses in a dict, an exception hierarchy, and rounded money). Both are asserted against CPython and run injust verify-runtimeunder Node andwasmtime- Collections compare by value.
==and!=on two tuples or two lists compare element by element, and<,<=,>,>=order them as CPython does (the first unequal element decides; a prefix orders first).in,list.index,list.count, set membership and de-duplication, and dict keys all follow: a tuple works as a set member and as a dict key. Every one of these compared the two pointers, so(a, 2) == (1, 2)was False, a set of tuples never found or de-duplicated its members, and a breadth-first search over coordinates never terminated. A list, dict, or set as a set member or dict key is refused, as it is unhashable in CPython; comparing collections whose element types differ, or whose element types are not known, is refused rather than guessed - Strings order:
<,<=,>,>=compare byte-wise, which for UTF-8 is code-point order, CPython's order forstr, andiscompares identity. All of these answered False whatever the strings were - A module-level definition that is not a plain constant is evaluated once, in source order, by a start function the module runs at instantiation, and read through a WASM global. Every read used to inline its initializer, so each read was a new object:
G.append(2); len(G)answered 1,G[0] = 9was lost,C = Counter()was a new instance wherever it was mentioned, andX = f()ranfat every mention. Plain constants (N = 5,NAME = "x", arithmetic over them) are still inlined. A definition that reads one defined after it is refused, as it is aNameErrorat import in CPython. Class-level variables (class C: items = []) are shared the same way - Tuple unpacking types its targets from the value, so a float member binds as a float (it bound as its low 32 bits), and a string or tuple member can be used as one. A value of the wrong length raises
ValueError, as it does in CPython; it was ignored, so the targets read past the end self.items: Dict[str, Item] = {}, an annotated field, declares the field's type ahead of anything inferred from its values. It was refused- Augmented assignment through a subscript (
counts[w] += 1) and through a computed object (self.items[sku].qty += n), each target evaluated once. The subscript form was refused outright round(x)andround(x, ndigits), rounding the exact binary value of the float half-to-even as CPython does, soround(2.675, 2)is 2.67 andround(1.005, 2)is 1.0 rather than the 2.68 and 1.01 the usualfloor(x * 10**n + 0.5)shortcut gives. The product is computed exactly with Dekker's two-product and the error term consulted only at a tie.abs()of an int or a float- A playground page on the docs site (
docs/playground/): Python in an editor on the left, the generated WebAssembly on the right as text (WAT) or an annotated hex dump, with a Run tab that calls the exported functions in the browser. The compiler is waspy itself built to wasm32, so everything runs locally. Examples, an optional Binaryen pass (binaryen.js), shareable links, and located compile errors marked in the editor - A Talks section on the docs home page: a card per talk on Waspy with its event, date, speakers, and links to the recording, slides, and event page, starting with talks at IICT 2026 (IISc Bengaluru), PyCon India 2025, and EuroPython 2025
just board-check: every feature on the development board names the test intests/integration/board.rsthat backs it (one per feature, 220 in all), and the check fails when a done feature has no passing test, an open feature's test passes, or a test backs no feature. It runs injust ciand as its own CI step, and it is the command the 1.0 gate runs- Sequence repetition and concatenation:
"ab" * 3and3 * "ab"(answered""),[0] * nand(a, b) * 3(trapped), andxs + ysfor two lists or two tuples (trapped). Repetition copies the elements, so nested lists are shared as in CPython; a tuple's count must be a constant, since its length is part of its type - The set operators and methods:
|,&,^,-(the first three answered wrong sets and-trapped),union/intersection/difference/symmetric_differencewith a set argument,issubset/issuperset/isdisjoint, and==/!=/</<=/>/>=between sets - Dict merge,
d1 | d2, in CPython's order (it answered a dict of the left operand's size) - Keyword arguments in calls to this module's functions and constructors, placed by name with skipped defaults filled in. They were dropped on every plain call:
f(1, b=5)ranf(1),C(1, y=7)kepty's default, andenumerate(xs, start=10),sum(xs, start=10), andround(x, ndigits=1)ignored the keyword.enumerate'sstart=works; any other keyword a builtin does not take, or one passed to a closure or another file's function, is refused else:onfor,while, andtry. The loop forms were refused, and thetryform was droppedmin()/max()of floats (failed validation) and of strings (answered 1), keeping the first argument unless a later one is strictly smaller or larger, which is what decides ties and NaN in CPythonisinstance()against a builtin type (int,float,str,bool,list, ...) or a tuple of types, answered from the value's static type. It answered False for every target that was not a user class,isinstance(3, int)includedlen()of a range (it read the range's start as a count), and stepped slices of a str or bytes (s[::-1]answeredsunchanged)- A fill character for
ljust/rjust/center, on a literal receiver (where it was ignored) and a runtime one (where it was refused) re.subandre.escapeover constants fold to CPython's string: Python's replacement syntax (\1,\g<name>) andre.escape's character set, where the fold used theregexcrate's ($1, and a set that leaves spaces bare), and the result is interned (it was read from offset 0)from math import pi, and any from-import of a standard-library name, reads through the module (it read an undefined name, -999)is Noneandis not Noneon anOptionalinstance orstr, andisbetween instances- An
optimizecargo feature, on by default, gating the Binaryen dependency. Withdefault-features = falsewaspy builds forwasm32-unknown-unknown, andCompilerOptions::optimizehas no effect
Fixed
list.remove(v)of a missing value raisesValueError. It did nothing, so a handler never ran and the list kept its lengthx is Noneanda is banswer. Identity on anything but strings and collections dropped both operands and answered False, sowhile node is not Nonenever ran andif x is Nonenever held- A true division is a float wherever it is stored (#132): a field set from one truncated (
Ratio(3, 2).vwas 1.0), and a local or a generator's lifted local failed validation. Nothing truncates a float silently any more: a float stored, returned, or passed where an int is held is refused tuple.indexanswers the position. It pushed the position it found and branched past it, answering -1 for every value the tuple held; each position is now compared at its own width, and a missing value trapsbool(x)is the truth value (it passedxthrough, sobool(2) + 1was 3), andnot 0.0is True (the float test was inverted twice)- A generator's
a, b = b, a + bsurvives ayield. Tuple-unpacking targets stayed locals of the step function and restarted from zero on every resume, so a Fibonacci generator yielded zeros - A bare
raise(orraise e) re-raises the caught exception. It raised a generic code no typed handler matched, andraise elookedeup as an exception class center()puts an odd margin's extra space where CPython does (on the left for an odd width), on literal and runtime receivers- A dict's key and value loops bind at the dict's types. Every key and value was read as an int word, so a string key had no length and a float value was half a float
- Calling a
-> Nonefunction or method inside a loop validates. Its result word was left on the stack setup.pyvalues are read without their quotes:name="pkg",parsed as"pkg"super().m()with no base method,namedtuple(), the statement form of__import__, a captured name that is not a local, an unknown attribute of a user module, and a name nothing binds each answered a placeholder (0, a null pointer, or -999). Each is a compile error now- Instances of sibling classes dispatch on their own class. A list literal took its element class from its first element, and a local, field, list element, or dict value kept the class of the first instance stored in it, so a method called through any of them ran that class's method on the others:
[Circle(1.0), Rect(2.0, 3.0)]summed to 15.0 where CPython says 9.0. Each now takes the nearest common base of what is stored in it. A program that contradicts its own annotations (aRectpassed to aCircleparameter) is refused, as is a method call on instances with no common base (#128) - A comprehension over a generator or iterator drives it. It read the generator object as a list, so
sum([x for x in evens(10)])answered 0 andlen(...)1. An iterator in a comprehension's firstforis drained into a list first, for list, set, and dict comprehensions, generator expressions, a generator held in a variable, and a class implementing__iter__/__next__. One in an innerforclause is refused (#129) - A generator yielding a true division yields floats.
yield i / 2was inferred as an int generator and truncated every value - Every evaluation of a collection literal is a new object. A literal was built into one compile-time region per source site, and only one evaluated inside a loop was copied out. A function body runs once per call, so a function called twice returned the same list, dict, set, or tuple both times, and the second call reset the first call's result:
a = make(1); b = make(2); b.append(3)leftlen(a)at 2 anda[0]at 2. A literal whose element called back into its own function had its earlier elements overwritten by the inner call, and a recursive function looping overrange()shared one range object between activations (walk(4)answered 1 where CPython answers 15). Each literal and range is now built straight into its own__allocblock - A dict literal evaluates each key and value once. The first pair was evaluated a second time to learn its types, so
{f(): g()}called both functions twice //and%on integers floor, as Python's do. They truncated toward zero, which agrees with Python only when the operands share a sign:-7 // 2answered -3 (CPython: -4) and-7 % 3answered -1 (CPython: 2). Float//and%were already rightx op= vgives the same answer asx = x op v. Augmented assignment carried its own copy of the arithmetic, wrong in several ways: string+=added a length to an offset and leftlen()at 0,/=divided as integers (3.0 for 7 / 2), float%=subtracted the wrong way round, and%=and//=truncated. It now lowers to the plain form. A list+=extends in place, so another name for the same list sees the changeobj.attr op= vlikewise, and it no longer holds the object pointer in a scratch local that the value's own code could overwrite, or silently does nothing for a field the class does not have- A single character of a string (
s[i]) is a real string wherever it goes. It was a pointer into the middle of the source string, whose length every consumer that received it as one word recovered from the four bytes before it, which were the preceding characters. Passed to a function, stored in a field or a list, or returned, it answered garbage:len()of it was 1627389952 in one case, andlen(first("hello"))was 5 int("12")is 12. A string argument was treated as an int: the call answered the string's length and left its offset on the stack, which shifted every later argument of an enclosing call by one- An item assignment whose key or value does any work stores where it should. The container pointer and key were held in scratch locals that the key and value expressions also use, so
xs[1] = ys[2]andd[s[i]] = 1wrote through whatever the nested expression left behind - A string field set from a literal keeps its type.
self.s = ""was typed as nothing in particular, so every later read of the field lost its length:c.s = "abc"; len(c.s)answered 6513249, which is "abc" read as an integer - A tuple subscript is an index.
d[(1, 2)]andd[1, 2]were taken for a slice, on the belief that the parser spellsa:bas a tuple, so they sliced the dict and answered its pointer, andxs[(1, 2)]on a list returnedxs[1:2] - An
intest survives a container that does work. The searched value was held in a scratch local while the container was emitted, so"b" in d.keys()had it overwritten by the method call - A set hashes a string by its contents. It hashed by offset, so a string built at runtime missed an equal member already in the set
for row in boardover a list of lists binds each row as a list. The loop variable was typed only for string elements, sosum(row)answered the row's pointer
Changed
- The standard library's functions are refused rather than faked, naming the release that implements them (0.20.0). They compiled to placeholders:
json.dumpsanswered"{}"andjson.loadsa null pointer,os.getcwd()"/",os.getenvNone,os.path.joinits first argument,datetime.now()the moment the module was compiled, a runtimere.matcha match whatever the input,sys.argvan empty list, and everyloggingcall nothing. The constants still compile;os.nameis"posix", as CPython reports on WebAssembly hosts, where it was"wasm".examples/test_json.pyandexamples/test_system_calls.py, which exercised only placeholders, are removed, and the other stdlib examples keep to constants - A
re.subover constants whose answer could differ from CPython's is refused: a pattern that can match the empty string (the engines resume differently after one), backreferences, lookaround, inline flags, set operations in a class,$over text with a newline, VERBOSE, or non-ASCII text - Untyped places hold only words. An unannotated parameter or return, and the elements of a bare
list/set/dictparameter or return, take ints, bools, None, and functions; a string, float, collection, or instance through one is refused (#116). A string passed todef f(s): return len(s)answered its first four bytes, and runtime strings returned through-> liststopped matching as dict keys.len()of an untyped value or of a number is refused too except E as ebinds no object yet, soecan only be re-raised;str(e)andisinstance(e, E)answered wrong and are refused until 0.19.0. Anexceptnaming its types other than by plain names (except errors:) is refused; it caught everything- An operator applied to operands it is not defined for is refused, as CPython raises TypeError:
[1] | [2]and{1: 2} & {1: 2}answered 1, two instances added compiled, and unary-,+,~on a string or collection did arithmetic on a pointer - Runtime
%formatting is refused with a hint (it failed validation and reported a code generation bug); 0.19.0 implements it. Constant%formatting renders the%s %r %d %i %x %X %o %fconversions with flags, width, and precision as CPython does (it left%5d,%.2f, and%ras written, printed%xin decimal, andTrueastrue), and refuses the rest len()of a generator is refused, as CPython raises TypeError, and a generator afterzip()'s first argument is refused rather than read by length and index (#133). A loop target a generator reads after ayieldis refused- Decorators CPython does not define are refused:
@memoize,@debug,@timer,@default_value,@type_check,@pure, and@wasm_exportcompiled as no-ops (or placeholder IR), where CPython raises NameError.DecoratorRegistrykeeps only decorators a library user registers - Module-level and class-body statements that would not run are refused: a chained or tuple assignment, and any class-body statement other than a method, docstring, or assignment, were skipped
x is Noneon anOptional[int]or an untyped value is refused, since None and 0 are the same word;isbetween two numbers is refused, as CPython leaves it undefined"{} {0}".format(n)is refused, as CPython raises ValueError for mixed field numbering- A conditional expression (
a if cond else b), or any other expression the compiler does not lower, is refused by name rather than with a dump of its syntax tree - The development board's headline percentage counts features (an in-progress one as half), as each card's progress bar does. It counted whole cards, so a card with one open feature added nothing and the board read 38% where its features are 80% done
sum()of a value it cannot walk as a list or tuple is a compile error. It used to answer the argument itself, sosum()of a set, or of a list whose element type had been lost, returned a pointer as the totalfloat()of a string is a compile error. It converted the string's length. Producing the double CPython would needs correctly rounded parsing, which a digit loop does not give, so it is refused rather than approximatedint()of a collection, instance, orNoneis a compile error, as it is aTypeErrorin CPython. It passed the pointer through as a numberx /= yon an int local or field is a compile error. Python makesxa float, and a local's width is fixed for the whole function, so the result would be truncated back into an integer slot- Assigning a field the class never declares is a compile error. The write was dropped with nothing said; reading such a field was already refused
@, and the bitwise operators on a float, are compile errors, as they areTypeErrors in CPython. They pushed a 0 over their operands- A dynamic import is a compile error. It evaluated the module name and answered 0 as if a module had been loaded
- The docs site is built and deployed by the Documentation workflow (GitHub Pages from Actions) instead of being served from
docs/onmain, so the playground's compiler is built fresh on every deploy rather than committed. Every page's nav gains the playground, shows GitHub as an icon, and keeps its side padding below 1200px - The docs no longer claim standard-library support the compiler does not have. The README, the development board, and the docs home and programs pages listed
sys,os,math,random,json,re,datetime,logging,collections, anditertoolsas implemented. Checking each claim against CPython showed that only the module constants,re.sub/re.escapeon constant arguments, and thefunctoolsdecorators work: most other calls are refused, and some compile and answer wrong. The board now marks each module's functions as todo, its other overclaims (print(), tuple.index,namedtuple, dynamic imports, set operations, the non-Python@memoize/@debug/@timerdecorators) are corrected, and the items it understated (catchable runtime faults, pointer identity across growth, closures capturing by reference) are marked done. The README gains a "Known wrong answers" section listing every construct found that still compiles and answers differently from CPython - The README and the programs page say that the parameterised annotation (
List[str],Dict[str, int]) is what gives a collection its element type, and what a barelistordictcosts: string keys built at run time and returned through-> listdo not deduplicate in a dict, andlen()of a string element of anxs: listparameter answers garbage. Both are listed under the README's known wrong answers until #116 is fixed (#119)
0.17.0
Added
- A method call reaches the override the receiver's runtime class defines. Dispatch was static everywhere: a call compiled to the implementation the receiver's declared class holds, so
self.kind()inside a base method reached the base's version even when the instance was a subclass that overrode it, and the plain template-method pattern answered"media:b"where CPython answers"video:b"while reporting success. A method name some class overrides now gets a column in a per-class dispatch table, indexed by the class tag every instance already carries at offset 0, and the call becomes acall_indirect: one load and three arithmetic ops.@propertygetters and__eq__behind==had the same defect through the same route and go through the same table.super().method()stays non-virtual, as in Python, so an override can still extend its base - Only a name some subclass actually overrides is dispatched this way. A method nothing overrides, a class with no subclasses, and a program with no classes at all keep the direct
callthey have always had and emit no table, so nothing that was already correct changes shape examples/oop_polymorphism.py, a notification hierarchy with arender()written once in the base, a list holding three runtime classes at one call site, and asuper()chain. It is part ofjust verify-runtime, so the dispatch table is exercised under Node and thewasmtimeCLI at both optimization levels rather than only the in-process harness@functools.singledispatchdispatches (#120). The decorator was recognised by name and never applied, so@kind.registerarms were compiled as unrelated functions and every call reached the base implementation:kind("hi")answeredkind(5)'s value and reported success. Each registered arm is now an ordinary function under a synthesized name, recorded against the type its first parameter is annotated with (or theregister(type)argument), and a call picks the arm by the first argument's static type: the exact type, thenintfor abool, then the nearest base class with an arm, then the base function. A call whose first argument has no known type is refused rather than sent to the base@functools.total_orderingderives the ordering methods a class leaves out from the one it defines, spelled the way CPython's implementation spells them (__gt__from__lt__isnot lt and not eq, and so on), with__eq__falling back to identity. It used to be ignored, soa > bon the instances compared their heap pointers and answered allocation order. A class that defines none of the four is refused- Ordering between class instances dispatches to the rich comparison method:
a < bcallsa.__lt__(b), orb.__gt__(a)when the left class has no__lt__, as CPython does. A user-written__lt__was ignored entirely before, and a class with no ordering method at all compared pointers where CPython raisesTypeError; both now either call the method or are refused with CPython's message. An unannotatedotherin a rich comparison method is typed as the class, sodef __lt__(self, other): return self.v < other.vreads the field it names list(),dict(),set(), andtuple()with no argument are the empty literals. They used to fall through to the unknown-call path and answer a null pointerexamples/functools_decorators.py, demonstrating both decorators with asserted tests
Security
- The README documents the
waspy_hostfile interface in full (the four imports, their signatures, the mode flag bits) and states plainly that the embedder owns confinement: the compiled program picks the path string it passes towaspy_host.open, so a host that forwards it to a real filesystem hands the program whatever the process can reach. Nothing in the module constrains that, and the compiler cannot, since the path is a runtime value. The reference hosts in this repo are in-memory test fixtures, not a sandbox to copy - CI installs
wasmtimefrom a pinned release asset with a checked SHA-256, instead of pipingwasmtime.dev/install.shtobash. The script was fetched fresh on every run, so what the required runtime gate executed was whatever that URL served at the time, and the version moved on its own
Fixed
- A value written into a collection is stored at the width the collection is read back at (#123). Every write path stored whatever the value expression produced, so an int written into a
List[float]put four bytes in an eight-byte slot:xs.append(3)on[1.0, 2.0]read back as1.5e-323, andxs[0] = 3, which overwrote the slot's low half, as1.0000000000000007.append,insert, item assignment, a dict value, andset.addall widen now. The type hint alone was not enough, since it is consumed by constants and arithmetic and an int arriving through a variable came out an int whatever the destination asked for, so the conversion happens at the write itself - An unannotated local takes
intorboolfrom the value assigned to it, as it already took every pointer-shaped type. Both live in the same i32 slot, so the local layout is unchanged; what it fixes is a value passing through a local on its way into a float collection, which reached the write as untyped and one word wide
Changed
- An override that changes the signature is refused. Every implementation of an overridden name is reached through one
call_indirect, which names a single WebAssembly type, so the implementations have to agree on their parameters and return type. Python does not require that; here it is reported at compile time, naming both methods, rather than left to trap on the call - A write the compiler cannot make good on is refused instead of stored at the wrong width (#123). A float into a collection whose elements are read as integers would have to truncate (CPython keeps the 2.5; an int slot cannot), a float into a collection with no element type cannot be read back at all, a value of unknown type has no width to widen from, and
list.extend()between lists read at different widths copies raw slots across. Each names the problem and what to do about it. The mixed-width literal check has always taken this stand; the runtime write paths now take it too - A decorator the compiler does not implement is a compile error, on functions, methods, and classes alike. Any decorator that was not a name was silently dropped, and any name the compiler did not know was silently ignored, so a user-written decorator,
@cached_property(whose attribute read answered 0),@partial-style wrappers, and every other unimplemented decorator compiled to the bare definition and reported success. The allowlist is the method kinds,@abstractmethod,@dataclass,@total_ordering,@singledispatch/@f.register, and the caching decorators (@lru_cache,@cache,@wraps), which change nothing a compiled module can observe - A call to a name that is neither a function the program defines nor a builtin the compiler implements is a compile error. It used to push a 0 and report success, so
reduce(add, xs),partial(add, 10),abs(-3),round(x), a function imported withfrom math import sqrt, and a misspelled function name all answered 0. The error names the callee - Reading an attribute through a value whose type is not known is a compile error, with a hint to annotate the parameter or variable it is read from; a field the class does not have is refused too. Both answered 0 before, which is what left
other.vinside an untyped__lt__(self, other)comparing against nothing - The stdlib smoke examples (
stdlib_test.py,test_json.py,test_logging.py) no longer take functions and classes as values (f = math.sqrt,json.JSONEncoder); that is not a supported expression, and it now says so
0.16.0
Added
- String methods work on a string built at runtime, not only on a literal.
upper,lower,strip,lstrip,rstrip,capitalize,title,find,index,count,startswith,endswith,replace,split,join,ljust,rjust,center, and theisdigit/isalpha/isalnum/isspace/isupper/islowerpredicates are all emitted as real byte loops now. Every one of them was a stub that returned the receiver unchanged, or 0, orfalse, while reporting a successful compilation:"ALPHA".lower()on a variable answered"ALPHA",",".join(parts)answered",",text.find(sub)always answered -1, andtext.split(sep)returned a region whose length word was garbage. A constant receiver was folded correctly during lowering, which is exactly why no feature test caught any of it. A byte at or above 0x80 traps in the case transforms rather than passing through unchanged, since CPython case-folds the whole of Unicode and answering"CAFÉ".lower() == "cafÉ"would be the same class of silent wrong answer str.format()interpolates runtime values. A literal template now lowers to the same+chain an f-string does, reusing that path, so"{} items".format(n)rendersninstead of answering the empty string. Automatic ({}) and positional ({0}) fields are supported; a named field, a format specifier, and a conversion are rejected with a hint rather than ignored. Constant arguments fold through the shared renderer, so"{}".format(True)isTrueand not Rust'strue- Dicts have methods:
get(key[, default]),keys(),values(), anditems(). There was no dict arm in method dispatch at all, so every one of them was reported as unsupported.counts[w] = counts.get(w, 0) + 1, the idiomatic accumulator, works, and.items()is a value rather than only something aforloop can desugar insearches the containers it could not before.key in some_dict,x in some_tuple, andsub in texteach answered a constantFalsefor anything the container actually held, because only lists and sets were considered searchable. Dict keys are scanned at their own two-slot stride, tuples share the list scan, and a substring test runs the same searchfinddoes. A container that still cannot be searched is a compile error instead of a constantlist.sort()andlist.reverse()reorder the list. Neither name appeared anywhere in the compiler, so both fell through to a default arm that dropped the receiver and pushed 0, which meant[3, 1, 2].sort()left the list untouched and reported success.sortis an in-place insertion sort over int, float, and bool elements and takesreverse=True;reverseswaps whole slots, so it is correct for any element width- List slicing returns a real list.
xs[:n]computed a length, dropped it, and pushed a null pointer, and its clamp pushed a value from both arms of aniftyped as returning nothing, so the module did not validate. Indices follow Python: a negative one counts from the end, both ends clamp into range, and the result is a fresh region, so growing it never touches the list it came from. A step other than 1 is rejected rather than silently ignored examples/text_report.pyandexamples/library_project/, the second and third of the 0.16.0 end-to-end programs, joining the shopping cart. The text report is a word-frequency tool: it tokenizes a block of prose by stripping punctuation and case-folding each word, counts them into a dict keyed by those runtime strings, ranks by count with alphabetical tie-breaking, and formats a report with a fixed-point mean. It exercises string methods, dict accumulation, sorting, comprehensions, and f-strings all at once, which is why it found a dozen defects nothing else had. The library project is a book catalogue split across four modules with the domain class shared between them, compiled from its entry file with imports resolved from disk. Both are asserted against what CPython prints for the same source, and both were checked underwasmi, Node, andwasmtimeexamples/shopping_cart.py, the first of the 0.16.0 end-to-end programs. It is a whole program rather than a feature demo: aCartholding a list ofIteminstances, anaddmethod that constructs an item and appends it to a field, atotalmethod that calls each element's own method, float money arithmetic, and a plain rules function over the result. Nothing in it was chosen to show off a compiler feature, which is the point: the first time this program was compiled it hit three defects in a row (a Binaryen process abort, a silently wrong0.0, and a trap) that no single-feature test had caught, because they lived in the composition of features that each worked alone. Four asserted tests intests/integration/coverage.rscheck its results against what CPython answers for the same source, including both discount-tier boundaries and the empty-cart case, and the compiled module was checked to give identical answers underwasmi, Node, andwasmtimejust verify-runtime, which runs the end-to-end programs under Node andwasmtimerather than only under the test harness, andjust verify-examplesnow depends on it. The integration suite asserts these programs' results withwasmiinside the test process, which leaves the engines a user actually ships against unchecked. Each program is now compiled twice, unoptimized and Binaryen-optimized, with a checker suffix fromtests/fixtures/runtime/appended: the suffix addsruntime_check_*functions that compare the program's own results against what CPython answers and return 1 or 0, so a runner only has to call every such export. Comparing inside the module is what lets thewasmtimeCLI, which can invoke an export but cannot read the module's memory, check the same returned strings Node does. Aruntime_check_negative_*function must answer 0, so a comparison that regressed to a constantTrueis caught rather than papered over. Both runners are a required gate in CI, andverify-examplesalso compilesexamples/library_project/, which it had been skipping- A "what a program can look like" page in the docs, at
docs/programs/. One complete working program, taken unedited fromexamples/, and a plain list of the shapes that do not compile yet, each tagged with what happens when you try: refused at compile time, answers differently from CPython, or has a supported way to write the same thing. The README's limitations stay the exhaustive statement; this is the page to read in a minute to tell whether your code is in scope tests/unit/miscompiles.rs, 36 regression tests for the silent miscompiles the whole-program work turned up, over method dispatch, constant folding, and error surfacing. Every expected value is what CPython answers for the same source, so a failure is a divergence from the reference implementation rather than a change in what the compiler happens to do.tests/unit/memory_safety.rsholds the earlier round of the same exercisetests/integration/wasmrun_plugin.rs, which takes the end-to-end programs through the wasmrun plugin's ownWasmBuilderrather than the library API, asserts the module lands whereBuildResultsays it does, and then runs it and checks the same answers the library path is checked against.examples/plugin_test.rscovered the plugin before this, but it printed its findings: a failed build printed a cross and the process still exited 0, and the module was never instantiated. It fails properly now, and both plugin optimization levels are checkedsorted(iterable[, key=...][, reverse=...])sorts. It fell through to the generic builtin path and returned a value with nothing to do with sorting, while reporting success:sorted([3, 1, 2])[0]answered 2. It is a stable insertion sort over a copy now (the original is untouched, as in Python), ordering ints, floats, strings, and tuples, the last compared lexicographically member by member.key=is applied once per element rather than on every comparison. A key whose result type cannot be determined, or a sequence whose element type is unknown, is a compile error rather than a comparison of raw slot words, which for a list of tuples would compare pointers and leave the list in allocation order- Fixed-point formatting in f-strings:
f"{value:.2f}". The specifier used to be rejected outright, so a program could not print money. The integer and fractional parts are handled separately (no 64-bit arithmetic needed), the fraction is rounded to nearest with ties to even the way IEEE-754 and CPython both do, and a fraction that rounds up carries into the integer part. Every other specifier (widths, alignment, separators) is still refused rather than silently ignored
Changed
list.index(value)traps when the value is absent, instead of answering -1. CPython raisesValueError; nothing in a compiled module can carry one, so it traps, the wayset.removeof a missing member already does. Answering -1 isstr.find's contract, notlist.index's, and a caller who then indexes with it reads the wrong element. Nothing can be relying on the old behaviour, becauseindexnever once returned a correct position (see### Fixed)- Indexing a lambda's parameter, measuring it, or calling a method on it is a compile error (#115). A lambda's parameters carry no annotation, so a lifted lambda's parameter is untyped and code generation reads it as a bare word:
(lambda kv: kv[1])((1, 5))answered 0 where CPython answers 5, and(lambda w: len(w))("abcd")answered 1684234849, which is the four bytes read as an integer. Both compiled and reported success, which the roadmap had recorded as a loud refusal it turned out not to be. Typing a lambda's parameters from its call sites is a typing pass of its own, so until then this is refused rather than miscompiled. Arithmetic and comparison on a parameter are untouched, sosorted(xs, key=lambda v: 0 - v)still works; anything more wants a nameddef, whose parameters can be annotated - A list method called with the wrong number of arguments is a compile error. Every arm read the arguments it needed and ignored the rest, so
xs.append(3, 4)compiled and appended only the 3, andxs.clear(9)dropped its argument on the floor. CPython raisesTypeErrorfor both. The optional forms Python accepts still compile:pop(),pop(i),sort(), andsort(reverse=True)
Fixed
- A parse or lowering failure no longer disappears into a warning. Both the file and the multi-file entry points logged the failure and skipped the file, which is how every user compiles. On a single file the located message with its hint was replaced by "No valid functions found in any of the provided files"; in a project the build succeeded with that module missing, so one unsupported construct anywhere in an imported module took every other function in it down and a call across the import answered 0 with nothing said. A typo in any imported module reached this. Both are errors now, naming the file and carrying the real cause
list.pop(i)removes the element ati. The index was evaluated, stored, and used only for the load: the length was decremented and the tail left where it was, so[9, 3, 2].pop(0)answered 9 (right) and left[9, 3]behind where CPython leaves[3, 2]. Every element above the popped one moves down a slot now, whole slots at a time so a float or a string relocates intact, and a position the list does not have traps instead of reading a neighbouring slot. This is the mirror of theinsertdefect below: the argument was read and discardedlist.index(value)answers the position it found. The search loop pushed the matching position and then branched out of the enclosing block, and abrto a label whose result type is empty discards whatever sits above it, so the position was thrown away and execution fell through to the-1after the loop.[2, 3].index(2)answered -1 for a value the list held, and reported success.countshares the same scan and was always right, which is why nothing caught it"a b a".split(" ")returns a list. A constant separator on a literal receiver folded into a string spelling the result the way Python'sreprdoes, so the answer was the 15-byte['a', 'b', 'a']:len()answered 15 instead of 3, iterating it walked characters, and using one as a dict key trapped. There is no list constant to fold into, so the fold is gone and the runtime implementation is the only path, which is the ones.split(sep)on a variable always took and always got right- f-string
.0frounds ties to even. Ties-to-even needs the parity of the digit being rounded into, and at precision zero that digit is the integer part's last one. Splitting the value and rounding the fraction alone lost it, and rounding 0.5 to nearest-even is 0 whatever sits to its left, sof"{3.5:.0f}"answered3where CPython answers4.2.5and4.5were right only because their integer part was already even. Every other precision was, and stays, correct - An unknown string method names the method instead of failing validation. It dropped the receiver's
(offset, length)and pushed nothing, so the module failed WebAssembly validation and the error blamed code generation rather than the Python source. The list and dict receivers already reported theirs list.insert(i, value)honours its position. The index was evaluated and discarded, so every insertion appended the value. It is now normalized and clamped like Python's, and the existing tail shifts right before the new value is stored./on two integers is true division. Python 3's/yields a float, so7 / 2is 3.5; this emitted an integer divide and answered 3, silently truncating wherever the result was used and failing WebAssembly validation wherever a float was expected.//is unchanged and remains the floor division that keeps an integer. Areturnnow also converts its value to the function's declared return type, which is what lets-> intaccept a true-division result- Iterating a dict read whatever the stride landed on.
for k in dwas only implemented for lists and strings, so a dict fell through to a branch that walked it at the wrong width: a three-key dict counted 131072 iterations, and looking a key up inside the loop trapped. Keys are walked at the entry stride now, and the loop variable takes the dict's key type - Indexing a tuple always reported the first member's type.
pairs[0][1]on a(int, str)came back typedint, so concatenating two of them compiled as integer addition and the sum was then read back as a string offset. The index decides the type now; a computed index into a tuple whose members differ is a compile error rather than a guess - A comprehension over a call bound untyped targets.
[f"{w}" for c, w in top_words(...)]did not consult the callee's declared return type, so a string member rendered as its pointer - Dict keys compare by content, not by where they happen to live. A string slot holds only the blob's offset, so comparing slots as words compared identity: a key built at runtime never matched an equal key already in the dict, and
counts[word]with a word fromsplit()could not find its own entry. Strings now compare byte for byte wherever a slot is matched, which fixesinover lists and sets at the same time - A string value is an
(offset, length)pair everywhere. A string parameter arrived as the offset alone, and afortarget or comprehension target binding a string element had no companion length local at all, so reading one pushed a single word where the rest of code generation expects two.len(w)on a loop variable answered whatever the companion happened to hold, a placeholder rendered the pointer as a number,sum(len(w) for w in words)answered garbage, and using such a value as a dict key dropped the offset and kept whatever was underneath it. Loop and comprehension targets also take the element's type now, so a string method can be called on one - Truthiness follows Python. A
stris an(offset, length)pair, so testing one directly left the offset on the stack and the module failed to validate inside a loop; a collection is a region pointer, which is never null, soif xs:on an empty list, dict, set, or tuple answeredTrue. One helper now reduces a value to Python's truth value forif,while, andnot - Keyword arguments are no longer dropped on the floor. Only a call's positional arguments were read, so
xs.sort(reverse=True)reached code generation as a baresort()and would have sorted the wrong way round without saying so.sort'sreverseis lowered to a positional argument and every other keyword is rejected with a hint - A method the compiler does not implement is a compile error rather than a no-op. An unknown list method was silently discarded, which is what let
sortandreversedo nothing; an unknown string method emitted a module that failed to validate. Both now report the method, the receiver, and the enclosing function. The list hint namesreverseandsortnow that both are implemented. - Two modules defining the same function name is an error. Merged modules share one flat namespace, so the second definition was dropped with a warning while compilation reported success, and every call to either one reached the first:
alpha.rate()andbeta.rate()both answered alpha's value. Resolving this properly means qualifying names by module and rewriting call sites per file's imports, so for now it is refused instead of miscompiled
Performance baseline
First recorded baseline, so later releases have something to compare against.
Median of nine release-build compilations each on an Apple M1 (Darwin arm64),
via just benchmark. These are wall-clock numbers from one machine: the point
is to catch an order-of-magnitude regression, not to defend a percentage.
| Program | Source | Compile | Module | Compile (opt) | Module (opt) |
|---|---|---|---|---|---|
shopping_cart |
2.0 KiB | 0.2 ms | 1.4 KiB | 1.2 ms | 1014 B |
text_report |
3.4 KiB | 0.4 ms | 6.5 KiB | 5.8 ms | 5.7 KiB |
library_project |
3.3 KiB | 0.5 ms | 3.6 KiB | 2.7 ms | 3.1 KiB |
nested_collections |
4.7 KiB | 0.5 ms | 13.6 KiB | 4.1 ms | 12.7 KiB |
The first three are the end-to-end programs; nested_collections is the
feature example that produces the largest module, as a second data point.
Binaryen dominates the optimized column, costing 5 to 15 times the rest of the
pipeline while taking 7 to 27% off the module, so the unoptimized compile is
the number to watch for a codegen regression.
0.15.0
Added
- Generated WebAssembly is validated before anything else touches it. Code generation could emit a module that does not validate (an unbalanced stack, a local index past the function's local vector, a type mismatch); handing one to Binaryen aborted the whole process with
UNREACHABLE executed at bits.h:436and no file, line, or function to go on, and skipping optimization just handed the caller a binary every runtime rejects. Every compilation path now runs the module through a validator first (waspy::compiler::validate_wasm) and reports a normalChakraErrorcarrying the validator's message, the byte offset, and the name of the exported function whose body contains that offset, stating plainly that it is a compiler bug rather than a problem with the Python source. Validation runs whether or not optimization is enabled, so a successful compilation always means a module a runtime will accept - Module-level statements whose effects the compiler silently dropped are now rejected up front (#3). A WebAssembly module has no top-level run step, and Waspy compiles module-level variables by inlining their initializers into the functions that read them, so only definitions can run:
X = 1,X: int = 1,X = helper(),X = ClassName(),def,class, and imports. Everything else written at module level compiled "successfully" and then did nothing at all, so the program read the value from before the statement and returned a silently wrong answer: a module-levelforloop accumulating into a global returned the initial value, awhileloop and a non-guardifthe same,ITEMS.append(3)left the list at its literal length,TOTAL += 2leftTOTALunchanged,D[1] = 9andO.n = 9were dropped, andA, B = 1, 2read back garbage. Each of those shapes now fails at the front door with the statement named, its line and column, and a hint pointing at the two things that do work (move it into a function, or drive it fromif __name__ == "__main__":, which is recognized as the entry point). The entry-point guard and thetry/except ImportErrorguarded-import idiom stay allowed, since neither carries a runtime effect to drop - An error channel in code generation.
compile_functionandcompile_bodyreturned()and expression codegen runs behind a shared&CompilationContext, so a construct codegen could not generate code for had no way to say so and could only emit a runtime trap. The context now carries an error sink (CompilationContext::report), the whole module is still walked so one compile run surfaces every such call, andcompile_ir_modulethen fails with the collected errors instead of returning a module that dies at runtime. The first construct moved across is a method call on a receiver kind with no method support (set mutation,s.add(...)above all): it is now a compile error naming the method, the receiver's type, and the function it appears in, where before it compiled "successfully" and trapped.compiler::compile_ir_modulereturnsResult<Vec<u8>, ChakraError>accordingly (an implementation-detail module; the crate-root API is unchanged) - Set mutation:
s.add(v),s.remove(v), ands.discard(v)work at runtime, where before they compiled to a trap. The set's open-addressing table gained ausedcounter (occupied buckets plus tombstones) and an 8-byte-aligned 16-byte header;addrehashes into a fresh, larger__allocblock when the load factor would pass 1/2, re-inserting every live bucket (which also compacts the tombstones away) and writing the new pointer back through the variable or instance field the set was reached through, the same reserve-and-rebind shape lists and dicts use.remove/discardleave a tombstone rather than an empty bucket, so members whose probe ran past the removed one stay reachable, and membership now checks a bucket's state before comparing its value, so a removed member does not come back to life through the value left behind. Float members hash and compare at f64 width throughout.removeof a value the set does not hold traps (Python raisesKeyError, and a compiled module has no exception value to raise);discardignores the miss, as Python's does finallyand__exit__run on the non-local exit paths. Areturn,break, orcontinueleaving awithblock or atry/finallyjumped straight past the cleanup, which code generation only emits after the block, so the cleanup was silently skipped: awithleft bybreaknever called__exit__, and atry/finallyleft byreturnnever ran itsfinally. Both constructs are now lowered by the one bottom-up walk inir::context_managers, which inserts a copy of the cleanup ahead of every exit that leaves the block (abreakorcontinuebelonging to a loop written inside the block is left alone, since it does not leave). Because both use the same walk, cleanups nest in Python's order in either direction: atry/finallyinside awithruns itsfinallybefore__exit__, and awithinside atry/finallyruns__exit__first. The exception path is covered too, by the exception work below:withlowers ontotry/finally, whose propagation path runs the cleanup- Collection literals that mix float elements with int or bool ones (
[1, 2.5],(1, 2.5),{1: 1.5, 2: 2},{1, 2.5}) are rejected at compile time. Every element occupies one 8-byte slot and the collection reads them all at a single width, so one of the two element types came back as garbage; since the module-validation pass above, they failed WASM validation with a message blaming code generation instead. They are now a proper compile error through the codegen error channel, naming the literal kind and the function. Bools next to ints are unaffected (both are word-width, and Python calls bool an int), andexamples/tuple_example.py, which carried a mixed(42, "hello", 3.14)tuple, demonstrates a float tuple and the restriction instead - Indexing is normalized and bounds-checked.
xs[-1]is Python for "the last element", but with the count word at offset 0 it computed an address before the region and read that count back as though it were an element; an index past the end read, or wrote, whatever followed the region; a dict read for a key the dict did not hold answered 0, which is a real value and indistinguishable from a stored one; assigning into a tuple left the stack unbalanced (so the module failed validation while the compiler reported success); and assigning into a string was dropped on the floor. Every index is now folded against the length and checked: a negative one counts from the end, out of range raisesIndexError, a missing key raisesKeyError, and item assignment into a tuple or a string is a compile error naming the type, the way Python raisesTypeError. Both exceptions are catchable and both travel out of calls, since a function that indexes counts as one that can raise. Closure environments share the collection slot layout but hold a dispatch table slot where a collection holds its length, so reading a capture became its own IR node (IRExpr::EnvRead) instead of an index that would be checked against that word - Division by zero raises
ZeroDivisionError. Integer division and modulo trapped, which is loud but uncatchable and not what Python does, and float division answeredinfsilently. The divisor is checked first now (a nonzero literal divisor still compiles to a bare divide, son // 2is unchanged), and the raise is catchable and travels out of calls like any other - Growing a collection no longer moves it. Elements now live in a block the region's header points at, so
append,extend,insert,dict[key] = valuefor a new key, andset.addreallocate that block and leave the collection itself where it is. Growth used to reallocate the whole region and rebind the one variable or field the collection was reached through, which left every other name for it holding a stale pointer: a list grown inside a function it was passed to silently lost everything added after it outgrew its original block (elements that fitted were visible, so it looked intermittent), and one reached by indexing another collection had nowhere to rebind and trapped instead. Both work now, as do two locals naming the same list, a list inside a dict, and a set or dict grown through a parameter. The write-back machinery is gone with it - Closures capture the variable, not its value. A lambda copied its captured variables into the environment when it was created, so a captured variable reassigned afterwards kept its old value inside the closure, and closures made in a loop each froze a different iteration's value; Python's see the variable itself. A captured variable now lives in a one-slot heap cell that the enclosing function and every closure over it share: the enclosing function reads and writes through the cell, the environment carries the cell's pointer, and a lifted lambda binds that pointer from its environment and reads through it too, which is what makes nested closures share a cell all the way down. A loop variable is mirrored into its cell on each pass, so closures made in a loop all see the final value, as Python's do
str()of an integer renders its digits. The IR converter erased the call and left the argument in its place, sostr(123)was the integer 123:len(str(n))answered 0, comparing the result against a string literal never matched, and concatenating it built the wrong string, all silently. The call survives to code generation now and goes through the__i32_to_strrenderer, including for a value whose type codegen cannot resolve (a single i32 word like any other). What it cannot render is a compile error rather than an empty string: a bool would come out as1rather than Python'sTrue, a float needs a formatter this runtime does not carry, and a collection needs its elements rendered too- Three arithmetic helpers wrote to WASM locals 0, 1, and 2 outright, which are a function's first parameters.
a ** bclobbereda, so reading it afterwards gave the wrong value, and in a function whose first locals were not the width the helper assumed the module failed to validate. They use dedicated scratch locals now, and the bugs they were hiding are fixed with them: float modulo subtracted the wrong way round (3.5 % 2.0answered -1.5), and float power usedreturnfor its special cases, which returned from the enclosing function rather than the expression, and answered the base itself for a fractional exponent. Float power now computes an integral exponent by repeated multiplication (a negative one through the reciprocal, with0.0 ** -nraisingZeroDivisionError) and traps on a fractional one, which needs exp/log this runtime does not have; integer power traps on a negative exponent, whose Python answer is a float an i32 result cannot hold - F-strings interpolate every placeholder. An f-string holding a value kept only its first piece:
f"{n} items"compiled to the bare integerntyped as astr, solen()of it answered 0 and comparing it to a literal never matched, andf"Total: {n}"compiled to the label"Total: "with the value missing. Both reported a successful compilation. Only an f-string with no placeholders at all, or one whose placeholders were all constants, came out right. An f-string now lowers the way Python defines it: each placeholder renders its value withstr(), the pieces are concatenated left to right, and adjacent literal text is merged so each interned label stays a single blob. Constant placeholders still fold into the literal at compile time, and they fold the way Python spells them, sof"{True}"isTruerather than Rust'strueandf"{3.0}"keeps its decimal point. A float constant folds only over the range where Rust and Python write the same digits (positionally, no exponent); past it Python switches to1e+16where Rust spells the number out, so the placeholder is reported rather than folded to something else. What a placeholder cannot render is a compile error rather than a wrong string: a format specifier (f"{x:.2f}") and the!r/!aconversions change what is printed and have no implementation here, so both are rejected (!sis accepted, being what a bare placeholder already does), and a valuestr()cannot render, a float, a bool, or a collection, is reported by name.examples/fstrings.pydemonstrates the working shapes, with asserted results in the coverage suite and the truncation cases pinned intests/unit/memory_safety.rs
Changed
- Documented that
intis a 32-bit two's-complement integer, in a new "Numbers" section inREADME.md, next to the type list, and onIRType::Intitself. CPython'sintis arbitrary precision, so arithmetic that leaves the range -2147483648..2147483647 wraps here instead of growing:1000000 * 1000000answers -727379968 and2 ** 40answers 0. This is the one place the compiler answers differently without saying so, and it is now part of the definition of the subset rather than an unstated property of the backend. Checking every add, subtract, multiply, and power for overflow and trapping was the alternative; it costs instructions on the hottest path in any program and rejects code that relies on wrapping, and a 64-bit integer would only move the boundary.tests/unit/basics.rspins the wrapping behaviour so it stays deliberate - Exceptions transfer control.
raiseused to set a per-function flag that the end of the enclosingtrybody read to pick a handler, so the statements after araisestill ran, araiseinside a loop kept looping, a handler for another type swallowed the exception, an uncaughtraisewas ignored and the function returned normally, and nothing crossed a call boundary. Only one shape behaved like Python, by accident. Now the pending exception's type lives in a module global, araisebranches out of the block it is in (to the enclosingtry's handler dispatch, or out of the function), and any call to a function that can raise is followed by a check that keeps unwinding when one is pending. A handler whose type matches clears the exception and runs; one that does not match passes it on, so it keeps travelling outward through as many frames as it takes.finallyruns on the way past, including while an exception is propagating, andwithnow lowers ontotry/finally, so__exit__runs when an exception leaves the block too (including one raised inside a call the body made). An exception nothing catches unwinds out of the program and traps, instead of handing the host a default value: a call-depth global tells a propagating frame whether anything below it will check - Exception handlers match more like Python's.
except (A, B):was read as a bareexcept:(the IR held one optional type name, and a tuple is not a name), so it caught exceptions it should have passed on; handlers now carry every name they list and match any of them.except Exception:andexcept BaseException:catch anything, since in Python everything derives from them. Exception types outside the built-in table, a user-defined exception class above all, get a stable code derived from the name instead of sharing one generic code, so two different user exceptions no longer catch each other. Known limits, documented inREADME.md: exceptions carry a type and not an object (the message inraise ValueError("...")is dropped, andexcept ... as ebinds the type code), matching does not consult a user exception's base classes, and runtime faults such as an out-of-range index or1 // 0trap rather than raising, so they cannot be caught
0.14.0
Added
- Coverage audit: every bundled
examples/*.py(plus the multi-fileexamples/user_modules_app/and the project-directoryexamples/calculator_project/) now has an integration test that compiles it, instantiates the WASM withwasmi, and asserts concrete runtime results (tests/integration/coverage.rs), alongside operator-level unit suites for arithmetic, comparisons, boolean/bitwise logic, conversions, and augmented assignment (tests/unit/basics.rs) and for error quality (tests/unit/errors.rs). The integration harness gained untyped-call helpers (f64/mixed signatures), a disk-based entry-file compile path, and an in-memorywaspy_hostfilesystem that drivesexamples/file_io.pyend to end. The suite runs as an explicit named gate in the test workflow - Early validation of unsupported Python syntax:
parse_pythonnow walks the AST before lowering and rejectsasync def/async for/async with/await,match,global,nonlocal,del,assert,typealiases,except*,from module import *,*args/**kwargs/keyword-only parameters, class keywords (metaclasses), and loopelse:clauses — each with the construct named, its line/column, the enclosing function, and a workaround hint, instead of failing deep in codegen with an AST debug dump or compiling silently wrong code (loopelsebodies were previously dropped on the floor).min()/max()over a single iterable argument (previously a stub that always produced 0) are rejected the same way until implemented - Python syntax errors now report line and column (computed from the parser's byte offset) with the parser's own message;
ChakraError's located variants render their position inDisplay sum()over lists and tuples computes a real result — the previous codegen was a stub that always produced 0. The emitted loop walks the collection's[len][slot...]layout, accumulates at f64 width for float lists (all-float tuples included), and honors the optional start argument- New
examples/algorithms.py(gcd, primality, digit math, Collatz, Newton's sqrt) andexamples/calculator_project/(the project-compilation demo the justfile referenced but which was never committed);examples/builtins.py,bytes_example.py,range_example.py,set_example.py, andtuple_example.pygained assertable functions so the harness checks real values - justfile recipes:
verify-examples(compile every bundled example through the real drivers, including the multi-file and project paths),benchmark(wall-clock compile times, release build), andclean-all(build artifacts plus generated example outputs);just compilenow reports Python-source vs WASM-output sizes after compilation - User-defined module imports (#41): programs import their own
.pyfiles, not just the bundled stdlib. A new entry pointcompile_python_file(andcompile_python_file_with_options) resolves an entry file's imports against its directory —import modfindsmod.pyormod/__init__.py,import pkg.modfindspkg/mod.py— transitively through each resolved module's own imports, and links everything into the single output WASM module. Within any multi-file compilation (compile_multiple_python_files*,compile_python_project*, and the new resolver), all import forms now resolve across files:from mod import fcalls the merged function,from mod import f as gbinds the alias,import mod/import mod as mmake namespace calls (mod.f(...)), namespace constants (mod.CONST, inlined like any module-level variable), and namespace class instantiation (mod.ClassName(...)) work, with a local variable of the same name shadowing the module binding. Import-resolved module files bypass the special-file skip, so a genuine local module named e.g.config.py(or a package__init__.py) still links - Module caching (#41): a module imported through several paths is compiled and merged exactly once. The import resolver visits each module a single time (a diamond
app -> util/helper -> sharedlinks one copy ofshared), and the multi-file merge skips a filename it has already processed, so re-imports never duplicate functions or module state - File I/O through a documented host interface (#25):
open(path[, mode]),f.read([n]),f.write(s),f.close(),f.flush(), andwith open(...) as f:(desugared during lowering to open/body/close, sidestepping the unsupported general context-manager path, #5). The web target has no filesystem, so file operations compile to calls into four imported host functions — WASM modulewaspy_host:open(path_ptr, path_len, flags) -> fd,read(fd, buf, len) -> n(0 = EOF),write(fd, buf, len) -> n,close(fd) -> status— that the embedder provides; mode strings are folded to flag bits at compile time (r=1,w=2,a=4,b=8,+=16).read()fills a fresh length-prefixed heap blob and returns it as a regular string (default cap 64 KiB per call;read(n)caps atn); a newIRType::Filetypes handles so method calls dispatch statically. The import section is emitted only when the program callsopen()— everything else keeps instantiating with an empty import object, and the wasmi test suite ships a reference host implementation over an in-memory filesystem (examples/file_io.pyshows the JS equivalent) - Conditional imports in
try/exceptverified end to end (#4): an import insidetry/except ImportErrorparses, resolves, and its members are usable afterwards, asserted by a runtime test examples/user_modules_app/(namespace calls, aliases, a module constant, a class imported across modules, and a shared module imported twice) andexamples/file_io.py(write/read round trip, nestedwith open, append mode), both compiled and executed against Node as part of verificationjust compile <file>(theadvanced_compilerexample) and the wasmrun plugin's single-file build now compile by path, so an entry file's local imports resolve automatically- Python comments are preserved in the generated WebAssembly. The parser discards them, so a scanner recovers them from the source (skipping
#inside strings and docstrings, single- and triple-quoted), the IR module carries them, and the backend emits them as apython.commentscustom section: UTF-8 text, onefile:line:textentry per line. Custom sections carry no code, so nothing about how the module runs changes, and the section survives the Binaryen optimization pass.waspy::core::comments::comments_from_wasmreads them back out of a compiled binary - Runnable rustdoc examples on every crate-root entry point: single-source, multi-file, entry-file, project-directory, metadata, and
type_to_string, including the option-carrying variants and the on-disk paths (import resolution,pyproject.tomlmetadata). Each is a doctest, so the documented usage is checked by the test suite examples/context_managers.py, asserted end to end by the integration suite: entering and exiting, theasbinding, an earlyreturnfrom the body, nested managers, re-entry in a loop, and a subclass inheriting the protocoltests/unit/memory_safety.rs: regression coverage for the constructs that used to compile "successfully" and then misbehave at runtime (context managers, float-list iteration, list and dict growth past a literal's size, the unbalanced-drop set method call, and element types for collection fields holding instances), wired into the named CI gate alongside the error-message suite
Changed
- Breaking:
CompilerOptionsnow carries exactly the options the pipeline honors —optimizeandverbosity. The five removed fields (debug_info,max_memory,entry_point,generate_html,include_metadata) were never read by any compilation stage: linear memory is sized automatically from the module's data and grows on demand, entry points are auto-detected, and metadata printing/HTML harness generation are driver concerns (the example drivers and the wasmrun plugin keep their behavior through their own flags) - Breaking: the
waspy::parsercrate-root re-export is gone; usewaspy::core::parser(the root surface no longer leaks rustpython AST types). Crate-level rustdoc now states the stable public API — the crate-root exports — with a compilable quick-start example, and marks the pipeline modules as implementation detail - Augmented assignment on f64 locals (
x += 2.0,x /= 2.0, …) emitted i32 arithmetic on f64 operands and produced invalid WASM; it now selects the instruction width from the local's type and coerces the operand, and the bitwise/shift augmented operators (&=,|=,^=,<<=,>>=) gained real implementations instead of a placeholder that zeroed the target examples/stdlib_test.pyrestructured from a script with no functions (which the per-file driver rejected) into module-level imports plus amain()entry point; the multi-file and project example drivers print real usage text- Cargo.toml metadata says what Waspy is (a Python-to-WebAssembly compiler, not an interpreter); README documents the supported Python subset and the explicitly-rejected constructs in one authoritative place, and the docs site's feature/status claims are synced with the compiler (i32/f64 value types,
sum()status,withstatement coverage, 0.20.0 hardening entries) - The three multi-file compilation entry points share one merge implementation; project compilation (
compile_python_project*/compile_multiple_python_files_with_config) now applies function decorators like every other path (it previously skipped decorator processing) and merges per-file IR metadata consistently - A source file containing only module-level constants (no functions) now contributes its variables to a multi-file merge instead of being skipped
- Generators actually run (#6, #45), replacing the placeholder that dropped every yielded value. A generator function is rewritten during IR lowering into a resumable state machine: a synthesized state class holds the resume point, the
send()value, and every parameter and local as instance fields (so all live state survives suspension in linear memory), and a__stepmethod dispatches the original body — flattened into basic blocks — on the stored resume point inside a trampoline loop.yield vstores the next block id and returnsv; the next request re-enters at the stored block. Calling the generator function returns a fresh suspended generator object (a plain heap instance), so several instances of one generator advance independently. Yields work insidewhileloops,for-over-range/list/tuple loops, and conditionals; a generator yielding float values produces f64s end to end;return(or falling off the end) marks the generator exhausted and raisesStopIteration yield fromdelegates to an inner iterable — a range, a list, or another generator — and generators compose (a generator can drive another generator in its ownforloop, including recursively)- The full generator protocol:
for x in gen(...)drives a generator to exhaustion,next(g)pulls one value,x = yield vresumes with the value passed byg.send(v)(0 when resumed by plainnext), andg.close()finalizes the generator so later requests raiseStopIteration. Iteration is desugared at the IR level into an explicit__next__drive loop with staticClass::methoddispatch; exhaustion crosses the call boundary through a dedicated StopIteration flag (a new WASM global) set byraise StopIterationand read-and-cleared by a codegen intrinsic - Custom iterator protocol (#40): a user class implementing
__iter__/__next__iterates in aforloop, withraise StopIterationin__next__ending the loop — the same drive-loop machinery generators use.__iter__is honored when present (its declared return class types the iterator); a class with only__next__iterates itself - Unsupported generator shapes fail loudly at compile time instead of miscompiling:
yield/returninsidetry/with(suspension cannot re-enter a protected frame) and generator methods (yieldinside a class method) are rejected with clear errors. Known subset limits: generator locals bound by tuple unpacking or by a yield-freeforloop live in WASM locals and don't survive across ayield;send()on an unprimed generator starts it likenext();close()skipsGeneratorExit/finallysemantics examples/generators.py, asserted end to end by the integration suite: while/range/conditional suspension, earlyreturn,yield fromover a range and a generator, generator-consuming-generator, manualnext(),send()accumulation,close(), a userCountdowniterator class, andbreakout of a drive loop- Tuple targets in
forstatements:for a, b in pairsbinds a hidden loop variable and unpacks it per iteration (star targets included, reusing the extended-unpacking machinery), and the iterator-shaped builtins desugar during lowering —for i, x in enumerate(xs[, start])threads an explicit counter alongside the driven iterable,for a, b, ... in zip(s0, s1, ...)drives the first sequence and indexes the rest with a shared counter (stopping at the shortest), andfor k, v in d.items()(plus single-target.keys()/.values()) walks the dict's entry slots positionally through two new codegen intrinsics. All of it composes with generators: anenumerateoritems()loop containingyieldsuspends and resumes with its counters and dict pointer preserved in the generator state.examples/loop_unpacking.pyasserts each shape at runtime len()of a value whose type codegen can't resolve (e.g. a collection read back out of an instance field) now reads the count word from the pointer instead of answering a constant 0, and instance fields initialized with collection literals orrange()keep their collection type (#44), replacing the placeholder that evaluated the iterable and yielded an empty list. List ([x * 2 for x in xs if cond]), set ({x % 3 for x in xs}, deduped at construction via a runtime-built open-addressing hash table), and dict ({k: v for k, v in items}, including tuple-unpacking targets) comprehensions all build their result at runtime in a fresh__allocblock — the element count depends on iterable lengths and filters, so capacity is computed first (the iterable's length, or a counting pre-pass over the outer generators when there are several; with multiple generators an inner iterable expression is therefore evaluated once per outer iteration in both passes) and the final count is written back after the fill loops run. Iterables can be lists, tuples, or ranges (ascending and descending, with runtime trip-count math; strings and sets as comprehension iterables are a follow-up); filters compose per generator; float elements round-trip as f64 slots. Multiple generators ([x for row in m for x in row]) and nesting (a comprehension as another's element or iterable, tracked by a per-function nesting depth that keys the reserved helper locals) work, as does a comprehension as aforstatement's iterable. Generator expressions lower as list comprehensions (every consumer here drains them eagerly). Python 3 scoping is honored: comprehension variables are renamed to unique names during lowering, so they never clobber (or leak into) same-named function locals- Comprehension results adopt their concrete element types on assignment (e.g.
List(Float),List(List(Int))), so indexing a float or nested result loads the right width — the scan pass can only type them as collection-of-Unknown, which previously stuck - Full closure variable capture (#43), replacing the stub that compiled every lambda to the constant
1. A whole-module finalize pass lifts each lambda into a real function whose trailing__envparameter carries a heap environment[table_slot][captured...]; free variables are detected by scope analysis (parameters, nested-lambda parameters, and comprehension targets bind; module functions/classes/variables, stdlib modules, and builtins resolve globally) and captured into the environment at creation. Closure values dispatch through a funcref table withcall_indirect(one signature per arity), so closures are first-class: returned from functions (make_adder(5)works), passed as arguments and called through untyped parameters, stored in collections, nested (lambda x: lambda y: x + y— the inner closure captures the outer's parameter), and defined at module level. Capture is by value at creation time — a captured variable mutated after the closure is created keeps its old value inside the closure (Python's late-binding cells are a follow-up), and float captures read as 0 for now. Calling a subscripted expression directly (fs[0](https://github.com/anistark/waspy/blob/main/x)) is not lowerable yet; bind it to a local first - Extended (starred) unpacking (#24):
a, *b, c = xsbinds scalars positionally from the front and back and collects the middle slice into a fresh runtime list with onememory.copy(slots are contiguous), solen(b), indexing, and iteration work on the starred target. The star can sit anywhere (*xs, last/first, *rest), tuples and lists both unpack, and an exact-fit unpack leaves the starred list empty rather than trapping examples/comprehensions.py,examples/closures.py, andexamples/extended_unpacking.py, each asserted end to end by the integration suite (filters, multi-generator flattening, comprehension-in-comprehension, set membership on comprehension results, per-iteration freshness inside loops; capture independence between closures from one factory, zero-argument closures, closures built inside a comprehension; star-position variants and empty middles)
Fixed
withover a user-written context manager (#5) compiles and runs. It previously reported a successful compilation and emitted a module that failed WASM validation (invalid local index), because the code generator added locals after the function's local vector was fixed and never called either protocol method. A whole-module IR pass (ir::context_managers) now rewriteswith expr as name:into__enter__/body/__exit__over ordinary assignments, running after the class table is complete so the synthesized locals are typed from the real__enter__return type (an f64-returning__enter__included). Nested blocks, managers inherited from a base class, re-entering the same manager in a loop, and an inlinewith ClassName(...) as x:all work, and an earlyreturnfrom the body runs__exit__before leaving (the returned expression is evaluated into a temporary first, so it still sees the state from inside the block). A class missing__enter__or__exit__, or a context expression whose class cannot be resolved at compile time, is now a located compile error instead of a broken module.with open(...)keeps its existing lowering. Known limits: an exception or abreakleaving the body skips__exit__, and__exit__'s return value never suppresses an exception- Iterating a float list held in a variable (
for x in xs) bound the loop variable as an i32 and produced garbage values; only float literal iterables worked. Collection literals now carry their element type through the assignment scan, and an element-less annotation (xs: list = [1.5, 2.5]) adopts the element type of the value it is assigned, so the loop variable is typed f64 and the elements read back at full width. A mixed-type literal stays untyped rather than mis-typing part of it - Growing a list past the size of its literal overwrote whatever collection sat next in linear memory:
a = [1, 2]; b = [100, 200]; a.append(3); a.append(4)leftb[0]reading 4. Collection regions now carry a capacity alongside their length, andappend,extend, andinsertreserve room before writing, reallocating into a fresh__allocblock (capacity doubling, with a floor of 4) and copying the live elements when the region is full. The grown pointer is written back through the variable or instance field the list was reached through, soxs = []filled in a loop andself.items.append(v)both work. Where there is nowhere to write the new pointer back (a list reached by indexing another collection, or a temporary), the grow path traps rather than writing out of bounds - Adding a key to a dict past its literal's entry count had the same effect on the neighbouring collection;
dict[key] = valuefor a new key now reserves an entry through the same path, sod = {}filled in a loop works and float values survive the move. Updating a key the dict already holds still writes in place - A method call on a receiver kind with no method support,
set.add(...)above all, emitted twodropinstructions for a one-word receiver, so the module failed WASM validation while the compiler reported success. The drop count now matches what the receiver left on the stack, and the call compiles to a trap: set mutation is still unimplemented, and failing loudly at runtime beats silently doing nothing. Receiver types are only known during code generation, so this cannot yet be a compile-time rejection - The empty-tuple literal copied only 4 of its header bytes when rebuilt inside a loop
- A collection field started empty (
self.items = [], the ordinary way to begin one) carried no element type, so iterating it bound an untyped element:for it in self.items: it.priceread 0 and calling a method on the element trapped. Class building now also collects the element types a class puts into its own collection fields throughappend/insert, threading a local environment through each method body so the two-step form (item = Item(...)thenself.items.append(item)) resolves as well as the directself.items.append(Item(...)), and fills in an element-less field from that evidence. A field initialized with a literal of instances (self.ps = [P(1.5), P(2.5)]) and a field assigned an instance (self.origin = Point()) now record the class too. With this, the first realistic whole program compiled against waspy (a shopping-cart domain: classes holding a list of instances, float money arithmetic, methods calling methods) returns the same values CPython does
0.12.0
Added
@dataclass(#18): a class decorated with@dataclass(or@dataclasses.dataclass) gets__init__,__eq__, and__repr__generated from its annotated fields during IR conversion, so the regular field-discovery and instantiation machinery applies unchanged. The constructor takes one parameter per field with field defaults honored — a call site that omits trailing arguments has the defaults spliced in by a new whole-module post-pass (src/ir/finalize.rs), which also fills parameter defaults for ordinary function calls (previously an omitted default underflowed the stack into invalid WASM) and rejects a construction missing a required argument. Python's dataclass rules are enforced at compile time: a field without a default may not follow one with a default, mutable defaults (list/dict/set literals) are rejected, anddataclasses.field(...)/@dataclass(...)with arguments fail loudly as unsupported. A method the user writes in the class body wins over the generated one;ClassVar-annotated names stay class variables==/!=between class instances now dispatches to__eq__when the left operand's class defines or inherits one (generated or hand-written) — the two instance pointers already on the stack are exactly the(self, other)argument pair, keeping dispatch static like the rest of the object model. Dataclass equality therefore compares field values; classes without__eq__keep pointer identity- The generated
__repr__rendersName(field=value, ...)at runtime: a new runtime helper__i32_to_str(value) -> offset(emitted alongside__alloc/__alloc_obj) renders an i32 as decimal digits in a length-prefixed__allocblob, andstr(x)on runtimeint/boolvalues now compiles to it (previouslystrwas an unknown builtin yielding garbage). String fields are spliced in quoted, like Python's repr; a dataclass with a float field skips__repr__generation (no f64 formatter yet) - Abstract base classes (#13): a class deriving from
abc.ABC(directly or transitively) that still has unimplemented@abstractmethodmethods rejects instantiation at compile time with Python's "Can't instantiate abstract class" TypeError message — including a subclass that fails to implement an inherited abstract method. Concrete methods on the ABC are inherited normally,isinstanceworks against the abstract base, and theABCbase is a marker that neither occupies the single-inheritance slot nor contributes layout.abcanddataclassesare recognized stdlib modules (compile-time only, no runtime surface) - String/bytes-typed instance fields now work end to end:
self.text = "..."narrows the(offset, length)pair to the offset word its 8-byte slot holds (previously the store left an extra value on the stack — invalid WASM), and reading the field rebuilds the pair from the blob's length prefix. Call results were fixed the same way across every call path (user functions, methods, property getters,super()calls): astr-returning callee leaves a single offset word, and the caller now recovers the length viaload(offset - 4)instead of misreading the stack passcompiles as the no-op it is (e.g. an@abstractmethodbody); previously any function containing it failed conversionexamples/oop_dataclasses.py(construction with and without defaults, override of a default,==/!=by value,__repr__round-tripped byte-for-byte including a negative int and a quoted string field) andexamples/oop_abc.py(concrete subclass instantiation, inherited concrete method, abstract-method dispatch,isinstanceagainst the ABC); the integration suite asserts each runtime result plus the six rejection errors (default ordering, mutable default,field(...), missing required argument, abstract class, abstract subclass)- Method kinds beyond plain instance methods, dispatched statically by a per-method kind recorded at class registration:
@staticmethod(#17) takes no implicit argument and is callable on the class (Counter.add(a, b)) or on an instance (whose pointer is dropped);@classmethod(#16) receives the class implicitly — call sites push the class id ascls, and inside the bodycls(...),cls.method(...), andcls.varresolve statically to the defining class (consistent with the object model's no-vtable dispatch), enabling the classmethod factory pattern;@propertywith@<name>.setter(#10) compilesobj.attrreads to the getter,obj.attr = vassignments to the setter, andobj.attr OP= vto a getter-then-setter chain, instead of direct field access — a property's name never occupies a field slot (the setter body assigns the real backing field, e.g.self._attr) - Conflicting or unsupported method decorator stacks fail compilation with a clear error instead of silently mis-dispatching: combining two kinds (e.g.
@staticmethod+@classmethod), a@<name>.setter/@<name>.getterwhose name doesn't match its method, a setter with no matching@propertygetter (it could never be reached), and property deleters (unsupported) examples/oop_method_kinds.pycovering a static method called on the class and on an instance, a classmethod factory viacls(...), a classmethod called through an instance, property reads (stored and computed), a property setter, and augmented assignment through a property; the integration suite asserts each runtime result plus the three rejection errors- Single class inheritance with method resolution (#9): a subclass extends one base class, inheriting its fields and methods. The base's fields are laid out as a prefix of the subclass instance (identical offsets), so a base method reading
self.xworks unchanged on a subclass instance; the subclass's own fields append after the base's size. An inherited method dispatches to the base's already-compiled WASM function (no duplication); a method redefined in the subclass overrides it at call sites typed as the subclass. Dispatch remains fully static — a base method callingself.helper()internally resolves to the base'shelpereven on a subclass that overrides it (true virtual dispatch throughselfwould need a vtable and stays out of scope). Multiple inheritance is rejected with a clear compile error instead of silently mislaying fields super().__init__(...)andsuper().method(...)dispatch statically to the immediate base class of the enclosing method's class, passingself(local 0) through, so construction and behavior chain across multi-level hierarchies (each class's method table already contains its base's fully resolved entries)isinstance(obj, ClassName)andissubclass(Sub, Base)over the user class hierarchy. Every instance now carries its class id in the tag word at offset 0 (the slot every layout already reserved), stamped by a new runtime helper__alloc_obj(size, class_id)that wraps__alloc— keeping the instantiation sequence stack-only.isinstancecompares the runtime tag against the target class and all its subclasses, so it answers correctly even when the static type is a base class (e.g. a factory annotated-> Animalreturning aDog);issubclassfolds to a compile-time constant. Checks against built-in types (isinstance(x, int)) are a follow-upexamples/oop_inheritance.pycovering method override, an inherited method reading a base-prefix field,super().__init__chaining,super().method()reaching the base implementation past an override, a two-levelPuppy -> Dog -> Animalhierarchy,isinstanceacross the hierarchy and via a base-typed factory, and compile-timeissubclass; the integration suite asserts each runtime result plus the multiple-inheritance rejection- Heap-allocated, multi-instance objects:
ClassName(...)now calls the runtime bump allocator (__alloc(instance_size)) and returns a distinct pointer per instantiation, replacing the fixed compile-time address that limited every class to a single live instance. Multiple instances of one class coexist with independent field state, and instances are first-class values — passable as arguments, returnable from factory functions, storable in collections (the slot holds the instance pointer, consistent with the string/bytes convention), and mutable through the shared pointer.__init__is compiled to returnselfso the instantiation sequence is stack-only and nested instantiations compose; classes without__init__allocate a zeroed instance directly. Objects are not reclaimed (the bump allocator has nofree) and__del__is not invoked; GC is tracked post-1.0 examples/oop_objects.pycovering two independently mutated instances, a fresh zeroed instance per call, a factory return, an instance mutated through a function argument, instances stored in a list/tuple/dict and read back live, augmented assignment on a field (self.value += n), and per-instance f64 fields; the integration suite asserts each runtime result- Integration test harness (
tests/integration/,tests/utils/, registered as theintegration_examplestest target): compiles everyexamples/*.py, validates and instantiates the module withwasmi, and asserts runtime results (e.g.break/continueand multi-file cross-calls). The sweep immediately surfaced the two parameter/raisefixes below examples/nested_collections.py, covering nested list-of-lists indexing, a per-iteration list literal that escapes its loop, float dict/set values, and lossless f64 round-trip through list/dict/tuple slots,in, and float-list iteration; the integration suite asserts each result- Non-lossy f64 collection layout: every collection element now occupies an 8-byte slot (the count header stays 4 bytes), so float members of lists, dicts, sets and tuples round-trip with full f64 precision instead of being narrowed to f32 (~7 significant digits). The change spans every access path — literals, indexing,
dict[key]lookup/assign, set dedup,in/not in,foriteration (a float list literal binds an f64 loop variable), and the list/tuple methods (append,pop,extend,insert,remove,index,count). Slot address arithmetic is type-independent; only the load/store/compare width is chosen by element type. Binding floats from afor/tuple-unpack over a variable (rather than a literal) remains a follow-up - Sets are now an open-addressing hash table (linear probing) instead of a linear array, so membership (
in/not in) and construction dedup are amortised constant time rather thanO(n)/O(n²)scans. Layout is[count:i32][cap:i32]followed bycapbuckets of[state:i32][_pad][value:8 bytes], withcapa power of two kept above the member count so a probe always terminates at an empty bucket; the member count stays at offset 0 solen()is unchanged. The whole region is zeroed (memory.fill) on construction, which also clears stale bucket state when a set literal is rebuilt each iteration of an enclosing loop. Float members are hashed by folding both halves of their f64 bit pattern and compared at full width. Lists keep their linearinscan
Fixed
- A quoted forward-reference annotation (
def create(...) -> "Counter":) now resolves to the class type instead ofAny, so a value returned under one is typed as its class — previously a method call on such a value hit the unknown-object path, which drops two stack values and produced invalid WASM - A string/bytes argument to a class constructor (
ClassName("text")) is now narrowed to its single offset word like any other user-function argument; previously the full(offset, length)pair was pushed against__init__'s one parameter slot, leaving an extra value on the stack (invalid WASM) - Float
dictkeys now match at full f64 width on both lookup and index-assign. Previously the key expression was coerced tointand compared withi32.eq, so{1.5: ...}[1.5]never matched — and because1.5and2.5share their low 32 bits, distinct float keys were also indistinguishable. The key path is now width-aware (mirroring the value path): the index is hinted with the container's key type, a float key needle is kept in a dedicated second f64 scratch so it can coexist with a float value, and the search compares (and the append stores) atf64width - A collection literal built inside a loop reused one compile-time region every iteration, so per-iteration lists/dicts/sets/tuples that escaped the loop all aliased the last iteration's data (#14). Inside a loop the literal is now built into that shared template region and then copied (
memory.copy) into a fresh runtime__allocblock, so each iteration's collection gets its own region; outside a loop the unique compile-time region is still used directly. Nested literals compose — an inner literal stores its own runtime pointer into the outer template before the outer region is copied out - Float values in
dict, float members of aset, and float elements of lists/tuples now keep full f64 precision (see the 8-byte slot layout above); previously they were stored as f32, and a float-valueddictorlist[i] = <float>index-assign could even emit a wrong-width store that overflowed into the next slot raise ExceptionType(arg)(e.g.raise ValueError("msg")) emitted the constructor call and left its argument on the stack, producing invalid WASM ("values remaining on stack"); the raised exception is now resolved to its integer type code by name, shared with theexcepthandler dispatch so the two cannot diverge- String/bytes function parameters now form a complete
(offset, length)pair. Referencing astr/bytesparameter (e.g.op == "add") pushed only its offset, so consumers like==underflowed the stack into invalid WASM; the length is recovered from the blob prefix (load(offset - 4)) when there is no companion length local. Passing a string/bytes value as an argument to a user function now narrows it to the single offset word each parameter slot holds (the callee recovers the length), fixing an out-of-bounds read that previously passed the length as the offset
0.11.0
Added
breakandcontinuestatements inforandwhileloops. Each loop body is wrapped in an inner block socontinuefalls through to the iterator step, and a block-depth counter plus loop-context stack compute the correct branch depth so break/continue nested insideif/tryframes or nested loops target the right loop (#23)- Runtime bump allocator (
__alloc(size) -> ptr) over a mutable global heap pointer that grows linear memory on demand (nofree), emitted after the user functions; backs runtime-built strings/bytes such as concatenation results
Fixed
for x in <list>read each element fromptr + i*4with load offset 0, loading the leading length word as element 0 and dropping the last element; the load now uses a+4offset so iteration sees the real elements- Reading a string/bytes value out of a list or tuple rebuilds its
(offset, length)pair instead of emitting invalid WASM (alocal.setstack underflow): each interned string/bytes blob is laid out as[len:i32][bytes][nul?]with its recorded offset pointing past the prefix, so the length is recovered viaload(offset - 4); the stored slot stays the interned offset, so offset-based membership and set de-duplication are unchanged - String/bytes concatenation (
+) copies both operands into a freshly allocated, length-prefixed blob viamemory.copy, replacing the placeholder that returned the left operand's offset with the combined length (correct only when literals happened to be adjacent in memory) - A string/bytes function return yields the value's offset rather than its length (a length-prefixed blob lets the caller recover the length via
load(offset - 4)) - Binaryen optimization runs with the bulk-memory feature enabled so the
memory.copyemitted by concatenation survives optimization instead of aborting it
0.10.0
Fixed
- Compiled modules are valid and runnable: Code section before Data, per-function scratch locals, corrected
whileexit test, barelist/dict/set/tupleannotations, andMemoryLayoutpropagated to codegen (#78) - Collection runtime:
dict[key] = valueupdate/append, set de-duplication,in/not infor sets and lists, andlen()for sets/tuples/bytes (#78) - Collections no longer alias in memory — each list/set/tuple/dict/range literal gets its own region via a compile-time bump allocator; distinct and nested literals previously shared one address (Issue #14)
- Float elements in lists/tuples round-trip correctly (stored as f32 in the one-word slot), and unannotated collection locals keep their element type so
xs[i]no longer returns0 for ... in range(...)iterates (ascending): iterator locals are reserved up front (nested loops get distinct locals) and range fields use the correct store operand ordertry/except/finallycompiles to a valid module: exception-state locals reserved in the scan, and balanced control flow (was emitting oneEndtoo many)- Numeric typing:
and/oryield ani32result, mixed int/float arithmetic widens the int operand tof64, unannotated float locals infer asf64, and locals are declared in index order - Module-level variables (e.g.
PI = 3.14159) resolve inside functions by inlining their initializer int()/float()actually convert (truncate / widen);min()/max()reduce correctly;os.pathsubmodule attributes (e.g.os.path.sep) resolve- Expression statements no longer emit a stray
dropafterprint() - Descending
range()loops iterate: the for-range break test is now step-sign-aware (stop oncecurrent >= stopascending, orcurrent <= stopdescending), and integer unary negation is corrected —-xpreviously evaluated tox(#84) - String/bytes locals round-trip: each carries a companion length local so the
(offset, length)pair survives assignment,len()keeps the length, bytes constants are written to the data section, andx[a:b]slicing lowers correctly via a branchless clamp (#85) - Float-aware classes: instance fields are typed (f64 for float fields),
selfresolves as its class, constructor/method arguments coerce to declared parameter types (int literals widen to f64), and augmented field assignment (self.x *= f) and class-variable reads (ClassName.var) work (#86) - Float return types are resolved up front — inferred from
returnstatements when unannotated — and float-valued stdlib-constant locals (e.g.pi = math.pi) are typedf64, fixing an f64-into-i32 mismatch that aborted the optimizer (#87)
0.9.0
Added
-
Regular Expression (re) Module Runtime Implementation (Issue #30)
- Full runtime support using the
regexcrate - Core functions:
re.compile(),re.search(),re.match(),re.fullmatch() - Multi-match functions:
re.findall(),re.finditer() - String manipulation:
re.split(),re.sub(),re.subn() - Utility functions:
re.escape(),re.purge() - Match object support with methods:
group()- returns matched stringgroups()- returns tuple of captured groupsstart(),end()- match position indicesspan()- returns (start, end) tuple
- Regex flags support:
re.IGNORECASE/re.I- case-insensitive matchingre.MULTILINE/re.M- multi-line mode (^ and $ match line boundaries)re.DOTALL/re.S- dot matches newlinesre.VERBOSE/re.X- verbose patterns with commentsre.ASCII/re.A- ASCII-only matchingre.UNICODE/re.U- Unicode matching (default)
- Compile-time pattern evaluation for constant string patterns
- Full runtime support using the
-
Datetime Module Runtime Implementation (Issue #32)
- Full runtime support for
datetimemodule with chrono backend - Constructors:
datetime.datetime(),datetime.date(),datetime.time(),datetime.timedelta() - Class methods:
datetime.datetime.now(),datetime.datetime.today(),datetime.date.today() - Factory methods:
fromtimestamp(),fromisoformat(),strptime() - Instance methods:
strftime(),isoformat(),replace(),timestamp(),weekday(),isoweekday() - Date arithmetic operations:
datetime + timedelta→ datetimedatetime - timedelta→ datetimedatetime - datetime→ timedeltadate + timedelta→ datedate - timedelta→ datedate - date→ timedeltatimedelta + timedelta→ timedeltatimedelta - timedelta→ timedelta
- New dedicated IRType variants:
Datetime,Date,Time,Timedelta - Datetime represented as 7 i32s: (year, month, day, hour, minute, second, microsecond)
- Date represented as 3 i32s: (year, month, day)
- Time represented as 4 i32s: (hour, minute, second, microsecond)
- Timedelta represented as 3 i32s: (days, seconds, microseconds)
- Test suite in
examples/test_datetime.py
- Full runtime support for
-
Logging Module (Issue #34)
- Complete implementation of Python's
loggingstandard library module - Log level constants:
DEBUG,INFO,WARNING,ERROR,CRITICAL,NOTSET - Aliases:
WARN(forWARNING),FATAL(forCRITICAL) - Logging functions:
debug(),info(),warning(),error(),critical(),exception(),log() - Configuration:
basicConfig(),setLevel(),disable() - Logger management:
getLogger() - Handler support:
addHandler(),removeHandler() - Classes:
Logger,Handler,StreamHandler,FileHandler,Formatter,Filter,LogRecord - Test suite in
examples/test_logging.py
- Complete implementation of Python's
-
JSON Module Runtime Implementation (Issue #31)
- Implemented runtime support for
json.dumps()- serialize Python objects to JSON strings - Implemented runtime support for
json.loads()- parse JSON strings to Python objects - Added runtime support for
json.load()andjson.dump()for file-based operations - Added support for
JSONEncoderandJSONDecoderclasses - Comprehensive test suite in
examples/test_json.pycovering:- Basic type serialization (dict, list, string, int, bool, None)
- Deserialization of JSON strings
- Nested data structures
- All major json module functions
- Implemented runtime support for
-
Complete System Calls Implementation (Issue #27)
- Functional
osmodule method calls (os.getcwd(),os.getenv(),os.getpid(),os.urandom()) - Full
os.pathmodule support with working method calls- Path manipulation:
os.path.join(),os.path.basename(),os.path.dirname(),os.path.abspath() - Path inspection:
os.path.exists(),os.path.isfile(),os.path.isdir() - Path operations:
os.path.split(),os.path.splitext()
- Path manipulation:
- Stdlib module method call handling in compiler for
sys,os, andos.pathmodules - WASM-appropriate implementations with platform limitations for web environments
- Comprehensive test suite in
examples/test_system_calls.py
- Functional
Fixed
- Fixed json module functions that were previously only stubs without runtime implementation
- JSON module now properly compiles and executes in WASM environment
- Fixed
osmodule functions that were previously only stubs - Fixed
os.pathsub-module access that was returning only attributes, not callable functions - Fixed stdlib method call compilation to properly handle module and sub-module method invocations
0.8.0
Added
-
Standard Library Modules (Complete)
- sys module: System parameters and functions
- Attributes:
argv,platform,version,maxsize,stdin,stdout,stderr,path
- Attributes:
- os module: Operating system interface
- Attributes:
name,sep,pathsep,linesep,devnull,curdir,pardir,extsep,environ - Functions:
getcwd,getenv,getpid,urandom - os.path submodule: Path manipulation functions
- Functions:
join,exists,isfile,isdir,basename,dirname,abspath,split,splitext - Attributes:
sep,pathsep,curdir,pardir
- Functions:
- Attributes:
- math module: Mathematical functions and constants
- Constants:
pi,e,tau,inf,nan - Trigonometric:
sin,cos,tan,asin,acos,atan,atan2 - Hyperbolic:
sinh,cosh,tanh - Exponential/Logarithmic:
exp,log,log10,log2,pow - Rounding:
floor,ceil,trunc,round - Utility:
sqrt,abs,fabs,copysign,fmod,remainder,degrees,radians,hypot,factorial,gcd,isnan,isinf,isfinite
- Constants:
- random module: Random number generation
- Functions:
random,randint,randrange,uniform,choice,shuffle,sample,seed,getrandbits,gauss,normalvariate,expovariate
- Functions:
- json module: JSON encoding and decoding
- Functions:
loads,dumps,load,dump,JSONEncoder,JSONDecoder - Implementation: Uses
serde_jsonfor compile-time JSON operations - Runtime JSON parsing infrastructure in place
- Functions:
- re module: Regular expression operations
- Functions:
compile,search,match,fullmatch,findall,finditer,split,sub,subn,escape,purge - Flags:
IGNORECASE,MULTILINE,DOTALL,VERBOSE,ASCII(and short forms:I,M,S,X,A)
- Functions:
- datetime module: Date and time manipulation using chrono crate
- Types:
datetime,date,time,timedelta,timezone,tzinfo - Methods:
now,today,fromtimestamp,fromisoformat,strftime,strptime,replace,timestamp,isoformat,weekday,isoweekday - Constants:
MINYEAR,MAXYEAR - Implementation: Uses
chronocrate for compile-time datetime operations - Compile-time helpers:
datetime_now_utc(),datetime_now_local(),date_today(),datetime_from_timestamp(),datetime_from_iso(),datetime_to_iso(),datetime_strftime()
- Types:
- collections module: Specialized container datatypes
- Functions:
namedtuple,deque,Counter,OrderedDict,defaultdict,ChainMap,UserDict,UserList,UserString
- Functions:
- itertools module: Iterator building blocks
- Infinite iterators:
count,cycle,repeat - Terminating iterators:
chain,compress,dropwhile,filterfalse,groupby,islice,starmap,takewhile,tee,zip_longest - Combinatoric iterators:
product,permutations,combinations,combinations_with_replacement - Additional:
accumulate,batched,pairwise
- Infinite iterators:
- functools module: Higher-order functions and operations on callable objects
- Functions:
reduce,partial,partialmethod,wraps,update_wrapper,total_ordering,cmp_to_key - Decorators:
lru_cache,cache,cached_property,singledispatch,singledispatchmethod
- Functions:
- sys module: System parameters and functions
-
Generator Functions & Iterators
yieldstatement support in function bodies:yield value- Generator type system:
IRType::Generator[T]for type tracking - Yield statement compilation to WASM instructions
- Foundation for iterator protocol implementation
- Support for generator expressions in comprehensions
-
Import System
- Import statement parsing:
import moduleandimport module as alias - From-import support:
from module import name1, name2with aliases - Star imports:
from module import *with detection and tracking - Conditional imports in try/except blocks with fallback tracking
- Dynamic imports via
__import__(module_name)function - Dynamic imports via
importlib.import_module(module_name) - Module variable tracking and registration in IR
- Module type system:
IRType::Module(name)for imported modules - Import statement IR generation and WASM compilation
- Import statement parsing:
-
Functional Programming Features
- Lambda functions: Anonymous function support with
lambda x: x + 1syntax - Parameter support in lambdas with type inference
- Callable type tracking for function objects:
IRType::Callable { params, return_type } - Closures: Variables captured from outer scope with
captured_varsfield - Foundation for higher-order functions (passing functions as arguments)
- Lambda functions: Anonymous function support with
-
List Comprehensions
- List comprehension syntax:
[expr for var in iterable] - Filter conditions in comprehensions:
[x for x in list if condition] - Single generator comprehension support with proper iteration
- Constant list literal optimization for comprehensions
- Runtime support for variable-based iterables
- Memory allocation for result lists via
allocate_list()helper
- List comprehension syntax:
-
Exception Handling Enhancements
raisestatement with exception type support- Exception type tracking and flag management in WASM execution
- Multiple exception handler support (already present, verified working)
- Exception propagation through try/except/finally blocks
- Exception state preservation and restoration
-
Tuple Data Type
- Tuple literals with variable expressions:
(a, b, c)and(x + 1, y * 2) - Tuple indexing with type tracking:
tuple[0],tuple[1], etc. - Heterogeneous tuples with mixed types:
(42, "hello", 3.14) - Empty tuples with type annotations:
empty: tuple[int] = () - Single-element tuples:
(value,)with proper syntax - Proper type preservation for each element in the tuple
- Memory layout:
[length:i32][elem0:i32][elem1:i32]...
- Tuple literals with variable expressions:
-
Range Function
range(stop)- Single argument formrange(start, stop)- Two argument formrange(start, stop, step)- Full three argument form with custom step- Full integration with for loops
- Range iteration support with step handling:
for i in range(0, 10, 2): - Negative step support:
for i in range(10, 0, -1): - Range object stored in memory:
[start:i32][stop:i32][step:i32][current:i32]
Changed
- Added
Yield { value }statement variant for generator support - Added
ImportModule { module_name, alias }statement variant for module execution - Added
Generator(Box<IRType>)variant toIRTypeenum for generator type tracking - Added
Lambda { params, body, captured_vars }variant toIRExprenum - Added
Callable { params, return_type }variant toIRTypeenum - Updated
ListComphandling to support filter conditions in comprehensions - Added
allocate_list(element_count: u32)helper method toMemoryLayout - Removed error blocking for list comprehension filters (now supported)
- Enhanced type_to_string() function in both metadata.rs and lib.rs for Callable and Generator types
- Added
TupleLiteral(Vec<IRExpr>)variant toIRExprenum - Added
RangeCall { start, stop, step }variant toIRExprenum - Added
IRType::Rangeto type system - Enhanced for loop handler to support range iteration with proper step increments
- Extended compiler/function.rs to handle Yield and ImportModule statements
0.7.0
Added
-
Bytes Type Support
- Bytes literals:
b"hello"andb'world' - Bytes indexing (read & write):
bytes_var[i]andbytes_var[i] = value - Bytes slicing:
bytes_var[start:end:step]with proper bounds checking - Bytes concatenation with the
+operator - Full WASM compilation support for binary data handling
- Bytes literals:
-
Object-Oriented Programming
- Class definitions with full parsing, IR generation, and WASM compilation
- Instance method definitions with implicit
selfparameter - Object instantiation via class constructor calls (e.g.,
ClassName(args)) - Automatic
__init__method invocation during object creation - Method calls with proper dispatch to compiled methods (e.g.,
obj.method()) - Instance attribute access (getter):
obj.attrreturns field value - Instance attribute assignment (setter):
obj.attr = valuestores to memory - Per-instance field storage with calculated memory offsets
- Memory layout extensions to support object heap allocation (starting at 64KB)
- Qualified method export names (ClassName::method_name) for WASM exports
- Class method compilation alongside module functions
- Support for mixed instance variables and methods in class definitions
- Proper type tracking with
IRType::Class(name)throughout compilation
-
Control Flow Features - Full Implementation
forloop iteration over lists and strings with proper element assignment- Allocates iterator state: pointer, counter, and length tracking
- Loads list length from memory and iterates through indexed elements
- Fallback support for integer-based counting
try/except/finallyexception handling- Exception flag and type tracking using special local variables
- Typed exception handlers (ZeroDivisionError, ValueError, TypeError, KeyError, IndexError, AttributeError, RuntimeError)
- Bare except clause for catching all exceptions
- Exception handler variable assignment (
except Error as e:) - Multiple exception handler matching with type-based dispatch
- Finally blocks execute regardless of exceptions
withstatement context manager support- Context expression evaluation and variable binding (
with expr as var:) - Exception state preservation across with blocks
- Proper exception flag initialization and restoration
- Context expression evaluation and variable binding (
-
Built-In Functions Implementation
len()function: Full support for strings, lists, and dictionaries- For strings: Returns length from stack (offset, length) pair
- For lists/dicts: Loads length from first 4 bytes in memory
print()function: Proper argument handling with type-aware stack cleanup- Handles string pairs (offset, length) separately from scalar types
min()andmax()functions: Multiple argument support- Iterative comparison with conditional branch logic
- Stack-based implementation with temporary local variables
sum()function: Partial implementation with iterable and start value support- Comprehensive test file:
examples/builtins.pydemonstrating all functions
-
Collections Support
- List literals with memory allocation:
[1, 2, 3] - List indexing (read & write):
list[i]andlist[i] = value - List methods:
.append(value),.pop([index]),.clear(),.insert(index, value) - List search methods:
.index(value)with linear search returning -1 if not found,.count(value) - Dictionary literals with memory allocation:
{"key": value} - Dictionary indexing (read & write):
dict[key]anddict[key] = value - Efficient memory allocation strategy: List ptr + 4 + (index * 4)
- Type tracking for
List[T]andDict[K, V]
- List literals with memory allocation:
Fixed
- ✓ List indexing implementation in compiler/expression.rs
- ✓ Dictionary indexing implementation in compiler/expression.rs
- ✓ List assignment support via new
IndexAssignIR statement - ✓ Dictionary assignment support via
IndexAssignIR statement - ✓ List method call dispatch in emit_list_method_call function
Changed
- Restructured IR types to support
IndexAssignstatement for subscript assignments - Enhanced converter.rs to handle subscript assignments in AST to IR conversion
- Updated modules.html documentation to reflect completed Collections module
0.6.3
Added
- Documentation updates for module development board
- Verbose AST and IR logging example entries
- Configurable logging options for verbose and debug modes
- IRModule logging for better development experience
- GitHub Actions CI workflows
- Documentation website with module development status board
Fixed
- Packaging files organization
- Format and linting issues
- rustfmt.toml configuration for stable Rust
- Binaryen upgrade compatibility
- Documentation links
Changed
- WebAssembly compilation pattern improvements
- Removed cross-platform matrix testing at this stage
- Updated WASM compilation with FFI for wasmrun
0.6.2
Added
- wasmrun plugin integration for WASM runtime execution
Fixed
- Cargo build after upgrading binaryen
0.6.1
Added
- Documentation homepage
- Module development status page with Kanban board visualization
- Interactive development board for tracking feature status
Fixed
- Documentation links and navigation
0.6.0
Added
-
Decorator Support
- @memoize decorator for function result caching
- @debug decorator for logging function calls
- @timer decorator for performance measurement
- Custom decorator registration mechanism
-
Raise Statement Parsing
- Exception raising syntax support (parsing only)
-
Verbose Logging
- AST log in verbose mode for debugging
Changed
- Project renamed from ChakraPy to Waspy (2025-06-06)
- Code refactoring for better maintainability
- Linting and formatting improvements
Fixed
- Linting errors across codebase
0.5.0
Added
-
Multi-File Project Support
- Multi-file compilation to single WASM module
- Dependency analysis with circular dependency detection
- Entry point detection (
__main__.pyandif __name__ == "__main__") - Configuration file parsing (setup.py, pyproject.toml, init.py)
-
Import System
- Import syntax parsing (all types:
import,from ... import, star imports) - Conditional imports in try/except blocks
- Dynamic imports using
__import__()andimportlib.import_module() - Dynamic import expression handling
- Import syntax parsing (all types:
Changed
- Module variable support with operator identification
- Project structure reorganization
0.4.0
Added
-
Complete Core Language Features
- Arithmetic operations:
+,-,*,/,%,//,** - Comparison operations:
==,!=,<,<=,>,>= - Boolean operations:
and,or,notwith short-circuit evaluation - Bitwise operations:
&,|,^,<<,>>,~
- Arithmetic operations:
-
Control Flow
if/elif/elsestatements with proper branchingwhileloops with exit conditions- Comparison and boolean logic operations
-
Functions
- Function definitions with parameters
- Type annotations for parameters and return types
- Function calls between compiled functions
- Multiple functions per module support
- Augmented assignment operations:
+=,-=,*=,/=,%=,//=,**=
-
Type System
- Basic types:
int,float,bool,str - Type annotations and inference
- Type coercion between compatible types
- Support for generic types:
List[T],Dict[K,V],Tuple[T,...] - Union and Optional types:
Union[T,U],Optional[T] - Custom class type annotations
- Basic types:
-
Variables & Assignment
- Variable declarations and assignments
- Attribute assignment:
obj.attr = value - Augmented assignment operations
- Type inference from usage patterns
0.3.0
Added
-
String Operations (Complete Implementation)
- String literals and constants
- String indexing with positive and negative indices
- String slicing:
str[start:end:step]with bounds checking - String concatenation with the
+operator - Compile-time concatenation optimization for constants
-
String Methods (20+ methods implemented)
- Case conversion:
.upper(),.lower(),.capitalize(),.title() - Whitespace handling:
.strip(),.lstrip(),.rstrip() - Test methods:
.isdigit(),.isalpha(),.isalnum(),.isspace(),.isupper(),.islower() - Search methods:
.find(),.index(),.count(),.startswith(),.endswith() - Transform methods:
.replace(),.split(),.join() - Layout methods:
.ljust(),.rjust(),.center()
- Case conversion:
-
String Formatting
.format()method with support for{},{0},{name}placeholders%string formatting with%s,%d,%f,%x,%o,%%specifiers- f-string support with constant and dynamic variable interpolation
- Compile-time optimization for constant strings
Changed
- Code modularization and refactoring
- Code organization improvements
0.2.0
Added
-
Project Management Infrastructure
- Multi-function support in single module
- Operator categorization and labeling
- Build system with category updates
-
Documentation & Examples
- Basic project documentation
- Example compilation workflows
Changed
- Category organization and labeling system
- Base compiler architecture improvements
0.1.0
Added
-
Initial Compiler Implementation
- Base WebAssembly code generation
- Python to WASM compilation pipeline
- AST parsing and IR conversion
- Function compilation from Python to WASM instructions
- Basic operator support and execution
-
Development Tools
- Error handling system with structured error types
- WebAssembly optimization using Binaryen
- Metadata extraction from compiled modules
- Compiler context management for local variables
- Memory layout management for string storage
Contributing
See CONTRIBUTING.md for guidelines on contributing to Waspy.
License
See LICENSE file for details.