Repository navigation
pycc-next v0.4.0
Fast untyped code, no leaks. --non-strict keeps untyped code native: strings index in
O(1), names widen per function, and public untyped --ext functions get guarded native
copies, so diff-match-patch's own speed test runs 11.5x faster than CPython (v0.3:
1.7x). A second real package, tomli (the parser behind tomllib), builds unchanged and
passes its own suite, 1.67x faster than CPython. Exceptions are reference counted and
freed. Strict mode compiles 171/200 real-world programs unchanged (v0.3: 161), and
--non-strict runs all but 8 of their 1191 top-level statements natively (v0.3: 23).
Circular imports, IntEnum as int, sorting class instances and dataclasses.fields
/ asdict work as in CPython.
Benchmarks (python3 bench/run.py, best of 7, shared 4-core box): fib 18.9x faster than
CPython / 2.2x of Rust, mandelbrot 35.2x / 0.98x, nbody 32.8x / 2.6x, sieve 21.3x /
2.5x, spectral_norm 47.5x / 1.8x, collections_bfs 6.5x / 2.7x, numpy kernels stencil2d
459x / 2.8x and prefix_sum_np 95x / 2.1x.
Added
- Untyped code runs native (v0.4 accept 1): an
--ext --non-strictmodule's unannotated
function or method that loops gets a guarded native specialization -- a copy whose
parameters are typedstrorintfrom how its body uses them, called when the
arguments are exactly those types (any other call runs on the CPython objects as
before; a copy the checker refuses is dropped, never fallen back). diff-match-patch's
own speed test now runs 12x faster than CPython (1.03 s vs 12.3 s) (v0.3: 1.7x), its suite passing
(python3 bench/package_check.py --time).--report-fallbackslistsspecialized:
andnot specialized:functions. - A second real package (v0.4 accept 4):
bench/package_check.pyis a table of pinned
packages and adds tomli 2.5.0 (MIT, the TOML parser
tomllibcomes from; GitHub tag archive pinned by its sha256 and that of the files used). Its
four modules build unchanged with--ext --non-strict --foreign-relative-imports, its
own test suite passes against them (CI) and 36 of 40 functions compile;loads()runs
1.6x faster than in CPython (python3 bench/package_check.py tomli --time --runs 15,
best of 15 runs of 100 calls on one machine: 0.036 s vs 0.059 s, 1.52x-1.75x over seven
runs; was 0.40x). What typed packages written for mypyc needed:from __future__ import annotations: an annotation pycc cannot lower is erased
instead of making its statement fall back, and fallback code is compiled with the
flag; names imported underif TYPE_CHECKING:from a sibling module that are
aliases (Pos = int,Key = tuple[str, ...]) are read through to their definition.- A function whose signature the
--extboundary cannot carry (tuple[int, str]
result,frozensetparameter) is compiled for the module's callers and runs as its
CPython twin when Python calls it (twin:in--report-fallbacks). - Non-strict signature wrappers: defaults of unannotated or object parameters, and a
Python caller passing a value of another type than a scalar parameter's runs the
function's CPython twin, so its own error handling answers (loads(b"")raises
tomli'sTypeError, not the boundary's). - A module function with an
Iterable[T]parameter gets afrozenset[T]
specialization that the module's own calls with a native frozenset run. - Native
frozenset(str),frozenset(chr(i) for i in range(n))(achrwhose
argument is provably not a surrogate is native in non-strict mode too), set
displays in membership tests,isdisjoint/issuperset/issubsetwith astr,
str.startswith(p, start),find/index(sub, start). - Non-strict:
isinstance(o, (A, B))of an object,repr(o),raise o from cause,
type(native), an object rebinding anint/float/boolparameter (converted),
an object passed for afrozenset[str]/frozenset[int]parameter (copied), a
tuple display returned astuple[...], an object in a native frozenset, atry
and its handler binding a name with two types,Callable[...],Iterable[...]
andtuple[X, ...]annotations as objects,self.x = {}as an object attribute.
- Faster code around CPython objects and tuples (v0.4 accept 4):
- Multi-value returns: a function whose every
returnis a tuple display of one
arity, or forwards another such function, gets a compiled twin that returns the
values without a tuple, and a caller that unpacks the call (pos, value = f(...))
calls it -- no CPython tuple (nor pycc tuple, for a nativetuple[int, str]) is
built, unpacked and freed. Result types are what the returns agree on, an object
where they differ; the function itself serves every other use, and a call whose
name may be rebound runs it (tests/multi_return.rs). a, b = x, ywith object values binds the targets without building two tuples
(tests/object_swap_unpack.rs).- Non-strict: a
str | Nonelocal bound only to nativestrvalues andNone
(char: str | None = src[pos]under postponed annotations) stays a nativestr,
null standing forNone; its literal comparisons,frozenset[str]membership and
is Nonetests are native and any other use sees the object it stands for
(tests/optional_str_local.rs). while s[i] in chars: i += 1(ornot in) over a set ofstris one scan of the
text, theIndexErrorof running off either end raised as the loop raises it
(tests/str_scan_loop.rs).- In a condition, an object compared with a
strliteral, or tested against a native
frozenset ofstr, is compared by its UTF-8 bytes when it is an exactstr, with
nostrmade for the literal and no CPython copy of the set; anything else takes
CPython's own operation (tests/object_text_compare.rs). - Non-strict object operations take shorter paths:
o[i]ando[a:b]with native
intkeys read an exact tuple or list directly, a tuple display of objects is
built as a tuple,o.m(args)with literal or parameter arguments is one
PyObject_VectorcallMethod, a rich comparison in anif/whileanswers a native
bool(two exactstroperands are compared without a result object), and
isinstanceof a plain built-in value skips the__class__lookup
(tests/object_fast_paths.rs). - A string literal reads its site's cached, immortal
strwith no call; freeing a
list or an instance no longer allocates; error paths are laid out of line; the
runtime builds as one codegen unit.
- Multi-value returns: a function whose every
- Crossing to CPython is cheaper: a native
stror frozenset converted to a CPython
object keeps that object and shares it on its next crossing (astra Python caller
passed in shares the caller's);strliterals are built once per site; attribute
names are interned once; the C shim is compiled with-O2; the pending-exception test
after each runtime call no longer reads thread-local storage while no thread has an
exception pending;c in frozenset_of_strfor one ASCII character is a bit test. - Circular imports between project modules run as in CPython (v0.4 accept 5): a module
imported back sees the partially initialized one -- the names it bound before the
import -- andfrom m import xof a namemhas not bound yet raises CPython's
ImportErrorwith its message. Only the entry module imported back is still refused
(E0108, now "import cycle through the entry module"). - Language gaps from v0.3 (v0.4 accept 5):
IntEnummembers are usable where
anintis needed (arithmetic, comparisons,range(), indexing,intparameters);
sorted(),min(),max()andlist.sort()of class instances order by__lt__
(or__gt__, or@dataclass(order=True)), also withkey=returning instances and
reverse=; a list changed while it is sorted raisesValueError: list modified during sortas in CPython;x in sfor a set of instances uses__hash__/__eq__;
dataclasses.fields(),asdict(),astuple()andreplace()compile natively
(asdict()of mixed field types needs--non-strict); printing a list or tuple of
instances uses their__repr__; a strict--extmodule publishes its native
module-level variables (numbers, strings, tuples of them) as attributes, kept current
when a function rebinds them withglobal. - Format specs and printf-style formatting compile natively:
f"{x:>10,.2f}",
format(x, spec)and"%5d|%-8.3f|%s" % (...)overint,bool,floatandstr
follow CPython's format-spec mini-language and%conversions exactly, errors
included (pycc_rt::format;tests/format_spec.rs). A conversion CPython refuses
for its argument's type ('%x' % 1.5) is a compile-timeT0021in strict mode; a
spec with a nested field ({x:{w}}) is stillC0001. - Strict mode admits a read of a name bound on some paths only: it is checked at run
time and raises CPython'sUnboundLocalError/NameErrorwhere CPython does
(tests/maybe_unbound_runtime.rs).T0041stays for a name CPython would read
something else for when unbound (a builtin,__name__), adel/except ... as
name, and a buffer. - Non-strict: a comprehension over a CPython object whose elements are native lists,
sets, dicts or tuples collects a native list;set(o)of an object and an empty
set()/dict()the empty-container pass cannot type are CPython's own; a name
rebound from one container kind to another (t = [..], thent = {..}on some
path) becomes an object;t |= {..}on such a name and aforover a maybe-bound
object run through CPython (tests/non_strict_object_containers.rs). d.get(k)with no default compiles (asd[k] if k in d else None, for a key that is a
name, a literal or a subscript of those): anOptionalof the value type. An
Optional[int|float|bool]compares with== None/!= None(narrowing like
is None) and renders withprint,str()and f-strings
(tests/dict_get_optional.rs).- An unannotated
self.d = {}is typed from the firstself.d[k] = vin the class's
own methods (it wasC0001; with no such store it is nowT0003), and an empty
attribute container whose producer reads an unannotated method parameter is typed
once the solver has typed the parameter (tests/attribute_dict_producer.rs). - An empty
set()/dict()/list()or attribute container whose producer reads an
unannotated function's result (s.add(dsu.find(i))) is typed once the solver has
typed the function;s.update(it)/xs.extend(it)type an empty container from any
iterable;sorted((a, b))(andmin,max,sum) of a tuple display compiles
(tests/solver_typed_containers.rs). - Non-strict: a name holding a native container that meets a CPython object (a call
argument, an item store, a comparison) becomes an object when every binding of it is a
fresh container -- a display, a comprehension, an empty constructor, or the result of a
function that only returns lists nothing else holds -- so aliasing is kept; a list
display nested in an object list, a list holding a bigintliteral, a
collections.defaultdictthe native dict cannot hold (built by CPython from its
factory) andsys.stdin.bufferread as bytes are CPython's own; a chained comparison
over objects whose middle operands are names or literals is the conjunction of its
links (tests/non_strict_widened_containers.rs). The corpus meets v0.4 accept 3:
strict 170/200, non-strict 200/200 with 8/1191 statements in CPython fallback. - A module-level
passstatement compiles; a barezip()statement is dropped. - The empty-container pass reads
d.get(k, 0)when the call is also readable as a
user or CPython method (non-strict), sod = {}filled that way stays native.
Changed
- Native
strindexing, slicing andlen()are O(1) in the string's length: a string
caches its code-point count and, when not ASCII, the byte offset of every 32nd code
point; one-character ASCII strings are shared.inonstruses a substring search. - Non-strict widening is per function: a name one function rebinds with another type
becomes a CPython object in that function only, not in every function of the module. - Non-strict: an object tested for membership in a native
str(c in "aeiou") is
CPython's own test (was a fallback).
Fixed
- An empty
frozenset()was alwaysfrozenset[int], sofrozenset("") == frozenset()
answeredFalse: it is now typed by its uses, likeset()(an equality, a set
operator or a membership test with a typed set, orfrozenset(x)'s argument)
(tests/empty_frozenset.rs). - Implicit concatenation of plain string literals and f-strings
("allowed" f" {n} levels") dropped the plain parts. - A method call on an imported module (
m.set(...),builtins.set()) was bound to the
signature of a method of the same name a class of the module defines, filling in its
defaults. - A call of a module-level function before its
defstatement ran (from a function
called earlier) raises CPython's catchableNameError: name 'f' is not defined, before
any argument is evaluated, in native strict,--non-strictand--extbuilds and
across a circular import; it aborted the process (exit 134). - The peak-RSS leak tests no longer fail at random when the suite runs in parallel. On
Linux a child'sru_maxrssalso counts the resident set of the test harness that
spawned it (execfolds the replaced address space's high-water mark into it), so
two probes compared how much the harness grew in between. Every probe now reads its
own peak (VmHWM, at its exit) through one helper,tests/memory_support. The growth
limit is 2 MB (was 1 MB): a probe embedding CPython grows by up to one 1 MiB obmalloc
arena or not depending on the loop count; one leaked object per iteration still adds
3-6 MB. - A
NameErrorfor a global of an imported project module read before it is bound
names it as written (name 'LATE' is not defined), not by the compiler's qualified
spelling. - Exception objects are freed (v0.4 accept 2): a raised or caught exception, its
message,__cause__, group members and aStopIteration's value are reference counted
and released with the last reference -- the end of anexcepthandler (itsas
binding is unbound then, as in CPython), areturn/break/continueout of it, a
finallyclause that replaces it, or whatever list, attribute or local kept it. A
loop catchingZeroDivisionErrorno longer grows by ~220 bytes per iteration, nor one
catching a failingjson.loadsby ~1.9 kB, in strict,--non-strictand--ext
builds (tests/memory_leaks_exceptions.rs,tests/memory_leaks_ext.rs). A CPython
exception bridged into compiled code now carries its original object, which is
released with it, instead of a per-call table of originals.
Known gaps
- See
docs/DEVIATIONS.mdfor every known difference from CPython 3.13.